1. 下载和安装VTK

  1. 下载源码包, 保存到工作目录中. 比如, 我自己的目录为c:/libs/vtk.
  2. 解压缩vtk的源码包. 默认解压缩当当前目录后是VTK-9.3.0, 我打算将这个目录用于构建后的版本目录, 因此将它改一下名字, VTK-9.3.0-src
  3. 建立两个同级的目录, VTK-9.3.0-buildVTK-9.3.0, 第一个是构建时的工作目录, 这样cmake的所有生成文件都放在这里, 不会破坏源代码目录VTK-9.3.0-src. 而VTK-9.3.0用于安装VTK的目录. 将这些目录都和安装目录隔离开, 避免万一构建出了什么问题后清理起来麻烦.

1.1 修改bug

VTK9.3有一个bug, 会导致Debug模式下编译失败. 我们先修改一下.

打开Common\Core\vtkConstantImplicitBackend.h,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
VTK_ABI_NAMESPACE_BEGIN
template <typename ValueType>
struct VTKCOMMONCORE_EXPORT vtkConstantImplicitBackend final
{
/**
* A non-trivially contructible constructor
*
* \param val the constant value to return for all indeces
*/
vtkConstantImplicitBackend(ValueType val)
: Value(val)
{
}
...

我们需要将VTKCOMMONCORE_EXPORT给删掉. 不然会重复导出符号.

1.2 配置CMake

  1. 启动CMAKE, 指定源目录为C:/libs/vtk/VTK=9.3.0-src, build目录为c:/libs/vtk/VTK-9.3.0-build.

  2. 按下Configure按钮. 此时, 也可以将GroupedAdvanced两个复选框勾上. 此时会弹出对话框, 选择工具. 在里面选择Visual Studio 17 2022x64.

Qt6 编译使用的是Visual Studio 16 2019, 可能选择它是更好的做法. 我的电脑里面只有2022, 因此只能选择这个. 不过, 我们可以为Visual Studio 2022安装2019的生成工具使用. 如下所示:

  1. 确认之后, CMAKE会进行配置分析. 取决于你的电脑, 这是一个很漫长的过程, 等完成后, 要进行下面的修改:

    • 注意选中BUILD_SHARED_LIBS, 这个一定要选中 (默认是选中的, 要检查一下不要弄没了).
    • 寻找VTK_LEGACY_REMOVE, 选中它会禁用deprecated api. 我们会使用最新的API接口, 不会盲目抄网上的老东西, 所以这个最好是选中它.
    • VTK_MODULE_ENABLE_VTK_iossVTK_MODULE_ENABLE_VTK_IOIOSS都设置为NO, 以前的版本会编译失败. 我们反正用不到, 也懒得花上一个多小时去尝试了, 就设置为NO了. 这个有啥用我还不知道.
    • 寻找CMAKE_INSTALL_PREFIX, 把它设置为我们前面创建的目录C:/libs/vtk/VTK-9.3.0. 这个一定要慎重, 一旦确定了就不要以后再乱动了.
    • 最后, 寻找Qt, 将VTK_GROUP_ENABLE_QT给勾选上, 然后将其他几个都设置为WANT. 我因为不用QML, 就没有选中Quick的这一项. 注意的是, Qt6的新版本的目录结构有变化, 和VTK不怎么兼容, 如果选择了quick, 需要自己去修改几个目录的位置.
  2. 选择了之后, 就可以再次按下Configure按钮了. 这个时候还会有错误. 我们要如下修改:

    • 设置Qt5_DIRQt6_DIR. 对于Qt6, 默认位置在C:\Qt\6.7.0\msvc2019_64\lib\cmake\Qt6下面. Qt5也一样.
    • 确认一下VTK_QTVERSION的值, 因为这台电脑只装了Qt6, 我也只会在Qt6上面用VTK, 我就设置为6了.

  3. 再次按下Configure按钮, 结果如下图所示. 红色的是给你确认的. 因为这台电脑上没有装vulkan, 所以会有警告. 我们不用它, 所以不用关心. 等以后需要的时候再重新安装Vulkan SDK就是了. 然后确认一下没问题了. 就可以按下Generate按钮生成VC工程了.

  4. 按下Generate, 会在build目录中生成VTK.sln文件. 然后我们可以关闭CMAKE了.

1.3 编译VLK和安装

用Visual Studio打开VTK.sln工程. 选择ALL BUILD项目, 打开工程属性页:

  • 选择所有配置, 然后选择常规/C++标准, 将其改为C++17或更新的版本.
  • 如果担心和Qt的版本不一致, 可以修改平台工具集, 改为2019的v142版本. 据说这个版本不一致会影响plugin, 但是我基本上不拖拉控件, 所以也就不想改了.


然后, 激活Release, 并开始Build. 这个过程很漫长, 我的11代i5的笔记本, 估计会跑上1个多小时.

编译完成后, 选择INSTALL项目, 选择仅生成INSTALL.

然后, 将配置切换到Debug, 并重新做一轮. 编译和安装就完成了.

修改完毕之后, 注意将VTK_DIR放到环境变量里面去.

2. 在Qt的qmake项目中使用VTK

使用qmake,无法利用cmake,使用起来很麻烦,需要人工将库一个个地添加到工程里面。例如:

修改.pro文件, 在文件按末尾增加vtk库:

1
2
3
4
5
6
7
INCLUDEPATH += /path/to/VTK/include/vtk-<version>
LIBS += -L/path/to/VTK/lib \
-lvtkCommonCore-<version> \
-lvtkFiltersSources-<version> \
-lvtkInteractionStyle-<version> \
-lvtkRenderingOpenGL2-<version> \
-lvtkGUISupportQt-<version>

这种做法太麻烦,尝试一次就不会再有做第二次的兴趣了。除非是在已有的qmake工程中增加VTK,否则还是不主张使用。

3. 在Qt的cmake项目中使用VTK

3.1 创建一个纯Qt项目的CMake文件

首先注意一点的是,比较新的QtCreator自动创建的widget项目的cmake文件是有问题的,而且里面有很多做Windows用不到的东西,如果不跨平台,只做Windows上位机程序,还是按照它的文档手工写文件比较好,做出来的cmake文件更干净一些。

下面是一个支持GUI的CMakeLists.txt文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# GUI和Console都需要的部分
cmake_minimum_required(VERSION 3.16)
project(exam01 VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Console项目只需要Core组件.
find_package(Qt6 REQUIRED COMPONENTS Core)
#如果是GUI, 至少要将Widgets加进去. Widgets自动包含Core了
find_package(Qt6 REQUIRED COMPONENTS Widgets)
# 如果需要添加多语种支持, 加入LinguistTools
find_package(Qt6 REQUIRED COMPONENTS LinguistTools)
# QtCreator模板生成的项目不再使用这个函数了. 但是文档中还是这个
qt_standard_project_setup()
# 我们可以自己定义多个变量来对源码自己做分类. 模板是直接加到qt_add_executable()里面.
# 我们自己分组分类更便于维护
# 按照cmake的说法, 不需要将.h文件加到makelist里面. 但是加进去也没啥.
set(PROJECT_SOURCES
main.cpp
mainwindow.cpp
mainwindow.h
mainwindow.ui
)
qt_add_executable(exam01
MANUAL_FINALIZATION
${PROJECT_SOURCES}
)
# 添加语言资源文件. 模板生成的也不用这个了. 感觉Qt团队做事情太随意了.
qt_add_translations(${PROJECT_NAME} TS_FILES exam01_zh_CN.ts)

# 如果是Core项目, 则使用Qt6::Core代替这里的Qt6::Widgets
target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Widgets
)
# Console项目不需要这个一段.
set_target_properties(${PROJECT_NAME} PROPERTIES
WIN32_EXECUTABLE ON
MACOSX_BUNDLE ON
)

3.2 添加VTK

然后, 要支持VTK,我们只需要将下面几行加到里面去:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 添加对VTK的引用. 也可以使用COMPONENTS指定具体的组件. 对新手来说, 这个有点困难. 
find_package(VTK REQUIRED)

# 在target_link_libraries里面加入${VTK_LIBRARIES}
target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Widgets
${VTK_LIBRARIES}
)

# 加入下面
vtk_module_autoinit(
TARGETS ${PROJECT_NAME}
MODULES ${VTK_LIBRARIES}
)

这样,CMake工程文件就完成了。

接下来,我们还需要找个地方实现auto_init。这个活动需要在使用::New()的前面进行,个人建议就放到main.cpp里面。这样不管是Console还是GUI程序,都能利用它。比如:

1
2
3
4
5
6
7
8
9
10
11
12
...
#include <vtkAutoInit.h>
VTK_MODULE_INIT(vtkRenderingOpenGL2);
VTK_MODULE_INIT(vtkInteractionStyle);
VTK_MODULE_INIT(vtkRenderingVolumeOpenGL2);
VTK_MODULE_INIT(vtkRenderingFreeType);
VTK_MODULE_INIT(vtkRenderingContextOpenGL2);
...
int main()
{
...
}

3.3 其他改进

接下来,我们可以考虑利用预编译头文件。这一点上,cmake做的比qmake,比VC都差了很多。尤其是VTK的头文件多如牛毛,好像也没有一个如OpenCV的opencv.hpp这样的打包头文件,用起来很不友好。个人的建议是把你自己经常用到的头文件都定义在一起。

比如,下面我定义了一个变量PCH_FILES,里面包含了经常用到的头文件,然后用target_precompile_headers来声明它。如果是多子工程的项目,可以将PCH_FILES定义到顶层文件中,每个子目录项目的cmakelists文件中使用target_precompile_headers来引用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
set(PCH_FILES
<vtk3DSImporter.h>
<vtkActor.h>
<vtkBMPReader.h>
<vtkCallbackCommand.h>
<vtkCamera.h>
<vtkConeSource.h>
<vtkCubeSource.h>
<vtkCylinderSource.h>
<vtkExtractVOI.h>
<vtkGenericOpenGLRenderWindow.h>
<vtkImageActor.h>
<vtkImageCanvasSource2D.h>
<vtkImageData.h>
<vtkImageViewer2.h>
<vtkInteractorStyleImage.h>
<vtkJPEGReader.h>
<vtkLight.h>
<vtkMarchingCubes.h>
<vtkMetaImageReader.h>
<vtkNamedColors.h>
<vtkPolyDataMapper.h>
<vtkProperty.h>
<vtkRenderer.h>
<vtkRenderWindow.h>
<vtkRenderWindowInteractor.h>
<vtkSmartPointer.h>
<vtkStdString.h>
<vtkStringArray.h>
<vtkStructuredPointsReader.h>
<vtkSphereSource.h>
<vtkTexture.h>
)

target_precompile_headers(${PROJECT_NAME} PUBLIC
${PCH_FILES}
)

4. Qt中使用VTK编程

4.1 控制台程序

既然是控制台,就无所谓Qt不Qt的了,使用的是vtkRenderWindow,用什么开发工具和库都是一样的。当然,如果只是看图,也可以直接使用ImageView2

4.2 GUI程序

但是,使用Qt的价值在于我们要编写GUI程序,我们要使用Qt提供的一系列的Widget。VTK的历史上,先后给出了好多个用于Qt的Widget组件:

  • QVTKWidget
  • QVTKWidget2
  • QVTKOpenGLWidget
  • QVTKOpenGLNativeWidget
  • QVTKOpenGLStereoWidget

对于我们来说,如果你使用的是Qt5.4以上的版本,VTK的版本是8.2以上,那么前面的都不需要考虑,只需要记住最后两个就好了。根据VTK的文档,建议一般情况下使用QVTKOpenGLNativeWidget,特殊场景下可以使用SteroWidget这个。

以前的版本,VTK还会编译出一个dll,你可以将它放到Qt的plugin目录里面,然后就可以在QtCreator的设计节目中看到一个QVTKWidget了。这个新的VTK版本也没有了。所以新的用户也不要纠结怎么找不到了。QVTKOpenGLNativeWidgetQOpenGLWidget的派生类。现在我们先拖一个QWidget控件,然后把它提升到QVTKOpenGLNativeWidget就是了。

下面是一个典型的工作流程。假定ui->qvtkWidget是我们在ui文件中定义的一个QVTKOpenGLNativeWidget组件,那么这样使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建source
vtkNew<vtkConeSource> cone;
// 创建Mapper
vtkNew<vtkPolyDataMapper> coneMapper;
coneMapper->SetInputConnection(cone->GetOutputPort());
// 创建Actor
vtkNew<vtkActor> coneActor;
coneActor->SetMapper(coneMapper);
// 创建Render
vtkNew<vtkRender> renderer;
renderer->AddActor(coneActor);
// 创建窗口
vtkNew<vtkGenericOpenGLRenderWindow> renwindow;
renwindow->AddRender(renderer);
renwindow->Render();
// 为Widget指定渲染窗口
ui->qvtkWidget->SetRenderWindow(renwindow.Get());
ui->qvtkWidget->update();

和Console程序的主要区别在于:

  • 使用vtkGenericOpenGLRenderWindow代替了vtkRenderWindow
  • 不需要vtkRenderWindowInteractor的创建和关联的操作了。不然会出错的。

VTK的资料有些是过时的——几乎所有的开源的东西的文档都有这个问题,像Qt这么高质量的文档实在是凤毛麟角了——更好的做法是直接看API和Example。Qt相关的例子在VTK/Examples/GUI/Qt下面。

另外,vtkEventQtSlotConnect类用于连接vtkObject的事件到QObjectslot上面。

1
2
3
4
5
6
7
8
connections = vtkEventQtSlotConnect::New();
// get right mouse pressed with high priority
connections->Connect(
qVTK1->GetRenderWindow()->GetInteractor(),
vtkCommand::RightButtonPressEvent,
this,
SLOT(popup( vtkObject*, unsigned long, void*, void*, vtkCommand*)),
popup1, 1.0);

在Widget中使用vtkImageView2

下面是经过测试的一种做法. 其中, ui->vtkWidget是在界面中创建的QVTKOpenGLNativeWidget, 其他的都放在函数中了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void ImageForm::onLoadImage()
{
const char* path = "...."; // 文件名
vtkNew<vtkJPEGReader> reader;
reader->SetFileName(path);

vtkNew<vtkImageViewer2> imageViewer;
imageViewer->SetInputConnection(reader->GetOutputPort());

imageViewer->UpdateDisplayExtent();
imageViewer->SetupInteractor(ui->vtkWidget->interactor());
imageViewer->SetRenderWindow(ui->vtkWidget->renderWindow());
ui->vtkWidget->interactor()->SetInteractorStyle(vtkSmartPointer<vtkInteractorStyleImage>::New());

imageViewer->GetRenderer()->SetBackground(0.6,0.6,0.6);
imageViewer->GetRenderer()->ResetCamera();
imageViewer->Render();
}

另外,我们也可以使用vtkImageView2。下面是外网上面的两种思路。不过,就我而言,我更喜欢使用vtkImageActor来自己实现操作。

这是在VTK论坛上这个问题的答复. 其中, qvtk是一个QVTKOpenGLWidget, 它应该和QVTKOpenGLNativeWidget一样的:

1
2
3
4
5
6
imageViewer = vtkSmartPointer<vtkImageViewer2>::New();
imageViewer->SetRenderWindow(qvtk->GetRenderWindow());
imageViewer->SetSliceOrientation(sliceOrientation);
qvtk->GetInteractor()->SetInteractorStyle(vtkSmartPointer<InteractorStyleImage>::New());

// ... Set vtkImageViewer2 input...

这是网上的另一种做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//VTK 在QT中的渲染窗口部件(QT与VTK 在此被链接)
OpenGLNativeWidget = vtkSmartPointer<QVTKOpenGLNativeWidget>::New();

m_pImageViewer = vtkSmartPointer<vtkImageViewer2>::New();
m_pImageImport = vtkSmartPointer<vtkImageImport>::New(); //图像数据读取器
m_pImageData = vtkSmartPointer<vtkImageData>::New();

//初始化交互器
m_pImageViewer->SetupInteractor(OpenGLNativeWidget->interactor());
//初始化渲染窗口
m_pImageViewer->SetRenderWindow(OpenGLNativeWidget->renderWindow());
//给渲染窗口添加场景
OpenGLNativeWidget->renderWindow()->AddRenderer(m_pImageViewer->GetRenderer());

//背景颜色
m_pImageViewer->GetRenderer()->SetBackground(0, 0, 0);

//交互样式
m_pStyle = vtkSmartPointer<vtkInteractorStyleImage>::New();
OpenGLNativeWidget->interactor()->SetInteractorStyle(m_pStyle);

//渲染
m_pImageViewer->GetRenderer()->ResetCamera();