第一个 DCMTK 程序:显示 DICOM 图像(DCMTK 3.6.4 + Qt 5.14.2 + VS2015)_家在东北-程序员ITS304

技术标签: DCMTK  

开发环境

  • 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 文件,程序正常启动。

在这里插入图片描述

源码下载

参考

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/blackwoodcliff/article/details/109481195

智能推荐

OpenCV学习笔记-轮廓特征_Charles.zhang的博客-程序员ITS304

查找轮廓的不同特征,例如面积,周长,重心,边界框等矩:cv.moments()轮廓面积:cv.contourArea()轮廓周长:cv.arcLength()轮廓近似:cv.approxPolyDp()边界矩形:cv.boundingRect()最小外接矩形: cv.minAreaRect() cv.boxPoints()最小外接圆:cv.minEnclosingCircle()椭圆拟合:cv.e...

智慧水务RTU 水利遥测终端_jixunwulian的博客-程序员ITS304

计讯物联智慧水务RTU TY910,网关型水利RTU,支持视频图像采集,数据主动上报,符合智慧水务相关协议规约,全网通4G网络,支持数据叠加,支持断电断网后数据续传,实现本地存储,数据导出、实时上报、远程查询、遥信、遥控等功能。智慧水务RTU功能1、遵循行业规约,广泛应用,支持国家《水文监测数据通信规约》(ASCII和HEX全项)、《水资源监测数据传输规约》和其他省市特殊规约、SL180-2015水文自动测报系统设备遥测终端机。2、通信方式多样不受限,支持WAN/LAN、ADSL、GPRS、 4G、

JDBC学习_無言46的博客-程序员ITS304

JDBC学习这里写目录标题JDBC学习一级目录二级目录三级目录一、JDBC开发的六个步骤二、SQL注入问题(Statement与preparement)1、Statement 的sql注入问题2、使用PrepareStatement解决sql注入问题三、ORM对象关系映射四、JDBC工具类1、数据源写到dp.properties文件中2、工具类的封装五、三层架构六、JDBC事务处理转账问题1、DAO层(实现数据库信息的查询,更新等)Accout : 实体类AccountDAO : 实体类操作数据库的接口,

uni-app笔记---HbuilderX快捷键_郎lang郎的博客-程序员ITS304_uniapp全局搜索快捷键

几个常用的记录一下vbase:生成一段基本的vue代码结构viewfor:生成一段带有v-for循环结构的视图代码块常用js代码块iff:简单if forr:for循环结构体 fori:for循环结构体并包含i funn:函数 funa:匿名函数 rt:return true clog:输出:"console.log()" clogvar:增强的日志输出,...

SL651-2014 《水文监测数据通信规约》 中心站查询遥测站实时数据详解_A__wood的博客-程序员ITS304_水文监测数据通信规约

 SL651-2014 《水文监测数据通信规约》中心站查询遥测站实时数据详解全国水文标准化技术委员会水文仪器分技术委员会为适应我国水文仪器标准化工作的迅速发展,对用来监测河流、水库等水情的水文遥测终端RTU的数据通信制定了SL651-2014《水文监测数据通信规约》,本文将以蓝普lanpu-1802型水文遥测终端RTU为例,详细介绍SL651-2014《水文监测数据通信规约》要求的,中心站查询遥测...

Seasar的ORM框架Doma学习笔记系列1——安装设置_死鸡的博客-程序员ITS304_doma框架

官方网站:http://doma.seasar.org/index.htmlDoma的一大优势是完全实现了代码跟sql文件的分离。1. 安装设置 1)doma要求JDK1.6以上的JDBC。 2)把doma-x.x.x.jar包导入工程。 3)注解处理设定    工程属性,【Java Compiler】 - 【Annotation Processing】里,

随便推点

C编程经验_YoungHonker的博客-程序员ITS304

①、全局变量用具有描述意义的名字,局部变量用短名字。函数采用动作性的名字。保持一致性。②、缩进形式显示程序结构,使用一致的缩行和加括号风格。使用空行显示模块③、充分而合理地使用程序注释 给函数和全局数据加注释。不要注释不好的代码,应该重写。不要与代码矛盾。④友好的程序界面,程序界面的方便性及有效性⑤不要滥用语言技巧 使用表达式的自然形式。利用括号排除歧义。分解复杂的表达式。当心副作

推荐资源地址_weixin_34235135的博客-程序员ITS304

http://51ctodown.blog.51cto.com/948211/547721 转载于:https://blog.51cto.com/bavol214/890648

云计算基本概念_visionarywind的博客-程序员ITS304_云计算的基本概念

云计算是一种利用互联网实现随时随地、按需便捷地访问共享资源(如计算设施、存储设备、应用程序等)的计算模式。      云计算的核心概念是计算机资源服务化。云计算将互联网转化成一个可以满足各种需求的应用和服务的交付平台,面向服务架构将计算资源抽象为服务,为云计算提供计算服务能力,虚拟化赋予云计算用于构建各种应用系统时必要的可定制的、灵活的硬件资源。一、云计算的定义      云计算是一种

A Simple Math Problem (莫比乌斯函数反演)_yezzz.的博客-程序员ITS304

A Simple Math Problem分析:莫比乌斯函数反演∑i=1n∑j=1i[gcd(i,j)==1]f(j)=∑j=1n∑i=jnf(j)∑d∣(i,j)u(d)=∑d=1n∑j=1[nd]∑i=j[nd]f(j∗d)∗u[d]=∑d=1nu(d)∑j=1[nd]f(j∗d)∑i=j[nd]1=∑d=1nu(d)∑j=1[nd]f(j∗d)∗([nd]−j+1)\begin{aligned}&amp;\sum_{i=1}^n\sum_{j=1}^i[gcd(i,j)==1]f(j).

java版飞机大战小游戏详细教程(零基础小白也可以分分钟学会!!!)_胖胖的懒羊羊的博客-程序员ITS304_java飞机大战教程

目录一:游戏展示二:游戏教程1.View视图层1.1制作游戏面板类1.2.制作游戏内容显示类2.enetiy实体层2.1游戏实体抽象类2.2战机类2.3敌机类2.4战机不断出现类3.controller控制飞机移动层3.1PlaneController类4.utils工具层4.1飞机常量类4.2加载图片类5.run启动层5.1游戏启动类三:游戏源码一:游戏展示飞机大战小游戏我们都玩过,通过移动飞机来打敌机,这里给大家展示一下游戏成果:呜呜呜由于gif只能上传5M大小,所以就不能给大家展示操作了,如果大

EXT4.0 (4~9章)学习资料_clever027的博客-程序员ITS304

第四章 MVC学习 从这个图中我们可以很清楚的看到M 、V、C在ExtJS4.0里面所对应数据类型。 靠右边是对应的代码结构。 下描述一下这model、store、view、controller以及application这几者之间的关系。(1)application:它是MVC的入口,用来告诉ExtJS到那里去找对应js文件以及启动加载controlle

推荐文章

热门文章

相关标签