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

news2024/9/25 2:26:24

文章目录

  • 开发环境
  • 创建 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_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    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);

    // 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();
    }

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

在这里插入图片描述

参考

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/140529.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Linux】进程间通信(万字详解) —— 上篇

&#x1f387;Linux&#xff1a; 博客主页&#xff1a;一起去看日落吗分享博主的在Linux中学习到的知识和遇到的问题博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a; 看似不起波澜的日复一日&#xff0c;一定会在某一天让你看见坚持…

谷粒学院——第九章、阿里云视频点播

阿里云视频点播 开通 地址&#xff1a; 上传测试 开通以后&#xff0c;点击控制台&#xff0c;然后选择音/视频&#xff1a; 注意&#xff1a;先点击启用存储地址再上传。 添加转码模版&#xff1a; 开发文档 官方地址&#xff1a;https://help.aliyun.com/p…

批量统计不同块的数量

CAD收集块的数量一般采用FI等命令&#xff0c;或者使用天正等软件&#xff0c;这些方法或多或少都存在某些问题。这时就可以编写插件满足不同场景的使用。已应用到实际工作中。 一、界面及其功能 采用c#制作cad插件&#xff0c;框选待统计范围&#xff0c;直接输出到表格中&…

数据库连接超时的处理

报错信息&#xff1a;Caused by: java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms.at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:695)at com.zaxxer.hikari.pool.H…

Java高手速成 | 数据库实训:图书馆管理系统建模

图书馆管理系统是常见的管理信息系统&#xff0c;考虑到系统的推广性&#xff0c;本系统采用SQL SERVER2000作为数据库。并且采用PowerDesigner进行数据建模&#xff0c;从而自动生成sql脚本。 01、数据库概念设计 1. 数据库表设计 管理员表admin&#xff1a;管理员编号&am…

[JavaEE] volatile与wait和notify

专栏简介: JavaEE从入门到进阶 题目来源: leetcode,牛客,剑指offer. 创作目标: 记录学习JavaEE学习历程 希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长. 学历代表过去,能力代表现在,学习能力代表未来! 目录 一.volatile 关键字. 1.volatile 能保证内存可见性…

12个爆款 Java 开源项目

1JavaGuidehttps://github.com/Snailclimb/JavaGuide Star 10503【Java学习面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识。2symphonyhttps://github.com/b3log/symphony Star 6664一款用 Java 实现的现代化社区&#xff08;论坛/BBS/社交网络/博客&#xff09;平台…

17-Golang中的包

Golang中的包基本介绍包的三大作用相关说明包的注意事项和细节说明基本介绍 1.在实际的开发中&#xff0c;我们往往需要在不同的文件中&#xff0c;去调用其他文件的定义的幻术&#xff0c;比如main.go中&#xff0c;去使用utils.go文件中的函数2.包的本质就是创建不同的文件夹…

vue简单的数据传输

很久没有水文了&#xff0c;最近又得了新冠才好&#xff0c;学习也没什么进度&#xff0c;先复习下之前的组件的数据传输吧&#xff01; props传值 这个很简单就是在组件标签上转递数据&#xff0c;值得注意的是如果不使用v-bind:&#xff08;&#xff1a;&#xff09;,转递的…

任务二:Web隐藏信息获取

任务二:Web隐藏信息获取 任务环境说明: 服务器场景名:web20200604服务器场景用户名:未知通过本地PC中渗透测试平台Kali使用Nmap扫描目标靶机HTTP服务子目录,将扫描子目录命令所需参数及第四条扫描结果关键目录以&符号拼接,作为Flag提交(例:-p 22&/root/); …

【信息学CSP-J近16年历年真题64题】真题练习与解析 第11题之纪念品

纪念品 描述 小伟突然获得一种超能力,他知道未来 T 天 N 种纪念品每天的价格。某个纪念品 的价格是指购买一个该纪念品所需的金币数量,以及卖出一个该纪念品换回的金币数量。 每天,小伟可以进行以下两种交易无限次: 任选一个纪念品,若手上有足够金币,以当日价格购买该…

Android---DrawerLayout + NavigationView

现在 Android Studio 已经直接提供左滑菜单功能&#xff0c;只需要在创建新项目时选择 Navigation Drawer Activity 就可以直接创建一个有左滑菜单功能的 APP。 目录 DrawerLayout NavigationView android:src 与 app:srcCompat fitsSystemWindows DrawerLayout …

数据报告重要的是业务看得懂

一、前言上篇文章我们从指标库的角度梳理了指标是如何计算出来的&#xff0c;确保业务人员有数可寻&#xff0c;但对于业务人员而言&#xff0c;并不是所有的指标都看得懂&#xff0c;也不是所有的指标都需要理解&#xff0c;笔者见过太多不知所云的数据报告&#xff0c;各种模…

Maven高级-分模块拆分

Maven高级 分模块开发与设计 聚合 继承 属性 版本管理 资源配置 多环境开发配置 跳过测试 私服 分模块开发与设计 拷贝原始项目中对应的相关内容到ssm_pojo模块中 ​ 实体类&#xff08;User&#xff09; ​ 配置文件&#xff08;无&#xff09; User.java package com.i…

S32K144-S32DS 导入/编译/烧录 遇到的问题

找到你开发套件软件安装包的路径&#xff0c;里面有四个例子&#xff0c;任选一个&#xff1b;不要忘记复制到工作区 问题一&#xff1a;修改电机库的路径 问题二&#xff1a;freemaster路径问题 编译还是报错&#xff0c;好像还必须安装FreeMaster 安装之后&#xff0c;还是这…

电子招标采购系统源码:构建高效智能数字化采购

过去几十年&#xff0c;公用事业行业发生了重大变化。能源需求的转变导致企业利润率的波动&#xff0c;但不是运营成本的波动。 许多公用事业公司通过后勤部门流程自动化来削减成本&#xff0c;比如招采流程自动化。 在招采活动中&#xff0c;人工招采会产生盲点。由于公共事业…

【MySQL】MySQL视图原理与实战(MySQL专栏启动)

&#x1f4eb;作者简介&#xff1a;小明java问道之路&#xff0c;专注于研究 Java/Liunx内核/C及汇编/计算机底层原理/源码&#xff0c;就职于大型金融公司后端高级工程师&#xff0c;擅长交易领域的高安全/可用/并发/性能的架构设计与演进、系统优化与稳定性建设。 &#x1f4…

ArcGIS基础实验操作100例--实验51CAD转要素类

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 高级编辑篇--实验51 CAD转要素类 目录 一、实验背景 二、实验数据 三、实验步骤 &#xff08;1&#…

多线程之线程安全问题

1.线程安全示例 class Count{int a 0;public void add(){a;} } public class ThreadDemo8 {public static void main(String[] args) {Count count new Count();Thread t1 new Thread(()->{for (int i 0; i < 5_0000; i) {count.add();}});Thread t2 new Thread(()…

这款企业报表工具给你灵活的数据查询体验

随着对BI应用程度的加深&#xff0c;用户需要连接和管理的数据越来越多&#xff0c;也越来越复杂。电子表格软件支持丰富的数据源接入&#xff0c;但一般并不能直接使用接入的业务库直接进行数据分析。所以在报表开发前的取数过程&#xff0c;把需要的数据整合成一个数据集合&a…