首页 / 资讯中心 / 文章详情

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

Qt qwt图表综合应用源码解析:实时曲线、柱状图与仪表盘实战 ★ FEATURED ARTICLE
简介这份资源是面向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 c11 # 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.librelease 版不带qwt.lib。如果只写-lqwtdebug 编译时就会找不到库。上面用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里需要先单独编译 qwtcd 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传的是指针数据数组的生命周期必须覆盖曲线对象的整个使用期。如果数组是局部变量函数返回后指针就悬空了曲线会画出随机值或者直接崩溃。我一般把数据缓冲区作为类的成员变量或者用QVectordouble动态管理。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_castQwtDialSimpleNeedle *(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 QwtPlotWindows或nm -C libqwt.a | grep QwtPlotLinux检查库里的符号是否存在。最后确认编译器和 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 带 BOMWindows 下 MSVC 需要 BOM 才能正确识别中文字符串。4.4 图例位置遮挡曲线现象QwtPlotLegendItem默认放在右上角数据曲线刚好经过那个区域时被挡住。原因图例是独立绘制的 item默认位置固定不会自动避让。解决调用setLegendAttribute或者手动设置图例的setMaxColumns和setAlignment把图例移到画布外侧。更彻底的做法是继承QwtPlotLegendItem重写draw方法根据曲线数据范围动态计算图例位置。4.5 多曲线时颜色重复现象创建了 5 条曲线但颜色只有两三种在循环分不清哪条是哪条。原因QwtPlotCurve不会自动分配颜色每次new出来的曲线默认用同一个 pen。解决维护一个颜色表创建曲线时按索引取色QListQColor 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; QVectorQwtPlotCurve * m_curves; QVectorQVectordouble m_xBuffers; QVectorQVectordouble 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; } QVectorT latest(int count) { QMutexLocker locker(m_mutex); count qMin(count, m_size); QVectorT 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: QVectorT m_data; int m_capacity; int m_head; int m_size; QMutex m_mutex; };这个缓冲区的好处是读写分离采集线程只管pushUI 线程定时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 都值得单独跑一遍跑通了再动手改比对着文档空想快得多。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站