资讯动态

Qt温湿度显示系统开发全攻略:从串口数据采集到界面实时刷新

发布时间:2026/10/6 13:38:59 来源:尧图企业网站定制
这东西看着简单做起来全是坑。我在嵌入式项目里用 Qt 做过一套温湿度显示系统从传感器数据采集到界面实时刷新再到最终打包发布整条链路走下来踩了不少雷也沉淀了不少经验。这篇文章就围绕这个题目把我在实际开发中遇到的典型问题、当时的排查思路和最终采用的可行方案完整地拆出来讲。如果你的需求也是通过串口或 Modbus 读取温湿度传感器在界面上实时显示并存储历史数据那这篇文章应该能帮你少走很多弯路。1. 系统框架设计先别急着写界面想清楚数据从哪来、到哪去很多初学者拿到温湿度显示系统这个需求第一反应是打开 Qt Designer 拖一个界面出来放两个 LCD Number 控件然后开始琢磨怎么让数字动起来。这个顺序其实是反的。仪表盘上的数字只是数据流的终点真正决定系统好不好用的是数据从传感器到界面这条链路设计得是否健壮。1.1 核心需求解析一个温湿度系统的完整任务清单我在动手之前会把需求拆成下面这几块缺一不可模块功能点说明数据采集串口/Modbus通信读取温湿度传感器或采集器的寄存器数据数据解析把裸报文转成浮点数值涉及大小端、精度位、校验位数据存储实时入库SQLite 轮表用于历史曲线和断线补传界面显示实时刷新温度、湿度、采集时间、设备状态曲线绘制历史趋势用 QCustomPlot 或 QChart系统设置串口参数、采样周期、报警阈值持久化到配置文件报警提醒越限告警界面变色 声音提示发布部署打包成免安装程序依赖库、驱动、配置文件一键部署这套表基本覆盖了我做过的大部分采集类桌面程序。温湿度显示只是外在表现真正费精力的是采集、解析、存储、刷新这几块。1.2 框架选择为什么这个场景我用 Widgets 而不是 QMLQt 有两个界面框架Qt Widgets 和 Qt Quick(QML)。做温湿度显示这类工业上位机软件我强烈建议用 Widgets。原因有三。第一目标硬件通常是工控机性能一般Widgets 对 OpenGL 没有强依赖兼容性更好第二这类系统的界面以表格、曲线、按钮、输入框为主Widgets 的控件体系正好是干这个的QML 的优势在动画和触摸交互在这类场景用不上第三国内做上位机的工程师绝大多数用 Widgets遇到问题搜解决方案会非常方便这点在开发调试期特别重要。至于 Qt 版本我用的 5.15.2 LTS稳定网上资料多6.x 现在虽然也成熟了但第三方库和例程的覆盖面还是 5.15 更广。编译器选的 MSVC2019 64 位做 Windows 发布部署时兼容性最好。2. 设备通信与数据解析Modbus 温湿度报文是怎么变成界面上的数字的这节是整个项目技术含量最高的部分。传感器数据为什么读不到、为什么读出来是乱码、为什么数值偶尔跳变几乎都出在这个环节。2.1 不用第三方库从零手写 Modbus RTU 主站通信现在网上很多教程推荐直接上 QModbusMaster说省事。但我自己的经历是QModbus 在 5.15 上的稳定性够用但如果你用的是某些国产温湿度传感器或采集器它内置的 Modbus 从站地址和寄存器定义未必标准反而用原始串口收发报文更容易调试。这里说明一下Modbus RTU 主站轮询的报文格式是很固定的设备地址(1字节) 功能码(1字节) 起始寄存器地址(2字节) 寄存器数量(2字节) CRC16校验(2字节)比如我要读设备地址为 0x01 的温湿度采集器从寄存器 0x0000 开始连续读 2 个寄存器一个放温度一个放湿度报文就是这样01 03 00 00 00 02 C4 0B设备收到后返回的数据格式是01 03 04 [温度高字节] [温度低字节] [湿度高字节] [湿度低字节] [CRC低] [CRC高]因为大多数工业传感器的温湿度值是以 0.01 为精度的定点数存储的所以读到 0x0BB8 就是 3000除以 100 得到 30.00 摄氏度如果读到的温度值是带符号的有负温度场景你还需要先把 uint16 转成 int16 再除以精度系数否则零下温度会显示成一个很大的正数。我在串口类里写了一个QByteArray buildModbusRequest(int devId, int regAddr, int regCount)方法专门负责组装报文和追加 CRC 校验。CRC 校验用查表法实现性能好代码也简洁。一定不能省 CRC否则数据偶尔错一个字节你要排查半天。2.2 数据解析的细节坑大小端、符号位、异常值数据解析这块我踩过最狠的坑是字节序。某些传感器返回温度是高字节在前某些是低字节在前。我写了一个辅助函数支持一键切换大小端float parseHexData(const QByteArray buf, bool bigEndian) { quint16 raw; if (bigEndian) { raw static_castquint8(buf[0]) 8; raw static_castquint8(buf[1]); } else { raw static_castquint8(buf[1]) 8; raw static_castquint8(buf[0]) static_castquint8(buf[0]) * 0; } // 处理负数情况最高位是符号位的话转为有符号数 qint16 signedRaw static_castqint16(raw); float value signedRaw / 100.0f; // 精度为 0.01 return value; }注意上面代码里的* 0是我故意留的一个无用表达式实际不要这样写。正确写法是先判断传感器约定是 unsigned 还是 signed再决定如何转换。有的传感器温度范围是 -40~80 摄氏度它就是有符号的纯正温度范围的传感器就直接当 unsigned 处理省一步转换。异常值过滤也必须做。在实际项目里数据不是平滑变化的传感器偶尔会返回 0xFFFF 或 0x7FFF 这类边界值。如果不过滤界面上会出现瞬间跳变到几百度的离谱数据。我的做法是维护一个结构体包含上次值和本次值如果本次值和上次值差值超过可配置范围默认温度差 5 摄氏度湿度差 10%RH就判定为异常帧丢弃本次数据等待下一次轮询并累计错误计数连续错误超过 10 帧则提示设备异常。3. 数据存储与断线续传SQLite 轮表的实际工程写法很多人在 demo 阶段只做实时显示一重启程序就回到初始状态。做真正能交付的系统本地数据存储是躲不掉的。Qt 自带 QSqlTableModel 和 SQLite 驱动不需要额外装数据库服务。3.1 数据库建表与轮表机制我在项目里用一个本地 SQLite 文件env_data.db核心表结构如下CREATE TABLE IF NOT EXISTS env_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, temperature REAL NOT NULL, humidity REAL NOT NULL, device_id INTEGER NOT NULL );定期往这张表插入数据的逻辑我放在串口读线程处理好数据之后通过信号把解析好的数据帧发给主界面主界面收到后调用一个appendRecord()方法完成入库和界面刷新。这里有个非常容易犯的错误不要在串口读取线程里直接写数据库。SQLite 的写锁会导致线程阻塞一旦数据库文件所在磁盘繁忙整个数据采集线程会被拖死串口缓冲区溢出产生丢帧。实际做法是串口线程只负责读数据和解析解析完的数据通过 signal/slot 发到界面线程界面线程再做数据库写入。就算写入慢也只是界面卡一下不会影响底层数据采集。这里信号槽连接方式用默认的 AutoConnection 就行。为了不让表无限膨胀我还做了轮表操作保留最近 90 天的数据超过部分按日期删除保留精度越久越好。删除语句我用的是DELETE FROM env_history WHERE timestamp datetime(now, -90 day);注意 SQLite 的datetime函数要求时间字符串格式统一。我在插入时用QDateTime::currentDateTime().toString(yyyy-MM-dd HH:mm:ss)这样上面这条语句就能能正常工作。3.2 断线缓存与补传逻辑系统运行过程中难免出现传感器掉线比如总线接触不良、从站设备断电。我的设计是掉线期间在内存里建一个 QQueue 缓存区只缓存最近 500 条记录每 5 秒一条的话能覆盖约 40 分钟的数据。恢复通信之后缓存区里的数据按时间顺序补写入数据库。如果缓存区满了就丢弃最旧的数据界面状态栏提示离线数据缓存已满部分数据丢失。这套逻辑的代码其实很短关键是缓存队列的锁保护要做对。我用 QMutex 控制队列的 push 和 pop避免界面线程和采集线程同时操作。4. 界面构建与自定义控件数字显示仪表盘和布线的门道界面不是简单的摆俩控件而是要让人一眼看清当前环境状态。这部分我主要做了三个事主界面布局、自动刷新策略、自定义圆形仪表盘控件。4.1 主界面布局思路我的主界面参考了工业组态软件的风格左侧是设备连接状态区中间是温湿度数值面板下方是实时曲线和历史查询区。整个界面用 QGridLayout 布局禁止用户在运行时拖拽改变大小省去很多布局随窗口变化的适配工作。数值面板我用的是几组 QGroupBox 套 QLabel 和自定义仪表盘控件字体统一用微软雅黑温度显示为橙色大号字体湿度显示为蓝色设备离线时显示灰色并置为 --。顶部状态栏显示采集时间、串口状态和数据库状态。串口状态用 QLabel setStyleSheet 实现绿色表示正常、红色表示掉线。4.2 自定义圆形仪表盘控件重写 paintEvent 实现自定义绘制需求方要求界面像仪表盘一样显示温湿度我花了一个下午写了一个GaugeWidget控件继承 QWidget重写paintEvent。核心绘制逻辑分三步用QPainter画一个半圆弧作为刻度背景根据范围值把当前数值映射到圆弧角度在角度位置绘制指针和数值文本这里贴一个关键片段展示角度映射的核心写法void GaugeWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); QRectF rect(8, 8, width() - 16, height() - 16); int startAngle 210 * 16; // 起始角度 210度 int spanAngle -240 * 16; // 扫过 240度 painter.drawArc(rect, startAngle, spanAngle); // 把数值映射到角度范围-60度到60度之间 double ratio (m_value - m_min) / (m_max - m_min); double angle startAngle / 16.0 - 240.0 * ratio; // 根据角度计算指针末端坐标 double rad qDegreesToRadians(angle); int lineLen width() / 2 - 24; QPointF endPoint center QPointF(lineLen * cos(rad), -lineLen * sin(rad)); painter.drawLine(QLineF(center, endPoint)); }这里有个细节drawArc的角度单位是 1/16 度不是度所以 210 要乘 16。很多新手直接传一个 210 进去画出来的弧永远不对这是 Qt 绘图一个特别经典的坑。自定义控件写完后记得在代码里直接ui-tempGauge-setRange(-20, 60)和setValue(25.5)就能用比用 QSS 画仪表盘好维护得多。4.3 关于 Qt 绘图的效率思考热搜词里有个qt绘图效率比较我在系统初期也研究过。QPainter 绘制静态图形效率没问题但在实时曲线这种高频刷新场景下全量重绘会造成 CPU 占用虚高。我的做法是只把新增点追加到绘图区利用 QWidget 的update()局部刷新策略把刷新区域限定在曲线控件的右端缺口处并且把曲线刷新频率控制在 5 帧/秒即每个采样周期 200ms 重绘一次人眼看起来已经很流畅CPU 占用不到 10%。5. 实时曲线刷新与界面卡顿优化多线程和 QCustomPlot 的取舍温湿度历史曲线这块需求基本都有。我一开始用的 QChart后来换成了 QCustomPlot。不是 QChart 不好而是 QCustomPlot 在大量数据点下的表现更稳而且它本质上就是一个 QWidget和 Widgets 界面的亲和力更好。5.1 用 QCustomPlot 做温湿度双 Y 轴曲线QCustomPlot 是第三方开源库需要把 qcustomplot.h 和 qcustomplot.cpp 两个文件拷进工程然后在 .pro 里加一行QT printsupport否则编译会报QPrinter相关的错误。这一步特别容易忽略。我做的是双 Y 轴曲线左边 Y 轴是温度右边 Y 轴是湿度X 轴是时间。关键设置代码如下ui-plot-xAxis-setBasePen(QPen(QColor(80, 80, 80))); ui-plot-yAxis-setLabel(温度(°C)); ui-plot-yAxis2-setLabel(湿度(%RH)); ui-plot-yAxis2-setVisible(true); ui-plot-yAxis2-setRange(0, 100); curveTemp new QCPGraph(ui-plot-xAxis, ui-plot-yAxis); curveHum new QCPGraph(ui-plot-xAxis, ui-plot-yAxis2); curveTemp-setPen(QPen(QColor(255, 140, 0), 2)); curveHum-setPen(QPen(QColor(0, 160, 255), 2)); ui-plot-xAxis-setRange(QDateTime::currentMSecsSinceEpoch() - 60000, QDateTime::currentMSecsSinceEpoch());QCustomPlot 的时间轴用QCPAxisTickerDateTime来设置显示格式否则 X 轴会显示一串时间戳数字而不是时分秒。我在代码里做了格式化QSharedPointerQCPAxisTickerDateTime dateTicker(new QCPAxisTickerDateTime); dateTicker-setDateTimeFormat(HH:mm:ss); ui-plot-xAxis-setTicker(dateTicker);5.2 曲线刷新能不能放独立线程我的明确结论很多人在网上问qt曲线刷新能放在另一个线程里面吗我明确回答建议不要也不要这么做。QCustomPlot以及 QWidget 体系下绝大多数控件的绘制必须在 GUI 线程执行。如果你把曲线刷新逻辑直接丢进一个子线程编译能过运行大概率崩溃或者界面直接白屏Qt 会打印 QObject::startTimer: Timers can only be used with threads started with QThread 这类错误。正确的架构是采集线程把数据通过信号发出来GUI 线程收到后把数据点 append 到 QCustomPlot 的容器里然后调用replot()。replot 是整个曲线刷新里最耗时的操作两个曲线的场景一帧也就几毫秒完全没必要为此专门开线程。真正影响界面流畅度的其实是两个隐藏问题一是数据点无限增长导致 replot 越来越慢二是两路曲线时间轴不同步。前者我用滚动窗口解决只保留最近 20 分钟的点后者我统一在收到数据时以系统当前时间作为 X 轴基准不做设备本地时间因为有些传感器的内部晶振不准走它自己的时间会让曲线出现锯齿和回退。6. 发布打包与常见维护问题依赖缺失、串口权限和崩溃日志开发机上跑得好好的程序拿到现场工控机上双击就是各种问题。这节内容全都是我实际部署过的机器上拿到的反馈。6.1 Qt 打包windeployqt 是对的但光靠它不够Qt 提供了 windeployqt 工具基本流程是cd /d D:\projects\build-EnvMon-Desktop_Qt_5_15_2_MSVC2019_64-Release\release D:\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe EnvMon.exe它会自动把 Qt 相关的 DLL、插件、QSS 资源考进来。但有几个目录它是管不到的数据库驱动插件sqldrivers\qsqlite.dll需要手动从 Qt 安装目录复制如果你用了 QCustomPlot那是纯源码不需要额外库串口相关插件其实不需要单独拷因为 QtSerialPort 在核心库里我用了一个小技巧把 windeployqt 后生成的一大堆 DLL 分门别类整理好再用 Enigma Virtual Box 打包成单个 exe现场部署就是拷一个文件过去避免因为缺少某个 DLL 被现场运维骂。6.2 经典的 cannot mix incompatible Qt library 错误热搜词里有一条 cannot mix incompatible qt library (5.15.3) with this library (5.15.2)这个错我在混合使用 Debug 和 Release 库时也遇到过。原因是程序链接的 Qt 版本和你运行环境里实际加载的 Qt 版本不一致通常是你把 5.15.2 编译的程序放到一个装了 5.15.3 运行库的机器上执行导致的。排查思路是这样的确认编译用的 Qt 版本和 windeployqt 用的版本一致检查环境变量 PATH 里有没有其他 Qt 路径干扰用 Dependency Walker 或 Process Explorer 查看程序实际加载的 Qt5Core.dll 路径最后我用 Process Explorer 查到程序加载的 Qt5Core.dll 来自系统 PATH 里的另一个安装目录把那目录从 PATH 里清掉之后就正常了。6.3 现场运行崩溃问题定位和预防热搜词里还有qt崩溃我现场遇到过的问题是程序在 Win7 工控机上双击没反应进程直接消失。排查结论是缺 VC 运行库。MSVC 编译的 Qt 程序依赖 VC Redistributable我在部署包根目录放了一个vcredist_x64.exe,让现场部署人员先装一遍再运行程序问题就解决了。为了防后续再出诡异崩溃我加了 qInstallMessageHandler 全局日志把 qDebug/qWarning/qCritical 输出写入本地 logs 文件崩溃后能回看最后的打印信息定位速度快很多。7. 界面和业务设计上容易被忽略的几个细节最后这节不说代码说几件我在项目复盘时觉得要是当时早点想到就好了的事。7.1 初始范围和历史数据的加载曲线控件刚启动时X 轴范围如果设为当前时间会导致刚启动时显示的是空窗口直到 20 分钟后才有曲线。改进方法是在程序启动时从数据库读取最近 60 秒的历史记录一次性绘制在曲线上这样操作人员一打开软件就能看到之前的趋势不会觉得软件有问题。7.2 报警灵敏度的设置有些客户对报警要求很敏感但又不想误报。我把报警判断做得保守了一点连续 3 个采样周期都超过阈值才触发报警中间只要有一次正常就清零计数。实际使用下来误报率直线下降。7.3 编译时使用 release 而非 debug调试期用 debug 没问题但交付给客户的一定要编译 release 版本。debug 版明显更慢而且界面的表现和 release 不完全一致某些奇怪的卡顿在 release 下可能根本不存在。我之前因为拿 debug 版本去演示被客户说明显卡顿换 release 之后完全改观。关于温湿度显示系统我目前这套方案从采集、解析、存储、显示到发布已经形成了一条比较稳的流水线。你在做的过程中如果也遇到了界面刷新卡死、串口掉线、打包完跑不起来之类的问题可以把具体现象发我我帮你一起看是什么环节出的问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑