250ETS-MASTER

项目功能

  • 读取设备的电压,报警等参数
  • 将电流电压等参数写入plc,通过上传excel,实现自动电流的写入
  • 柱状图图标数据可视化
  • 实时数据报表
  • csv文件实时写入保存
  • 将实时数据存入数据库,在界面中管理数据,根据日期查询数据
  • 同步光伏报警数据到csv,每分钟记录光伏的所有数据

重要功能实现逻辑

读取设备的电压等参数(异步处理)

五个参数数组,从plc从获取modbus地址读入

 

不管是读取还是写入,避免连续请求,在收到上一个请求的回复后发送下一个请求,在读写过程中都使用

1
2
3
4
5
6
7
8
9
10
11
12
 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函数用来处理数据

 

相似类比 此处的异步处理机制

1
2
3
4
5
6
假设你是老师,要批改 7 份作业(每份作业就是一个读取请求):
你(老师)把第一份作业交给学生(设备)批改(processNextRequest())。
学生批改完后,敲门告诉你“批改好了!”(设备返回数据,Qt 自动调用 readReady())。
你拿到作业,登记成绩(readReady() 解析数据)。
你看还剩几份没批改,如果还有,就再交下一份给学生(readReady() 里再次调用 processNextRequest())。
全部批改完后,你通知班主任“成绩都出来了!”(emit dataUpdated)。

电压柱状图

利用Qbar类实现视觉上的柱状图

数据更新关键代码:

每次 timeout,都会自动调用你 connect 的 lambda 函数,进而调用 updateBarSeries(),刷新柱状图的数据,并更新 UI 上的标签。

1
2
3
4
5
6
7
8
connect(&timer,&QTimer::timeout,this,[this](){//每次 timeout,都会自动调用你 connect 的 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));
});

 

当窗口显示时启动定时器,窗口隐藏时停止定时器。
这样可以保证只有在窗口可见时,才会定时刷新数据,节省资源。

1
2
3
4
5
6
7
8
9
10
11
12
void CellVoltageViewDialog::showEvent(QShowEvent *event)
{
timer.start(int(ui->timeStepSpinBox->value()*1000)); // 窗口显示时启动定时器,timeout的时间就在这定义
QDialog::showEvent(event);
}

void CellVoltageViewDialog::hideEvent(QHideEvent *event)
{
timer.stop(); //窗口隐藏时停止定时器
QDialog::hideEvent(event);
}

 

整体实现逻辑

1
2
3
4
窗口显示时,showEvent() 被触发,定时器 timer 启动。
定时器每隔一段时间(由 timeStepSpinBox 控件决定)就会发出 timeout 信号。
每次 timeout,都会自动调用你 connect 的 lambda 函数,进而调用 updateBarSeries(),刷新柱状图的数据,并更新 UI 上的标签。
窗口隐藏时,hideEvent() 被触发,定时器停止,数据不再刷新。

写入函数和自动电流写入

AutoCurrentWriter 模块

文件信息

  • 文件名: autocurrentwriter.cpp

  • 功能: 自动电流写入控制器,实现从 Excel 加载写入计划并定时执行

功能流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
用户选择Excel文件

loadExcelData() 读取数据 → targetDateTimes/currentValues

fileWatcher 开始监控文件

用户点击“开始自动写入” → 启动定时器

timerAutoCurrentWrite 每秒触发 handNextCurrentWtite()

检查是否到达目标时间 → 到了就写入

┌─────────────────────────────────┐
│ Excel 文件被修改 │
│ ↓ │
│ onExcelFileChanged() 被触发 │
│ ↓ │
│ 延迟 1s → loadExcelData(path) │
│ ↓ │
│ 如果正在写入 → recalculateWriteIndex() │
└─────────────────────────────────┘

类构造与析构

1
2
3
4
5
6
7
8
`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`

信号槽绑定

1
2
3
4
5
6
7
8
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
  • 提供测试功能,通过定时器模拟报警与数据导出。

构造函数

1
2
3
4
5
6
7
DataLogger::DataLogger(QObject *parent)
: QObject(parent)
{
alarmDescriptions = getAlarmDescriptions(); // 报警备注列表
alarmDescriptions_2 = getDescriptions();
alarmDescriptions_3 = QDataLabels();
}

日志写入函数

writeCsvLog

将事件记录到桌面路径的 data_log.csv 文件。

1
2
void DataLogger::writeCsvLog(const QString &timestamp,
const QString &description);

首次写入时会自动添加表头:时间,事件描述

写入成功后输出日志到控制台。

报警处理函数
processTestAlarmData
处理并记录报警与事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
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。

1
2
3
4
5
void DataLogger::exportCurrentDataToCSV(
const std::array<quint16, 100>& readDatas_words2);
首次导出时写表头:时间戳 + 数据标签。

后续写入仅追加数据行。

测试功能

testAlarmSystem
模拟报警系统,每 5 秒触发一次 processTestAlarmData。

1
2
3
4
5
6
7
8
void DataLogger::testAlarmSystem(QBitArray* testDatas_3,
std::array<quint16, 13>* testDatas_words,
QBitArray* testDatas_5);
注意:传入的指针数据必须在定时器生命周期内保持有效。
建议使用 QSharedPointer 或 std::shared_ptr 提升安全性。

exportSystem
定时导出数据到 CSV 文件,每分钟执行一次。

 

 

1
2
3
4
5
void DataLogger::exportSystem(std::array<quint16, 100>* readDatas_words2);
代码安全性提示
当前版本使用 裸指针 传递 QBitArray 和 std::array 数据,依赖于外部对象生命周期。

更推荐使用 QSharedPointer 或 std::shared_ptr 来避免悬空指针。

输出文件
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
2
3
connect(modbusWorker,&ModbusWorker::deviceState,this,\[this\](int state){//子线程向主线程反馈数据状态,主线程获取设备的状态  
connect(modbusWorker,&ModbusWorker::deviceStateResponse,this,...
connect(modbusWorker,&ModbusWorker::dataUpdated,this,&MainWindow::updateDataAndUI);//读取数据更新

子线程向主线程返回状态和更新的数据
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
2
3
4
5
6
7
8
9
//启动线程,以在创建通信设备  
modbusThread->start();
//连接设备
emit connectDevice(1);

modbusWorker->deleteLater();//安全触发modbusWorker析构函数,将停止读取
modbusThread->quit();//关闭线程的事件循环
modbusThread->wait();//确保执行完成
delete modbusThread;//关闭线程

debug point

inline

在reportviewdialog.h文件中

1
2
3
4
5
6
7
8
9
inline int indexof(float value, const std::array&lt;float,125&gt;& 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);</int>
<int>}</int>
<int>return -1; // 没找到</int>
<int>}</int>
定义inline的原因:多个.cpp文件包含了reportviewdialog.h文件

 

inline 和多重定义: 如果将非 inline 函数的定义放在头文件中,并在多个 .cpp 文件中包含该头文件,会导致链接错误(多重定义错误)。但是,将 inline 函数的定义放在头文件中是安全的,因为编译器知道这些定义在链接时可能会被多次看到,并且会正确处理它们(通常只会保留一个实际的函数实现,或者完全内联)。

柱状图中不显示第一或第二根柱子

9c5e1b2d1faa2ebded52648d38b37414.png

数据报表导出excel卡顿问题

更换成csv格式导出
Excel格式(如.xlsx)需要用专门的库(如QXlsx),每次写入都要处理复杂的格式、压缩、XML结构,写入速度远慢于CSV。
CSV是纯文本,写入速度极快,适合实时、批量导出。

实时报表中没有进行实时更新

问题原因:
页面数据不能实时更新,根本原因是:
m_reportData1 和 m_reportData2 只在构造函数里赋值了一次,之后就没有再改变。
每次定时器触发时,虽然会重绘页面(update() → paintEvent() → 重新计算参数),但用的还是老数据,所以页面内容不会变。
最根本原因还是最新数据数组需要用指针进行传参,才能改变数组中的值

解决方案
用指针传递数组地址,才能保证数据变化时,所有用到这份数据的地方都能“看到”最新内容。
这也是C++/Qt开发中实时数据共享的常用技巧。

 

传入数组指针的函数实现如下:

1
2
3
4
5
void ReportViewDialog::setExternalDataSource(const std::array&lt;float,125&gt;\* pData1, const std::array&lt;float,44&gt;\* pData2)  
{
m_externalData1 = pData1;
m_externalData2 = pData2;
}

 

定时器触发updatereport代码

connect(&m_timer, &QTimer::timeout, this, &ReportViewDialog::updateReport);

 

updatareport的修改逻辑如下:

1
2
3
4
5
6
7
if (m_externalData1 && m_externalData2) {  
// 使用外部数据源实时刷新
m_contentWidget->setData(\*m_externalData1, \*m_externalData2);
} else {
// 保持原有逻辑
m_contentWidget->update();
}

 

setdata的修改逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
void setData(const std::array&lt;float, 125&gt;& data1, const std::array&lt;float, 44&gt;& data2)  
{
m_reportData1 = data1;
m_reportData2 = data2;
// 重新计算参数
// ... 计算电流密度、最大电压、平均电压、标准差等 ...
update(); // 触发重绘
}

setData() 会把新数据保存到控件内部,并重新计算各种报表参数(如最大电压、平均电压、标准差等)。
最后调用 update(),触发界面重绘,让用户看到最新的数据和图表。

&nbsp;

集成在主窗口函数中使用:
mainwindow.cpp

1
2
3
4
5
6
7
8
9
10
11
12
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();
}

数据更新逻辑:

  1. mainwindow中调用的setExternalDataSource函数把最新数据的指针传递给报表窗口
  2. ReportViewDialog函数窗口显示启动定时器,和updataReport()关联
  3. 定时器刷新一次就自动调用updataReport(),updataReport函数会调用setdata函数
  4. setdata()函数接收最新数据,处理内容控件数据,调用update()函数 请求Qt重绘控件
  5. Qt 框架检测到 update() 被调用后,会自动触发控件的 paintEvent()。在 paintEvent() 里,调用 drawReportContent(),用最新的数据把报表内容画到页面上。

新增数据库类管理模块

MVC框架

model view controller

model: 基础的类的实现

view: 页面显示类的实现

controller: 实现基础类的逻辑控制层

基础数据库类(单例类)

单例类的设计

关键代码

1
2
3
4
5
6
7
8
9
std::shared_ptr <database>Database::instance()//懒汉模式使用智能指针</database>  
<database>{</database>
<database>static std::once_flag s_flag;//静态标志,保证下面的初始化只执行一次(多线程安全)。</database>
<database>std::call_once(s_flag,</database>&<database>//只会执行一次 lambda 表达式里的代码,确保只创建一个实例。</database>
<database>{</database>
<database>data = std::shared_ptr(new Database("2"));</database>
<database>});</database>
<database>return data;//外部只能instance获取唯一数据库对象</database>
<database><database>}</database></database>

创建数据库类为单例类,在项目中只允许一个数据库对象实例的出现,确保不重复访问数据库

登录界面的设计

关键的密码隐蔽

1
2
3
4
5
6
7
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和lineEdit控件的连接转换

关键 model-view模式

1.model 使用QSqlTableModel模板,有自带的数据库获取属性

2.model对象设置完数据库表后,放入tableview中

3.调用model中自带的数据库操作函数完成编写

表嵌入到模型和视图的代码

1
2
3
4
5
6
m_model = new QSqlTableModel(this, db);  
m_model->setTable("DAta"); // 设置你的表名
m_model->setEditStrategy(QSqlTableModel::OnManualSubmit);//设置编辑模式

// 将模型设置到视图中
m_view->setModel(m_model);

插入行代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);

删除 提交操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//删除操作  
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->removeRow();

//提交操作
m_model->submitAll();

删除行操作需要先获取视图中所选择的行,再进行循环删除

 

view类的实现

将控件作为私有变量,定义get方法获取控件
代码示例:

1
2
3
4
5
6
7
//头文件中  
private:
QTableView\* DatabaseView;
public:
QTableView\* getTableView(); const
//实现源文件
QTableView\* DatabaseView::getTableView() const { return ui->tableView; }

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 或 QSharedPointer,注意不要再设置父对象,否则冲突。</qobject>

00ab277b9395849df6680bd1e951c898.png