250ETS 总结

250ETS 总结

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 份作业,每份作业就是一个读取请求:

  1. 你(老师)把第一份作业交给学生(设备)批改,对应 processNextRequest()。
  2. 学生批改完后,敲门告诉你“批改好了”,对应设备返回数据,Qt 自动调用 readReady()。
  3. 你拿到作业,登记成绩,对应 readReady() 解析数据。
  4. 你看还剩几份没批改,如果还有,就再交下一份给学生,对应在 readReady() 中再次调用 processNextRequest()。
  5. 全部批改完后,你通知班主任“成绩都出来了”,对应 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);
}

整体实现逻辑

  1. 窗口显示时,showEvent() 被触发,定时器 timer 启动。
  2. 定时器每隔一段时间(由 timeStepSpinBox 控件决定)发出 timeout 信号。
  3. 每次 timeout,自动调用 connect 中的 lambda。
  4. lambda 调用 updateBarSeries(),刷新柱状图数据。
  5. 同时更新 UI 上的相关标签。
  6. 窗口隐藏时,hideEvent() 被触发,定时器停止。
  7. 窗口隐藏后不再刷新数据。

写入函数和自动电流写入

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
)

主要功能:

  1. 清空旧数据
  2. 从 Excel 中逐行读取
  3. 第一列 → targetDateTimes
  4. 第二列 → currentValues
  5. 最终存储到容器中,支持 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 &timestamp,
    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 是自定义的通信处理对象,继承自 QObject
  • moveToThread() 把 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() 会:

  1. 接收最新数据
  2. 保存到控件内部
  3. 重新计算报表参数
  4. 计算最大电压
  5. 计算平均电压
  6. 计算标准差等参数
  7. 调用 update()
  8. 请求 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()
    │
    ↓
显示最新数据

具体步骤:

  1. MainWindow 调用 setExternalDataSource(),把最新数据的指针传递给报表窗口。
  2. ReportViewDialog 显示窗口后启动定时器。
  3. 定时器触发 updateReport()。
  4. updateReport() 调用 setData()。
  5. setData() 接收最新数据,并处理内容控件数据。
  6. setData() 调用 update() 请求 Qt 重绘控件。
  7. Qt 检测到 update() 后触发 paintEvent()。
  8. paintEvent() 中调用 drawReportContent()。
  9. 使用最新数据绘制报表。

新增数据库类管理模块

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 派生类

例如:

  • QWidget
  • QTimer
  • QDialog
  • QObject

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 更新
载入内容