ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Qt qwt图表综合应用源码解析:实时曲线、柱状图与仪表盘实战

Qt qwt图表综合应用源码解析:实时曲线、柱状图与仪表盘实战

简介:这份资源是面向Qt开发者的QWT图表综合应用源码集合,适合科学可视化、工程绘图与数据分析场景,帮助读者从实例入手掌握QWT的图表组件用法与底层绘制机制。压缩包共25个文件,以9个cpp源文件、8个h头文件、6个ui界面文件为主,另含pro工程文件与user配置,整体约27KB,结构紧凑便于逐模块研读。源码覆盖线图、散点图、条形图、饼图、极坐标图等常见图表类型,并涉及数据配置、样式调整、图例管理、鼠标交互与拖拽缩放、动态数据更新、自定义绘图元素、布局嵌入以及数据导入导出等实践要点,可帮助开发者理解如何将图表与Qt窗口控件协同工作。目前已有171人学习下载,适合希望提升Qt图形编程能力、需要参考完整示例进行二次开发的初、中级开发者。

1. 拿到一份 qwt 图表综合应用集合源码,我建议先跑通再谈改造

如果你正在用 Qt 做工业上位机、监控看板或者数据采集软件,大概率绕不开一个名字:qwt。它是一套基于 Qt 的二维绘图控件库,曲线、柱状、仪表盘、刻度轴这些常见图表元素都能直接拿来用。但 qwt 官方只给了基础控件和示例,真正落到项目里,怎么把多条曲线叠在一张图上、怎么让坐标轴跟着数据动态滚动、怎么把图例和十字准星联动起来,这些“组合拳”官方文档讲得并不细。这份 qwt 图表综合应用集合源码,就是把这些高频场景打包成可运行的工程,适合两类人:一类是刚接触 qwt、想找个能跑起来的参照物;另一类是做了一段时间 Qt 界面、想把图表模块抽出来复用的开发者。我拿到这类源码的习惯是先把工程跑起来看效果,再回头拆结构,最后才动代码,顺序反了容易在编译错误里绕圈。

2. 工程结构与技术栈:先看清 qwt 在 Qt 项目里怎么落位

2.1 目录布局与模块划分

这类综合应用集合通常不会只有一个.pro文件,而是按功能拆成多个子工程。我见过的典型布局是这样:

qwt_chart_demo/ ├── qwt_chart_demo.pro # 顶层工程,用 subdirs 聚合 ├── common/ # 公共封装 │ ├── chartbase.h/cpp # 图表基类,统一初始化 qwt 控件 │ └── datagenerator.h/cpp # 模拟数据源 ├── curve_demo/ # 实时曲线 │ ├── curve_demo.pro │ └── mainwindow.h/cpp ├── bar_demo/ # 柱状图与频谱 │ ├── bar_demo.pro │ └── mainwindow.h/cpp ├── dial_demo/ # 仪表盘与指针 │ ├── dial_demo.pro │ └── mainwindow.h/cpp └── third_party/ └── qwt/ # qwt 源码或预编译库

顶层.pro用TEMPLATE = subdirs把各子工程串起来,SUBDIRS里按依赖顺序排列。common一般放在最前面,因为其他模块会链接它。third_party/qwt里放的是 qwt 的源码或者编译好的.lib/.so,具体是哪种取决于作者习惯。我一般会优先找qwt.pro或者qwt.pri,有.pri说明作者做了 qmake 的模块化封装,集成起来更省事。

提示:如果third_party/qwt里只有头文件和库文件,没有.pro,说明作者是预编译后直接拷贝进来的,这时候要确认库的编译器和 Qt 版本跟你本机一致,否则链接阶段会报一堆未定义符号。

2.2 技术栈与依赖关系

核心依赖就三层:Qt 本身(通常是 Qt 5.x,少数老工程用 Qt 4.x)、qwt 库、以及可选的 Qt Charts(有些工程会同时用两套绘图方案做对比)。qwt 的版本从 6.1 到 6.2 都有,6.2 对 Qt 5.15 的支持更好,如果工程里用了QwtPlotLayout或者QwtScaleWidget的高级特性,版本差异会直接体现在编译错误上。

.pro文件里关键的几行通常长这样:

QT += core gui widgets CONFIG += c++11 # qwt 头文件路径 INCLUDEPATH += $$PWD/third_party/qwt/include # qwt 库路径,区分 debug 和 release win32 { CONFIG(debug, debug|release) { LIBS += -L$$PWD/third_party/qwt/lib -lqwtd } else { LIBS += -L$$PWD/third_party/qwt/lib -lqwt } } unix { LIBS += -L$$PWD/third_party/qwt/lib -lqwt }

这里有个血泪经验:Windows 下 qwt 的 debug 库和 release 库名字不同,debug 版带d后缀(qwtd.lib),release 版不带(qwt.lib)。如果只写-lqwt,debug 编译时就会找不到库。上面用CONFIG(debug, debug|release)做条件判断是标准做法,但很多新手会漏掉。

2.3 编译与运行的最小步骤

假设你拿到的是完整源码包,按下面顺序操作:

# 1. 确认 Qt 环境,qmake 在 PATH 里 qmake -v # 2. 进入顶层目录,生成 Makefile cd qwt_chart_demo qmake qwt_chart_demo.pro # 3. 编译,-j 后面跟 CPU 核心数 make -j8 # Linux/macOS # 或者 Windows 下用 nmake / mingw32-make

如果 qwt 是源码形式放在third_party里,需要先单独编译 qwt:

cd third_party/qwt qmake qwt.pro make -j8 make install # 可选,安装到系统目录

编译通过后,每个子工程目录下会生成可执行文件。先跑curve_demo,因为它最能体现 qwt 的核心能力——实时数据刷新和坐标轴联动。运行后如果看到曲线在动、坐标轴跟着滚,说明 qwt 库链接没问题,环境就算通了。

3. 核心模块拆解:实时曲线、柱状图与仪表盘的关键实现

3.1 实时曲线:QwtPlot + QwtPlotCurve 的数据刷新链路

实时曲线是 qwt 用得最多的场景。核心类就两个:QwtPlot作为画布,QwtPlotCurve作为曲线对象。数据刷新一般用QTimer定时触发,每次追加一个新点,然后调用replot()。

// curve_demo/mainwindow.cpp 核心片段 #include <qwt_plot.h> #include <qwt_plot_curve.h> #include <qwt_plot_grid.h> #include <QTimer> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { // 1. 创建画布 plot = new QwtPlot(this); plot->setTitle("实时数据监控"); plot->setCanvasBackground(Qt::black); // 2. 加网格,方便读数 QwtPlotGrid *grid = new QwtPlotGrid(); grid->enableXMin(true); grid->enableYMin(true); grid->setMajorPen(Qt::gray, 0, Qt::DotLine); grid->attach(plot); // 3. 创建曲线 curve = new QwtPlotCurve("通道1"); curve->setPen(Qt::green, 2); curve->setRenderHint(QwtPlotItem::RenderAntialiased, true); curve->attach(plot); // 4. 定时器驱动数据更新 timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &MainWindow::updateData); timer->start(50); // 20Hz 刷新率 // 5. 初始化数据缓冲区 for (int i = 0; i < MAX_POINTS; ++i) { xData[i] = i; yData[i] = 0; } curve->setSamples(xData, yData, MAX_POINTS); plot->replot(); } void MainWindow::updateData() { // 数据整体左移一位,末尾追加新值 for (int i = 0; i < MAX_POINTS - 1; ++i) { yData[i] = yData[i + 1]; } yData[MAX_POINTS - 1] = generateRandomValue(); // 重新设置样本并重绘 curve->setSamples(xData, yData, MAX_POINTS); plot->replot(); }

这段代码的逻辑链条很清晰:QwtPlot负责画布和坐标轴,QwtPlotCurve负责曲线样式和数据,setSamples把数据指针交给曲线对象,replot()触发重绘。参数上,timer->start(50)决定刷新率,50ms 对应 20Hz,工业场景够用;如果数据量再大,比如 10000 点以上,replot()会明显卡顿,这时候要改用QwtPlotDirectPainter做局部刷新,只画新增的那一段。

注意:setSamples传的是指针,数据数组的生命周期必须覆盖曲线对象的整个使用期。如果数组是局部变量,函数返回后指针就悬空了,曲线会画出随机值或者直接崩溃。我一般把数据缓冲区作为类的成员变量,或者用QVector<double>动态管理。

3.2 柱状图与频谱:QwtPlotBarChart 的定制

柱状图在频谱显示、统计报表里很常见。qwt 提供了QwtPlotBarChart,但默认样式比较朴素,实际项目里通常要自定义颜色和间距。

// bar_demo/mainwindow.cpp 核心片段 #include <qwt_plot_barchart.h> #include <qwt_column_symbol.h> class BarChart : public QwtPlotBarChart { public: BarChart(const QString &title = QString()) : QwtPlotBarChart(title) { setLegendMode(QwtPlotBarChart::LegendBarTitles); setSymbol(new QwtColumnSymbol(QwtColumnSymbol::Box)); } protected: // 重写这个函数,为每个柱子设置不同颜色 QwtColumnSymbol *specialSymbol(int sampleIndex, const QPointF &) const override { QwtColumnSymbol *symbol = new QwtColumnSymbol(QwtColumnSymbol::Box); symbol->setLineWidth(2); symbol->setFrameStyle(QFrame::Plain | QFrame::Box); // 根据索引渐变着色 QColor color = QColor::fromHsv((sampleIndex * 30) % 360, 200, 200); symbol->setPalette(QPalette(color)); return symbol; } };

specialSymbol是QwtPlotBarChart的虚函数,每个柱子绘制前都会调用一次。返回的QwtColumnSymbol决定柱子的外观,包括颜色、边框、线宽。上面用 HSV 色环按索引取色,做出来的频谱图每个频段颜色不同,视觉上更容易区分。参数上,setLineWidth(2)控制边框粗细,QFrame::Plain | QFrame::Box是边框样式,这些都可以按项目 UI 规范调整。

3.3 仪表盘:QwtDial 与 QwtAnalogClock 的指针联动

仪表盘类控件在 qwt 里对应QwtDial,它本身是个抽象基类,实际用的时候要么继承它,要么用现成的QwtAnalogClock。做工业仪表时,常见需求是指针角度范围、刻度标签、以及指针颜色随数值变化。

// dial_demo/mainwindow.cpp 核心片段 #include <qwt_dial.h> #include <qwt_dial_needle.h> class GaugeDial : public QwtDial { public: GaugeDial(QWidget *parent = nullptr) : QwtDial(parent) { setRange(0, 100); // 量程 setScaleArc(220, -40); // 刻度弧起止角度 setWrapping(false); // 不循环 setReadOnly(true); // 只读,禁止鼠标拖动 // 设置指针样式 QwtDialSimpleNeedle *needle = new QwtDialSimpleNeedle( QwtDialSimpleNeedle::Arrow, true, Qt::red, Qt::darkRed); setNeedle(needle); // 刻度主次间隔 setScaleMaxMajor(10); setScaleMaxMinor(5); } // 数值变化时改变指针颜色 void setValueWithColor(double value) { setValue(value); QwtDialSimpleNeedle *needle = static_cast<QwtDialSimpleNeedle *>(this->needle()); if (value > 80) { needle->setColor(Qt::red); } else if (value > 50) { needle->setColor(Qt::yellow); } else { needle->setColor(Qt::green); } update(); } };

setScaleArc(220, -40)这两个参数控制刻度弧的起始和结束角度,单位是度,0 度在正上方,顺时针为正。setScaleMaxMajor(10)表示主刻度分 10 段,setScaleMaxMinor(5)表示每两个主刻度之间有 5 个小刻度。指针颜色联动是通过setValueWithColor手动实现的,先setValue更新数值,再根据阈值改指针颜色,最后update()触发重绘。这套逻辑在监控报警场景里很实用,数值超标时指针变红,操作员一眼就能看到。

4. 避坑与排查:qwt 集成时最容易翻车的几个点

4.1 编译报错 “undefined reference to `QwtPlot::QwtPlot‘”

现象:.pro里已经写了LIBS += -lqwt,但链接阶段还是报一堆 qwt 类的未定义引用。

原因:库路径不对,或者库文件跟当前编译器不匹配。Windows 下 MinGW 编译的 qwt 库不能给 MSVC 用,反之亦然。另外,如果 qwt 是 debug 版但工程是 release 编译,也会出现这个错误。

解决:先确认LIBS里的-L路径指向的目录下确实有qwt.lib或libqwt.a。然后用dumpbin /symbols qwt.lib | findstr QwtPlot(Windows)或nm -C libqwt.a | grep QwtPlot(Linux)检查库里的符号是否存在。最后确认编译器和 Qt 版本跟 qwt 库的编译环境一致。

4.2 曲线刷新时界面卡顿

现象:数据量不大(几千点),但replot()调用频繁时界面明显掉帧,鼠标拖动窗口都有延迟。

原因:replot()会重绘整个画布,包括坐标轴、网格、图例,数据量越大耗时越长。如果刷新率超过 30Hz,累积的绘制任务会阻塞 UI 线程。

解决:改用QwtPlotDirectPainter做增量绘制,只画新增的数据段。或者降低刷新率,把QTimer间隔从 20ms 调到 50ms 甚至 100ms。另一个思路是把数据缓冲和绘制分离,用双缓冲队列,绘制线程只取最新一帧。

4.3 坐标轴标签中文乱码

现象:setAxisTitle设置的中文标题显示成方块或问号。

原因:qwt 默认字体不支持中文,或者源码文件编码跟编译器解析编码不一致。

解决:在main函数里显式设置应用字体:

int main(int argc, char *argv[]) { QApplication app(argc, argv); QFont font("Microsoft YaHei", 10); app.setFont(font); // ... }

同时确保.cpp文件保存为 UTF-8 带 BOM(Windows 下 MSVC 需要 BOM 才能正确识别中文字符串)。

4.4 图例位置遮挡曲线

现象:QwtPlotLegendItem默认放在右上角,数据曲线刚好经过那个区域时被挡住。

原因:图例是独立绘制的 item,默认位置固定,不会自动避让。

解决:调用setLegendAttribute或者手动设置图例的setMaxColumns和setAlignment,把图例移到画布外侧。更彻底的做法是继承QwtPlotLegendItem,重写draw方法,根据曲线数据范围动态计算图例位置。

4.5 多曲线时颜色重复

现象:创建了 5 条曲线,但颜色只有两三种在循环,分不清哪条是哪条。

原因:QwtPlotCurve不会自动分配颜色,每次new出来的曲线默认用同一个 pen。

解决:维护一个颜色表,创建曲线时按索引取色:

QList<QColor> colors = {Qt::red, Qt::green, Qt::blue, Qt::cyan, Qt::magenta}; for (int i = 0; i < curveCount; ++i) { QwtPlotCurve *curve = new QwtPlotCurve(QString("通道%1").arg(i + 1)); curve->setPen(colors[i % colors.size()], 2); curve->attach(plot); }

5. 二次开发改造:从能跑到好用,我一般会动这几个地方

5.1 把图表模块抽成独立组件

原始工程里,每个 demo 的MainWindow都直接持有QwtPlot和曲线对象,代码耦合度不低。二次开发时,我一般会先抽一个ChartWidget类,把画布、曲线管理、数据更新、坐标轴配置都封进去,对外只暴露addCurve、appendData、setAxisRange这几个接口。这样主窗口只负责布局和业务逻辑,图表相关的改动都收敛在一个文件里。

// chartwidget.h 接口设计 class ChartWidget : public QWidget { Q_OBJECT public: explicit ChartWidget(QWidget *parent = nullptr); int addCurve(const QString &name, const QColor &color); void appendData(int curveId, double x, double y); void setAxisRange(int axisId, double min, double max); void setRefreshRate(int ms); private: QwtPlot *m_plot; QTimer *m_timer; QVector<QwtPlotCurve *> m_curves; QVector<QVector<double>> m_xBuffers; QVector<QVector<double>> m_yBuffers; };

抽出来之后,curve_demo、bar_demo、dial_demo都可以复用这个组件,只是配置不同。改造的验证方法很简单:把curve_demo的MainWindow里所有跟QwtPlot直接相关的代码删掉,换成ChartWidget,如果编译通过且运行效果跟原来一致,说明抽象层没漏东西。

5.2 数据源与绘制解耦

原始工程用QTimer在 UI 线程里直接生成数据并刷新,数据量小的时候没问题,但接入真实设备后,数据来自串口或网络,回调频率不稳定,直接在回调里replot()容易出问题。我一般会加一个线程安全的环形缓冲区,数据采集线程只负责写,UI 线程定时读最新一批数据再刷新。

// 环形缓冲区简化实现 template <typename T> class RingBuffer { public: explicit RingBuffer(int capacity) : m_capacity(capacity), m_head(0), m_size(0) { m_data.resize(capacity); } void push(const T &value) { QMutexLocker locker(&m_mutex); m_data[m_head] = value; m_head = (m_head + 1) % m_capacity; if (m_size < m_capacity) ++m_size; } QVector<T> latest(int count) { QMutexLocker locker(&m_mutex); count = qMin(count, m_size); QVector<T> result(count); int start = (m_head - count + m_capacity) % m_capacity; for (int i = 0; i < count; ++i) { result[i] = m_data[(start + i) % m_capacity]; } return result; } private: QVector<T> m_data; int m_capacity; int m_head; int m_size; QMutex m_mutex; };

这个缓冲区的好处是读写分离,采集线程只管push,UI 线程定时latest取一批,取的时候加锁时间很短,不会阻塞采集。改造后,updateData里不再生成随机数,而是从缓冲区取真实数据。

5.3 样式与交互的定制技巧

qwt 的默认样式偏工程风,如果项目 UI 要求更现代,有几个地方可以改:画布背景用setCanvasBackground设成深色或渐变;坐标轴用setAxisFont和setAxisLabelAlignment调整字体和对齐;曲线用setRenderHint(QwtPlotItem::RenderAntialiased, true)开抗锯齿。交互方面,QwtPlotPicker可以实现十字准星和数据点提示,QwtPlotZoomer支持框选放大,这两个类在common目录里通常有封装好的示例。

我踩过的一个坑是:开了抗锯齿之后,曲线在低刷新率下会有拖影,尤其是深色背景上。后来发现是replot()没有清屏导致的,需要在replot()之前调用plot->canvas()->repaint(),或者用QwtPlotDirectPainter的setClipping控制重绘区域。从那以后我每次改绘图相关代码,都会先跑一遍高刷新率场景,确认没有残影再继续。

希望这份拆解能帮你把 qwt 图表模块顺利跑起来,改造的时候少走点弯路。源码包里的每个 demo 都值得单独跑一遍,跑通了再动手改,比对着文档空想快得多。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进