前言

在很多调试场景下,我们需要配置一条参数曲线给某些模块使用。比如在各种图像处理软件中,我们都可以看到一个 Gamma 曲线调整的功能,里面的曲线可以通过鼠标随意地拖动,十分的方便。如果你有接触过硬件调试,那么就会知道配置曲线基本是通过预设几个数据点,然后通过线性插值获取的,每次想更改曲线的形状就得把数据点一个一个地通过键盘修改,可以说低效至极,因此得想办法实现以上的效果。

对于曲线作图,很多语言基本都有相关的函数工具库,比如 Python 里面就有 matplotlib.pypplot,可以完成各种复杂的数据显示效果。然而,这种作图一般是静态的,也就是只能显示由已知数据点所连成的曲线,而不能自由地增减数据点,以及拖动已有的数据点,同时相应地改变曲线形状。很显然,要实现这种效果,我们必须要和鼠标事件进行交互,同时也离不开窗体应用的开发。在这方面,Qt 是具有比较明显的优势的,一方面其本身绝大部分功能都是开源的,而且大多数属于 LGPL 协议,这就意味这只要我们通过动态链接的方式使用这些 Qt 库,就可以实现代码的闭源,这对于商业用途是十分友好的;另外,Qt 是基于 C++ 的,只要你对 C++ 面向对象的概念有基本的理解,那么你只需要知道 Qt 里面有哪些工具,工具里面有哪些方法,就可以十分流畅地使用。同时,Qt 也有一些现成的曲线作图工具,比如 QChart 和 Qwt。其中 QChart 功能比较丰富,在安装 Qt 时一般都有直接安装 QChart 的选项,但问题在于 QChart 是 GPL 协议的,其缺点就是沾上了 GPL 协议的代码也得按 GPL 开源。而 Qwt 则是 LGPL 协议的,虽然功能上要比 QChart 有所逊色,但实现本文的效果已经是绰绰有余了,因此这里选择的是 Qt + Qwt 的方案。Qt 和 Qwt 可选择以下的下载链接。编译安装流程相对比较简单,但是要先安装一个 VS2019,可以选择社区版,主要是提供一个编译环境,实际开发的时候还是在 Qt Creator 内的。貌似 VS2019 以下版本容易编译不过。Qwt 编译成功后会在 plugins 下生成相应的 Qt Designer 插件 qwt_designer_plugin.dll,把它复制到 Qt 安装目录相应的位置,比如我的是 F:\Qt\5.15.2\msvc2019_64\plugins\designer,那么在 Qt Creator 项目的 .ui 文件右键选择在 Designer 中打开即可看到 Qwt 的窗体,然而直接在 Creator 的设计界面是无法正常显示的。不过一般来说直接将 Qwt 作为依赖库,然后通过代码创建使用就可以了。

Qt: https://mirrors.tuna.tsinghua.edu.cn/qt/archive/online_installers/4.4/qt-unified-windows-x64-4.4.1-online.exe
Qwt: https://udomain.dl.sourceforge.net/project/qwt/qwt/6.2.0/qwt-6.2.0.zip

在开始实现之前,我们还得知道一种数据插值的算法。因为 Qwt 作图的时候默认是使用直线来连接相邻两个点的,所以整体上表现的是分段折线图,不过我们是可以配置选项让其自动插值出一条曲线并显示的。然而,我们不仅要看到曲线,还要根据曲线来计算任意 x 值对应的 y 值,这就需要知道这条曲线的具体表达式,自动插值的曲线是满足不了我们的需求的。因此,比较合理的一种方案是,给定少数几个已知数据点,这些数据点可以通过鼠标点击创建以及拖动,插值算法基于这几个已知数据点计算出相应的表达式,并插值出相对密集且平滑的数据点,然后通过 Qwt 把这些密集的点用折线连接起来,就形成了我们想要的平滑曲线了。具体的插值算法在我之前的一篇文章中有所提及,主要是分段三次 Hermite 插值多项式(PCHIP),其中包括形状保持的 PCHIP 算法即 SPPCHIP 还有样条插值算法 Spline,区别在于前者是一阶导连续的,而后者是二阶导连续的,具体原理可查阅以下文章,大多数作图工具也会基于这类的插值算法。SPPCHIP 可以保证相邻两个点之间的单调性,即相邻两点间通过插值所得的 y 值不会超过这两点的 y 值范围,会比较符合数据的变化趋势,因此才被称为形状保持。而且其具体实现非常简单,复杂度低,是比较合适的选择。

https://blog.csdn.net/qq_33552519/article/details/102742715

最终实现的效果如下所示。由于本人不是专门做 Qt 开发的,这只是项目临时需要,所以文章侧重点在于实现自己想要的效果,对于 Qt 以及 Qwt 额外的功能不会过多提及,如果有兴趣可以查阅相关的文档,也可以直接阅读相关的代码,毕竟这些都是开源的。

实现

具体的实现主要分为三个部分。第一是插值算法实现,第二是曲线作图,第三是鼠标事件响应。插值算法在上面的链接中有具体的 Python 实现,当然本文后面开源的代码也会有相应的 C++ 实现,这部分的作用是根据给定的几个已知点,建立一条相对平滑的曲线,并且曲线表达式也是可以计算的,后续只要提供 x 值,就可以直接计算出相应的 y 值。我们的重点主要在后面两个部分。话不多说,先给出实现的头文件,方便后面描述。注意其中 “qwt_xx.h” 都是 Qwt 的头文件,需要先编译安装好 Qwt 然后把相关路径加入到 Qt 项目的 .pro 文件。由于我所应用的领域一般是图像处理,这里假设所有数据都限制在 0~1 的范围,并且固定有 x=0 和 x=1 两个点,不过这里的插值算法是可以处理任意范围的数据的。

#include <QHBoxLayout>
#include <QWidget>
#include "qwt_plot.h"
#include "qwt_plot_curve.h"
#include "qwt_plot_picker.h"
#include "sppchip.h"  // 这是插值算法实现的类class PlotLayout : public QHBoxLayout
{Q_OBJECT
public:explicit PlotLayout(QWidget* = nullptr, _Tp y0 = 0, _Tp y1 = 1);void setPickerEnabled(bool);      // 设置是否响应鼠标事件void setPickerXMovable(bool);      // 设置数据点是否可以左右移动void setPickerInsertable(bool);     // 设置是否可以增加数据点void deleteSelectedPoint();           // 删除选中点void reset();                       // 重置曲线void interp(const std::vector<_Tp>& xs, std::vector<_Tp>& ys); // 根据曲线插值uint32_t getCurveVersion() const;    // 返回曲线版本protected slots:void slotPointSelected(const QPointF& mousePos);   // 响应鼠标点击事件void slotPointDragged(const QPointF& mousePos);      // 响应鼠标拖动数据protected:void plotShow();                   // 曲线作图void setBasePointsSamples();     // 根据基础数据点建立曲线protected:QwtPlot *m_plot = NULL;                // 作图区域QwtPlotCurve *m_points = NULL;      // 用于显示数据点QwtPlotCurve *m_curve = NULL;        // 用于显示曲线QwtPlotCurve *m_marker = NULL;        // 用于高亮选中点QwtPlotPicker *m_picker = NULL;      // 鼠标事件相关SPPCHIP m_sppchip;                 // 插值类QList<QPointF> m_base_points;       // 基础数据点_Tp m_init_y0 = 0.;                    // 初始 y0_Tp m_init_y1 = 1.;                    // 初始 y1const int m_max_base_pnum = 16;        // 最大基础数据点个数std::vector<_Tp> m_base_xs;           // 用于插值类基础 x 数据输入std::vector<_Tp> m_base_ys;          // 用于插值类基础 y 数据输入bool m_basex_movable = 1;           // 基础数据点是否可以左右移动(x=0和x=1除外)bool m_base_insertable = 1;         // 是否可以新增基础数据点const int m_show_pnum = 51;         // 在0~1范围内均匀分布相对密集的点用于显示曲线std::vector<_Tp> m_show_xs;            // 记录密集点的 x 值std::vector<_Tp> m_show_ys;          // 记录密集点的 y 值_Tp m_mark_x = 0;                 // 被选中点的 x 坐标_Tp m_mark_y = 0;                 // 被选中点的 y 坐标bool m_mark_selected = 0;         // 是否有基础点被选中int  m_mark_base_idx = 0;          // 被选中基础点的索引uint32_t m_curveVersion = 0;       // 曲线版本
};

曲线作图

类似于 Python 里面的 Maplotlib,Qwt 作图首先也需要创建一个作图区域,需要使用到 QwtPlot 类,其继承于 QFrame,一般可依附于一个 QHBoxLayout,这样其作图区域就可根据给定的方框大小自动调整。QwtPlot 可设置上下左右四个坐标轴,这里只需要用到左边和下边两个,分别为 QwtPlot::xBottom 和 QwtPlot::yLeft,那么可通过以下代码建立一个作图区域 m_plot。后续包括网格、数据点以及曲线等都可通过各自的 attach(m_plot) 方法依附到 m_plot 上,通过 m_plot->replot() 即可在作图区域上显示。注意在 Qt 窗体相关的类中,一般父对象被析构后会自动把子对象以及依附于其的类对象都析构掉,不需要自己手动管理指针和析构,因此 Qt 的代码中通常很少见有手动 delete 对象的情况。

m_plot = new QwtPlot;
this->addWidget(m_plot); // 这里的this指QHBoxLayout
m_plot->setAxisScale(QwtPlot::xBottom, 0., 1., 0.2); // x轴范围为0~1,每0.2为一个大刻度
m_plot->setAxisScale(QwtPlot::yLeft, 0., 1., 0.2);       // y轴范围为0~1,每0.2为一个大刻度QwtPlotGrid *grid = new QwtPlotGrid;              // 创建网格
grid->setMajorPen(Qt::darkGray, 0, Qt::DotLine); // 设置网格风格
grid->attach(m_plot);                                // 将网格依附到m_plot上

在 QwtPlot 上画曲线需要用到 QwtPlotCurve,但是 QwtPlotCurve 默认是用折线把数据点连接起来的,要实现平滑的显示,可以通过设置曲线属性的方式实现。但前面已经说了,我们希望使用自己的插值算法来建立曲线。因为一个 QwtPlot 对象可以同时显示多个 QwtPlotCurve 的数据,因此合适的做法是:

  1. 首先用一个 QwtPlotCurve 对象显示基础数据点,但是需要设置只显示点,不显示连线,同时需要设置数据的标记风格,用以突出显示这些基础数据点,代码里用 m_points 表示。
  2. 插值算法 SPPCHIP 基于所提供的基础数据点获取曲线的具体表达式。将 x 定义域均匀划分为比较密集的点,根据插值表达式分别算出这些点的 y 值,使用另外一个 QwtPlotCurve 对象以折线图形式显示这些密集数据点,注意此时不需要设置数据的标记风格,我们只需要把这根曲线显示出来,代码里用 m_curve 表示。
  3. 还需另外一个 QwtPlotCurve 对象,但这个对象只用于显示一个数据点,用来高亮标记我们用鼠标选中的数据点。该数据点的标记风格和基础数据点稍有不同,以此与其他数据点区分开来,代码里用 m_marker 表示。注意,鼠标点击曲线以外的区域时,就意味着没有数据点被选中,这时就应该取消数据点的高亮标记。我们可以比较简单地通过设置其 visible 属性实现。

将以上三个 QwtPlotCurve 对象都依附到 m_plot 上,就可以实现自定义的插值曲线显示了。实现代码如下。各对象显示的数据由 setSamples 设置,具体可查看完整代码。

/* 创建基础数据点的标记风格 */
QwtSymbol *symbol = new QwtSymbol(QwtSymbol::Ellipse);     // 圆形
symbol->setSize(7);                                      // 标记大小
symbol->setBrush(QBrush(Qt::red, Qt::SolidPattern));     // 实心填充红色/* 基础数据点显示对象 */
m_points = new QwtPlotCurve;
// m_points->setCurveAttribute(QwtPlotCurve::Fitted);    // 这句代码可以实现曲线自动插值
m_points->setStyle(QwtPlotCurve::NoCurve);           // 但我们这里不需要显示曲线
m_points->setSymbol(symbol);
m_points->attach(m_plot);/* 创建高亮选中数据点的标记风格 */
symbol = new QwtSymbol(QwtSymbol::Ellipse);
symbol->setSize(9);
symbol->setBrush(QBrush(Qt::green, Qt::SolidPattern));/* 高亮数据点实现对象 */
m_marker = new QwtPlotCurve;
m_marker->setSymbol(symbol);
m_marker->setVisible(false);
m_marker->attach(m_plot);/* 自定义插值曲线显示对象,不需要数据点标记,使用直线连接即可 */
m_curve = new QwtPlotCurve;
m_curve->setPen(Qt::blue, 2);    // 设置线宽
m_curve->attach(m_plot);

鼠标响应

Qwt 可响应鼠标以及键盘事件,这里我们主要讨论鼠标事件。事件捕捉通过设置一个 QwtPlotPicker 对象实现,在定义对象时需要设置其需要捕捉事件的画布,可通过 m_plot->canvas() 获取。同时还需要添加一个状态机 QwtPickerMachine,用来设置 QwtPlotPicker 需要响应的事件。不同的 QwtPickerMachine 根据事件的不同会产生不同的命令队列,相应地会发射不同的信号,具体可查看 Qwt 的源码,主要在 qwt_picker.h(.cpp), qwt_plot_picker.h(.cpp), qwt_picker_machine.h(.cpp) 等几个文件。

常用的 QwtPickerMachine 有 QwtPickerClickPointMachine 和 QwtPickerDragPointMachine,两者的区别在于前者只响应鼠标点击事件,而后者还会响应鼠标移动事件。因此我们这里选择的是后者。当鼠标点击事件发生时,QwtPickerDragPointMachine 会产生一个 Append 命令,该命令会发射一个 appended(const QPointF& pos) 信号,pos 即为鼠标点击位置的坐标值,注意是当前显示坐标系下的坐标值,而不是鼠标的像素位置;而当鼠标移动事件发生时,QwtPickerDragPointMachine 会产生一个 Move 命令,该命令会发射一个 moved(const QPointF& pos) 信号,pos 为鼠标当前所在位置的坐标值。因此,当鼠标点击的时候,QwtPlotPicker 就会发射一个 appended 信号;只要鼠标不松开,并且产生移动,那么 QwtPlotPicker 就会持续地发射 moved 信号;当鼠标被松开,鼠标点击以及拖动的动作结束,QwtPlotPicker 不再响应鼠标的移动事件,直到下一次点击的发生。

因此,我们可以定义两个槽函数,分别响应 QwtPlotPicker 的 appended 和 moved 信号,根据其发射的坐标信息,确定当前曲线中是否有基础数据点被选中,即该坐标与某个数据点的距离是否足够小。对于 appended 信号,如果有基础数据点被选中,则高亮该数据点;如果没有基础数据点被选中,但是该坐标落在我们所插值的曲线上,则需要新增一个基础数据点,并高亮该数据点;否则不做任何处理,如果在此之前有基础数据点被选中高亮,则应该取消选中和高亮。对于 moved 信号,因为 moved 信号产生之前必定先有 appended 信号,我们需要先看响应 appended 信号后是否有基础数据点被选中。如果有,则需要把该数据点的坐标更改为当前鼠标所在坐标,因为插值曲线是由基础数据点决定的,所以这时候我们需要重新执行一次插值算法重构曲线,并且把更新后的基础数据点和曲线通过 m_plot->replot() 方法重新显示一遍,而人眼是察觉不到那么快的变化的,在主观上就形成了拖动曲线的效果;如果没有,则不做任何处理即可。

以下为 QwtPlotPicker 对象定义以及信号连接的代码。其中,slotPointSelected 和 slotPointDragged 为自定义的分别响应鼠标点击和移动事件的方法,具体可查看完整代码。TrackerMode 主要用于控制鼠标十字光标在画布上移动时所显示的效果,默认的是鼠标所在位置的坐标值,这里选择的是 AlwaysOff,也就是关闭显示。如果选择 AlwaysOn,则会一直显示;如果选择 ActiveOnly,则只有以上所述事件发生时才会显示。除此以外还可以设置更加复杂的显示效果,这里不赘述。

m_picker = new QwtPlotPicker(m_plot->canvas());
m_picker->setStateMachine(new QwtPickerDragPointMachine());
m_picker->setTrackerMode(QwtPicker::AlwaysOff);connect(m_picker, SIGNAL(appended(QPointF)), this, SLOT(slotPointSelected(QPointF)));
connect(m_picker, SIGNAL(moved(QPointF)), this, SLOT(slotPointDragged(QPointF)));

要注意的是,在确定是否有基础数据点被选中时,通常的做法是计算鼠标坐标值与每个基础数据点的距离,然后选中距离最小的一个。QwtPlotCurve 有一个 closestPoint(const QPointF& pos, double* dist) 的方法,比如通过 m_points->closestPoint(pos) 就可求得基础数据点中与 pos 最靠近的点以及坐标。但这里坑在于,pos 并不是所显示坐标系下的坐标值,而被当作是鼠标在画布中的像素坐标值,里面的计算也是先把个基础数据点坐标转换为像素坐标计算的。因此,建议自己实现 closestPoint 的方法,也就是个简单的遍历而已。

完整的代码我放在了个人 Github 上,欢迎下载。

https://github.com/ZoengMingWong/EasyCurve-With-Qt-Qwt

C++、基于Qt和Qwt实现交互式曲线图相关推荐

  1. 基于QT的CHAI3D开发框架搭建

    基于QT的CHAI3D开发框架搭建 CHAI3D简介 CHI3D(计算机触觉和主动界面)是一个开放的C++库,用于计算机触觉.可视化和交互式实时仿真.CHAI3D最初是在斯坦福大学的人工智能实验室开始 ...

  2. (项目实战)基于QT嵌入式ARM数据采集卡上位机(二)——页面布局

    (项目实战)基于QT嵌入式ARM数据采集卡上位机(二)--页面布局 上一篇文章<基于 QT 嵌入式ARM数据采集卡上位机(一)> 下一篇文章<(项目实战)基于QT嵌入式ARM数据采集 ...

  3. QT用QWT绘制心电图、脉氧饱和度波形图、波形图

    qwt是一个基于LGPL版权协议的开源项目, 可生成各种统计图.它为具有技术专业背景的程序提供GUI组件和一组实用类,其目标是以基于2D方式的窗体部件来显示数据, 数据源以数值,数组或一组浮点数等方式 ...

  4. 基于 QT 嵌入式ARM数据采集卡上位机(一)

    基于 QT 嵌入式ARM数据采集卡上位机(一) 下一篇<基于 QT 嵌入式ARM数据采集卡上位机(二)-- 页面布局> 由于自己最近较为闲,刚好手上有设备,所以在业余时间编写了一个上位机和 ...

  5. 基于 Qt 框架的开源笔记软件 VNote

    关注.星标公众号,直达精彩内容 来源:OSC开源社区 作者:tamlok VNote是一个受Vim启发开发的专门为Markdown而优化.设计的笔记软件. 授权协议:MIT 开发语言:C/C++ Ja ...

  6. linux qt5.7下打地鼠源程序,基于QT的打地鼠游戏

    [实例简介] 基于QT的一个打地鼠游戏,采用随机数的方法,是地鼠产生随机序列,有得分界面,动画效果也不错,用C++进行编程 [实例截图] [核心代码] 打地鼠 └── 打地鼠 ├── erwei │  ...

  7. 基于QT Plugin框架结构

    基于QT Plugin框架结构 2009-04-24 18:56:02|  分类: 日常总结|举报件一样,是一种计算机应用程序,它和主应用程序(host application)互相交互,以提供特定的 ...

  8. Qt Creator创建基于Qt Widget的应用程序

    Qt Creator创建基于Qt Widget的应用程序 创建基于Qt Widget的应用程序 创建文本查找器项目 设计用户界 头文件 源文件 创建资源文件 编译并运行程序 创建基于Qt Widget ...

  9. 基于Qt\C++实现的网络远程控制系统

    基于Qt\C++实现的网络远程控制系统     本系统在Qt平台上采用C++语言实现的网络远程控制.通过将server部署到腾讯云服务器上,利用云中转的内网穿透方式实现不同内网之间的远程控制. 该系统 ...

最新文章

  1. Gson:我爸是 Google
  2. wince5使用access数据库_关于wince系统支持什么数据库的阿里云论坛用户知识和技术交流...
  3. 【模式识别】学习笔记(3)【Fisher线性判别】
  4. linux shell之cut用法
  5. matlabfor循环语句举例_笨办法学python(七)条件、选择和循环
  6. 从网上找到一个清晰CSS视频教程和大家分享一下
  7. spring boot + vue + element-ui全栈开发入门——基于Electron桌面应用开发
  8. CentOS 7 + nginx-1.12 + php-7.2 + MySQL-5.7
  9. UE4学习-UE4结合vs2019混合编程
  10. vb ftp linux,VB FTP上传和下载模块
  11. python语法学习—实现猜拳游戏
  12. docker build run 卡住_还在使用第三方Docker插件?SpringBoot官方插件真香!
  13. robotframework的学习笔记(十六)----robotframework标准库String
  14. ibm笔记本电脑电池_笔记本电脑是一直插着电源好,还是拔了电源好?
  15. Windows窗口程序设计入门(C#版)
  16. STM32—驱动GY85-IMU模块
  17. 常见逻辑谬误 -推断不当
  18. 在Expression Blend中制作侧面为梯形的类棱柱体
  19. 斯年,愿做岁月的知音
  20. 刘邦的用人之道!真心服气

热门文章

  1. 污水治理智能化管理解决方案
  2. 嚯——ChatGPT是很强,但也会胡说八道。。。
  3. 从春秋战国学习企业管理——管仲
  4. linux qt compiler叹号,qt里感叹号什么意思
  5. 第十一届蓝桥杯 ——限高杆
  6. 正则-密码至少8位,且含有数字、字母大小写
  7. Hive炸裂函数explode的使用
  8. Filebeat入门
  9. windows 10删除指纹解锁
  10. 教你在Windows下上传iOS APP到苹果应用商店