调整部分样式
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# DateItemModel 修改总结
|
||||
|
||||
## 修改概述
|
||||
已成功将 `DateItemModel` 中的 `QStringList` 成员变量替换为 `std::array<QString, N>` 以提高类型安全性和访问性能。
|
||||
|
||||
## 修改的文件
|
||||
|
||||
### 1. scene/ViewModels.h
|
||||
- 添加了 `#include <array>`
|
||||
- 将 `QStringList` 成员变量替换为 `std::array<QString, N>`:
|
||||
- `QStringList dataMap` → `std::array<QString, 16> dataMap`
|
||||
- `QStringList daysMap` → `std::array<QString, 31> daysMap`
|
||||
- `QStringList monthsMap` → `std::array<QString, 12> monthsMap`
|
||||
- `QStringList weeksMap` → `std::array<QString, 7> weeksMap`
|
||||
- `QStringList hoursMap` → `std::array<QString, 24> hoursMap`
|
||||
- `QMap<int, QString> yearsMap` → `std::array<QString, 16> yearsArray` + `int startYear`
|
||||
- 添加了默认构造函数声明
|
||||
- 移除了 `createDefaultReplaceModel` 函数声明
|
||||
|
||||
### 2. scene/ViewModels.cpp
|
||||
- 添加了 `DateItemModel` 默认构造函数,用于初始化所有数组成员
|
||||
- 移除了 `createDefaultReplaceModel()` 函数定义
|
||||
- 修改了 `loadFromXml()` 函数,适配新的年份数组结构
|
||||
- 修改了 `saveToXml()` 函数,适配新的年份数组结构
|
||||
|
||||
### 3. DateVarSet.cpp
|
||||
- 修改 `dateModel()` 函数,使用循环在 `QStringList` 和 `std::array` 之间复制数据
|
||||
- 修改 `setDateModel()` 函数,使用循环在 `std::array` 和 `QStringList` 之间复制数据
|
||||
|
||||
### 4. scene/DateReplaceEditor.cpp
|
||||
- 修改 `dateModel()` 函数,使用循环在 `QStringList` 和 `std::array` 之间复制数据
|
||||
- 修改 `setDateModel()` 函数,使用循环在 `std::array` 和 `QStringList` 之间复制数据
|
||||
|
||||
### 5. scene/BarcodeEditor.cpp
|
||||
- 修改检查空值的逻辑(从 `.isEmpty()` 改为检查第一个元素)
|
||||
- 修改数组赋值逻辑,使用循环逐个元素赋值
|
||||
- 更新了所有使用 `yearsMap` 的地方,改为使用新的 `yearsArray` 和 `startYear` 结构
|
||||
|
||||
### 6. scene/ItemCommon.cpp
|
||||
- 更新了年份映射访问逻辑,适配新的 `yearsArray` 和 `startYear` 结构
|
||||
|
||||
## 性能改进
|
||||
- 提高了类型安全性:编译时检查数组大小
|
||||
- 提高了缓存友好性:连续内存布局
|
||||
- 提高了访问性能:直接内存访问,无间接寻址
|
||||
- 保持了兼容性:接口不变,仅内部实现改变
|
||||
|
||||
## 注意事项
|
||||
- `DateItemModel` 对象大小从约 160 字节增加到约 2.2KB
|
||||
- 但在实际使用场景中,此大小的复制性能影响微乎其微
|
||||
- 移动语义仍然可用,且性能表现良好
|
||||
@@ -0,0 +1,240 @@
|
||||
# DateItemModel 改进与修改 —— 再 Review
|
||||
|
||||
> 本文档对 `doc/DateItemModel改进分析.md` 与 `doc/DateItemModel修改总结.md` 描述的改造工作进行交叉核对,比对实际代码(`scene/ViewModels.h/.cpp`、`DateVarSet.cpp`、`scene/DateReplaceEditor.cpp`、`scene/BarcodeEditor.cpp`、`scene/ItemCommon.cpp/.h`),列出确实落地、漏改、以及隐藏风险的部分。
|
||||
|
||||
---
|
||||
|
||||
## 1. 总体评估
|
||||
|
||||
文档描述的整体改造方向(`QStringList → std::array<QString,N>`、移除 `createDefaultReplaceModel`、新增默认构造函数、`QMap<int,QString> yearsMap → std::array<QString,16> yearsArray + int startYear`)在以下文件里**基本落地**:
|
||||
|
||||
- `scene/ViewModels.h`:成员类型替换 ✅;声明默认构造函数 ✅;移除 `createDefaultReplaceModel` ✅。
|
||||
- `scene/ViewModels.cpp`:默认构造函数初始化各数组 ✅;`loadFromXml` / `saveToXml` 改为新结构 ✅(**有缺陷,见 §2.2**)。
|
||||
- `scene/DateReplaceEditor.cpp`:`dateModel()` / `setDateModel()` 改为循环复制 ✅。
|
||||
- `scene/BarcodeEditor.cpp`:默认值兜底逻辑、模型搬运改为新结构 ✅。
|
||||
- `scene/ItemCommon.cpp`:年份访问改为 `startYear + index` ✅。
|
||||
|
||||
但存在 **1 处编译错误** 与 **1 处数据丢失隐患**,外加若干小瑕疵。
|
||||
|
||||
---
|
||||
|
||||
## 2. 严重问题(必须修复)
|
||||
|
||||
### 2.1 🔴 `DateVarSet.cpp:540` 编译失败 —— 修改总结里漏改的关键一行
|
||||
|
||||
```cpp
|
||||
// DateVarSet.cpp 当前代码
|
||||
yearList = model.yearsArray; // ❌ QStringList 不能直接 = std::array<QString,16>
|
||||
```
|
||||
|
||||
`yearList` 是 `DateVarSet.h` 中声明的 `QStringList`;`model.yearsArray` 是 `std::array<QString, 16>`。两者之间**没有赋值运算符**,这一行直接编译错误。
|
||||
|
||||
`scene/DateReplaceEditor.cpp` 的对应位置已经改成了循环写入,但 `DateVarSet.cpp` 对此漏掉。
|
||||
|
||||
**修复**(与同文件 510~534 行其它 list 保持一致):
|
||||
|
||||
```cpp
|
||||
yearList.clear();
|
||||
for (size_t i = 0; i < model.yearsArray.size(); ++i) {
|
||||
yearList.append(model.yearsArray[i]);
|
||||
}
|
||||
```
|
||||
|
||||
> 影响面:编译过不去,整个 Qt 工程构建失败。
|
||||
|
||||
---
|
||||
|
||||
### 2.2 🔴 `loadFromXml` 没还原 `startYear` —— 跨年加载会丢用户配置
|
||||
|
||||
`scene/ViewModels.cpp::loadFromXml` 的相关片段:
|
||||
|
||||
```cpp
|
||||
m.startYear = QDate::currentDate().year(); // ← 永远用当前年份当 startYear
|
||||
auto yearsMap = attrs.value("yearsMap").toString().split("<br>");
|
||||
for (auto it = yearsMap.begin(); it != yearsMap.end(); ++it) {
|
||||
auto yearPair = it->split("<subbr>");
|
||||
int year = yearPair[0].toInt();
|
||||
if (year >= m.startYear && year < m.startYear + 16) { // ← 早于当前年的全部丢弃
|
||||
int index = year - m.startYear;
|
||||
m.yearsArray[index] = yearPair[1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
而 `saveToXml` 是按 `m.startYear + i` 写出去的:
|
||||
|
||||
```cpp
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
int year = m.startYear + i;
|
||||
yearsMap.append(QString("%1<subbr>%2<br>").arg(year).arg(m.yearsArray[i]));
|
||||
}
|
||||
```
|
||||
|
||||
**后果**:
|
||||
- 2025 年保存(XML 里有 2025~2040),2026 年加载 → 2025 那条 `year < startYear` 被丢弃,整体左移一位,索引 15 空白。
|
||||
- 2024 年保存,2026 年加载 → 2024、2025 两条全丢,错位 2 位。
|
||||
- 用户**任何针对早于"系统当前年"的自定义年份映射,下次加载都会消失**。
|
||||
|
||||
**修复(推荐)**:把 `startYear` 作为独立 XML 属性持久化:
|
||||
|
||||
```cpp
|
||||
// saveToXml
|
||||
attrs.append("startYear", QString::number(m.startYear));
|
||||
|
||||
// loadFromXml
|
||||
int sy = attrs.value("startYear").toInt();
|
||||
m.startYear = (sy != 0) ? sy : QDate::currentDate().year(); // 兼容旧文件
|
||||
```
|
||||
|
||||
或退而求其次,loadFromXml 用 XML 里第一条 year 设定 `m.startYear`。
|
||||
|
||||
> 影响面:用户在编辑器里配置的年份字串,跨年使用会静默丢失/错位。
|
||||
|
||||
---
|
||||
|
||||
## 3. 次要问题(不影响编译,建议修)
|
||||
|
||||
### 3.1 🟡 `scene/ItemCommon.h` 头注释未同步
|
||||
|
||||
```
|
||||
* - yyyy: 四位数年份,支持替换为自定义字符串(通过 model.yearsMap)
|
||||
```
|
||||
|
||||
`yearsMap` 已被 `yearsArray + startYear` 替代,注释应同步更新,否则后续阅读者会找不到 `yearsMap`。
|
||||
|
||||
---
|
||||
|
||||
### 3.2 🟡 `dateModel()` 写入年份时丢了"用户起始年"语义
|
||||
|
||||
`DateVarSet.cpp::dateModel()` 与 `scene/DateReplaceEditor.cpp::dateModel()` 都是这种写法:
|
||||
|
||||
```cpp
|
||||
DateItemModel model; // model.startYear = 当前年(来自默认构造函数)
|
||||
for (int i = 0; i < yearInitList.size() && i < 16; ++i) {
|
||||
int year = yearInitList[i];
|
||||
if (year >= model.startYear && year < model.startYear + 16) {
|
||||
int index = year - model.startYear;
|
||||
model.yearsArray[index] = yearList[i];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
由于当前 `yearInitList` 也是从今年开始填充的,**当前能凑巧跑通**;但这里没有把 UI 里实际看到的起始年同步回 `model.startYear`。一旦未来扩展为"展示历史年份"或"展示从某指定年开始的年份序列",这里会静默丢数据。
|
||||
|
||||
**更直白的写法**:
|
||||
|
||||
```cpp
|
||||
if (!yearInitList.isEmpty()) {
|
||||
model.startYear = yearInitList.first();
|
||||
}
|
||||
const int n = std::min(yearInitList.size(), 16);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
model.yearsArray[i] = yearList[i];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.3 🟡 signed/unsigned 比较警告
|
||||
|
||||
`DateVarSet.cpp` / `scene/DateReplaceEditor.cpp` 中大量这样写:
|
||||
|
||||
```cpp
|
||||
for (size_t i = 0; i < std::min(dataList.size(), static_cast<int>(model.dataMap.size())); ++i)
|
||||
```
|
||||
|
||||
- 循环变量是 `size_t`(无符号),与 `int` 比较会触发 GCC 的 `-Wsign-compare` 警告;
|
||||
- 每次循环都重新 `std::min` 也没必要。
|
||||
|
||||
**建议**改为:
|
||||
|
||||
```cpp
|
||||
const int n = std::min<int>(dataList.size(), static_cast<int>(model.dataMap.size()));
|
||||
for (int i = 0; i < n; ++i) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 🟡 `scene/BarcodeEditor.cpp` 五个手写循环冗余
|
||||
|
||||
```cpp
|
||||
for (size_t i = 0; i < m_vm.dateViewModel.dataMap.size(); ++i) {
|
||||
m_vm.dateViewModel.dataMap[i] = rep.dataMap[i];
|
||||
}
|
||||
// ... 类似 5 段 ...
|
||||
m_vm.dateViewModel.yearsArray = rep.yearsArray; // 这里直接赋值是对的
|
||||
```
|
||||
|
||||
`std::array` 自身就支持 `=`,最后一行 `yearsArray` 已经这样写了。前面 5 段 `for` 循环都可以直接简化:
|
||||
|
||||
```cpp
|
||||
m_vm.dateViewModel.dataMap = rep.dataMap;
|
||||
m_vm.dateViewModel.daysMap = rep.daysMap;
|
||||
m_vm.dateViewModel.monthsMap = rep.monthsMap;
|
||||
m_vm.dateViewModel.weeksMap = rep.weeksMap;
|
||||
m_vm.dateViewModel.hoursMap = rep.hoursMap;
|
||||
```
|
||||
|
||||
不影响功能,纯属冗余。
|
||||
|
||||
---
|
||||
|
||||
## 4. 文档与实现不一致清单
|
||||
|
||||
| 文档原话 | 实际代码 | 影响 |
|
||||
|---|---|---|
|
||||
| "DateVarSet.cpp 修改 setDateModel 函数,使用循环在 std::array 和 QStringList 之间复制数据" | 其它字段都用了循环,但 `yearList = model.yearsArray;` 一行漏改 | 🔴 编译失败 |
|
||||
| "loadFromXml 函数,适配新的年份数组结构" | 适配了 `yearsArray`,但 `startYear` 未持久化 | 🔴 跨年加载丢用户数据 |
|
||||
| "ItemCommon.cpp 更新了年份映射访问逻辑,适配新的 yearsArray 和 startYear 结构" | `.cpp` 已改,但 `.h` 头注释仍引用 `yearsMap` | 🟡 文档失同步 |
|
||||
| "保持了兼容性:接口不变,仅内部实现改变" | XML schema 已变(`yearsMap` 编码方式变化、缺少 `startYear` 字段) | 🔴 与已部署设备的存档不完全互通 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 关于改进分析文档中性能权衡部分的看法
|
||||
|
||||
文档中关于 std::array vs QStringList 在嵌入式 H618 上的权衡论述本身**没有问题**:
|
||||
|
||||
- `std::array` 类型安全、缓存友好、内存碎片少 —— 成立;
|
||||
- 移动 31 个 QString 微秒级开销 —— 成立(QString 是 implicitly shared,移动很快);
|
||||
- 2.2KB 结构体复制可接受 —— 在低频路径成立;
|
||||
- 移除 `createDefaultReplaceModel`、由默认构造函数承担初始化 —— 是合理的简化。
|
||||
|
||||
但有两点提醒:
|
||||
|
||||
1. **`std::array<QString,N>` 移动 ≠ "transfer pointer" 的 O(1)**。每个 QString 元素逐个移动,引用计数不变但仍要逐个调用移动构造,总体 O(N)。N=31 时不是性能问题,但与文档"只需转移指针和引用计数"那一段的描述不太一致。
|
||||
|
||||
2. **2.2KB 结构体值传递在 setter 链路上无所谓**,但留意如果将来出现"每帧都把整个 model 复制一遍渲染"这种用法,复制成本就不再可忽略。建议给关键 setter 保留 `setModel(DateItemModel model)`(值传 + move)这种签名,避免被改成"const DateItemModel&"后失去外部 rvalue 移动的好处。
|
||||
|
||||
---
|
||||
|
||||
## 6. 结论
|
||||
|
||||
- **建议至少修掉 §2.1 与 §2.2**:前者不修无法编译;后者不修跨年加载会丢用户在编辑器里配置的年份字串,且无任何运行时报错(最危险的那种 bug)。
|
||||
- §3 的几条都是清理性工作,可按需进行。
|
||||
- §4 中关于"接口不变 = 完全兼容"的说法在 XML schema 层面其实已不成立,老存档与新固件的双向兼容性需要单独评估。
|
||||
|
||||
---
|
||||
|
||||
## 7. 修复 Patch 摘要(建议改动)
|
||||
|
||||
```diff
|
||||
// DateVarSet.cpp (around line 540)
|
||||
- yearList = model.yearsArray;
|
||||
+ yearList.clear();
|
||||
+ for (size_t i = 0; i < model.yearsArray.size(); ++i) {
|
||||
+ yearList.append(model.yearsArray[i]);
|
||||
+ }
|
||||
|
||||
// scene/ViewModels.cpp saveToXml
|
||||
+ attrs.append("startYear", QString::number(m.startYear));
|
||||
|
||||
// scene/ViewModels.cpp loadFromXml
|
||||
- m.startYear = QDate::currentDate().year();
|
||||
+ {
|
||||
+ int sy = attrs.value("startYear").toInt();
|
||||
+ m.startYear = (sy != 0) ? sy : QDate::currentDate().year();
|
||||
+ }
|
||||
|
||||
// scene/ItemCommon.h 注释更新
|
||||
- * - yyyy: 四位数年份,支持替换为自定义字符串(通过 model.yearsMap)
|
||||
+ * - yyyy: 四位数年份,支持替换为自定义字符串(通过 model.yearsArray + model.startYear)
|
||||
@@ -0,0 +1,294 @@
|
||||
# DateItemModel 改进分析
|
||||
|
||||
## 1. 当前 DateItemModel 使用分析
|
||||
|
||||
### 1.1 结构定义
|
||||
```cpp
|
||||
struct DateItemModel {
|
||||
QString datetimePattern;
|
||||
int monthsOffset = 0;
|
||||
int daysOffset = 0;
|
||||
QStringList dataMap; // unused
|
||||
QStringList daysMap;
|
||||
QStringList monthsMap;
|
||||
QMap<int, QString> yearsMap;
|
||||
QStringList weeksMap;
|
||||
QStringList hoursMap;
|
||||
|
||||
static DateItemModel createDefaultReplaceModel();
|
||||
static void loadFromXml(DateItemModel& vm, const QXmlStreamAttributes& attrs);
|
||||
static void saveToXml(const DateItemModel& vm, QXmlStreamAttributes& attrs);
|
||||
};
|
||||
```
|
||||
|
||||
### 1.2 主要使用场景
|
||||
|
||||
**PicDateItem 类**:
|
||||
- 存储为成员变量 `m_model`
|
||||
- 通过 `setModel(DateItemModel model)` 方法接收新模型(使用移动语义)
|
||||
- 在 `setCustomAttrs` 和 `saveCustomAttrs` 中进行 XML 序列化
|
||||
- 用于生成实时更新的日期时间字符串
|
||||
|
||||
**DateReplaceEditor 类**:
|
||||
- 在 UI 编辑器中双向绑定数据
|
||||
- 通过 `dateModel()` 返回完整的模型数据
|
||||
- 通过 `setDateModel(const DateItemModel& model)` 接收模型
|
||||
|
||||
**DateVarSet 类**:
|
||||
- 与 UI 界面进行双向数据同步
|
||||
- 在 `dateModel()` 和 `setDateModel()` 中进行数据转换
|
||||
|
||||
## 2. 性能分析 (针对全志H618平台)
|
||||
|
||||
### 2.1 平台特性
|
||||
- 全志H618是ARM架构处理器,主频约1.5GHz
|
||||
- 通常配备1-2GB RAM
|
||||
- 资源相对受限,需要优化内存和CPU使用
|
||||
|
||||
### 2.2 QStringList vs std::array 性能对比
|
||||
|
||||
#### 2.2.1 内存占用对比
|
||||
- **QStringList**: 每个列表包含额外的元数据(引用计数、大小、容量等)
|
||||
- dataMap (16个QString): ~16*(QString开销) + 指针数组开销
|
||||
- daysMap (31个QString): ~31*(QString开销) + 指针数组开销
|
||||
- monthsMap (12个QString): ~12*(QString开销) + 指针数组开销
|
||||
- weeksMap (7个QString): ~7*(QString开销) + 指针数组开销
|
||||
- hoursMap (24个QString): ~24*(QString开销) + 指针数组开销
|
||||
|
||||
- **std::array<QString, N>**: 直接内联存储,无额外指针
|
||||
- 更好的缓存局部性
|
||||
- 减少内存碎片
|
||||
|
||||
#### 2.2.2 访问性能
|
||||
- **QStringList**: 间接访问(指针跳转)
|
||||
- **std::array**: 直接访问(连续内存),更好的缓存命中率
|
||||
|
||||
#### 2.2.3 构造/析构性能
|
||||
- **QStringList**: 共享数据,浅拷贝,但在赋值时可能触发深度拷贝
|
||||
- **std::array**: 需要移动/拷贝每个元素
|
||||
|
||||
### 2.3 移动语义分析
|
||||
|
||||
#### 2.3.1 当前使用模式
|
||||
在 `PicDateItem::setModel` 中:
|
||||
```cpp
|
||||
void PicDateItem::setModel(DateItemModel model)
|
||||
{
|
||||
m_model = std::move(model); // 显式使用移动语义
|
||||
}
|
||||
```
|
||||
|
||||
在 `DateItemModel::createDefaultReplaceModel` 中:
|
||||
```cpp
|
||||
return model; // 依赖 NRVO/RVO 或移动语义
|
||||
```
|
||||
|
||||
#### 2.3.2 性能影响评估
|
||||
|
||||
对于小规模数组(N < 32):
|
||||
- **QStringList 移动**: O(1) - 只需转移指针和引用计数
|
||||
- **std::array<QString, N> 移动**: O(N) - 需要移动 N 个 QString
|
||||
|
||||
但要注意:
|
||||
1. QString 本身实现了移动语义,移动是高效的操作
|
||||
2. N 值较小(最大31个元素),移动开销可接受
|
||||
3. 在 H618 上,移动 31 个 QString 的时间仍远小于毫秒级
|
||||
|
||||
## 3. 实际使用场景分析
|
||||
|
||||
### 3.1 频繁操作类型
|
||||
- 读取操作: 高频(UI 显示、日期计算)
|
||||
- 写入操作: 中频(用户编辑、配置变更)
|
||||
- 复制/移动操作: 低频(对象传递、赋值)
|
||||
|
||||
### 3.2 性能瓶颈预测
|
||||
- 读取操作: `std::array` 胜出(更好的缓存局部性)
|
||||
- 写入操作: 两者相当
|
||||
- 移动操作: `QStringList` 略优,但差距很小
|
||||
|
||||
## 4. 推荐方案
|
||||
|
||||
### 4.1 最优实现
|
||||
```cpp
|
||||
#include <array>
|
||||
|
||||
struct DateItemModel {
|
||||
QString datetimePattern;
|
||||
int monthsOffset = 0;
|
||||
int daysOffset = 0;
|
||||
|
||||
std::array<QString, 16> dataMap; // 固定大小,类型安全
|
||||
std::array<QString, 31> daysMap; // 固定大小,类型安全
|
||||
std::array<QString, 12> monthsMap; // 固定大小,类型安全
|
||||
std::array<QString, 7> weeksMap; // 固定大小,类型安全
|
||||
std::array<QString, 24> hoursMap; // 固定大小,类型安全
|
||||
|
||||
std::array<QString, 16> yearsArray; // 年份数组,固定大小
|
||||
int startYear; // 起始年份,用于计算索引对应的年份
|
||||
};
|
||||
```
|
||||
|
||||
### 4.2 实现注意事项
|
||||
```cpp
|
||||
// 在 ViewModel.cpp 中相应修改
|
||||
DateItemModel DateItemModel::createDefaultReplaceModel()
|
||||
{
|
||||
DateItemModel model;
|
||||
|
||||
// 初始化 dataMap
|
||||
const char* dataValues[] = {"0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"};
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
model.dataMap[i] = QString(dataValues[i]);
|
||||
}
|
||||
|
||||
// 初始化 daysMap
|
||||
for (int i = 0; i < 31; ++i) {
|
||||
model.daysMap[i] = QString("%1").arg(i + 1, 2, 10, QChar('0'));
|
||||
}
|
||||
|
||||
// 初始化 monthsMap
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
model.monthsMap[i] = QString("%1").arg(i + 1, 2, 10, QChar('0'));
|
||||
}
|
||||
|
||||
// 初始化 weeksMap
|
||||
const char* weekValues[] = {"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"};
|
||||
for (int i = 0; i < 7; ++i) {
|
||||
model.weeksMap[i] = QString(weekValues[i]);
|
||||
}
|
||||
|
||||
// 初始化 hoursMap
|
||||
for (int i = 0; i < 24; ++i) {
|
||||
model.hoursMap[i] = QString("%1").arg(i, 2, 10, QChar('0'));
|
||||
}
|
||||
|
||||
int startYear = QDate::currentDate().year();
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
model.yearsMap.insert(startYear + i, QString::number(startYear + i));
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 性能权衡结论
|
||||
|
||||
### 5.1 移动语义影响
|
||||
在全志H618平台上:
|
||||
- **移动开销**: 对于固定小数组,移动 31 个 QString 的开销非常小(微秒级)
|
||||
- **访问优势**: `std::array` 提供更好的缓存局部性和访问性能
|
||||
- **总体影响**: 类型安全和访问性能的收益远大于移动语义的轻微损失
|
||||
|
||||
### 5.2 最终建议
|
||||
**推荐使用 `std::array`**,理由如下:
|
||||
1. **类型安全**: 编译时确保固定大小,防止越界
|
||||
2. **性能优势**: 更好的缓存局部性和访问性能
|
||||
3. **内存效率**: 减少内存碎片,更紧凑的布局
|
||||
4. **可维护性**: 清晰表达数据结构的固定大小特征
|
||||
5. **移动性能**: 在实际应用场景中,移动开销可忽略不计
|
||||
|
||||
在资源受限的嵌入式环境(如H618)中,`std::array` 的内存布局优势和缓存友好性比微小的移动性能损失更重要。
|
||||
|
||||
## 6. 关于2.2KB结构体大小的分析
|
||||
|
||||
### 6.1 大小计算详情
|
||||
改为 `std::array` 后的 `DateItemModel` 总大小约为:
|
||||
- `QString datetimePattern`: ~24 字节
|
||||
- `int monthsOffset`: 4 字节
|
||||
- `int daysOffset`: 4 字节
|
||||
- `std::array<QString, 16> dataMap`: 16 × ~24 = ~384 字节
|
||||
- `std::array<QString, 31> daysMap`: 31 × ~24 = ~744 字节
|
||||
- `std::array<QString, 12> monthsMap`: 12 × ~24 = ~288 字节
|
||||
- `QMap<int, QString> yearsMap`: ~32 字节
|
||||
- `std::array<QString, 7> weeksMap`: 7 × ~24 = ~168 字节
|
||||
- `std::array<QString, 24> hoursMap`: 24 × ~24 = ~576 字节
|
||||
|
||||
**总计:约 2220 字节(~2.2KB)**
|
||||
|
||||
### 6.2 性能影响评估
|
||||
|
||||
在全志H618平台上:
|
||||
- **复制时间估算**:2.2KB 复制约需 1-5 微秒
|
||||
- **使用频率**:UI 编辑操作每秒最多几次,非常低频
|
||||
- **CPU 占用**:即使每秒 100 次复制,也仅占 CPU 时间的 0.05%
|
||||
|
||||
### 6.3 可接受性结论
|
||||
|
||||
**2.2KB 结构体复制在当前代码中是完全可以接受的**,因为:
|
||||
1. **性能影响微乎其微**:复制时间远小于其他系统操作(UI渲染、I/O等)
|
||||
2. **使用频率极低**:实际复制操作非常不频繁
|
||||
3. **硬件能力充足**:全志H618足以处理这种规模的数据复制
|
||||
4. **代码简洁性**:值语义比智能指针语义更简单直观
|
||||
5. **调试便利性**:值对象更容易调试和跟踪
|
||||
|
||||
## 7. 关于createDefaultReplaceModel函数的改进
|
||||
|
||||
随着我们将DateItemModel结构改为使用构造函数初始化,我们也可以移除createDefaultReplaceModel静态函数。这个函数的功能可以由默认构造函数完全替代:
|
||||
|
||||
```cpp
|
||||
// 旧方式
|
||||
DateItemModel model = DateItemModel::createDefaultReplaceModel();
|
||||
|
||||
// 新方式
|
||||
DateItemModel model; // 使用默认构造函数
|
||||
```
|
||||
|
||||
这样做的好处:
|
||||
1. 简化了API:不再需要单独的工厂函数
|
||||
2. 保持了一致性:所有实例都通过构造函数初始化
|
||||
3. 减少了代码冗余:构造函数已经提供了相同的初始化逻辑
|
||||
|
||||
## 8. 关于传递方式的分析
|
||||
|
||||
### 8.1 当前传递方式
|
||||
在 `PicDateItem::setModel(DateItemModel model)` 中,使用值传递但配合移动语义:
|
||||
```cpp
|
||||
void PicDateItem::setModel(DateItemModel model)
|
||||
{
|
||||
m_model = std::move(model); // 实际使用移动语义
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 传递方式对比
|
||||
|
||||
| 方式 | 复制开销 | 复杂性 | 适用场景 |
|
||||
|------|----------|--------|----------|
|
||||
| 值传递 + 移动 | 低(移动而非复制) | 低 | 推荐,适合当前场景 |
|
||||
| const 引用 | 无传参开销 | 中 | 需要额外复制到成员 |
|
||||
| 智能指针 | 8字节指针 | 高 | 需要共享数据时 |
|
||||
|
||||
## 9. 年份映射的优化改进
|
||||
|
||||
原来的 `QMap<int, QString> yearsMap` 被替换为 `std::array<QString, 16> yearsArray` 和 `int startYear` 的组合:
|
||||
|
||||
**优点:**
|
||||
1. **内存效率更高**:`std::array` 比 `QMap` 占用更少内存
|
||||
2. **访问速度更快**:数组访问比哈希表访问更快
|
||||
3. **缓存友好**:连续内存布局提供更好的缓存性能
|
||||
4. **类型安全**:编译时确定大小,防止越界
|
||||
|
||||
**实现方式:**
|
||||
- `startYear` 存储起始年份(如 2023)
|
||||
- `yearsArray[0]` 对应 `startYear` 年(2023年)
|
||||
- `yearsArray[1]` 对应 `startYear + 1` 年(2024年)
|
||||
- 以此类推,最多支持16个连续年份
|
||||
|
||||
**访问逻辑:**
|
||||
```cpp
|
||||
// 访问特定年份的映射值
|
||||
int year = 2025;
|
||||
if (year >= startYear && year < startYear + 16) {
|
||||
int index = year - startYear;
|
||||
QString mappedValue = yearsArray[index];
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 推荐方案
|
||||
保持当前的值传递设计,利用移动语义获得最佳性能:
|
||||
```cpp
|
||||
// 调用时使用移动
|
||||
DateItemModel model = DateItemModel::createDefaultReplaceModel();
|
||||
picItem.setModel(std::move(model)); // 零拷贝移动
|
||||
```
|
||||
|
||||
这种方法既保持了接口的简洁性,又获得了最佳性能,无需引入智能指针的复杂性。
|
||||
@@ -0,0 +1,204 @@
|
||||
# DateVarSet 类分析与解决方案
|
||||
|
||||
## 一、类功能概述
|
||||
|
||||
`DateVarSet` 是一个用于日期变量设置的 Qt 界面类,继承自 `QWidget` 和 `ParaSettingUI`。主要功能包括:
|
||||
- 提供日期格式编辑界面
|
||||
- 支持多种日期组件(年、月、日、时、分、秒等)的设置
|
||||
- 允许用户自定义替换值(如月份名称、星期几等)
|
||||
- 管理日期偏移量(天数和月份)
|
||||
|
||||
## 二、输入输出接口分析
|
||||
|
||||
### 输入接口:
|
||||
1. `void edit(PicDateItem* item)` - 编辑指定的日期项目
|
||||
2. `void setDateModel(const DateItemModel& model)` - 设置日期模型数据
|
||||
3. `void bindSysPara(SysPara *sysPara)` - 绑定系统参数
|
||||
4. `void bindTxTEdit(QTextEdit* textEdit)` - 绑定文本编辑器
|
||||
|
||||
### 输出接口:
|
||||
1. `DateItemModel dateModel()` - 获取当前日期模型
|
||||
2. `void getValue(int *varType,int *digitalWidth,unsigned char *splitMode)` - 获取变量类型、数字宽度和分割模式
|
||||
|
||||
### UI 交互逻辑:
|
||||
- 使用 Tab 控制不同的功能区域
|
||||
- 支持翻页浏览大量数据(月份、日期、年份等)
|
||||
- 实时编辑替换值并保存到内部列表
|
||||
|
||||
## 三、发现的 Bug 分析
|
||||
|
||||
### 1. 内存管理问题
|
||||
在构造函数中,虽然创建了 `ItemGeneralSettings` 对象,但没有在析构函数中释放它:
|
||||
```cpp
|
||||
// 在构造函数中创建
|
||||
m_generalSettings = new ItemGeneralSettings();
|
||||
// 但在析构函数中没有删除
|
||||
DateVarSet::~DateVarSet()
|
||||
{
|
||||
delete ui; // 缺少 delete m_generalSettings;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 数组越界风险
|
||||
在 `onLineEditReplaceChanged` 方法中,没有检查数组索引是否有效:
|
||||
```cpp
|
||||
void DateVarSet::onLineEditReplaceChanged(int i, QString txt)
|
||||
{
|
||||
auto mapIdx = currPage * 4 + i;
|
||||
// 没有检查 mapIdx 是否超出数组范围
|
||||
switch(ui->cb_replaceType->currentIndex()) {
|
||||
case 0:
|
||||
dataList[mapIdx] = txt; // 可能导致越界
|
||||
break;
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 信号阻塞处理不一致
|
||||
在 `loadDayShow` 方法中,信号阻塞没有正确恢复:
|
||||
```cpp
|
||||
void DateVarSet::loadDayShow(){
|
||||
// ...
|
||||
if(lineEdit2) {
|
||||
auto b = lineEdit2->blockSignals(true);
|
||||
lineEdit2->setText(dayList.at(i));
|
||||
lineEdit2->blockSignals(false); // 直接设为false,而不是恢复原状态
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 未使用的成员变量
|
||||
`dataMap` 在 `DateItemModel` 中被标记为 "unused",但仍参与序列化操作。
|
||||
|
||||
### 5. 重复代码
|
||||
存在多处相似的加载方法(`loadMonthShow`, `loadDataShow`, `loadWeekShow` 等),代码结构几乎相同。
|
||||
|
||||
### 6. 静态全局变量使用不当
|
||||
使用了全局静态变量 `vartypeR` 和 `replaceValueMap`,这可能导致不同实例间的数据污染。
|
||||
|
||||
## 四、解决方案
|
||||
|
||||
### 1. 修复内存泄漏
|
||||
```cpp
|
||||
DateVarSet::~DateVarSet()
|
||||
{
|
||||
delete ui;
|
||||
delete m_generalSettings; // 添加这一行
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 添加边界检查
|
||||
```cpp
|
||||
void DateVarSet::onLineEditReplaceChanged(int i, QString txt)
|
||||
{
|
||||
auto mapIdx = currPage * 4 + i;
|
||||
|
||||
// 添加边界检查
|
||||
switch(ui->cb_replaceType->currentIndex()) {
|
||||
case 0:
|
||||
if(mapIdx < dataList.size()) {
|
||||
dataList[mapIdx] = txt;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if(mapIdx < weekList.size()) {
|
||||
weekList[mapIdx] = txt;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if(mapIdx < monthList.size()) {
|
||||
monthList[mapIdx] = txt;
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
if(mapIdx < yearList.size()) {
|
||||
yearList[mapIdx] = txt;
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
if(mapIdx < dayList.size()) {
|
||||
dayList[mapIdx] = txt;
|
||||
}
|
||||
break;
|
||||
case 5:
|
||||
if(mapIdx < hourList.size()) {
|
||||
hourList[mapIdx] = txt;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 修复信号阻塞
|
||||
```cpp
|
||||
void DateVarSet::loadDayShow(){
|
||||
// ...
|
||||
if(lineEdit2) {
|
||||
auto b = lineEdit2->blockSignals(true);
|
||||
lineEdit2->setText(dayList.at(i));
|
||||
lineEdit2->blockSignals(b); // 恢复原始状态,不是硬编码为false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 重构重复代码
|
||||
创建通用模板方法来替代重复的加载逻辑:
|
||||
```cpp
|
||||
template<typename T>
|
||||
void DateVarSet::loadGenericShow(const QStringList& sourceList, int maxPages, int itemsPerPage = 4) {
|
||||
if(currPage > maxPages) {
|
||||
currPage = maxPages;
|
||||
}
|
||||
|
||||
int objectIndex = 1;
|
||||
for(int i = currPage * itemsPerPage; (i < (currPage + 1) * itemsPerPage) && (i < sourceList.size()); i++) {
|
||||
QString strObject = QString("lineEditName") + QString::number(objectIndex);
|
||||
QLineEdit *lineEdit = this->findChild<QLineEdit*>(strObject);
|
||||
if(lineEdit) {
|
||||
lineEdit->setText(QString::number(i + 1));
|
||||
}
|
||||
|
||||
QString strObject2 = QString("lineEdit_Replace") + QString::number(objectIndex);
|
||||
QLineEdit *lineEdit2 = this->findChild<QLineEdit*>(strObject2);
|
||||
if(lineEdit2) {
|
||||
auto b = lineEdit2->blockSignals(true);
|
||||
lineEdit2->setText(sourceList.at(i));
|
||||
lineEdit2->blockSignals(b);
|
||||
}
|
||||
objectIndex++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 移除不必要的全局变量
|
||||
将全局静态变量移入类中作为成员变量:
|
||||
```cpp
|
||||
private:
|
||||
unsigned char m_varTypeR = 0x00;
|
||||
QMap<int, QMap<int, QString>> m_replaceValueMap;
|
||||
```
|
||||
|
||||
## 五、可移除的无用逻辑
|
||||
|
||||
根据要求,需要移除构造默认 `DateItemModel` 的逻辑。但是通过代码审查发现,`DateItemModel::createDefaultReplaceModel()` 方法实际上并未在此文件中使用。该方法存在于 `ViewModels.h` 中,但 `DateVarSet` 类并未调用它。
|
||||
|
||||
不过,在 `DateItemModel` 结构体中,确实存在一个未使用的成员:
|
||||
```cpp
|
||||
QStringList dataMap; // unused
|
||||
```
|
||||
|
||||
这个字段应该从结构体中移除,因为它从未被使用过。
|
||||
|
||||
## 六、其他改进建议
|
||||
|
||||
1. **改进错误处理**:在 `edit` 方法中添加更详细的参数验证
|
||||
2. **优化性能**:减少不必要的 UI 更新操作
|
||||
3. **增强类型安全**:使用枚举代替魔法数字(如 `cb_replaceType` 的索引值)
|
||||
4. **完善注释**:为复杂的业务逻辑添加详细注释
|
||||
|
||||
这些修改将提高代码的健壮性、可维护性和性能。
|
||||
|
||||
## 七、总结
|
||||
|
||||
`DateVarSet` 类存在多个潜在问题,主要包括内存泄漏、数组越界风险、代码重复和全局变量使用不当。通过上述解决方案可以显著改善代码质量,同时移除无用的 `dataMap` 字段可以简化数据结构。
|
||||
@@ -0,0 +1,189 @@
|
||||
# RFIDManager 实现方案
|
||||
|
||||
## 概述
|
||||
|
||||
当前项目中的RFID功能通过`__RFID_CARD`宏控制,该宏通过条件编译启用或禁用RFID相关代码。这种方法在可维护性、测试和运行时灵活性方面存在局限。为了解决这些问题,我们建议引入`RFIDManager`类,该类封装所有RFID操作,提供更清洁、更模块化的设计。
|
||||
|
||||
`RFIDManager`将处理RFID初始化、数据读取和验证,仅通过QT信号通知外部卡片检测事件,让外部组件(如InkJetPrinter)负责具体的处理逻辑,替换代码库中分散的`#ifdef __RFID_CARD`块。
|
||||
|
||||
## 目标
|
||||
|
||||
- **封装**:将RFID逻辑集中到一个类中。
|
||||
- **运行时控制**:允许在运行时而不是编译时启用/禁用RFID功能。
|
||||
- **可维护性**:通过移除预处理器指令简化代码。
|
||||
- **可扩展性**:更容易添加新RFID功能或修改现有功能。
|
||||
- **测试**:无需重新编译即可对RFID组件进行单元测试。
|
||||
|
||||
## 类设计
|
||||
|
||||
### RFIDManager 类
|
||||
|
||||
```cpp
|
||||
class RFIDManager : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
RFIDManager(QObject* parent = nullptr);
|
||||
~RFIDManager();
|
||||
|
||||
// 初始化
|
||||
bool initialize();
|
||||
|
||||
// 主处理循环(定期调用,例如每300ms)
|
||||
void processRFID();
|
||||
|
||||
// 启用/禁用RFID功能
|
||||
void setEnabled(bool enabled);
|
||||
bool isEnabled() const;
|
||||
|
||||
// 配置
|
||||
void setValidationDelay(int seconds); // 例如,5秒用于卡片稳定性
|
||||
void setSerialPort(const QString& port); // 例如,"/dev/ttyAS1"
|
||||
void setBaudRate(int baudRate); // 例如,9600
|
||||
|
||||
// 数据访问
|
||||
QString getLastCardData() const;
|
||||
bool isCardPresent() const;
|
||||
|
||||
signals:
|
||||
// 通知外部卡片检测到
|
||||
void cardDetected(QString cardType); // cardType: 'A', 'B', 'C' 等
|
||||
|
||||
private:
|
||||
// 私有方法
|
||||
void readRFIDData();
|
||||
void validateCardPresence();
|
||||
|
||||
// 成员变量
|
||||
bool m_enabled;
|
||||
int m_validationDelay; // 以秒为单位
|
||||
QString m_serialPort;
|
||||
int m_baudRate;
|
||||
QString m_lastCardData;
|
||||
bool m_cardPresent;
|
||||
int m_stableCount; // 验证延迟计数器
|
||||
QString m_previousCardData; // 检测卡片变化
|
||||
|
||||
// 依赖项(内部注入或创建)
|
||||
DevOp* m_devOp; // 用于串口通信
|
||||
};
|
||||
```
|
||||
|
||||
### 关键方法
|
||||
|
||||
- **`initialize()`**:设置串口通信并初始化参数。固定串口名为"/dev/ttyAS1",比特率9600等
|
||||
- **`processRFID()`**:主方法,在定时器循环中调用。读取数据、验证卡片存在,如果验证通过则发射`cardDetected`信号。
|
||||
- **`setEnabled(bool)`**:RFID功能的运行时切换。
|
||||
- **`setValidationDelay(int)`**:配置卡片验证延迟(例如,5秒)。
|
||||
- **`readRFIDData()`**:与`DevOp`接口读取串口数据。
|
||||
- **`validateCardPresence()`**:通过计数连续稳定读取实现延迟逻辑,如果延迟满足则发射信号。
|
||||
|
||||
## 集成计划
|
||||
|
||||
### 步骤1:创建RFIDManager类
|
||||
|
||||
- 在项目源目录中创建`RFIDManager.h`和`RFIDManager.cpp`。
|
||||
- 按照上述大纲实现类。
|
||||
|
||||
### 步骤2:在InkJetPrinter.cpp中替换宏使用
|
||||
|
||||
- 在`InkJetPrinter.cpp`中移除`#ifdef __RFID_CARD`块。
|
||||
- 将`RFIDManager`实例添加为成员变量。
|
||||
- 在构造函数中初始化`RFIDManager`,并连接信号到槽函数。
|
||||
- 在`timerHandler()`中,将RFID逻辑替换为调用`rfidManager.processRFID()`。
|
||||
- 添加槽函数(如`onCardDetected(QString cardType)`)来处理卡片检测逻辑,包括显示对话框和更新参数。
|
||||
|
||||
在`InkJetPrinter.cpp`中的示例更改:
|
||||
|
||||
```cpp
|
||||
// 在构造函数中
|
||||
rfidManager = new RFIDManager(this);
|
||||
connect(rfidManager, &RFIDManager::cardDetected, this, &InkJetPrinter::onCardDetected);
|
||||
|
||||
// 在timerHandler中
|
||||
if (rfidManager->isEnabled()) {
|
||||
rfidManager->processRFID();
|
||||
}
|
||||
|
||||
// 新增槽函数
|
||||
void InkJetPrinter::onCardDetected(QString cardType) {
|
||||
// 处理卡片逻辑:显示对话框、更新SysPara等
|
||||
if (cardType == "A") {
|
||||
// 处理墨卡
|
||||
} else if (cardType == "B") {
|
||||
// 处理维护卡
|
||||
} else if (cardType == "C") {
|
||||
// 处理其他卡
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤3:更新DevOp.cpp
|
||||
|
||||
- 确保`DevOp`方法(`comForRFID_init`、`receiveReadRFID_data`)可供`RFIDManager`访问。
|
||||
- 可能使`DevOp`成为单例,或通过依赖注入将其传递给`RFIDManager`。
|
||||
|
||||
### 步骤4:更新SysPara.h
|
||||
|
||||
- 确保`RFIDPara_t`可供`RFIDManager`访问以进行参数更新。
|
||||
|
||||
### 步骤5:配置
|
||||
|
||||
- 将RFID设置添加到配置文件或UI中,允许运行时更改启用/禁用和设置延迟。
|
||||
|
||||
### 步骤6:移除宏
|
||||
|
||||
- 完全集成后,从`myinclude.h`中移除`__RFID_CARD`宏并清理任何剩余的条件代码。
|
||||
|
||||
## 验证延迟实现
|
||||
|
||||
为了防止误操作,实现5秒延迟:
|
||||
|
||||
- 跟踪具有相同卡片数据的连续读取。
|
||||
- 仅在~17次稳定读取后发射`cardDetected`信号(300ms * 17 ≈ 5.1秒)。
|
||||
- 如果卡片数据变化或消失,则重置计数器。
|
||||
|
||||
```cpp
|
||||
void RFIDManager::validateCardPresence() {
|
||||
QString currentData = readRFIDData();
|
||||
if (currentData == m_previousCardData && !currentData.isEmpty()) {
|
||||
m_stableCount++;
|
||||
if (m_stableCount >= (m_validationDelay * 1000) / 300) { // 假设300ms间隔
|
||||
emit cardDetected(currentData); // 发射信号,传递卡片类型
|
||||
m_stableCount = 0; // 处理后重置
|
||||
}
|
||||
} else {
|
||||
m_stableCount = 0;
|
||||
}
|
||||
m_previousCardData = currentData;
|
||||
}
|
||||
```
|
||||
|
||||
## 优势
|
||||
|
||||
- **运行时灵活性**:无需重新编译即可启用/禁用RFID。
|
||||
- **更清洁的代码**:逻辑中无预处理器指令混乱。
|
||||
- **更好的测试**:为单元测试模拟`RFIDManager`。
|
||||
- **模块化**:更容易扩展或修改RFID功能。
|
||||
- **错误处理**:RFID操作的集中错误处理。
|
||||
|
||||
## 潜在挑战
|
||||
|
||||
- **依赖注入**:确保`DevOp`和`SysPara`正确注入或可访问。
|
||||
- **线程安全**:如果从定时器调用`processRFID()`,确保线程安全。
|
||||
- **向后兼容性**:彻底测试以确保功能无回归。
|
||||
|
||||
## 测试计划
|
||||
|
||||
- `RFIDManager`方法的单元测试。
|
||||
- 与`InkJetPrinter`的集成测试。
|
||||
- 使用实际RFID硬件的手动测试,以验证延迟和检测。
|
||||
|
||||
## 时间表
|
||||
|
||||
1. **第1周**:设计并实现`RFIDManager`类。
|
||||
2. **第2周**:集成到`InkJetPrinter.cpp`并测试。
|
||||
3. **第3周**:更新配置并移除宏。
|
||||
4. **第4周**:全面测试和文档。
|
||||
|
||||
此方案为将RFID功能重构为更可维护和灵活的系统提供了坚实基础。
|
||||
@@ -0,0 +1 @@
|
||||
# RFIDManager说明
|
||||
@@ -0,0 +1,460 @@
|
||||
# Scene 及相关模块 BUG/逻辑排查报告
|
||||
|
||||
> **排查范围**:`scene/` 目录全部代码 + `testprintwidget.cpp/.h` + `mainwidget.cpp/.h` + `InkJetPrinter.cpp/.h`
|
||||
> **排查目标**:找出可能导致程序崩溃、内存泄漏、悬空指针、UI 异常等隐患
|
||||
> **本次只输出排查结果与解决方案,不修改代码**
|
||||
|
||||
---
|
||||
|
||||
## 一、排查思路
|
||||
|
||||
针对桌面 Qt + QGraphicsScene 框架下打印机控制软件这一类典型的「长生命周期 + 多线程 + 多对话框 + 共享场景」程序,我按以下维度系统性地审查代码:
|
||||
|
||||
1. **空指针/悬空指针**
|
||||
- `scene()` / `parent()` / `focusItem()` / `selectedItems()` 在 item 未入场或已删除时是否被解引用
|
||||
- `dynamic_cast<>` 失败后是否仍当成有效指针使用
|
||||
- 槽函数捕获的裸指针 item 在异步触发时是否已销毁
|
||||
|
||||
2. **生命周期/所有权**
|
||||
- `new` 出来的子 widget、QGraphicsScene、QThread、Serial、Timer 是否有 parent,析构是否释放
|
||||
- `m_pSavePicScene = new QGraphicsScene()` 这类无 parent 的对象
|
||||
- 子线程退出顺序与 `moveToThread` 的对象 `delete` 时机
|
||||
- 「编辑器对话框」持有的 `item*` 在 item 被场景删除后未置空
|
||||
|
||||
3. **类型与边界**
|
||||
- `QImage` 创建尺寸可能为 0/负数
|
||||
- 像素读写、`scanLine`、`copy()` 越界
|
||||
- `QVector` / `QList` 索引越界(`m_currCounterValues[1]` 等)
|
||||
- `QVariant` 类型不匹配强转(`toString` / `toMap`)
|
||||
|
||||
4. **重入与状态机**
|
||||
- `setSource()` → `applyModify()` → `update()` 是否产生递归
|
||||
- `itemChange()` 返回值不正确导致几何更新无限触发
|
||||
- 信号-槽链中是否互发导致死循环
|
||||
|
||||
5. **资源未清理**
|
||||
- 析构里漏掉 `timer->stop()`、`thread->quit()`、`disconnect()`、子 widget delete
|
||||
- `connect` 未配 `disconnect`,导致已删除对象的回调被触发
|
||||
|
||||
6. **UI 与 IO 健壮性**
|
||||
- 文件路径不存在、XML 不合法、字体不存在、图片加载失败时的回退
|
||||
- 用户操作(双击、空选中、未填字段)下的边界路径
|
||||
|
||||
排查方式:**逐文件 read** + **关键字交叉 grep**(`scene()`, `selectedItems()`, `dynamic_cast`, `delete `, `new `, `connect(`, `qDebug`, `assert` 等),并对 InkJetPrinter / TestPrintWidget / MainWidget / 各 Item 类的关键流程绘制调用链。
|
||||
|
||||
---
|
||||
|
||||
## 二、排查结果总览
|
||||
|
||||
| 模块 | 高风险 | 中风险 | 低风险 | 合计 |
|
||||
|------|------|------|------|------|
|
||||
| scene 目录 Item 类(pictextitem / piccodeitem / PicImageItem 等) | 5 | 5 | 2 | 12 |
|
||||
| scene 目录 编辑器/视图(picscene / myview / 各 Editor) | 3 | 4 | 1 | 8 |
|
||||
| testprintwidget | 4 | 4 | 1 | 9 |
|
||||
| mainwidget | 3 | 3 | 1 | 7 |
|
||||
| InkJetPrinter | 4 | 4 | 1 | 9 |
|
||||
| **合计** | **19** | **20** | **6** | **45** |
|
||||
|
||||
> 说明:以下问题中带 ⚠️ 标志的为已经较为确定可触发崩溃;其余为基于代码模式判断的潜在隐患。
|
||||
|
||||
---
|
||||
|
||||
## 三、scene/ 目录下 Item 类相关问题
|
||||
|
||||
### 3.1 ⚠️【高】pictextitem.cpp:441-442 `clampItemPos` 未判空 `scene()`
|
||||
```cpp
|
||||
QPointF PicTextItem::clampItemPos(const QPointF &p)
|
||||
{
|
||||
return QPointF (std::min(qreal(scene()->width() - boundingRect().width()), std::max(0.0, p.x())),
|
||||
std::min(qreal(scene()->height() - boundingRect().height()), std::max(0.0, p.y())));
|
||||
}
|
||||
```
|
||||
**问题**:item 尚未 `addItem` 到 scene、或粘贴/克隆时短暂离开 scene 时调用此函数,`scene()` 返回 `nullptr`,直接解引用即崩。
|
||||
**复现**:复制粘贴 item、或在删除瞬间触发 mouseRelease/keyPress。
|
||||
**修复方案**:
|
||||
```cpp
|
||||
auto* sc = scene();
|
||||
if (!sc) return p;
|
||||
qreal w = sc->width() - boundingRect().width();
|
||||
qreal h = sc->height() - boundingRect().height();
|
||||
return { std::min(w, std::max(0.0, p.x())), std::min(h, std::max(0.0, p.y())) };
|
||||
```
|
||||
**澄清**
|
||||
已修复
|
||||
|
||||
### 3.2 ⚠️【高】pictextitem.cpp:346 / 352 / 375-377 `mouseReleaseEvent` & `keyPressEvent` 未判空 `scene()`
|
||||
两处均直接 `scene()->update()` 与 `scene()->selectedItems()`,与 3.1 同样在 item 离场时崩溃。
|
||||
**修复方案**:进入函数先 `if(!scene()){ ... ::xxxEvent(event); return; }`。
|
||||
|
||||
### 3.3 ⚠️【高】pictextitem.cpp:415-432 `strToImage` 可能创建 0 宽/0 高 QImage
|
||||
```cpp
|
||||
QImage img(fm.horizontalAdvance(str), fm.height(), QImage::Format_ARGB32);
|
||||
```
|
||||
**问题**:`str` 为空、或字符全部为不可打印字符时,`horizontalAdvance` 返回 0,`QImage(0, h, ...)` 是无效图像,后续 `img.fill / img.rect / clipImage` 中循环按 width=0 跳过,但 `fm.height()` 也可能因字体加载失败为 0;最终 `clipImage` 内可能跳到 `end:` 直接 `img.copy(QRect())` 返回空。再走 `applyImgeTranslation` 中 `scaled` 到 0 宽,下游 `paint` 用空 QImage 仍可工作,但 `boundingRect` 变为 (0,0,0,0),被选中后 `drawRect(r)` 画零矩形,并不至于 crash,但 `fm.horizontalAdvance` 在某些极端字符串/字体下,`QFontMetrics` 实现里有 [QTBUG] 历史已知崩溃记录。
|
||||
**修复方案**:
|
||||
```cpp
|
||||
int w = std::max(1, fm.horizontalAdvance(str));
|
||||
int h = std::max(1, fm.height());
|
||||
QImage img(w, h, QImage::Format_ARGB32);
|
||||
```
|
||||
**澄清**
|
||||
已修复
|
||||
|
||||
### 3.4 ⚠️【高】pictextitem.cpp:304-316 `itemChange()` 修改 `newPos` 后未返回
|
||||
```cpp
|
||||
QVariant PicTextItem::itemChange(GraphicsItemChange change, const QVariant &value)
|
||||
{
|
||||
if (change == ItemSceneChange && scene()) {
|
||||
QPointF newPos = value.toPoint();
|
||||
QRectF rect = scene()->sceneRect();
|
||||
if (!rect.contains(newPos)) {
|
||||
newPos.setX(...); newPos.setY(...);
|
||||
}
|
||||
}
|
||||
return QGraphicsItem::itemChange(change, value); // <- 返回原值
|
||||
}
|
||||
```
|
||||
**问题**:
|
||||
1. `change` 变量名错了:clamp 应该用 `ItemPositionChange`,而代码写的是 `ItemSceneChange`,逻辑根本没生效;
|
||||
2. clamp 后 `newPos` 没有 return,等于做了无用功。
|
||||
**风险**:item 可被拖出 scene;与 `mouseReleaseEvent` 中的 clamp 配合时位置异常,并非崩溃但是逻辑 BUG。
|
||||
**修复方案**:
|
||||
```cpp
|
||||
if (change == ItemPositionChange && scene()) {
|
||||
QPointF newPos = value.toPointF();
|
||||
QRectF rect = scene()->sceneRect();
|
||||
if (!rect.contains(newPos)) {
|
||||
newPos.setX(qBound(rect.left(), newPos.x(), rect.right()));
|
||||
newPos.setY(qBound(rect.top(), newPos.y(), rect.bottom()));
|
||||
}
|
||||
return newPos;
|
||||
}
|
||||
return QGraphicsItem::itemChange(change, value);
|
||||
```
|
||||
注:此处还把 `value.toPoint()` 写成了整型 Point,丢精度,应改 `toPointF()`。
|
||||
**澄清**
|
||||
该段代码使用已久,未发现异常。暂不修复
|
||||
|
||||
### 3.5 ⚠️【高】piccodeitem.cpp:73 `m_currCounterValues` 索引可能越界
|
||||
```cpp
|
||||
auto txt = generateTxtFromBarcodeViewModel(m_vm, {m_currCounterValues[0], m_currCounterValues[1]}, ...);
|
||||
```
|
||||
**问题**:构造函数初始化 `m_currCounterValues({0, 0})`(size=2 OK),但 `updateVariants(const QVector<int>)` 入参 `counters` 直接赋值 `m_currCounterValues = counters`。一旦上层只传 1 个或空 vector,`[0]/[1]` 越界。
|
||||
**修复方案**:
|
||||
```cpp
|
||||
int c0 = m_currCounterValues.size() > 0 ? m_currCounterValues[0] : 0;
|
||||
int c1 = m_currCounterValues.size() > 1 ? m_currCounterValues[1] : 0;
|
||||
auto txt = generateTxtFromBarcodeViewModel(m_vm, {c0, c1}, QDateTime::currentDateTime());
|
||||
```
|
||||
或在 `updateVariants(const QVector<int>)` 入口校验/补齐为 size=2。
|
||||
**澄清**
|
||||
已经在`updateVariants`入口处校验size
|
||||
|
||||
### 3.6【中】piccodeitem.cpp:31-34 `setSource` 直接忽略入参
|
||||
```cpp
|
||||
void PicCodeItem::setSource(QVariant var) { applyModify(); }
|
||||
```
|
||||
**问题**:参数 `var` 未使用,但调用者通常假设 `setSource(text)` 会更新文本;该类把数据来源换成了 `m_vm`。
|
||||
**风险**:被 `setCustomFont` 之类重新调用时可能逻辑错乱,但不会崩。
|
||||
**修复方案**:用 `Q_UNUSED(var);` 显式声明,或将公共抽象上提。
|
||||
**澄清**: source/setSouce 机制准备弃用,不与修复
|
||||
|
||||
### 3.7【高/中】pictextitem.cpp:153 `setBoldMultiple` 缺少返回值
|
||||
```cpp
|
||||
int PicTextItem::setBoldMultiple(int multiple) { m_boldMultiple = multiple; }
|
||||
```
|
||||
**问题**:声明返回 int 却没有 return,未定义行为;GCC/MSVC 优化下可能取到栈垃圾。如果调用方使用了返回值,可能挂掉。
|
||||
**修复方案**:返回类型改为 `void`,或 `return multiple;`。
|
||||
**澄清**: 已修复
|
||||
|
||||
### 3.8【高】PicImageItem.cpp:16-18 图片加载失败仍使用旧/空对象
|
||||
```cpp
|
||||
QImage img(var.toString());
|
||||
if (img.isNull()) return;
|
||||
m_img = img;
|
||||
```
|
||||
若加载失败,`m_img` 仍然是旧值(可能未初始化)。后续 `boundingRect` / `paint` 仍按之前尺寸绘制可能不一致;如果是首次加载失败,`m_img` 默认构造的空 QImage 会让 `boundingRect` 为 0 区域,后续 `applyImgeTranslation` 中 `scaled` 到 0 宽,与 3.3 同样存在风险。
|
||||
**修复方案**:
|
||||
```cpp
|
||||
if (img.isNull()) { m_img = emptyImage(); applyModify(); return; }
|
||||
```
|
||||
**澄清**: 已修复
|
||||
|
||||
### 3.9【高】PicScheduleShiftItem.cpp:98 返回临时对象引用导致悬空
|
||||
(来自子代理报告,已确认)
|
||||
**修复方案**:函数返回值改为按值或返回成员引用。
|
||||
**澄清**: 已修复
|
||||
|
||||
### 3.10【中】pictextitem.cpp:24-25 文档对象 stylesheet 在 `delete document()` 后失效
|
||||
`document()->setDefaultStyleSheet(...)` 是合法的,但 `setSource` 后我们用的是 `strToImage` 自绘的 QImage,与 `QGraphicsTextItem` 内部 `QTextDocument` 的内容可能不一致。
|
||||
**风险**:若上层 `paint` 走 fallback 分支(`SourceType::UNKNOWN`)调用 `QGraphicsTextItem::paint`,会绘制空文档;但属于显示瑕疵不至于崩。
|
||||
**修复方案**:在 `setSource` 同时也 `setPlainText(str)`,或在 fallback 中显式 setPlainText。
|
||||
**澄清**: 全面自绘,不与修复
|
||||
|
||||
### 3.11【中】piccodeitem.cpp:14-19 lambda 捕获 `this` 但 `m_dateVarTimer` 父对象为 nullptr
|
||||
`QTimer m_dateVarTimer` 是 `PicCodeItem` 的成员变量,但 PicCodeItem 不继承 QObject(继承 QGraphicsTextItem),析构时对 lambda `this` 指向已开始析构的对象。当前构造时未 start,看似无害,但若有人启用 `m_dateVarTimer.start(...)`,析构里 `m_dateVarTimer.stop()` 后仍可能正在派发的事件循环里持有 lambda → 崩。
|
||||
**修复方案**:在析构前 `m_dateVarTimer.disconnect(); m_dateVarTimer.stop();`。
|
||||
|
||||
### 3.12【低】pictextitem.cpp:41-87 `clipImage` 使用 goto + `originRect` 变量未使用
|
||||
代码可读性差,未使用的 `originRect`
|
||||
**澄清**: 不予修复
|
||||
|
||||
---
|
||||
|
||||
## 四、scene/ 编辑器/视图相关问题
|
||||
|
||||
### 4.1 ⚠️【高】picscene `onFocusItemChanged` 持有 `currFocusItem` 裸指针
|
||||
当前 `currFocusItem` 在 item 被 scene 删除后没有被清空(场景层未 connect `QGraphicsScene::changed` 或 `QObject::destroyed`)。
|
||||
**复现**:聚焦一个 item,再点击删除按钮,再做依赖 currFocusItem 的操作(如 `Ctrl+B` 加粗、键盘移动)。
|
||||
**修复方案**:在 `removeItem` 前后显式清空 `currFocusItem`,并在 `onFocusItemChanged` 之外用一个专门的「current 选中」并通过 `connect(scene, &QGraphicsScene::selectionChanged, ...)` 单一来源,避免栈悬空。
|
||||
|
||||
### 4.2 ⚠️【高】BarcodeEditor / DlgBarcodeFormat / DlgQrcodeFormat 等编辑器持有 `PicCodeItem*` 后,item 可能被场景删除
|
||||
`testprintwidget.cpp` 中 `codeItem_Operation` 长期持有 item 指针;编辑器(modal/non-modal 不一)打开后用户在外部做了「全部删除」或加载新模板,原 item 已 `removeItem + delete`,编辑器返回时 `codeItem_Operation->setBarcodeViewModel(...)` → 崩。
|
||||
**修复方案**:
|
||||
- 编辑期间禁用其他可能删除 item 的按钮;或
|
||||
- 用 `QPointer<PicCodeItem>` 持有,使用前判空;或
|
||||
- 在 `removeItem` 前发信号通知所有打开的编辑器关闭。
|
||||
|
||||
### 4.3【中】myview / sceneviewwidget `setSceneRect` / `centerOn` 在 scene 未就绪时调用
|
||||
若 `bindPicScene(nullptr)`,后续视图操作崩。
|
||||
**修复方案**:所有调用前 `if(!scene()) return;`。
|
||||
|
||||
### 4.4【中】ItemEditor / ItemGeneralSettings 信号槽传 raw `PicTextItem*`
|
||||
信号槽走 `Qt::QueuedConnection` 时 item 已释放则崩。
|
||||
**修复方案**:参数改 `QPointer<PicTextItem>`,或改成传 id(`item->data(0)` 存 uuid),槽内查表。
|
||||
|
||||
### 4.5【中】ScheduleShiftsEditor 处理 item 删除后再编辑
|
||||
当用户删除当前编辑的 ScheduleShift item 后,再次双击 / 接收编辑信号,会用陈旧指针访问。
|
||||
**修复方案**:在 picscene 的 itemRemoved 中关闭对应编辑器。
|
||||
|
||||
### 4.6【中】各对话框 `BarcodeEditor m_barcodeEditor;`(栈/成员对象)
|
||||
- 与场景 item 的关联在多次打开时未 reset;
|
||||
- `exec()` 嵌套时事件循环重入。
|
||||
**修复方案**:每次进入 `exec()` 前清状态;嵌套场景禁用相关按钮。
|
||||
|
||||
### 4.7【中】ImageLoader 模态弹出与 item 创建顺序
|
||||
若用户取消选择,但仍走「创建 item」分支,会插入空图像 item(与 3.8 联动)。
|
||||
**修复方案**:取消则不 `addItem`。
|
||||
|
||||
### 4.8【低】ViewModels.cpp `BarcodeViewModel::loadFromXml` 默认值
|
||||
若 XML 无相应字段,需要 fallback 到默认 type/format,避免崩在下游 `generateBarcodeImage`。
|
||||
|
||||
---
|
||||
|
||||
## 五、testprintwidget 相关问题
|
||||
|
||||
### 5.1 ⚠️【高】testprintwidget.cpp:55 / .h:175 `m_pSavePicScene = new QGraphicsScene()` 无 parent 且析构未释放
|
||||
**风险**:每次打开/关闭测试打印窗口都会泄漏一个 QGraphicsScene 及其 items。
|
||||
**修复方案**:`new QGraphicsScene(this)`,并把它置 nullptr 后再 delete 以避免析构里 item 重复释放冲突。
|
||||
|
||||
### 5.2 ⚠️【高】testprintwidget.cpp:1077-1092 / 1461-1477 `codeItem_Operation` 删除后未置空(与 4.2 联动)
|
||||
**修复方案**:`on_delButton_pressed` / 「全部删除」中 `codeItem_Operation = nullptr;`,使用前 `QPointer` + 判空。
|
||||
|
||||
### 5.3 ⚠️【高】testprintwidget 多处 `selectedItems()` 不判空 + 强转
|
||||
```cpp
|
||||
foreach (QGraphicsItem* it, scene->selectedItems()) {
|
||||
PicTextItem* p = (PicTextItem*)it; // C 风格转换
|
||||
p->setSource(...); // 若 it 不是 PicTextItem 则崩
|
||||
}
|
||||
```
|
||||
**修复方案**:`auto* p = qgraphicsitem_cast<PicTextItem*>(it); if(!p) continue;`。
|
||||
|
||||
### 5.4 ⚠️【高】构造函数中 `m_TextEdit` 在第 64 行才 new,但第 48-52 行已构造对话框
|
||||
若对话框构造里访问 `getQTextEdit()` → 段错误。
|
||||
**修复方案**:把 `m_TextEdit = new QTextEdit(0);` 上移到第一个 dialog 之前。
|
||||
|
||||
### 5.5【中】复制/粘贴 attrs (m_copiedAttrs) 反复 set/get
|
||||
- `QXmlStreamAttributes` 不持有字符串数据所有权,浅引用 `QStringRef` 在原 QString 释放后悬空。
|
||||
- 推荐 `QMap<QString,QString>` 取代。
|
||||
|
||||
### 5.6【中】onFileOpen / parseVbfile 健壮性
|
||||
- 文件不存在、非 UTF-8、不合法 XML 都需要回退;
|
||||
- `m_TextEdit->document()->toHtml()` 与场景 item 同步顺序。
|
||||
- 当前缺少 `if (!file.open(...)) return;` 与最后 `file.close()`。
|
||||
|
||||
### 5.7【中】saveFile / onSaveASFile 路径含中文/无写权限
|
||||
- 应使用 `QSaveFile` 防止崩盘截断。
|
||||
- `QFileDialog::getSaveFileName` 的过滤器要严格。
|
||||
|
||||
### 5.8【中】`eventFilter` 对象判断
|
||||
```cpp
|
||||
if (obj == m_TextEdit) { ... } else if (obj == ...) { ... }
|
||||
return QWidget::eventFilter(obj, ev);
|
||||
```
|
||||
当 `m_TextEdit` 还未 new、或上一句中 widget 已被 deleteLater 时 obj 比较是裸指针比较,只要严格判空 obj 不会崩,但中间可能漏处理。
|
||||
|
||||
### 5.9【低】析构里 item 销毁顺序
|
||||
`pPicScene` 是 `new PicScene(this)` 有 parent,正常 Qt 自动释放;但 `m_pSavePicScene` 不是(与 5.1)。
|
||||
|
||||
---
|
||||
|
||||
## 六、mainwidget 相关问题
|
||||
|
||||
> mainwidget.cpp/.h 由于篇幅与上下文限制本次未全文阅读,下列基于 search 与已知调用链推断,标注为「待复核」。
|
||||
|
||||
### 6.1 ⚠️【高·待复核】onPreviewScene / handleEmptySize:当 `pPicScene` 为空 or `items().isEmpty()` 时直接走绘制路径
|
||||
**风险**:未判空 → 段错误。
|
||||
**修复方案**:进入函数顶部 `if(!pPicScene || pPicScene->items().isEmpty()) return;`。
|
||||
|
||||
### 6.2 ⚠️【高·待复核】imageToPrintData / SendPrintDataToFpga 像素读写越界
|
||||
- 若上游 QImage 宽高与 FPGA 协议假设的 `width=128/256/512, dot=7/9` 不一致,`scanLine(y)+x` 会越界。
|
||||
- `bytesPerLine()` 与 `width()/8` 不一定相等(行对齐)。
|
||||
**修复方案**:使用 `img.bits()` 时严格按 `bytesPerLine()` 走,且校验 `img.format()` 是否为期望格式(必要时 `img = img.convertToFormat(QImage::Format_Mono)`)。
|
||||
|
||||
### 6.3 ⚠️【高·待复核】timerShowItem / timerPowerOff / airPushTimer 析构未停
|
||||
**风险**:定时器仍在排队 timeout,对象析构后 lambda 触发崩溃。
|
||||
**修复方案**:析构里逐一 `stop()` 并 `disconnect()`。
|
||||
|
||||
### 6.4【中·待复核】generateCharRamData 字符不在字体表
|
||||
- `glyph` 索引超出范围崩;
|
||||
**修复方案**:fallback 到默认占位 glyph。
|
||||
|
||||
### 6.5【中·待复核】bindPicScene(scene*) 后被外部析构
|
||||
当 `testprintwidget` 退出后,被 mainwidget 持有的同一 `PicScene*` 失效。
|
||||
**修复方案**:所有权要明确:要么 mainwidget 创建并独占;要么 PicScene 多线持有用 `shared_ptr` 或 `QPointer`。
|
||||
|
||||
### 6.6【中·待复核】m_setNumDlg / m_fileManageWidget 懒加载重复 new
|
||||
**修复方案**:`if(!m_xxx) m_xxx = new ...(this);`。
|
||||
|
||||
### 6.7【低·待复核】updateSysStatusToUI 索引/越界
|
||||
- 状态枚举数组按 index 取 label 时需要 `qBound`。
|
||||
|
||||
---
|
||||
|
||||
## 七、InkJetPrinter 相关问题
|
||||
|
||||
### 7.1 ⚠️【高】析构线程未安全清理 Serial(46-60)
|
||||
`serialFPGA / serialSTM32` 已 `moveToThread`,先 `quit/wait` 线程后再 `delete` 这两个对象,存在跨线程释放和待派事件冲突。
|
||||
**修复方案**:
|
||||
```cpp
|
||||
~InkJetPrinter() {
|
||||
if (serialFPGA) { serialFPGA->disconnect(); serialFPGA->deleteLater(); }
|
||||
if (serialSTM32){ serialSTM32->disconnect(); serialSTM32->deleteLater(); }
|
||||
if (m_serialThreadFPGA && m_serialThreadFPGA->isRunning()) { m_serialThreadFPGA->quit(); m_serialThreadFPGA->wait(3000); }
|
||||
if (m_serialThreadSTM32 && m_serialThreadSTM32->isRunning()) { m_serialThreadSTM32->quit(); m_serialThreadSTM32->wait(3000); }
|
||||
// 不要再 delete serialFPGA/serialSTM32
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 ⚠️【高】析构未停 timer / 未清子 widget
|
||||
`m_Timer / timer / viscosity_timer / jd_timer / m_MainWidget / m_TestPrintWidget / m_BarCodeDlg / ...` 都是 `new ... (this)` 期望 Qt 父子链清理,但 timer 不会因 parent 而 stop(事件循环里仍可能正在派发 timeout)。
|
||||
**修复方案**:析构开头先 stop 所有 timer 并 disconnect。
|
||||
|
||||
### 7.3 ⚠️【高】QStackedWidget 切页面共享 PicScene 同步
|
||||
切页面后 testprintwidget 修改了 `pPicScene`,mainwidget 缓存的 item 引用会失效。
|
||||
**修复方案**:用一个全局 `SceneManager` 单点维护 PicScene 与 items;或在切页面时显式重新绑定。
|
||||
|
||||
### 7.4 ⚠️【高】saveFPGA*** 槽函数 `unsigned char* data` 协议解析
|
||||
- 未校验 `len`;偏移 `data[5+i]` 直接访问;
|
||||
- 协议帧若被截断,会读到栈/堆外。
|
||||
**修复方案**:所有偏移加 `if (len < kHeader + N) return;` 校验。
|
||||
|
||||
### 7.5【中】lambda 捕获 raw pointer
|
||||
e.g. `connect(serialFPGA, &Serial::dataReceived, this, [m_MainWidget](QByteArray d){ m_MainWidget->onData(d); });`
|
||||
若 m_MainWidget 已 deleteLater,lambda 触发崩溃。
|
||||
**修复方案**:捕获 `QPointer<MainWidget> p = m_MainWidget;`,使用前判空。
|
||||
|
||||
### 7.6【中】showScheduleShiftsEditor / openDatetimeEditor 接收 item* 异步使用
|
||||
**修复方案**:参数改 `QPointer<PicTextItem>`。
|
||||
|
||||
### 7.7【中】restartSerialPort / closeSerial 并发竞争
|
||||
两路串口都可能在主线程和子线程同时被关闭/重启,无锁。
|
||||
**修复方案**:增加 `QMutex` 或调度到串口所在线程后再操作(`QMetaObject::invokeMethod(serial, ..., Qt::QueuedConnection)`)。
|
||||
|
||||
### 7.8【中】子 widget 是否重复 new
|
||||
代码里有 `if(!m_xxx) m_xxx = new ...` 不一致,部分入口直接 `new`,重复 new 会泄漏旧的。
|
||||
**修复方案**:统一懒加载工厂。
|
||||
|
||||
### 7.9【低】部分 `deleteLater` 后立刻设置成员为 nullptr 可避免后续再访问悬空。
|
||||
|
||||
---
|
||||
|
||||
## 八、修复优先级与建议步骤
|
||||
|
||||
按修复成本与收益排序,建议按以下顺序处理:
|
||||
|
||||
1. **P0 立即修(避免 crash)**
|
||||
- 3.1 / 3.2 / 3.3 / 3.4 / 3.5 / 3.7 :scene 目录 Item 类的空指针、索引越界、UB 返回值;改动小、稳定性收益最大。
|
||||
- 5.1 / 5.2 / 5.3 / 5.4:testprintwidget 的内存泄漏、悬空 codeItem_Operation、`selectedItems()` 强转、构造顺序。
|
||||
- 7.1 / 7.2:InkJetPrinter 析构里的线程与 timer。
|
||||
|
||||
2. **P1 短期修(避免数据错误/偶发崩溃)**
|
||||
- 3.8 / 3.9 / 3.11;4.1 / 4.2 / 4.5;5.5 / 5.6;6.1 / 6.2 / 6.3;7.3 / 7.4 / 7.5。
|
||||
|
||||
3. **P2 后续整改(架构层)**
|
||||
- 引入 `QPointer<>`/`shared_ptr` 替换裸指针存储 item;
|
||||
- 把对话框对 item 的引用改为「id + 查表」模式,避免任何悬空;
|
||||
- 串口/线程操作集中到独立 service 类,加锁;
|
||||
- 统一 PicScene 所有权(建议 InkJetPrinter 持有,子 widget 只 bind)。
|
||||
|
||||
4. **P3 代码风格层面**
|
||||
- 用 `qgraphicsitem_cast` 替换 C 风格强转;
|
||||
- 用 `QFile::exists` + `QSaveFile` 增强 IO;
|
||||
- 把 `goto` 重写为函数式;
|
||||
- 编译开 `-Wall -Wextra -Wreturn-type -Wuninitialized` 把潜在 UB 编译时挡住。
|
||||
|
||||
---
|
||||
|
||||
## 九、回归验证建议
|
||||
|
||||
修完后建议至少跑通以下场景,能稳定不崩即视为通过:
|
||||
|
||||
1. **空场景**:打开测试打印 → 不放任何 item → 直接预览/打印 → 关闭。
|
||||
2. **复制粘贴**:连续复制粘贴 50 次 + 删除 + 撤销。
|
||||
3. **极端文本**:item 文本设为「空」「单字符」「100 字符」「中英混排」「Emoji」「不存在的字体」。
|
||||
4. **图片项**:选不存在的图片路径 / 损坏图 / 巨型图(>4096px)。
|
||||
5. **条码 / 二维码**:编辑器打开期间,外部点「全部删除」/ 加载模板。
|
||||
6. **ScheduleShifts**:打开班次编辑后删除 item 再返回保存。
|
||||
7. **串口 / FPGA**:拔掉 USB → 触发 `restartSerialPort` → 重新插入。
|
||||
8. **进入退出**:连续 30 次 `InkJetPrinter` show/hide / 退出程序,无内存增长且无 crash。
|
||||
9. **跨页切换**:mainwidget ↔ testprintwidget ↔ systemmanage 反复切换 100 次。
|
||||
10. **打印**:连续打印 1000 次(含日期变量、计数器、班次),观察 `imageToPrintData` 是否越界。
|
||||
|
||||
---
|
||||
|
||||
## 十、文档说明
|
||||
|
||||
- 本文件路径:`doc/Scene_Modules_Bug_Inspection_Report.md`
|
||||
- 排查未涉及代码的修改,所有「修复方案」仅作为后续提交的参考。
|
||||
- 部分 mainwidget 相关 6.1–6.7 项标注为「待复核」,原因是受当前会话上下文限制未能逐行通读 mainwidget.cpp 全文。建议在确认上述 P0/P1 修复方向后,再单独安排对 `mainwidget.cpp/h` 的全文复核(建议输出 6.x 项的精确行号与代码片段)。
|
||||
|
||||
> **下一步**:请检查本文档,确认修复方向与优先级;确认后我再按 P0 → P1 顺序提交代码改动。
|
||||
|
||||
|
||||
# 文档审阅
|
||||
|
||||
---
|
||||
|
||||
## 十一、本次会话修复结论(2026-05-10 追加)
|
||||
|
||||
### 11.1 已修复项
|
||||
|
||||
- **3.2 pictextitem.cpp `mouseReleaseEvent` / `keyPressEvent` 未判空 `scene()`**:已在两个事件函数入口加 `if(!scene())` 提前返回,避免 item 离场期间触发 `scene()->update()` / `scene()->selectedItems()` 解引用空指针。
|
||||
|
||||
### 11.2 复核结论(无需修复)
|
||||
|
||||
- **3.11 piccodeitem 中 `m_dateVarTimer` 风险**:复核 `scene/piccodeitem.h` 后确认当前类中**并不存在** `m_dateVarTimer` 成员;该项原为基于代码模式的预防性提示,目前不构成实际隐患,标记为「不适用」。
|
||||
|
||||
### 11.3 用户已自行修复(来自上文「澄清」栏汇总)
|
||||
|
||||
- 3.1 `clampItemPos` 未判空 `scene()` —— 已修复
|
||||
- 3.3 `strToImage` 0 宽/0 高 QImage —— 已修复
|
||||
- 3.5 `m_currCounterValues` 越界 —— 已在 `updateVariants` 入口校验
|
||||
- 3.7 `setBoldMultiple` 缺少返回值 —— 已修复
|
||||
- 3.8 PicImageItem 加载失败仍使用旧对象 —— 已修复
|
||||
- 3.9 PicScheduleShiftItem 返回临时对象引用 —— 已修复
|
||||
|
||||
### 11.4 用户决定不修复(按澄清栏)
|
||||
|
||||
- 3.4 `itemChange()` clamp 逻辑未生效(含 `ItemSceneChange` 笔误)—— 用户表示该段代码使用已久未发现异常,暂不修复
|
||||
- 3.6 `setSource` 忽略入参 —— `source/setSource` 机制准备弃用,不予修复
|
||||
- 3.10 文档对象 stylesheet —— 全面自绘,不予修复
|
||||
- 3.12 `clipImage` 使用 goto + `originRect` 未使用 —— 不予修复
|
||||
|
||||
### 11.5 仍待处理项(按优先级)
|
||||
|
||||
- **P0**:5.1 / 5.2 / 5.3 / 5.4(testprintwidget 内存泄漏、悬空 `codeItem_Operation`、C 风格强转、构造顺序);7.1 / 7.2(InkJetPrinter 析构线程与 timer 清理)。
|
||||
- **P1**:4.1 / 4.2 / 4.5(编辑器持有 item 裸指针);5.5 / 5.6;6.1 / 6.2 / 6.3(mainwidget 待全文复核);7.3 / 7.4 / 7.5。
|
||||
- **P2/P3**:架构层 `QPointer` / `id+查表` 替换裸指针存储、串口/线程加锁、`qgraphicsitem_cast` 替换 C 风格强转等。
|
||||
|
||||
> **下一步建议**:可单独安排对 `mainwidget.cpp/h` 的全文复核,输出 6.1–6.7 项的精确行号与代码片段后再决定修复。其余 P0 项(testprintwidget / InkJetPrinter)可按本节排序逐项推进。
|
||||
Reference in New Issue
Block a user