Przeglądaj źródła

Merge pull request #728 from lassoan/dicom_metadata_search_filter

ENH: Added filtering and multi-file export to DICOM metadata dialog
Steve Pieper 7 lat temu
rodzic
commit
8afaca8b0d

Plik diff jest za duży
+ 59 - 13
Libs/DICOM/Widgets/Resources/UI/ctkDICOMObjectListWidget.ui


+ 223 - 41
Libs/DICOM/Widgets/ctkDICOMObjectListWidget.cpp

@@ -26,6 +26,7 @@
 #include <QApplication>
 #include <QClipboard>
 #include <QDesktopServices>
+#include <QSortFilterProxyModel>
 #include <QString>
 #include <QStringList>
 #include <QUrl>
@@ -35,6 +36,54 @@
 #include <ctkLogger.h>
 static ctkLogger logger("org.commontk.DICOM.Widgets.ctkDICOMObjectListWidget");
 
+class qRecursiveTreeProxyFilter : public QSortFilterProxyModel
+{
+public:
+  qRecursiveTreeProxyFilter(QObject *parent = NULL):
+    QSortFilterProxyModel(parent)
+  {
+  }
+
+  bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+  {
+    if (filterRegExp().isEmpty())
+      {
+      return true;
+      }
+    QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
+    return filterAcceptsIndex(index);
+  }
+
+private:
+  bool filterAcceptsIndex(const QModelIndex index) const
+  {
+    // Accept item if its tag, attribute, or value text matches
+    if ((sourceModel()->data(sourceModel()->index(index.row(), ctkDICOMObjectModel::TagColumn,
+      index.parent()), Qt::DisplayRole).toString().contains(filterRegExp()))
+      || (sourceModel()->data(sourceModel()->index(index.row(), ctkDICOMObjectModel::AttributeColumn,
+      index.parent()), Qt::DisplayRole).toString().contains(filterRegExp()))
+      || (sourceModel()->data(sourceModel()->index(index.row(), ctkDICOMObjectModel::ValueColumn,
+      index.parent()), Qt::DisplayRole).toString().contains(filterRegExp())))
+      {
+      return true;
+      }
+    // Accept item if any child matches
+    for (int row = 0; row < sourceModel()->rowCount(index); row++)
+      {
+      QModelIndex childIndex = sourceModel()->index(row, 0, index);
+      if (!childIndex.isValid())
+        {
+        break;
+        }
+      if (filterAcceptsIndex(childIndex))
+        {
+        return true;
+        }
+      }
+    return false;
+  }
+};
+
 //----------------------------------------------------------------------------
 class ctkDICOMObjectListWidgetPrivate: public Ui_ctkDICOMObjectListWidget
 {
@@ -43,11 +92,15 @@ public:
   ~ctkDICOMObjectListWidgetPrivate();
   void populateDICOMObjectTreeView(const QString& fileName);
   void setPathLabel(const QString& currentFile);
-  QString dicomObjectModelAsString(QModelIndex parent = QModelIndex(), int indent = 0);
+  QString dicomObjectModelAsString(QAbstractItemModel* dicomObjectModel, QModelIndex parent = QModelIndex(), int indent = 0, QString rowPrefix = QString());
+  void setFilterExpressionInModel(qRecursiveTreeProxyFilter* filterModel, const QString& expr);
 
+  QString endOfLine;
   QString currentFile;
   QStringList fileList;
   ctkDICOMObjectModel* dicomObjectModel;
+  qRecursiveTreeProxyFilter* filterModel;
+  QString filterExpression;
 };
 
 //----------------------------------------------------------------------------
@@ -56,6 +109,13 @@ public:
 //----------------------------------------------------------------------------
 ctkDICOMObjectListWidgetPrivate::ctkDICOMObjectListWidgetPrivate()
 {
+#ifdef WIN32
+  this->endOfLine = "\r\n";
+#else
+  this->endOfLine = "\n";
+#endif
+  this->dicomObjectModel = 0;
+  this->filterModel = 0;
 }
 
 //----------------------------------------------------------------------------
@@ -64,10 +124,26 @@ ctkDICOMObjectListWidgetPrivate::~ctkDICOMObjectListWidgetPrivate()
 }
 
 //----------------------------------------------------------------------------
+void ctkDICOMObjectListWidgetPrivate::setFilterExpressionInModel(qRecursiveTreeProxyFilter* filterModel, const QString& expr)
+{
+  const QString regexpPrefix("regexp:");
+  if (expr.startsWith(regexpPrefix))
+    {
+    filterModel->setFilterRegExp(expr.right(expr.length() - regexpPrefix.length()));
+    }
+  else
+    {
+    filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+    filterModel->setFilterWildcard(expr);
+    }
+}
+
+//----------------------------------------------------------------------------
 void ctkDICOMObjectListWidgetPrivate::populateDICOMObjectTreeView(const QString& fileName)
 {
   this->dicomObjectModel->setFile(fileName);
-  this->dcmObjectTreeView->setModel(this->dicomObjectModel);
+  this->filterModel->invalidate();
+  this->dcmObjectTreeView->setModel(this->filterModel);
   this->dcmObjectTreeView->expandAll();
 }
 
@@ -78,22 +154,16 @@ void ctkDICOMObjectListWidgetPrivate::setPathLabel(const QString& currentFile)
 }
 
 // --------------------------------------------------------------------------
-QString ctkDICOMObjectListWidgetPrivate::dicomObjectModelAsString(QModelIndex parent /*=QModelIndex()*/, int indent /*=0*/)
+QString ctkDICOMObjectListWidgetPrivate::dicomObjectModelAsString(QAbstractItemModel* aDicomObjectModel, QModelIndex parent /*=QModelIndex()*/, int indent /*=0*/, QString rowPrefix /*=QString()*/)
 {
   QString dump;
   QString indentString(indent, '\t'); // add tab characters, (indent) number of times
-#ifdef WIN32
-  QString newLine = "\r\n";
-#else
-  QString newLine = "\n";
-#endif
-  for (int r = 0; r < this->dicomObjectModel->rowCount(parent); ++r)
+  for (int r = 0; r < aDicomObjectModel->rowCount(parent); ++r)
     {
-    dump += indentString;
-    for (int c = 0; c < this->dicomObjectModel->columnCount(); ++c)
+    for (int c = 0; c < aDicomObjectModel->columnCount(); ++c)
       {
-      QModelIndex index = this->dicomObjectModel->index(r, c, parent);
-      QString name = this->dicomObjectModel->data(index).toString();
+      QModelIndex index = aDicomObjectModel->index(r, c, parent);
+      QString name = aDicomObjectModel->data(index).toString();
       if (c == 0)
         {
         // Replace round brackets by square brackets.
@@ -101,20 +171,19 @@ QString ctkDICOMObjectListWidgetPrivate::dicomObjectModelAsString(QModelIndex pa
         // as a negative number (-80,012). Instead, [0008,0012] is displayed fine.
         name.replace('(', '[');
         name.replace(')', ']');
-        dump += name;
+        dump += rowPrefix + indentString + name;
         }
       else
         {
         dump += "\t" + name;
         }
-      
       }
-    dump += newLine;
-    // here is your applicable code
-    QModelIndex index0 = this->dicomObjectModel->index(r, 0, parent);
-    if (this->dicomObjectModel->hasChildren(index0))
+    dump += endOfLine;
+    // Print children
+    QModelIndex index0 = aDicomObjectModel->index(r, 0, parent);
+    if (aDicomObjectModel->hasChildren(index0))
       {
-      dump += dicomObjectModelAsString(index0, indent+1);
+      dump += dicomObjectModelAsString(aDicomObjectModel, index0, indent + 1, rowPrefix);
       }
     }
   return dump;
@@ -124,22 +193,37 @@ QString ctkDICOMObjectListWidgetPrivate::dicomObjectModelAsString(QModelIndex pa
 // ctkDICOMObjectListWidget methods
 
 //----------------------------------------------------------------------------
-ctkDICOMObjectListWidget::ctkDICOMObjectListWidget(QWidget* _parent):Superclass(_parent), 
+ctkDICOMObjectListWidget::ctkDICOMObjectListWidget(QWidget* _parent):Superclass(_parent),
   d_ptr(new ctkDICOMObjectListWidgetPrivate)
 {
   Q_D(ctkDICOMObjectListWidget);
 
   d->setupUi(this);
+
+  d->metadataSearchBox->setAlwaysShowClearIcon(true);
+  d->metadataSearchBox->setShowSearchIcon(true);
+
   d->dicomObjectModel = new ctkDICOMObjectModel(this);
+  d->filterModel = new qRecursiveTreeProxyFilter(this);
+  d->filterModel->setSourceModel(d->dicomObjectModel);
 
+  d->fileSliderWidget->setMaximum(1);
+  d->fileSliderWidget->setMinimum(1);
   d->fileSliderWidget->setPageStep(1);
 
   d->currentPathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
   connect(d->fileSliderWidget, SIGNAL(valueChanged(double)), this, SLOT(updateWidget()));
-  connect(d->dcmObjectTreeView, SIGNAL(doubleClicked(const QModelIndex&))
-                               ,this, SLOT(openLookupUrl(const QModelIndex&)));
+  connect(d->dcmObjectTreeView, SIGNAL(doubleClicked(const QModelIndex&)),
+    this, SLOT(itemDoubleClicked(const QModelIndex&)));
   connect(d->copyPathPushButton , SIGNAL(clicked(bool)),this, SLOT(copyPath()));
+
+  connect(d->expandAllPushButton, SIGNAL(clicked(bool)), d->dcmObjectTreeView, SLOT(expandAll()));
+  connect(d->collapseAllPushButton, SIGNAL(clicked(bool)), d->dcmObjectTreeView, SLOT(collapseAll()));
   connect(d->copyMetadataPushButton, SIGNAL(clicked(bool)), this, SLOT(copyMetadata()));
+  connect(d->copyAllFilesMetadataPushButton, SIGNAL(clicked(bool)), this, SLOT(copyAllFilesMetadata()));
+
+  QObject::connect(d->metadataSearchBox, SIGNAL(textChanged(QString)), this, SLOT(setFilterExpression(QString)));
+  QObject::connect(d->metadataSearchBox, SIGNAL(textChanged(QString)), this, SLOT(onFilterChanged()));
 }
 
 //----------------------------------------------------------------------------
@@ -147,6 +231,7 @@ ctkDICOMObjectListWidget::~ctkDICOMObjectListWidget()
 {
   Q_D(ctkDICOMObjectListWidget);
   d->dicomObjectModel->deleteLater();
+  d->filterModel->deleteLater();
 }
 
 //----------------------------------------------------------------------------
@@ -164,11 +249,25 @@ void ctkDICOMObjectListWidget::setFileList(const QStringList& fileList)
   if (d->fileList.size() > 0)
     {
     d->currentFile = d->fileList[0];
-    d->setPathLabel(d->currentFile );
-    d->populateDICOMObjectTreeView(d->currentFile );
-    d->fileSliderWidget->setMaximum(fileList.size()-1);
+    
+    d->populateDICOMObjectTreeView(d->currentFile);
+    d->fileSliderWidget->setMaximum(fileList.size());
+    d->fileSliderWidget->setSuffix(QString(" / %1").arg(fileList.size()));
+    for (int columnIndex = 0; columnIndex < d->dicomObjectModel->columnCount(); ++columnIndex)
+      {
+      d->dcmObjectTreeView->resizeColumnToContents(columnIndex);
+      }
+    }
+  else
+    {
+    d->currentFile.clear();
+    d->dicomObjectModel->clear();
     }
+
+  d->setPathLabel(d->currentFile);
+  d->fileSliderWidget->setVisible(d->fileList.size() > 1);
 }
+
 // --------------------------------------------------------------------------
 QString ctkDICOMObjectListWidget::currentFile()
 {
@@ -182,29 +281,34 @@ QStringList ctkDICOMObjectListWidget::fileList()
   Q_D(ctkDICOMObjectListWidget);
   return d->fileList;
 }
-// --------------------------------------------------------------------------
 
-void ctkDICOMObjectListWidget::openLookupUrl(const QModelIndex& index)
+// --------------------------------------------------------------------------
+void ctkDICOMObjectListWidget::openLookupUrl(QString tag)
 {
-  if (index.column() == 0)
-  {
-    QVariant  data = index.data();
-    QString lookupUrl = "http://dicomlookup.com/lookup.asp?sw=Tnumber&q="+data.toString();
-    QUrl url(lookupUrl);
-    QDesktopServices::openUrl(url);
-  }
+  QString lookupUrl = "http://dicomlookup.com/lookup.asp?sw=Tnumber&q=" + tag;
+  QUrl url(lookupUrl);
+  QDesktopServices::openUrl(url);
 }
+
 // --------------------------------------------------------------------------
+void ctkDICOMObjectListWidget::itemDoubleClicked(const QModelIndex& index)
+{
+  Q_D(ctkDICOMObjectListWidget);
+  QModelIndex tagIndex = d->filterModel->index(index.row(), 0, index.parent());
+  QString tag = d->filterModel->data(tagIndex).toString();
+  openLookupUrl(tag);
+}
 
+// --------------------------------------------------------------------------
 void ctkDICOMObjectListWidget::updateWidget()
 {
   Q_D(ctkDICOMObjectListWidget);
-  d->currentFile = d->fileList[static_cast<int>(d->fileSliderWidget->value())];
+  d->currentFile = d->fileList[static_cast<int>(d->fileSliderWidget->value())-1];
   d->setPathLabel(d->currentFile);
   d->populateDICOMObjectTreeView(d->currentFile);
  }
-// --------------------------------------------------------------------------
 
+// --------------------------------------------------------------------------
 void ctkDICOMObjectListWidget::copyPath()
 {
   Q_D(ctkDICOMObjectListWidget);
@@ -213,18 +317,96 @@ void ctkDICOMObjectListWidget::copyPath()
 }
 
 // --------------------------------------------------------------------------
-
-QString ctkDICOMObjectListWidget::metadataAsText()
+QString ctkDICOMObjectListWidget::metadataAsText(bool allFiles /*=false*/)
 {
   Q_D(ctkDICOMObjectListWidget);
-  return d->dicomObjectModelAsString();
+  QString metadata;
+  if (allFiles)
+    {
+    foreach(QString fileName, d->fileList)
+      {
+      // copy metadata of all files
+
+      ctkDICOMObjectModel* aDicomObjectModel = new ctkDICOMObjectModel();
+      aDicomObjectModel->setFile(fileName);
+
+      qRecursiveTreeProxyFilter* afilterModel = new qRecursiveTreeProxyFilter();
+      afilterModel->setSourceModel(aDicomObjectModel);
+      d->setFilterExpressionInModel(afilterModel, d->filterExpression);
+
+      QString thisFileMetadata = d->dicomObjectModelAsString(afilterModel, QModelIndex(), 0, fileName + "\t");
+
+      if (!thisFileMetadata.isEmpty())
+        {
+        metadata += thisFileMetadata;
+        }
+      else
+        {
+        metadata += fileName + "\t(none)" + d->endOfLine;
+        }
+
+      delete afilterModel;
+      delete aDicomObjectModel;
+      }
+    }
+  else
+    {
+    // single file
+    metadata = d->dicomObjectModelAsString(d->filterModel);
+    }
+  return metadata;
 }
 
 // --------------------------------------------------------------------------
-
 void ctkDICOMObjectListWidget::copyMetadata()
 {
   Q_D(ctkDICOMObjectListWidget);
   QClipboard *clipboard = QApplication::clipboard();
   clipboard->setText(metadataAsText());
 }
+
+// --------------------------------------------------------------------------
+void ctkDICOMObjectListWidget::copyAllFilesMetadata()
+{
+  Q_D(ctkDICOMObjectListWidget);
+  QApplication::setOverrideCursor(QCursor(Qt::BusyCursor));
+  QClipboard *clipboard = QApplication::clipboard();
+  clipboard->setText(metadataAsText(true));
+  QApplication::restoreOverrideCursor();
+}
+
+//------------------------------------------------------------------------------
+void ctkDICOMObjectListWidget::onFilterChanged()
+{
+  Q_D(ctkDICOMObjectListWidget);
+
+  // Change the searchbox background to yellow
+  // if there are no matches
+  bool showWarning = (d->filterModel->rowCount() == 0 &&
+    d->dicomObjectModel->rowCount() != 0);
+  QPalette palette;
+  if (showWarning)
+    {
+    palette.setColor(QPalette::Base, Qt::yellow);
+    }
+  else
+    {
+    palette.setColor(QPalette::Base, Qt::white);
+    }
+  d->metadataSearchBox->setPalette(palette);
+}
+
+//------------------------------------------------------------------------------
+void ctkDICOMObjectListWidget::setFilterExpression(const QString& expr)
+{
+  Q_D(ctkDICOMObjectListWidget);
+  d->filterExpression = expr;
+  d->setFilterExpressionInModel(d->filterModel, expr);
+}
+
+//------------------------------------------------------------------------------
+QString ctkDICOMObjectListWidget::filterExpression()
+{
+  Q_D(ctkDICOMObjectListWidget);
+  return d->filterExpression;
+}

+ 17 - 3
Libs/DICOM/Widgets/ctkDICOMObjectListWidget.h

@@ -21,7 +21,7 @@
 #ifndef __ctkDICOMObjectListWidget_h
 #define __ctkDICOMObjectListWidget_h
 
-// Qt includes 
+// Qt includes
 #include <QItemSelection>
 #include <QWidget>
 
@@ -35,6 +35,7 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMObjectListWidget : public QWidget
   Q_OBJECT
   Q_PROPERTY(QString currentFile READ currentFile WRITE setCurrentFile)
   Q_PROPERTY(QStringList fileList READ fileList WRITE setFileList)
+  Q_PROPERTY(QString filterExpression READ filterExpression WRITE setFilterExpression)
 
 public:
   typedef QWidget Superclass;
@@ -44,8 +45,18 @@ public:
   QString currentFile();
   QStringList fileList();
 
+  /// Filter displayed metadata based on content in Tag, Attribute, and Value columns.
+  /// Simple search : enter any text to show only those items that contains the text.
+  /// Use ? and * wildcards to represent any single character or sequence of characters.
+  /// Regular expression search: Enter regexp: followed by a regular expression.
+  /// For example, show 3 specific tags, enter : regexp : 0010, 0010 | 0010, 0020 | 0010, 0030
+  QString filterExpression();
+
   /// Get metadata tree as plain text
-  QString metadataAsText();
+  QString metadataAsText(bool allFiles = false);
+
+  /// Open DICOM tag definition in a web browser
+  void openLookupUrl(QString tag);
 
 protected:
   QScopedPointer<ctkDICOMObjectListWidgetPrivate> d_ptr;
@@ -60,12 +71,15 @@ Q_SIGNALS:
 public Q_SLOTS:
   void setCurrentFile(const QString& newFileName);
   void setFileList(const QStringList& fileList);
+  void setFilterExpression(const QString& expr);
 
 protected Q_SLOTS:
-  void openLookupUrl(const QModelIndex&);
+  void itemDoubleClicked(const QModelIndex&);
+  void onFilterChanged();
   void updateWidget();
   void copyPath();
   void copyMetadata();
+  void copyAllFilesMetadata();
 };
 
 #endif

+ 10 - 0
Libs/DICOM/Widgets/ctkDICOMObjectModel.h

@@ -43,6 +43,7 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMObjectModel
   Q_OBJECT
   typedef QStandardItemModel Superclass;
   //Q_PROPERTY(setFile);
+  Q_ENUMS(ColumnIndex)
 
 public:
 
@@ -50,6 +51,15 @@ public:
   virtual ~ctkDICOMObjectModel();
   Q_INVOKABLE void setFile (const QString& fileName);
 
+  enum ColumnIndex
+    {
+    TagColumn = 0,
+    AttributeColumn = 1,
+    ValueColumn = 2,
+    VRColumn = 3,
+    LengthColumn = 4
+    };
+
 protected:
   QScopedPointer<ctkDICOMObjectModelPrivate> d_ptr;