From 93229b7d4927e437de6b452c0942c0005a06432b Mon Sep 17 00:00:00 2001 From: Steve Pieper Date: Mon, 28 Jun 2021 20:33:11 -0400 Subject: [PATCH 1/3] ENH: Display thumbnail in DICOM metadata To allow a way to reduce "time to first image" in the DICOM browser and help testing troubleshooting, an image thumbnail is displayed in the DICOM metadata browser window. The thumbnail is only retrieved if the collapsible section is open (to save space and improve performance when accessing large images). Based on ProjectWeek discussion: https://projectweek.na-mic.org/PW35_2021_Virtual/Projects/mpReview/ Co-authored-by: Andras Lasso --- .../Resources/UI/ctkDICOMObjectListWidget.ui | 46 ++++++++++++---- .../Widgets/ctkDICOMObjectListWidget.cpp | 54 +++++++++++++++++-- Libs/DICOM/Widgets/ctkDICOMObjectListWidget.h | 4 ++ .../Widgets/ctkDICOMThumbnailGenerator.cpp | 36 +++++++++++-- .../Widgets/ctkDICOMThumbnailGenerator.h | 6 +++ 5 files changed, 129 insertions(+), 17 deletions(-) diff --git a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMObjectListWidget.ui b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMObjectListWidget.ui index 29020c49a1..07f4235181 100644 --- a/Libs/DICOM/Widgets/Resources/UI/ctkDICOMObjectListWidget.ui +++ b/Libs/DICOM/Widgets/Resources/UI/ctkDICOMObjectListWidget.ui @@ -7,7 +7,7 @@ 0 0 442 - 311 + 514 @@ -51,8 +51,8 @@ - - + + 0 @@ -106,20 +106,46 @@ - - - - - - Double-click to show DICOM tag definition. + + + 0 - + + + + Double-click to show DICOM tag definition. + + + + + + + Thumbnail + + + + + + + Show image thumbnail + + + true + + + + + + ctkExpandButton + QToolButton +
ctkExpandButton.h
+
ctkSearchBox QLineEdit diff --git a/Libs/DICOM/Widgets/ctkDICOMObjectListWidget.cpp b/Libs/DICOM/Widgets/ctkDICOMObjectListWidget.cpp index 99304ea20c..1648031e4a 100644 --- a/Libs/DICOM/Widgets/ctkDICOMObjectListWidget.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMObjectListWidget.cpp @@ -20,12 +20,14 @@ // ctkDICOMWidgets includes #include "ctkDICOMObjectListWidget.h" +#include "ctkDICOMThumbnailGenerator.h" #include "ui_ctkDICOMObjectListWidget.h" // Qt includes #include #include #include +#include #include #include #include @@ -101,6 +103,7 @@ class ctkDICOMObjectListWidgetPrivate: public Ui_ctkDICOMObjectListWidget ctkDICOMObjectModel* dicomObjectModel; qRecursiveTreeProxyFilter* filterModel; QString filterExpression; + bool thumbnailVisible{true}; }; //---------------------------------------------------------------------------- @@ -212,6 +215,9 @@ ctkDICOMObjectListWidget::ctkDICOMObjectListWidget(QWidget* _parent):Superclass( d->fileSliderWidget->setMinimum(1); d->fileSliderWidget->setPageStep(1); + d->showThumbnailButton->setChecked(d->thumbnailVisible); + d->thumbnailLabel->setVisible(d->thumbnailVisible); + d->currentPathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); connect(d->fileSliderWidget, SIGNAL(valueChanged(double)), this, SLOT(updateWidget())); connect(d->dcmObjectTreeView, SIGNAL(doubleClicked(const QModelIndex&)), @@ -225,6 +231,8 @@ ctkDICOMObjectListWidget::ctkDICOMObjectListWidget(QWidget* _parent):Superclass( QObject::connect(d->metadataSearchBox, SIGNAL(textChanged(QString)), this, SLOT(setFilterExpression(QString))); QObject::connect(d->metadataSearchBox, SIGNAL(textChanged(QString)), this, SLOT(onFilterChanged())); + + QObject::connect(d->showThumbnailButton, SIGNAL(toggled(bool)), this, SLOT(setThumbnailVisible(bool))); } //---------------------------------------------------------------------------- @@ -250,10 +258,9 @@ void ctkDICOMObjectListWidget::setFileList(const QStringList& fileList) if (d->fileList.size() > 0) { d->currentFile = d->fileList[0]; - - d->populateDICOMObjectTreeView(d->currentFile); d->fileSliderWidget->setMaximum(fileList.size()); d->fileSliderWidget->setSuffix(QString(" / %1").arg(fileList.size())); + this->updateWidget(); for (int columnIndex = 0; columnIndex < d->dicomObjectModel->columnCount(); ++columnIndex) { d->dcmObjectTreeView->resizeColumnToContents(columnIndex); @@ -307,7 +314,19 @@ void ctkDICOMObjectListWidget::updateWidget() d->currentFile = d->fileList[static_cast(d->fileSliderWidget->value())-1]; d->setPathLabel(d->currentFile); d->populateDICOMObjectTreeView(d->currentFile); - } + + if (this->isThumbnailVisible()) + { + // only update the thumbnail if visible for better update performance + ctkDICOMThumbnailGenerator thumbnailGenerator; + QImage thumbnailImage; + if (!thumbnailGenerator.generateThumbnail(d->currentFile, thumbnailImage)) + { + thumbnailGenerator.generateBlankThumbnail(thumbnailImage); + } + d->thumbnailLabel->setPixmap(QPixmap::fromImage(thumbnailImage)); + } +} // -------------------------------------------------------------------------- void ctkDICOMObjectListWidget::copyPath() @@ -406,3 +425,32 @@ QString ctkDICOMObjectListWidget::filterExpression() Q_D(ctkDICOMObjectListWidget); return d->filterExpression; } + +//------------------------------------------------------------------------------ +void ctkDICOMObjectListWidget::setThumbnailVisible(bool visible) +{ + Q_D(ctkDICOMObjectListWidget); + if (visible == d->thumbnailVisible) + { + // no change + return; + } + d->thumbnailVisible = visible; + + QSignalBlocker blocker(d->showThumbnailButton); + d->showThumbnailButton->setChecked(visible); + + d->thumbnailLabel->setVisible(visible); + if (visible) + { + // Previously the thumbnail was not visible, so it was not updated. Update it now. + this->updateWidget(); + } +} + +//------------------------------------------------------------------------------ +bool ctkDICOMObjectListWidget::isThumbnailVisible()const +{ + Q_D(const ctkDICOMObjectListWidget); + return d->thumbnailVisible; +} diff --git a/Libs/DICOM/Widgets/ctkDICOMObjectListWidget.h b/Libs/DICOM/Widgets/ctkDICOMObjectListWidget.h index 5e96592d19..b7bd501451 100644 --- a/Libs/DICOM/Widgets/ctkDICOMObjectListWidget.h +++ b/Libs/DICOM/Widgets/ctkDICOMObjectListWidget.h @@ -36,6 +36,7 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMObjectListWidget : public QWidget Q_PROPERTY(QString currentFile READ currentFile WRITE setCurrentFile) Q_PROPERTY(QStringList fileList READ fileList WRITE setFileList) Q_PROPERTY(QString filterExpression READ filterExpression WRITE setFilterExpression) + Q_PROPERTY(bool thumbnailVisible READ isThumbnailVisible WRITE setThumbnailVisible) public: typedef QWidget Superclass; @@ -58,6 +59,8 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMObjectListWidget : public QWidget /// Open DICOM tag definition in a web browser void openLookupUrl(QString tag); + bool isThumbnailVisible()const; + protected: QScopedPointer d_ptr; @@ -72,6 +75,7 @@ public Q_SLOTS: void setCurrentFile(const QString& newFileName); void setFileList(const QStringList& fileList); void setFilterExpression(const QString& expr); + void setThumbnailVisible(bool visible); protected Q_SLOTS: void itemDoubleClicked(const QModelIndex&); diff --git a/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.cpp b/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.cpp index 2c0a07cbe8..3439f3d1b0 100644 --- a/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.cpp @@ -123,11 +123,10 @@ void ctkDICOMThumbnailGenerator::setSmoothResize(bool on) } //------------------------------------------------------------------------------ -bool ctkDICOMThumbnailGenerator::generateThumbnail(DicomImage *dcmImage, const QString &path) +bool ctkDICOMThumbnailGenerator::generateThumbnail(DicomImage *dcmImage, QImage& image) { Q_D(ctkDICOMThumbnailGenerator); - QImage image; // Check whether we have a valid image EI_Status result = dcmImage->getStatus(); if (result != EIS_Normal) @@ -184,14 +183,43 @@ bool ctkDICOMThumbnailGenerator::generateThumbnail(DicomImage *dcmImage, const Q return false; } } - image.scaled( d->Width, d->Height, Qt::KeepAspectRatio, - (d->SmoothResize ? Qt::SmoothTransformation : Qt::FastTransformation) ).save(path,"PNG"); + image = image.scaled( d->Width, d->Height, Qt::KeepAspectRatio, + (d->SmoothResize ? Qt::SmoothTransformation : Qt::FastTransformation) ); return true; } +//------------------------------------------------------------------------------ +bool ctkDICOMThumbnailGenerator::generateThumbnail(DicomImage *dcmImage, const QString &path) +{ + QImage image; + if (this->generateThumbnail(dcmImage, image)) + { + return image.save(path,"PNG"); + } + return false; +} + +//------------------------------------------------------------------------------ +bool ctkDICOMThumbnailGenerator::generateThumbnail(const QString dcmImagePath, QImage& image) +{ + DicomImage dcmImage(QDir::toNativeSeparators(dcmImagePath).toUtf8()); + return this->generateThumbnail(&dcmImage, image); +} + //------------------------------------------------------------------------------ bool ctkDICOMThumbnailGenerator::generateThumbnail(const QString dcmImagePath, const QString& thumbnailPath) { DicomImage dcmImage(QDir::toNativeSeparators(dcmImagePath).toUtf8()); return this->generateThumbnail(&dcmImage, thumbnailPath); } + +//------------------------------------------------------------------------------ +void ctkDICOMThumbnailGenerator::generateBlankThumbnail(QImage& image) +{ + Q_D(ctkDICOMThumbnailGenerator); + if (image.width() != d->Width || image.height() != d->Height) + { + image = QImage(d->Width, d->Height, QImage::Format_RGB32); + } + image.fill(Qt::darkGray); +} diff --git a/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.h b/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.h index e556e14250..61c1aa3aaf 100644 --- a/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.h +++ b/Libs/DICOM/Widgets/ctkDICOMThumbnailGenerator.h @@ -47,8 +47,14 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMThumbnailGenerator : public ctkDICOMAbstr virtual bool generateThumbnail(DicomImage* dcmImage, const QString& path); + Q_INVOKABLE bool generateThumbnail(DicomImage *dcmImage, QImage& image); + Q_INVOKABLE bool generateThumbnail(const QString dcmImagePath, QImage& image); Q_INVOKABLE bool generateThumbnail(const QString dcmImagePath, const QString& thumbnailPath); + /// Generate a blank thumbnail image (currently a solid gray box of the requested thumbnail size). + /// It can be used as a placeholder for invalid images or duringan image is loaded. + Q_INVOKABLE void generateBlankThumbnail(QImage& image); + /// Set thumbnail width void setWidth(int width); /// Get thumbnail width From b4a9ee3c0d940c2eaabfb0d8eade03182dfd7770 Mon Sep 17 00:00:00 2001 From: Andras Lasso Date: Fri, 20 Jan 2023 01:17:39 -0500 Subject: [PATCH 2/3] BUG: Fix initial state of ctkExpandButton The initial direction of the double-arrow button was incorrect (left-to-right instead of right-to-left), as the icon was only updated when the user clicked on it, and not when the checked state was changed by calling `setChecked()`. --- Libs/Widgets/ctkExpandButton.cpp | 28 ++++++---------------------- Libs/Widgets/ctkExpandButton.h | 5 +---- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/Libs/Widgets/ctkExpandButton.cpp b/Libs/Widgets/ctkExpandButton.cpp index 7eea69c9cd..7d46c65df4 100644 --- a/Libs/Widgets/ctkExpandButton.cpp +++ b/Libs/Widgets/ctkExpandButton.cpp @@ -36,7 +36,6 @@ class ctkExpandButtonPrivate bool mirrorOnExpand; QPixmap defaultPixmap; Qt::Orientation orientation; - Qt::LayoutDirection direction; }; //----------------------------------------------------------------------------- @@ -45,7 +44,6 @@ ctkExpandButtonPrivate::ctkExpandButtonPrivate(ctkExpandButton &object) { this->mirrorOnExpand = false; this->orientation = Qt::Horizontal; - this->direction = Qt::LeftToRight; } //----------------------------------------------------------------------------- @@ -56,6 +54,8 @@ void ctkExpandButtonPrivate::init() q->setOrientation(Qt::Horizontal); q->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Minimum); q->setCheckable(true); + + QObject::connect(q, SIGNAL(toggled(bool)), q, SLOT(updateIcon())); } //----------------------------------------------------------------------------- @@ -80,7 +80,7 @@ void ctkExpandButton::setMirrorOnExpand(bool newBehavior) { Q_D(ctkExpandButton); d->mirrorOnExpand = newBehavior; - this->updateIcon(d->direction); + this->updateIcon(); } //----------------------------------------------------------------------------- @@ -115,7 +115,7 @@ void ctkExpandButton::setOrientation(Qt::Orientation newOrientation) QStyle::SP_ToolBarVerticalExtensionButton, &opt); d->orientation = Qt::Vertical; } - this->updateIcon(d->direction); + this->updateIcon(); } //----------------------------------------------------------------------------- @@ -126,15 +126,14 @@ Qt::Orientation ctkExpandButton::orientation() const } //----------------------------------------------------------------------------- -void ctkExpandButton::updateIcon(Qt::LayoutDirection newDirection) +void ctkExpandButton::updateIcon() { Q_D(ctkExpandButton); // If the orientation is vertical, UpToBottom is LeftToRight and // BottomToUp is RightToLeft. Rotate 90' clockwise. - if(newDirection == Qt::LeftToRight) + if(!d->mirrorOnExpand || !this->isChecked()) { this->setIcon(QIcon(d->defaultPixmap)); - d->direction = Qt::LeftToRight; } else { @@ -142,20 +141,5 @@ void ctkExpandButton::updateIcon(Qt::LayoutDirection newDirection) d->defaultPixmap.toImage().mirrored(d->orientation == Qt::Horizontal, d->orientation == Qt::Vertical); this->setIcon(QIcon(QPixmap::fromImage(mirrorImage))); - d->direction = Qt::RightToLeft; } } - -//----------------------------------------------------------------------------- -void ctkExpandButton::nextCheckState() -{ - Q_D(ctkExpandButton); - if (d->mirrorOnExpand) - { - Qt::LayoutDirection newDirection = - this->isChecked() ? Qt::LeftToRight : Qt::RightToLeft; - this->updateIcon(newDirection); - } - - return this->Superclass::nextCheckState(); -} diff --git a/Libs/Widgets/ctkExpandButton.h b/Libs/Widgets/ctkExpandButton.h index c7c05d89ae..51e2a10982 100644 --- a/Libs/Widgets/ctkExpandButton.h +++ b/Libs/Widgets/ctkExpandButton.h @@ -62,10 +62,7 @@ class CTK_WIDGETS_EXPORT ctkExpandButton virtual QSize sizeHint() const; private Q_SLOTS: - void updateIcon(Qt::LayoutDirection newDirection); - -protected: - virtual void nextCheckState(); + void updateIcon(); protected: QScopedPointer d_ptr; From 124d89cc307f5bc23a0551c0926316557fddb2a3 Mon Sep 17 00:00:00 2001 From: Andras Lasso Date: Fri, 20 Jan 2023 01:21:06 -0500 Subject: [PATCH 3/3] BUG: Fixed restoring of DICOM metadata viewer window size After the DICOM metadata viewer window was show maximized and then closed by hitting Escape button: the metadata viewer opened the widget maximized, but the layout inside was much smaller. Applied parts of [this workaround](https://stackoverflow.com/questions/44005852/qdockwidgetrestoregeometry-not-working-correctly-when-qmainwindow-is-maximized) to ensure that the widget size is restored when the window is maximized. --- Libs/DICOM/Widgets/ctkDICOMBrowser.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Libs/DICOM/Widgets/ctkDICOMBrowser.cpp b/Libs/DICOM/Widgets/ctkDICOMBrowser.cpp index 8d958eef09..65b7c1e951 100644 --- a/Libs/DICOM/Widgets/ctkDICOMBrowser.cpp +++ b/Libs/DICOM/Widgets/ctkDICOMBrowser.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -105,6 +106,10 @@ class ctkDICOMMetadataDialog : public QDialog if (!savedGeometry.isEmpty()) { this->restoreGeometry(savedGeometry); + if (this->isMaximized()) + { + this->setGeometry(QApplication::desktop()->availableGeometry(this)); + } } }