From 7c8d97c73dc89984417142fad1a09118f1217905 Mon Sep 17 00:00:00 2001 From: sunwen Date: Thu, 27 Oct 2022 11:03:25 +0800 Subject: [PATCH] Add C-Store protocol. --- src/src/PACS/Common/dicomviewerhelper.cpp | 2 +- src/src/PACS/Common/dicomviewerhelper.h | 3 +- src/src/PACS/Network/GetWorker.cpp | 1 - src/src/PACS/Network/StoreWorker.cpp | 89 +++++++++ src/src/PACS/Network/StoreWorker.h | 35 ++++ src/src/PACS/Widget/StoreDialog.cpp | 57 ++++++ src/src/PACS/Widget/StoreDialog.h | 27 +++ src/src/UI/Widget/ToolBar/DefaultToolBar.cpp | 15 +- src/src/UI/Widget/ToolBar/DefaultToolBar.h | 5 +- .../UI/Widget/ToolBar/ExportToolButton.cpp | 179 ++++++++++++++++++ src/src/UI/Widget/ToolBar/ExportToolButton.h | 28 +++ src/src/UI/Window/QDicomViewer.cpp | 3 +- 12 files changed, 436 insertions(+), 8 deletions(-) create mode 100644 src/src/PACS/Network/StoreWorker.cpp create mode 100644 src/src/PACS/Network/StoreWorker.h create mode 100644 src/src/PACS/Widget/StoreDialog.cpp create mode 100644 src/src/PACS/Widget/StoreDialog.h create mode 100644 src/src/UI/Widget/ToolBar/ExportToolButton.cpp create mode 100644 src/src/UI/Widget/ToolBar/ExportToolButton.h diff --git a/src/src/PACS/Common/dicomviewerhelper.cpp b/src/src/PACS/Common/dicomviewerhelper.cpp index 052e025..a098e8c 100644 --- a/src/src/PACS/Common/dicomviewerhelper.cpp +++ b/src/src/PACS/Common/dicomviewerhelper.cpp @@ -1,8 +1,8 @@ #include "dicomviewerhelper.h" -#include #include #include #include +#include //---------------------------------------------------------------------------------- diff --git a/src/src/PACS/Common/dicomviewerhelper.h b/src/src/PACS/Common/dicomviewerhelper.h index 3ccca3b..b2b3519 100644 --- a/src/src/PACS/Common/dicomviewerhelper.h +++ b/src/src/PACS/Common/dicomviewerhelper.h @@ -8,7 +8,8 @@ #include -struct host { +struct host +{ QString name; QString ae; QString ip; diff --git a/src/src/PACS/Network/GetWorker.cpp b/src/src/PACS/Network/GetWorker.cpp index 8f0f85f..116f9a3 100644 --- a/src/src/PACS/Network/GetWorker.cpp +++ b/src/src/PACS/Network/GetWorker.cpp @@ -49,7 +49,6 @@ bool GetWorker::initDcmSCU() void GetWorker::getBySeriesUID(const QString& studyInstanceUID, const QString& seriesInstanceUID, const QString& aDicomType) { - qDebug(aDicomType.toLatin1()); if (!initDcmSCU()) { mScu->abortAssociation(); diff --git a/src/src/PACS/Network/StoreWorker.cpp b/src/src/PACS/Network/StoreWorker.cpp new file mode 100644 index 0000000..7367d30 --- /dev/null +++ b/src/src/PACS/Network/StoreWorker.cpp @@ -0,0 +1,89 @@ +#include "StoreWorker.h" +#include "dcmtk/dcmnet/scu.h" + +StoreWorker::StoreWorker(QObject* aParent) + : QObject(aParent) +{ +} + +bool StoreWorker::initDcmSCU() +{ + mScu = new DcmSCU(); + mScu->setAETitle(mOurTitle); + mScu->setPeerHostName(mPeerIP); + mScu->setPeerPort(OFstatic_cast(Uint16, mPeerPort)); + mScu->setPeerAETitle(mPeerTitle); + OFList syntaxes; + syntaxes.push_back(UID_LittleEndianExplicitTransferSyntax); + mScu->addPresentationContext(UID_CTImageStorage, syntaxes); + mScu->addPresentationContext(UID_MRImageStorage, syntaxes); + OFCondition cond = mScu->initNetwork(); + if (cond.bad()) + { + return false; + } + cond = mScu->negotiateAssociation(); + if (cond.bad()) + { + return false; + } + return true; +} + +void StoreWorker::setPacsInfo(const std::string& aPeerIP, unsigned short aPeerPort, const std::string& aPeerTitle, const std::string& aOurTitle) +{ + mPeerIP = aPeerIP; + mPeerPort = aPeerPort; + mPeerTitle = aPeerTitle; + mOurTitle = aOurTitle; +} + +void StoreWorker::cancel() +{ + mCancelFlag = true; +} + +void StoreWorker::store(const QStringList& aFilePathList, const QString& aDicomType) +{ + if (!initDcmSCU()) + { + mScu->abortAssociation(); + emit notifyStoreDone(0); + emit notifyStoreFinished(); + delete mScu; + mScu = nullptr; + return; + } + T_ASC_PresentationContextID pcid; + Uint16 response; + DcmDataset dataset; + for(int i = 0;i < aFilePathList.size(); ++i) + { + if(mCancelFlag) + { + mCancelFlag = false; + break; + } + if(aDicomType.toLower() == "mr") + { + pcid = mScu->findPresentationContextID(UID_MRImageStorage,""); + } + else + { + pcid = mScu->findPresentationContextID(UID_CTImageStorage,""); + } + OFCondition cond = mScu->sendSTORERequest(pcid,aFilePathList.at(i).toStdString(), &dataset, response); + if (cond.bad() || cond != EC_Normal || response != 0) + { + mScu->closeAssociation(DCMSCU_ABORT_ASSOCIATION); + emit notifyStoreDone(0); + emit notifyStoreFinished(); + return; + } + emit notifyStoreDone(i+1); + } + mScu->releaseAssociation(); + delete mScu; + mScu = nullptr; + emit notifyStoreFinished(); +} diff --git a/src/src/PACS/Network/StoreWorker.h b/src/src/PACS/Network/StoreWorker.h new file mode 100644 index 0000000..40d395b --- /dev/null +++ b/src/src/PACS/Network/StoreWorker.h @@ -0,0 +1,35 @@ +#ifndef STOREWORKER_H +#define STOREWORKER_H + +#include + +class DcmSCU; + +class StoreWorker : public QObject +{ + Q_OBJECT +public: + StoreWorker(QObject* aParent); + void setPacsInfo(const std::string& aPeerIP, unsigned short aPeerPort, const std::string& aPeerTitle, const std::string& aOurTitle); + void cancel(); + Q_INVOKABLE void store(const QStringList& aFilePathList, const QString& aDicomType); + +signals: + void notifyStoreDone(int aIndex); //0 failed,1,2,3.. sucessed + void notifyStoreFinished(); + +private: + bool initDcmSCU(); + +private: + DcmSCU* mScu; + QString mStoreFilePath; + std::string mPeerTitle; + std::string mPeerIP; + std::string mOurTitle; + unsigned short mPeerPort; + unsigned short mOurPort; + bool mCancelFlag = false; +}; + +#endif // STOREWORKER_H diff --git a/src/src/PACS/Widget/StoreDialog.cpp b/src/src/PACS/Widget/StoreDialog.cpp new file mode 100644 index 0000000..ef000de --- /dev/null +++ b/src/src/PACS/Widget/StoreDialog.cpp @@ -0,0 +1,57 @@ +#include "StoreDialog.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ + const QString SENDING_IMAGES_TO_PACS = "Sending images to PACS..."; + QString makeText(int aTotalNum, int aCurrentNum) + { + return SENDING_IMAGES_TO_PACS + "[" + QString::number(aCurrentNum) + "/" + QString::number(aTotalNum) + "]"; + } +} + +StoreDialog::StoreDialog(QWidget* aParent) + : QDialog(aParent) + , mFileSize(0) + , mProgressBar(new QProgressBar(this)) + , mText(new QLabel(this)) + , mCancelButton(nullptr) +{ + setFixedSize(QSize(380,105)); + initWidget(); +} + +void StoreDialog::initWidget() +{ + mProgressBar->setTextVisible(false); + QVBoxLayout* vLayout = new QVBoxLayout(this); + vLayout->addWidget(mProgressBar); + vLayout->addWidget(mText); + QWidget* buttonContainer = new QWidget(this); + vLayout->addWidget(buttonContainer); + QHBoxLayout* hLayout = new QHBoxLayout(buttonContainer); + hLayout->addSpacerItem(new QSpacerItem(280,105)); + mCancelButton = new QPushButton(tr("Cancel"),buttonContainer); + mCancelButton->setFixedHeight(23); + hLayout->addWidget(mCancelButton); + connect(mCancelButton,&QPushButton::clicked,this,&QDialog::reject); +} + +void StoreDialog::setFileSize(int aFileSize) +{ + mFileSize = aFileSize; + mText->setText(makeText(mFileSize, 0)); +} + +void StoreDialog::setStoreProgress(int aNum) +{ + mProgressBar->setValue((aNum*100)/mFileSize); + mText->setText(makeText(mFileSize, aNum)); +} + diff --git a/src/src/PACS/Widget/StoreDialog.h b/src/src/PACS/Widget/StoreDialog.h new file mode 100644 index 0000000..72816df --- /dev/null +++ b/src/src/PACS/Widget/StoreDialog.h @@ -0,0 +1,27 @@ +#ifndef STOREDIALOG_H +#define STOREDIALOG_H + +#include + +class QProgressBar; +class QPushButton; +class QLabel; + +class StoreDialog : public QDialog +{ +public: + StoreDialog(QWidget* aParent = nullptr); + void setStoreProgress(int aNum); + void setFileSize(int aFileSize); + +private: + void initWidget(); + +private: + int mFileSize; + QProgressBar* mProgressBar; + QLabel* mText; + QPushButton* mCancelButton; +}; + +#endif // STOREDIALOG_H diff --git a/src/src/UI/Widget/ToolBar/DefaultToolBar.cpp b/src/src/UI/Widget/ToolBar/DefaultToolBar.cpp index c1b58ba..2a4afde 100644 --- a/src/src/UI/Widget/ToolBar/DefaultToolBar.cpp +++ b/src/src/UI/Widget/ToolBar/DefaultToolBar.cpp @@ -8,8 +8,9 @@ #include #include #include "Rendering/Measure/Measure.h" - #include "Common/QGlobals.h" +#include "ExportToolButton.h" + typedef tuple ActionProperty; namespace { const char *SYNC_MANUAL_URL = ":/InfiniteViewer/Icon/sync/sync_manual.png"; @@ -30,7 +31,7 @@ namespace { DefaultToolBar::DefaultToolBar(QWidget *parent) : QToolBar(parent) , mBtnFile(new QToolButton(this)) , mBtnImport(new QToolButton(this)) - , mBtnSave(new QToolButton(this)) + , mBtnSave(new ExportToolButton(this)) , mBtnGrid(new QToolButton(this)) , mBtnSync(new QToolButton(this)) , mBtnAnonymize(new QToolButton(this)) @@ -52,7 +53,8 @@ DefaultToolBar::DefaultToolBar(QWidget *parent) : QToolBar(parent) , mActionMinimize(nullptr) , mActionMaximize(nullptr) , mActionClose(nullptr) - , mActionFullScreen(nullptr) { + , mActionFullScreen(nullptr) +{ //init icons mManualIcon.addFile(SYNC_MANUAL_URL); mAutoIcon.addFile(SYNC_AUTO_URL); @@ -64,6 +66,12 @@ DefaultToolBar::~DefaultToolBar() { } +void DefaultToolBar::setViewManager(ImageViewManager *aManager) +{ + mImageViewManager = aManager; + mBtnSave->setImageViewManager(mImageViewManager); +} + QAction* DefaultToolBar::addButton(QToolButton* button, const char* objectName) { button->setObjectName(objectName); button->setToolButtonStyle(Qt::ToolButtonIconOnly); @@ -164,6 +172,7 @@ void DefaultToolBar::initImportButton() { } void DefaultToolBar::initExportButton() { + mBtnSave->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); mBtnSave->setToolTip(QString("Export images")); connect(mBtnSave, &QToolButton::clicked, this, &DefaultToolBar::save); } diff --git a/src/src/UI/Widget/ToolBar/DefaultToolBar.h b/src/src/UI/Widget/ToolBar/DefaultToolBar.h index 9af0093..466e647 100644 --- a/src/src/UI/Widget/ToolBar/DefaultToolBar.h +++ b/src/src/UI/Widget/ToolBar/DefaultToolBar.h @@ -11,6 +11,7 @@ #include "UI/Manager/ImageViewStateCheckWorker.h" class QButtonGroup; +class ExportToolButton; class DefaultToolBar : public QToolBar { Q_OBJECT @@ -21,6 +22,7 @@ public: void resetNeedCheckFunctionButtons(); void updateNeedCheckFunctionButtons(ViewFunctionState state); + void setViewManager(ImageViewManager* aManager); signals: void openFile(); @@ -70,7 +72,7 @@ private: QIcon mDisIcon; QToolButton *mBtnFile; QToolButton *mBtnImport; - QToolButton *mBtnSave; + ExportToolButton *mBtnSave; QToolButton *mBtnGrid; QToolButton *mBtnSync; QToolButton *mBtnAnonymize; @@ -99,6 +101,7 @@ private: QAction *mActionHideMeasure; QAction *mActionHidePatData; QAction* mSyncActions[3]={nullptr,nullptr,nullptr}; + ImageViewManager* mImageViewManager; }; diff --git a/src/src/UI/Widget/ToolBar/ExportToolButton.cpp b/src/src/UI/Widget/ToolBar/ExportToolButton.cpp new file mode 100644 index 0000000..8d05e09 --- /dev/null +++ b/src/src/UI/Widget/ToolBar/ExportToolButton.cpp @@ -0,0 +1,179 @@ +#include "ExportToolButton.h" + +#include "Common/ImageSetStore.h" +#include "PACS/Common/dicomviewerhelper.h" +#include "PACS/Network/StoreWorker.h" +#include "PACS/Widget/StoreDialog.h" +#include "UI/Manager/ImageViewManager.h" + +#include + +namespace +{ + void createFilePathListFromAllOpenedStudies(QStringList& aResult,ExtendMedicalImageProperties* aProperty) + { + if (aProperty == nullptr) + { + return; + } + std::vector> fileNames = aProperty->GetFileNames(); + auto itr1 = fileNames.begin(); + for(; itr1 != fileNames.end();++itr1) + { + aResult.push_back(itr1->first.c_str()); + } + } + + void createFilePathListFromCurrentStudy(QStringList& aResult,ExtendMedicalImageProperties* aProperty, const QString& aCurrentStudyUid) + { + if (aProperty == nullptr || aCurrentStudyUid != aProperty->GetStudyUID()) + { + return; + } + std::vector> fileNames = aProperty->GetFileNames(); + auto itr1 = fileNames.begin(); + for(; itr1 != fileNames.end();++itr1) + { + aResult.push_back(itr1->first.c_str()); + } + } + + void createFilePathListFromCurrentSeries(QStringList& aResult,ExtendMedicalImageProperties* aProperty, const QString& aCurrentSeriesUid) + { + if (aProperty == nullptr || aCurrentSeriesUid != aProperty->GetSeriesUID()) + { + return; + } + std::vector> fileNames = aProperty->GetFileNames(); + auto itr1 = fileNames.begin(); + for(; itr1 != fileNames.end();++itr1) + { + aResult.push_back(itr1->first.c_str()); + } + } +} + +ExportToolButton::ExportToolButton(QWidget* aParent) + : QToolButton(aParent) + , mImageViewManager(nullptr) + , mStoreDialog(nullptr) + , mStoreWorker(nullptr) + , mThread(nullptr) +{ + initSendToPacsMenu(); +} + +void ExportToolButton::initSendToPacsMenu() +{ + QMenu* saveMenu = new QMenu(this); + QMenu* sendPacs = new QMenu(tr("Send to PACS"), this); + QMenu* allOpenedStudies = new QMenu(tr("All opened studies"), sendPacs); + QMenu* currentStudy = new QMenu(tr("Current study"), sendPacs); + QMenu* currentSeries = new QMenu(tr("Current series"), sendPacs); + QList hosts; + DicomViewerHelper::pacsInfo(hosts); + foreach(auto h,hosts) + { + QString text = h.name + " [" + h.ae + "@" + h.ip + ":" + h.port + "]"; + auto sendToPacs = [h,this]() + { + QAction* action = qobject_cast(sender()); + if (action == nullptr) + { + return; + } + std::vector openedFile = ImageSetStore::GetInstance()->getProperties(); + if (mImageViewManager == nullptr) + { + return; + } + + DicomImageView* imageView = mImageViewManager->getCurrentView(); + if(imageView == nullptr) + { + return; + } + + SeriesImageSet* imageSet = imageView->getSeriesInstance(); + if(imageSet == nullptr) + { + return; + } + auto itr = openedFile.begin(); + QStringList filePathList; + for(;itr != openedFile.end();++itr) + { + std::vector> fileNames = (*itr)->GetFileNames(); + if(action->data().toString() == "All opened studies") + { + createFilePathListFromAllOpenedStudies(filePathList, (*itr)); + } + else if(action->data().toString() == "Current study") + { + createFilePathListFromCurrentStudy(filePathList, (*itr), imageSet->getStudyUID()); + } + else if(action->data().toString() == "Current series") + { + createFilePathListFromCurrentSeries(filePathList, (*itr), imageSet->getSeriesName()); + } + } + if(filePathList.isEmpty()) + { + return; + } + if (mThread == nullptr) + { + mThread = new QThread(this); + } + if (mStoreWorker == nullptr) + { + mStoreWorker = QSharedPointer(new StoreWorker(nullptr)); + mStoreWorker->moveToThread(mThread); + connect(mStoreWorker.data(),&StoreWorker::notifyStoreFinished,mThread,&QThread::quit); + } + + if(mStoreDialog == nullptr) + { + mStoreDialog = new StoreDialog(this); + connect(mStoreDialog,&QDialog::rejected,this,[this]() + { + mStoreWorker->cancel(); + }); + connect(mStoreWorker.data(),&StoreWorker::notifyStoreDone,mStoreDialog,&StoreDialog::setStoreProgress); + connect(mStoreWorker.data(),&StoreWorker::notifyStoreFinished,mStoreDialog,&StoreDialog::accept); + } + mThread->start(); + mStoreWorker->setPacsInfo(h.ip.toStdString(), static_cast(h.port.toShort()), + h.ae.toStdString(), DicomViewerHelper::ourTitle().toStdString()); + QMetaObject::invokeMethod(mStoreWorker.data(), "store", Qt::QueuedConnection, Q_ARG(QStringList, filePathList), Q_ARG(QString, "")); + mStoreDialog->setFileSize(filePathList.size()); + mStoreDialog->exec(); + }; + QAction* allAction = new QAction(text,allOpenedStudies); + allAction->setData("All opened studies"); + connect(allAction,&QAction::triggered,this,sendToPacs); + allOpenedStudies->addAction(allAction); + + QAction* studyAction = new QAction(text,currentStudy); + studyAction->setData("Current study"); + connect(studyAction,&QAction::triggered,this,sendToPacs); + currentStudy->addAction(studyAction); + + QAction* seriesAction = new QAction(text,currentSeries); + seriesAction->setData("Current series"); + connect(seriesAction,&QAction::triggered,this,sendToPacs); + currentSeries->addAction(seriesAction); + } + saveMenu->addMenu(sendPacs); + sendPacs->addMenu(allOpenedStudies); + sendPacs->addMenu(currentStudy); + sendPacs->addMenu(currentSeries); + + setPopupMode(QToolButton::MenuButtonPopup); + setMenu(saveMenu); +} + +void ExportToolButton::setImageViewManager(ImageViewManager* aImageViewManager) +{ + mImageViewManager = aImageViewManager; +} diff --git a/src/src/UI/Widget/ToolBar/ExportToolButton.h b/src/src/UI/Widget/ToolBar/ExportToolButton.h new file mode 100644 index 0000000..af50896 --- /dev/null +++ b/src/src/UI/Widget/ToolBar/ExportToolButton.h @@ -0,0 +1,28 @@ +#ifndef EXPORTTOOLBUTTON_H +#define EXPORTTOOLBUTTON_H + +#include +#include + +class ImageViewManager; +class StoreDialog; +class StoreWorker; +class QThread; + +class ExportToolButton : public QToolButton +{ +public: + ExportToolButton(QWidget* aParent); + void setImageViewManager(ImageViewManager* aImageViewManager); + +private: + void initSendToPacsMenu(); + +private: + ImageViewManager* mImageViewManager; + StoreDialog* mStoreDialog; + QSharedPointer mStoreWorker; + QThread* mThread; +}; + +#endif // EXPORTTOOLBUTTON_H diff --git a/src/src/UI/Window/QDicomViewer.cpp b/src/src/UI/Window/QDicomViewer.cpp index 998e6e9..d46ceb7 100644 --- a/src/src/UI/Window/QDicomViewer.cpp +++ b/src/src/UI/Window/QDicomViewer.cpp @@ -1,4 +1,4 @@ -#include "QDicomViewer.h" +#include "QDicomViewer.h" #include #include @@ -23,6 +23,7 @@ QDicomViewer::QDicomViewer(QWidget *parent) : QMainWindow(parent), m_import(nullptr) { ui->setupUi(this); this->statusBar()->showMessage(tr("Ready")); + ui->toolBar->setViewManager(ui->viewContainer->getViewManager()); QWidget::setWindowTitle(Project_NAME); this->Initial();