1. 项目概述与需求分析
在工业自动化和环境监测领域,温度湿度数据采集系统是最基础也最关键的组成部分之一。作为一名长期从事工业控制软件开发的工程师,我经常需要为各类传感器设备开发配套的上位机软件。Qt框架因其跨平台特性和丰富的功能库,成为这类开发任务的首选工具。
这个项目要实现的核心功能包括:
- 实时采集温度湿度传感器数据(本示例使用模拟数据)
- 在图形界面直观展示当前数值
- 将采集数据按时间戳记录到本地文件
- 提供开始/停止记录的控制接口
2. 开发环境准备
2.1 Qt开发环境配置
推荐使用Qt 5.15 LTS版本,这是目前最稳定的长期支持版本。安装时务必勾选以下组件:
- Qt Creator 4.11以上
- MSVC 2019 64-bit编译器(Windows平台)
- Qt Charts模块(可选,用于后期数据可视化扩展)
提示:虽然Qt6已经发布,但在工业控制领域建议仍使用Qt5,因为许多硬件厂商提供的驱动库还未完全适配Qt6。
2.2 项目创建注意事项
在Qt Creator中新建项目时,建议采用以下配置:
- 项目类型:Qt Widgets Application
- 类名:MainWindow(保持默认)
- 构建系统:qmake(工业项目更推荐CMake,但qmake更简单易用)
- 取消勾选"Generate form"选项(我们将手动设计UI)
3. 界面设计与实现
3.1 UI布局规划
一个专业的传感器监控界面应该包含以下元素:
- 数值显示区:大字体显示当前温湿度
- 数据记录区:表格形式展示历史数据
- 控制按钮区:开始/停止记录、导出数据等
- 状态栏:显示最后更新时间、记录状态
cpp复制// mainwindow.h 中添加的UI成员变量
private:
QLabel *tempLabel;
QLabel *humidityLabel;
QTableWidget *dataTable;
QPushButton *recordButton;
QStatusBar *statusBar;
3.2 使用Qt Designer高效布局
在Qt Designer中采用栅格布局(Grid Layout)实现响应式设计:
- 创建中央窗口部件(Central Widget)
- 添加垂直布局(Vertical Layout)
- 在顶部添加水平布局(Horizontal Layout)用于数值显示
- 中间部分添加QTableWidget
- 底部添加按钮布局
专业技巧:为重要控件设置objectName时采用匈牙利命名法,如btnRecord、lblTemperature等,方便后续代码维护。
4. 核心功能实现
4.1 传感器数据模拟模块
实际项目中,传感器通信通常通过以下方式实现:
- 串口通信(RS232/RS485)
- I2C/SPI总线
- 工业以太网(Modbus TCP)
本示例采用模拟数据生成器:
cpp复制// sensor_simulator.h
#ifndef SENSOR_SIMULATOR_H
#define SENSOR_SIMULATOR_H
#include <QObject>
#include <random>
class SensorSimulator : public QObject {
Q_OBJECT
public:
explicit SensorSimulator(QObject *parent = nullptr);
double getTemperature();
double getHumidity();
private:
std::default_random_engine generator;
std::normal_distribution<double> tempDistribution;
std::normal_distribution<double> humidityDistribution;
};
#endif // SENSOR_SIMULATOR_H
cpp复制// sensor_simulator.cpp
#include "sensor_simulator.h"
SensorSimulator::SensorSimulator(QObject *parent)
: QObject(parent),
tempDistribution(25.0, 5.0), // 均值25℃,标准差5
humidityDistribution(50.0, 10.0) // 均值50%,标准差10
{
// 使用时间作为随机种子
generator.seed(QDateTime::currentMSecsSinceEpoch());
}
double SensorSimulator::getTemperature() {
double val = tempDistribution(generator);
return qBound(-10.0, val, 50.0); // 限制在-10~50℃范围内
}
double SensorSimulator::getHumidity() {
double val = humidityDistribution(generator);
return qBound(0.0, val, 100.0); // 限制在0~100%范围内
}
4.2 数据记录模块优化
工业级数据记录需要考虑以下因素:
- 文件存储格式(CSV、二进制、数据库)
- 数据缓存机制
- 异常处理
- 性能优化
改进后的记录模块:
cpp复制// datalogger.h
#ifndef DATALOGGER_H
#define DATALOGGER_H
#include <QObject>
#include <QFile>
#include <QTextStream>
class DataLogger : public QObject {
Q_OBJECT
public:
explicit DataLogger(QObject *parent = nullptr);
~DataLogger();
bool startLogging(const QString &filename);
void stopLogging();
void logData(const QDateTime ×tamp, double temp, double humidity);
private:
QFile logFile;
QTextStream logStream;
bool isLogging;
};
#endif // DATALOGGER_H
cpp复制// datalogger.cpp
#include "datalogger.h"
#include <QDir>
DataLogger::DataLogger(QObject *parent)
: QObject(parent), isLogging(false)
{
}
DataLogger::~DataLogger() {
stopLogging();
}
bool DataLogger::startLogging(const QString &filename) {
// 确保日志目录存在
QDir().mkpath(QFileInfo(filename).absolutePath());
logFile.setFileName(filename);
if (!logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
qWarning() << "Failed to open log file:" << filename;
return false;
}
logStream.setDevice(&logFile);
isLogging = true;
// 如果是新文件,写入CSV表头
if (logFile.size() == 0) {
logStream << "Timestamp,Temperature(C),Humidity(%)\n";
}
return true;
}
void DataLogger::stopLogging() {
if (isLogging) {
logStream.flush();
logFile.close();
isLogging = false;
}
}
void DataLogger::logData(const QDateTime ×tamp, double temp, double humidity) {
if (isLogging) {
logStream << timestamp.toString(Qt::ISODate) << ","
<< QString::number(temp, 'f', 2) << ","
<< QString::number(humidity, 'f', 2) << "\n";
// 定期flush确保数据不丢失
static int counter = 0;
if (++counter % 10 == 0) {
logStream.flush();
}
}
}
4.3 主控制逻辑实现
主窗口类整合所有功能模块:
cpp复制// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "sensor_simulator.h"
#include "datalogger.h"
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void onRecordButtonClicked();
void updateSensorData();
private:
Ui::MainWindow *ui;
SensorSimulator *sensor;
DataLogger *logger;
QTimer *dataTimer;
bool isRecording;
void setupUI();
void setupConnections();
void updateStatusBar(const QString &message);
};
#endif // MAINWINDOW_H
cpp复制// mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDateTime>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent),
ui(new Ui::MainWindow),
sensor(new SensorSimulator(this)),
logger(new DataLogger(this)),
dataTimer(new QTimer(this)),
isRecording(false)
{
ui->setupUi(this);
setupUI();
setupConnections();
// 初始化状态栏
updateStatusBar("Ready");
// 设置定时器
dataTimer->setInterval(5000); // 5秒采样间隔
}
MainWindow::~MainWindow() {
delete ui;
}
void MainWindow::setupUI() {
// 设置窗口标题和大小
setWindowTitle("Temperature & Humidity Monitor");
resize(800, 600);
// 初始化表格
ui->dataTable->setColumnCount(3);
ui->dataTable->setHorizontalHeaderLabels({"Time", "Temperature (°C)", "Humidity (%)"});
ui->dataTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
}
void MainWindow::setupConnections() {
connect(ui->recordButton, &QPushButton::clicked, this, &MainWindow::onRecordButtonClicked);
connect(dataTimer, &QTimer::timeout, this, &MainWindow::updateSensorData);
}
void MainWindow::onRecordButtonClicked() {
if (!isRecording) {
// 开始记录
QString logPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)
+ "/SensorData/"
+ QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss")
+ ".csv";
if (logger->startLogging(logPath)) {
isRecording = true;
ui->recordButton->setText("Stop Recording");
dataTimer->start();
updateStatusBar("Recording to: " + logPath);
}
} else {
// 停止记录
isRecording = false;
ui->recordButton->setText("Start Recording");
dataTimer->stop();
logger->stopLogging();
updateStatusBar("Recording stopped");
}
}
void MainWindow::updateSensorData() {
double temp = sensor->getTemperature();
double humidity = sensor->getHumidity();
QDateTime currentTime = QDateTime::currentDateTime();
// 更新显示
ui->tempLabel->setText(QString::number(temp, 'f', 1) + " °C");
ui->humidityLabel->setText(QString::number(humidity, 'f', 1) + " %");
// 记录数据
logger->logData(currentTime, temp, humidity);
// 更新表格
int row = ui->dataTable->rowCount();
ui->dataTable->insertRow(row);
ui->dataTable->setItem(row, 0, new QTableWidgetItem(currentTime.toString("hh:mm:ss")));
ui->dataTable->setItem(row, 1, new QTableWidgetItem(QString::number(temp, 'f', 1)));
ui->dataTable->setItem(row, 2, new QTableWidgetItem(QString::number(humidity, 'f', 1)));
// 自动滚动到最后一行
ui->dataTable->scrollToBottom();
updateStatusBar("Last update: " + currentTime.toString("hh:mm:ss"));
}
void MainWindow::updateStatusBar(const QString &message) {
statusBar()->showMessage(message);
}
5. 高级功能扩展
5.1 数据可视化
添加Qt Charts模块实现实时曲线显示:
- 在.pro文件中添加:
code复制QT += charts
- 创建图表视图:
cpp复制// 在mainwindow.h中添加
#include <QtCharts>
QT_CHARTS_USE_NAMESPACE
private:
QChart *chart;
QChartView *chartView;
QLineSeries *tempSeries;
QLineSeries *humiditySeries;
- 初始化图表:
cpp复制void MainWindow::initChart() {
chart = new QChart();
chart->setTitle("Temperature & Humidity Trend");
tempSeries = new QLineSeries();
tempSeries->setName("Temperature (°C)");
humiditySeries = new QLineSeries();
humiditySeries->setName("Humidity (%)");
chart->addSeries(tempSeries);
chart->addSeries(humiditySeries);
chart->createDefaultAxes();
chart->axes(Qt::Horizontal).first()->setTitleText("Time");
chart->axes(Qt::Vertical).first()->setTitleText("Value");
chartView = new QChartView(chart);
chartView->setRenderHint(QPainter::Antialiasing);
// 将图表添加到UI布局中
ui->chartLayout->addWidget(chartView);
}
5.2 数据库存储
对于长期数据存储,建议使用SQLite数据库:
cpp复制bool DatabaseManager::initialize() {
db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("sensor_data.db");
if (!db.open()) {
qWarning() << "Database error:" << db.lastError().text();
return false;
}
QSqlQuery query;
return query.exec("CREATE TABLE IF NOT EXISTS sensor_readings ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"timestamp DATETIME NOT NULL,"
"temperature REAL NOT NULL,"
"humidity REAL NOT NULL)");
}
5.3 多线程处理
为避免界面卡顿,应将传感器通信和数据处理放在工作线程中:
cpp复制class SensorWorker : public QObject {
Q_OBJECT
public:
explicit SensorWorker(QObject *parent = nullptr);
public slots:
void startSampling(int interval);
void stopSampling();
signals:
void newDataAvailable(double temp, double humidity);
private:
QTimer *timer;
SensorSimulator *sensor;
};
6. 项目部署与打包
6.1 Windows平台打包
使用windeployqt工具自动收集依赖:
bash复制windeployqt --release --compiler-runtime your_app.exe
6.2 创建安装程序
使用NSIS或Inno Setup创建专业安装包:
- 安装Inno Setup
- 创建脚本文件(.iss)
- 包含以下内容:
code复制[Setup]
AppName=Temperature Humidity Monitor
AppVersion=1.0
DefaultDirName={pf}\THMonitor
DefaultGroupName=THMonitor
OutputDir=output
OutputBaseFilename=THMonitorSetup
Compression=lzma
SolidCompression=yes
[Files]
Source: "release\THMonitor.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "release\*.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "release\platforms\*"; DestDir: "{app}\platforms"; Flags: ignoreversion recursesubdirs
[Icons]
Name: "{group}\TH Monitor"; Filename: "{app}\THMonitor.exe"
7. 常见问题与解决方案
7.1 串口通信问题
问题现象:无法读取传感器数据
排查步骤:
- 检查串口线连接
- 确认波特率、数据位、停止位等参数匹配
- 使用串口调试工具验证硬件是否正常
- 检查Qt串口模块是否已安装(QtSerialPort)
7.2 数据记录延迟
优化方案:
- 使用内存缓存,定期批量写入
- 采用二进制格式替代文本格式
- 减少文件打开/关闭频率
7.3 界面卡顿
解决方案:
- 将数据采集和处理移至工作线程
- 限制界面刷新频率(如每秒最多更新10次)
- 使用QCustomPlot等高效绘图库替代Qt Charts
8. 性能优化技巧
-
数据采样优化:
- 动态调整采样频率
- 实现异常值过滤算法
- 添加数据平滑处理
-
内存管理:
- 限制表格中显示的数据量(如只保留最近1000条)
- 使用模型/视图架构优化大数据显示
-
文件IO优化:
- 采用内存映射文件技术
- 实现环形缓冲区日志系统
- 考虑使用SQLite WAL模式
9. 项目扩展方向
-
网络功能:
- 添加TCP/UDP远程数据传输
- 实现WebSocket实时推送
- 开发REST API接口
-
数据分析:
- 集成简单统计功能(均值、极值、标准差)
- 添加数据导出为Excel功能
- 实现报警阈值设置
-
多平台支持:
- 移植到Linux嵌入式系统
- 开发Android移动端应用
- 创建Web管理界面
10. 工程实践建议
-
版本控制:
- 使用Git管理源代码
- 采用语义化版本控制
- 实现自动化构建
-
文档编写:
- 使用Doxygen生成API文档
- 编写用户手册和开发手册
- 记录设计决策和架构图
-
测试策略:
- 单元测试(QTest)
- 集成测试
- 硬件在环测试
在实际工业项目中,我们还需要考虑以下方面:
- 看门狗机制防止程序卡死
- 异常恢复和自动重启
- 数据加密和安全传输
- 用户权限管理
- 操作日志审计
这个Qt温度湿度监控系统的实现展示了工业级上位机软件开发的基本流程和方法论。通过模块化设计、合理的线程模型和健壮的错误处理,可以构建出稳定可靠的数据采集系统。