第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)

开发环境

  • DCMTK 3.6.4
  • Qt 5.14.2
  • Visual Studio 2015
  • Qt Visual Studio Tools 2.6.0

DCMTK 使用 《编译 DCMTK DLL(DCMTK 3.6.4 + VS2015 + Win10)》 一文所生成的 DLL。

Qt 下载地址:http://download.qt.io/archive/qt/
从 Qt 5.15 版本开始,开源版的 Qt 已经不提供离线安装包了,仅支持在线安装。
本文使用 Qt 5.14.2,是最后一个提供离线安装包的开源版。
Qt 5.14.2 离线安装包的下载页面:
http://download.qt.io/archive/qt/5.14/5.14.2/qt-opensource-windows-x86-5.14.2.exe.mirrorlist
可选择国内镜像下载,可提高下载速度。
安装 qt-opensource-windows-x86-5.14.2.exe 时会要求登录 Qt 账号,如果没有 Qt 账号或不想登录,可先断网再安装,就不会要求登录了。

Qt Visual Studio Tools 2.6.0 下载地址:http://download.qt.io/archive/vsaddin/2.6.0/

Visual Studio Qt 选项

启动 Visual Studio,点击菜单【Qt VS Tools > Qt Options】,打开 Qt 选项窗口。确保存在如图设置。

在这里插入图片描述
如果 Visual Studio 没有 【Qt VS Tools】菜单,说明没有安装 Qt Visual Studio Tools。

创建 Qt Widgets 程序

点击菜单【文件 > 新建 > 项目】,打开“新建项目”窗口,选择 Qt Widgets Application

在这里插入图片描述
输入项目名称,点击【确定】后,进入 Qt Widgets Application Wizard 界面。

在这里插入图片描述
在这里插入图片描述
点击【Finish】按钮创建项目。创建完成的项目如下图:

在这里插入图片描述

设计界面

双击 DcmtkDemo.ui 文件,打开 Qt 设计师。

向窗体上拖拽一个 Vertical Layout 控件和一个 Graphics View 控件,Vertical Layout 放左侧,Graphics View 放右侧。
按住 Ctrl 键,同时选中 Vertical LayoutGraphics View,然后点击工具条上的【使用分裂器水平布局】按钮。如下图:

在这里插入图片描述
然后点击窗体空白处,再点击工具条上的【水平布局】按钮,使控件充满整个窗体。如下图:

在这里插入图片描述
再依次向 Vertical Layout 里拖拽 Push ButtonList WidgetProgress Bar 三个控件,从上到下顺序排列。

  • 设置 Push ButtonobjectNamebtnOpenFoldertext打开文件夹
  • 设置 List WidgetobjectNamelstSeriesList
  • 设置 Progress Barvalue0,取消勾选 textVisible
  • 选中 Vertical Layout,点击右键,选择菜单【变型为 > QFrame】,设置 minimumSize 宽度为 300maximumSize 宽度为 500

在这里插入图片描述
设计完成的界面结构如下图:

在这里插入图片描述

配置 DCMTK 路径

编写代码前,应先配置好 DCMTK 的 include 路径,免得编码时提示找不到 DCMTK 头文件。

DCMTK 由《编译 DCMTK DLL(DCMTK 3.6.4 + VS2015 + Win10)》生成,安装路径位于 D:\dcmtk-3.6.4-install

DcmtkDemo 项目名称上点击鼠标右键,在弹出菜单上选择 [属性],打开项目属性页对话框。选择[配置属性 > VC++ 目录],在[包含目录]里添加 D:\dcmtk-3.6.4-install\include

在这里插入图片描述
一般在添加 include 目录时,也会顺手把 lib 文件添加上。但 lib 文件编译程序前加上就可以,此处先不添加。

编写代码

代码主要由下面几部分组成:

  • 界面交互代码
  • DICOM 文件读取代码
  • 图像数据模型类
  • 序列数据模型类
  • 图像显示视图类

图像数据模型类

我们将定义一个 ImageData 类,用于表示图像数据模型。一个 ImageData 对象,表示一幅图像。

ImageData 类是本程序的一个核心类。这个类主要有两个功能:

  • 调用 DCMTK 库,使用 DcmFileFormat 从磁盘读取一个 DICOM 文件,生成 DicomImage 对象。
  • DicomImage 对象转换成 QPixmap 对象,交由图像视图类显示。

ImageData 类有三个核心函数:readDicomFile()dicomImageToPixmap()ucharArrayToPixmap()

  • readDicomFile() 用于读取 DICOM 头信息,及生成 DicomImage 对象。
  • dicomImageToPixmap()ucharArrayToPixmap() 是两个静态函数,通过调用 DicomImage 类的 createWindowsDIB() 函数获得 BMP 位图数据,然后转换成 QPixmap 对象。

ImageData 类头文件定义如下:

#pragma once
#include <QString>
#include <QPixmap>

#include "dcmtk/dcmdata/dcfilefo.h"
#include "dcmtk/dcmimgle/dcmimage.h"

class ImageData
{
public:
	explicit ImageData(const QString &filename);
	~ImageData();

	//判断是否读取成功
	bool isNormal() const
	{
		return _pDcmImage && (_pDcmImage->getStatus() == EIS_Normal);
	}

	QString getSeriesUID() const
	{
		return _seriesUID;
	}
	int getInstanceNumber()
	{
		return _instanceNumber.toInt();
	}
	//获取窗宽窗位
	void getWindow(double &center, double &width) const
	{
		center = _winCenter; width = _winWidth;
	}
	//设置窗宽窗位
	void setWindow(const double &center, const double &width)
	{
		_winCenter = center; _winWidth = width;
	}

	bool getPixSpacing(double & spacingX, double & spacingY, double & spacingZ) const;
	bool getPixmap(QPixmap & pixmap); //得到该图像的位图 pixmap

	static bool dicomImageToPixmap(DicomImage & dcmImage, QPixmap & pixmap);
	static bool ucharArrayToPixmap(uchar *data, int w, int h, int bitSize, QPixmap & pixmap, int biBitCount = 8);

private:
	QString _filename;
	DcmFileFormat _dcmFile;
	DicomImage* _pDcmImage;

	//图像所属序列的唯一标识
	QString _seriesUID;
	//图像在序列中的编号(位置)
	QString _instanceNumber;
	//像素间距,用于显示比例
	double _spaceX, _spaceY, _spaceZ;
	//窗宽、窗位
	double _winWidth, _winCenter;
	//图像宽度、高度
	int _imageWidth, _imageHeight;

	void readDicomFile(const QString &filename);
};

核心函数定义如下:

void ImageData::readDicomFile(const QString &filename)
{
	OFCondition oc = _dcmFile.loadFile(OFFilename(filename.toLocal8Bit()));
	if (oc.bad())
	{
		qDebug() << "Fail:" << oc.text();
		return;
	}

	_filename = filename;

	DcmDataset *dataset = 0;
	OFCondition result;
	const char *value = nullptr;
	if (!(dataset = _dcmFile.getDataset()))
		return;

	// 头信息读取
	result = dataset->findAndGetString(DCM_SeriesInstanceUID, value);
	if (result.bad())
		return;
	_seriesUID = QString::fromLatin1(value);

	result = dataset->findAndGetString(DCM_InstanceNumber, value);
	if (result.bad())
		return;
	_instanceNumber = QString(value);

	result = dataset->findAndGetFloat64(DCM_PixelSpacing, _spaceX, 1);
	if (result.bad())
		_spaceX = 1;

	result = dataset->findAndGetFloat64(DCM_PixelSpacing, _spaceY, 0);
	if (result.bad())
		_spaceY = 1;

	result = dataset->findAndGetFloat64(DCM_SliceThickness, _spaceZ);
	if (result.bad())
		_spaceZ = 1;

	result = dataset->findAndGetFloat64(DCM_WindowWidth, _winWidth);
	result = dataset->findAndGetFloat64(DCM_WindowCenter, _winCenter);

	// 创建DcmImage
	/********解压缩**********/
	std::string losslessTransUID = "1.2.840.10008.1.2.4.70";
	std::string lossTransUID = "1.2.840.10008.1.2.4.51";
	std::string losslessP14 = "1.2.840.10008.1.2.4.57";
	std::string lossyP1 = "1.2.840.10008.1.2.4.50";
	std::string lossyRLE = "1.2.840.10008.1.2.5";

	E_TransferSyntax xfer = dataset->getOriginalXfer();
	const char*	transferSyntax = nullptr;
	_dcmFile.getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, transferSyntax);

	if (transferSyntax == losslessTransUID || transferSyntax == lossTransUID ||
		transferSyntax == losslessP14 || transferSyntax == lossyP1)
	{
		//对压缩的图像像素进行解压
		DJDecoderRegistration::registerCodecs();
		dataset->chooseRepresentation(EXS_LittleEndianExplicit, nullptr);
		DJDecoderRegistration::cleanup();
	}
	else if (transferSyntax == lossyRLE)
	{
		DcmRLEDecoderRegistration::registerCodecs();
		dataset->chooseRepresentation(EXS_LittleEndianExplicit, nullptr);
		DcmRLEDecoderRegistration::cleanup();
	}

	_pDcmImage = new DicomImage(&_dcmFile, dataset->getOriginalXfer(), CIF_TakeOverExternalDataset);
	if (_pDcmImage->getStatus() == EIS_Normal)
	{
		_imageWidth = _pDcmImage->getWidth();
		_imageHeight = _pDcmImage->getHeight();
		if (_winWidth < 1)
		{
			// 设置窗宽窗位
			_pDcmImage->setRoiWindow(0, 0, _imageWidth, _imageHeight);
			// 重新对winCenter, winWidth赋值
			_pDcmImage->getWindow(_winCenter, _winWidth);
		}
	}
}
bool ImageData::dicomImageToPixmap(DicomImage& dcmImage, QPixmap & pixmap)
{
	bool res = true;

	void *pDIB = nullptr;
	int size = 0;
	if (dcmImage.isMonochrome())
	{
		// 灰度图像
		size = dcmImage.createWindowsDIB(pDIB, 0, 0, 8, 1, 1);
		if (!pDIB)
			return false;

		res = ucharArrayToPixmap((uchar *)pDIB, dcmImage.getWidth(), dcmImage.getHeight(), size, pixmap);
	}
	else
	{
		// RGB图像
		size = dcmImage.createWindowsDIB(pDIB, 0, 0, 24, 1, 1);
		if (!pDIB)
			return false;

		res = ucharArrayToPixmap((uchar *)pDIB, dcmImage.getWidth(), dcmImage.getHeight(), size, pixmap, 24);
	}
	delete pDIB;
	return res;
}
bool ImageData::ucharArrayToPixmap(uchar *data, int w, int h, int bitSize, QPixmap & pixmap, int biBitCount)
{
	//位图文件由四部分依序组成:BITMAPFILEHEADER,BITMAPINFOHEADER,调色板,Image Data。
	BITMAPFILEHEADER lpfh;// 文件头  固定的14个字节, 描述文件的有关信息
	BITMAPINFOHEADER lpih;// 固定的40个字节,描述图像的有关信息

	RGBQUAD palette[256];// 调色板RGBQUAD的大小就是256
	memset(palette, 0, sizeof(palette));
	for (int i = 0; i < 256; ++i) {
		palette[i].rgbBlue = i;
		palette[i].rgbGreen = i;
		palette[i].rgbRed = i;
	}

	memset(&lpfh, 0, sizeof(BITMAPFILEHEADER));
	lpfh.bfType = 0x4d42;//'B''M' must be 0x4D42.

	//the sum bits of BITMAPFILEHEADER,BITMAPINFOHEADER and RGBQUAD;the index byte of the image data.
	lpfh.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + sizeof(palette);

	memset(&lpih, 0, sizeof(BITMAPINFOHEADER));
	lpih.biSize = sizeof(BITMAPINFOHEADER); //the size of this struct. it is 40 bytes.
	lpih.biWidth = w;
	lpih.biHeight = h;
	lpih.biCompression = BI_RGB;
	lpih.biPlanes = 1; //must be 1. 

	void *pDIB = data;
	int size = bitSize;
	lpih.biBitCount = biBitCount;

	//the size of the whole bitmap file.
	lpfh.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + sizeof(palette) + size;

	QByteArray bmp;
	bmp.append((char*)&lpfh, sizeof(BITMAPFILEHEADER));
	bmp.append((char*)&lpih, sizeof(BITMAPINFOHEADER));
	bmp.append((char*)palette, sizeof(palette));
	bmp.append((char*)pDIB, size);

	return pixmap.loadFromData(bmp);
}

序列数据模型类

定义一个 SeriesData 类,其主要目的是为了表示属于同一个序列的图像集合。

SeriesData 类头文件定义如下:

#pragma once
#include <QList>
#include <QMap>
#include "ImageData.h"

class SeriesData
{
public:
	explicit SeriesData(const QString seriesUID);
	~SeriesData();

	static QList<SeriesData*> SeriesList;

	QMap<int, ImageData*> images;

	QString getSeriesUID() const
	{
		return _seriesUID;
	}
	// 窗宽窗位
	void getDefaultWindow(double &center, double &width) const
	{
		center = _defaultCenter; width = _defaultWidth;
	}

	void appendImage(ImageData* pImage);

private:
	QString _seriesUID;
	double _defaultCenter, _defaultWidth;  //默认窗位窗宽
};

读取 DICOM 文件

读取磁盘文件是一个耗时的操作,为了防止界面失去响应,一般这种 IO 操作都放在单独的线程里执行。

本来读取文件只需写一个函数就可以,但为了使用 QObject::moveToThread() 函数实现多线程,我们将定义一个 ReadWorker 类来读取 DICOM 文件。

DcmtkDemo 项目名称上点击鼠标右键,在弹出菜单上选择 [添加 > Add Qt Class…],打开 Add Class 对话框。

在这里插入图片描述
选择 Qt Class,输入类名称,然后点击【Add】按钮。

在这里插入图片描述
无须改变向导页的信息,点击【Finish】按钮,添加 ReadWorker 类。

ReadWorker 类主要实现了一个槽和两个信号,头文件定义如下:

#pragma once

#include <QObject>

class ReadWorker : public QObject
{
	Q_OBJECT

public:
	ReadWorker(QString dicomFolder, QObject *parent = nullptr);
	~ReadWorker();

public slots:
	void readDicomFiles();  //检查文件夹下Dicom序列个数,应在单独的线程运行

signals:
	void progress(int);		//发送进度(0-100)
	void finish();			//线程结束信号

private:
	QString _dicomFolder;	//包含多个 Dicom 单文件的文件夹,可能有多个序列。
};

readDicomFiles() 定义如下:

void  ReadWorker::readDicomFiles()
{
	SeriesData::SeriesList.clear();

	QDir dir(_dicomFolder);
	QStringList files = dir.entryList(QDir::Files);
	int filesCount = files.count(); //所有文件的个数

	ImageData* pImgData = nullptr;  //单Dicom文件

	int progressValue = 0;
	QString seriesUID;

	//遍历每个文件
	foreach(QString fileName, files)
	{
		++progressValue;
		emit progress(100 * progressValue / filesCount/* 转到0-100之间*/);

		//带路径的文件名
		fileName = _dicomFolder + "/" + fileName;

		//使用我们写的ImageData类读取dicom
		pImgData = new ImageData(fileName);
		if (!pImgData->isNormal())
		{
			delete pImgData;
			pImgData = nullptr;
			continue;  //读取不成功(不是Dicom文件),跳出直接读取下一个文件
		}

		seriesUID = pImgData->getSeriesUID();  //获取该序列的 UID

		bool found = false;
		for (size_t i = 0; i < SeriesData::SeriesList.size(); i++)
		{
			if (SeriesData::SeriesList.at(i)->getSeriesUID() == seriesUID)
			{
				SeriesData::SeriesList.at(i)->appendImage(pImgData);
				found = true;
			}
		}

		if (false == found)
		{
			SeriesData* pSeriesData = new SeriesData(seriesUID);
			pSeriesData->appendImage(pImgData);
			SeriesData::SeriesList.append(pSeriesData);
		}
	}
	emit finish();
}

读取 DICOM 文件过程中,还有两个与界面相关的操作:

  • 读取文件过程中,更新进度条。
  • 读取文件完成后,打开图像。

这两个操作将在“界面交互”一节中介绍。

图像显示视图类

图像视图类封装了图像显示及所有与图像交互的动作,是本程序的核心类。

DcmtkDemo 项目名称上点击鼠标右键,在弹出菜单上选择 [添加 > Add Qt Class…],打开 Add Class 对话框。

在这里插入图片描述
选择 Qt Class,输入类名称,然后点击【Add】按钮,进入向导界面。

在这里插入图片描述
修改 Base classQGraphicsView,修改 Constructor signatureQWidget *parent,点击【Finish】按钮,添加 DicomView 类。

DicomView 类继承自 QGraphicsView,包含如下控件:

  • 一个 QGraphicsPixmapItem,用于显示图像。
  • 两个 QGraphicsSimpleTextItem,用于显示图像窗宽窗位和当前图像索引。
  • 一个 QSlider,在图像上显示一个滑动条,滑块指示当前图像索引位置,拖拽滑块可切换图像。

另外,重写了 QGraphicsViewresizeEventwheelEvent 两个函数。

重写 resizeEvent 是为了响应窗口缩放事件。当缩放窗口时,缩放图像并使图像居中显示,同时调整窗宽窗位标签和图像索引标签的位置。

重写 wheelEvent 是为了响应鼠标滚轮事件。滚动鼠标滚轮可切换图像,并改变滑块位置。

DicomView 类有两个核心函数:loadSeries()updateView(),分别用于加载序列和更新视图。

DicomView 类头文件定义如下:

#pragma once

#include <QGraphicsView>
#include <QSlider>

#include "SeriesData.h"

class DicomView : public QGraphicsView
{
	Q_OBJECT

public:
	DicomView(QWidget *parent);
	~DicomView();

	void loadSeries(QString &seriesUID);
	void updateView();

private:
	void resizePixmapItem();   //窗口缩放时,缩放图像
	void repositionAuxItems(); //窗口缩放时,改变标签位置

public slots:
	void setCurFrameItem(int);

protected:
	void resizeEvent(QResizeEvent *event);  //响应窗口缩放事件
	void wheelEvent(QWheelEvent *event);    //响应滚轮事件

private:
	QGraphicsScene* _pScene;                 //场景
	QGraphicsPixmapItem* _pPixmapItem;       //图像项 场景中的图像
	QGraphicsSimpleTextItem* _pWLValueItem;  //文本项 显示当前窗宽窗位值
	QGraphicsSimpleTextItem* _pCurFrameItem; //文本项 显示当前帧索引
	QGraphicsWidget* _pGraphicsSlider;       //滑动条
	QSlider* _pSlider;                       //控制切片位置的控件
	QGraphicsProxyWidget* _pProxyWidget;

	QString _seriesUID;
	SeriesData* _pSeriesData;
	int _currImageIndex;
	double _fixFactor;  // xspace/yspace 宽高的比例
};

DicomView 类核心函数定义如下:

void DicomView::loadSeries(QString &seriesUID)
{
	SeriesData* pSeriesData = nullptr;
	for (size_t i = 0; i < SeriesData::SeriesList.size(); i++)
	{
		if (SeriesData::SeriesList.at(i)->getSeriesUID() == seriesUID)
		{
			pSeriesData = SeriesData::SeriesList.at(i);
		}
	}

	if (nullptr == pSeriesData)
	{
		QMessageBox::critical(this, QStringLiteral("加载错误"), QStringLiteral("序列不存在。"));
		return;
	}
	if (pSeriesData->images.size() == 0)
	{
		return;
	}

	this->_seriesUID = seriesUID;
	this->_pSeriesData = pSeriesData;
	this->_currImageIndex = 0;

	double xSpacing = 0, ySpacing = 0, zSpacing = 0;
	if (pSeriesData->images.values().at(this->_currImageIndex)->getPixSpacing(xSpacing, ySpacing, zSpacing)) {
		if (xSpacing > 0.000001 && ySpacing > 0.000001) {
			double psX = xSpacing;
			double psY = ySpacing;
			_fixFactor = psY / psX;
		}
	}

	_pSlider->setMaximum(_pSeriesData->images.size() - 1);
	_pSlider->setValue(_currImageIndex);

	double winWidth, winCenter;
	_pSeriesData->getDefaultWindow(winCenter, winWidth);
	_pWLValueItem->setText(tr("W:%1, L:%2").arg(winWidth).arg(winCenter));

	_pPixmapItem->setPos(0, 0);
	_pPixmapItem->setRotation(0);
	_pPixmapItem->resetTransform();

	updateView();
	_pScene->update(_pScene->sceneRect());
}
void DicomView::updateView()
{
	if (nullptr == _pSeriesData)
	{
		return;
	}

	QPixmap pixmap;
	if (_pSeriesData->images.size() > this->_currImageIndex && _pSeriesData->images.values().at(this->_currImageIndex)->isNormal()) {
		double winWidth, winCenter;
		_pSeriesData->getDefaultWindow(winCenter, winWidth);
		_pSeriesData->images.values().at(this->_currImageIndex)->setWindow(winCenter, winWidth);
		_pSeriesData->images.values().at(this->_currImageIndex)->getPixmap(pixmap);
		_pPixmapItem->setPixmap(pixmap);
		_pPixmapItem->setTransformOriginPoint(_pPixmapItem->boundingRect().center());
		_pCurFrameItem->setText(tr("%1 / %2").arg(_currImageIndex + 1).arg(_pSeriesData->images.size()));
	}
	else {
		_pPixmapItem->setPixmap(pixmap);
		_pCurFrameItem->setText("");
		_pWLValueItem->setText("");
	}

	resizePixmapItem();
	repositionAuxItems();
}

注意:当使用 QGraphicsScene::addWidget() 函数将 QSlider 添加到 QGraphicsView 后,在 5.14.0 版本 Qt 上,会出现程序界面关闭后,程序进程不能退出的现象。后来更换 5.14.2 版本 Qt 后,此问题不再出现。

提升窗口部件

双击 DcmtkDemo.ui 文件,打开 Qt 设计师。

DcmtkDemo 窗口上,鼠标右键点击 QGraphicsView 控件,在弹出菜单上选择【提升为…】:

在这里插入图片描述
在打开的 提升的窗口部件 对话框中,在 提升的类名称 框输入 DicomView,同时下面的 头文件 框会自动输入 dicomview.h

在这里插入图片描述
点击【添加】按钮,添加到 提升的类 列表,然后点击【提升】按钮,完成提升操作。

在这里插入图片描述
提升完成之后,在 对象查看器 中,可见 Graphics View 控件的 已经变成了 DicomView

在这里插入图片描述
点击 Qt 设计师的【保存】按钮,对刚才的提升操作进行保存,然后退出 Qt 设计师。

界面交互

与图像相关的交互动作,都已经封装到 DicomView 类里了,剩余的交互动作还有:

  • 点击【打开文件夹】按钮,读取文件。
  • 双击序列列表项,打开图像。

另外,还有读取 DICOM 文件过程中,两个与界面相关的操作:

  • 读取文件过程中,更新进度条。
  • 读取文件完成后,打开图像。

由于界面 UI 定义在 DcmtkDemo 类中,所以界面交互代码自然而然也定义在 DcmtkDemo 类中。

DcmtkDemo.h 文件中添加四个槽函数,对应上面四个动作:

void on_btnOpenFolder_clicked();
void on_lstSeriesList_itemDoubleClicked(QListWidgetItem* item);
void setProgressBarValue(int);
void readDicomFilesCompleted();

DcmtkDemo.cpp 文件中添加这四个函数的定义:

void DcmtkDemo::on_btnOpenFolder_clicked()
{
	QSettings setting("blackwood-cliff", "DcmtkDemo");  //为了记住上一次的路径
	QString dirStr = setting.value("OPEN_FOLDER", ".").toString();   //不存在的话为当前应用程序路径
	dirStr = QFileDialog::getExistingDirectory(this, QStringLiteral("打开文件夹"), dirStr);
	if (dirStr.isEmpty())
		return;
	setting.setValue("OPEN_FOLDER", dirStr);   //记住该路径,以备下次使用

	ui.btnOpenFolder->setEnabled(false);
	ui.progressBar->setVisible(true);
	ui.progressBar->setValue(0);

	ReadWorker *worker = new ReadWorker(dirStr);
	QThread *thread = new QThread();
	connect(thread, SIGNAL(started()), worker, SLOT(readDicomFiles()));   //线程开始后执行worker->readDicomFiles()
	connect(worker, SIGNAL(progress(int)), this, SLOT(setProgressBarValue(int)));   //worker 发送信号,执行this->setProgressBarValue
	connect(worker, SIGNAL(finish()), this, SLOT(readDicomFilesCompleted()));   //读取完毕,执行this->readDicomFilesCompleted()
	connect(worker, SIGNAL(finish()), worker, SLOT(deleteLater()));       //执行完成,析构worker
	connect(worker, SIGNAL(destroyed(QObject*)), thread, SLOT(quit()));   //析构worker 完成, 推出线程
	connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));     //退出线程, 析构线程

	worker->moveToThread(thread);   //把worker移动到线程
	thread->start();                //开始线程
	ui.lstSeriesList->clear();
}
void DcmtkDemo::on_lstSeriesList_itemDoubleClicked(QListWidgetItem* item)
{
	ui.graphicsView->loadSeries(item->text());
}
void DcmtkDemo::setProgressBarValue(int progress)
{
	ui.progressBar->setValue(progress);
}
void DcmtkDemo::readDicomFilesCompleted()
{
	ui.btnOpenFolder->setEnabled(true);
	ui.progressBar->setVisible(false);

	if (SeriesData::SeriesList.size() == 0)
	{
		QMessageBox::warning(this, "读取完毕", "没有发现 DICOM 文件。");
		return;
	}

	for (size_t i = 0; i < SeriesData::SeriesList.size(); i++)
	{
		ui.lstSeriesList->addItem(SeriesData::SeriesList.at(i)->getSeriesUID());
	}

	//默认显示第一个序列的图像
	ui.graphicsView->loadSeries(SeriesData::SeriesList.at(0)->getSeriesUID());
}

编译程序

编译之前,应首先设置程序字符集为 使用多字节字符集,否则会出现下面三个错误:

  • C2665 “dcmtk::log4cplus::Logger::getInstance”: 2 个重载中没有一个可以转换所有参数类型
  • C2678 二进制“+”: 没有找到接受“const wchar_t [8]”类型的左操作数的运算符(或没有可接受的转换)
  • C2664 “void dcmtk::log4cplus::Logger::forcedLog(const dcmtk::log4cplus::spi::InternalLoggingEvent &) const”: 无法将参数 3 从“int”转换为“const char *”

DcmtkDemo 项目名称上点击鼠标右键,在弹出菜单上选择 [属性],打开项目属性页对话框。选择[配置属性 > 常规],设置[字符集]为 使用多字节字符集

在这里插入图片描述
一般添加 include 目录时,会同时把 lib 文件添加上。由于本文前面添加 DCMTK include 目录时,没有添加 lib 文件,故此时必须先添加 lib 文件,否则编辑时会出现一大堆链接错误(LNK2001 和 LNK2019)。

添加 lib 文件时,一定要注意项目的 平台配置 要与引入的 DCMTK lib 文件保持一致。由于《编译 DCMTK DLL(DCMTK 3.6.4 + VS2015 + Win10)》一文里选择的是 x64 平台,所以本项目也必须选择 x64 平台。至于项目配置,如果编译 Debug 版本程序,就要选择 Debug 版 DCMTK,如果编译 Release 版本程序,就要选择 Release 版 DCMTK。

DcmtkDemo 项目名称上点击鼠标右键,在弹出菜单上选择 [属性],打开项目属性页对话框。选择[配置属性 > 链接器 > 输入],在[附加依赖项]里添加 D:\dcmtk-3.6.4-install\lib\*.lib

在这里插入图片描述
DcmtkDemo 项目名称上点击鼠标右键,在弹出菜单上选择 [生成],正常会出现生成成功的提示。

========== 生成: 成功 1 个,失败 0 个,最新 0 个,跳过 0 个 ==========

由于每个电脑的环境不同,也许会出现 LNK1104 无法打开文件“shell32.lib” 的错误。
如果出现这个错误,那一定是找不到 shell32.lib 文件了。shell32.lib 文件包含在 Windows SDK 里。Windows SDK 一般安装在 C:\Program Files (x86)\Windows Kits\10\Lib 文件夹里。如果电脑上安装了多个版本的 Windows SDK 的话,在 C:\Program Files (x86)\Windows Kits\10\Lib 文件夹里,会有多个子文件夹。在 C:\Program Files (x86)\Windows Kits\10\Lib 文件夹里,搜索 shell32.lib 文件,找到之后,记下子文件夹名称,一般是 Windows 10 SDK 的版本号,如 10.0.10586.0

记下 Windows 10 SDK 的版本号后,打开项目属性页对话框,选择[配置属性 > 常规],将[目标平台版本]设置为刚才记下的版本号,点击【确定】关闭对话框,保存程序之后重新生成。

也可以把 目标平台版本 下拉列表里的各项逐个试试,看看哪个能够编译通过。

在这里插入图片描述

运行程序

F5Ctrl + F5 运行程序,会提示找不到 DLL 文件。

把需要的 DLL 从 D:\dcmtk-3.6.4-install\bin 下面复制到 D:\DcmtkDemo\x64\Debug 里,然后重新运行程序。

程序最终界面如下:

在这里插入图片描述
上面是在 Visual Studio 里运行的,正常情况下我们应该是直接运行 .exe 文件。
如果直接在 D:\DcmtkDemo\x64\Debug 下面运行 DcmtkDemo.exe 文件,会提示找不到 Qt DLL。

我们可以像上面复制 DCMTK DLL 一样,把 Qt DLL 从 C:\Qt\Qt5.14.2\5.14.2\msvc2015_64\bin 文件夹复制到 D:\DcmtkDemo\x64\Debug 里,然后重新运行 DcmtkDemo.exe 文件。但此时会出现下面的错误提示:

在这里插入图片描述
可见直接复制 Qt DLL 不是个好办法。

其实对于这个问题,Qt 已经提供了 Qt Windows Deployment Tool

打开 命令提示符 窗口,进入 C:\Qt\Qt5.14.2\5.14.2\msvc2015_64\bin 文件夹,执行 windeployqt.exe D:\DcmtkDemo\x64\Debug\DcmtkDemo.exe 命令。

在这里插入图片描述
windeployqt.exe 命令执行完毕后,打开 D:\DcmtkDemo\x64\Debug 文件夹,可见文件夹里不仅增加了几个 DLL 文件,还多了一些子目录。再次运行 DcmtkDemo.exe 文件,程序正常启动。

在这里插入图片描述

源码下载

参考

  • 5
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值