From 875d9ba3c6283759de8d2729891a29b410f942a0 Mon Sep 17 00:00:00 2001 From: suyu <1643689728@qq.com> Date: Sat, 22 Aug 2026 10:56:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E7=9B=91=E6=8E=A7=E4=B8=8E=E6=8E=A7=E5=88=B6=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/domain/active_register_repository.cpp | 1 + app/src/domain/active_register_repository.h | 2 ++ app/src/domain/alarm_model.h | 3 ++ app/src/domain/control_logic_model.cpp | 1 + app/src/domain/control_logic_model.h | 33 +++++++++++++++++++ app/src/domain/project_limits.h | 1 + app/src/domain/project_model.cpp | 3 ++ app/src/domain/project_model.h | 8 +++-- app/src/domain/register_address.h | 10 ++++++ app/src/domain/register_monitor_model.h | 8 +++++ app/src/domain/register_repository.h | 15 +++++---- app/src/domain/runtime_state.h | 9 ++--- app/src/infrastructure/json_project_storage.h | 2 +- .../plc_communication_error_classifier.h | 3 ++ .../plc_communication_service.cpp | 9 +++++ .../plc_communication_service.h | 11 +++++++ .../infrastructure/plc_register_repository.h | 6 ++++ app/src/services/alarm_editor_service.h | 8 +++++ app/src/services/alarm_service.cpp | 1 + app/src/services/alarm_service.h | 6 ++++ app/src/services/editor_history.h | 3 ++ app/src/services/hmi_editor_service.cpp | 1 + app/src/services/hmi_navigation_service.cpp | 1 + app/src/services/hmi_navigation_service.h | 7 ++++ app/src/services/hmi_runtime_service.h | 1 + app/src/services/logic_editor_service.cpp | 4 +++ app/src/services/logic_editor_service.h | 13 ++++++++ .../services/offline_simulation_service.cpp | 4 +++ app/src/services/offline_simulation_service.h | 7 ++++ app/src/services/plc_communication_gateway.h | 8 +++++ app/src/services/project_service.cpp | 1 + app/src/services/project_service.h | 4 ++- app/src/services/register_comment_service.h | 6 ++++ app/src/services/register_monitor_service.h | 9 +++++ app/src/services/runtime_mode_service.cpp | 6 ++++ app/src/services/runtime_mode_service.h | 4 ++- app/src/services/software_logic_executor.cpp | 3 ++ app/src/services/software_logic_executor.h | 11 +++++++ app/src/ui/alarm_configuration_dialog.h | 2 ++ app/src/ui/free_monitor_widget.h | 2 ++ app/src/ui/hmi_editor_widget.cpp | 5 +++ app/src/ui/hmi_editor_widget.h | 1 + app/src/ui/logic_editor_widget.cpp | 2 ++ app/src/ui/logic_editor_widget.h | 3 ++ app/src/ui/logic_instruction_dialog.h | 2 ++ app/src/ui/main_window.cpp | 2 ++ app/src/ui/main_window.h | 1 + app/src/ui/plc_connection_dialog.h | 2 ++ app/src/ui/project_workspace_controller.h | 1 + app/src/ui/property_panel_controller.h | 1 + app/src/ui/register_comment_dialog.h | 1 + app/src/ui/runtime_monitor_widget.cpp | 2 ++ app/src/ui/runtime_monitor_widget.h | 3 ++ app/src/ui/runtime_monitor_window.cpp | 1 + app/src/ui/runtime_monitor_window.h | 3 ++ app/src/ui/runtime_panel_controller.cpp | 4 ++- app/src/ui/runtime_panel_controller.h | 2 ++ app/src/ui/toolbar_icon_factory.h | 2 ++ 58 files changed, 258 insertions(+), 17 deletions(-) diff --git a/app/src/domain/active_register_repository.cpp b/app/src/domain/active_register_repository.cpp index 0c1c5dd..a91c5e8 100644 --- a/app/src/domain/active_register_repository.cpp +++ b/app/src/domain/active_register_repository.cpp @@ -7,6 +7,7 @@ ActiveRegisterRepository::ActiveRegisterRepository(RegisterRepository &initial_r void ActiveRegisterRepository::use(RegisterRepository &repository) { + // 只替换转发目标,不复制寄存器值;切换模式必须使用各自数据源 repository_ = &repository; } diff --git a/app/src/domain/active_register_repository.h b/app/src/domain/active_register_repository.h index d5857bb..cb11e0b 100644 --- a/app/src/domain/active_register_repository.h +++ b/app/src/domain/active_register_repository.h @@ -2,11 +2,13 @@ #include "register_repository.h" +// 根据当前运行模式把寄存器访问转发给虚拟仓库或 PLC 缓存仓库 class ActiveRegisterRepository final : public RegisterRepository { public: explicit ActiveRegisterRepository(RegisterRepository &initial_repository); + // 切换活动仓库;运行模式服务负责保证切换时机和生命周期 void use(RegisterRepository &repository); BitReadResult readBit(const RegisterAddress &address) const override; RegisterWriteResult writeBit(const RegisterAddress &address, bool value) override; diff --git a/app/src/domain/alarm_model.h b/app/src/domain/alarm_model.h index c0859e3..a5a2465 100644 --- a/app/src/domain/alarm_model.h +++ b/app/src/domain/alarm_model.h @@ -5,6 +5,7 @@ #include #include +// 报警触发条件:M 位为 1,或 D 字高于/低于阈值 enum class AlarmCondition { MOn, @@ -12,6 +13,7 @@ enum class AlarmCondition DLow }; +// 可保存的报警定义;运行时 AlarmService 根据此定义生成报警记录 struct AlarmDefinition { std::string id; @@ -20,5 +22,6 @@ struct AlarmDefinition std::int16_t threshold = 0; std::string message; + // 校验标识、地址区域、消息长度和条件之间的组合是否有效 bool validate(std::string *error = nullptr) const; }; diff --git a/app/src/domain/control_logic_model.cpp b/app/src/domain/control_logic_model.cpp index 2d393af..e1b61d3 100644 --- a/app/src/domain/control_logic_model.cpp +++ b/app/src/domain/control_logic_model.cpp @@ -691,6 +691,7 @@ bool ConditionExpression::validateForRunning(std::string *error) const void normalizeConditionExpression(std::optional *expression) { + // 删除节点后折叠单子项容器,并合并相邻同类容器,保持 JSON 结构稳定 if (expression == nullptr || !expression->has_value()) { return; diff --git a/app/src/domain/control_logic_model.h b/app/src/domain/control_logic_model.h index b1aab8f..2b4fd12 100644 --- a/app/src/domain/control_logic_model.h +++ b/app/src/domain/control_logic_model.h @@ -9,12 +9,14 @@ #include #include +// 普通触点的导通方式;常闭触点会对读取到的位值取反 enum class ContactMode { NormallyOpen, NormallyClosed }; +// 线圈写入方式:普通写入、置位保持、复位清零 enum class CoilMode { Normal, @@ -22,12 +24,14 @@ enum class CoilMode Reset }; +// 上升沿/下降沿触点的边沿方向 enum class EdgeMode { Rising, Falling }; +// D 字比较节点支持的比较运算 enum class ComparisonOperator { Equal, @@ -38,18 +42,21 @@ enum class ComparisonOperator GreaterThanOrEqual }; +// 字操作数可以来自常量,也可以来自 D 寄存器 enum class WordOperandKind { Constant, Register }; +// MOVE、ADD/SUB、计数器预置值共用的字操作数 struct WordOperand { WordOperandKind kind = WordOperandKind::Constant; RegisterAddress address{RegisterArea::D, 0}; std::int16_t constant = 0; + // 常量始终有效;寄存器操作数必须是有效 D 地址 bool validate(std::string *error = nullptr) const; }; @@ -78,6 +85,7 @@ struct CompareNodeConfig std::int16_t value = 0; }; +// 离线执行器内部使用的 T 区地址,不会加入 PLC M/D 轮询 class TimerAddress { public: @@ -97,12 +105,14 @@ private: int index_ = 0; }; +// 读取 TON 完成状态的条件触点 struct TimerContactNodeConfig { TimerAddress address; ContactMode mode = ContactMode::NormallyOpen; }; +// 离线执行器内部使用的 C 区地址,不会直接映射 PLC C 区 class CounterAddress { public: @@ -128,12 +138,14 @@ enum class CounterMode Down }; +// 读取计数器完成状态的条件触点 struct CounterContactNodeConfig { CounterAddress address; ContactMode mode = ContactMode::NormallyOpen; }; +// TON 输出的定时器和预置时间,单位为毫秒 struct TonNodeConfig { static constexpr int kMinimumPresetMs = 1; @@ -143,6 +155,7 @@ struct TonNodeConfig int presetMs = 1000; }; +// CTU/CTD 输出配置;当前值、预置值和复位信号都可引用 M/D struct CounterNodeConfig { CounterAddress address; @@ -152,6 +165,7 @@ struct CounterNodeConfig RegisterAddress resetAddress{RegisterArea::M, 0}; }; +// MOVE 输出:把一个字操作数写入目标 D 地址 struct MoveNodeConfig { WordOperand source; @@ -164,6 +178,7 @@ enum class ArithmeticOperation Subtract }; +// ADD/SUB 输出:计算两个字操作数并写入目标 D 地址 struct ArithmeticNodeConfig { ArithmeticOperation operation = ArithmeticOperation::Add; @@ -172,6 +187,7 @@ struct ArithmeticNodeConfig RegisterAddress destination{RegisterArea::D, 0}; }; +// 所有梯形图指令的强类型配置联合,避免用无关字段拼装指令 using LogicNodeConfig = std::variant< ContactNodeConfig, EdgeContactNodeConfig, @@ -184,20 +200,25 @@ using LogicNodeConfig = std::variant< MoveNodeConfig, ArithmeticNodeConfig>; +// 返回节点最主要的 M/D 地址;复合输出请使用下面的收集函数 std::optional registerAddressForLogicNode( const LogicNodeConfig &config); +// 收集节点中所有显式 M/D 引用,用于 PLC 轮询集合构建 void collectRegisterAddressesForLogicNode( const LogicNodeConfig &config, std::vector *addresses); +// 表达式中的一个指令节点;configured=false 表示编辑中的未完成草稿 struct LogicNode { std::string id; LogicNodeConfig config; bool configured = true; + // 校验节点 ID、配置类型和配置内容 bool validate(std::string *error = nullptr) const; bool isConfigured() const; + // 判断节点能否放在条件区或固定输出槽 bool isCondition() const; bool isOutput() const; }; @@ -210,6 +231,7 @@ enum class ConditionExpressionKind Parallel }; +// 条件区中的持久化横线;columnSpan 表示跨越的网格列数 struct WireSegment { static constexpr int kMinimumColumnSpan = 1; @@ -232,7 +254,9 @@ struct ConditionExpression static ConditionExpression fromNode(LogicNode node); static ConditionExpression fromWire(std::string id, int column_span = 1); + // 编辑态校验允许空配置节点,但必须保持树结构合法 bool validate(std::string *error = nullptr) const; + // 运行态校验额外要求每个节点都已经配置完成 bool validateForRunning(std::string *error = nullptr) const; }; @@ -260,11 +284,15 @@ struct LadderRung std::optional condition; std::optional output; + // 编辑态允许没有条件或输出,便于逐步搭建网络 bool validate(std::string *error = nullptr) const; + // 只检查表达式拓扑、列数和输出位置 bool validateStructure(std::string *error = nullptr) const; + // 运行态要求有输出且所有引用都已配置 bool validateForRunning(std::string *error = nullptr) const; }; +// 一组按顺序扫描的梯形图网络;enabled=false 时运行校验会跳过它 struct ControlLogic { std::string id; @@ -272,17 +300,22 @@ struct ControlLogic std::vector rungs; bool enabled = true; + // 校验可保存结构 bool validate(std::string *error = nullptr) const; bool validateStructure(std::string *error = nullptr) const; + // 校验运行所需资源和网络配置 bool validateForRunning(std::string *error = nullptr) const; }; +// 检查同一 T 地址是否被多个逻辑节点以冲突方式使用 bool validateTimerReferencesForRunning( const std::vector &logics, std::string *error = nullptr); +// 检查同一 C 地址是否被多个计数器配置重复占用 bool validateCounterReferencesForRunning( const std::vector &logics, std::string *error = nullptr); +// 一次性执行 T/C 资源引用冲突检查 bool validateLogicResourceReferencesForRunning( const std::vector &logics, std::string *error = nullptr); diff --git a/app/src/domain/project_limits.h b/app/src/domain/project_limits.h index 69b0d5a..d403c58 100644 --- a/app/src/domain/project_limits.h +++ b/app/src/domain/project_limits.h @@ -2,6 +2,7 @@ #include +// 所有跨层共享的数量和范围边界集中在这里,避免 UI、服务和 JSON 各自写数字 namespace ProjectLimits { constexpr std::size_t kMaximumProjectFileBytes = 16U * 1024U * 1024U; diff --git a/app/src/domain/project_model.cpp b/app/src/domain/project_model.cpp index 0ec0a6a..b7068cc 100644 --- a/app/src/domain/project_model.cpp +++ b/app/src/domain/project_model.cpp @@ -111,6 +111,7 @@ const RegisterComment *Project::findRegisterComment( bool Project::validate(std::string *error) const { + // 这里是工程级总闸门:先检查数量和唯一性,再向下委托页面、报警和逻辑 if (metadata.id.empty() || metadata.name.empty() || metadata.formatVersion.empty()) { setError(error, "工程 ID、名称和格式版本不能为空"); @@ -258,6 +259,7 @@ bool Project::validate(std::string *error) const } } + // 同时收集所有显式 M/D 引用,提前检查 PLC 轮询集合不会超过上限 std::vector poll_addresses; for (const HmiPage &page : hmiPages) { @@ -313,6 +315,7 @@ bool Project::validate(std::string *error) const bool Project::validateForRunning(std::string *error) const { + // 运行校验建立在可保存校验之上,再额外拒绝未配置的运行对象 if (!validate(error)) { return false; diff --git a/app/src/domain/project_model.h b/app/src/domain/project_model.h index 96b7b4d..7f5abab 100644 --- a/app/src/domain/project_model.h +++ b/app/src/domain/project_model.h @@ -7,11 +7,13 @@ #include #include +// 工程级 M/D 注释,只描述地址含义,不保存实时值 struct RegisterComment { RegisterAddress address{RegisterArea::M, 0}; std::string text; + // 校验地址有效性和 UTF-8 文本长度 bool validate(std::string *error = nullptr) const; }; @@ -26,7 +28,7 @@ struct ProjectMetadata std::string formatVersion = "1.0"; }; -// 聚合工程中的 HMI 页面和控制逻辑 +// 聚合工程中的 HMI 页面、报警、寄存器注释和控制逻辑 struct Project { // 工程基本信息 @@ -42,8 +44,10 @@ struct Project // 工程包含的控制逻辑集合 std::vector controlLogics; + // 按地址查找工程注释,找不到时返回 nullptr const RegisterComment *findRegisterComment(const RegisterAddress &address) const; - // 校验工程配置并通过 error 返回失败原因 + // 编辑态校验:允许尚未绑定完成的草稿 bool validate(std::string *error = nullptr) const; + // 运行态校验:额外要求所有运行所需对象都已配置 bool validateForRunning(std::string *error = nullptr) const; }; diff --git a/app/src/domain/register_address.h b/app/src/domain/register_address.h index a9f2ccf..7373c23 100644 --- a/app/src/domain/register_address.h +++ b/app/src/domain/register_address.h @@ -2,6 +2,7 @@ #include +// 解析用户输入的 M/D 地址时可能出现的失败原因 enum class RegisterAddressParseError { None, @@ -11,23 +12,30 @@ enum class RegisterAddressParseError OutOfRange }; +// PLC 中支持的寄存器区域:M 是位,D 是 16 位有符号字 enum class RegisterArea { M, D }; +// 表示一个已经拆分为“区域 + 下标”的 M/D 地址 +// 下标统一使用 0~4000,和 Modbus 原始地址保持一致 class RegisterAddress { public: static constexpr int kMinimumIndex = 0; static constexpr int kMaximumIndex = 4000; + // 构造地址对象;非法下标不会在构造时抛异常,应通过 isValid() 检查 RegisterAddress(RegisterArea area, int index); + // 返回地址区域和数字下标 RegisterArea area() const; int index() const; + // 判断区域和下标是否满足项目边界 bool isValid() const; + // 转成界面和日志中使用的形式,例如 M10 或 D4000 std::string toString() const; bool operator==(const RegisterAddress &other) const; @@ -38,6 +46,7 @@ private: int index_; }; +// 文本地址解析结果;失败时 address 保持默认值,具体原因见 error struct RegisterAddressParseResult { bool succeeded = false; @@ -45,4 +54,5 @@ struct RegisterAddressParseResult RegisterAddressParseError error = RegisterAddressParseError::InvalidFormat; }; +// 解析 M0、D4000 这类地址文本,不接受带偏移的 Modbus 地址 RegisterAddressParseResult parseRegisterAddress(const std::string &text); diff --git a/app/src/domain/register_monitor_model.h b/app/src/domain/register_monitor_model.h index 090fd90..e9a11ca 100644 --- a/app/src/domain/register_monitor_model.h +++ b/app/src/domain/register_monitor_model.h @@ -6,6 +6,7 @@ #include #include +// 监控读数的可用状态 enum class MonitorValueState { Valid, @@ -13,6 +14,7 @@ enum class MonitorValueState CommunicationFault }; +// 一个监控行的地址、可用性和当前值 struct MonitorValue { RegisterAddress address{RegisterArea::M, 0}; @@ -21,15 +23,21 @@ struct MonitorValue std::int16_t wordValue = 0; }; +// 运行期间由用户临时维护的监控地址列表,不写入工程文件 class RegisterMonitorModel { public: static constexpr std::size_t kMaximumItemCount = 64U; + // 返回当前按添加顺序保存的地址 const std::vector &addresses() const; + // 判断地址是否已经在监控列表中 bool contains(const RegisterAddress &address) const; + // 添加一个地址;重复或超过上限时返回 false bool add(const RegisterAddress &address); + // 删除一个地址;地址不存在时返回 false bool remove(const RegisterAddress &address); + // 清空当前会话的监控列表 void clear(); private: diff --git a/app/src/domain/register_repository.h b/app/src/domain/register_repository.h index 9fee8e5..a90d477 100644 --- a/app/src/domain/register_repository.h +++ b/app/src/domain/register_repository.h @@ -6,7 +6,7 @@ #include #include -// 寄存器仓库操作结果 +// 寄存器仓库操作失败原因;服务层会把它转换成用户可读消息 enum class RegisterError { None, // 操作成功 无错误 @@ -40,23 +40,24 @@ struct RegisterWriteResult }; // M/D 寄存器访问边界 +// 所有运行态读写都必须经过此接口,UI 不直接接触串口或缓存实现 class RegisterRepository { public: virtual ~RegisterRepository() = default; - // 读取 M 区位寄存器 + // 读取 M 区位寄存器;传入 D 区地址会返回 AreaMismatch virtual BitReadResult readBit(const RegisterAddress &address) const = 0; - // 写入 M 区位寄存器 + // 写入 M 区位寄存器;真机实现不会在收到 PLC 确认前乐观修改缓存 virtual RegisterWriteResult writeBit(const RegisterAddress &address, bool value) = 0; - // 读取 D 区字寄存器 + // 读取 D 区 16 位字寄存器 virtual WordReadResult readWord(const RegisterAddress &address) const = 0; - // 写入 D 区字寄存器 + // 写入 D 区 16 位字寄存器 virtual RegisterWriteResult writeWord( const RegisterAddress &address, std::int16_t value) = 0; }; -// 基于内存的离线 M/D 寄存器仓库 +// 基于内存的离线 M/D 寄存器仓库;地址范围与真实 PLC 保持一致 class VirtualRegisterRepository : public RegisterRepository { public: @@ -72,7 +73,7 @@ public: RegisterWriteResult writeWord( const RegisterAddress &address, std::int16_t value) override; - // 将所有虚拟寄存器恢复为默认值 + // 将所有虚拟寄存器恢复为默认值,开始新的离线会话 void clear(); private: diff --git a/app/src/domain/runtime_state.h b/app/src/domain/runtime_state.h index a5187c1..92937aa 100644 --- a/app/src/domain/runtime_state.h +++ b/app/src/domain/runtime_state.h @@ -1,6 +1,6 @@ #pragma once -// 应用当前运行模式 +// 应用当前运行模式;三个状态之间不能直接从离线切到真机 enum class ApplicationMode { Editing, // 编辑工程状态 @@ -8,7 +8,7 @@ enum class ApplicationMode OnlineRunning // 真机联机运行状态 }; -// 当前运行模式对应的能力策略 +// 当前运行模式对应的能力策略,UI 用它决定哪些入口可用 struct ModePolicy { // 是否允许编辑工程 @@ -23,7 +23,7 @@ struct ModePolicy bool requiresInitialPlcRead = false; }; -// 根据运行模式返回对应的能力策略 +// 根据运行模式返回对应的能力策略;default 用于防御非法枚举值 constexpr ModePolicy policyForMode(ApplicationMode mode) { switch (mode) @@ -68,7 +68,7 @@ constexpr ModePolicy policyForMode(ApplicationMode mode) } } -// 模式切换失败原因 +// 模式切换失败原因,服务层会进一步补充可读消息 enum class ModeTransitionError { None, // 切换成功 @@ -88,6 +88,7 @@ struct ModeTransitionResult ModeTransitionError error = ModeTransitionError::None; }; +// 保存当前模式并执行最基本的状态机约束 class RuntimeState { public: diff --git a/app/src/infrastructure/json_project_storage.h b/app/src/infrastructure/json_project_storage.h index dcabc68..5e9158f 100644 --- a/app/src/infrastructure/json_project_storage.h +++ b/app/src/infrastructure/json_project_storage.h @@ -2,7 +2,7 @@ #include "domain/project_storage.h" -// 使用版本化 JSON 文件保存和加载工程 +// 使用严格 1.0 JSON 文件保存和加载工程;加载成功后才替换调用方工程 class JsonProjectStorage final : public ProjectStorage { public: diff --git a/app/src/infrastructure/plc_communication_error_classifier.h b/app/src/infrastructure/plc_communication_error_classifier.h index ceeefd1..af0ab27 100644 --- a/app/src/infrastructure/plc_communication_error_classifier.h +++ b/app/src/infrastructure/plc_communication_error_classifier.h @@ -5,6 +5,7 @@ #include #include +// Qt 错误码之外的通信现场信息,用于区分“未打开串口”和“已断线” struct PlcCommunicationErrorContext { QString portName; @@ -12,12 +13,14 @@ struct PlcCommunicationErrorContext bool receivedValidResponse = false; }; +// 分类后的用户可读通信故障 struct PlcCommunicationFailure { PlcCommunicationError type = PlcCommunicationError::Unknown; QString message; }; +// 将 Qt Modbus 错误和上下文转换为项目统一错误类型 PlcCommunicationFailure classifyPlcCommunicationError( QModbusDevice::Error error, const PlcCommunicationErrorContext &context); diff --git a/app/src/infrastructure/plc_communication_service.cpp b/app/src/infrastructure/plc_communication_service.cpp index 5479728..48f6412 100644 --- a/app/src/infrastructure/plc_communication_service.cpp +++ b/app/src/infrastructure/plc_communication_service.cpp @@ -190,6 +190,7 @@ PlcCommunicationResult PlcCommunicationService::connectDevice( { return {false, "PLC 连接已经启动,请先断开当前连接"}; } + // 每次连接都递增代次;旧连接的异步回复即使晚到也不能污染新缓存 configuration_ = configuration; ++connection_generation_; disconnecting_ = false; @@ -213,6 +214,7 @@ PlcCommunicationResult PlcCommunicationService::connectDevice( static_cast(configuration.stopBits)); master_->setTimeout(configuration.responseTimeoutMs); master_->setNumberOfRetries(configuration.retries); + // 新连接必须重新完成完整首读,不能沿用上一条连接的运行资格 repository_.invalidate(); updateInitialReadCompleted(false, true); rebuildPollBlocks(); @@ -253,6 +255,7 @@ PlcCommunicationResult PlcCommunicationService::setPollAddresses( { return {false, "PLC 轮询地址拆分后最多允许 64 个读块"}; } + // 当前读请求完成前暂存新集合,避免按半旧半新的地址解析回复 if (pending_reply_ != nullptr) { pending_poll_addresses_ = std::move(normalized); @@ -320,6 +323,7 @@ void PlcCommunicationService::rebuildPollBlocks() return left.index() < right.index(); }); addresses.erase(std::unique(addresses.begin(), addresses.end()), addresses.end()); + // 将相邻地址合并为读块,同时遵守 Modbus 单次读取上限 poll_blocks_.clear(); for (const RegisterAddress &address : addresses) { @@ -373,6 +377,7 @@ void PlcCommunicationService::pollNextBlock() { return; } + // 每次只发一个异步请求,完成回调中再推进到下一个块 const std::size_t block_index = next_poll_block_; const PollBlock block = poll_blocks_.at(block_index); next_poll_block_ = (next_poll_block_ + 1U) % poll_blocks_.size(); @@ -390,6 +395,7 @@ void PlcCommunicationService::pollNextBlock() this, [this, reply, block, block_index, generation] { + // 连接已经重建时,直接丢弃旧回复 if (generation != connection_generation_) { reply->deleteLater(); @@ -406,6 +412,7 @@ void PlcCommunicationService::pollNextBlock() initial_blocks_read_.cbegin(), initial_blocks_read_.cend(), [](bool read) { return read; }); + // 所有轮询块都成功读过,才授予真机运行资格 if (completed && !initial_read_completed_) { updateInitialReadCompleted(true); @@ -534,6 +541,7 @@ void PlcCommunicationService::handleReadFinished(QModbusReply *reply, PollBlock handleModbusError(reply->error()); return; } + // 只有成功回复才能更新 PLC 缓存;写请求不会直接改缓存 const QModbusDataUnit result = reply->result(); for (uint index = 0; index < result.valueCount(); ++index) { @@ -724,6 +732,7 @@ void PlcCommunicationService::setError(const PlcCommunicationFailure &failure) last_error_ = toUtf8(failure.message); poll_timer_.stop(); recovery_timer_.stop(); + // 任意通信故障都会撤销首读资格;恢复后必须重新完整读取 updateInitialReadCompleted(false); const bool disconnected = failure.type == PlcCommunicationError::SerialPortOpenFailed || failure.type == PlcCommunicationError::SerialConnectionLost diff --git a/app/src/infrastructure/plc_communication_service.h b/app/src/infrastructure/plc_communication_service.h index 657a406..1258cd7 100644 --- a/app/src/infrastructure/plc_communication_service.h +++ b/app/src/infrastructure/plc_communication_service.h @@ -17,6 +17,8 @@ class QModbusReply; class QModbusRtuSerialMaster; struct PlcCommunicationFailure; +// 基于 Qt Modbus RTU 的异步 PLC 通信实现 +// 负责连接、轮询、单点写入、故障恢复和首读资格,不阻塞 UI 线程 class PlcCommunicationService final : public QObject, public PlcCommunicationGateway { Q_OBJECT @@ -53,21 +55,29 @@ signals: private: struct PollBlock { + // 同一区域的一段连续 Modbus 原始地址 RegisterArea area = RegisterArea::M; int startAddress = 0; int count = 1; }; + // 把去重后的地址集合压缩为有限数量的连续读块 void rebuildPollBlocks(); + // 应用新的轮询集合;有请求在途时由 pending_* 延后应用 void applyPollAddresses(const std::vector &addresses); + // 发送下一段异步读取请求 void pollNextBlock(); + // Faulted 状态下用轻量探测判断通信是否恢复 void probeRecovery(); void handleRecoveryProbeFailure(QModbusDevice::Error error); void restoreCommunication(); + // 处理读回复并把值写入 PLC 缓存 void handleReadFinished(QModbusReply *reply, PollBlock block); + // 发送单点 M/D 写请求;缓存等待后续轮询确认 RegisterWriteResult sendBitWrite(const RegisterAddress &address, bool value); RegisterWriteResult sendWordWrite( const RegisterAddress &address, std::int16_t value); + // 更新“所有轮询块均成功读取”的真机进入资格 void updateInitialReadCompleted(bool completed, bool force_notification = false); void handleUnexpectedDisconnect(); void handleModbusError(QModbusDevice::Error error); @@ -93,6 +103,7 @@ private: std::uint64_t connection_generation_ = 0; PlcConnectionState state_ = PlcConnectionState::Disconnected; bool initial_read_completed_ = false; + // 首读阶段每个读块的完成标记,全部为 true 后才允许进入真机 std::vector initial_blocks_read_; PlcCommunicationError last_error_type_ = PlcCommunicationError::None; std::string last_error_; diff --git a/app/src/infrastructure/plc_register_repository.h b/app/src/infrastructure/plc_register_repository.h index 8d4b6a1..5b0ad18 100644 --- a/app/src/infrastructure/plc_register_repository.h +++ b/app/src/infrastructure/plc_register_repository.h @@ -5,6 +5,8 @@ #include #include +// 保存 PLC 最近一次成功读回的 M/D 缓存,并把写请求转交给通信服务 +// 未读到过的地址保持 Unavailable,避免界面显示伪造的初始值 class PlcRegisterRepository final : public RegisterRepository { public: @@ -16,12 +18,16 @@ public: RegisterWriteResult writeWord( const RegisterAddress &address, std::int16_t value) override; + // 注入异步写入回调;回调成功不代表缓存已经更新 void setWriteHandlers( std::function bit_handler, std::function word_handler); + // 由通信服务在读回成功后更新缓存并标记地址有效 void updateBit(int address, bool value); void updateWord(int address, std::int16_t value); + // 通信会话失效时清除“已读”标记,但保留数组内的旧值用于诊断 void invalidate(); + // 判断是否至少收到过一个有效 M/D 值 bool hasAnyValidValue() const; private: diff --git a/app/src/services/alarm_editor_service.h b/app/src/services/alarm_editor_service.h index 376bbe3..4b82805 100644 --- a/app/src/services/alarm_editor_service.h +++ b/app/src/services/alarm_editor_service.h @@ -7,6 +7,7 @@ class ProjectService; +// 报警编辑操作的失败分类 enum class AlarmEditorError { None, @@ -15,6 +16,7 @@ enum class AlarmEditorError InvalidDefinition }; +// 报警编辑结果;成功时 id 是新增或更新后的定义 ID struct AlarmEditorResult { bool succeeded = false; @@ -23,16 +25,22 @@ struct AlarmEditorResult std::string id; }; +// 负责报警定义的增删改,不负责读取实时寄存器 class AlarmEditorService { public: explicit AlarmEditorService(ProjectService &project_service); + // 返回当前工程中的报警定义 const std::vector &definitions() const; + // 按稳定 ID 查找报警定义 const AlarmDefinition *findDefinition(const std::string &id) const; + // 添加定义;失败时工程保持不变 AlarmEditorResult addDefinition(const AlarmDefinition &definition); + // 更新指定 ID;definition.id 会被服务统一为传入的 id AlarmEditorResult updateDefinition( const std::string &id, const AlarmDefinition &definition); + // 删除指定报警定义 AlarmEditorResult removeDefinition(const std::string &id); private: diff --git a/app/src/services/alarm_service.cpp b/app/src/services/alarm_service.cpp index c6f3c19..0f1d92d 100644 --- a/app/src/services/alarm_service.cpp +++ b/app/src/services/alarm_service.cpp @@ -16,6 +16,7 @@ AlarmService::AlarmService( void AlarmService::refresh() { + // 每次刷新只根据当前读回值增删记录,不会把通信失败误判为报警解除 const auto now = std::chrono::system_clock::now(); for (const AlarmDefinition &definition : project_service_.project().alarmDefinitions) diff --git a/app/src/services/alarm_service.h b/app/src/services/alarm_service.h index ec82ab8..e0a83c2 100644 --- a/app/src/services/alarm_service.h +++ b/app/src/services/alarm_service.h @@ -9,6 +9,7 @@ class ProjectService; class RegisterRepository; struct AlarmDefinition; +// 运行时产生的报警记录,不会写入工程 JSON struct AlarmRecord { std::string definitionId; @@ -17,6 +18,7 @@ struct AlarmRecord std::chrono::system_clock::time_point occurredAt; }; +// 根据当前活动寄存器仓库评估报警,并维护确认状态 class AlarmService { public: @@ -24,9 +26,13 @@ public: const ProjectService &project_service, RegisterRepository &repository); + // 重新评估全部定义;仍处于触发状态的记录会保留确认状态 void refresh(); + // 确认指定报警;不存在时返回 false bool acknowledge(const std::string &definition_id); + // 清空当前运行会话的报警记录 void reset(); + // 返回按触发时间保存的报警记录 const std::vector &records() const; private: diff --git a/app/src/services/editor_history.h b/app/src/services/editor_history.h index a42f7a3..aef409d 100644 --- a/app/src/services/editor_history.h +++ b/app/src/services/editor_history.h @@ -17,6 +17,7 @@ public: static constexpr std::size_t kMaximumEntries = 100U; template + // 只有前后状态不同才记录;新编辑会清空重做栈 void record(State before, const State &after, Equal equal) { if (equal(before, after)) @@ -28,6 +29,7 @@ public: redo_states_.clear(); } + // 返回撤销后的目标状态,并把当前状态放入重做栈 std::optional undo(State current) { if (undo_states_.empty()) @@ -41,6 +43,7 @@ public: return target; } + // 返回重做后的目标状态,并把当前状态放回撤销栈 std::optional redo(State current) { if (redo_states_.empty()) diff --git a/app/src/services/hmi_editor_service.cpp b/app/src/services/hmi_editor_service.cpp index 00c3e3f..4a95bcb 100644 --- a/app/src/services/hmi_editor_service.cpp +++ b/app/src/services/hmi_editor_service.cpp @@ -526,6 +526,7 @@ HmiEditorResult HmiEditorService::moveControl( HmiControl candidate = *control; candidate.bounds = bounds; // 更新前保留原控件,只有全部编辑规则通过才覆盖原值 + // 属性更新是整体替换:类型不变,但绑定、外观和扩展属性必须一起通过校验 std::string error; if (!validateEditableControl(*page, candidate, &error)) { diff --git a/app/src/services/hmi_navigation_service.cpp b/app/src/services/hmi_navigation_service.cpp index f99c5cf..1eea336 100644 --- a/app/src/services/hmi_navigation_service.cpp +++ b/app/src/services/hmi_navigation_service.cpp @@ -11,6 +11,7 @@ HmiNavigationService::HmiNavigationService(ProjectService &project_service) HmiNavigationResult HmiNavigationService::start() { + // 运行开始始终从工程声明的初始页面进入,而不是沿用上次会话页面 const Project &project = project_service_.project(); if (project.hmiPages.empty()) { diff --git a/app/src/services/hmi_navigation_service.h b/app/src/services/hmi_navigation_service.h index c7793a8..1e0e1f6 100644 --- a/app/src/services/hmi_navigation_service.h +++ b/app/src/services/hmi_navigation_service.h @@ -4,6 +4,7 @@ class ProjectService; +// HMI 页面跳转失败分类 enum class HmiNavigationError { None, @@ -11,6 +12,7 @@ enum class HmiNavigationError PageNotFound }; +// 页面跳转结果;成功时 pageId 是当前页 ID struct HmiNavigationResult { bool succeeded = false; @@ -19,14 +21,19 @@ struct HmiNavigationResult std::string pageId; }; +// 管理运行态当前页面,不直接操作 Qt 页面控件 class HmiNavigationService { public: explicit HmiNavigationService(ProjectService &project_service); + // 进入工程初始页面 HmiNavigationResult start(); + // 跳转到指定页面;目标不存在时保留原页面 HmiNavigationResult navigateTo(const std::string &page_id); + // 结束运行会话并清空当前页 void stop(); + // 返回当前运行页面 ID,未启动时为空 const std::string ¤tPageId() const; private: diff --git a/app/src/services/hmi_runtime_service.h b/app/src/services/hmi_runtime_service.h index e6c95e6..c4753bf 100644 --- a/app/src/services/hmi_runtime_service.h +++ b/app/src/services/hmi_runtime_service.h @@ -60,6 +60,7 @@ enum class HmiButtonEvent * * 服务不依赖虚拟寄存器实现、串口或 Modbus,可由运行模式注入不同仓库 */ +// 仅通过统一寄存器仓库执行 HMI 运行态读写 class HmiRuntimeService { public: diff --git a/app/src/services/logic_editor_service.cpp b/app/src/services/logic_editor_service.cpp index 923ca0b..170f312 100644 --- a/app/src/services/logic_editor_service.cpp +++ b/app/src/services/logic_editor_service.cpp @@ -918,6 +918,7 @@ LogicEditorResult LogicEditorService::appendCondition( { return failure(LogicEditorError::InvalidNode, "梯形图条件不能使用线圈节点"); } + // 先生成稳定节点 ID,再把新节点接到已有表达式的串联末尾 const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config)); HistoryState before = captureState(); @@ -941,6 +942,7 @@ LogicEditorResult LogicEditorService::appendCondition( std::move(leaf)); } std::string validation_error; + // 编辑服务修改后立即做整条网络校验;失败就恢复包括脏标记在内的完整快照 if (!rung->validate(&validation_error)) { rollbackEdit(std::move(before), modified_before); @@ -981,6 +983,7 @@ LogicEditorResult LogicEditorService::insertConditionAtColumn( return failure(LogicEditorError::InvalidOperation, "所选网格已被条件或横线占用"); } + // 点击远端空网格时,用持久化横线填充前置空档,再放入真实条件节点 const int leading_wire_columns = column - occupied_columns; const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config)); @@ -1035,6 +1038,7 @@ LogicEditorResult LogicEditorService::insertConditionAtColumn( } std::string validation_error; + // 原子提交要求候选表达式整体通过校验,否则撤销本次所有局部拼接 if (!rung->validate(&validation_error)) { rollbackEdit(std::move(before), modified_before); diff --git a/app/src/services/logic_editor_service.h b/app/src/services/logic_editor_service.h index 833d78b..de1a048 100644 --- a/app/src/services/logic_editor_service.h +++ b/app/src/services/logic_editor_service.h @@ -8,6 +8,7 @@ class ProjectService; +// 梯形图编辑失败分类 enum class LogicEditorError { None, @@ -22,6 +23,7 @@ enum class LogicEditorError LastLogicRequired }; +// 梯形图编辑结果;成功时 id 通常是新建或更新对象的稳定 ID struct LogicEditorResult { bool succeeded = false; @@ -30,11 +32,13 @@ struct LogicEditorResult std::string id; }; +// 负责把 UI 编辑命令转换为结构化表达式树操作,并维护撤销/重做 class LogicEditorService { public: explicit LogicEditorService(ProjectService &project_service); + // 以下查询接口只读工程模型,供编辑器投影和属性面板使用 const ControlLogic *findLogic(const std::string &logic_id) const; const LadderRung *findRung( const std::string &logic_id, const std::string &rung_id) const; @@ -50,13 +54,16 @@ public: const std::string &logic_id, const std::string &node_id) const; std::string registerCommentFor(const RegisterAddress &address) const; + // 确保新工程至少有一组可编辑逻辑 LogicEditorResult ensureDefaultLogic(); + // 逻辑组管理 LogicEditorResult addLogic(const std::string &name); LogicEditorResult renameLogic( const std::string &logic_id, const std::string &name); LogicEditorResult removeLogic(const std::string &logic_id); LogicEditorResult moveLogic(const std::string &logic_id, int offset); LogicEditorResult setLogicEnabled(const std::string &logic_id, bool enabled); + // 网络管理 LogicEditorResult addRung(const std::string &logic_id); LogicEditorResult removeRung( const std::string &logic_id, const std::string &rung_id); @@ -64,6 +71,7 @@ public: const std::string &logic_id, const std::string &rung_id, const std::string &comment); + // 条件区编辑:串联、按列插入、横线和并联分支 LogicEditorResult appendCondition( const std::string &logic_id, const std::string &rung_id, @@ -107,11 +115,13 @@ public: const std::string &logic_id, const std::string &rung_id, const std::vector &selected_expression_ids); + // 输出槽编辑;每个网络最多一个输出节点 LogicEditorResult setOutput( const std::string &logic_id, const std::string &rung_id, const LogicNodeConfig &config, bool configured = false); + // 节点属性和删除操作 LogicEditorResult updateNodeConfig( const std::string &logic_id, const std::string &node_id, @@ -130,6 +140,7 @@ public: const std::string &rung_id, const std::vector &expression_ids); + // 当前编辑会话的撤销/重做 bool canUndo() const; bool canRedo() const; LogicEditorResult undo(); @@ -137,6 +148,7 @@ public: void clearHistory(); private: + // 只保存逻辑集合快照,当前选择等 UI 会话状态不进入历史 struct HistoryState { std::vector logics; @@ -144,6 +156,7 @@ private: HistoryState captureState() const; void recordHistory(HistoryState before); + // 失败时恢复工程内容和操作前脏标记,保证编辑原子性 void rollbackEdit(HistoryState before, bool modified_before); static bool statesEqual( const HistoryState &left, const HistoryState &right); diff --git a/app/src/services/offline_simulation_service.cpp b/app/src/services/offline_simulation_service.cpp index 3413d44..a1b59de 100644 --- a/app/src/services/offline_simulation_service.cpp +++ b/app/src/services/offline_simulation_service.cpp @@ -22,6 +22,7 @@ SimulationStartResult OfflineSimulationService::start( return {false, SimulationStartError::AlreadyRunning, {}}; } + // 仿真期间使用启动时快照,编辑器后续修改不会改变正在扫描的逻辑 std::vector snapshot = logics; const LogicScanResult validation = executor_.validate(snapshot); if (!validation.succeeded) @@ -30,6 +31,7 @@ SimulationStartResult OfflineSimulationService::start( return {false, SimulationStartError::InvalidLogic, validation}; } + // 重启会话时清空 T/C/沿触发状态和虚拟 M/D,避免继承上次运行现场 timer_.stop(); executor_.resetRuntime(); repository_.clear(); @@ -68,6 +70,7 @@ LogicScanResult OfflineSimulationService::executeOnce() {}, {}}; } + // 执行器负责一轮完整扫描;任何读写错误都会让服务进入 Faulted const LogicScanResult result = executor_.executeScan( logic_snapshot_, repository_, &trace_snapshot_); if (!result.succeeded) @@ -112,6 +115,7 @@ void OfflineSimulationService::handleTimeout() void OfflineSimulationService::enterFault(const LogicScanResult &error) { + // 故障后停止定时器和运行时状态,保留错误及最后轨迹供 UI 诊断 timer_.stop(); executor_.resetRuntime(); last_error_ = error; diff --git a/app/src/services/offline_simulation_service.h b/app/src/services/offline_simulation_service.h index 8c0a048..93a4545 100644 --- a/app/src/services/offline_simulation_service.h +++ b/app/src/services/offline_simulation_service.h @@ -8,6 +8,7 @@ #include #include +// 离线扫描器的生命周期状态 enum class SimulationState { Stopped, @@ -15,6 +16,7 @@ enum class SimulationState Faulted }; +// 启动离线仿真失败的原因 enum class SimulationStartError { None, @@ -22,6 +24,7 @@ enum class SimulationStartError InvalidLogic }; +// 离线仿真启动结果;detail 保存领域执行器的具体校验错误 struct SimulationStartResult { bool succeeded = false; @@ -41,14 +44,18 @@ public: VirtualRegisterRepository &repository, QObject *parent = nullptr); + // 复制工程逻辑作为本次会话快照并启动定时扫描 SimulationStartResult start(const std::vector &logics); + // 停止扫描但保留最近一次轨迹供 UI 显示 void stop(); + // 立即执行一轮扫描,主要供定时器和调试调用 LogicScanResult executeOnce(); SimulationState state() const; int scanIntervalMs() const; std::uint64_t successfulScanCount() const; const LogicScanResult &lastError() const; + // 返回最近一轮按逻辑 ID 隔离的运行轨迹 const LogicTraceSnapshot &traceSnapshot() const; signals: diff --git a/app/src/services/plc_communication_gateway.h b/app/src/services/plc_communication_gateway.h index c97cd18..fc2a55d 100644 --- a/app/src/services/plc_communication_gateway.h +++ b/app/src/services/plc_communication_gateway.h @@ -9,6 +9,7 @@ #include #include +// PLC 串口和轮询参数;parity 使用 Qt 约定值:0 无、2 偶、3 奇 struct PlcSerialConfiguration { std::string portName; @@ -22,6 +23,7 @@ struct PlcSerialConfiguration int pollIntervalMs = 200; }; +// PLC 异步通信生命周期 enum class PlcConnectionState { Disconnected, @@ -31,6 +33,7 @@ enum class PlcConnectionState Faulted }; +// 通信故障分类,用于状态栏提示和真机运行资格撤销 enum class PlcCommunicationError { None, @@ -47,12 +50,14 @@ enum class PlcCommunicationError Unknown }; +// 通信命令的受理结果;真正的读写完成由回调和缓存更新通知 struct PlcCommunicationResult { bool succeeded = false; std::string message; }; +// 校验串口配置,不打开串口也不产生副作用 inline PlcCommunicationResult validatePlcSerialConfiguration( const PlcSerialConfiguration &configuration) { @@ -108,6 +113,7 @@ inline PlcCommunicationResult validatePlcSerialConfiguration( return {true, {}}; } +// UI/运行服务使用的异步 PLC 通信契约,具体实现位于 infrastructure class PlcCommunicationGateway { public: @@ -116,12 +122,14 @@ public: virtual PlcCommunicationResult connectDevice( const PlcSerialConfiguration &configuration) = 0; virtual void disconnectDevice() = 0; + // 设置后续轮询的 M/D 地址集合;实现可以合并相邻地址块 virtual PlcCommunicationResult setPollAddresses( const std::vector &addresses) = 0; virtual PlcConnectionState state() const = 0; virtual bool initialReadCompleted() const = 0; virtual PlcCommunicationError lastErrorType() const = 0; virtual const std::string &lastError() const = 0; + // 注册通信状态、首读资格、缓存更新和错误通知 virtual void setCallbacks( std::function state_changed, std::function initial_read_changed, diff --git a/app/src/services/project_service.cpp b/app/src/services/project_service.cpp index 3a5a710..bcf89b2 100644 --- a/app/src/services/project_service.cpp +++ b/app/src/services/project_service.cpp @@ -134,6 +134,7 @@ ProjectOperationResult ProjectService::load(const std::string &file_path) "必须指定工程文件路径"}; } + // 存储层先在临时结果中解析,避免半个工程替换当前工程 ProjectLoadResult result = storage_.load(file_path); if (!result.succeeded) { diff --git a/app/src/services/project_service.h b/app/src/services/project_service.h index 6fd9a8e..66d719c 100644 --- a/app/src/services/project_service.h +++ b/app/src/services/project_service.h @@ -14,7 +14,7 @@ enum class ProjectServiceError StorageFailure // 工程文件存储操作失败 }; -// 工程管理操作结果 +// 工程管理操作结果;失败时当前工程和文件路径保持原状 struct ProjectOperationResult { bool succeeded = false; // 操作是否成功 @@ -30,7 +30,9 @@ public: // 使用外部提供的工程存储实现创建服务 explicit ProjectService(ProjectStorage &storage); + // 只读访问当前工程;UI 和其他服务不应绕过 editProject 修改它 const Project &project() const; + // 获取服务层授权的可编辑引用,调用方必须随后完成校验 Project &editProject(); const std::string ¤tFilePath() const; bool hasCurrentFile() const; diff --git a/app/src/services/register_comment_service.h b/app/src/services/register_comment_service.h index 5a3b287..91e8804 100644 --- a/app/src/services/register_comment_service.h +++ b/app/src/services/register_comment_service.h @@ -7,6 +7,7 @@ class ProjectService; +// 工程级寄存器注释操作的失败分类 enum class RegisterCommentError { None, @@ -14,6 +15,7 @@ enum class RegisterCommentError NotFound }; +// 注释编辑结果 struct RegisterCommentResult { bool succeeded = false; @@ -21,15 +23,19 @@ struct RegisterCommentResult std::string message; }; +// 维护工程里的 M/D 注释,不读取也不修改寄存器实时值 class RegisterCommentService { public: explicit RegisterCommentService(ProjectService &project_service); const std::vector &comments() const; + // 按地址查找注释 const RegisterComment *findComment(const RegisterAddress &address) const; + // 新建或更新注释;空文本通常表示删除该注释 RegisterCommentResult setComment( const RegisterAddress &address, const std::string &text); + // 删除指定地址注释 RegisterCommentResult removeComment(const RegisterAddress &address); private: diff --git a/app/src/services/register_monitor_service.h b/app/src/services/register_monitor_service.h index ec1dc40..5540c6f 100644 --- a/app/src/services/register_monitor_service.h +++ b/app/src/services/register_monitor_service.h @@ -8,6 +8,7 @@ #include #include +// 自由监控列表编辑的失败分类 enum class RegisterMonitorError { None, @@ -17,6 +18,7 @@ enum class RegisterMonitorError NoChange }; +// 监控列表编辑结果;affectedCount 表示本次受影响的地址数量 struct RegisterMonitorResult { bool succeeded = false; @@ -25,6 +27,7 @@ struct RegisterMonitorResult int affectedCount = 0; }; +// 自由监控写入结果;底层 RegisterError 会原样保留 struct RegisterMonitorWriteResult { bool succeeded = false; @@ -32,15 +35,21 @@ struct RegisterMonitorWriteResult std::string message; }; +// 编排监控模型和寄存器仓库,地址列表属于当前会话 class RegisterMonitorService { public: explicit RegisterMonitorService(RegisterRepository &repository); + // 从起始地址连续添加 count 个同区域地址 RegisterMonitorResult addRange(const std::string &start_address, int count); + // 删除一批地址并通知 UI 刷新轮询集合 RegisterMonitorResult remove(const std::vector &addresses); + // 清空监控列表 RegisterMonitorResult clear(); + // 对单个 M 地址写位 RegisterMonitorWriteResult writeBit(const RegisterAddress &address, bool value); + // 对单个 D 地址写字 RegisterMonitorWriteResult writeWord( const RegisterAddress &address, std::int16_t value); const std::vector &addresses() const; diff --git a/app/src/services/runtime_mode_service.cpp b/app/src/services/runtime_mode_service.cpp index 9fe511f..46885dd 100644 --- a/app/src/services/runtime_mode_service.cpp +++ b/app/src/services/runtime_mode_service.cpp @@ -67,10 +67,12 @@ ModeTransitionResult RuntimeModeService::enterOfflineRunning() { return {false, ModeTransitionError::ProjectNotReady}; } + // 离线仿真必须先切到虚拟仓库,避免扫描结果写入 PLC 缓存 if (active_repository_ != nullptr && virtual_repository_ != nullptr) { active_repository_->use(*virtual_repository_); } + // start() 会复制逻辑快照并做运行前校验;失败时不进入运行态 const SimulationStartResult start_result = offline_simulation_service_.start( project_service_.project().controlLogics); if (!start_result.succeeded) @@ -87,6 +89,7 @@ ModeTransitionResult RuntimeModeService::enterOfflineRunning() ModeTransitionResult RuntimeModeService::enterOnlineRunning() { + // 真机模式只允许使用已完成首读的 PLC 缓存,不复制离线寄存器值 const ModeTransitionResult result = state_.enterOnlineRunning( initialPlcReadCompleted()); if (result.succeeded && active_repository_ != nullptr && plc_repository_ != nullptr) @@ -140,6 +143,7 @@ void RuntimeModeService::configurePlc( active_repository_ = &active_repository; virtual_repository_ = &virtual_repository; plc_repository_ = &plc_repository; + // 通信故障撤销首读资格;若正在真机运行,立即回到编辑态 gateway.setCallbacks( [this] { @@ -212,6 +216,7 @@ PlcCommunicationResult RuntimeModeService::refreshPlcPollAddresses() { return {false, "PLC 通信服务尚未配置"}; } + // 轮询集合来自自由监控、HMI、报警和梯形图引用的并集 std::vector addresses = monitor_addresses_; const Project &project = project_service_.project(); for (const HmiPage &page : project.hmiPages) @@ -262,6 +267,7 @@ PlcCommunicationResult RuntimeModeService::refreshPlcPollAddresses() } return left.index() < right.index(); }); + // 排序后去重,保证同一个地址只占一份轮询配额 addresses.erase(std::unique(addresses.begin(), addresses.end()), addresses.end()); if (addresses.size() > ProjectLimits::kMaximumPollAddresses) { diff --git a/app/src/services/runtime_mode_service.h b/app/src/services/runtime_mode_service.h index ac01a07..010edeb 100644 --- a/app/src/services/runtime_mode_service.h +++ b/app/src/services/runtime_mode_service.h @@ -20,7 +20,7 @@ class ProjectService; class ActiveRegisterRepository; class RegisterRepository; -// 隔离 UI 与领域状态机并编排进入真机运行态的前置条件 +// 隔离 UI 与领域状态机,并编排寄存器仓库切换和进入真机的前置条件 class RuntimeModeService { public: @@ -69,11 +69,13 @@ public: std::uint64_t successfulScanCount() const; const LogicScanResult &simulationError() const; OfflineSimulationService &offlineSimulationService(); + // 注入 PLC 网关、活动仓库和两种实际数据源;应用生命周期内只调用一次 void configurePlc( PlcCommunicationGateway &gateway, ActiveRegisterRepository &active_repository, RegisterRepository &virtual_repository, RegisterRepository &plc_repository); + // 连接 PLC 并开始异步通信,不阻塞 UI 等待首读 PlcCommunicationResult connectPlc(const PlcSerialConfiguration &configuration); void setMonitorAddresses(const std::vector &addresses); PlcCommunicationResult refreshPlcPollAddresses(); diff --git a/app/src/services/software_logic_executor.cpp b/app/src/services/software_logic_executor.cpp index 6f6f033..964ba39 100644 --- a/app/src/services/software_logic_executor.cpp +++ b/app/src/services/software_logic_executor.cpp @@ -113,6 +113,7 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic( LogicScanResult SoftwareLogicExecutor::validate( const std::vector &logics) const { + // 先检查每个逻辑自身,再检查跨网络共享线圈和 T/C 资源的冲突 std::map output_modes; for (const ControlLogic &logic : logics) { @@ -209,10 +210,12 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( return validation; } + // 每轮从空轨迹开始,防止已删除或未执行的节点残留旧状态 if (trace != nullptr) { trace->clear(); } + // 按工程顺序扫描;前面网络写入的 M/D 值对后面网络立即可见 for (const ControlLogic &logic : logics) { if (!logic.enabled) diff --git a/app/src/services/software_logic_executor.h b/app/src/services/software_logic_executor.h index 209025b..f94bc93 100644 --- a/app/src/services/software_logic_executor.h +++ b/app/src/services/software_logic_executor.h @@ -11,6 +11,7 @@ #include #include +// 一轮离线扫描可能返回的错误 enum class LogicScanError { None, @@ -20,6 +21,7 @@ enum class LogicScanError RegisterWriteFailed }; +// 扫描结果;失败时携带出错的逻辑、网络和节点 ID struct LogicScanResult { bool succeeded = false; @@ -30,6 +32,7 @@ struct LogicScanResult std::string nodeId; }; +// TON 节点在最近一轮扫描中的可视化状态 struct TonTraceValue { bool input = false; @@ -38,6 +41,7 @@ struct TonTraceValue int presetMs = 0; }; +// CTU/CTD 节点在最近一轮扫描中的可视化状态 struct CounterTraceValue { bool input = false; @@ -47,12 +51,14 @@ struct CounterTraceValue std::int16_t preset = 0; }; +// MOVE/ADD/SUB 节点的结果值和溢出标记 struct WordTraceValue { std::int16_t value = 0; bool overflow = false; }; +// 一条逻辑或一个网络的运行轨迹快照 struct LogicTraceValues { std::unordered_map nodeValues; @@ -68,6 +74,7 @@ struct LogicTraceValues void clear(); }; +// 全工程轨迹;logicValues 按逻辑 ID 隔离,避免节点 ID 重复互相覆盖 struct LogicTraceSnapshot : LogicTraceValues { std::unordered_map logicValues; @@ -83,12 +90,16 @@ public: using Clock = std::chrono::steady_clock; using TimePoint = Clock::time_point; + // 在启动仿真前检查所有启用逻辑及 T/C 资源引用 LogicScanResult validate(const std::vector &logics) const; + // 清除沿触发、定时器和计数器的跨扫描运行状态 void resetRuntime(); + // 使用当前 steady_clock 执行一轮扫描 LogicScanResult executeScan( const std::vector &logics, RegisterRepository &repository, LogicTraceSnapshot *trace = nullptr); + // 使用指定时间执行扫描,便于稳定验证 TON 等时间逻辑 LogicScanResult executeScanAt( const std::vector &logics, RegisterRepository &repository, diff --git a/app/src/ui/alarm_configuration_dialog.h b/app/src/ui/alarm_configuration_dialog.h index c0df271..5473a1b 100644 --- a/app/src/ui/alarm_configuration_dialog.h +++ b/app/src/ui/alarm_configuration_dialog.h @@ -12,6 +12,7 @@ class AlarmConfigurationDialog; class AlarmEditorService; struct AlarmDefinition; +// 报警定义编辑对话框;保存和删除都委托 AlarmEditorService class AlarmConfigurationDialog final : public QDialog { public: @@ -21,6 +22,7 @@ public: ~AlarmConfigurationDialog() override; private: + // 刷新左侧列表并尽量恢复原选中项 void reloadDefinitions(const std::string &selected_id = {}); void loadSelectedDefinition(); void updateConditionOptions(); diff --git a/app/src/ui/free_monitor_widget.h b/app/src/ui/free_monitor_widget.h index 4729149..f894a5c 100644 --- a/app/src/ui/free_monitor_widget.h +++ b/app/src/ui/free_monitor_widget.h @@ -13,6 +13,7 @@ class FreeMonitorWidget; class RegisterMonitorService; +// 自由监控 UI:地址列表属于当前会话,读写统一委托 RegisterMonitorService class FreeMonitorWidget final : public QWidget { Q_OBJECT @@ -23,6 +24,7 @@ public: QWidget *parent = nullptr); ~FreeMonitorWidget() override; + // 真机未连接、通信故障或编辑态时应关闭写入入口 void setWriteEnabled(bool enabled); void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); void reloadAddresses(); diff --git a/app/src/ui/hmi_editor_widget.cpp b/app/src/ui/hmi_editor_widget.cpp index 2d164c1..8a31cee 100644 --- a/app/src/ui/hmi_editor_widget.cpp +++ b/app/src/ui/hmi_editor_widget.cpp @@ -902,6 +902,7 @@ HmiEditorWidget::HmiEditorWidget( // 切换显示页面 void HmiEditorWidget::setPageId(const std::string &page_id) { + // 切换页面只改变投影上下文,真正的页面数据仍由 ProjectService 持有 if (page_id_ == page_id) { return; @@ -945,6 +946,7 @@ void HmiEditorWidget::setRuntimeWriteEnabled(bool enabled) // 全部重新加载当前页面,把内存模型 HmiPage 渲染成画面上图形 void HmiEditorWidget::reloadPage() { + // 模型发生变化后完全重建图元,避免增量刷新遗漏属性或选择状态 // 画布始终从当前领域页面重建,避免保留已删除控件的图元 scene_->clear(); const HmiPage *page = editor_service_.findPage(page_id_); @@ -1052,6 +1054,7 @@ std::vector HmiEditorWidget::selectedControlIds() const void HmiEditorWidget::refreshRuntimeValues() { + // 运行态才读取寄存器;编辑态画布只显示工程配置,不显示缓存值 if (!runtime_active_) { return; @@ -1119,6 +1122,7 @@ void HmiEditorWidget::handleSelectionChanged() void HmiEditorWidget::handleControlMoved( const std::string &control_id, const QPointF &position) { + // 拖动结束后把坐标交给服务层,由服务层负责页面边界校验和历史记录 const HmiControl *control = editor_service_.findControl(page_id_, control_id); if (control == nullptr) { @@ -1146,6 +1150,7 @@ void HmiEditorWidget::handleButtonEvent( { return; } + // 运行态按钮事件只通过 HmiRuntimeService 写寄存器,不直接访问仓库 const HmiControl *control = editor_service_.findControl(page_id_, control_id); if (control == nullptr) { diff --git a/app/src/ui/hmi_editor_widget.h b/app/src/ui/hmi_editor_widget.h index 5eca681..c1d32bf 100644 --- a/app/src/ui/hmi_editor_widget.h +++ b/app/src/ui/hmi_editor_widget.h @@ -57,6 +57,7 @@ public: */ void setRuntimeActive(bool active); // 故障态保持运行值可见,但禁止按钮和数值输入继续写入 + // 故障态保持运行值可见,但禁止按钮和数值输入继续写入 void setRuntimeWriteEnabled(bool enabled); /** * @brief 使用当前页面模型完全重建场景 diff --git a/app/src/ui/logic_editor_widget.cpp b/app/src/ui/logic_editor_widget.cpp index 4d0aa61..2967e10 100644 --- a/app/src/ui/logic_editor_widget.cpp +++ b/app/src/ui/logic_editor_widget.cpp @@ -1048,6 +1048,7 @@ void LogicEditorWidget::setEditingEnabled(bool enabled) void LogicEditorWidget::setRuntimeTrace( const LogicTraceSnapshot &trace, const std::string &fault_node_id) { + // 轨迹只是模型的只读投影;编辑器不因显示轨迹而修改工程表达式 trace_ = trace; fault_node_id_ = fault_node_id; runtime_trace_enabled_ = true; @@ -1064,6 +1065,7 @@ void LogicEditorWidget::clearRuntimeTrace() void LogicEditorWidget::reloadLogic() { + // 逻辑或选择变化后重新计算网格布局和可点击插入目标 scene_->clear(); const ControlLogic *logic = editor_service_.findLogic(logic_id_); if (logic == nullptr) diff --git a/app/src/ui/logic_editor_widget.h b/app/src/ui/logic_editor_widget.h index 26629d0..8cff549 100644 --- a/app/src/ui/logic_editor_widget.h +++ b/app/src/ui/logic_editor_widget.h @@ -13,6 +13,7 @@ class QGraphicsScene; class QResizeEvent; +// 将结构化梯形图表达式投影成网格图元;不保存自由线段,编辑通过服务提交 class LogicEditorWidget final : public QGraphicsView { Q_OBJECT @@ -31,6 +32,7 @@ public: void setLogicId(const std::string &logic_id); void setEditingEnabled(bool enabled); + // 设置离线运行轨迹;真机模式应传入空轨迹,避免显示本地伪轨迹 void setRuntimeTrace( const LogicTraceSnapshot &trace, const std::string &fault_node_id = {}); @@ -42,6 +44,7 @@ public: std::string selectedRungId() const; LogicEditorResult addRung(); + // 下列编辑命令只负责把当前选择翻译成服务调用 LogicEditorResult addCondition(const LogicNodeConfig &config); LogicEditorResult addParallelBranch(const LogicNodeConfig &config); LogicEditorResult addHorizontalWire(); diff --git a/app/src/ui/logic_instruction_dialog.h b/app/src/ui/logic_instruction_dialog.h index a08ee7c..aeeba69 100644 --- a/app/src/ui/logic_instruction_dialog.h +++ b/app/src/ui/logic_instruction_dialog.h @@ -13,6 +13,7 @@ namespace Ui { class LogicInstructionDialog; } +// 梯形图节点配置对话框;只负责把表单转换为强类型 LogicNodeConfig class LogicInstructionDialog final : public QDialog { Q_OBJECT @@ -23,6 +24,7 @@ public: QWidget *parent = nullptr); ~LogicInstructionDialog() override; + // 返回当前表单配置,调用方仍需交给 LogicEditorService 校验 LogicNodeConfig config() const; private: diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index a920393..c3c1d36 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -1494,6 +1494,7 @@ void MainWindow::appendOutputMessage(const QString &message) void MainWindow::requestMode(ApplicationMode requested_mode) { + // UI 动作只提出目标模式,所有前置条件和仓库切换由 RuntimeModeService 决定 // UI 仅转发模式意图,合法性由服务层和领域状态机决定 ModeTransitionResult result; switch (requested_mode) @@ -1544,6 +1545,7 @@ void MainWindow::requestMode(ApplicationMode requested_mode) void MainWindow::updateModeUi(const QString &message) { + // 按服务层策略统一启用/禁用编辑入口,避免单个按钮遗漏状态同步 const ApplicationMode mode = runtime_mode_service_.mode(); const ModePolicy policy = runtime_mode_service_.policy(); restoreCurrentModeAction(); diff --git a/app/src/ui/main_window.h b/app/src/ui/main_window.h index 666b3f0..9364d2b 100644 --- a/app/src/ui/main_window.h +++ b/app/src/ui/main_window.h @@ -51,6 +51,7 @@ class RuntimePanelController; * * 主窗口不直接访问寄存器、工程文件或 PLC 通信实现 */ +// 编程器主窗口:负责组装 UI 控制器、窗口动作和模式切换,不承载领域规则 class MainWindow final : public QMainWindow { Q_OBJECT diff --git a/app/src/ui/plc_connection_dialog.h b/app/src/ui/plc_connection_dialog.h index 8ef96d4..fc55062 100644 --- a/app/src/ui/plc_connection_dialog.h +++ b/app/src/ui/plc_connection_dialog.h @@ -10,6 +10,7 @@ namespace Ui { class PlcConnectionDialog; } +// PLC 串口参数对话框;只编辑配置,不负责打开串口 class PlcConnectionDialog final : public QDialog { Q_OBJECT @@ -20,6 +21,7 @@ public: QWidget *parent = nullptr); ~PlcConnectionDialog() override; + // 返回表单中的串口配置,连接前由网关再次校验 PlcSerialConfiguration configuration() const; private: diff --git a/app/src/ui/project_workspace_controller.h b/app/src/ui/project_workspace_controller.h index f308ef1..0e6e6ab 100644 --- a/app/src/ui/project_workspace_controller.h +++ b/app/src/ui/project_workspace_controller.h @@ -24,6 +24,7 @@ class MainWindow; } // 组织工程树、当前页面/逻辑选择和工程对象的界面刷新 +// 它不直接改 Project,所有增删改都转发给对应服务 class ProjectWorkspaceController final { public: diff --git a/app/src/ui/property_panel_controller.h b/app/src/ui/property_panel_controller.h index 4328c21..269ad23 100644 --- a/app/src/ui/property_panel_controller.h +++ b/app/src/ui/property_panel_controller.h @@ -20,6 +20,7 @@ class MainWindow; } // 组织 HMI/梯形图选择状态、属性面板和编辑器信号 +// 属性提交统一经过编辑服务,以便校验、原子回滚和撤销/重做 class PropertyPanelController final { public: diff --git a/app/src/ui/register_comment_dialog.h b/app/src/ui/register_comment_dialog.h index ede20fa..2bc7b74 100644 --- a/app/src/ui/register_comment_dialog.h +++ b/app/src/ui/register_comment_dialog.h @@ -13,6 +13,7 @@ class RegisterCommentDialog; class RegisterCommentService; +// 工程级 M/D 注释编辑对话框;注释不包含实时寄存器值 class RegisterCommentDialog final : public QDialog { public: diff --git a/app/src/ui/runtime_monitor_widget.cpp b/app/src/ui/runtime_monitor_widget.cpp index 1b227ed..25d1689 100644 --- a/app/src/ui/runtime_monitor_widget.cpp +++ b/app/src/ui/runtime_monitor_widget.cpp @@ -132,6 +132,7 @@ void RuntimeMonitorWidget::setRuntimePage(const std::string &page_id) void RuntimeMonitorWidget::setMode( ApplicationMode mode, PlcConnectionState plc_state) { + // 真机时清空本地轨迹,防止把 PC 离线执行结果误认为 PLC 内部程序轨迹 const bool offline = mode == ApplicationMode::OfflineRunning; const bool online = mode == ApplicationMode::OnlineRunning; ui_->logicFrame->setVisible(offline); @@ -168,6 +169,7 @@ void RuntimeMonitorWidget::setFreeMonitorWriteEnabled(bool enabled) void RuntimeMonitorWidget::refreshValues( ApplicationMode mode, PlcConnectionState plc_state) { + // HMI、报警和自由监控共享同一个活动寄存器仓库 hmi_view_->refreshRuntimeValues(); free_monitor_widget_->refreshValues(mode, plc_state); } diff --git a/app/src/ui/runtime_monitor_widget.h b/app/src/ui/runtime_monitor_widget.h index 4862d8c..1c3155b 100644 --- a/app/src/ui/runtime_monitor_widget.h +++ b/app/src/ui/runtime_monitor_widget.h @@ -24,6 +24,7 @@ class LogicEditorWidget; class RegisterMonitorService; class ProjectService; +// 唯一的工程师运行监控投影,组合 HMI、梯形图轨迹和自由监控 class RuntimeMonitorWidget final : public QWidget { Q_OBJECT @@ -42,10 +43,12 @@ public: void setProjectObjects(const std::string &page_id, const std::string &logic_id); void setRuntimePage(const std::string &page_id); + // 根据模式决定是否显示本地逻辑轨迹以及是否允许写入 void setMode(ApplicationMode mode, PlcConnectionState plc_state); void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); void setHmiWriteEnabled(bool enabled); void setFreeMonitorWriteEnabled(bool enabled); + // 只接收离线执行器轨迹;真机时由上层清空 void setLogicTrace(const LogicTraceSnapshot &trace, const std::string &fault_node_id = {}); FreeMonitorWidget *freeMonitorWidget() const; std::string selectedLogicId() const; diff --git a/app/src/ui/runtime_monitor_window.cpp b/app/src/ui/runtime_monitor_window.cpp index 3a0006b..1f25b13 100644 --- a/app/src/ui/runtime_monitor_window.cpp +++ b/app/src/ui/runtime_monitor_window.cpp @@ -44,6 +44,7 @@ void RuntimeMonitorWindow::closeForApplicationExit() void RuntimeMonitorWindow::closeEvent(QCloseEvent *event) { + // 运行期间关闭按钮等价于请求返回编辑态,应用退出时才真正放行关闭 if (runtime_active_ && !application_exit_) { emit exitRequested(); diff --git a/app/src/ui/runtime_monitor_window.h b/app/src/ui/runtime_monitor_window.h index 33623ba..98365dc 100644 --- a/app/src/ui/runtime_monitor_window.h +++ b/app/src/ui/runtime_monitor_window.h @@ -21,8 +21,11 @@ public: ~RuntimeMonitorWindow() override; void setMonitorWidget(QWidget &widget); + // 进入运行态时显示并最大化窗口 void showForRuntime(); + // 返回编辑态时隐藏窗口但保留唯一监控控件 void hideForEditing(); + // 应用退出时允许真正关闭窗口 void closeForApplicationExit(); signals: diff --git a/app/src/ui/runtime_panel_controller.cpp b/app/src/ui/runtime_panel_controller.cpp index 4d1c41f..a7d019c 100644 --- a/app/src/ui/runtime_panel_controller.cpp +++ b/app/src/ui/runtime_panel_controller.cpp @@ -158,7 +158,8 @@ void RuntimePanelController::enterRuntime( if (!runtime_session_active_) { runtime_monitor_widget_->setProjectObjects(page_id, logic_id); - runtime_session_active_ = true; + // 运行窗口只创建一次,后续进入运行态复用同一个监控投影 + runtime_session_active_ = true; } runtime_monitor_widget_->setMode(mode, plc_state); runtime_monitor_window_->showForRuntime(); @@ -273,6 +274,7 @@ void RuntimePanelController::updateSimulationUi(bool report_fault) void RuntimePanelController::handleRuntimeTimer() { + // 定时器只负责刷新投影;离线逻辑扫描由 OfflineSimulationService 自己的定时器驱动 if (runtime_mode_service_.mode() == ApplicationMode::Editing) { return; diff --git a/app/src/ui/runtime_panel_controller.h b/app/src/ui/runtime_panel_controller.h index 66367db..5151af6 100644 --- a/app/src/ui/runtime_panel_controller.h +++ b/app/src/ui/runtime_panel_controller.h @@ -56,11 +56,13 @@ public: ~RuntimePanelController(); void configure(); + // 创建/复用唯一监控窗口,并把当前工程对象投影到运行态 void enterRuntime( const std::string &page_id, const std::string &logic_id, ApplicationMode mode, PlcConnectionState plc_state); + // 停止刷新并隐藏监控窗口,离线服务的停止由运行模式服务负责 void leaveRuntime(ApplicationMode mode, PlcConnectionState plc_state); void closeForApplicationExit(); void updateSimulationUi(bool report_fault); diff --git a/app/src/ui/toolbar_icon_factory.h b/app/src/ui/toolbar_icon_factory.h index 1c1d2b5..397d8c9 100644 --- a/app/src/ui/toolbar_icon_factory.h +++ b/app/src/ui/toolbar_icon_factory.h @@ -2,6 +2,7 @@ #include +// 工具栏语义图标;具体 QIcon 绘制集中在 toolbar_icon_factory.cpp enum class UiIcon { Edit, @@ -55,4 +56,5 @@ enum class UiIcon ClearList }; +// 根据语义图标类型创建 Qt 图标 QIcon makeUiIcon(UiIcon icon);