文章目录

  • 开发环境
  • 创建 Qt Widgets 程序
  • 设计界面
  • 配置 LeadTools 路径
  • 编写代码
    • 使用 LDicomNet 实现 SCP 的步骤
    • 日志输出
    • 编写 SCP Server 类
    • 编写 SCP Client 类
    • 启动 LDicomNet 及启动监听
  • 编译程序
  • 运行程序
  • 发布与部署
  • 测试程序
  • 界面美化
  • 源码下载
  • 参考

开发环境

  • LeadTools 17
  • Qt 5.15.2 MSVC2019 32bit
  • Qt Creator 7.0.0
  • Visual Studio 2019
  • Windows 11

创建 Qt Widgets 程序

启动 Qt Creator,点击欢迎界面左上角的 Create Project… 按钮,打开 “New Project” 窗口,选择 Qt Widgets Application

点击 Choose… 按钮,进入 Project Location 界面,输入项目名称,点击 下一步(N)> 按钮,进入下一个界面。

继续点击 下一步(N)> 按钮。

注:Qt Creator 默认文件名全小写,如需单词首字母大写文件名,可通过菜单【工具 > 选项】打开选项窗口,再选择【C++ > File Naming】,去掉 Lower case file names 前的勾。
另外,Qt Creator 默认使用 #ifndef 避免同一个文件被多次包含,如果想使用 #pragma once,也可以在这个界面设置,只要勾选 Use "#pragma once" instead of "#ifndef" guards 即可。

继续点击 下一步(N)> 按钮。

说明:由于本文使用的 LeadTools 版本较老,此处一定要选择 MSVC 32 位编译器,不知新版 LeadTools 对 64 位及 MinGW 支持得怎么样。

继续点击 下一步(N)>,最后点击 完成(F) 按钮。创建完成的项目如下图:

设计界面

双击 MainWindow.ui 打开设计界面。
向窗体上拖拽一个 Horizontal Layout 控件和一个 Table View 控件。
再依次向 Horizontal Layout 里拖拽 Combo BoxLine EditPush ButtonLine EditPush Button 五个控件,从左到右顺序排列。然后,依次设置这 5 个控件的 objectName 为:cmbAddressedtPortbtnListenedtImagePathbtnBrowse

  • 设置 edtPortmaximumSize > 宽度60text6666maxLength5
  • 设置 btnListentext启动监听
  • 设置 edtImagePathreadOnlytrue
  • 设置 btnBrowsetext浏览...

MainWindow 空白处点击鼠标右键,在弹出菜单上选择 布局 > 垂直布局

设计完成的界面如下:

界面结构图:

配置 LeadTools 路径

在项目结构图里,在项目名称 CStoreSCPDemo 上点击鼠标右键,在弹出菜单上选择 添加库...,打开 添加库 对话框。

在 “添加库” 对话框中选择 外部库,然后点击 下一步(N)> 按钮。

在 Details 界面,选择 库文件包含路径,取消勾选 LinuxMac,取消勾选 为debug版本添加'd'作为后缀

继续点击 下一步(N)>,然后点击 完成(F) 按钮。

点击 完成(F) 后,CStoreSCPDemo.pro 文件将自动打开,在 win32: LIBS 一行的最后,再添加 -lLtkrn_u -lLtwvc_u,如下:

win32: LIBS += -L'E:/LeadTools 17/Lib/CDLL/Win32/' -lLtdic_u -lLtkrn_u -lLtwvc_u

编写代码

编码前,应该对 DICOM、SCU、SCP、DIMSE、C-STORE、AE 等概念有简单了解,不了解的小伙伴可以先阅读这篇文章:DICOM医学图像处理:DICOM网络传输

LeadTools 提供了 LDicomNet 类用以支持 DICOM 网络连接和消息传输。LDicomNet 类支持 DICOM 协议的全部 11 种 DIMSE 服务(C-STORE、C-GET、C-MOVE、C-FIND、C-ECHO、N-EVENT-REPORT、N-GET、N-SET、N-ACTION、N-CREATE、N-DELETE)。
LDicomNet 类同时实现了对 SCU 和 SCP 的支持,我们在实际使用时,只需重写特定的虚函数,即可实现不同的 SCU 或 SCP 服务。本文我们仅实现 C-STORE SCP。

使用 LDicomNet 实现 SCP 的步骤

  1. LDicomNet 为基类,定义两个派生类,一个用于接收 SCU Connection,一个用于处理 DIMSE 消息。接收 SCU Connection 的类我们称为 SCP Server,处理 DIMSE 消息的类我们称为 SCP Client
  2. 重写 SCP ServerOnAccept() 函数。当接收到 SCU Connection 时,OnAccept() 函数将会被自动调用。在 OnAccept() 内,以 SCP Client 实例为参数调用 Accept() 函数,接受 SCU Connection 请求。
  3. 重写 SCP ClientOnReceiveAssociateRequest() 函数。当接收到 SCU Association 时,OnReceiveAssociateRequest() 函数将会被自动调用。建立 Association 连接是两个 DICOM 实体(AE)之间进行交互的第一步。成功建立 Association 连接后,才能进行 DIMSE 消息交换。
  4. 根据所要实现的 DIMSE 服务不同,重写 SCP Client 类的特定函数。如实现 C-STORE SCP 服务,需重写 OnReceiveCStoreRequest() 函数。

日志输出

在前面的界面设计中,在界面上放了一个 Table View 控件,这是用来显示操作日志的。

日志显示功能,准备使用 Qt 的自定义事件实现。为此,需要定义一个派生自 QEvent 的日志事件类,还需要重写 MainWindowcustomEvent 函数,最后再把显示日志的功能封装成一个 showLog 函数。

这里之所以要先提一下显示日志的实现方式,是因为在编写 SCP ServerSCP Client 时需要考虑如何输出日志。

// LogEvent.h
#pragma once#include <QEvent>
#include <QString>const QEvent::Type LogEventType = QEvent::Type(QEvent::User+1);class LogEvent : public QEvent
{public:LogEvent(QString text);QString text() {return m_text;}private:QString m_text;
};
// LogEvent.cpp
#include "LogEvent.h"LogEvent::LogEvent(QString text): QEvent(LogEventType)
{m_text = text;
}
void MainWindow::customEvent(QEvent* event)
{if (event->type() != LogEventType) {return;}LogEvent* logEvent = static_cast<LogEvent*>(event);showLog(logEvent->text());
}
void MainWindow::showLog(QString strLog)
{int rowCount = m_tableModel.rowCount();m_tableModel.setItem(rowCount, 0, new QStandardItem(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss:zzz")));m_tableModel.setItem(rowCount, 1, new QStandardItem(strLog));QCoreApplication::processEvents();
}

编写 SCP Server 类

SCP Server 类派生自 LDicomNet,用于接收 SCU Connection 请求,代码相对简单,核心代码在 OnAccept 函数。

SCPServer 类头文件定义如下:

// SCPServer.h
#pragma once
#include <QString>
#include <QObject>#define LTV17_CONFIG
#include "Ltdic.h"#include "SCPClient.h"class SCPServer : public LDicomNet
{public:SCPServer();void setServerIPPort(QString strIP, uint nPort) {m_strIP = strIP;m_nPort = nPort;}void setImageFolder(QString strImageFolder) {m_strImageFolder = strImageFolder;}void registerLogger(QObject* pLogger) {m_pLogger = pLogger;}void Close();private:QString m_strIP;uint m_nPort;QString m_strImageFolder;QObject* m_pLogger = nullptr;L_VOID OnAccept(L_INT nError);L_VOID OnClose(L_INT nError, LDicomNet *pClient);void outputLog(QString strLog);void manageConnection(QString strClientIP, SCPClient *pClient);
};

OnAccept 函数代码如下:

L_VOID SCPServer::OnAccept(L_INT nError)
{if (nError != DICOM_SUCCESS) {QString dcmError = DicomError::instance().text(nError);QString strError = QString("接收 SCU 连接时发生错误,原因:%1").arg(dcmError);outputLog(strError);return;}SCPClient* pClient = new SCPClient();pClient->setImageFolder(m_strImageFolder);pClient->registerLogger(m_pLogger);L_INT nRet = Accept(pClient);if(nRet != DICOM_SUCCESS) {QString dcmError = DicomError::instance().text(nRet);QString strError = QString("接收 SCU 连接时发生错误,原因:%1").arg(dcmError);outputLog(strError);pClient->Close();delete pClient;return;}L_TCHAR szPeerAddress[PDU_MAX_TITLE_SIZE];L_UINT nPeerPort;pClient->GetPeerInfo(szPeerAddress, sizeof(L_TCHAR) * PDU_MAX_TITLE_SIZE, &nPeerPort);QString strPeerIP = QString::fromStdWString(szPeerAddress);pClient->setSCUIPPort(strPeerIP, nPeerPort);pClient->setServerIPPort(m_strIP, m_nPort);QString strLog = QString("%1:接受一个 SCU 连接(%1:%2)").arg(strPeerIP).arg(nPeerPort);outputLog(strLog);manageConnection(strPeerIP, pClient);
}

编写 SCP Client 类

SCP Client 类派生自 LDicomNet,用于接收 SCU Association 请求,以及实现 C-STORE SCP 服务,核心代码在 OnReceiveAssociateRequestOnReceiveCStoreRequest 函数。

SCPClient 类头文件定义如下:

// SCPClient.h
#pragma once
#include <QString>
#include <QObject>
#include <QCoreApplication>#define LTV17_CONFIG
#include "Ltdic.h"class SCPClient : public LDicomNet
{public:SCPClient();void setServerIPPort(QString strServerIP, uint nServerPort) {m_strServerIP = strServerIP;m_nServerPort = nServerPort;}QString getSCUIP() {return m_strSCUIP;}void setSCUIPPort(QString strSCUIP, uint nSCUPort) {m_strSCUIP = strSCUIP;m_nSCUPort = nSCUPort;}QString getSCUAETitle() {return m_strSCUAETitle;}void setImageFolder(QString strImageFolder) {m_strImageFolder = strImageFolder;}void registerLogger(QObject* pLogger) {m_pLogger = pLogger;}private:QString m_strServerIP;uint m_nServerPort = 0;QString m_strSCUIP;uint  m_nSCUPort = 0;QString m_strSCUAETitle;QString m_strImageFolder = QCoreApplication::applicationDirPath();QObject* m_pLogger = nullptr;L_VOID OnReceiveAssociateRequest (LDicomAssociate *pPDU);L_VOID OnReceiveCStoreRequest(L_UCHAR nPresentationID, L_UINT16 nMessageID, L_TCHAR* pszClass, L_TCHAR* pszInstance, L_UINT16 nPriority, L_TCHAR* pszMoveAE, L_UINT16 nMoveMessageID, LDicomDS* pDS);L_VOID OnReceiveReleaseRequest();L_VOID OnReceiveAbort(L_UCHAR nSource, L_UCHAR nReason);bool getElementValue(LDicomDS* pDS, L_UINT32 nTag, QString& strValue);bool getElementValue(LDicomDS* pDS, pDICOMELEMENT pElement, QString& strValue);bool getDateTime(LDicomDS* pDS, L_UINT32 nDateTag, L_UINT32 nTimeTag, QDateTime& dateTime);void outputLog(QString strLog);
};

OnReceiveAssociateRequest 函数代码如下:

L_VOID SCPClient::OnReceiveAssociateRequest(LDicomAssociate *pPDU)
{outputLog(m_strSCUIP + ":收到 SCU Associate 请求...");//要先调用 GetCalled 后调用 GetCalling,否则出错L_TCHAR szCalled[PDU_MAX_TITLE_SIZE];pPDU->GetCalled(szCalled, PDU_MAX_TITLE_SIZE);L_TCHAR szCalling[PDU_MAX_TITLE_SIZE];pPDU->GetCalling(szCalling, PDU_MAX_TITLE_SIZE);QString strClientAE = QString::fromWCharArray(szCalling);m_strSCUAETitle = strClientAE;//验证客户端版本L_UINT16 nVer=pPDU->GetVersion();if(nVer != 1) {outputLog(QString("%1:不支持的协议版本(%2),拒绝该连接请求。").arg(m_strSCUIP).arg(nVer));SendAssociateReject(PDU_REJECT_RESULT_PERMANENT, PDU_REJECT_SOURCE_PROVIDER1, PDU_REJECT_REASON_VERSION);return;}LDicomAssociate dicomAssociate(L_FALSE);dicomAssociate.Reset(L_FALSE);dicomAssociate.SetCalled(szCalled);dicomAssociate.SetCalling(szCalling);dicomAssociate.SetApplication(const_cast<L_TCHAR*>(UID_APPLICATION_CONTEXT_NAME));dicomAssociate.SetVersion(1);L_TCHAR szUID[] = L"1.2.3.108.56497.33";L_TCHAR szVersion[] = L"StoreSCP";dicomAssociate.SetImplementClass(L_TRUE, szUID);dicomAssociate.SetImplementVersion(L_TRUE, szVersion);QStringList firstTransferList;firstTransferList.append(QString::fromWCharArray(UID_JPEG_LOSSLESS_NONHIER_14B));firstTransferList.append(QString::fromWCharArray(UID_JPEG_LOSSLESS_NONHIER_14));firstTransferList.append(QString::fromWCharArray(UID_JPEG_LOSSLESS_NONHIER_15));firstTransferList.append(QString::fromWCharArray(UID_JPEG_LOSSLESS_HIER_PROCESS_28));firstTransferList.append(QString::fromWCharArray(UID_JPEG_LOSSLESS_HIER_PROCESS_29));firstTransferList.append(QString::fromWCharArray(UID_JPEG2000_LOSSLESS_ONLY));firstTransferList.append(QString::fromWCharArray(UID_RLE_LOSSLESS));firstTransferList.append(QString::fromWCharArray(UID_EXPLICIT_VR_LITTLE_ENDIAN));firstTransferList.append(QString::fromWCharArray(UID_IMPLICIT_VR_LITTLE_ENDIAN));firstTransferList.append(QString::fromWCharArray(UID_EXPLICIT_VR_BIG_ENDIAN));QList<L_TCHAR*> szFirstTransferList;szFirstTransferList.append(const_cast<L_TCHAR*>(UID_JPEG_LOSSLESS_NONHIER_14B));szFirstTransferList.append(const_cast<L_TCHAR*>(UID_JPEG_LOSSLESS_NONHIER_14));szFirstTransferList.append(const_cast<L_TCHAR*>(UID_JPEG_LOSSLESS_NONHIER_15));szFirstTransferList.append(const_cast<L_TCHAR*>(UID_JPEG_LOSSLESS_HIER_PROCESS_28));szFirstTransferList.append(const_cast<L_TCHAR*>(UID_JPEG_LOSSLESS_HIER_PROCESS_29));szFirstTransferList.append(const_cast<L_TCHAR*>(UID_JPEG2000_LOSSLESS_ONLY));szFirstTransferList.append(const_cast<L_TCHAR*>(UID_RLE_LOSSLESS));szFirstTransferList.append(const_cast<L_TCHAR*>(UID_EXPLICIT_VR_LITTLE_ENDIAN));szFirstTransferList.append(const_cast<L_TCHAR*>(UID_IMPLICIT_VR_LITTLE_ENDIAN));szFirstTransferList.append(const_cast<L_TCHAR*>(UID_EXPLICIT_VR_BIG_ENDIAN));//在 Abstract 表中查找,如果存在(并能找到Transfer),表示支持。优先支持无损压缩的传输语法。QStringList acceptClassUIDs;L_TCHAR szAbstract[PDU_MAX_UID_SIZE];L_INT nPresentationCount = pPDU->GetPresentationCount();for (int ind = 0; ind < nPresentationCount; ind++) {L_UCHAR   nID = pPDU->GetPresentation(ind);pPDU->GetAbstract(nID, szAbstract, PDU_MAX_UID_SIZE);QString strAbstract = QString::fromWCharArray(szAbstract);if (acceptClassUIDs.contains(strAbstract)) {dicomAssociate.AddPresentation(nID, PDU_ACCEPT_RESULT_ABSTRACT_SYNTAX, szAbstract);continue;} else {acceptClassUIDs.append(strAbstract);}dicomAssociate.AddPresentation(nID, PDU_ACCEPT_RESULT_SUCCESS, szAbstract);QStringList transferList;L_TCHAR szTransfer[PDU_MAX_UID_SIZE];L_INT transferCount = pPDU->GetTransferCount(nID);for (int j = 0; j < transferCount; j++) {pPDU->GetTransfer(nID, j, szTransfer, PDU_MAX_UID_SIZE);QString strTransfer = QString::fromWCharArray(szTransfer);if (transferList.contains(strTransfer) == false) {transferList.append(strTransfer);}}bool bFound = false;for (int j = 0; j < firstTransferList.length(); j++) {if (transferList.contains(firstTransferList.at(j))) {dicomAssociate.AddTransfer(nID, szFirstTransferList[j]);bFound = true;break;}}if (false == bFound && transferList.length() > 0) {L_TCHAR pszTransfer[PDU_MAX_TITLE_SIZE];transferList.at(0).toWCharArray(pszTransfer);pszTransfer[transferList.at(0).length()] = '\0';dicomAssociate.AddTransfer(nID, pszTransfer);bFound = true;}if (bFound) {dicomAssociate.SetResult(nID, PDU_ACCEPT_RESULT_SUCCESS);} else {dicomAssociate.SetResult(nID, PDU_ACCEPT_RESULT_TRANSFER_SYNTAX);}}L_INT nRet = SendAssociateAccept(&dicomAssociate);if (nRet != DICOM_SUCCESS) {QString dcmError = DicomError::instance().text(nRet);QString strError = QString("%1:发送 associate accept 发生错误,原因:%2").arg(m_strSCUIP, dcmError);outputLog(strError);return;}outputLog(m_strSCUIP + ":SCU Associate 已接受。");
}

OnReceiveCStoreRequest 函数代码如下:

L_VOID SCPClient::OnReceiveCStoreRequest(L_UCHAR nPresentationID, L_UINT16 nMessageID, L_TCHAR* pszClass, L_TCHAR* pszInstance, L_UINT16 nPriority, L_TCHAR* pszMoveAE, L_UINT16 nMoveMessageID, LDicomDS* pDS)
{Q_UNUSED(nPriority)Q_UNUSED(pszMoveAE)Q_UNUSED(nMoveMessageID)if (nullptr == pDS) {outputLog(m_strSCUIP + ":C-STORE DataSet Is NULL,返回 COMMAND_STATUS_PROCESSING_FAILURE。");SendCStoreResponse(nPresentationID, nMessageID, pszClass, pszInstance, COMMAND_STATUS_PROCESSING_FAILURE);return;}QString strInstanceUID = QString::fromWCharArray(pszInstance);QString strPatientID, strPatientName;getElementValue(pDS, TAG_PATIENT_ID, strPatientID);getElementValue(pDS, TAG_PATIENT_NAME, strPatientName);outputLog(QString("%1:收到 C-STORE 请求,PatientName:%2,InstanceUID:%3,ClassUID:%4").arg(m_strSCUIP, strPatientName, strInstanceUID, pszClass));if (strPatientName.isEmpty() || "***" == strPatientName) {  //此处对***做特殊处理,是因为测试图像的PatientName是***strPatientName = "UnknownName";}QString strStudyDate;QDateTime dtStudyTime;bool bSuccess = getDateTime(pDS, TAG_STUDY_DATE, TAG_STUDY_TIME, dtStudyTime);if (bSuccess) {strStudyDate = dtStudyTime.toString("yyyyMMdd");} else {strStudyDate = QDateTime::currentDateTime().toString("yyyyMMdd");}QString strModality;getElementValue(pDS, TAG_MODALITY, strModality);QString strSopInstanceUID = QString::fromWCharArray(pszInstance);QString strImagePath = QString("%1/%2").arg(m_strImageFolder, strPatientName);QDir qDir;if (qDir.exists(strImagePath) == false) {qDir.mkpath(strImagePath);}QString strImageFilename = QString("%1/%2_%3.dcm").arg(strImagePath, strModality, strSopInstanceUID);L_TCHAR pszImageFilename[_MAX_PATH];strImageFilename.toWCharArray(pszImageFilename);pszImageFilename[strImageFilename.length()] = '\0';try {pDS->ChangeTransferSyntax(const_cast<L_TCHAR*>(UID_JPEG_LOSSLESS_NONHIER_14B), 100, DICOM_CHANGETRAN_MINIMIZE_JPEG_SIZE);int nRet = pDS->SaveDS(pszImageFilename, DS_METAHEADER_PRESENT);if(nRet != DICOM_SUCCESS) {QString dcmError = DicomError::instance().text(nRet);QString strError = QString("%1:保存 DICOM 文件到 %2 失败,原因:%3").arg(m_strSCUIP, strImageFilename, dcmError);outputLog(strError);SendCStoreResponse(nPresentationID, nMessageID, pszClass, pszInstance, COMMAND_STATUS_PROCESSING_FAILURE);return;}QString strLog = m_strSCUIP + ":保存 DICOM 文件到 " + strImageFilename;outputLog(strLog);} catch(...) {outputLog(QString("%1:保存 DICOM 文件到 %2 失败,返回 COMMAND_STATUS_PROCESSING_FAILURE 状态。").arg(m_strSCUIP, strImageFilename));SendCStoreResponse(nPresentationID, nMessageID, pszClass, pszInstance, COMMAND_STATUS_PROCESSING_FAILURE);return;}outputLog(m_strSCUIP + ":保存 DICOM 文件成功,返回 COMMAND_STATUS_SUCCESS 状态。");SendCStoreResponse(nPresentationID, nMessageID, pszClass, pszInstance, COMMAND_STATUS_SUCCESS);
}

启动 LDicomNet 及启动监听

MainWindow 的构造函数里调用 LDicomNet::StartUp() 来启动 LDicomNet。

启动监听是通过调用 SCPServer 实例的 Listen() 函数完成的。整个启动监听的过程,封装成了一个名为 startListen() 的函数,便于点击【启动监听】按钮时调用。

MainWindow 类头文件定义如下:

#pragma once#include <QMainWindow>
#include <QStandardItemModel>
#include "SCPServer.h"QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private slots:void on_btnListen_clicked();void on_btnBrowse_clicked();private:Ui::MainWindow *ui;QStandardItemModel m_tableModel;SCPServer* m_pServer = nullptr;void customEvent(QEvent* event);void showLog(QString strLog);bool startListen(QString strListenIP, uint nListenPort);bool stopListen();
};

MainWindow 构造函数代码如下:

#define LTV17_CONFIG
#include "L_Bitmap.h"
#include "Ltdic.h"
#include "classlib\LtWrappr.h"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);this->setWindowTitle("DICOM C-STORE SCP Demo");ui->cmbAddress->addItem("127.0.0.1");QHostInfo hostInfo = QHostInfo::fromName(QHostInfo::localHostName());for (QHostAddress address : hostInfo.addresses()) {if (address.protocol() == QAbstractSocket::IPv4Protocol) {ui->cmbAddress->addItem(address.toString());}}QString picturesLocation = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);ui->edtImagePath->setText(picturesLocation);ui->tableView->setModel(&m_tableModel);QStringList tableHeaders;tableHeaders.append("时间");tableHeaders.append("信息");m_tableModel.setHorizontalHeaderLabels(tableHeaders);ui->tableView->setColumnWidth(0, 180);ui->tableView->setColumnWidth(1, 1000);ui->tableView->setSelectionBehavior(QAbstractItemView::SelectRows);ui->tableView->setSelectionMode(QAbstractItemView::SelectionMode::SingleSelection);ui->tableView->setEditTriggers(QAbstractItemView::NoEditTriggers);// leadtools network 初始化UNLOCKSUPPORT();LBase::LoadLibraries(LT_DIS | LT_FIL | LT_IMG | LT_KRN);L_INT nError = LDicomNet::StartUp();if (nError != DICOM_SUCCESS) {QString dcmError = DicomError::instance().text(nError);QString strError = QString("初始化 DICOM Network 失败,原因:%1").arg(dcmError);showLog(strError);QMessageBox::critical(this, "错误", strError);}
}

注意这里使用了 QHostInfoQHostAddress 类,需要在项目文件 CStoreSCPDemo.pro 里添加 network 模块,否则无法编译通过。

startListen 函数代码如下:

bool MainWindow::startListen(QString strListenIP, uint nListenPort)
{QString strInfo = QString("正在启动 DICOM 监听(IP: %1,Port: %2)...").arg(strListenIP).arg(nListenPort);showLog(strInfo);m_pServer = new SCPServer();if (m_pServer == nullptr) {QString strError = QStringLiteral("创建 SCP Server 对象失败。");showLog(strError);QMessageBox::critical(this, "错误", strError);return false;}m_pServer->registerLogger(this);m_pServer->setImageFolder(ui->edtImagePath->text());m_pServer->setServerIPPort(strListenIP, nListenPort);L_TCHAR* pszListenIP = (L_TCHAR*)reinterpret_cast<const L_TCHAR*>(strListenIP.utf16());int nRet = m_pServer->Listen(pszListenIP, nListenPort, 5);if (nRet != DICOM_SUCCESS) {QString dcmError = DicomError::instance().text(nRet);QString strError = QString("启动监听(%1:%2)失败,原因:%3。").arg(strListenIP).arg(nListenPort).arg(dcmError);showLog(strError);delete m_pServer;m_pServer = nullptr;QMessageBox::critical(this, "错误", strError);return false;}return true;
}

编译程序

编译程序前,先在项目文件 CStoreSCPDemo.pro 里添加下面三个选项:

MOC_DIR = temp/moc
UI_DIR = temp/ui
OBJECTS_DIR = temp/obj

默认设置,编译过程中生成的临时文件会与可执行文件放在同一个文件夹下,加上这三个选项后,会把临时文件分类放在不同的目录里。

点击界面左下角的【锤子】图标,编译程序。

我们会发现,在【问题】窗口出现很多错误提示,大致有下面几种:

  • C1057: 宏扩展中遇到意外的文件结束
  • C2001: 常量中有换行符
  • C2143: 语法错误: 缺少“)”(在“}”的前面)
  • C2146: 语法错误: 缺少“)”(在标识符“SendAssociateReject”的前面)
  • C2664: “L_VOID L_UnlockSupport(L_UINT,L_TCHAR *)”: 无法将参数 2 从“const wchar_t [11]”转换为“L_TCHAR *”

错误虽然很多,但绝大部分(C1057、C2001、C2143、C2146)都是字符编码导致的问题,原因在于我们使用了 MSVC 编译器。

MSVC 编译器通过源码文件的 BOM 头识别字符编码,如果源码文件没有 BOM 头,那么就取操作系统的默认字符编码。
这样问题就来了,Qt Creator 生成的文件,默认是没有 BOM 头的,MSVC 检测不到 BOM 头,就取了操作系统的默认字符编码。
本程序的开发环境是 Windows 11 中文版,所有 Windows 中文版的默认字符编码都是 GBK,所以 MSVC 使用了 GBK 作为字符编码来编译程序。而 Qt Creator 生成的文件,默认字符编码是 UTF-8,所以产生了错误。
对于英文来说,GBK 与 UTF-8 的字符编码是相同的,不会产生错误,因此我们会发现,出现错误的地方,都是有中文的地方。

要解决这个错误,我们可以修改 Qt Creator 的 UTF-8 BOM 选项,给所有文件都加上 BOM 头。
通过菜单【工具 > 选项】打开选项窗口,再选择【文本编辑器 > Behavior】,将 UTF-8 BOM 选项的值改为 如果编码是UTF-8则添加

这样修改后,对于有错误的文件,还要重新保存,以便生成 BOM 头。然后再重新编译程序。

这个方法虽然能够解决问题,但如果有错误的文件太多的话,逐个重新保存也很麻烦。而且修改了 Qt Creator 的选项后,不只影响当前项目,对于以后编写的程序,都会产生影响。所以不推荐这个做法。

除了上面的方法外,我们还可以通过在项目文件 CStoreSCPDemo.pro 里添加 QMAKE 选项来解决问题:

QMAKE_CXXFLAGS += /utf-8

添加上面这个选项后,再重新编译程序,会发现只剩 C2664 错误了。这个错误也可以通过添加编译器选项解决:

QMAKE_CXXFLAGS -= -Zc:strictStrings

运行程序

点击界面左下角的绿色【箭头】图标,运行程序,会出现这样的提示:

共有 Ltdicu.dll、Ltkrnu.dll、Ltwvcu.dll 三个文件找不到。
我们到 E:\LeadTools 17\Bin\CDLL\Win32 下面找到这三个文件,然后复制到 D:\build-CStoreSCPDemo-Desktop_Qt_5_15_2_MSVC2019_32bit-Debug\debug 目录里,再重新运行程序。

发布与部署

上面运行程序的方法,是直接在 Qt Creator 里启动程序。而实际发布程序,是不可能带着开发环境一起发布的。
真正发布与部署程序,我们都是将编译好的 .exe 文件与所需的 Qt 运行库一同发布,执行程序时,也是直接运行 .exe 文件。

那么问题来了,Qt 运行库有很多 DLL 文件,我们怎么知道到底需要哪些 DLL 呢?其实对于这个问题,Qt 提供了一个很好用的小工具:Qt Windows Deploy Tool

打开 命令提示符 窗口,进入 C:\Qt\5.15.2\msvc2019\bin 文件夹,执行 windeployqt.exe D:\build-CStoreSCPDemo-Desktop_Qt_5_15_2_MSVC2019_32bit-Debug\debug\CStoreSCPDemo.exe 命令,Qt 部署工具会检查目标文件的依赖关系,然后将所依赖的 Qt 库文件复制到目标文件夹。

测试程序

程序编译通过,仅是完成了第一步,程序是不是好用,有没有错误,还是要经过测试才知道。
本程序是一个 C-Store SCP,用于接收 C-Store SCU 发送来的图像。所以我们需要找一个 C-Store SCU 程序,真正的发送图像试一试。

这里我们使用 DCMTK 附带的工具 storescu.exe,下载地址:https://dicom.offis.de/download/dcmtk/dcmtk367/bin/dcmtk-3.6.7-win32-dynamic.zip
下载后,将 dcmtk-3.6.7-win32-dynamic.zip 解压到 D:\dcmtk-3.6.7-win32-dynamic 文件夹。

测试还需要图像,我们使用《第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)》一文所使用的图像,下载地址 https://download.csdn.net/download/blackwoodcliff/13193834
图像下载后,解压到 E:\MR20200821 文件夹。

准备工作完成后,就可以开始测试了。

首先,双击 D:\build-CStoreSCPDemo-Desktop_Qt_5_15_2_MSVC2019_32bit-Debug\debug\CStoreSCPDemo.exe 运行程序,点击界面上的【启动监听】按钮,启动 DICOM 监听。

然后,打开 命令提示符 窗口,进入 D:\dcmtk-3.6.7-win32-dynamic\bin 文件夹,输入下面命令:

storescu.exe 127.0.0.1 6666 E:\MR20200821\MR01010001.dcm --propose-lossless

CStoreSCPDemo.exe 收到 storescu.exe 的请求,响应如下:

说明:上面的 storescu 命令里指定了传输语法 --propose-lossless,这是因为我们所用的测试图像的 Transfer Syntax UID 是 1.2.840.10008.1.2.4.70 (JPEG Lossless, Nonhierarchical, First - Order Prediction (Processes 14[Selection Value 1]))

上面的测试,只发送了一幅图像。我们也可以同时发送指定文件夹里的全部图像,命令如下:

storescu.exe 127.0.0.1 6666 --scan-directories E:\MR20200821 --propose-lossless

界面美化

其实这一步可有可无,上面的程序,功能已经很完整了。但鉴于 QSS 简单易用,我们就再做点锦上添花的工作。

编写一个 QSS 文件,命名为 style.qss,内容如下:

QMainWindow {background-color: #07304f;
}QPushButton {border-radius: 4px;background-color: #092542;border: 1px solid #2dbdff;color: white;padding-top: 6px;padding-bottom: 6px;padding-left: 12px;padding-right: 12px;font-family: Microsoft YaHei;
}QPushButton:hover {background-color: #2dbdff;
}/* pressed 必须放在 hover 之后,否则 QComboBox::drop-down 会使 QPushButton:pressed 失效 */
QPushButton:pressed {background-color: #0b5ed7;color: white;
}QLineEdit {border: 2px solid #07304f;border-radius: 4px;padding: 3px 6px;font-family: Microsoft YaHei;
}QLineEdit:focus {border: 2px solid #2dbdff;
}QComboBox {border: 2px solid #07304f;border-radius: 4px;padding: 3px 6px;font-family: Microsoft YaHei;
}QComboBox:focus {border: 2px solid #2dbdff;
}QComboBox::drop-down {subcontrol-origin: padding;subcontrol-position: top right;width: 15px;border-left-width: 1px;border-left-color: darkgray;border-left-style: solid;border-top-right-radius: 3px;border-bottom-right-radius: 3px;
}QComboBox::down-arrow {image: url(arrow_down.svg);
}

修改 main.cpp,在 QApplication a(argc, argv); 一行下面,添加如下代码:

    QFile styleFile("style.qss");if (styleFile.open(QIODevice::ReadOnly|QIODevice::Text)) {QString styleSheet(styleFile.readAll());qApp->setStyleSheet(styleSheet);styleFile.close();}

重新编译并运行程序,效果如下:

源码下载

  • 程序源码:https://download.csdn.net/download/blackwoodcliff/87367108
  • DICOM 图像:https://download.csdn.net/download/blackwoodcliff/13193834

参考

  • DICOM医学图像处理:DICOM网络传输
  • LDicomNet
  • LDicomAssociate
  • Creating a DICOM Network Connection
  • Creating an SCP

DICOM 图像传输:使用 LeadTools 实现 C-Store SCP 服务相关推荐

  1. Sharepoint2013商务智能学习笔记之Secure Store Service服务配置(二)

    Secure Store Service 是运行在应用程序服务器上的授权服务,它提供一个存储用户凭据的数据库,Secure Store Service 在商务智能中的地位很重要,Sharepoint商 ...

  2. iOS APP开发者福音:苹果启动新App Store订阅服务

    去年,苹果向iOS APP开发者承诺,他们将会改变其App Store订阅服务的规则,对于那些拥有长期订阅者的开发者来说,他们将获得更多的应用营收分成.如今,苹果已经兑现了这个承诺,他们已经开始实施计 ...

  3. linux怎么安装scp服务,linux下ssh安装与scp命令使用详解

    ubuntu默认并没有安装ssh服务,可以通过如下命令进行: 复制代码 代码如下: yblin@yblin-desktop:~$ ssh localhost ssh: connect to host ...

  4. 关于ubuntu系统的scp服务提示Permission denied

    网上总共提到了三个地方 首先第一点,修改/etc/ssh/sshd_config文件中的PermitRootLogin prohibit-password 改为PermitRootLogin yes( ...

  5. 【转】DICOM 网关的设计与实现

    何 博 曹晓光 杜振洲 (北京航空航天大学图像中心 北京     100083) DICOM 网关是医学图像存档与通信系统(PACS) 的关键部分,用于接收.存储.转发DICOM 医学图像,实现了DI ...

  6. 【转】dicom通讯的工作方式及dicom标准简介!!

    转自:dicom通讯的工作方式及dicom标准简介 - assassinx - 博客园 本文主要讲述dicom标准及dicom通讯的工作方式.dicom全称医学数字图像与通讯 其实嘛就两个方面 那就是 ...

  7. [医疗信息化][DICOM教程]DICOM标准简介

    [医疗信息化][DICOM教程]DICOM标准简介 使用OsiriX的DICOM标准简介 内容 介绍 什么是DICOM 医院系统内的图像传输 了解DICOM服务 OsiriX提供的DICOM服务 其他 ...

  8. DICOM 开发工具总结

    网上流行的DICOM协议开发工具: 1.DICOM开发类库主要有: (1).DCMTK(3.6.0), 官方下载网站,(如何安装编译DCMTK3.6.0) DCMTK实现了对DICOM图像存储.传输. ...

  9. 【转】DICOM开发工具总结

    转自:DICOM开发工具总结_qimo601的专栏-CSDN博客 网上流行的DICOM协议开发工具: 1.DICOM开发类库主要有: (1)DCMTK(3.6.0), 官方下载网站,(如何安装编译DC ...

最新文章

  1. 每日一皮:你不知道你的骑手为了给你送餐要经历什么...
  2. 博图注册表删除方法_技成周报40期 | 三菱、西门子软件安装常见出错解决方法...
  3. 用C语言用指针怎么算通用定积分,C语言:利用指针编写程序,用梯形法计算给定的定积分实例...
  4. [C++STL]常用集合算法
  5. java list遍历添加元素_java遍历List过程中添加和删除元素的问题
  6. 掘金外链即将失效?论如何用脚本一次性下载/替换失效的外链图片
  7. leetcode刷题:二叉树的中序遍历
  8. XML DOM学习笔记(JS)
  9. SQL Server DATEDIFF() 函数
  10. Java并发编程之美系列汇总
  11. 简单易懂的ueditor新手教程
  12. ROS联合Webots之实现趣味机器人巡线刷圈
  13. 计算机机房运行环境条件要求,机房环境都有哪些要求
  14. 使用VS2015+win7编译WebKit(WebKit-r189384)
  15. python自学爬虫要多久_自学python爬虫需要多久
  16. echarts组织架构图
  17. Word控件Spire.Doc 【文本】教程(17) ;在Word中设置文本方向
  18. 前端url编码解码方法
  19. python是什么意思中文、好学吗-python好学吗
  20. PCB 多层板为什么都是偶数层?

热门文章

  1. SQL 多表查询例题
  2. linux命令之unzip
  3. Gitlab 设置页面语言为简体中文
  4. 数据库中索引的填充因子
  5. 酷炫浪漫表白页面(附代码)HTML5代码类资源
  6. web高德地图路线规划(多条)
  7. 笔记本nc10装linux,三星nc10笔记本如何设置U盘启动
  8. B. Pleasant Pairs
  9. python 跳跃游戏
  10. 共阴数码管段码-共阳数码管段码