250ETS 总结
250ETS-MASTER
项目功能
- 读取设备的电压、报警等参数
- 将电流、电压等参数写入 PLC,通过上传 Excel,实现自动电流的写入
- 柱状图数据可视化
- 实时数据报表
- CSV 文件实时写入保存
- 将实时数据存入数据库,在界面中管理数据,根据日期查询数据
- 同步光伏报警数据到 CSV,每分钟记录光伏的所有数据
重要功能实现逻辑
读取设备的电压等参数(异步处理)
五个参数数组,从 PLC 获取 Modbus 地址读入
不管是读取还是写入,都避免连续请求。
在收到上一个请求的回复后,再发送下一个请求,在读写过程中都使用异步处理。
QModbusDataUnit unit = readRequestQueue.dequeue();
if (auto *reply = modbusDevice->sendReadRequest(unit, 1)) {
// 异步处理
// 在绝大多数情况下,Modbus 请求是异步的,
// 也就是说你发出请求后,设备还没来得及返回数据,
// 这时 isFinished() 返回 false。
if (!reply->isFinished()) {
// 没完成这个请求,插入队列
readReplyToAddress.insert(reply, unit.startAddress());
/*
* 请求还在进行中,连接信号与槽,
* 提前告诉 Qt:
* “等数据回来了,自动通知我”
*
* finished 的判断和触发,
* 全部由 Qt 框架自动完成。
*
* 你只需要专注于:
* “收到信号后怎么处理数据”
*/
connect(
reply,
&QModbusReply::finished,
this,
&ModbusWorker::readReady
);
} else {
delete reply;
processNextRequest();
}
}
readReady() 函数负责处理返回的数据。
异步处理机制类比
假设你是老师,要批改 7 份作业,每份作业就是一个读取请求:
- 你(老师)把第一份作业交给学生(设备)批改,对应
processNextRequest()。 - 学生批改完后,敲门告诉你“批改好了”,对应设备返回数据,Qt 自动调用
readReady()。 - 你拿到作业,登记成绩,对应
readReady()解析数据。 - 你看还剩几份没批改,如果还有,就再交下一份给学生,对应在
readReady()中再次调用processNextRequest()。 - 全部批改完后,你通知班主任“成绩都出来了”,对应
emit dataUpdated。
这种方式可以避免一次发送大量 Modbus 请求造成通信阻塞。
电压柱状图
利用 QBar 类实现视觉上的柱状图。
数据更新关键代码
每次 timeout,都会自动调用 connect 的 lambda 函数,进而调用 updateBarSeries(),刷新柱状图的数据,并更新 UI 上的标签。
connect(
&timer,
&QTimer::timeout,
this,
[this]() {
// 每次 timeout,都会自动调用这个 lambda 函数
updateBarSeries();
// UI 更新
ui->reportLabel_1->setText(
QString::number(reportDatas[1], 'f', 3)
);
ui->reportLabel_5->setText(
QString::number(reportDatas[5], 'f', 3)
);
ui->reportLabel_6->setText(
QString::number(reportDatas[6], 'f', 3)
);
}
);
当窗口显示时启动定时器,窗口隐藏时停止定时器。
这样可以保证只有在窗口可见时才定时刷新数据,节省资源。
void CellVoltageViewDialog::showEvent(QShowEvent *event)
{
timer.start(
int(ui->timeStepSpinBox->value() * 1000)
);
QDialog::showEvent(event);
}
void CellVoltageViewDialog::hideEvent(QHideEvent *event)
{
timer.stop();
QDialog::hideEvent(event);
}
整体实现逻辑
- 窗口显示时,
showEvent()被触发,定时器timer启动。 - 定时器每隔一段时间(由
timeStepSpinBox控件决定)发出timeout信号。 - 每次
timeout,自动调用connect中的 lambda。 - lambda 调用
updateBarSeries(),刷新柱状图数据。 - 同时更新 UI 上的相关标签。
- 窗口隐藏时,
hideEvent()被触发,定时器停止。 - 窗口隐藏后不再刷新数据。
写入函数和自动电流写入
AutoCurrentWriter 模块
文件信息
- 文件名:
autocurrentwriter.cpp - 功能:自动电流写入控制器,实现从 Excel 加载写入计划并定时执行
功能流程
用户选择 Excel 文件
↓
loadExcelData() 读取数据
↓
targetDateTimes / currentValues
↓
fileWatcher 开始监控文件
↓
用户点击“开始自动写入”
↓
启动定时器
↓
timerAutoCurrentWrite 每秒触发 handNextCurrentWtite()
↓
检查是否到达目标时间
↓
到达目标时间 → 执行写入
↓
┌──────────────────────────────────────────────┐
│ Excel 文件被修改 │
│ ↓ │
│ onExcelFileChanged() 被触发 │
│ ↓ │
│ 延迟 1 秒 → loadExcelData(path) │
│ ↓ │
│ 如果正在写入 → recalculateWriteIndex() │
└──────────────────────────────────────────────┘
类构造与析构
AutoCurrentWriter::AutoCurrentWriter(
...,
int flag,
QObject *parent
)
: QObject(parent),
m_flag(flag),
xlsxDocument(nullptr)
{
fileWatcher = new QFileSystemWatcher(this);
}
AutoCurrentWriter::~AutoCurrentWriter()
{
delete xlsxDocument;
}
主要工作:
- 初始化 UI 控件和标志位
- 创建
QFileSystemWatcher,用于监控 Excel 文件变化 - 析构时释放
xlsxDocument
信号槽绑定
void AutoCurrentWriter::initializeConnections()
{
connect(
fileWatcher,
&QFileSystemWatcher::fileChanged,
this,
&AutoCurrentWriter::onExcelFileChanged
);
connect(
m_xlsxSelectButton,
&QPushButton::clicked,
this,
&AutoCurrentWriter::selectExcelFile
);
connect(
&timerAutoCurrentWrite,
&QTimer::timeout,
this,
&AutoCurrentWriter::handNextCurrentWtite
);
connect(
&timerShowElapsedTime,
&QTimer::timeout,
this,
&AutoCurrentWriter::showElapsedTime
);
connect(
m_xlsxAutoWriteCurrentButton,
&QPushButton::clicked,
this,
&AutoCurrentWriter::on_xlsxAutoWriteCurrentButton_clicked
);
}
核心方法
1. 文件选择
void AutoCurrentWriter::selectExcelFile()
主要功能:
- 打开文件选择框
- 添加路径到
fileWatcher - 调用
loadExcelData()读取写入计划
2. 启动/停止写入
void AutoCurrentWriter::on_xlsxAutoWriteCurrentButton_clicked()
主要功能:
- 校验是否有数据
- 切换自动写入状态
- 启动/停止定时器
- 更新 UI 显示
3. 定时写入
void AutoCurrentWriter::handNextCurrentWtite()
主要功能:
- 每秒检查是否到达目标时间点
- 光伏电流模式
flag == 1:构造QModbusDataUnit并发信号 - 普通模式
flag == 2:模拟回车事件完成写入 - 写入后推进
writeCurrentIndex
4. 流逝时间显示
void AutoCurrentWriter::showElapsedTime()
主要功能:
- 计算并显示从启动以来的流逝时间
- 格式:
hh:mm:ss
5. 开始/暂停按钮
void AutoCurrentWriter::on_start_clicked()
void AutoCurrentWriter::on_pause_clicked()
其中:
on_start_clicked()调用on_xlsxAutoWriteCurrentButton_clicked()启动写入on_pause_clicked()在写入状态下调用同一方法,实现暂停
6. Excel 数据加载
void AutoCurrentWriter::loadExcelData(
const QString& fileName
)
主要功能:
- 清空旧数据
- 从 Excel 中逐行读取
- 第一列 →
targetDateTimes - 第二列 →
currentValues - 最终存储到容器中,支持
QDateTime + float
7. 文件变化处理
void AutoCurrentWriter::onExcelFileChanged(
const QString& path
)
主要功能:
- 检测到文件变化后,延迟 1 秒重新加载
- 如果定时器正在运行,则调用
recalculateWriteIndex()
8. 写入索引重计算
void AutoCurrentWriter::recalculateWriteIndex()
根据当前时间跳过已经过期的数据点。
确保 Excel 更新后,自动写入的进度仍然正确。
DataLogger 类代码(光伏数据导出)
功能概述
DataLogger 类主要用于处理报警数据和事件日志,并将相关信息导出到 CSV 文件。
主要功能:
- 记录报警和事件日志到
data_log.csv - 检测数据变化并生成报警日志
- 将实时数据定时导出到
data.csv - 提供测试功能,通过定时器模拟报警与数据导出
构造函数
DataLogger::DataLogger(QObject *parent)
: QObject(parent)
{
alarmDescriptions = getAlarmDescriptions();
alarmDescriptions_2 = getDescriptions();
alarmDescriptions_3 = QDataLabels();
}
其中:
alarmDescriptions:报警备注列表alarmDescriptions_2:报警描述alarmDescriptions_3:数据标签
日志写入函数
writeCsvLog
将事件记录到桌面路径的 data_log.csv 文件。
void DataLogger::writeCsvLog(
const QString ×tamp,
const QString &description
);
首次写入时自动添加表头:
时间,事件描述
写入成功后输出日志到控制台。
报警处理函数
processTestAlarmData
处理并记录报警与事件。
void DataLogger::processTestAlarmData(
const QBitArray& testDatas_3,
const std::array<quint16, 13>& testDatas_words,
const QBitArray& testDatas_5
);
主要逻辑:
事件检测
基于 datas_words[7],判断:
- 光伏启动
- 光伏停止
- 光伏复位
- 电源报警
- 其他相关事件
报警检测
检测 testDatas_3 中报警位从:
无 → 有
的变化。
扩展报警检测
检测 testDatas_5 中的报警变化。
历史状态
更新历史状态,避免同一个报警重复写入。
工具函数
processQuint16ToBit
将一个 quint16 数据解析为 QBitArray。
QBitArray DataLogger::processQuint16ToBit(
quint16 data
);
数据导出函数
exportCurrentDataToCSV
将当前数据导出到桌面路径的 data.csv。
void DataLogger::exportCurrentDataToCSV(
const std::array<quint16, 100>& readDatas_words2
);
首次导出时写入:
时间戳 + 数据标签
后续写入仅追加数据行。
测试功能
testAlarmSystem
模拟报警系统,每 5 秒触发一次 processTestAlarmData()。
void DataLogger::testAlarmSystem(
QBitArray* testDatas_3,
std::array<quint16, 13>* testDatas_words,
QBitArray* testDatas_5
);
注意:
传入的指针数据必须在定时器生命周期内保持有效。
当时的建议是使用:
QSharedPointer
或:
std::shared_ptr
提升安全性。
exportSystem
定时导出数据到 CSV 文件,每分钟执行一次。
void DataLogger::exportSystem(
std::array<quint16, 100>* readDatas_words2
);
代码安全性提示
当前版本使用裸指针传递 QBitArray 和 std::array 数据,依赖于外部对象生命周期。
更推荐使用:
QSharedPointer
或:
std::shared_ptr
来避免悬空指针。
输出文件
data_log.csv
存储:
- 报警
- 事件日志
data.csv
存储:
- 实时数据快照
多线程迁移
1. 线程创建
modbusThread = new QThread(this);
modbusWorker = new ModbusWorker;
modbusWorker->moveToThread(modbusThread);
其中:
modbusThread是一个QThread,代表一个独立线程modbusWorker是自定义的通信处理对象,继承自QObjectmoveToThread()把modbusWorker的线程归属切换到modbusThread
也就是说:
modbusWorker的事件循环和槽函数在modbusThread中执行。
2. 信号与槽连接
connect(
modbusThread,
&QThread::started,
modbusWorker,
&ModbusWorker::createModbusDevice
);
// 主线程控制读取设备的连接与断开
connect(
this,
&MainWindow::connectDevice,
modbusWorker,
&ModbusWorker::connectModbusDevice
);
线程启动后:
modbusThread 启动
↓
createModbusDevice()
↓
子线程创建通信设备
主线程发射:
emit connectDevice(1);
然后调用子线程中的:
connectModbusDevice()
3. 子线程与主线程的交互
子线程向主线程反馈数据状态:
connect(
modbusWorker,
&ModbusWorker::deviceState,
this,
[this](int state) {
// 获取设备状态
}
);
connect(
modbusWorker,
&ModbusWorker::deviceStateResponse,
this,
...
);
connect(
modbusWorker,
&ModbusWorker::dataUpdated,
this,
&MainWindow::updateDataAndUI
);
整体关系:
主线程
│
│ 发起请求
↓
ModbusWorker
│
│ 子线程执行
↓
Modbus通信
│
│ 数据返回
↓
ModbusWorker
│
│ signal
↓
主线程
│
↓
updateDataAndUI()
modbusWorker 在子线程中采集到数据后,通过信号把数据传递给主线程。
主线程的槽函数,例如:
updateDataAndUI()
在 UI 线程中执行,负责更新界面。
主线程发起写入
connect(
this,
&MainWindow::writeData,
modbusWorker,
&ModbusWorker::write
);
connect(
this,
&MainWindow::normalButton,
modbusWorker,
&ModbusWorker::normalButtonWrite
);
connect(
this,
&MainWindow::resetButton,
modbusWorker,
&ModbusWorker::resetButtonWrite
);
主线程负责发起:
- 数据写入
- 常规按钮操作
- 复位操作
具体通信函数则在 ModbusWorker 所在线程执行。
4. 线程的启动与关闭
启动线程:
// 启动线程,以创建通信设备
modbusThread->start();
// 连接设备
emit connectDevice(1);
关闭线程:
// 安全触发 modbusWorker 析构函数
// 停止读取
modbusWorker->deleteLater();
// 关闭线程事件循环
modbusThread->quit();
// 确保执行完成
modbusThread->wait();
// 删除线程
delete modbusThread;
Debug Point
inline
在 reportviewdialog.h 文件中定义:
inline int indexof(
float value,
const std::array<float, 125>& arr,
float epsilon = 1e-4
)
{
for (size_t i = 0; i < arr.size(); ++i) {
if (std::fabs(arr[i] - value) < epsilon)
return static_cast<int>(i);
}
return -1;
}
为什么定义为 inline
因为多个 .cpp 文件包含了:
reportviewdialog.h
如果将普通函数定义直接放在头文件中:
int indexof(...)
{
...
}
然后多个 .cpp 都包含这个头文件,就可能产生链接阶段的:
multiple definition
即多重定义错误。
而将函数定义为:
inline
之后,允许多个翻译单元看到相同的函数定义,链接器可以正确处理这些定义。
因此,将这种较小的函数直接定义在头文件中时,可以使用 inline。
柱状图中不显示第一或第二根柱子
该问题在开发过程中通过调试图表数据和显示逻辑进行排查。
数据报表导出 Excel 卡顿问题
最初使用 Excel 格式:
.xlsx
进行数据导出。
但是在实时数据导出过程中出现卡顿。
因此更换为:
CSV
格式。
原因:
Excel 格式,例如 .xlsx,需要使用专门的库,例如:
QXlsx
每次写入都需要处理:
- 文件格式
- XML 结构
- 压缩
- 数据组织
因此写入速度远慢于 CSV。
CSV 本质上是纯文本文件:
时间,电压,电流,温度
...
写入速度很快,更适合:
- 实时数据
- 批量导出
- 高频数据记录
实时报表中没有进行实时更新
问题原因
页面数据不能实时更新。
根本原因是:
m_reportData1
m_reportData2
只在构造函数中赋值了一次,之后没有继续更新。
每次定时器触发时,虽然会:
update()
↓
paintEvent()
重新绘制页面,但是使用的仍然是旧数据。
因此页面内容不会发生变化。
根本原因
最新数据数组需要通过指针传递,才能让报表窗口访问到原始数据。
这样原始数组发生变化后,报表窗口就能获取最新数据。
这也是 C++/Qt 实时数据共享的一种常见方式。
解决方案
使用指针传递数组地址:
void ReportViewDialog::setExternalDataSource(
const std::array<float, 125>* pData1,
const std::array<float, 44>* pData2
)
{
m_externalData1 = pData1;
m_externalData2 = pData2;
}
定时器触发 updateReport
connect(
&m_timer,
&QTimer::timeout,
this,
&ReportViewDialog::updateReport
);
updateReport 修改逻辑
if (m_externalData1 && m_externalData2) {
// 使用外部数据源实时刷新
m_contentWidget->setData(
*m_externalData1,
*m_externalData2
);
} else {
// 保持原有逻辑
m_contentWidget->update();
}
setData 修改逻辑
void setData(
const std::array<float, 125>& data1,
const std::array<float, 44>& data2
)
{
m_reportData1 = data1;
m_reportData2 = data2;
// 重新计算参数
// ...
// 计算电流密度、最大电压、平均电压、标准差等
update();
}
setData() 会:
- 接收最新数据
- 保存到控件内部
- 重新计算报表参数
- 计算最大电压
- 计算平均电压
- 计算标准差等参数
- 调用
update() - 请求 Qt 重新绘制控件
集成在主窗口函数中使用
mainwindow.cpp:
void MainWindow::showReportDialog()
{
if (!m_reportViewDialog) {
m_reportViewDialog =
new ReportViewDialog(
readDatas_1,
readDatas_2,
this
);
// 集成实时数据源
m_reportViewDialog->setExternalDataSource(
&readDatas_1,
&readDatas_2
);
} else {
// 确保每次显示都使用最新数据源
m_reportViewDialog->setExternalDataSource(
&readDatas_1,
&readDatas_2
);
}
m_reportViewDialog->show();
}
数据更新逻辑
整个数据更新过程:
MainWindow
│
│ setExternalDataSource()
│
↓
传递 readDatas_1 / readDatas_2 的指针
│
↓
ReportViewDialog
│
│ 窗口显示
↓
启动定时器
│
↓
updateReport()
│
↓
setData()
│
↓
更新报表内部数据
│
↓
update()
│
↓
Qt 请求重绘
│
↓
paintEvent()
│
↓
drawReportContent()
│
↓
显示最新数据
具体步骤:
MainWindow调用setExternalDataSource(),把最新数据的指针传递给报表窗口。ReportViewDialog显示窗口后启动定时器。- 定时器触发
updateReport()。 updateReport()调用setData()。setData()接收最新数据,并处理内容控件数据。setData()调用update()请求 Qt 重绘控件。- Qt 检测到
update()后触发paintEvent()。 paintEvent()中调用drawReportContent()。- 使用最新数据绘制报表。
新增数据库类管理模块
MVC 框架
MVC:
Model
View
Controller
Model
负责基础数据模型和数据库相关逻辑。
View
负责页面显示。
Controller
负责逻辑控制。
整体职责:
Model
↓
数据与数据库
View
↓
界面显示
Controller
↓
业务逻辑
基础数据库类(单例类)
单例类的设计
使用智能指针实现懒汉模式:
std::shared_ptr<Database> Database::instance()
{
static std::once_flag s_flag;
std::call_once(
s_flag,
[&]()
{
data = std::shared_ptr<Database>(
new Database("2")
);
}
);
return data;
}
其中:
static std::once_flag s_flag;
用于保证下面的初始化只执行一次。
std::call_once()
保证 lambda 表达式中的代码只执行一次。
因此:
第一次调用 instance()
↓
创建 Database
↓
保存到 shared_ptr
↓
后续调用
↓
直接返回同一个对象
创建数据库类为单例类,使整个项目中只允许存在一个数据库对象实例,避免重复创建数据库对象。
登录界面的设计
关键的密码隐蔽
使用:
ui->lineEdit_passwd->setEchoMode(
QLineEdit::Password
);
默认将密码隐藏。
通过复选框切换密码显示状态:
connect(
ui->checkBox,
&QCheckBox::toggled,
this,
[=](bool checked) {
if (checked)
ui->lineEdit_passwd->setEchoMode(
QLineEdit::Normal
);
else
ui->lineEdit_passwd->setEchoMode(
QLineEdit::Password
);
}
);
实现:
checkbox
↓
toggled
↓
checked ?
┌───────┴───────┐
↓ ↓
Normal Password
↓ ↓
显示密码 隐藏密码
Model-View 模式
基本流程
1. Model
使用:
QSqlTableModel
作为数据库表模型。
2. 设置数据库表
将 Model 对象设置对应的数据库表。
3. View
将 Model 放入:
QTableView
中。
4. 数据操作
调用 Model 自带的数据库操作函数完成:
- 查询
- 插入
- 删除
- 修改
- 提交
表嵌入到 Model 和 View
m_model = new QSqlTableModel(this, db);
m_model->setTable("DAta");
// 设置编辑模式
m_model->setEditStrategy(
QSqlTableModel::OnManualSubmit
);
// 将模型设置到视图中
m_view->setModel(m_model);
插入行
int rowCount = m_model->rowCount();
m_model->insertRow(rowCount);
// 获取当前时间并转换为字符串
QString currentDateTime =
QDateTime::currentDateTime()
.toString("yyyy-MM-dd hh:mm:ss");
// 创建 QModelIndex,指向新插入行的第一列
QModelIndex index =
m_model->index(rowCount, 0);
// 使用 setData() 设置新行第一列的值
m_model->setData(index, currentDateTime);
// 将视图滚动到新插入的行
m_view->scrollTo(index);
// 进入编辑模式
m_view->edit(index);
删除与提交操作
删除操作
首先获取 View 中选中的行:
QModelIndexList selectedRows =
m_view->selectionModel()->selectedRows();
if (selectedRows.isEmpty())
{
QMessageBox::information(
nullptr,
"提示",
"请选择要删除的行。"
);
return;
}
然后循环删除:
for (int i = selectedRows.count() - 1;
i >= 0;
--i)
{
m_model->removeRow(
selectedRows.at(i).row()
);
}
最后提交:
m_model->submitAll();
删除流程
QTableView
↓
获取 selectedRows()
↓
判断是否为空
↓
循环 removeRow()
↓
submitAll()
↓
提交数据库
删除行操作需要先获取视图中所选择的行,再进行循环删除。
View 类的实现
将控件作为私有变量,然后定义 getter 获取控件。
头文件
private:
QTableView* DatabaseView;
public:
QTableView* getTableView() const;
源文件
QTableView* DatabaseView::getTableView() const
{
return ui->tableView;
}
View 和 Model 如何在 MainWindow 中融合交互
1. 在头文件中定义 View 和 Model 指针变量
例如:
DatabaseView* m_databaseView;
DatabaseController* m_controller;
2. 在需要的函数中进行初始化
m_databaseView =
new DatabaseView(nullptr);
m_controller =
new DatabaseController(
m_databaseView->getTableView(),
this
);
3. 通过 connect 进行连接
让 View 中的控件对应 Controller 中的实现函数:
QObject::connect(
m_databaseView->getAddButton(),
&QPushButton::clicked,
m_controller,
&DatabaseController::addRecord
);
这样:
View
│
│ 用户点击按钮
↓
QPushButton::clicked
│
↓
Controller
│
↓
addRecord()
4. 显示 View
m_databaseView->show();
智能指针不能应用的场景
QObject 派生类
例如:
QWidgetQTimerQDialogQObject
Qt 本身拥有父子对象管理机制。
当 QObject 的父对象析构时,其子对象会自动被 delete。
例如:
QTimer* timer = new QTimer(this);
这里:
this
↓
QTimer
this 是 QTimer 的父对象。
当 this 析构时,Qt 会自动析构 timer。
与智能指针混用的风险
如果再使用:
std::unique_ptr
或者:
std::shared_ptr
管理已经拥有 Qt 父对象的 QObject,就可能出现生命周期管理冲突。
例如:
std::shared_ptr<QTimer> timer =
std::make_shared<QTimer>(this);
此时:
shared_ptr
↓
负责 delete
Qt parent
↓
也负责 delete
可能导致重复析构。
建议
有父对象
例如:
QTimer* timer = new QTimer(this);
这种情况下直接使用裸指针即可:
QTimer* timer;
生命周期由 Qt 父子对象机制管理。
没有父对象
没有 Qt 父对象的普通 C++ 对象,可以考虑:
std::unique_ptr
或者:
std::shared_ptr
跨线程
如果对象需要跨线程管理,则需要根据具体生命周期和线程归属设计对象所有权。
不能简单地认为“所有对象都应该使用智能指针”。
总结
这个 250ETS-MASTER 项目主要涉及以下几个方面:
250ETS-MASTER
│
├── Modbus 通信
│ ├── 异步读取
│ ├── 请求队列
│ └── 数据解析
│
├── Qt UI
│ ├── 实时数据
│ ├── 电压柱状图
│ └── 实时报表
│
├── Excel / CSV
│ ├── Excel 自动电流写入
│ ├── CSV 实时数据记录
│ └── 报警日志
│
├── 多线程
│ ├── QThread
│ ├── ModbusWorker
│ ├── 信号与槽
│ └── 主线程 / 子线程通信
│
├── 数据库
│ ├── MVC
│ ├── QSqlTableModel
│ ├── QTableView
│ └── Database 单例
│
└── C++ / Qt
├── inline
├── 指针
├── 智能指针
├── QObject 生命周期
└── 实时数据共享
这次项目中比较核心的几个技术点包括:
- Qt
QModbus异步通信 - 请求队列
QThread + QObject::moveToThread()- Qt 信号槽跨线程通信
QTimer定时任务QFileSystemWatcher文件监控- Excel / CSV 数据处理
- Qt Model/View
- SQLite / 数据库管理
- MVC 思想
- 单例模式
std::shared_ptr- QObject 父子对象生命周期
- C++ 指针与实时数据共享
- Qt 自定义绘制和实时 UI 更新