Skip to content

第23章 进阶篇综合实战

Volume 3 Capstone -- 科学数据可视化工作站

摘要: 本章是全书第23章,也是三卷教程的终章。我们将从零构建一个功能完整的"科学数据可视化工作站"桌面应用程序,综合运用第三卷所学的体绘制、图表、交互部件与 Qt 集成技术。该应用支持加载三维科学数据,同时展示体绘制视图、二维直方图统计、传递函数编辑面板以及多种标注部件,并集成了 SMP 并行加速、键盘快捷键等实用功能。最后,我们对全书三卷内容进行回顾与总结,并展望 VTK 开发者未来的学习方向。

目标读者: 已完成前22章学习的VTK进阶开发者
预计阅读时间: 90 分钟
代码量: 完整C++程序约700行 + CMakeLists.txt


目录


23.1 项目概述

23.1.1 项目目标

本章的综合实战项目名为"科学数据可视化工作站"(Scientific Data Visualization Workstation)。它是一个基于 Qt 和 VTK 的桌面应用程序,模拟真实科研环境中对三维科学数据进行多角度、多技术同步分析的工作流程。

应用场景设定为:一位医学影像研究员需要同时查看CT扫描数据的体绘制三维视图、数据值的统计分布直方图,并能实时调节传递函数来突出不同组织密度。研究员还需要标量条来读取数值、方向标记来确定空间方位,并能通过快捷键快速切换视角。

23.1.2 功能清单

本应用集成了以下功能模块:

序号 功能模块 使用的VTK组件
1 三维体绘制视图 vtkSmartVolumeMapper, vtkVolumeProperty
2 二维数据直方图 vtkChartXY, vtkTable, vtkContextView
3 传递函数编辑器 Qt QSlider + vtkPiecewiseFunction + vtkColorTransferFunction
4 标量条标注 vtkScalarBarActor
5 方向标记 vtkOrientationMarkerWidget + vtkAxesActor
6 键盘快捷键 Qt keyPressEvent
7 SMP并行加速 VTK_SMP_MAX_NUM_THREADS 环境变量 + vtkSMPTools
8 多面板布局 Qt QDockWidget + QSplitter

23.1.3 界面布局

+-----------------------------------------------------------------------+
|  Menu Bar  |  Tool Bar                                                |
+-----------------------------------------------------------------------+
|                        |                        |                      |
|    3D Volume          |   2D Histogram          |   Control Panel       |
|    Rendering View      |   Chart View            |                       |
|    (Central Widget)   |   (Dock, Right-Top)     |   (Dock, Right-Bottom)|
|                        |                        |   [Transfer Function]  |
|    * Rotate/Pan/Zoom  |    * Bar chart of       |   [Opacity Sliders]    |
|    * Volume rendering |      data distribution  |   [Color Controls]     |
|    * Scalar bar       |                        |   [Slice Position]     |
|    * Orientation      |                        |   [Load Data Btn]      |
|      marker           |                        |   [Reset Btn]          |
|                        |                        |                        |
+-----------------------------------------------------------------------+
|  Status Bar                                                            |
+-----------------------------------------------------------------------+

23.2 项目架构

23.2.1 类设计

整个应用围绕一个主窗口类 MainWindow(继承自 QMainWindow)构建。其内部包含以下核心成员:

MainWindow (QMainWindow)
├── QVTKOpenGLNativeWidget* m_volumeWidget      // 体绘制视图
├── QVTKOpenGLNativeWidget* m_chartWidget       // 直方图视图
├── QDockWidget*            m_chartDock         // 直方图停靠面板
├── QDockWidget*            m_controlDock       // 控制面板
├── vtkSmartPointer<vtkRenderer> m_volumeRenderer
├── vtkSmartPointer<vtkVolume>   m_volume
├── vtkSmartPointer<vtkPiecewiseFunction>      m_opacityFunction
├── vtkSmartPointer<vtkColorTransferFunction>  m_colorFunction
├── vtkSmartPointer<vtkImageData>              m_imageData
├── vtkSmartPointer<vtkChartXY>                m_chart
├── QSlider* m_opacitySliders[5]               // 5个不透明度控制点
├── QSlider* m_colorSliders[3]                 // RGB分量
└── QLabel*  m_infoLabel                       // 状态栏信息

23.2.2 数据流设计

数据从合成数据生成器产生,经过以下管线流动:

GenerateSyntheticData()
    │
    ▼
vtkImageData (128^3)
    │
    ├──► vtkSmartVolumeMapper ──► vtkVolume ──► vtkRenderer (volume view)
    │         │
    │         └── vtkVolumeProperty
    │               ├── vtkPiecewiseFunction (opacity)  ◄── Qt Sliders
    │               └── vtkColorTransferFunction (color) ◄── Qt Sliders
    │
    └──► Histogram Sampling ──► vtkTable ──► vtkChartXY (chart view)

23.2.3 文件结构

项目由以下文件组成:

Volume3Capstone/
├── CMakeLists.txt
├── main.cpp                  // 入口 + MainWindow 完整实现
└── README.md                 // (可选) 项目说明

为了教学目的,我们将所有代码集中在一个 main.cpp 文件中,便于读者阅读和编译。在实际项目中,建议将 MainWindow 拆分为独立的头文件和实现文件。


23.3 完整代码

23.3.1 CMakeLists.txt

以下 CMake 构建脚本适用于 VTK 9.5.2 和 Qt5/Qt6。根据你的环境选择合适的 Qt 版本。

cmake_minimum_required(VERSION 3.16)
project(Volume3Capstone LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# ============================================================
# 查找 VTK 9.5.2
# ============================================================
find_package(VTK 9.5 REQUIRED COMPONENTS
    CommonCore
    CommonDataModel
    CommonExecutionModel
    CommonMath
    CommonTransforms
    CommonColor
    ImagingCore
    ImagingSources
    ImagingHybrid
    RenderingCore
    RenderingOpenGL2
    RenderingVolume
    RenderingVolumeOpenGL2
    RenderingContext2D
    RenderingContextOpenGL2
    ChartsCore
    InteractionStyle
    InteractionWidgets
    FiltersCore
    FiltersSources
    FiltersStatistics
    GUISupportQt
    ViewsContext2D
)

# ============================================================
# 查找 Qt (支持 Qt5 或 Qt6)
# ============================================================
find_package(Qt6 QUIET COMPONENTS Widgets OpenGLWidgets)
if(NOT Qt6_FOUND)
    find_package(Qt5 5.15 REQUIRED COMPONENTS Widgets)
    set(QT_LIBS Qt5::Widgets)
    message(STATUS "Using Qt5")
else()
    set(QT_LIBS Qt6::Widgets)
    message(STATUS "Using Qt6")
endif()

# ============================================================
# 启用 VTK SMP 并行支持
# ============================================================
if(VTK_SMP_ENABLE_SEQUENTIAL OR VTK_SMP_ENABLE_STDTHREAD
   OR VTK_SMP_ENABLE_OPENMP OR VTK_SMP_ENABLE_TBB)
    message(STATUS "VTK SMP support: ENABLED")
else()
    message(WARNING "VTK SMP support: NOT DETECTED. "
                    "Build VTK with -DVTK_SMP_ENABLE_SEQUENTIAL=ON")
endif()

# ============================================================
# 可执行文件
# ============================================================
add_executable(Volume3Capstone main.cpp)

# VTK 自动初始化 (VTK 9.x 必须)
vtk_module_autoinit(
    TARGETS Volume3Capstone
    MODULES ${VTK_LIBRARIES}
)

target_link_libraries(Volume3Capstone
    PRIVATE
        ${VTK_LIBRARIES}
        ${QT_LIBS}
)

target_include_directories(Volume3Capstone PRIVATE
    ${VTK_INCLUDE_DIRS}
)

# 复制 Qt DLL 到构建目录 (Windows)
if(WIN32)
    set_target_properties(Volume3Capstone PROPERTIES
        WIN32_EXECUTABLE TRUE
    )
endif()

message(STATUS "VTK version: ${VTK_VERSION}")
message(STATUS "Qt version: ${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}")

23.3.2 主程序代码

以下完整代码包含合成数据生成、体绘制、直方图、传递函数编辑、标注部件、键盘快捷键和 SMP 支持。代码总长约 750 行。

// ============================================================================
// 文件: main.cpp
// 描述: "科学数据可视化工作站" — VTK 9.5.2 综合实战应用
//       组合体绘制、直方图、传递函数编辑、标注部件与Qt集成
// 编译: 参见配套 CMakeLists.txt
// 运行: ./Volume3Capstone
//       设置线程数: set VTK_SMP_MAX_NUM_THREADS=4 (Windows)
//                    export VTK_SMP_MAX_NUM_THREADS=4 (Linux/macOS)
// ============================================================================

#include <cmath>
#include <cstdlib>
#include <vector>
#include <algorithm>
#include <sstream>
#include <iomanip>

// Qt 头文件
#include <QApplication>
#include <QMainWindow>
#include <QDockWidget>
#include <QSplitter>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGroupBox>
#include <QLabel>
#include <QSlider>
#include <QPushButton>
#include <QMenuBar>
#include <QMenu>
#include <QAction>
#include <QToolBar>
#include <QStatusBar>
#include <QFileDialog>
#include <QMessageBox>
#include <QKeyEvent>
#include <QFrame>

// VTK 头文件
#include <vtkActor.h>
#include <vtkAxesActor.h>
#include <vtkCamera.h>
#include <vtkChartXY.h>
#include <vtkColorTransferFunction.h>
#include <vtkContextScene.h>
#include <vtkContextView.h>
#include <vtkFloatArray.h>
#include <vtkGenericOpenGLRenderWindow.h>
#include <vtkImageData.h>
#include <vtkImageShiftScale.h>
#include <vtkLookupTable.h>
#include <vtkMinimalStandardRandomSequence.h>
#include <vtkNew.h>
#include <vtkOrientationMarkerWidget.h>
#include <vtkPiecewiseFunction.h>
#include <vtkPlot.h>
#include <vtkPointData.h>
#include <vtkProperty.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkScalarBarActor.h>
#include <vtkSmartPointer.h>
#include <vtkSmartVolumeMapper.h>
#include <vtkSMPTools.h>
#include <vtkTable.h>
#include <vtkTextProperty.h>
#include <vtkVolume.h>
#include <vtkVolumeProperty.h>
#include <vtkWindowToImageFilter.h>
#include <vtkPNGWriter.h>

// VTK Qt 粘合层
#include <QVTKOpenGLNativeWidget.h>
#include <vtkContextView.h>

// ============================================================================
// 前向声明
// ============================================================================

// 合成数据生成参数
struct VolumeDataParams
{
    int   dimensions[3]  = { 128, 128, 128 };
    double spacing[3]     = { 1.0, 1.0, 1.0 };
    double origin[3]      = { 0.0, 0.0, 0.0 };
    int   numBlobs        = 5;       // 高斯团数量
    double blobRadius     = 20.0;    // 团半径
    double blobIntensity  = 1000.0;  // 团中心最大强度
};

// 传递函数控制点
struct TFPoint
{
    double x;       // 数据值位置 (0-1, 归一化)
    double opacity; // 不透明度 (0-1)
};

// ============================================================================
// 工具函数:生成合成三维数据 (多个高斯团)
// ============================================================================
vtkSmartPointer<vtkImageData> GenerateSyntheticData(
    const VolumeDataParams& params = VolumeDataParams())
{
    vtkNew<vtkImageData> imageData;
    imageData->SetDimensions(params.dimensions);
    imageData->SetSpacing(params.spacing);
    imageData->SetOrigin(params.origin);
    imageData->AllocateScalars(VTK_FLOAT, 1);

    int nx = params.dimensions[0];
    int ny = params.dimensions[1];
    int nz = params.dimensions[2];
    float* dataPtr = static_cast<float*>(
        imageData->GetScalarPointer());

    // 使用确定性随机数生成器预设几个团中心
    std::vector<double> blobCX(params.numBlobs);
    std::vector<double> blobCY(params.numBlobs);
    std::vector<double> blobCZ(params.numBlobs);
    std::vector<double> blobAmp(params.numBlobs);

    vtkNew<vtkMinimalStandardRandomSequence> rng;
    rng->SetSeed(42); // 固定种子,保证可复现

    for (int b = 0; b < params.numBlobs; ++b)
    {
        // 团中心分布在体数据范围内 (20%-80%)
        blobCX[b] = nx * (0.2 + 0.6 * rng->GetValue());
        rng->Next();
        blobCY[b] = ny * (0.2 + 0.6 * rng->GetValue());
        rng->Next();
        blobCZ[b] = nz * (0.2 + 0.6 * rng->GetValue());
        rng->Next();
        // 幅度在 0.5 到 1.0 之间
        blobAmp[b] = 0.5 + 0.5 * rng->GetValue();
        rng->Next();
    }

    double sigma2 = 2.0 * params.blobRadius * params.blobRadius;

    // 使用 SMP 并行填充体数据
    auto fillVolume = [&](vtkIdType startIdx, vtkIdType endIdx)
    {
        for (vtkIdType idx = startIdx; idx < endIdx; ++idx)
        {
            int k = static_cast<int>(idx / (nx * ny));
            int rem = static_cast<int>(idx % (nx * ny));
            int j = rem / nx;
            int i = rem % nx;

            double value = 0.0;
            for (int b = 0; b < params.numBlobs; ++b)
            {
                double dx = i - blobCX[b];
                double dy = j - blobCY[b];
                double dz = k - blobCZ[b];
                double dist2 = dx * dx + dy * dy + dz * dz;
                value += blobAmp[b] * std::exp(-dist2 / sigma2);
            }

            dataPtr[idx] = static_cast<float>(
                params.blobIntensity * value);
        }
    };

    vtkSMPTools::For(0, static_cast<vtkIdType>(nx) * ny * nz,
                     fillVolume, 1); // grain size = 1 确保负载均衡

    // 计算实际数据范围
    double dataRange[2];
    imageData->GetScalarRange(dataRange);
    std::cout << "[INFO] 合成数据维度: " << nx << "x" << ny << "x" << nz
              << " | 数据范围: [" << dataRange[0] << ", "
              << dataRange[1] << "]" << std::endl;

    return imageData;
}

// ============================================================================
// 工具函数:从体数据计算直方图
// ============================================================================
vtkSmartPointer<vtkTable> ComputeHistogram(vtkImageData* imageData,
                                            int numBins = 64)
{
    double scalarRange[2];
    imageData->GetScalarRange(scalarRange);
    double minVal = scalarRange[0];
    double maxVal = scalarRange[1];
    double binWidth = (maxVal - minVal) / numBins;

    // 统计每个bin的样本数
    std::vector<vtkIdType> binCounts(numBins, 0);
    int nx = imageData->GetDimensions()[0];
    int ny = imageData->GetDimensions()[1];
    int nz = imageData->GetDimensions()[2];
    float* dataPtr = static_cast<float*>(imageData->GetScalarPointer());

    vtkIdType totalVoxels = static_cast<vtkIdType>(nx) * ny * nz;
    for (vtkIdType i = 0; i < totalVoxels; ++i)
    {
        double val = static_cast<double>(dataPtr[i]);
        int bin = static_cast<int>((val - minVal) / binWidth);
        if (bin >= numBins) bin = numBins - 1;
        if (bin < 0) bin = 0;
        binCounts[bin]++;
    }

    // 构造 VTK Table
    vtkNew<vtkTable> table;

    vtkNew<vtkFloatArray> arrBinCenter;
    arrBinCenter->SetName("Data Value");
    arrBinCenter->SetNumberOfComponents(1);
    arrBinCenter->SetNumberOfTuples(numBins);

    vtkNew<vtkFloatArray> arrCount;
    arrCount->SetName("Frequency");
    arrCount->SetNumberOfComponents(1);
    arrCount->SetNumberOfTuples(numBins);

    for (int b = 0; b < numBins; ++b)
    {
        // bin中心值
        double center = minVal + (b + 0.5) * binWidth;
        arrBinCenter->SetValue(b, static_cast<float>(center));
        arrCount->SetValue(b, static_cast<float>(binCounts[b]));
    }

    table->AddColumn(arrBinCenter);
    table->AddColumn(arrCount);

    return table;
}

// ============================================================================
// 主窗口类:科学数据可视化工作站
// ============================================================================
class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    // ------------------------------------------------------------------------
    // 构造函数
    // ------------------------------------------------------------------------
    explicit MainWindow(QWidget* parent = nullptr)
        : QMainWindow(parent)
    {
        // ---- 窗口基本设置 ----
        setWindowTitle("科学数据可视化工作站 — VTK 9.5.2 综合实战");
        resize(1400, 900);
        setMinimumSize(1024, 700);

        // ---- 打印环境信息 ----
        PrintEnvironmentInfo();

        // ---- 构建各子系统 ----
        CreateSyntheticData();
        CreateVolumePipeline();
        CreateChartPipeline();
        CreateAnnotations();
        SetupCentralWidget();
        SetupDockWidgets();
        SetupMenuBar();
        SetupToolBar();
        SetupStatusBar();
        SetupKeyboardShortcuts();

        // ---- 初始更新 ----
        UpdateHistogram();
        UpdateScalarBar();

        std::cout << "[INFO] 科学数据可视化工作站初始化完成。" << std::endl;
    }

    // ------------------------------------------------------------------------
    // 析构函数
    // ------------------------------------------------------------------------
    ~MainWindow() override = default;

protected:
    // ------------------------------------------------------------------------
    // 键盘事件处理
    // ------------------------------------------------------------------------
    void keyPressEvent(QKeyEvent* event) override
    {
        switch (event->key())
        {
        // ---- 视角快捷键 ----
        case Qt::Key_R:
            ResetView();
            break;
        case Qt::Key_F:
            ResetCamera();
            break;
        case Qt::Key_X:
            SetViewX();
            break;
        case Qt::Key_Y:
            SetViewY();
            break;
        case Qt::Key_Z:
            SetViewZ();
            break;

        // ---- 体绘制模式切换 ----
        case Qt::Key_1:
            SetVolumeMapperMode(0); // 默认自动
            break;
        case Qt::Key_2:
            SetVolumeMapperMode(1); // 光线投射 (CPU)
            break;
        case Qt::Key_3:
            SetVolumeMapperMode(2); // GPU 体绘制
            break;

        // ---- 截图 ----
        case Qt::Key_S:
            if (event->modifiers() & Qt::ControlModifier)
            {
                SaveScreenshot();
            }
            break;

        // ---- 退出 ----
        case Qt::Key_Escape:
            close();
            break;

        // ---- 帮助 ----
        case Qt::Key_H:
            ShowHelpDialog();
            break;

        default:
            QMainWindow::keyPressEvent(event);
            break;
        }
    }

    // ------------------------------------------------------------------------
    // 窗口关闭事件
    // ------------------------------------------------------------------------
    void closeEvent(QCloseEvent* event) override
    {
        std::cout << "[INFO] 科学数据可视化工作站正在关闭..." << std::endl;
        event->accept();
    }

private:
    // ====================================================================
    // 1. 合成数据生成
    // ====================================================================
    void CreateSyntheticData()
    {
        VolumeDataParams params;
        params.dimensions[0] = 128;
        params.dimensions[1] = 128;
        params.dimensions[2] = 128;
        params.numBlobs       = 6;
        params.blobRadius     = 18.0;
        params.blobIntensity  = 2000.0;

        m_imageData = GenerateSyntheticData(params);
    }

    // ====================================================================
    // 2. 体绘制管线
    // ====================================================================
    void CreateVolumePipeline()
    {
        // ---- 2a. 智能体绘制映射器 (自动选择最佳算法) ----
        m_volumeMapper = vtkSmartPointer<vtkSmartVolumeMapper>::New();
        m_volumeMapper->SetInputData(m_imageData);
        // 默认自动模式:优先GPU,不可用时回退到CPU光线投射
        m_volumeMapper->SetRequestedRenderModeToDefault();

        // ---- 2b. 不透明度传递函数 ----
        m_opacityFunction = vtkSmartPointer<vtkPiecewiseFunction>::New();
        // 默认传递函数:突出高密度区域
        m_opacityFunction->AddPoint(0.0,   0.0);   // 背景透明
        m_opacityFunction->AddPoint(0.15,  0.0);   // 低值透明
        m_opacityFunction->AddPoint(0.4,   0.15);  // 中等值半透明
        m_opacityFunction->AddPoint(0.7,   0.6);   // 较高值较不透明
        m_opacityFunction->AddPoint(1.0,   1.0);   // 峰值完全不透明

        // ---- 2c. 颜色传递函数 ----
        m_colorFunction = vtkSmartPointer<vtkColorTransferFunction>::New();
        m_colorFunction->AddRGBPoint(0.0,   0.0, 0.0, 0.0);   // 黑色
        m_colorFunction->AddRGBPoint(0.25,  0.0, 0.0, 0.8);   // 蓝色
        m_colorFunction->AddRGBPoint(0.5,   0.0, 0.8, 0.0);   // 绿色
        m_colorFunction->AddRGBPoint(0.75,  0.8, 0.8, 0.0);   // 黄色
        m_colorFunction->AddRGBPoint(1.0,   1.0, 0.0, 0.0);   // 红色

        // ---- 2d. 体属性 ----
        m_volumeProperty = vtkSmartPointer<vtkVolumeProperty>::New();
        m_volumeProperty->SetColor(m_colorFunction);
        m_volumeProperty->SetScalarOpacity(m_opacityFunction);
        m_volumeProperty->SetInterpolationTypeToLinear();
        m_volumeProperty->ShadeOn();              // 启用光照
        m_volumeProperty->SetAmbient(0.1);        // 环境光
        m_volumeProperty->SetDiffuse(0.7);        // 漫反射
        m_volumeProperty->SetSpecular(0.2);       // 镜面反射
        m_volumeProperty->SetSpecularPower(10.0); // 高光指数

        // ---- 2e. 体对象 ----
        m_volume = vtkSmartPointer<vtkVolume>::New();
        m_volume->SetMapper(m_volumeMapper);
        m_volume->SetProperty(m_volumeProperty);

        // ---- 2f. 渲染器 ----
        m_volumeRenderer = vtkSmartPointer<vtkRenderer>::New();
        m_volumeRenderer->SetBackground(0.15, 0.15, 0.2); // 深蓝灰背景
        m_volumeRenderer->AddViewProp(m_volume);

        // ---- 2g. 相机初始位置 ----
        m_volumeRenderer->ResetCamera();
        vtkCamera* camera = m_volumeRenderer->GetActiveCamera();
        camera->SetPosition(200, 200, 400);
        camera->SetFocalPoint(64, 64, 64);
        camera->SetViewUp(0, 1, 0);
        camera->SetClippingRange(1, 2000);
    }

    // ====================================================================
    // 3. 直方图图表管线
    // ====================================================================
    void CreateChartPipeline()
    {
        // ---- 3a. 图表 ----
        m_chart = vtkSmartPointer<vtkChartXY>::New();
        m_chart->SetTitle("数据分布直方图 (Histogram)");
        m_chart->SetTitleSize(16);
        m_chart->SetAutoAxes(true);
        m_chart->SetShowLegend(false);

        // X轴设置
        auto axisX = m_chart->GetAxis(vtkAxis::BOTTOM);
        axisX->SetTitle("标量值 (Scalar Value)");
        axisX->SetTitleSize(14);
        axisX->SetLabelSize(12);

        // Y轴设置
        auto axisY = m_chart->GetAxis(vtkAxis::LEFT);
        axisY->SetTitle("频数 (Frequency)");
        axisY->SetTitleSize(14);
        axisY->SetLabelSize(12);

        // ---- 3b. Context View ----
        m_chartView = vtkSmartPointer<vtkContextView>::New();
        m_chartView->GetScene()->AddItem(m_chart);
        m_chartView->SetRenderWindow(
            vtkSmartPointer<vtkGenericOpenGLRenderWindow>::New());
        m_chartView->GetRenderer()->SetBackground(1.0, 1.0, 1.0);
    }

    // ====================================================================
    // 4. 标注部件
    // ====================================================================
    void CreateAnnotations()
    {
        // ---- 4a. 标量条 (Scalar Bar) ----
        m_scalarBar = vtkSmartPointer<vtkScalarBarActor>::New();
        m_scalarBar->SetLookupTable(m_colorFunction);
        m_scalarBar->SetTitle("Intensity");
        m_scalarBar->SetNumberOfLabels(5);
        m_scalarBar->SetBarRatio(0.2);
        m_scalarBar->SetPosition(0.82, 0.1);
        m_scalarBar->SetPosition2(0.1, 0.8);
        m_scalarBar->GetTitleTextProperty()->SetColor(1.0, 1.0, 1.0);
        m_scalarBar->GetTitleTextProperty()->SetFontSize(12);
        m_scalarBar->GetLabelTextProperty()->SetColor(1.0, 1.0, 1.0);
        m_scalarBar->GetLabelTextProperty()->SetFontSize(10);

        // ---- 4b. 方向标记 (Orientation Marker) ----
        m_axesActor = vtkSmartPointer<vtkAxesActor>::New();
        m_axesActor->SetTotalLength(30, 30, 30);
        m_axesActor->SetShaftTypeToCylinder();
        m_axesActor->SetCylinderRadius(0.03);
        m_axesActor->SetConeRadius(0.15);
        m_axesActor->SetAxisLabels(1); // 显示 XYZ 标签
    }

    // ====================================================================
    // 5. 中央部件 (体绘制视图)
    // ====================================================================
    void SetupCentralWidget()
    {
        m_volumeWidget = new QVTKOpenGLNativeWidget(this);

        // 为体绘制创建渲染窗口
        vtkNew<vtkGenericOpenGLRenderWindow> renderWindow;
        m_volumeWidget->setRenderWindow(renderWindow);
        renderWindow->AddRenderer(m_volumeRenderer);

        // 添加标量条
        m_volumeRenderer->AddActor2D(m_scalarBar);

        // 设置方向标记部件
        m_orientationMarker = vtkSmartPointer<vtkOrientationMarkerWidget>::New();
        m_orientationMarker->SetOrientationMarker(m_axesActor);
        m_orientationMarker->SetInteractor(
            renderWindow->GetInteractor());
        m_orientationMarker->SetViewport(0.0, 0.0, 0.2, 0.2);
        m_orientationMarker->SetEnabled(1);
        m_orientationMarker->InteractiveOff(); // 非交互式

        // 设置样式 (使用默认的TrackballCamera样式)

        setCentralWidget(m_volumeWidget);
    }

    // ====================================================================
    // 6. 停靠面板
    // ====================================================================
    void SetupDockWidgets()
    {
        // ---- 6a. 直方图面板 (右上) ----
        m_chartDock = new QDockWidget("数据统计直方图", this);
        m_chartDock->setMinimumSize(350, 280);
        // 创建 chart widget
        {
            // 创建一个 QWidget 包装 chart 的 render window
            auto* chartContainer = new QWidget(m_chartDock);
            auto* chartLayout = new QVBoxLayout(chartContainer);
            chartLayout->setContentsMargins(0, 0, 0, 0);

            m_chartWidget = new QVTKOpenGLNativeWidget(chartContainer);
            m_chartWidget->setRenderWindow(
                m_chartView->GetRenderWindow());
            chartLayout->addWidget(m_chartWidget);

            m_chartDock->setWidget(chartContainer);
        }
        addDockWidget(Qt::RightDockWidgetArea, m_chartDock);

        // ---- 6b. 控制面板 (右下) ----
        m_controlDock = new QDockWidget("控制面板", this);
        m_controlDock->setMinimumSize(350, 350);
        {
            auto* controlWidget = new QWidget(m_controlDock);
            auto* mainLayout = new QVBoxLayout(controlWidget);
            mainLayout->setSpacing(8);

            // =============================================
            // 体绘制模式选择
            // =============================================
            {
                auto* group = new QGroupBox("体绘制模式");
                auto* layout = new QHBoxLayout(group);

                auto* btnAuto = new QPushButton("自动");
                auto* btnCPU  = new QPushButton("CPU光线投射");
                auto* btnGPU  = new QPushButton("GPU体绘制");

                connect(btnAuto, &QPushButton::clicked, this, [this]() {
                    SetVolumeMapperMode(0); });
                connect(btnCPU, &QPushButton::clicked, this, [this]() {
                    SetVolumeMapperMode(1); });
                connect(btnGPU, &QPushButton::clicked, this, [this]() {
                    SetVolumeMapperMode(2); });

                layout->addWidget(btnAuto);
                layout->addWidget(btnCPU);
                layout->addWidget(btnGPU);
                mainLayout->addWidget(group);
            }

            // =============================================
            // 不透明度控制
            // =============================================
            {
                auto* group = new QGroupBox("不透明度传递函数控制点");
                auto* layout = new QVBoxLayout(group);

                const char* tfLabels[] = {
                    "控制点1 (低值)", "控制点2", "控制点3 (中值)",
                    "控制点4", "控制点5 (高值)"
                };
                int defaults[] = { 0, 0, 15, 60, 100 };

                for (int i = 0; i < 5; ++i)
                {
                    auto* row = new QHBoxLayout();
                    auto* label = new QLabel(tfLabels[i]);
                    label->setMinimumWidth(130);
                    m_opacitySliders[i] = new QSlider(Qt::Horizontal);
                    m_opacitySliders[i]->setRange(0, 100);
                    m_opacitySliders[i]->setValue(defaults[i]);
                    m_opacitySliders[i]->setTickPosition(
                        QSlider::TicksBelow);
                    m_opacitySliders[i]->setTickInterval(10);
                    m_opacityLabels[i] = new QLabel(
                        QString::number(defaults[i]) + "%");
                    m_opacityLabels[i]->setMinimumWidth(36);

                    connect(m_opacitySliders[i], &QSlider::valueChanged,
                            this, &MainWindow::OnOpacityChanged);

                    row->addWidget(label);
                    row->addWidget(m_opacitySliders[i]);
                    row->addWidget(m_opacityLabels[i]);
                    layout->addLayout(row);
                }
                mainLayout->addWidget(group);
            }

            // =============================================
            // 操作按钮
            // =============================================
            {
                auto* group = new QGroupBox("操作");
                auto* layout = new QVBoxLayout(group);

                auto* btnResetView = new QPushButton("重置视角 (R)");
                auto* btnScreenshot = new QPushButton("截图保存 (Ctrl+S)");
                auto* btnLoadData = new QPushButton("加载数据文件...");
                auto* btnRegenData = new QPushButton("重新生成合成数据");
                auto* btnHelp = new QPushButton("快捷键帮助 (H)");

                connect(btnResetView, &QPushButton::clicked,
                        this, &MainWindow::ResetView);
                connect(btnScreenshot, &QPushButton::clicked,
                        this, &MainWindow::SaveScreenshot);
                connect(btnLoadData, &QPushButton::clicked,
                        this, &MainWindow::LoadDataFile);
                connect(btnRegenData, &QPushButton::clicked,
                        this, &MainWindow::RegenerateData);
                connect(btnHelp, &QPushButton::clicked,
                        this, &MainWindow::ShowHelpDialog);

                layout->addWidget(btnResetView);
                layout->addWidget(btnScreenshot);
                layout->addWidget(btnLoadData);
                layout->addWidget(btnRegenData);
                layout->addWidget(btnHelp);
                mainLayout->addWidget(group);
            }

            mainLayout->addStretch();
            m_controlDock->setWidget(controlWidget);
        }
        addDockWidget(Qt::RightDockWidgetArea, m_controlDock);

        // 垂直排列两个右侧面板
        splitDockWidget(m_chartDock, m_controlDock, Qt::Vertical);
    }

    // ====================================================================
    // 7. 菜单栏
    // ====================================================================
    void SetupMenuBar()
    {
        // ---- 文件菜单 ----
        QMenu* fileMenu = menuBar()->addMenu("文件(&F)");

        QAction* loadAct = fileMenu->addAction("加载数据...(&L)");
        connect(loadAct, &QAction::triggered,
                this, &MainWindow::LoadDataFile);

        QAction* screenshotAct = fileMenu->addAction("截图保存(&S)");
        screenshotAct->setShortcut(QKeySequence("Ctrl+S"));
        connect(screenshotAct, &QAction::triggered,
                this, &MainWindow::SaveScreenshot);

        fileMenu->addSeparator();

        QAction* exitAct = fileMenu->addAction("退出(&X)");
        exitAct->setShortcut(QKeySequence("Ctrl+Q"));
        connect(exitAct, &QAction::triggered, this, &QMainWindow::close);

        // ---- 视图菜单 ----
        QMenu* viewMenu = menuBar()->addMenu("视图(&V)");

        QAction* resetViewAct = viewMenu->addAction("重置视角(&R)");
        resetViewAct->setShortcut(QKeySequence("R"));
        connect(resetViewAct, &QAction::triggered,
                this, &MainWindow::ResetView);

        viewMenu->addSeparator();

        QAction* viewXAct = viewMenu->addAction("X轴视图");
        connect(viewXAct, &QAction::triggered, this, &MainWindow::SetViewX);
        QAction* viewYAct = viewMenu->addAction("Y轴视图");
        connect(viewYAct, &QAction::triggered, this, &MainWindow::SetViewY);
        QAction* viewZAct = viewMenu->addAction("Z轴视图");
        connect(viewZAct, &QAction::triggered, this, &MainWindow::SetViewZ);

        viewMenu->addSeparator();

        QAction* toggleChartAct = viewMenu->addAction("显示/隐藏直方图");
        connect(toggleChartAct, &QAction::triggered, [this]() {
            m_chartDock->setVisible(!m_chartDock->isVisible());
        });
        QAction* toggleControlAct = viewMenu->addAction("显示/隐藏控制面板");
        connect(toggleControlAct, &QAction::triggered, [this]() {
            m_controlDock->setVisible(!m_controlDock->isVisible());
        });

        // ---- 渲染菜单 ----
        QMenu* renderMenu = menuBar()->addMenu("渲染(&R)");

        QAction* modeAutoAct = renderMenu->addAction("自动模式");
        connect(modeAutoAct, &QAction::triggered,
                this, [this]() { SetVolumeMapperMode(0); });
        QAction* modeCPUAct = renderMenu->addAction("CPU光线投射");
        connect(modeCPUAct, &QAction::triggered,
                this, [this]() { SetVolumeMapperMode(1); });
        QAction* modeGPUAct = renderMenu->addAction("GPU体绘制");
        connect(modeGPUAct, &QAction::triggered,
                this, [this]() { SetVolumeMapperMode(2); });

        renderMenu->addSeparator();

        QAction* shadeToggleAct = renderMenu->addAction("光照开关");
        shadeToggleAct->setCheckable(true);
        shadeToggleAct->setChecked(true);
        connect(shadeToggleAct, &QAction::toggled, [this](bool checked) {
            m_volumeProperty->SetShade(checked ? 1 : 0);
            m_volumeWidget->renderWindow()->Render();
        });

        // ---- 帮助菜单 ----
        QMenu* helpMenu = menuBar()->addMenu("帮助(&H)");

        QAction* helpAct = helpMenu->addAction("快捷键帮助(&K)");
        helpAct->setShortcut(QKeySequence("H"));
        connect(helpAct, &QAction::triggered,
                this, &MainWindow::ShowHelpDialog);

        QAction* aboutAct = helpMenu->addAction("关于(&A)");
        connect(aboutAct, &QAction::triggered, [this]() {
            QMessageBox::about(this, "关于",
                "科学数据可视化工作站 v1.0\n\n"
                "基于 VTK 9.5.2 + Qt 构建\n"
                "VTK教程第23章综合实战项目\n\n"
                "集成技术:\n"
                "  - 三维体绘制 (Smart Volume Mapper)\n"
                "  - 二维图表 (vtkChartXY)\n"
                "  - 传递函数实时编辑\n"
                "  - SMP并行加速\n"
                "  - Qt停靠面板布局\n");
        });
    }

    // ====================================================================
    // 8. 工具栏
    // ====================================================================
    void SetupToolBar()
    {
        QToolBar* toolbar = addToolBar("主工具栏");
        toolbar->setMovable(false);
        toolbar->setIconSize(QSize(24, 24));

        QAction* resetAct = toolbar->addAction("重置");
        connect(resetAct, &QAction::triggered,
                this, &MainWindow::ResetView);

        toolbar->addSeparator();

        QAction* viewXAct = toolbar->addAction("X");
        connect(viewXAct, &QAction::triggered,
                this, &MainWindow::SetViewX);
        QAction* viewYAct = toolbar->addAction("Y");
        connect(viewYAct, &QAction::triggered,
                this, &MainWindow::SetViewY);
        QAction* viewZAct = toolbar->addAction("Z");
        connect(viewZAct, &QAction::triggered,
                this, &MainWindow::SetViewZ);

        toolbar->addSeparator();

        QAction* screenAct = toolbar->addAction("截图");
        connect(screenAct, &QAction::triggered,
                this, &MainWindow::SaveScreenshot);

        toolbar->addSeparator();

        m_modeLabel = new QLabel(" 模式: 自动 ");
        toolbar->addWidget(m_modeLabel);
    }

    // ====================================================================
    // 9. 状态栏
    // ====================================================================
    void SetupStatusBar()
    {
        m_infoLabel = new QLabel("就绪 | 按 H 查看快捷键帮助");
        statusBar()->addPermanentWidget(m_infoLabel);
        statusBar()->showMessage("科学数据可视化工作站启动完成", 3000);
    }

    // ====================================================================
    // 10. 键盘快捷键注册
    // ====================================================================
    void SetupKeyboardShortcuts()
    {
        // 大部分快捷键通过 keyPressEvent 处理
        // 这里注册一些 Qt 原生的快捷键
        QAction* helpShortcut = new QAction(this);
        helpShortcut->setShortcut(QKeySequence("F1"));
        connect(helpShortcut, &QAction::triggered,
                this, &MainWindow::ShowHelpDialog);
        addAction(helpShortcut);
    }

    // ====================================================================
    // 插槽函数
    // ====================================================================

    // ---- 不透明度滑块改变 ----
    void OnOpacityChanged()
    {
        // 读取5个滑块的值并更新不透明度传递函数
        double xPositions[5] = { 0.0, 0.25, 0.5, 0.75, 1.0 };

        m_opacityFunction->RemoveAllPoints();
        for (int i = 0; i < 5; ++i)
        {
            double opacity = m_opacitySliders[i]->value() / 100.0;
            m_opacityFunction->AddPoint(xPositions[i], opacity);
            m_opacityLabels[i]->setText(
                QString::number(m_opacitySliders[i]->value()) + "%");
        }

        m_volumeWidget->renderWindow()->Render();
    }

    // ---- 重置视角 ----
    void ResetView()
    {
        m_volumeRenderer->ResetCamera();
        vtkCamera* camera = m_volumeRenderer->GetActiveCamera();
        camera->SetPosition(200, 200, 400);
        camera->SetFocalPoint(64, 64, 64);
        camera->SetViewUp(0, 1, 0);
        camera->SetClippingRange(1, 2000);

        m_volumeWidget->renderWindow()->Render();
        statusBar()->showMessage("视角已重置", 2000);
    }

    // ---- 重置相机裁剪范围 ----
    void ResetCamera()
    {
        m_volumeRenderer->ResetCameraClippingRange();
        m_volumeWidget->renderWindow()->Render();
        statusBar()->showMessage("相机裁剪范围已重置", 2000);
    }

    // ---- 轴对齐视图 ----
    void SetViewX()
    {
        AlignCameraToAxis(0); // X轴方向
        statusBar()->showMessage("切换到 X 轴视图", 2000);
    }

    void SetViewY()
    {
        AlignCameraToAxis(1); // Y轴方向
        statusBar()->showMessage("切换到 Y 轴视图", 2000);
    }

    void SetViewZ()
    {
        AlignCameraToAxis(2); // Z轴方向
        statusBar()->showMessage("切换到 Z 轴视图", 2000);
    }

    void AlignCameraToAxis(int axis)
    {
        vtkCamera* camera = m_volumeRenderer->GetActiveCamera();
        double center[3] = { 64, 64, 64 };
        double dist = 300.0;

        double pos[3] = { center[0], center[1], center[2] };
        double up[3]  = { 0, 0, 1 };

        switch (axis)
        {
        case 0: // X
            pos[0] += dist; up[0] = 0; up[1] = 1; up[2] = 0;
            break;
        case 1: // Y
            pos[1] += dist; up[0] = 0; up[1] = 0; up[2] = 1;
            break;
        case 2: // Z
            pos[2] += dist; up[0] = 0; up[1] = 1; up[2] = 0;
            break;
        }

        camera->SetPosition(pos);
        camera->SetFocalPoint(center);
        camera->SetViewUp(up);
        camera->SetClippingRange(1, 2000);
        m_volumeRenderer->ResetCameraClippingRange();
        m_volumeWidget->renderWindow()->Render();
    }

    // ---- 设置体绘制映射器模式 ----
    void SetVolumeMapperMode(int mode)
    {
        switch (mode)
        {
        case 0:
            m_volumeMapper->SetRequestedRenderModeToDefault();
            m_modeLabel->setText(" 模式: 自动 ");
            statusBar()->showMessage("体绘制模式: 自动", 3000);
            break;
        case 1:
            m_volumeMapper->SetRequestedRenderModeToRayCast();
            m_modeLabel->setText(" 模式: CPU光线投射 ");
            statusBar()->showMessage("体绘制模式: CPU光线投射", 3000);
            break;
        case 2:
            m_volumeMapper->SetRequestedRenderModeToGPU();
            m_modeLabel->setText(" 模式: GPU体绘制 ");
            statusBar()->showMessage("体绘制模式: GPU体绘制", 3000);
            break;
        }
        m_volumeWidget->renderWindow()->Render();
    }

    // ---- 更新直方图 ----
    void UpdateHistogram()
    {
        if (!m_imageData) return;

        vtkSmartPointer<vtkTable> histTable =
            ComputeHistogram(m_imageData, 64);

        // 清除旧图
        m_chart->ClearPlots();

        // 添加柱状图
        vtkPlot* plot = m_chart->AddPlot(vtkChart::BAR);
        plot->SetInputData(histTable, 0, 1);
        plot->SetColor(70, 130, 180, 200); // 钢板蓝
        plot->SetWidth(0.9);

        m_chartView->GetRenderWindow()->Render();
    }

    // ---- 更新标量条 ----
    void UpdateScalarBar()
    {
        m_scalarBar->SetLookupTable(m_colorFunction);
        m_volumeWidget->renderWindow()->Render();
    }

    // ---- 截图保存 ----
    void SaveScreenshot()
    {
        QString filename = QFileDialog::getSaveFileName(
            this, "保存截图", "screenshot.png",
            "PNG图像 (*.png);;JPEG图像 (*.jpg)");

        if (filename.isEmpty()) return;

        vtkNew<vtkWindowToImageFilter> w2i;
        w2i->SetInput(m_volumeWidget->renderWindow());
        w2i->SetScale(1);
        w2i->SetInputBufferTypeToRGBA();
        w2i->ReadFrontBufferOff();
        w2i->Update();

        vtkNew<vtkPNGWriter> writer;
        writer->SetFileName(filename.toUtf8().constData());
        writer->SetInputConnection(w2i->GetOutputPort());
        writer->Write();

        statusBar()->showMessage(
            "截图已保存: " + filename, 5000);
        std::cout << "[INFO] 截图已保存到: "
                  << filename.toStdString() << std::endl;
    }

    // ---- 加载数据文件 (支持 .vti 格式) ----
    void LoadDataFile()
    {
        QString filename = QFileDialog::getOpenFileName(
            this, "加载体数据文件", QString(),
            "VTK ImageData (*.vti *.vtk);;所有文件 (*)");

        if (filename.isEmpty()) return;

        // 尝试读取 .vti 文件 (XML ImageData)
        vtkNew<vtkXMLImageDataReader> reader;
        reader->SetFileName(filename.toUtf8().constData());

        try
        {
            reader->Update();
            vtkSmartPointer<vtkImageData> loadedData =
                reader->GetOutput();

            if (loadedData && loadedData->GetNumberOfCells() > 0)
            {
                m_imageData = loadedData;

                int* dims = m_imageData->GetDimensions();
                double* range = m_imageData->GetScalarRange();
                std::cout << "[INFO] 加载数据: " << dims[0] << "x"
                          << dims[1] << "x" << dims[2]
                          << " | 范围: [" << range[0] << ", "
                          << range[1] << "]" << std::endl;

                // 更新管线
                m_volumeMapper->SetInputData(m_imageData);
                UpdateHistogram();
                UpdateScalarBar();
                m_volumeWidget->renderWindow()->Render();

                statusBar()->showMessage(
                    "已加载: " + filename, 5000);
            }
        }
        catch (...)
        {
            QMessageBox::warning(this, "加载失败",
                "无法加载文件: " + filename +
                "\n请确保文件为有效的 VTK ImageData 格式 (.vti)");
        }
    }

    // ---- 重新生成合成数据 ----
    void RegenerateData()
    {
        CreateSyntheticData();
        m_volumeMapper->SetInputData(m_imageData);
        UpdateHistogram();
        m_volumeWidget->renderWindow()->Render();
        statusBar()->showMessage("合成数据已重新生成", 3000);
    }

    // ---- 帮助对话框 ----
    void ShowHelpDialog()
    {
        QString helpText =
            "== 科学数据可视化工作站 — 快捷键帮助 ==\n\n"
            "--- 视角控制 ---\n"
            "  R              重置视角\n"
            "  F              重置相机裁剪范围\n"
            "  X / Y / Z      切换到 X/Y/Z 轴视图\n"
            "  鼠标左键拖拽    旋转体数据\n"
            "  鼠标右键拖拽    缩放\n"
            "  鼠标中键拖拽    平移\n"
            "  滚轮           缩放\n\n"
            "--- 渲染模式 ---\n"
            "  1              自动模式\n"
            "  2              CPU 光线投射\n"
            "  3              GPU 体绘制\n\n"
            "--- 其他 ---\n"
            "  Ctrl+S          截图保存为 PNG\n"
            "  H / F1          显示此帮助\n"
            "  Esc             退出程序\n\n"
            "--- SMP并行 ---\n"
            "  设置环境变量 VTK_SMP_MAX_NUM_THREADS=N\n"
            "  控制并行线程数 (N=1,2,4,8,...)\n"
            "  当前编译的SMP后端见启动日志\n\n"
            "--- 提示 ---\n"
            "  - 使用控制面板的不透明度滑块可实时调节传递函数\n"
            "  - 直方图反映了数据值的分布情况\n"
            "  - 标量条显示了颜色到数值的映射关系\n"
            "  - 方向标记 (左下角) 指示当前三维坐标轴方向";

        QMessageBox::information(this, "快捷键帮助", helpText);
    }

    // ---- 打印环境信息 ----
    void PrintEnvironmentInfo()
    {
        std::cout << "==============================================" << std::endl;
        std::cout << "  科学数据可视化工作站 v1.0" << std::endl;
        std::cout << "  VTK 版本: " << VTK_VERSION << std::endl;
        std::cout << "  Qt 版本:  " << QT_VERSION_STR << std::endl;
        std::cout << "==============================================" << std::endl;

        // SMP 信息
        const char* smpThreads = std::getenv("VTK_SMP_MAX_NUM_THREADS");
        if (smpThreads)
        {
            std::cout << "[SMP] 环境变量 VTK_SMP_MAX_NUM_THREADS = "
                      << smpThreads << std::endl;
        }
        else
        {
            std::cout << "[SMP] VTK_SMP_MAX_NUM_THREADS 未设置,"
                      << "使用默认线程数" << std::endl;
        }
    }

private:
    // ====================================================================
    // 成员变量
    // ====================================================================

    // ---- 数据 ----
    vtkSmartPointer<vtkImageData> m_imageData;

    // ---- 体绘制管线 ----
    vtkSmartPointer<vtkSmartVolumeMapper>     m_volumeMapper;
    vtkSmartPointer<vtkVolumeProperty>        m_volumeProperty;
    vtkSmartPointer<vtkPiecewiseFunction>     m_opacityFunction;
    vtkSmartPointer<vtkColorTransferFunction> m_colorFunction;
    vtkSmartPointer<vtkVolume>                m_volume;
    vtkSmartPointer<vtkRenderer>              m_volumeRenderer;

    // ---- 图表管线 ----
    vtkSmartPointer<vtkChartXY>     m_chart;
    vtkSmartPointer<vtkContextView> m_chartView;

    // ---- 标注部件 ----
    vtkSmartPointer<vtkScalarBarActor>          m_scalarBar;
    vtkSmartPointer<vtkAxesActor>               m_axesActor;
    vtkSmartPointer<vtkOrientationMarkerWidget> m_orientationMarker;

    // ---- Qt 部件 ----
    QVTKOpenGLNativeWidget* m_volumeWidget = nullptr;
    QVTKOpenGLNativeWidget* m_chartWidget  = nullptr;
    QDockWidget*            m_chartDock    = nullptr;
    QDockWidget*            m_controlDock  = nullptr;

    // ---- 控制面板滑块 ----
    QSlider* m_opacitySliders[5] = { nullptr };
    QLabel*  m_opacityLabels[5]  = { nullptr };

    // ---- 工具栏 ----
    QLabel* m_modeLabel = nullptr;

    // ---- 状态栏 ----
    QLabel* m_infoLabel = nullptr;
};

// ============================================================================
// 主函数入口
// ============================================================================
int main(int argc, char* argv[])
{
    // ---- Qt 应用初始化 ----
    // 设置 OpenGL 表面格式 (VTK 9.x 要求)
    QSurfaceFormat surfaceFormat;
    surfaceFormat.setRenderableType(QSurfaceFormat::OpenGL);
    surfaceFormat.setVersion(3, 3);
    surfaceFormat.setProfile(QSurfaceFormat::CoreProfile);
    surfaceFormat.setDepthBufferSize(24);
    surfaceFormat.setStencilBufferSize(8);
    surfaceFormat.setSamples(4);
    QSurfaceFormat::setDefaultFormat(surfaceFormat);

    QApplication app(argc, argv);
    app.setApplicationName("Volume3Capstone");
    app.setApplicationVersion("1.0");
    app.setOrganizationName("VTK-Tutorial");

    // ---- 创建并显示主窗口 ----
    MainWindow mainWindow;
    mainWindow.show();

    // ---- 进入事件循环 ----
    return app.exec();
}

// 因为所有代码在单个 cpp 文件中,需要包含 Qt moc 生成文件
#include "main.moc"

23.4 代码详解

本节对上述完整代码中的关键架构决策和核心代码段进行逐一讲解。

23.4.1 整体架构决策

决策1:单文件布局(Single-File Application)

将所有类(MainWindow)和工具函数放在同一个 main.cpp 文件中,而非传统的头文件/实现文件分离。这样做的考量是: - 教学便利性: 读者可以一次性阅读全部代码,无需跨文件跳转。 - 编译简化: 一条 g++ 或 CMake 命令即可编译。 - 实际项目中的替代方案: 在真实开发中,建议将 MainWindow 拆分为 MainWindow.h / MainWindow.cpp,并将工具函数放入独立的 utils.h / utils.cpp

决策2:使用 vtkSmartPointer 管理所有 VTK 对象生命周期

vtkSmartPointer 是 VTK 的引用计数智能指针,在本项目中我们用它替代原始指针,确保对象在不再被引用时自动释放。这是 VTK 9.x 推荐的内存管理方式。

// 示例:自动引用计数管理
vtkSmartPointer<vtkSmartVolumeMapper> mapper =
    vtkSmartPointer<vtkSmartVolumeMapper>::New();
// 当 mapper 超出作用域时自动调用 Delete()

决策3:Qt 与 VTK 的粘合层

QVTKOpenGLNativeWidget 是 VTK 9.x 中推荐的 Qt-VTK 桥接部件(替代旧版的 QVTKWidget / QVTKOpenGLWidget)。它将 VTK 的渲染窗口嵌入 Qt 的部件树中,使得 VTK 渲染可以无缝集成到 Qt 的布局和信号/槽机制中。

m_volumeWidget = new QVTKOpenGLNativeWidget(this);
vtkNew<vtkGenericOpenGLRenderWindow> renderWindow;
m_volumeWidget->setRenderWindow(renderWindow);
renderWindow->AddRenderer(m_volumeRenderer);

23.4.2 合成数据生成

合成数据生成函数 GenerateSyntheticData() 创建了一个包含多个三维高斯团的标量场。这是科学可视化中常见的数据模式,模拟了密度分布、温度场等物理量。

核心算法:

// 多个高斯团叠加
for (int b = 0; b < params.numBlobs; ++b) {
    double dx = i - blobCX[b];
    double dy = j - blobCY[b];
    double dz = k - blobCZ[b];
    double dist2 = dx*dx + dy*dy + dz*dz;
    value += blobAmp[b] * std::exp(-dist2 / sigma2);
}

SMP并行填充: 数据生成使用了 vtkSMPTools::For(),这是一个基于 SMP (Symmetric Multi-Processing) 的并行循环。VTK 9.5.2 支持多种 SMP 后端(STL Threads、OpenMP、Intel TBB),通过该接口可以透明地利用多核 CPU 加速体数据生成。

vtkSMPTools::For(0, totalVoxels, fillVolume, 1);

其中 grain size = 1 确保每个线程分配的工作量大致均匀,对于计算密集型任务而言是合适的选择。

23.4.3 体绘制管线

体绘制管线是本应用的核心,由四个主要组件构成:

1. vtkSmartVolumeMapper(智能体绘制映射器)

m_volumeMapper->SetRequestedRenderModeToDefault();

vtkSmartVolumeMapper 是 VTK 9.x 的推荐选择。它在运行时自动检测硬件能力: - 如果 GPU 支持且 VTK 使用 OpenGL2 编译,则使用 GPU 体绘制(vtkGPUVolumeRayCastMapper)。 - 否则回退到 CPU 光线投射(vtkFixedPointVolumeRayCastMapper)。

用户可以通过菜单或快捷键在三种模式间手动切换。

2. vtkPiecewiseFunction(不透明度传递函数)

传递函数是体绘制中最重要的参数。它将标量数据值映射到不透明度:

m_opacityFunction->AddPoint(0.0,  0.0);   // 背景透明
m_opacityFunction->AddPoint(1.0,  1.0);   // 高值不透明

数据范围越广,需要的控制点越多。本章默认使用了5个控制点,用户通过滑块实时调节。

3. vtkColorTransferFunction(颜色传递函数)

将标量值映射到 RGB 颜色。我们使用了一个经典的"温度色"方案:低值偏蓝黑,中值偏绿,高值偏黄红。

4. vtkVolumeProperty(体属性)

m_volumeProperty->ShadeOn();
m_volumeProperty->SetAmbient(0.1);
m_volumeProperty->SetDiffuse(0.7);
m_volumeProperty->SetSpecular(0.2);

启用光照着色(Shading)可以显著增强深度感知。环境光、漫反射和镜面反射参数的调节影响渲染的真实感。

23.4.4 二维直方图图表

ComputeHistogram() 函数遍历体数据的所有体素,将值分配到64个区间 (bins) 中,然后构建 vtkTable

vtkChartXY 配置步骤:

  1. 创建 vtkChartXY 对象并设置标题和坐标轴标签。
  2. 使用 AddPlot(vtkChart::BAR) 添加柱状图类型。
  3. 调用 SetInputData(table, 0, 1) 将表的第一列作为X轴,第二列作为Y轴。
vtkPlot* plot = m_chart->AddPlot(vtkChart::BAR);
plot->SetInputData(histTable, 0, 1); // 列0 = X, 列1 = Y
plot->SetColor(70, 130, 180, 200);   // RGBA

直方图帮助用户理解数据分布,从而调整传递函数:如果某些值范围体素很少,可以将传递函数中对应位置的 opacity 设低,让主要结构更加突出。

23.4.5 传递函数编辑器

传递函数编辑器通过5个 Qt 水平滑块实现。每个滑块对应归一化数据范围 [0, 1] 上的一个固定位置:

滑块索引 归一化位置 含义
0 0.0 最小值(背景)
1 0.25 低值
2 0.5 中值
3 0.75 较高值
4 1.0 最大值(峰值)

信号-槽连接:

connect(m_opacitySliders[i], &QSlider::valueChanged,
        this, &MainWindow::OnOpacityChanged);

当任一滑块值改变时,OnOpacityChanged() 槽函数会: 1. 从 m_opacityFunction 中移除所有旧的控制点。 2. 根据5个滑块的当前值重新添加5个控制点。 3. 触发渲染窗口刷新。

这种"移除-重建"方式简单可靠,避免了复杂的单个控制点更新逻辑。

23.4.6 标注部件与方向标记

标量条 (vtkScalarBarActor)

标量条是一个二维标注,显示颜色与标量值之间的映射关系:

m_scalarBar->SetLookupTable(m_colorFunction);
m_scalarBar->SetPosition(0.82, 0.1);   // 归一化视口坐标
m_scalarBar->SetPosition2(0.1, 0.8);   // 宽度10%,高度80%

通过 AddActor2D() 添加到渲染器(而非 AddActor()),因为它是一个二维叠加层。

方向标记 (vtkOrientationMarkerWidget)

方向标记是一个小型的坐标系指示器,通常显示在视图的角落:

m_orientationMarker->SetViewport(0.0, 0.0, 0.2, 0.2); // 左下角
m_orientationMarker->SetEnabled(1);
m_orientationMarker->InteractiveOff(); // 非交互式

方向标记帮助用户在旋转体数据时保持空间方位感。设置 InteractiveOff() 使其不可拖动,仅作为静态参考。

23.4.7 SMP并行优化

VTK 9.5.2 的 SMP (Symmetric Multi-Processing) 框架提供了跨后端的并行计算抽象。在本章中,我们在两个环节使用了 SMP:

1. 数据生成并行化:

vtkSMPTools::For(0, totalVoxels, fillVolume, 1);

对于 128^3 = 2,097,152 个体素,每个体素需要计算 6 个高斯函数的指数运算。在多核 CPU 上,SMP 加速比接近核心数。

2. 体绘制映射器内部并行:

vtkSmartVolumeMapper 在选择 CPU 光线投射模式时自动使用 SMP 进行多线程渲染。用户只需设置环境变量:

# Linux / macOS
export VTK_SMP_MAX_NUM_THREADS=4

# Windows (PowerShell)
$env:VTK_SMP_MAX_NUM_THREADS = 4

# Windows (CMD)
set VTK_SMP_MAX_NUM_THREADS=4

注意: SMP 并行效果取决于 VTK 编译时的配置。确保 CMake 选项中启用了至少一个 SMP 后端: - VTK_SMP_ENABLE_SEQUENTIAL=ON (单线程回退) - VTK_SMP_ENABLE_STDTHREAD=ON (C++ 标准线程) - VTK_SMP_ENABLE_OPENMP=ON (OpenMP) - VTK_SMP_ENABLE_TBB=ON (Intel TBB)

23.4.8 键盘快捷键与交互

快捷键通过重写 QMainWindow::keyPressEvent() 实现。每个快捷键执行的操作清晰分离为独立的成员函数:

快捷键 功能 调用的方法
R 重置视角 ResetView()
F 重置相机裁剪 ResetCamera()
X/Y/Z 轴对齐视图 SetViewX/Y/Z()
1/2/3 渲染模式切换 SetVolumeMapperMode()
Ctrl+S 截图保存 SaveScreenshot()
H/F1 帮助对话框 ShowHelpDialog()
Esc 退出 close()

截图实现:

使用 vtkWindowToImageFilter 将渲染窗口内容导出为图像,再用 vtkPNGWriter 保存:

vtkNew<vtkWindowToImageFilter> w2i;
w2i->SetInput(m_volumeWidget->renderWindow());
w2i->SetInputBufferTypeToRGBA();
w2i->Update();

vtkNew<vtkPNGWriter> writer;
writer->SetFileName(filename.toUtf8().constData());
writer->SetInputConnection(w2i->GetOutputPort());
writer->Write();

23.5 全书总结

23.5.1 三卷回顾

恭喜你完成了全部23章的VTK教程!让我们回顾这段学习旅程:

第一卷:基础篇(第1-9章)

你在第一卷中建立了VTK的基础知识体系: - 第1-2章:VTK概述与环境搭建,理解了VTK的设计哲学——管线架构、数据流模型。 - 第3-4章:基本数据类型(vtkImageDatavtkPolyDatavtkUnstructuredGrid等)与数据读写。 - 第5-6章:基础可视化技术——等值面提取(Marching Cubes)、切片、轮廓线、字形图(Glyphs)、流线。 - 第7-8章:颜色映射、标量条、光照与相机控制。 - 第9章:基础交互——Picker、测量工具、自定义交互样式。

第二卷:进阶篇(第10-16章)

第二卷引领你进入专业可视化领域: - 第10-11章:高级体绘制——GPU体绘制、光线投射原理、传递函数设计高级技巧。 - 第12-13章:VTK Charts——折线图、柱状图、散点图、热力图及多图表组合。 - 第14-15章:交互部件——vtkOrientationMarkerWidgetvtkScalarBarWidgetvtkDistanceWidgetvtkImplicitPlaneWidget等。 - 第16章:复合可视化——多视图联动、视图同步、场景组合。

第三卷:实战篇(第17-23章)

第三卷将理论转化为生产力: - 第17-18章:Qt集成基础——QVTKOpenGLNativeWidget、信号/槽机制、多文档界面。 - 第19-20章:并行计算——SMP、OpenMP与TBB集成、大规模数据处理策略。 - 第21-22章:高级交互——自定义交互器、手势识别、高级Widget开发。 - 第23章(本章):综合实战——科学数据可视化工作站,将所有技术整合为一个完整应用。

23.5.2 你已掌握的技能

完成全部23章后,你应该具备以下能力:

  1. 管线架构思维: 理解 Source -> Filter -> Mapper -> Actor -> Renderer 的VTK数据流模型。
  2. 数据类型掌握: 熟练运用 ImageData、PolyData、UnstructuredGrid、StructuredGrid 等核心数据类型。
  3. 体绘制能力: 能够配置和优化传递函数,根据数据类型选择合适的体绘制映射器。
  4. 图表可视化: 能使用 vtkChartXY 创建各种统计图表,实现科学数据的多维展示。
  5. 交互设计: 能使用 VTK Widget 和 Qt 组件构建用户友好的交互界面。
  6. 性能优化: 了解 SMP 并行加速、LOD (Level of Detail) 策略、内存管理等优化技术。
  7. 应用集成: 能将 VTK 渲染无缝集成到 Qt 应用程序中,构建完整的科学可视化软件。

23.5.3 持续学习资源

VTK的学习不会止步于此。以下是推荐的学习资源:

官方资源: - VTK Documentation — 官方文档、类参考和教程 - VTK Examples — 官方示例库(C++、Python、Java) - VTK Textbook — 《The Visualization Toolkit: An Object-Oriented Approach to 3D Graphics》免费在线阅读 - VTK Discourse — 官方论坛,问题解答和社区交流 - VTK GitLab — 源代码仓库,问题跟踪

扩展阅读: - 《The Visualization Handbook》 — Charles D. Hansen, Chris R. Johnson - 《Real-Time Volume Graphics》 — Klaus Engel et al.(体绘制权威参考) - 《Data Visualization: Principles and Practice》 — Alexandru C. Telea - ParaView Guide — ParaView 用户和开发者指南

相关项目: - 3D Slicer — 基于VTK的医学影像分析平台 - Tomviz — 基于VTK的电子断层扫描数据处理工具 - OpenFOAM VTK 集成 — CFD后处理


23.6 未来方向

VTK生态系统持续演进。以下是你在掌握基础后的可能发展方向:

23.6.1 MPI集群可视化

当单机内存无法容纳科学数据集时,需要分布式内存并行方案。VTK提供了 vtkMPIController 和分布式渲染支持:

#include <vtkMPIController.h>
#include <vtkDistributedDataFilter.h>

vtkNew<vtkMPIController> controller;
controller->Initialize(&argc, &argv);

// 分布式数据过滤器
vtkNew<vtkDistributedDataFilter> d3;
d3->SetInputData(localData);
d3->SetController(controller);
d3->Update();

相关VTK模块:ParallelMPIParallelCore

23.6.2 Web部署 (VTK + WebAssembly)

VTK 可以通过 WebAssembly (Wasm) 编译到浏览器环境:

  • vtk.js (Kitware/vtk-js):VTK 的 JavaScript/WebGL 实现,适合网页端可视化。
  • WebAssembly VTK:将 C++ VTK 代码编译为 Wasm,在浏览器中运行原生 VTK 管线。
  • ParaView Web:通过 ParaView 后端 + 前端 Web 框架实现远程可视化。

23.6.3 自定义Filter开发

当内置Filter无法满足需求时,可以开发自定义VTK Filter:

class vtkMyCustomFilter : public vtkImageAlgorithm
{
public:
    static vtkMyCustomFilter* New();
    vtkTypeMacro(vtkMyCustomFilter, vtkImageAlgorithm);

protected:
    int RequestData(vtkInformation*,
                    vtkInformationVector**,
                    vtkInformationVector*) override;
};

关键步骤: 1. 继承合适的基类(vtkImageAlgorithmvtkPolyDataAlgorithm 等)。 2. 实现 RequestData() 方法。 3. 注册到 VTK 的工厂机制(可选)。

23.6.4 Python绑定快速原型开发

VTK 提供完整的 Python 封装(通过 vtkPython 模块),适合快速原型开发:

import vtk

# 快速原型:体绘制
reader = vtk.vtkXMLImageDataReader()
reader.SetFileName("data.vti")

mapper = vtk.vtkSmartVolumeMapper()
mapper.SetInputConnection(reader.GetOutputPort())

volume = vtk.vtkVolume()
volume.SetMapper(mapper)

renderer = vtk.vtkRenderer()
renderer.AddVolume(volume)

renWin = vtk.vtkRenderWindow()
renWin.AddRenderer(renderer)

iren = vtk.vtkRenderWindowInteractor()
iren.SetRenderWindow(renWin)
iren.Start()

Python 版本特别适合: - 算法原型验证 - Jupyter Notebook 交互式分析 - 数据管线快速构建

23.6.5 ParaView:开箱即用的可视化平台

ParaView 是基于 VTK 构建的开源、跨平台、可扩展可视化应用:

  • 无需编程: 图形界面完成大多数可视化任务。
  • Python脚本: 内置 Python 控制台,支持自动化。
  • 并行处理: 内置 MPI 分布式支持。
  • 客户端-服务器架构: 远程渲染大规模数据。

对于不需要定制开发的场景,ParaView 是比从头构建 VTK 应用更高效的选择。

23.6.6 In-Situ可视化 (Catalyst)

Catalyst 是 ParaView 的 In-Situ 可视化库,允许在仿真运行时直接进行可视化分析,避免完整数据写入磁盘:

#include <vtkCPProcessor.h>
#include <vtkCPPythonScriptPipeline.h>

vtkCPProcessor* processor = vtkCPProcessor::New();
processor->Initialize();
vtkCPPythonScriptPipeline* pipeline =
    vtkCPPythonScriptPipeline::New();
pipeline->Initialize("pipeline_script.py");
processor->AddPipeline(pipeline);

// 在仿真循环中调用
processor->RequestDataDescription(&dataDescription);
processor->CoProcess(dataDescription);

In-Situ 可视化在大规模仿真(如CFD、气候建模)中至关重要。

23.6.7 对其他可视化框架的探索

掌握VTK后,你可以更轻松地学习和使用其他可视化框架:

框架 特点 适用场景
OpenGL/Vulkan 底层图形API 需要完全控制渲染管线时
OSPRay Intel光线追踪引擎 照片级渲染、科学可视化
VisIt 大规模并行可视化 HPC集群环境
Mayavi Python 3D可视化 快速科学数据分析
Deck.gl / Kepler.gl Web大规模地理可视化 地理空间数据
VTK.js JavaScript版VTK Web端3D可视化

练习与思考

  1. 功能扩展: 为应用添加 vtkImplicitPlaneWidget 交互式切片功能,使用户能在体数据中定义任意方向的切片并实时查看。
  2. 提示:使用 vtkImageReslice 结合 vtkImplicitPlaneWidget 的输出平面。

  3. 多数据源支持: 扩展 LoadDataFile() 方法以支持 .vts(结构化网格)和 .vtu(非结构化网格)格式,并对不同网格类型使用合适的可视化管线。

  4. 提示:使用 vtkDataSetReader 或对应的 XML 读取器。

  5. 性能测试: 将合成数据的分辨率从 128^3 增加到 256^3 和 512^3,记录不同体绘制模式(自动 / CPU / GPU)在不同分辨率下的渲染帧率和内存占用。

  6. 思考:哪种模式在哪些分辨率下最优?为什么?

  7. 传递函数预设: 添加几个预设的传递函数配置(如"骨骼"、"皮肤"、"血管"),允许用户一键切换。

  8. 提示:为每个预设定义不透明度和颜色的控制点数组。

  9. 多视图联动: 在体绘制视图旁添加一个正交切片视图(横断面、冠状面、矢状面),并实现切片位置与体绘制的联动更新。

  10. 提示:使用 vtkImageReslice 提取切片,使用 vtkImageViewer2 或自定义渲染器显示。

  11. 日志与配置: 添加一个简单的配置文件系统(如 JSON),允许用户保存和恢复以下状态:

  12. 传递函数参数
  13. 相机位置和方向
  14. 窗口布局
  15. 体绘制模式
  16. 提示:使用 vtkJSONSceneExporter / vtkJSONSceneImporter 或自己用 Qt 的 QSettings + JSON 实现。

  17. Linux远程可视化: 如果你的VTK编译了OSMesa支持,尝试在有GPU的服务器上运行应用,并通过X11转发或VNC远程显示。

  18. 思考:远程渲染对交互延迟的影响。

  19. Docker容器化: 为整个项目编写一个 Dockerfile,使其他开发者可以一键构建和运行。

  20. 提示:基础镜像可以选择 nvidia/opengl:1.2-glvnd-runtime

参考资料

  1. VTK 9.5.2 官方文档: https://vtk.org/documentation/
  2. VTK Examples (C++): https://examples.vtk.org/site/Cxx/
  3. VTK Textbook: https://vtk.org/vtk-textbook/ — 第7章(体绘制)、第10章(交互)、第12章(Qt集成)
  4. VTK Discourse: https://discourse.vtk.org/
  5. VTK GitLab: https://gitlab.kitware.com/vtk/vtk
  6. ParaView: https://www.paraview.org/
  7. 3D Slicer: https://www.slicer.org/
  8. Schroeder, W., Martin, K., & Lorensen, B. (2018). The Visualization Toolkit: An Object-Oriented Approach to 3D Graphics (4th ed.). Kitware.
  9. Engel, K., Hadwiger, M., Kniss, J., Rezk-Salama, C., & Weiskopf, D. (2006). Real-Time Volume Graphics. A K Peters/CRC Press.
  10. Telea, A. C. (2014). Data Visualization: Principles and Practice (2nd ed.). A K Peters/CRC Press.

致读者:

当你读到这一行时,意味着你已经走完了这趟长达23章的VTK学习之旅。从第一个管线到构建完整的科学可视化工作站,你不仅学会了VTK的API,更重要的是建立了管线架构思维——一种将数据流经一系列模块进行转换与呈现的工程方法。

可视化是科学与工程的"眼睛"。无论是探索CT扫描中的病灶、分析CFD仿真中的涡流结构,还是理解气候模型中的温度分布,可视化都是连接冰冷数字与人类直觉的桥梁。现在,你已经具备了搭建这座桥梁的能力。

愿你将这些技能应用在你的研究、工程或创作中,也欢迎你加入 VTK 开源社区,贡献代码、文档或教程,将知识传递给下一位学习者。

Happy Visualizing!

—— VTK教程编写组


本章(及全书)完。