【Qwt 7.0 系列】总体架构解析 —— 从单体到三库模块化的演进

【Qwt 7.0 系列】总体架构解析 ------ 从单体到三库模块化的演进

本文是 Qwt 7.0 系列介绍和教程,如果你正在寻找一个高性能、协议友好、同时支持 2D 和 3D 绘图的 Qt 数据可视化库,那么这篇文章就是为你准备的。

系列总述文章:Qwt 7.0 ------ 基于 Qt 的高性能 2D/3D 绘图库

概述 | 高性能曲线绘制 | 常用图表类型 | 高级科学图表 | 多坐标轴与布局 | 交互功能 | 3D 数据可视化 | 坐标轴与刻度 | 控件与辅助元素 | 总体架构解析 | matplotlib 风格绘图

项目地址:GitHub | Gitee | 在线文档

一、引言

如果你用过原版 Qwt 6.x,一定对它的"一大坨"库印象深刻------所有 2D 绘图、控件、工具类全部打包成一个 .so / .dll,构建系统还是老派的 qmake。想要 3D 绘图?得额外引入一个独立的 QwtPlot3D 库,两套 API 风格、两套构建配置,维护起来相当痛苦。

Qwt 7.0 对底层架构进行了彻底重构,将原来的单体库拆分为 core + plot + plot3d 三个独立的共享库,全面迁移到 CMake 构建系统,并内置了原 QwtPlot3D 的 3D 绘图能力。这一架构改进不仅带来了更好的模块隔离和按需编译能力,也为后续的性能优化(如 SIMD 加速)和功能扩展(如 22 种科学 colormap 预设、3D 主题系统)打下了坚实基础。

本文将从架构演进的角度,深入解析 Qwt 7.0 的三库模块化设计。

二、Qwt 6.x 的架构回顾

要理解 Qwt 7.0 为什么这样重构,先得回顾原版的架构痛点。

2.1 单库结构

原版 Qwt 6.2.0 将所有代码编译为一个共享库。无论你只用到一条简单的曲线,还是用到完整的刻度引擎、栅格数据、SVG 导出,链接的都是同一个庞大的库文件。

这带来几个问题:

  • 无法按需链接:即使用户只需要基础曲线绘制,也不得不把 SVG 导出、OpenGL 画布等无关代码全部链接进来
  • 编译耦合:修改任何一个模块的代码,都要重新编译整个库
  • 3D 绘图割裂 :原 QwtPlot3D 是一个完全独立的项目(来自 SciDAVis/qwtplot3d),有自己的一套构建系统和代码风格,与 Qwt 主库毫无代码复用

2.2 qmake 构建系统

原版使用 qmake 的 .pro 文件管理构建。qmake 在 Qt 生态中虽然经典,但存在明显局限:

  • 不支持现代 CMake 的 find_package 机制,第三方项目引入 Qwt 需要手动配置 include 路径和库路径
  • 无法生成标准的 CMake Config 文件,与现代 C++ 项目集成困难
  • 条件编译选项管理不够灵活

2.3 Qt 版本支持

原版 Qwt 6.2.0 主要面向 Qt 5,对 Qt 6 的支持有限。随着 Qt 6 的普及,许多 API 发生了变化(如 QMouseEvent::pos()QMouseEvent::position()),需要大量条件编译代码来兼容。

三、Qwt 7.0 的三库架构

Qwt 7.0 的核心架构改进是将单体库拆分为三个独立的 shared library,每个库有清晰的职责边界。

3.1 三个共享库

目标 CMake target 输出名 DLL 导出宏
公共基础 qwt::core qwtcore.dll / libqwtcore.so QWTCORE_EXPORT
2D 绘图 qwt::plot qwtplot.dll / libqwtplot.so QWT_EXPORT
3D 绘图 qwt::plot3d qwtplot3d.dll / libqwtplot3d.so QWT3D_EXPORT

3.2 依赖关系

三个库的依赖关系非常清晰:

复制代码
    ┌──────────────────────────┐
    │        qwt::core         │  ← 基础工具库(颜色、数学、数据类型、几何、变换、时间等)
    └──────────────────────────┘
           ↗              ↖
  ┌──────────────┐  ┌───────────────┐
  │   qwt::plot   │  │  qwt::plot3d  │
  │     (2D)      │  │     (3D)      │
  └──────────────┘  └───────────────┘

核心原则:plotplot3d 都依赖 core,但彼此互不依赖

这意味着什么?如果你只做 2D 绘图,链接 qwt::plot 就够了,完全不会引入任何 OpenGL / 3D 相关的代码和依赖。反之亦然。这种解耦设计让依赖关系最小化,编译和部署都更加轻量。

3.3 各模块的 Qt 依赖

模块 Qt 依赖(public) Qt 依赖(private) 可选依赖
core Core, Gui --- ---
plot Core, Gui, Widgets Concurrent, PrintSupport OpenGL, OpenGLWidgets(Qt6), Svg
plot3d Core, Gui, Widgets, OpenGL, OpenGLWidgets(Qt6) --- 外部 gl2ps(找不到则内置)

注意 plot3d 还依赖 OpenGL::GLU,这是 3D 渲染的必需品。

关键设计:这些 Qt 依赖在 find_package(qwt) 引入时会自动传递 。你只需要 target_link_libraries(your_app PRIVATE qwt::plot),CMake 会自动把 Qt 的 Core、Gui、Widgets 等依赖全部加上,无需手动指定。

四、Core 模块详解

qwt::core 是整个项目的基础工具库,被 plot 和 plot3d 共享。它包含 28 个模块文件,涵盖了从颜色管理到坐标变换的全部基础能力。

4.1 模块分类

类别 核心文件 说明
全局 qwtcore_global.h 模块导出宏 QWTCORE_EXPORT
颜色工具 qwt_colormap.h, qwt_color_cycle.h, qwt_colormap_preset.h QwtColorMap 及子类、颜色循环、22 种科学 colormap 预设
数学工具 qwt_math.h, qwt_simd_argminmax.h 数学常量与工具函数、SSE2/AVX2/NEON 加速的 argmin/argmax
数据类型 qwt_interval.h, qwt_point_3d.h, qwt_point_polar.h, qwt_samples.h, qwt_box_statistics.h 区间、三维点、极坐标点、样本数据结构、箱线图统计量
几何算法 qwt_bezier.h, qwt_clipper.h 贝塞尔曲线(de Casteljau 算法)、多边形裁剪
坐标变换 qwt_transform.h, qwt_scale_map.h, qwt_scale_div.h, qwt_scale_engine.h 变换函数(线性/对数等)、坐标映射、刻度划分、刻度引擎
时间处理 qwt_date.h, qwt_system_clock.h 日期时间工具、高精度计时器
通用算法 qwt_algorithm.hpp, qwt_qt5qt6_compat.hpp 通用算法模板、Qt5/Qt6 兼容层
数据容器 qwt_grid_data.hpp 网格数据容器
数据系列 qwt_series_data.h, qwt_point_data.h, qwt_series_store.h 数据系列基类模板、点数据实现、数据存储模板
栅格数据 qwt_raster_data.h, qwt_matrix_raster_data.h, qwt_grid_raster_data.h 栅格数据基类、矩阵栅格数据、网格栅格数据(v7 新增)

4.2 为什么要独立出 core

将基础能力独立为 core 库的好处是显而易见的:

  1. 代码复用:plot 和 plot3d 共享同一套颜色映射、坐标变换、数据类型,避免重复代码
  2. 按需编译:如果有人只需要 Qwt 的数学工具或颜色映射(比如用于自定义绘图),可以只链接 core,不需要引入整个 2D/3D 绘图栈
  3. 关注点分离:core 不依赖任何 Widgets,可以用于无 GUI 的后端计算场景

举个例子,core 中的 QwtColorMapPreset 提供了 22 种科学 colormap 预设(viridis、plasma、jet、hot 等),这些预设既可用于 2D 的 QwtPlotSpectrogram,也可通过 ColorMapColor 适配器用于 3D 表面图。一个预设定义,两处复用。

4.3 SIMD 加速

core 模块中的 qwt_simd_argminmax.h 是一个值得关注的性能优化点。它利用 SSE2 / AVX2(x86)和 NEON(ARM)指令集来加速大规模数据的 argmin/argmax 运算。在处理百万级数据点的曲线时,这一优化可以显著减少渲染延迟。

五、Plot 模块详解

qwt::plot 是 2D 绘图的核心模块,承载了原版 Qwt 的全部 2D 绘图能力。

5.1 核心功能

plot 模块包含但不限于:

  • QwtPlot:主绘图容器,支持多轴、寄生绘图(Parasite Plot)
  • 曲线系列:QwtPlotCurveQwtPlotSpectrogramQwtPlotTradingCurve
  • 交互工具:缩放器、平移器、拾取器(Picker)
  • 刻度绘制:QwtScaleDrawQwtDateScaleDraw
  • 图形元素:QwtGraphicQwtColumnSymbolQwtIntervalSymbol
  • 布局管理:QwtDynGridLayout
  • 放大镜:QwtMagnifier

5.2 链接方式

plot 模块通过 PUBLIC 链接依赖 core:

cmake 复制代码
target_link_libraries(${QWTPLOT_LIB_NAME} PUBLIC
    ${QWT_LIB_NAME}::core
    Qt${QT_VERSION_MAJOR}::Core
    Qt${QT_VERSION_MAJOR}::Gui
    Qt${QT_VERSION_MAJOR}::Widgets
)

注意这里用的是 PUBLIC 而非 PRIVATE。这意味着当你的项目链接 qwt::plot 时,core 的头文件路径和符号会自动传递过来。你不需要单独再链接 qwt::core

5.3 可选功能

plot 模块有两个重要的条件编译选项:

  • QWT_CONFIG_QWTOPENGL (默认 ON):启用 OpenGL 画布支持。启用后会额外依赖 Qt::OpenGL(Qt6 还需 Qt::OpenGLWidgets),允许使用 GPU 加速渲染高频数据
  • QWT_CONFIG_QWTSVG (默认 ON):启用 SVG 图像显示和导出,额外依赖 Qt::Svg

如果你不需要这些功能,可以在 CMake 配置时关闭以减少依赖:

cmake 复制代码
cmake -S . -B build -DQWT_CONFIG_QWTOPENGL=OFF -DQWT_CONFIG_QWTSVG=OFF

六、Plot3D 模块详解

qwt::plot3d 是 3D 绘图模块,在 Qwt 7.1 之后正式整合进了主项目。

6.1 整合自 QwtPlot3D

原版 Qwt 6.x 时代,3D 绘图需要单独引入 QwtPlot3D 库,这个库来自 SciDAVis/qwtplot3d,有完全独立的代码库和构建系统。

Qwt 7.0 将 QwtPlot3D 的代码整合进了主项目,统一了构建系统和代码风格,通过 QWT_CONFIG_QWTPLOT_3D 选项控制是否编译。这解决了几个长期痛点:

  • 不再需要维护两套项目
  • 3D 绘图可以直接复用 core 模块的颜色映射、数据类型
  • 用户只需一次 find_package(qwt) 就能同时获得 2D 和 3D 能力

6.2 gl2ps 回退机制

plot3d 模块支持将 3D 场景导出为矢量图(PostScript / PDF),这依赖 gl2ps 库。CMakeLists.txt 中实现了一个优雅的回退机制:

cmake 复制代码
find_library(GL2PS_LIBRARY NAMES gl2ps)
if(GL2PS_LIBRARY)
    # 系统已安装 gl2ps,直接链接
    find_path(GL2PS_INCLUDE_DIR NAMES gl2ps.h REQUIRED)
    target_link_libraries(${QWTPLOT3D_LIB_NAME} ${GL2PS_LIBRARY})
else()
    # 系统没有 gl2ps,编译内置的 3rdparty/gl2ps 源码
    target_sources(${QWTPLOT3D_LIB_NAME} PRIVATE
        "3rdparty/gl2ps/gl2ps.c" "3rdparty/gl2ps/gl2ps.h")
    target_include_directories(${QWTPLOT3D_LIB_NAME} PRIVATE "3rdparty/gl2ps")
endif()

这段逻辑确保了:无论目标系统是否预装了 gl2ps,3D 矢量导出功能都能正常工作。系统有就用系统的,没有就编译内置的。这是"开箱即用"理念的典型实践。

6.3 Qwt3D 主题系统

plot3d 模块引入了 Qwt3D::Qwt3DTheme 主题系统,封装了 3D 绘图的全部视觉属性:

  • 背景色、网格色/线宽
  • 数据 colormap
  • 坐标轴颜色、标题样式
  • 光照预设:NoLightingFlatLightStudioOutdoorSoft
  • 着色模式、材质参数

内置 10 种预设主题:DefaultDarkScientificWarmCoolMatplotlibEarthTonesOceanHighContrastPresentation

cpp 复制代码
// 使用预设主题
plot->applyTheme(Qwt3D::Qwt3DTheme::Dark);

// 手动定制
Qwt3D::Qwt3DTheme theme(Qwt3D::Qwt3DTheme::Scientific);
theme.setDataColorPreset("plasma");  // 使用 core 模块的 colormap 预设
theme.setShininess(20.0);
theme.apply(plot);  // plot 是 Qwt3D::Plot3D* 指针

ColorMapColor 适配器桥接了 core 模块的 QwtColorMap 到 3D 的 Qwt3D::Color 接口,使得 22 种科学 colormap 预设可以直接用于 3D 表面图。这正是三库架构带来的复用优势------core 定义 colormap,plot 和 plot3d 各自消费。

七、CMake 构建系统

Qwt 7.0 全面迁移到 CMake,放弃了 qmake。这是现代化的关键一步。

7.1 find_package 一键引入

安装 Qwt 后,在你的项目中只需:

cmake 复制代码
# 指定 Qwt 的安装目录(如果非默认路径)
set(qwt_DIR "C:/path/to/install/lib/cmake/qwt")

# 一行加载
find_package(qwt REQUIRED)

# 按需链接
target_link_libraries(your_app PRIVATE qwt::plot)       # 只用 2D
# 或者
target_link_libraries(your_app PRIVATE qwt::plot3d)     # 只用 3D
# 或者
target_link_libraries(your_app PRIVATE qwt::plot qwt::plot3d)  # 2D + 3D

链接 qwt::plot 会自动传递 qwt::core 及其 Qt 依赖(Core、Gui、Widgets);链接 qwt::plot3d 会自动传递 qwt::core、Qt OpenGL 系列依赖和 OpenGL::GLU。你不需要手动管理任何传递依赖。

7.2 三个独立的 CMake target

每个库都创建了命名空间别名,使用现代 CMake 的 ALIAS 机制:

cmake 复制代码
# core 模块
add_library(${QWT_LIB_NAME}::${QWTCORE_LIB_NAME} ALIAS ${QWTCORE_LIB_NAME})
# plot 模块
add_library(${QWT_LIB_NAME}::${QWTPLOT_LIB_NAME} ALIAS ${QWTPLOT_LIB_NAME})
# plot3d 模块
add_library(${QWT_LIB_NAME}::${QWTPLOT3D_LIB_NAME} ALIAS ${QWTPLOT3D_LIB_NAME})

用户始终通过 qwt::coreqwt::plotqwt::plot3d 这三个带命名空间的 target 来引用,符合现代 CMake 的最佳实践。

7.3 条件编译选项

CMake 选项 默认 控制内容
QWT_CONFIG_QWTPLOT ON 核心绘图:QwtPlot、曲线、网格、缩放等
QWT_CONFIG_QWTPOLAR ON 极坐标绘图子模块
QWT_CONFIG_QWTWIDGETS ON 控件:滑块、旋钮、刻度盘、温度计等
QWT_CONFIG_QWTSVG ON SVG 导出/渲染
QWT_CONFIG_QWTOPENGL ON OpenGL 画布
QWT_CONFIG_QWTPLOT_3D ON 3D 绘图模块
QWT_CONFIG_BUILD_EXAMPLE ON 构建示例程序
QWT_CONFIG_BUILD_PLAYGROUND ON 构建实验性代码
QWT_CONFIG_BUILD_STATIC_EXAMPLE ON 构建静态链接示例
QWT_CONFIG_BUILD_TESTS OFF 测试构建

例如,只构建核心库,不编译示例和实验代码:

cmake 复制代码
cmake -S . -B build -G "Visual Studio 16 2019" -A x64 ^
    -DCMAKE_PREFIX_PATH="D:/Qt/6.7.3/msvc2019_64" ^
    -DQWT_CONFIG_BUILD_EXAMPLE=OFF ^
    -DQWT_CONFIG_BUILD_PLAYGROUND=OFF

7.4 与 qmake 的对比

对比项 qmake (Qwt 6.x) CMake (Qwt 7.0)
包发现 手动配置 include/lib 路径 find_package(qwt) 标准 CMake 机制
依赖传递 无自动传递 PUBLIC/PRIVATE 依赖自动传递
条件编译 .pro 文件中 define 标准 CMake option
跨平台 需多套 .pro 单一 CMakeLists.txt
IDE 支持 Qt Creator 为主 CLion / VS / Qt Creator / VSCode 全支持

八、PIMPL 模式

Qwt 7.0 在 PIMPL(Pointer to Implementation)模式上有一个值得注意的设计:它没有使用 Qt 标准的 Q_DECLARE_PRIVATE,而是自定义了一套宏。

8.1 自定义宏体系

这套宏定义在 qwt_global.h 中:

cpp 复制代码
// 头文件:声明 PIMPL
class QWT_EXPORT QwtFoo : public QWidget {
    QWT_DECLARE_PRIVATE(QwtFoo)  // → class PrivateData; unique_ptr<PrivateData> m_data;
};

// 源文件:实现 PrivateData
class QwtFoo::PrivateData {
    QWT_DECLARE_PUBLIC(QwtFoo)  // → QwtFoo* q_ptr;
};

// 构造函数初始化
QwtFoo::QwtFoo() : QWT_PIMPL_CONSTRUCT {}  // → m_data(qwt_make_unique<PrivateData>(this))

// 访问私有时使用
QWT_D(d);     // PrivateData* d = d_func()
QWT_DC(d);    // const PrivateData* d = d_func()

8.2 与 Qt 标准 PIMPL 的区别

Qt 标准的 Q_DECLARE_PRIVATE 使用的是间接指针(d_ptr 指向 QObjectPrivate 子类),而 Qwt 的自定义方案有一个关键区别:

  • m_data 直接存储 PrivateData (通过 unique_ptr),不使用堆指针重定向
  • 非_const_ 方法通过 QWT_D() 访问私有数据
  • const 方法通过 QWT_DC() 访问私有数据

这种设计简化了私有数据的访问路径,同时在 const 正确性上更加严格------QWT_DC 确保 const 方法只能 const 访问私有数据。

九、Qt5/Qt6 兼容层

Qwt 7.0 同时支持 Qt 5.12+ 和 Qt 6.x。为了优雅地处理两个版本之间的 API 差异,core 模块提供了 qwt::compat 命名空间下的兼容层(定义在 qwt_qt5qt6_compat.hpp 中)。

9.1 核心兼容函数

cpp 复制代码
// 鼠标/触摸事件位置
// Qt5: event->pos()              → QPoint
// Qt6: event->position().toPoint() → QPoint
qwt::compat::eventPos(event);

// 滚轮事件增量
// Qt5: event->delta()           → int
// Qt6: event->angleDelta().y()  → int
qwt::compat::wheelEventDelta(event);

// 字体度量(文本宽度)
// Qt5.12-: fm.width(str)            → int
// Qt5.12+: fm.horizontalAdvance(str) → int
qwt::compat::horizontalAdvance(fm, str);

9.2 设计理念

这套兼容层的理念是:一处封装,处处使用 。所有与 Qt 版本相关的 API 差异都集中在 qwt::compat 命名空间中处理,业务代码只调用兼容函数,不需要写任何 #if QT_VERSION 条件编译。

对比一下没有兼容层时的写法:

cpp 复制代码
// 没有兼容层 ------ 到处都是条件编译
QPoint pos;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    pos = event->position().toPoint();
#else
    pos = event->pos();
#endif

// 有兼容层 ------ 一行搞定
QPoint pos = qwt::compat::eventPos(event);

这使得代码可读性大幅提升,也降低了维护成本。当未来需要支持新的 Qt 版本时,只需修改兼容层即可。

十、与 Qwt 6.x 架构对比总结

对比维度 Qwt 6.x Qwt 7.0
库结构 单体库(一个 .so/.dll) 三库:core + plot + plot3d
构建系统 qmake (.pro) CMake (CMakeLists.txt)
3D 绘图 需独立引入 QwtPlot3D 库 内置 plot3d 模块,统一管理
包引入 手动配置路径 find_package(qwt) 标准机制
Qt 版本 主要 Qt5 Qt 5.12+ / Qt 6.x 双支持
兼容方案 #if QT_VERSION 散落各处 qwt::compat 命名空间统一封装
PIMPL 模式 标准 Qt Q_DECLARE_PRIVATE 自定义宏 QWT_DECLARE_PRIVATE / QWT_D / QWT_DC
颜色映射 基础 colormap 类 core 模块 + 22 种科学预设 + 3D 适配器
性能优化 SIMD 加速 argmin/argmax(SSE2/AVX2/NEON)
矢量导出(3D) 依赖外部 gl2ps 内置 gl2ps 回退,开箱即用
条件编译 .pro 中的 define 标准 CMake option,粒度更细

十一、总结

Qwt 7.0 的三库模块化架构是一次深思熟虑的重构。它不是简单地把代码拆成三份,而是重新梳理了模块间的依赖关系:

  • core 作为基础工具库,提供颜色、数学、数据类型、坐标变换等通用能力,被 plot 和 plot3d 共享
  • plot 专注于 2D 绘图,按需引入 OpenGL 和 SVG
  • plot3d 整合了原 QwtPlot3D,内置 gl2ps 回退,通过 ColorMapColor 适配器复用 core 的 colormap 预设

配合 CMake 构建系统、qwt::compat 兼容层和自定义 PIMPL 宏,Qwt 7.0 在保持 API 兼容性的同时,实现了现代化的工程架构。无论你是只需要简单的 2D 曲线,还是需要完整的 2D + 3D 科学可视化方案,都可以通过 find_package(qwt) 按需引入,不多不少。

这一架构为后续的性能优化和功能扩展打下了坚实基础。在后续系列文章中,我们将深入各个模块的具体实现细节。

系列文章

系列总述:Qwt 7.0 ------ 基于 Qt 的高性能 2D/3D 绘图库


相关链接

相关推荐
xcyxiner2 小时前
DicomViewer (编译以及优化)8
qt
旖-旎2 小时前
QT界面优化(6)
开发语言·c++·qt
特立独行的猫a2 小时前
为 HarmonyOS/OpenHarmony 构建第三方库的解决方案(转自Qt官方Blog)
qt·华为·harmonyos·三方库·鸿蒙pc
零点零一2 小时前
QT 5升级到 Qt 6 使用 Clazy 检查将 C++ 应用程序移植到 Qt 6
开发语言·c++·qt
初阳7852 小时前
【Qt】系统相关(1)——事件
qt
尘中远2 小时前
【Qwt 7.0 系列】高级科学图表 —— 光谱图、向量场、K线图与极坐标绘图
qt·数据可视化·qwt·工业软件·科学绘图·云图·向量场
不想学习!!2 小时前
Qt Quick 常用控件入门:Window、Button、CheckBox 与 RadioButton
qt·qml
qq_4017004114 小时前
Qt QSS 完全入门写出漂亮界面以及解决样式不生效问题
开发语言·qt
旖-旎15 小时前
QT系统篇(5)(下)
开发语言·c++·qt