250ETS
250ETS-MASTER
项目功能
- 读取设备的电压,报警等参数
- 将电流电压等参数写入plc,通过上传excel,实现自动电流的写入
- 柱状图图标数据可视化
- 实时数据报表
- csv文件实时写入保存
- 将实时数据存入数据库,在界面中管理数据,根据日期查询数据
- 同步光伏报警数据到csv,每分钟记录光伏的所有数据
重要功能实现逻辑
读取设备的电压等参数(异步处理)
五个参数数组,从plc从获取modbus地址读入
不管是读取还是写入,避免连续请求,在收到上一个请求的回复后发送下一个请求,在读写过程中都使用
1 | QModbusDataUnit unit = readRequestQueue.dequeue(); |
readReady函数用来处理数据
相似类比 此处的异步处理机制
1 | 假设你是老师,要批改 7 份作业(每份作业就是一个读取请求): |
电压柱状图
利用Qbar类实现视觉上的柱状图
数据更新关键代码:
每次 timeout,都会自动调用你 connect 的 lambda 函数,进而调用 updateBarSeries(),刷新柱状图的数据,并更新 UI 上的标签。
1 | connect(&timer,&QTimer::timeout,this,[this](){//每次 timeout,都会自动调用你 connect 的 lambda 函数 |
当窗口显示时启动定时器,窗口隐藏时停止定时器。
这样可以保证只有在窗口可见时,才会定时刷新数据,节省资源。
1 | void CellVoltageViewDialog::showEvent(QShowEvent *event) |
整体实现逻辑
1 | 窗口显示时,showEvent() 被触发,定时器 timer 启动。 |
写入函数和自动电流写入
AutoCurrentWriter 模块
文件信息
文件名:
autocurrentwriter.cpp功能: 自动电流写入控制器,实现从 Excel 加载写入计划并定时执行
功能流程
1 | 用户选择Excel文件 |
类构造与析构
1 | `AutoCurrentWriter::AutoCurrentWriter(..., int flag, QObject *parent) : QObject(parent), m_flag(flag), xlsxDocument(nullptr){ fileWatcher = new QFileSystemWatcher(this);}AutoCurrentWriter::~AutoCurrentWriter(){ delete xlsxDocument;}` |
信号槽绑定
1 | void AutoCurrentWriter::initializeConnections() |
核心方法
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。 - 提供测试功能,通过定时器模拟报警与数据导出。
构造函数
1 | DataLogger::DataLogger(QObject *parent) |
日志写入函数
writeCsvLog
将事件记录到桌面路径的 data_log.csv 文件。
1 | void DataLogger::writeCsvLog(const QString ×tamp, |
首次写入时会自动添加表头:时间,事件描述
写入成功后输出日志到控制台。
报警处理函数
processTestAlarmData
处理并记录报警与事件。
1 | void DataLogger::processTestAlarmData( |
工具函数
processQuint16ToBit
将一个 quint16 数据解析为 QBitArray。
QBitArray DataLogger::processQuint16ToBit(quint16 data);
数据导出函数
exportCurrentDataToCSV
将当前数据导出到桌面路径的 data.csv。
1 | void DataLogger::exportCurrentDataToCSV( |
测试功能
testAlarmSystem
模拟报警系统,每 5 秒触发一次 processTestAlarmData。
1 | void DataLogger::testAlarmSystem(QBitArray* testDatas_3, |
1 | void DataLogger::exportSystem(std::array<quint16, 100>* readDatas_words2); |
输出文件
data_log.csv:存储报警与事件日志。
data.csv:存储实时数据快照。
多线程迁移
1. 线程创建
modbusthread = Qthread(this);
modbusworker = new Modbusworker
modbuswoker->moveToThread(modbusthrea);
- modbusThread 是一个 QThread,代表一个独立的线程。
- modbusWorker 是你自定义的通信处理对象,继承自 QObject。
- moveToThread 把 modbusWorker 的事件循环和槽函数全部放到 modbusThread 线程中执行。
2.信号与槽连接
1 | connect(modbusThread,&QThread::started,modbusWorker,&ModbusWorker::createModbusDevice);//子线程启动时,在子线程中创建通信设备connect(this,&MainWindow::connectDevice,modbusWorker,&ModbusWorker::connectModbusDevice方法);//主线程控制读取设备的连接与断开 |
mobusThread启动时,子线程自动创建通信设备
主线程 中connectDevice信号的发射,就调用子线程中connectModbusDevice方法
3.子线程与主线程的交互
1 | connect(modbusWorker,&ModbusWorker::deviceState,this,\[this\](int state){//子线程向主线程反馈数据状态,主线程获取设备的状态 |
子线程向主线程返回状态和更新的数据
modbusWorker 在子线程中采集到数据后,通过信号把数据传递给主线程,主线程的槽函数(如 updateDataAndUI)在UI线程中安全执行,更新界面。
1 | connect(this,&MainWindow::writeData,modbusWorker,&ModbusWorker::write);//主线程发起数据写入,调用ModbusWorker中的函数write,write函数在子线程中进行connect(this,&MainWindow::normalButton,modbusWorker,&ModbusWorker::normalButtonWrite);//常规按钮connect(this,&MainWindow::resetButton,modbusWorker,&ModbusWorker::resetButtonWrite);//复位 |
主线程发起写入 等请求,子线程这函数执行
4.线程的启动与关闭
1 | //启动线程,以在创建通信设备 |
debug point
inline
在reportviewdialog.h文件中
1 | inline int indexof(float value, const std::array<float,125>& arr, float epsilon = 1e-4) |
inline 和多重定义: 如果将非 inline 函数的定义放在头文件中,并在多个 .cpp 文件中包含该头文件,会导致链接错误(多重定义错误)。但是,将 inline 函数的定义放在头文件中是安全的,因为编译器知道这些定义在链接时可能会被多次看到,并且会正确处理它们(通常只会保留一个实际的函数实现,或者完全内联)。
柱状图中不显示第一或第二根柱子
数据报表导出excel卡顿问题
更换成csv格式导出
Excel格式(如.xlsx)需要用专门的库(如QXlsx),每次写入都要处理复杂的格式、压缩、XML结构,写入速度远慢于CSV。
CSV是纯文本,写入速度极快,适合实时、批量导出。
实时报表中没有进行实时更新
问题原因:
页面数据不能实时更新,根本原因是:
m_reportData1 和 m_reportData2 只在构造函数里赋值了一次,之后就没有再改变。
每次定时器触发时,虽然会重绘页面(update() → paintEvent() → 重新计算参数),但用的还是老数据,所以页面内容不会变。
最根本原因还是最新数据数组需要用指针进行传参,才能改变数组中的值
解决方案:
用指针传递数组地址,才能保证数据变化时,所有用到这份数据的地方都能“看到”最新内容。
这也是C++/Qt开发中实时数据共享的常用技巧。
传入数组指针的函数实现如下:
1 | void ReportViewDialog::setExternalDataSource(const std::array<float,125>\* pData1, const std::array<float,44>\* pData2) |
定时器触发updatereport代码
connect(&m_timer, &QTimer::timeout, this, &ReportViewDialog::updateReport);
updatareport的修改逻辑如下:
1 | if (m_externalData1 && m_externalData2) { |
setdata的修改逻辑
1 | void setData(const std::array<float, 125>& data1, const std::array<float, 44>& data2) |
集成在主窗口函数中使用:
mainwindow.cpp
1 | void MainWindow::showReportDialog() |
数据更新逻辑:
- mainwindow中调用的setExternalDataSource函数把最新数据的指针传递给报表窗口
- ReportViewDialog函数窗口显示启动定时器,和updataReport()关联
- 定时器刷新一次就自动调用updataReport(),updataReport函数会调用setdata函数
- setdata()函数接收最新数据,处理内容控件数据,调用update()函数 请求Qt重绘控件
- Qt 框架检测到 update() 被调用后,会自动触发控件的 paintEvent()。在 paintEvent() 里,调用 drawReportContent(),用最新的数据把报表内容画到页面上。
新增数据库类管理模块
MVC框架
model view controller
model: 基础的类的实现
view: 页面显示类的实现
controller: 实现基础类的逻辑控制层
基础数据库类(单例类)
单例类的设计
关键代码
1 | std::shared_ptr <database>Database::instance()//懒汉模式使用智能指针</database> |
创建数据库类为单例类,在项目中只允许一个数据库对象实例的出现,确保不重复访问数据库
登录界面的设计
关键的密码隐蔽
1 | ui->lineEdit_passwd->setEchoMode(QLineEdit::Password); |
checkbox和lineEdit控件的连接转换
关键 model-view模式
1.model 使用QSqlTableModel模板,有自带的数据库获取属性
2.model对象设置完数据库表后,放入tableview中
3.调用model中自带的数据库操作函数完成编写
表嵌入到模型和视图的代码
1 | m_model = new QSqlTableModel(this, db); |
插入行代码
1 | int rowCount = m_model->rowCount(); |
删除 提交操作
1 | //删除操作 |
删除行操作需要先获取视图中所选择的行,再进行循环删除
view类的实现
将控件作为私有变量,定义get方法获取控件
代码示例:
1 | //头文件中 |
view和model如何在mainwindow或其他界面进行融合交互
1.在头文件中定义好view和model指针变量
2.在需要的函数中进行初始化
//初始化
m_databaseView = new DatabaseView(nullptr);
m_controller = new DatabaseController(m_databaseView->getTableView(), this);
3.通过connect进行连接,view中控件对应model中的实现函数
QObject::connect(m_databaseView->getAddButton(), &QPushButton::clicked, m_controller, &DatabaseController::addRecord);
4.绘制出视图、
m_databaseView->show();
智能指针不能应用的场景
QObject 派生类(如 QWidget、QTimer 等)
Qt 自带父子机制:QObject 的子对象会在父对象析构时自动 delete。
例子:
QTimer* timer = new QTimer(this); // this 是父对象
风险:如果你用 std::unique_ptr 或 shared_ptr 管理带父对象的 QObject,会和 Qt 父子管理冲突,可能析构两次。
✅ 建议:
有父对象的情况下,不要用智能指针,直接用裸指针即可,Qt 会管理生命周期。
没有父对象或跨线程时,可以考虑 std::unique_ptr
