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

文章目录

  • 开发环境
  • Visual Studio Qt 选项
  • 创建 Qt Widgets 程序
  • 设计界面
  • 配置 DCMTK 路径
  • 编写代码
    • 图像数据模型类
    • 序列数据模型类
    • 读取 DICOM 文件
    • 图像显示视图类
      • 提升窗口部件
    • 界面交互
  • 编译程序
  • 运行程序
  • 源码下载
  • 参考

开发环境

  • 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 选项窗口。确保存在如图设置。

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第1张图片
如果 Visual Studio 没有 【Qt VS Tools】菜单,说明没有安装 Qt Visual Studio Tools。

创建 Qt Widgets 程序

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

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第2张图片
输入项目名称,点击【确定】后,进入 Qt Widgets Application Wizard 界面。

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第3张图片
第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第4张图片
点击【Finish】按钮创建项目。创建完成的项目如下图:

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

设计界面

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

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

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第6张图片
然后点击窗体空白处,再点击工具条上的【水平布局】按钮,使控件充满整个窗体。如下图:

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第7张图片
再依次向 Vertical Layout 里拖拽 Push ButtonList WidgetProgress Bar 三个控件,从上到下顺序排列。

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

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第8张图片
设计完成的界面结构如下图:

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

配置 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

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第10张图片
一般在添加 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 
#include 

#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 
#include 
#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 对话框。

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第11张图片
选择 Qt Class,输入类名称,然后点击【Add】按钮。

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第12张图片
无须改变向导页的信息,点击【Finish】按钮,添加 ReadWorker 类。

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

#pragma once

#include 

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 对话框。

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第13张图片
选择 Qt Class,输入类名称,然后点击【Add】按钮,进入向导界面。

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第14张图片
修改 Base classQGraphicsView,修改 Constructor signatureQWidget *parent,点击【Finish】按钮,添加 DicomView 类。

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

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

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

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

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

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

DicomView 类头文件定义如下:

#pragma once

#include 
#include 

#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 控件,在弹出菜单上选择【提升为…】:

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第15张图片
在打开的 提升的窗口部件 对话框中,在 提升的类名称 框输入 DicomView,同时下面的 头文件 框会自动输入 dicomview.h

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第16张图片
点击【添加】按钮,添加到 提升的类 列表,然后点击【提升】按钮,完成提升操作。

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第17张图片
提升完成之后,在 对象查看器 中,可见 Graphics View 控件的 已经变成了 DicomView

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第18张图片
点击 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 项目名称上点击鼠标右键,在弹出菜单上选择 [属性],打开项目属性页对话框。选择[配置属性 > 常规],设置[字符集]为 使用多字节字符集

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第19张图片
一般添加 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

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第20张图片
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 的版本号后,打开项目属性页对话框,选择[配置属性 > 常规],将[目标平台版本]设置为刚才记下的版本号,点击【确定】关闭对话框,保存程序之后重新生成。

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

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

运行程序

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

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

程序最终界面如下:

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第22张图片
上面是在 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 文件。但此时会出现下面的错误提示:

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第23张图片
可见直接复制 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 命令。

第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_第24张图片
windeployqt.exe 命令执行完毕后,打开 D:\DcmtkDemo\x64\Debug 文件夹,可见文件夹里不仅增加了几个 DLL 文件,还多了一些子目录。再次运行 DcmtkDemo.exe 文件,程序正常启动。

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

源码下载

  • 下载程序源码:https://download.csdn.net/download/blackwoodcliff/13189854
  • 下载 DCMTK DLL:https://download.csdn.net/download/blackwoodcliff/13187054
  • 下载 DICOM 图像:https://download.csdn.net/download/blackwoodcliff/13193834
  • 更多 DICOM 图像:https://support.dcmtk.org/redmine/projects/dcmtk/wiki/DICOM_Images

参考

  • DCMTK(MD版)、QT、VS2015编写Dicom序列浏览应用程序-新建项目,配置环境
  • 第二章 基于QT和DCMTK的Dicom 图像浏览器—界面设计
  • 第三章 基于QT和DCMTK的Dicom 图像浏览器—单个Dicom图像读取类
  • 第四章 基于QT和DCMTK的Dicom 图像浏览器—检查文件夹下Dicom序列个数
  • 第五章 基于QT和DCMTK的Dicom 图像浏览器—Dicom图像序列类
  • 第六章 基于QT和DCMTK的Dicom 图像浏览器—Dicom视图类
  • 解决This application failed to start because no Qt platform plugin could be initialized问题

你可能感兴趣的:(DCMTK)