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 VS Tools > Qt Options】,打开 Qt 选项窗口。确保存在如图设置。
如果 Visual Studio 没有 【Qt VS Tools】菜单,说明没有安装 Qt Visual Studio Tools。
点击菜单【文件 > 新建 > 项目】,打开“新建项目”窗口,选择 Qt Widgets Application
。
输入项目名称,点击【确定】后,进入 Qt Widgets Application Wizard 界面。
双击 DcmtkDemo.ui
文件,打开 Qt 设计师。
向窗体上拖拽一个 Vertical Layout
控件和一个 Graphics View
控件,Vertical Layout
放左侧,Graphics View
放右侧。
按住 Ctrl 键,同时选中 Vertical Layout
和 Graphics View
,然后点击工具条上的【使用分裂器水平布局】按钮。如下图:
然后点击窗体空白处,再点击工具条上的【水平布局】按钮,使控件充满整个窗体。如下图:
再依次向 Vertical Layout
里拖拽 Push Button
、List Widget
、Progress Bar
三个控件,从上到下顺序排列。
Push Button
的 objectName
为 btnOpenFolder
,text
为 打开文件夹
。List Widget
的 objectName
为 lstSeriesList
。Progress Bar
的 value
为 0
,取消勾选 textVisible
。Vertical Layout
,点击右键,选择菜单【变型为 > QFrame】,设置 minimumSize
宽度为 300
,maximumSize
宽度为 500
。编写代码前,应先配置好 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 文件编译程序前加上就可以,此处先不添加。
代码主要由下面几部分组成:
我们将定义一个 ImageData 类,用于表示图像数据模型。一个 ImageData 对象,表示一幅图像。
ImageData 类是本程序的一个核心类。这个类主要有两个功能:
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 ¢er, double &width) const
{
center = _winCenter; width = _winWidth;
}
//设置窗宽窗位
void setWindow(const double ¢er, 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 ¢er, double &width) const
{
center = _defaultCenter; width = _defaultWidth;
}
void appendImage(ImageData* pImage);
private:
QString _seriesUID;
double _defaultCenter, _defaultWidth; //默认窗位窗宽
};
读取磁盘文件是一个耗时的操作,为了防止界面失去响应,一般这种 IO 操作都放在单独的线程里执行。
本来读取文件只需写一个函数就可以,但为了使用 QObject::moveToThread()
函数实现多线程,我们将定义一个 ReadWorker 类来读取 DICOM 文件。
在 DcmtkDemo
项目名称上点击鼠标右键,在弹出菜单上选择 [添加 > Add Qt Class…],打开 Add Class 对话框。
选择 Qt Class,输入类名称,然后点击【Add】按钮。
无须改变向导页的信息,点击【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 对话框。
选择 Qt Class,输入类名称,然后点击【Add】按钮,进入向导界面。
修改 Base class 为 QGraphicsView
,修改 Constructor signature 为 QWidget *parent
,点击【Finish】按钮,添加 DicomView
类。
DicomView
类继承自 QGraphicsView
,包含如下控件:
QGraphicsPixmapItem
,用于显示图像。QGraphicsSimpleTextItem
,用于显示图像窗宽窗位和当前图像索引。QSlider
,在图像上显示一个滑动条,滑块指示当前图像索引位置,拖拽滑块可切换图像。另外,重写了 QGraphicsView
的 resizeEvent
和 wheelEvent
两个函数。
重写 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
控件,在弹出菜单上选择【提升为…】:
在打开的 提升的窗口部件 对话框中,在 提升的类名称 框输入 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 的版本号后,打开项目属性页对话框,选择[配置属性 > 常规],将[目标平台版本]设置为刚才记下的版本号,点击【确定】关闭对话框,保存程序之后重新生成。
也可以把 目标平台版本 下拉列表里的各项逐个试试,看看哪个能够编译通过。
按 F5
或 Ctrl + 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 已经提供了 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
文件,程序正常启动。