数据流更改建议
n5321 | 2025年12月15日 00:18
这是一个非常典型的桌面应用程序架构演进问题。目前的架构中,数据在 数据库 (SQLite)、UI 控件状态 和 DataService (内存) 三者之间流转,确实存在“多头管理”和“同步不及时”的风险。
以下我将深入分析你当前的 数据流现状,并给出基于 DataService 作为唯一可信源 (Single Source of Truth) 的重构建议。
一、 当前数据流现状分析 (Current Data Flow)
目前的流向是混合的,主要痛点在于:数据是谁的?
1. 加载流程 (Loading)
路径: Database -> ProductController -> CustomerRequirementsPage (UI) -> FieldWidget
问题: 数据直接填入了 UI 控件。虽然 BLDCPage 里也顺手更新了一下 DataService,但主流程是依赖 UI 控件存储数据的。
2. 修改流程 (Editing)
路径 A (Design Target Page): 用户打字 -> on_text_changed -> 直接调用 DataService.update...。
路径 B (Input Panel): 用户打字 -> _on_text_changed -> 直接调用 DataService.update_maxwell_parameters。
问题: UI 直接操作 Service,耦合度高。而且如果代码里有 setText 操作,会触发信号,导致不必要的递归更新。
3. 仿真启动流程 (Simulation Start)
路径: InputPanel 点击开始 -> 从 DataService.customer_requirements 和 DataService.maxwell_parameters 拼凑参数 -> 发送给 Controller -> Worker。
问题: maxwell_parameters 和 customer_requirements 之间界限模糊。有些参数(如额定电压)既是客户需求,又是 Maxwell 输入,目前是简单粗暴的 update() 覆盖,容易弄丢数据来源。
4. 结果回填流程 (Result Backfill)
路径: Worker -> MaxwellAutomation -> DataService.update_result_detail (写入) -> Controller -> View.on_simulation_success -> View 读取 DataService 或直接使用传参。
问题: 这是一个比较好的部分,已经通过 Service 中转了,但 Worker 内部隐式实例化 DataService 有点依赖全局单例的副作用。
二、 改进建议:DataService 中枢化重构
目标:UI 只是数据的展示层,Database 是持久化层,DataService 是运行时的唯一核心。
1. 重新定义变量含义
首先理清 DataService 中那几个字典的具体职责:
| 变量名 | 当前定义 | 建议改进定义 |
|---|---|---|
| product_info | 产品基础信息(ID, Model) | 身份信息:当前选中产品的 ID、型号、系列ID。 |
| customer_requirements | 客户需求(电压、扭矩等) | 设计目标(Target):只读或通过"设计目标页"修改。代表“我想要达到的指标”。 |
| maxwell_parameters | 发送给 Maxwell 的参数 | 仿真输入(Input):实际发给 Maxwell 的几何/物理参数。默认从 Target 继承,但允许在 InputPanel 覆盖修改。 |
| maxwell_para_ranges | 优化范围 | 优化配置:AI 算法的上下限配置。 |
| result_detail | 仿真结果 | 仿真输出(Output):Maxwell 算完回填的标量数据。 |
2. 架构重构:引入信号驱动机制
修改 DataService,使其继承自 QObject,并在数据变更时发送信号。
修改后的 services/data_service.py 结构建议:
codePython
from PyQt6.QtCore import QObject, pyqtSignal, QMutex, QMutexLocker
class DataService(QObject):
_instance = None
_create_mutex = QMutex()
# === 定义信号,通知 UI 刷新 ===
# 这样 UI 不需要知道是谁改了数据,只要数据变了,UI 就自动刷新
data_changed = pyqtSignal(str, dict) # (category, new_data)
def __init__(self):
super().__init__()
if not hasattr(self, '_initialized'):
self._data_mutex = QMutex()
# 数据容器
self._data = {
"product_info": {},
"project_info": {},
"customer_requirements": {}, # 对应设计目标
"maxwell_parameters": {}, # 对应仿真输入
"process_limits": {},
"maxwell_para_ranges": {},
"result_detail": {}
}
self._initialized = True
# === 通用更新方法 ===
def _update_category(self, category: str, data: dict):
with QMutexLocker(self._data_mutex):
self._data[category].update(data)
# 拿到更新后的完整副本
full_data = self._data[category].copy()
# 发射信号 (在锁外部,防止死锁)
self.data_changed.emit(category, full_data)
# === 具体业务方法 ===
def set_active_product(self, product_dict: dict):
"""加载新产品时调用,清空旧状态,填入新状态"""
with QMutexLocker(self._data_mutex):
self._data["product_info"] = product_dict
self._data["customer_requirements"] = product_dict # 初始时,需求即参数
# 关键:仿真参数初始值 = 客户需求
self._data["maxwell_parameters"] = product_dict.copy()
self._data["result_detail"] = {} # 清空旧结果
# 通知所有监听者:产品变了,请刷新
self.data_changed.emit("all", self._data)
def update_maxwell_parameters(self, params: dict):
self._update_category("maxwell_parameters", params)
def update_result_detail(self, results: dict):
self._update_category("result_detail", results)
# ... get 方法保持不变 (返回 copy)
3. 修改 UI 逻辑:变为被动视图 (Passive View)
UI 不再直接持有数据,而是作为 DataService 的观察者。
在 InputPanel (示例) 中的改动:
codePython
class InputPanel(QWidget):
def __init__(self, data_service, parent=None):
super().__init__(parent)
self.data_service = data_service
self._init_ui()
# 1. 连接信号:只有 Service 说变了,我才变
self.data_service.data_changed.connect(self.on_data_changed)
def on_data_changed(self, category, data):
"""响应数据变化"""
if category == "maxwell_parameters" or category == "all":
self.block_signals(True) # 防止死循环:UI改->Service改->UI改
self.fill_input_fields(data)
self.block_signals(False)
if category == "result_detail" or category == "all":
self.update_result_labels(data)
def _on_text_changed(self, key, text):
"""用户手动修改时,只负责提交给 Service"""
# ... 解析 val ...
# 只要提交,不要在这里直接 setText,让 on_data_changed 回调来做
self.data_service.update_maxwell_parameters({key: val})
def block_signals(self, block: bool):
# 辅助函数,批量阻塞所有输入框信号
for widgets in self.fields_map.values():
if 'value' in widgets: widgets['value'].blockSignals(block)
三、 梳理后的数据流向图
采用上述重构后,数据流将变得非常清晰:
1. 选择产品 (Select Product)
Controller: 从 DB 读取 Product 对象。
Controller: 调用 data_service.set_active_product(product_dict)。
DataService: 更新内部状态,发射 data_changed("all", ...) 信号。
UI (TargetPage, InputPanel, DetailPage): 收到信号 -> 自动刷新界面显示。
2. 用户修改参数 (User Edit)
UI (InputPanel): 用户修改“线径”。
UI: 触发 textChanged -> 调用 data_service.update_maxwell_parameters({'wire_size': 0.5})。
DataService: 更新内存字典,发射 data_changed("maxwell_parameters", ...)。
UI: 收到信号 (如果是其他联动UI则更新,如果是自己则被 blockSignals 忽略)。
3. 执行仿真 (Run Simulation)
UI: 点击“开始”。
Controller: 调用 data_service.get_maxwell_parameters() 获取最新、最全的参数。
Controller: 将参数包传给 Worker。
Worker: 执行 Maxwell 脚本。
4. 仿真结束 (Simulation Finish)
Worker: 解析出结果字典。
Worker: 调用 data_service.update_result_detail(result_dict)。
DataService: 发射 data_changed("result_detail", ...)。
UI (ResultPanel, ChartPanel): 收到信号 -> 自动刷新结果显示 (对比值、图表)。
Controller: (可选) 调用 DB Service 将 DataService 中的 maxwell_parameters (输入) 和 result_detail (输出) 存入 SimulationResult 表,形成历史记录。
四、 具体的代码修改 Checklist
services/data_service.py:
继承 QObject。
添加 pyqtSignal。
在 update 方法中发射信号。
添加 set_active_product 方法来统一初始化一次仿真的上下文。
ui/pages/bldc_page/widgets/input_panel.py:
删除 fill_input_fields 的直接调用逻辑(由信号触发)。
添加 on_data_changed 槽函数。
在 _on_text_changed 中确保只是“提交数据”,而不是“修改界面”。
controllers/product_controller.py:
在 on_product_selected 中,除了更新 UI,务必调用 self.service.data_service.set_active_product(...)。目前你好像把 data service 混在了 ProductService 或者直接在 View 里调用,建议统一在 Controller 里分发。
utils/property_change.py:
确保它只负责翻译数据,不持有数据。它现在的逻辑是 fields_map (UI控件引用),这不好。
改进: 让它接收 dict (来自 DataService) 而不是 fields_map (UI控件)。这样后台 Worker 可以直接用它,不需要依赖 UI 线程。
通过这种方式,你的应用将遵循 MVVM (Model-View-ViewModel) 的变体模式,DataService 充当 ViewModel,负责持有状态和通知 View 更新,彻底解决数据流混乱的问题。