| @@ -271,11 +271,14 @@ bool HmiPage::validate(std::string *error) const | |||
| setError(error, "HMI 页面名称不能超过 4096 个 UTF-8 字节"); | |||
| return false; | |||
| } | |||
| if (width <= 0 || height <= 0 | |||
| if (width < ProjectLimits::kMinimumHmiPageWidth | |||
| || height < ProjectLimits::kMinimumHmiPageHeight | |||
| || width > ProjectLimits::kMaximumHmiPageWidth | |||
| || height > ProjectLimits::kMaximumHmiPageHeight) | |||
| { | |||
| setError(error, "HMI 页面宽度和高度必须在 1~8192 范围内"); | |||
| setError( | |||
| error, | |||
| "HMI 页面宽度必须在 320~1600、高度必须在 200~800 范围内"); | |||
| return false; | |||
| } | |||
| if (controls.size() > ProjectLimits::kMaximumHmiControlsPerPage) | |||
| @@ -80,6 +80,24 @@ ConditionExpression *findParentExpression( | |||
| return nullptr; | |||
| } | |||
| const ConditionExpression *findParentExpression( | |||
| const ConditionExpression &expression, const std::string &child_id) | |||
| { | |||
| for (const ConditionExpression &child : expression.children) | |||
| { | |||
| if (child.id == child_id) | |||
| { | |||
| return &expression; | |||
| } | |||
| if (const ConditionExpression *parent = findParentExpression( | |||
| child, child_id)) | |||
| { | |||
| return parent; | |||
| } | |||
| } | |||
| return nullptr; | |||
| } | |||
| using NodeIdSet = std::unordered_set<std::string>; | |||
| NodeIdSet conditionLeafIds(const ConditionExpression &expression) | |||
| @@ -652,7 +670,6 @@ LogicEditorResult LogicEditorService::ensureDefaultLogic() | |||
| ControlLogic logic; | |||
| logic.id = "logic-1"; | |||
| logic.name = "控制逻辑 1"; | |||
| logic.rungs.push_back({"rung-1", "网络 1", {}, std::nullopt, std::nullopt}); | |||
| Project &project = project_service_.editProject(); | |||
| project.controlLogics.push_back(std::move(logic)); | |||
| return {true, LogicEditorError::None, {}, project.controlLogics.back().id}; | |||
| @@ -684,7 +701,6 @@ LogicEditorResult LogicEditorService::addLogic(const std::string &name) | |||
| ControlLogic logic; | |||
| logic.id = makeUniqueLogicId(current); | |||
| logic.name = name; | |||
| logic.rungs.push_back({"rung-1", "网络 1", {}, std::nullopt, std::nullopt}); | |||
| HistoryState before = captureState(); | |||
| Project &project = project_service_.editProject(); | |||
| project.controlLogics.push_back(std::move(logic)); | |||
| @@ -850,10 +866,6 @@ LogicEditorResult LogicEditorService::removeRung( | |||
| { | |||
| return failure(LogicEditorError::RungNotFound, "未找到梯形图网络"); | |||
| } | |||
| if (logic->rungs.size() == 1U) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "控制逻辑至少需要保留一个网络"); | |||
| } | |||
| HistoryState before = captureState(); | |||
| Project &project = project_service_.editProject(); | |||
| auto target = std::find_if( | |||
| @@ -908,11 +920,19 @@ LogicEditorResult LogicEditorService::appendCondition( | |||
| const LogicNodeConfig &config) | |||
| { | |||
| const ControlLogic *logic = findLogic(logic_id); | |||
| if (logic == nullptr || findRung(logic_id, rung_id) == nullptr) | |||
| if (logic == nullptr) | |||
| { | |||
| return failure( | |||
| logic == nullptr ? LogicEditorError::LogicNotFound : LogicEditorError::RungNotFound, | |||
| logic == nullptr ? "未找到控制逻辑" : "未找到梯形图网络"); | |||
| return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑"); | |||
| } | |||
| const bool create_rung = rung_id.empty(); | |||
| if (!create_rung && findRung(logic_id, rung_id) == nullptr) | |||
| { | |||
| return failure(LogicEditorError::RungNotFound, "未找到梯形图网络"); | |||
| } | |||
| if (create_rung | |||
| && logic->rungs.size() >= ProjectLimits::kMaximumRungsPerLogic) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "单组控制逻辑最多包含 1024 个网络"); | |||
| } | |||
| if (!isConditionConfig(config)) | |||
| { | |||
| @@ -920,11 +940,25 @@ LogicEditorResult LogicEditorService::appendCondition( | |||
| } | |||
| // 先生成稳定节点 ID,再把新节点接到已有表达式的串联末尾 | |||
| const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); | |||
| const std::string target_rung_id = create_rung | |||
| ? makeUniqueRungId(*logic) : rung_id; | |||
| ConditionExpression leaf = ConditionExpression::fromNode(makeNode(node_id, config)); | |||
| HistoryState before = captureState(); | |||
| const bool modified_before = project_service_.isModified(); | |||
| Project &project = project_service_.editProject(); | |||
| LadderRung *rung = findEditableRung(project, logic_id, rung_id); | |||
| auto editable_logic = std::find_if( | |||
| project.controlLogics.begin(), project.controlLogics.end(), | |||
| [&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }); | |||
| if (create_rung) | |||
| { | |||
| editable_logic->rungs.push_back({ | |||
| target_rung_id, | |||
| "网络 " + std::to_string(editable_logic->rungs.size() + 1U), | |||
| {}, | |||
| std::nullopt, | |||
| std::nullopt}); | |||
| } | |||
| LadderRung *rung = findEditableRung(project, logic_id, target_rung_id); | |||
| if (!rung->condition.has_value()) | |||
| { | |||
| rung->condition = std::move(leaf); | |||
| @@ -1048,31 +1082,152 @@ LogicEditorResult LogicEditorService::insertConditionAtColumn( | |||
| return {true, LogicEditorError::None, {}, node_id}; | |||
| } | |||
| LogicEditorResult LogicEditorService::appendWire( | |||
| LogicEditorResult LogicEditorService::insertConditionInBranchAtColumn( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| int column_span) | |||
| const std::string &branch_expression_id, | |||
| int column, | |||
| const LogicNodeConfig &config) | |||
| { | |||
| const ControlLogic *logic = findLogic(logic_id); | |||
| if (logic == nullptr || findRung(logic_id, rung_id) == nullptr) | |||
| const LadderRung *existing_rung = findRung(logic_id, rung_id); | |||
| if (logic == nullptr || existing_rung == nullptr | |||
| || !existing_rung->condition.has_value()) | |||
| { | |||
| return failure( | |||
| logic == nullptr ? LogicEditorError::LogicNotFound | |||
| : LogicEditorError::RungNotFound, | |||
| logic == nullptr ? "未找到控制逻辑" : "未找到梯形图网络"); | |||
| } | |||
| if (!isConditionConfig(config)) | |||
| { | |||
| return failure(LogicEditorError::InvalidNode, "并联空网格只能插入条件节点"); | |||
| } | |||
| const ConditionExpression *branch = findExpression( | |||
| logic_id, rung_id, branch_expression_id); | |||
| const ConditionExpression *branch_parent = findParentExpression( | |||
| *existing_rung->condition, branch_expression_id); | |||
| if (branch == nullptr || branch_parent == nullptr | |||
| || branch_parent->kind != ConditionExpressionKind::Parallel) | |||
| { | |||
| return failure( | |||
| LogicEditorError::ExpressionNotFound, | |||
| "未找到并联分支空网格"); | |||
| } | |||
| const int branch_columns = expressionColumns(*branch); | |||
| const int parallel_columns = expressionColumns(*branch_parent); | |||
| if (column < branch_columns || column >= parallel_columns) | |||
| { | |||
| return failure( | |||
| LogicEditorError::InvalidOperation, | |||
| "所选位置不是并联分支中的可用空网格"); | |||
| } | |||
| const int leading_wire_columns = column - branch_columns; | |||
| const std::string node_id = makeUniqueNodeId(*logic, nodePrefix(config)); | |||
| const std::string wire_id = leading_wire_columns > 0 | |||
| ? makeUniqueWireId(*logic) : std::string{}; | |||
| const std::string series_id = makeUniqueExpressionId(*logic); | |||
| ConditionExpression leaf = ConditionExpression::fromNode( | |||
| makeNode(node_id, config)); | |||
| HistoryState before = captureState(); | |||
| const bool modified_before = project_service_.isModified(); | |||
| LadderRung *rung = findEditableRung( | |||
| project_service_.editProject(), logic_id, rung_id); | |||
| ConditionExpression *editable_branch = findConditionExpression( | |||
| *rung->condition, branch_expression_id); | |||
| if (editable_branch->kind == ConditionExpressionKind::Series) | |||
| { | |||
| if (leading_wire_columns > 0) | |||
| { | |||
| editable_branch->children.push_back(ConditionExpression::fromWire( | |||
| wire_id, leading_wire_columns)); | |||
| } | |||
| editable_branch->children.push_back(std::move(leaf)); | |||
| } | |||
| else | |||
| { | |||
| ConditionExpression series; | |||
| series.id = series_id; | |||
| series.kind = ConditionExpressionKind::Series; | |||
| series.children.push_back(std::move(*editable_branch)); | |||
| if (leading_wire_columns > 0) | |||
| { | |||
| series.children.push_back(ConditionExpression::fromWire( | |||
| wire_id, leading_wire_columns)); | |||
| } | |||
| series.children.push_back(std::move(leaf)); | |||
| *editable_branch = std::move(series); | |||
| } | |||
| std::string validation_error; | |||
| if (!rung->validate(&validation_error)) | |||
| { | |||
| rollbackEdit(std::move(before), modified_before); | |||
| return failure(LogicEditorError::InvalidOperation, validation_error); | |||
| } | |||
| recordHistory(std::move(before)); | |||
| return {true, LogicEditorError::None, {}, node_id}; | |||
| } | |||
| LogicEditorResult LogicEditorService::appendWire( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| int column_span) | |||
| { | |||
| const ControlLogic *logic = findLogic(logic_id); | |||
| if (logic == nullptr) | |||
| { | |||
| return failure(LogicEditorError::LogicNotFound, "未找到控制逻辑"); | |||
| } | |||
| WireSegment wire{column_span}; | |||
| std::string error; | |||
| if (!wire.validate(&error)) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, error); | |||
| } | |||
| const LadderRung *existing_rung = rung_id.empty() | |||
| ? nullptr : findRung(logic_id, rung_id); | |||
| if (!rung_id.empty() && existing_rung == nullptr) | |||
| { | |||
| return failure(LogicEditorError::RungNotFound, "未找到梯形图网络"); | |||
| } | |||
| bool create_rung = rung_id.empty(); | |||
| if (!create_rung && column_span == 1 && existing_rung->condition.has_value() | |||
| && expressionColumns(*existing_rung->condition) | |||
| >= ProjectLimits::kMaximumConditionColumns) | |||
| { | |||
| // 无选中目标的连续追加达到十列后,原子切换到下一个网络 | |||
| create_rung = true; | |||
| } | |||
| if (create_rung | |||
| && logic->rungs.size() >= ProjectLimits::kMaximumRungsPerLogic) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "单组控制逻辑最多包含 1024 个网络"); | |||
| } | |||
| const std::string target_rung_id = create_rung | |||
| ? makeUniqueRungId(*logic) : rung_id; | |||
| const std::string wire_id = makeUniqueWireId(*logic); | |||
| ConditionExpression leaf = ConditionExpression::fromWire(wire_id, column_span); | |||
| HistoryState before = captureState(); | |||
| const bool modified_before = project_service_.isModified(); | |||
| LadderRung *rung = findEditableRung( | |||
| project_service_.editProject(), logic_id, rung_id); | |||
| Project &project = project_service_.editProject(); | |||
| auto editable_logic = std::find_if( | |||
| project.controlLogics.begin(), project.controlLogics.end(), | |||
| [&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }); | |||
| if (create_rung) | |||
| { | |||
| editable_logic->rungs.push_back({ | |||
| target_rung_id, | |||
| "网络 " + std::to_string(editable_logic->rungs.size() + 1U), | |||
| {}, | |||
| std::nullopt, | |||
| std::nullopt}); | |||
| } | |||
| LadderRung *rung = findEditableRung(project, logic_id, target_rung_id); | |||
| if (!rung->condition.has_value()) | |||
| { | |||
| rung->condition = std::move(leaf); | |||
| @@ -1488,14 +1643,31 @@ LogicEditorResult LogicEditorService::setOutput( | |||
| bool configured) | |||
| { | |||
| const ControlLogic *logic = findLogic(logic_id); | |||
| const LadderRung *existing_rung = findRung(logic_id, rung_id); | |||
| if (logic == nullptr || existing_rung == nullptr || !isOutputConfig(config)) | |||
| const LadderRung *existing_rung = rung_id.empty() | |||
| ? nullptr : findRung(logic_id, rung_id); | |||
| if (logic == nullptr || (!rung_id.empty() && existing_rung == nullptr) | |||
| || !isOutputConfig(config)) | |||
| { | |||
| return failure( | |||
| LogicEditorError::InvalidNode, | |||
| "梯形图输出必须使用有效的输出指令"); | |||
| logic == nullptr ? LogicEditorError::LogicNotFound | |||
| : existing_rung == nullptr && !rung_id.empty() | |||
| ? LogicEditorError::RungNotFound | |||
| : LogicEditorError::InvalidNode, | |||
| logic == nullptr ? "未找到控制逻辑" | |||
| : existing_rung == nullptr && !rung_id.empty() | |||
| ? "未找到梯形图网络" | |||
| : "梯形图输出必须使用有效的输出指令"); | |||
| } | |||
| const std::string node_id = existing_rung->output.has_value() | |||
| if (rung_id.empty() | |||
| && logic->rungs.size() >= ProjectLimits::kMaximumRungsPerLogic) | |||
| { | |||
| return failure(LogicEditorError::InvalidOperation, "单组控制逻辑最多包含 1024 个网络"); | |||
| } | |||
| const bool create_rung = rung_id.empty(); | |||
| const std::string target_rung_id = create_rung | |||
| ? makeUniqueRungId(*logic) : rung_id; | |||
| const std::string node_id = existing_rung != nullptr | |||
| && existing_rung->output.has_value() | |||
| ? existing_rung->output->id : makeUniqueNodeId(*logic, nodePrefix(config)); | |||
| LogicNode node = makeNode(node_id, config); | |||
| node.configured = configured; | |||
| @@ -1504,14 +1676,26 @@ LogicEditorResult LogicEditorService::setOutput( | |||
| { | |||
| return failure(LogicEditorError::InvalidNode, validation_error); | |||
| } | |||
| if (existing_rung->output.has_value() | |||
| if (existing_rung != nullptr && existing_rung->output.has_value() | |||
| && nodesEqual(*existing_rung->output, node)) | |||
| { | |||
| return {true, LogicEditorError::None, {}, node_id}; | |||
| } | |||
| HistoryState before = captureState(); | |||
| Project &project = project_service_.editProject(); | |||
| findEditableRung(project, logic_id, rung_id)->output = std::move(node); | |||
| auto editable_logic = std::find_if( | |||
| project.controlLogics.begin(), project.controlLogics.end(), | |||
| [&logic_id](const ControlLogic &candidate) { return candidate.id == logic_id; }); | |||
| if (create_rung) | |||
| { | |||
| editable_logic->rungs.push_back({ | |||
| target_rung_id, | |||
| "网络 " + std::to_string(editable_logic->rungs.size() + 1U), | |||
| {}, | |||
| std::nullopt, | |||
| std::nullopt}); | |||
| } | |||
| findEditableRung(project, logic_id, target_rung_id)->output = std::move(node); | |||
| recordHistory(std::move(before)); | |||
| return {true, LogicEditorError::None, {}, node_id}; | |||
| } | |||
| @@ -54,7 +54,7 @@ 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); | |||
| @@ -81,6 +81,12 @@ public: | |||
| const std::string &rung_id, | |||
| int column, | |||
| const LogicNodeConfig &config); | |||
| LogicEditorResult insertConditionInBranchAtColumn( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| const std::string &branch_expression_id, | |||
| int column, | |||
| const LogicNodeConfig &config); | |||
| LogicEditorResult appendWire( | |||
| const std::string &logic_id, | |||
| const std::string &rung_id, | |||
| @@ -1,3 +1,11 @@ | |||
| /** | |||
| * @file alarm_configuration_dialog.h | |||
| * @brief 定义报警定义配置对话框 | |||
| * @version 1.0.0 | |||
| * @author suyu | |||
| * @date 2026-08-22 | |||
| */ | |||
| #pragma once | |||
| #include <QDialog> | |||
| @@ -12,7 +20,7 @@ class AlarmConfigurationDialog; | |||
| class AlarmEditorService; | |||
| struct AlarmDefinition; | |||
| // 报警定义编辑对话框;保存和删除都委托 AlarmEditorService | |||
| /** 报警定义编辑对话框;保存和删除都委托 AlarmEditorService */ | |||
| class AlarmConfigurationDialog final : public QDialog | |||
| { | |||
| public: | |||
| @@ -22,16 +30,25 @@ public: | |||
| ~AlarmConfigurationDialog() override; | |||
| private: | |||
| // 刷新左侧列表并尽量恢复原选中项 | |||
| /** 刷新左侧列表并尽量恢复原选中项 */ | |||
| void reloadDefinitions(const std::string &selected_id = {}); | |||
| /** 将当前选中报警加载到编辑区 */ | |||
| void loadSelectedDefinition(); | |||
| /** 根据报警类型更新条件输入项 */ | |||
| void updateConditionOptions(); | |||
| /** 新增一条报警定义 */ | |||
| void addDefinition(); | |||
| /** 更新当前选中的报警定义 */ | |||
| void updateDefinition(); | |||
| /** 删除当前选中的报警定义 */ | |||
| void removeDefinition(); | |||
| /** 从输入框读取报警定义 */ | |||
| AlarmDefinition definitionFromInputs() const; | |||
| /** 返回当前选中的报警标识 */ | |||
| std::string selectedId() const; | |||
| /** Qt Designer 生成的界面对象 */ | |||
| std::unique_ptr<Ui::AlarmConfigurationDialog> ui_; | |||
| /** 报警定义服务,不由对话框拥有 */ | |||
| AlarmEditorService &service_; | |||
| }; | |||
| @@ -1,3 +1,11 @@ | |||
| /** | |||
| * @file free_monitor_widget.h | |||
| * @brief 定义自由寄存器监控控件 | |||
| * @version 1.0.0 | |||
| * @author suyu | |||
| * @date 2026-08-22 | |||
| */ | |||
| #pragma once | |||
| #include "domain/runtime_state.h" | |||
| @@ -13,7 +21,7 @@ class FreeMonitorWidget; | |||
| class RegisterMonitorService; | |||
| // 自由监控 UI:地址列表属于当前会话,读写统一委托 RegisterMonitorService | |||
| /** 自由监控 UI:地址列表属于当前会话,读写统一委托 RegisterMonitorService */ | |||
| class FreeMonitorWidget final : public QWidget | |||
| { | |||
| Q_OBJECT | |||
| @@ -24,24 +32,37 @@ public: | |||
| QWidget *parent = nullptr); | |||
| ~FreeMonitorWidget() override; | |||
| // 真机未连接、通信故障或编辑态时应关闭写入入口 | |||
| /** 真机未连接、通信故障或编辑态时应关闭写入入口 */ | |||
| void setWriteEnabled(bool enabled); | |||
| /** 根据运行模式和 PLC 状态刷新所有监控值 */ | |||
| void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); | |||
| /** 从服务重新加载当前会话中的地址列表 */ | |||
| void reloadAddresses(); | |||
| signals: | |||
| /** 监控地址列表发生变化时发出 */ | |||
| void monitorAddressesChanged(); | |||
| /** 操作完成后发出可直接显示的消息 */ | |||
| void operationMessage(const QString &message); | |||
| private: | |||
| /** 从输入框添加一个或多个监控地址 */ | |||
| void addAddresses(); | |||
| /** 删除当前选中的监控地址 */ | |||
| void removeSelectedAddresses(); | |||
| /** 清空当前会话中的监控地址 */ | |||
| void clearAddresses(); | |||
| /** 写入指定行中的寄存器值 */ | |||
| void writeRow(int row); | |||
| /** 根据当前权限刷新写入控件状态 */ | |||
| void updateWriteControls(); | |||
| /** 统一处理监控服务返回结果 */ | |||
| void handleResult(const QString &action, bool succeeded, const std::string &message); | |||
| /** Qt Designer 生成的界面对象 */ | |||
| std::unique_ptr<Ui::FreeMonitorWidget> ui_; | |||
| /** 寄存器监控服务,不由控件拥有 */ | |||
| RegisterMonitorService &service_; | |||
| /** 当前是否允许执行寄存器写入 */ | |||
| bool write_enabled_ = false; | |||
| }; | |||
| @@ -1,6 +1,7 @@ | |||
| /** | |||
| * @file hmi_editor_widget.h | |||
| * @brief 定义基于 QGraphicsScene 的 HMI 页面编辑和运行画布 | |||
| * @version 1.0.0 | |||
| * @author suyu | |||
| * @date 2026-08-10 | |||
| */ | |||
| @@ -56,8 +57,7 @@ public: | |||
| * @param active 为 true 时允许按钮和数值输入调用运行时服务 | |||
| */ | |||
| void setRuntimeActive(bool active); | |||
| // 故障态保持运行值可见,但禁止按钮和数值输入继续写入 | |||
| // 故障态保持运行值可见,但禁止按钮和数值输入继续写入 | |||
| /** 故障态保持运行值可见,但禁止按钮和数值输入继续写入 */ | |||
| void setRuntimeWriteEnabled(bool enabled); | |||
| /** | |||
| * @brief 使用当前页面模型完全重建场景 | |||
| @@ -70,7 +70,9 @@ public: | |||
| * @param control_id 待选中控件标识,不存在时不改变当前选择 | |||
| */ | |||
| void selectControl(const std::string &control_id); | |||
| /** 返回当前选中的单个控件标识 */ | |||
| std::string selectedControlId() const; | |||
| /** 返回当前选中的多个控件标识 */ | |||
| std::vector<std::string> selectedControlIds() const; | |||
| /** | |||
| * @brief 从统一寄存器仓库读取当前值并刷新画布显示 | |||
| @@ -95,6 +97,7 @@ signals: | |||
| * @param message 可直接显示的错误信息 | |||
| */ | |||
| void editorError(const QString &message); | |||
| /** 运行态请求跳转到指定 HMI 页面 */ | |||
| void pageNavigationRequested(const QString &target_page_id); | |||
| protected: | |||
| @@ -115,18 +118,20 @@ private: | |||
| // 在运行态处理数值输入激活 | |||
| void handleNumericInputActivated(const std::string &control_id); | |||
| // 提供控件查找、移动和属性更新能力,不直接操作 Qt 图元数据 | |||
| /** 提供控件查找、移动和属性更新能力,不直接操作 Qt 图元数据 */ | |||
| HmiEditorService &editor_service_; | |||
| // 提供运行态寄存器读写能力,画布不直接访问寄存器仓库 | |||
| /** 提供运行态寄存器读写能力,画布不直接访问寄存器仓库 */ | |||
| HmiRuntimeService &runtime_service_; | |||
| /** 报警查询服务,不由控件拥有 */ | |||
| AlarmService &alarm_service_; | |||
| // 持有所有页面图元和页面边框 | |||
| /** 持有所有页面图元和页面边框 */ | |||
| QGraphicsScene *scene_ = nullptr; | |||
| // 当前画布投影的页面标识 | |||
| /** 当前画布投影的页面标识 */ | |||
| std::string page_id_; | |||
| // 控制是否允许拖动和编辑控件 | |||
| /** 控制是否允许拖动和编辑控件 */ | |||
| bool editing_enabled_ = true; | |||
| // 控制是否刷新运行值并响应运行态控件操作 | |||
| /** 控制是否刷新运行值并响应运行态控件操作 */ | |||
| bool runtime_active_ = false; | |||
| /** 控制运行态按钮和数值输入是否允许写入 */ | |||
| bool runtime_write_enabled_ = false; | |||
| }; | |||
| @@ -649,14 +649,24 @@ public: | |||
| const std::string &rung_id, | |||
| int column, | |||
| const QPointF ¢er, | |||
| bool show_label) | |||
| : rung_id_(rung_id), column_(column), show_label_(show_label) | |||
| bool show_label, | |||
| std::string branch_expression_id = {}) | |||
| : rung_id_(rung_id), | |||
| branch_expression_id_(std::move(branch_expression_id)), | |||
| column_(column), | |||
| show_label_(show_label) | |||
| { | |||
| setPos(center); | |||
| setFlag(ItemIsSelectable, true); | |||
| setZValue(2.0); | |||
| setToolTip(LogicEditorWidget::tr("空条件网格:第 %1 列,点击后可插入触点") | |||
| .arg(column + 1)); | |||
| setToolTip( | |||
| branch_expression_id_.empty() | |||
| ? LogicEditorWidget::tr( | |||
| "空条件网格:第 %1 列,点击后可插入触点") | |||
| .arg(column + 1) | |||
| : LogicEditorWidget::tr( | |||
| "并联空网格:第 %1 格,点击后可插入触点") | |||
| .arg(column + 1)); | |||
| } | |||
| QRectF boundingRect() const override | |||
| @@ -676,9 +686,6 @@ public: | |||
| painter->setPen(QPen(kSelectionBorderColor, 1.4, Qt::DashLine)); | |||
| painter->drawRect(boundingRect().adjusted(3, 3, -3, -3)); | |||
| } | |||
| painter->setPen(ladderPen(false)); | |||
| painter->drawLine( | |||
| QPointF(-kCellWidth / 2.0, 0), QPointF(kCellWidth / 2.0, 0)); | |||
| if (show_label_) | |||
| { | |||
| painter->setPen(kPlaceholderColor); | |||
| @@ -690,10 +697,15 @@ public: | |||
| } | |||
| const std::string &rungId() const { return rung_id_; } | |||
| const std::string &branchExpressionId() const | |||
| { | |||
| return branch_expression_id_; | |||
| } | |||
| int column() const { return column_; } | |||
| private: | |||
| std::string rung_id_; | |||
| std::string branch_expression_id_; | |||
| int column_ = 0; | |||
| bool show_label_ = false; | |||
| }; | |||
| @@ -782,6 +794,13 @@ public: | |||
| QRectF boundingRect() const override { return {kSceneMargin, 0, width_, height_}; } | |||
| QPainterPath shape() const override | |||
| { | |||
| QPainterPath path; | |||
| path.addRect(QRectF(kSceneMargin, 0, width_, kRungHeaderHeight)); | |||
| return path; | |||
| } | |||
| void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override | |||
| { | |||
| const bool selected = (option->state & QStyle::State_Selected) != 0; | |||
| @@ -1006,9 +1025,29 @@ RenderResult renderExpression( | |||
| scene.addLine( | |||
| QLineF(QPointF(left_join, branches[index].input.y()), branches[index].input), | |||
| ladderPen(branch_active)); | |||
| scene.addLine( | |||
| QLineF(branches[index].output, QPointF(right_join, branches[index].output.y())), | |||
| ladderPen(branch_active)); | |||
| if (branches[index].output.x() < right_join - 0.1) | |||
| { | |||
| // 分支宽度不足时补出到右侧汇合点的结构连接,不写入隐式 Wire | |||
| scene.addLine( | |||
| QLineF( | |||
| branches[index].output, | |||
| QPointF(right_join, branches[index].output.y())), | |||
| ladderPen(branch_active)); | |||
| } | |||
| const int branch_columns = measureExpression( | |||
| expression.children[index]).columns; | |||
| for (int column = branch_columns; column < metrics.columns; ++column) | |||
| { | |||
| scene.addItem(new LogicEditorWidget::EmptySlotItem( | |||
| rung_id, | |||
| column, | |||
| QPointF( | |||
| top_left.x() | |||
| + (static_cast<qreal>(column) + 0.5) * kCellWidth, | |||
| branches[index].output.y()), | |||
| false, | |||
| expression.children[index].id)); | |||
| } | |||
| } | |||
| return {QPointF(left_join, top_y), QPointF(right_join, top_y)}; | |||
| } | |||
| @@ -1142,14 +1181,14 @@ void LogicEditorWidget::reloadLogic() | |||
| column == 0 && !rung.output.has_value())); | |||
| } | |||
| const bool rung_active = runtime_trace_enabled_ && traceValue( | |||
| trace_, &LogicTraceSnapshot::rungValues, rung.id); | |||
| const qreal output_left = right_rail_x - kCellWidth; | |||
| scene_->addLine( | |||
| QLineF(expression_output, QPointF(output_left, main_y)), | |||
| ladderPen(rung_active)); | |||
| if (rung.output.has_value()) | |||
| { | |||
| const bool rung_active = runtime_trace_enabled_ && traceValue( | |||
| trace_, &LogicTraceSnapshot::rungValues, rung.id); | |||
| scene_->addLine( | |||
| QLineF(expression_output, QPointF(output_left, main_y)), | |||
| ladderPen(rung_active)); | |||
| scene_->addLine( | |||
| QLineF(QPointF(output_left, main_y), QPointF(right_rail_x, main_y)), | |||
| ladderPen(rung_active)); | |||
| @@ -1186,11 +1225,6 @@ void LogicEditorWidget::reloadLogic() | |||
| *scene_, | |||
| QRectF(output_left, grid_top, kCellWidth, kCellHeight), | |||
| tr("输出线圈")); | |||
| scene_->addLine( | |||
| QLineF( | |||
| QPointF(output_left + kCellWidth, main_y), | |||
| QPointF(right_rail_x, main_y)), | |||
| ladderPen(rung_active)); | |||
| } | |||
| top += rung_height + kRungGap; | |||
| ++number; | |||
| @@ -1353,17 +1387,17 @@ std::vector<std::string> LogicEditorWidget::selectedWireIds() const | |||
| return ids; | |||
| } | |||
| std::vector<int> LogicEditorWidget::selectedEmptySlotColumns() const | |||
| std::vector<std::pair<std::string, int>> LogicEditorWidget::selectedEmptySlots() const | |||
| { | |||
| std::vector<int> columns; | |||
| std::vector<std::pair<std::string, int>> targets; | |||
| for (QGraphicsItem *item : scene_->selectedItems()) | |||
| { | |||
| if (const EmptySlotItem *slot = dynamic_cast<const EmptySlotItem *>(item)) | |||
| { | |||
| columns.push_back(slot->column()); | |||
| targets.emplace_back(slot->branchExpressionId(), slot->column()); | |||
| } | |||
| } | |||
| return columns; | |||
| return targets; | |||
| } | |||
| std::vector<std::pair<std::string, int>> LogicEditorWidget::selectedWireCells() const | |||
| @@ -1456,6 +1490,18 @@ std::string LogicEditorWidget::selectedRungId() const | |||
| return rung_id; | |||
| } | |||
| bool LogicEditorWidget::hasSelectedRungItem() const | |||
| { | |||
| for (QGraphicsItem *item : scene_->selectedItems()) | |||
| { | |||
| if (dynamic_cast<const RungItem *>(item) != nullptr) | |||
| { | |||
| return true; | |||
| } | |||
| } | |||
| return false; | |||
| } | |||
| LogicEditorResult LogicEditorWidget::addRung() | |||
| { | |||
| const LogicEditorResult result = editor_service_.addRung(logic_id_); | |||
| @@ -1474,15 +1520,16 @@ LogicEditorResult LogicEditorWidget::addRung() | |||
| LogicEditorResult LogicEditorWidget::addCondition(const LogicNodeConfig &config) | |||
| { | |||
| const std::vector<int> selected_empty_columns = selectedEmptySlotColumns(); | |||
| const std::vector<std::pair<std::string, int>> selected_empty_slots = | |||
| selectedEmptySlots(); | |||
| const std::vector<std::pair<std::string, int>> selected_wire_cells = | |||
| selectedWireCells(); | |||
| const std::vector<std::string> selected_expressions = selectedExpressionIds(); | |||
| const std::vector<std::string> selected_wires = selectedWireIds(); | |||
| const std::vector<std::string> selected_ids = selectedNodeIds(); | |||
| LogicEditorResult result; | |||
| if (selected_empty_columns.size() > 1U || selected_wire_cells.size() > 1U | |||
| || (!selected_empty_columns.empty() | |||
| if (selected_empty_slots.size() > 1U || selected_wire_cells.size() > 1U | |||
| || (!selected_empty_slots.empty() | |||
| && (!selected_wire_cells.empty() | |||
| || !selected_expressions.empty() || !selected_ids.empty())) | |||
| || (!selected_wire_cells.empty() | |||
| @@ -1491,13 +1538,14 @@ LogicEditorResult LogicEditorWidget::addCondition(const LogicNodeConfig &config) | |||
| result = {false, LogicEditorError::InvalidOperation, | |||
| "插入条件时只能选择一个网格或条件对象", {}}; | |||
| } | |||
| else if (selected_empty_columns.size() == 1U) | |||
| else if (selected_empty_slots.size() == 1U) | |||
| { | |||
| result = editor_service_.insertConditionAtColumn( | |||
| logic_id_, | |||
| currentRungId(), | |||
| selected_empty_columns.front(), | |||
| config); | |||
| const auto &slot = selected_empty_slots.front(); | |||
| result = slot.first.empty() | |||
| ? editor_service_.insertConditionAtColumn( | |||
| logic_id_, currentRungId(), slot.second, config) | |||
| : editor_service_.insertConditionInBranchAtColumn( | |||
| logic_id_, currentRungId(), slot.first, slot.second, config); | |||
| } | |||
| else if (selected_wire_cells.size() == 1U) | |||
| { | |||
| @@ -1762,20 +1810,20 @@ LogicEditorResult LogicEditorWidget::deleteSelected() | |||
| { | |||
| result = editor_service_.removeNodes(logic_id_, node_ids); | |||
| } | |||
| else | |||
| else if (hasSelectedRungItem()) | |||
| { | |||
| const std::string rung_id = selectedRungId(); | |||
| if (rung_id.empty()) | |||
| { | |||
| return {false, LogicEditorError::InvalidOperation, | |||
| "请先选择要删除的逻辑节点或网络", {}}; | |||
| } | |||
| result = editor_service_.removeRung(logic_id_, rung_id); | |||
| if (result.succeeded && current_rung_id_ == rung_id) | |||
| { | |||
| current_rung_id_ = editor_service_.firstRungId(logic_id_); | |||
| } | |||
| } | |||
| else | |||
| { | |||
| result = {false, LogicEditorError::InvalidOperation, | |||
| "请先选择要删除的逻辑节点或网络", {}}; | |||
| } | |||
| if (result.succeeded) | |||
| { | |||
| reloadLogic(); | |||
| @@ -1,3 +1,11 @@ | |||
| /** | |||
| * @file logic_editor_widget.h | |||
| * @brief 定义结构化梯形图编辑和运行轨迹画布 | |||
| * @version 1.0.0 | |||
| * @author suyu | |||
| * @date 2026-08-22 | |||
| */ | |||
| #pragma once | |||
| #include "domain/control_logic_model.h" | |||
| @@ -13,7 +21,7 @@ | |||
| class QGraphicsScene; | |||
| class QResizeEvent; | |||
| // 将结构化梯形图表达式投影成网格图元;不保存自由线段,编辑通过服务提交 | |||
| /** 将结构化梯形图表达式投影成网格图元;不保存自由线段,编辑通过服务提交 */ | |||
| class LogicEditorWidget final : public QGraphicsView | |||
| { | |||
| Q_OBJECT | |||
| @@ -30,57 +38,97 @@ public: | |||
| LogicEditorService &editor_service, | |||
| QWidget *parent = nullptr); | |||
| /** 切换当前显示的控制逻辑 */ | |||
| void setLogicId(const std::string &logic_id); | |||
| /** 设置是否允许编辑梯形图 */ | |||
| void setEditingEnabled(bool enabled); | |||
| // 设置离线运行轨迹;真机模式应传入空轨迹,避免显示本地伪轨迹 | |||
| /** 设置离线运行轨迹;真机模式应传入空轨迹,避免显示本地伪轨迹 */ | |||
| void setRuntimeTrace( | |||
| const LogicTraceSnapshot &trace, | |||
| const std::string &fault_node_id = {}); | |||
| /** 清除当前运行轨迹和故障节点标记 */ | |||
| void clearRuntimeTrace(); | |||
| /** 根据当前逻辑模型重建梯形图场景 */ | |||
| void reloadLogic(); | |||
| /** 选中指定节点并滚动到可见区域 */ | |||
| void selectNode(const std::string &node_id); | |||
| /** 返回当前选中的单个节点标识 */ | |||
| std::string selectedNodeId() const; | |||
| /** 返回当前选中的多个节点标识 */ | |||
| std::vector<std::string> selectedNodeIds() const; | |||
| /** 返回当前选中的网络标识 */ | |||
| std::string selectedRungId() const; | |||
| /** 新增一个空的梯形图网络 */ | |||
| LogicEditorResult addRung(); | |||
| // 下列编辑命令只负责把当前选择翻译成服务调用 | |||
| /** 将当前选择转换为服务调用并新增串联条件 */ | |||
| LogicEditorResult addCondition(const LogicNodeConfig &config); | |||
| /** 将当前选择转换为服务调用并新增并联支路 */ | |||
| LogicEditorResult addParallelBranch(const LogicNodeConfig &config); | |||
| /** 在当前选择位置新增横线 */ | |||
| LogicEditorResult addHorizontalWire(); | |||
| /** 在当前选择位置新增竖线 */ | |||
| LogicEditorResult addVerticalWire(); | |||
| /** 删除当前选择位置的横线 */ | |||
| LogicEditorResult deleteHorizontalWire(); | |||
| /** 删除当前选择位置的竖线 */ | |||
| LogicEditorResult deleteVerticalWire(); | |||
| /** 设置当前网络的输出线圈 */ | |||
| LogicEditorResult setOutput( | |||
| const LogicNodeConfig &config, bool configured = false); | |||
| /** 删除当前选中的节点、线段或网络 */ | |||
| LogicEditorResult deleteSelected(); | |||
| signals: | |||
| /** 当前选中的节点发生变化时发出 */ | |||
| void nodeSelected(const QString &node_id); | |||
| /** 梯形图模型成功发生变化时发出 */ | |||
| void graphChanged(); | |||
| /** 编辑服务操作失败时发出可显示的错误信息 */ | |||
| void editorError(const QString &message); | |||
| protected: | |||
| /** 在窗口大小变化后重新布局梯形图场景 */ | |||
| void resizeEvent(QResizeEvent *event) override; | |||
| private: | |||
| /** 处理图形场景选择变化 */ | |||
| void handleSelectionChanged(); | |||
| /** 把编辑服务结果转换为错误信号 */ | |||
| void reportFailure(const LogicEditorResult &result); | |||
| /** 返回当前选中的网络标识 */ | |||
| std::string currentRungId() const; | |||
| /** 选中指定表达式图元 */ | |||
| void selectExpression(const std::string &expression_id); | |||
| /** 选中指定横线图元 */ | |||
| void selectWire(const std::string &wire_id); | |||
| /** 收集当前选中的表达式标识 */ | |||
| std::vector<std::string> selectedExpressionIds() const; | |||
| /** 收集当前选中的横线标识 */ | |||
| std::vector<std::string> selectedWireIds() const; | |||
| /** 收集当前选中的并联支路标识 */ | |||
| std::vector<std::string> selectedBranchIds() const; | |||
| std::vector<int> selectedEmptySlotColumns() const; | |||
| /** 收集当前选中的空网格位置 */ | |||
| std::vector<std::pair<std::string, int>> selectedEmptySlots() const; | |||
| /** 收集当前选中的横线网格位置 */ | |||
| std::vector<std::pair<std::string, int>> selectedWireCells() const; | |||
| /** 判断当前是否选中了网络内的图元 */ | |||
| bool hasSelectedRungItem() const; | |||
| /** 梯形图编辑服务,不由控件拥有 */ | |||
| LogicEditorService &editor_service_; | |||
| /** 承载梯形图图元的场景 */ | |||
| QGraphicsScene *scene_ = nullptr; | |||
| /** 当前显示的控制逻辑标识 */ | |||
| std::string logic_id_; | |||
| /** 当前选中的网络标识 */ | |||
| std::string current_rung_id_; | |||
| /** 最近一次收到的运行轨迹 */ | |||
| LogicTraceSnapshot trace_; | |||
| /** 最近一次运行故障节点标识 */ | |||
| std::string fault_node_id_; | |||
| /** 是否显示运行轨迹 */ | |||
| bool runtime_trace_enabled_ = false; | |||
| /** 是否允许编辑梯形图 */ | |||
| bool editing_enabled_ = true; | |||
| }; | |||
| @@ -1,3 +1,11 @@ | |||
| /** | |||
| * @file logic_instruction_dialog.h | |||
| * @brief 定义梯形图指令参数编辑对话框 | |||
| * @version 1.0.0 | |||
| * @author suyu | |||
| * @date 2026-08-22 | |||
| */ | |||
| #pragma once | |||
| #include "domain/control_logic_model.h" | |||
| @@ -13,7 +21,7 @@ namespace Ui { | |||
| class LogicInstructionDialog; | |||
| } | |||
| // 梯形图节点配置对话框;只负责把表单转换为强类型 LogicNodeConfig | |||
| /** 梯形图节点配置对话框;只负责把表单转换为强类型 LogicNodeConfig */ | |||
| class LogicInstructionDialog final : public QDialog | |||
| { | |||
| Q_OBJECT | |||
| @@ -24,22 +32,30 @@ public: | |||
| QWidget *parent = nullptr); | |||
| ~LogicInstructionDialog() override; | |||
| // 返回当前表单配置,调用方仍需交给 LogicEditorService 校验 | |||
| /** 返回当前表单配置,调用方仍需交给 LogicEditorService 校验 */ | |||
| LogicNodeConfig config() const; | |||
| private: | |||
| /** 根据计数器配置初始化计数器输入项 */ | |||
| void configureCounter(const CounterNodeConfig &config); | |||
| /** 根据传送配置初始化传送输入项 */ | |||
| void configureMove(const MoveNodeConfig &config); | |||
| /** 根据算术配置初始化运算输入项 */ | |||
| void configureArithmetic(const ArithmeticNodeConfig &config); | |||
| /** 根据操作数类型更新数值输入框的合法范围 */ | |||
| void updateOperandRanges(); | |||
| /** 将领域操作数写入对应的下拉框和数值框 */ | |||
| static void setOperand( | |||
| QComboBox *kind_combo, | |||
| QSpinBox *value_spin, | |||
| const WordOperand &operand); | |||
| /** 从下拉框和数值框读取领域操作数 */ | |||
| static WordOperand operandFrom( | |||
| const QComboBox *kind_combo, | |||
| const QSpinBox *value_spin); | |||
| /** Qt Designer 生成的界面对象 */ | |||
| std::unique_ptr<Ui::LogicInstructionDialog> ui_; | |||
| /** 打开对话框时保存的原始节点配置 */ | |||
| LogicNodeConfig original_config_; | |||
| }; | |||
| @@ -91,78 +91,105 @@ public: | |||
| ~MainWindow() override; | |||
| /** 返回当前选中的 HMI 页面标识 */ | |||
| const std::string ¤tHmiPageId() const; | |||
| /** 返回当前选中的控制逻辑标识 */ | |||
| const std::string ¤tLogicId() const; | |||
| protected: | |||
| /** 处理主窗口及子控件的事件过滤请求 */ | |||
| bool eventFilter(QObject *watched, QEvent *event) override; | |||
| /** 关闭窗口前处理保存确认和运行态退出 */ | |||
| void closeEvent(QCloseEvent *event) override; | |||
| private: | |||
| // 配置菜单和工具栏动作 | |||
| /** 配置菜单和工具栏动作 */ | |||
| void configureActions(); | |||
| // 配置主窗口外观和状态栏 | |||
| /** 配置主窗口外观和状态栏 */ | |||
| void configureAppearance(); | |||
| // 创建并连接 HMI 编辑画布 | |||
| /** 创建并连接 HMI 编辑画布 */ | |||
| void configureHmiEditor(); | |||
| // 创建并连接控制逻辑编辑画布 | |||
| /** 创建并连接控制逻辑编辑画布 */ | |||
| void configureLogicEditor(); | |||
| /** 创建并连接运行监控面板 */ | |||
| void configureRuntimeMonitor(); | |||
| /** 创建并连接工程树 */ | |||
| void configureProjectTree(); | |||
| // 创建并连接控件属性编辑表单 | |||
| /** 创建并连接控件属性编辑表单 */ | |||
| void configurePropertyEditor(); | |||
| /** 创建并连接 PLC 串口配置入口 */ | |||
| void configurePlcConnection(); | |||
| /** 创建并连接报警配置入口 */ | |||
| void configureAlarms(); | |||
| /** 创建并连接寄存器注释入口 */ | |||
| void configureRegisterComments(); | |||
| /** 撤销当前处于焦点的编辑器操作 */ | |||
| void undoActiveEditor(); | |||
| /** 重做当前处于焦点的编辑器操作 */ | |||
| void redoActiveEditor(); | |||
| /** 删除当前处于焦点的编辑器选中项 */ | |||
| void deleteActiveSelection(); | |||
| /** 清除当前处于焦点的编辑器选择 */ | |||
| void clearActiveSelection(); | |||
| /** 根据编辑器选择状态更新撤销、重做和删除动作 */ | |||
| void updateEditActions(); | |||
| /** 清空 HMI 和梯形图编辑器的撤销历史 */ | |||
| void clearEditorHistories(); | |||
| /** 根据工程路径和修改状态刷新窗口标题 */ | |||
| void updateWindowTitle(); | |||
| /** 对可能破坏当前工程的操作请求保存确认 */ | |||
| bool confirmSaveBeforeDestructiveAction(); | |||
| // 刷新工程树和当前 HMI 页面信息 | |||
| /** 刷新工程树和当前 HMI 页面信息 */ | |||
| void refreshProjectUi(); | |||
| /** 更新工程树相关动作的可用状态 */ | |||
| void updateProjectTreeActions(); | |||
| // 显示指定 HMI 控件的可编辑属性 | |||
| /** 显示指定 HMI 控件的可编辑属性 */ | |||
| void showControlProperties(const std::string &control_id); | |||
| // 显示指定控制逻辑节点的可编辑属性 | |||
| /** 显示指定控制逻辑节点的可编辑属性 */ | |||
| void showLogicNodeProperties(const std::string &node_id); | |||
| // 向当前页面添加指定类型的 HMI 控件 | |||
| /** 向当前页面添加指定类型的 HMI 控件 */ | |||
| void addHmiControl(HmiControlType type); | |||
| // 删除当前选中的 HMI 控件 | |||
| /** 删除当前选中的 HMI 控件 */ | |||
| void deleteSelectedControl(); | |||
| // 在当前光标节点后添加串联条件,没有选择时追加到网络末尾 | |||
| /** 在当前光标节点后添加串联条件,没有选择时追加到网络末尾 */ | |||
| void addLogicCondition(const LogicNodeConfig &config); | |||
| // 为当前选中的连续逻辑范围建立并联支路 | |||
| /** 为当前选中的连续逻辑范围建立并联支路 */ | |||
| void addLogicParallelBranch(const LogicNodeConfig &config); | |||
| /** 在当前梯形图位置添加横线 */ | |||
| void addLogicHorizontalWire(); | |||
| /** 在当前梯形图位置添加竖线 */ | |||
| void addLogicVerticalWire(); | |||
| /** 删除当前梯形图位置的横线 */ | |||
| void deleteLogicHorizontalWire(); | |||
| /** 删除当前梯形图位置的竖线 */ | |||
| void deleteLogicVerticalWire(); | |||
| // 设置当前网络右侧的输出线圈 | |||
| /** 设置当前网络右侧的输出线圈 */ | |||
| void setLogicOutput(const LogicNodeConfig &config); | |||
| /** 打开输出指令配置并提交输出线圈 */ | |||
| void configureAndSetLogicOutput(const LogicNodeConfig &config); | |||
| // 新增一个梯形图网络 | |||
| /** 新增一个梯形图网络 */ | |||
| void addLogicRung(); | |||
| /** 编辑当前网络的注释 */ | |||
| void editSelectedRungComment(); | |||
| // 删除当前选中的逻辑节点或网络 | |||
| /** 删除当前选中的逻辑节点或网络 */ | |||
| void deleteSelectedLogicObject(); | |||
| /** 打开 PLC 连接 */ | |||
| void connectPlc(); | |||
| /** 关闭 PLC 连接 */ | |||
| void disconnectPlc(); | |||
| // 合并同一事件循环内的 PLC 状态通知,避免重复刷新和重复日志 | |||
| /** 合并同一事件循环内的 PLC 状态通知,避免重复刷新和重复日志 */ | |||
| void schedulePlcStatusUpdate(); | |||
| // 创建新的工程并刷新编辑界面 | |||
| /** 创建新的工程并刷新编辑界面 */ | |||
| void createNewProject(); | |||
| // 保存当前工程到已关联的路径 | |||
| /** 保存当前工程到已关联的路径 */ | |||
| void saveProject(); | |||
| // 将当前工程保存到用户指定的路径 | |||
| /** 将当前工程保存到用户指定的路径 */ | |||
| void saveProjectAs(); | |||
| // 从用户指定的路径加载工程 | |||
| /** 从用户指定的路径加载工程 */ | |||
| void loadProject(); | |||
| // 在状态栏和输出面板显示工程操作结果 | |||
| /** 在状态栏和输出面板显示工程操作结果 */ | |||
| void showProjectResult(const QString &action, const QString &message, bool succeeded); | |||
| /** 向输出面板追加一条消息 */ | |||
| void appendOutputMessage(const QString &message); | |||
| /** | |||
| @@ -178,15 +205,17 @@ private: | |||
| * @param message 要显示在状态栏和输出区的模式结果消息 | |||
| */ | |||
| void updateModeUi(const QString &message); | |||
| // 根据仿真服务实际状态刷新执行器反馈和 HMI 写权限 | |||
| /** 根据仿真服务实际状态刷新执行器反馈和 HMI 写权限 */ | |||
| void updateSimulationUi(bool report_fault); | |||
| /** | |||
| * @brief 将互斥模式动作同步到服务层当前状态 | |||
| */ | |||
| void restoreCurrentModeAction(); | |||
| /** 完成主窗口初始化和首次界面刷新 */ | |||
| void initializeUi(); | |||
| /** Qt Designer 生成的主窗口界面对象 */ | |||
| std::unique_ptr<Ui::MainWindow> ui_; | |||
| /** | |||
| * @brief 非拥有的运行模式服务依赖,由应用入口保证生命周期 | |||
| @@ -196,29 +225,54 @@ private: | |||
| * @brief 非拥有的工程和 HMI 服务依赖,由应用入口保证生命周期 | |||
| */ | |||
| ProjectService &project_service_; | |||
| /** 非拥有的 HMI 编辑服务依赖 */ | |||
| HmiEditorService &hmi_editor_service_; | |||
| /** 非拥有的梯形图编辑服务依赖 */ | |||
| LogicEditorService &logic_editor_service_; | |||
| /** 非拥有的 HMI 运行服务依赖 */ | |||
| HmiRuntimeService &hmi_runtime_service_; | |||
| /** 非拥有的报警编辑服务依赖 */ | |||
| AlarmEditorService &alarm_editor_service_; | |||
| /** 非拥有的报警查询服务依赖 */ | |||
| AlarmService &alarm_service_; | |||
| /** 非拥有的寄存器注释服务依赖 */ | |||
| RegisterCommentService ®ister_comment_service_; | |||
| /** 非拥有的寄存器监控服务依赖 */ | |||
| RegisterMonitorService ®ister_monitor_service_; | |||
| /** 当调用方未提供导航服务时由主窗口负责拥有的实例 */ | |||
| std::unique_ptr<HmiNavigationService> owned_hmi_navigation_service_; | |||
| /** 当前使用的 HMI 页面导航服务 */ | |||
| HmiNavigationService *hmi_navigation_service_ = nullptr; | |||
| /** 工程树控制器 */ | |||
| std::unique_ptr<ProjectWorkspaceController> project_workspace_controller_; | |||
| /** 属性面板控制器 */ | |||
| std::unique_ptr<PropertyPanelController> property_panel_controller_; | |||
| /** 运行监控面板控制器 */ | |||
| std::unique_ptr<RuntimePanelController> runtime_panel_controller_; | |||
| /** 运行模式互斥动作组 */ | |||
| QActionGroup *mode_action_group_ = nullptr; | |||
| /** 主界面的 HMI 编辑器 */ | |||
| HmiEditorWidget *hmi_editor_widget_ = nullptr; | |||
| /** 主界面的梯形图编辑器 */ | |||
| LogicEditorWidget *logic_editor_widget_ = nullptr; | |||
| /** 唯一的运行监控控件 */ | |||
| RuntimeMonitorWidget *runtime_monitor_widget_ = nullptr; | |||
| /** 显示当前运行模式的状态标签 */ | |||
| QLabel *mode_status_label_ = nullptr; | |||
| /** 显示 PLC 连接状态的标签 */ | |||
| QLabel *register_status_label_ = nullptr; | |||
| /** 显示离线执行器状态的标签 */ | |||
| QLabel *executor_status_label_ = nullptr; | |||
| /** 当前保存的 PLC 串口配置 */ | |||
| PlcSerialConfiguration plc_configuration_; | |||
| /** 当前选中的 HMI 控件标识 */ | |||
| std::string selected_control_id_; | |||
| /** 当前选中的梯形图节点标识 */ | |||
| std::string selected_logic_node_id_; | |||
| /** 当前选中的 HMI 页面标识 */ | |||
| std::string current_hmi_page_id_; | |||
| /** 当前选中的控制逻辑标识 */ | |||
| std::string current_logic_id_; | |||
| /** 是否已经安排了待处理的 PLC 状态刷新 */ | |||
| bool plc_status_update_pending_ = false; | |||
| }; | |||
| @@ -1,3 +1,11 @@ | |||
| /** | |||
| * @file plc_connection_dialog.h | |||
| * @brief 定义 PLC 串口连接参数对话框 | |||
| * @version 1.0.0 | |||
| * @author suyu | |||
| * @date 2026-08-22 | |||
| */ | |||
| #pragma once | |||
| #include "services/plc_communication_gateway.h" | |||
| @@ -10,7 +18,7 @@ namespace Ui { | |||
| class PlcConnectionDialog; | |||
| } | |||
| // PLC 串口参数对话框;只编辑配置,不负责打开串口 | |||
| /** PLC 串口参数对话框;只编辑配置,不负责打开串口 */ | |||
| class PlcConnectionDialog final : public QDialog | |||
| { | |||
| Q_OBJECT | |||
| @@ -21,9 +29,10 @@ public: | |||
| QWidget *parent = nullptr); | |||
| ~PlcConnectionDialog() override; | |||
| // 返回表单中的串口配置,连接前由网关再次校验 | |||
| /** 返回表单中的串口配置,连接前由网关再次校验 */ | |||
| PlcSerialConfiguration configuration() const; | |||
| private: | |||
| /** Qt Designer 生成的界面对象 */ | |||
| std::unique_ptr<Ui::PlcConnectionDialog> ui_; | |||
| }; | |||
| @@ -1,3 +1,11 @@ | |||
| /** | |||
| * @file project_workspace_controller.h | |||
| * @brief 定义工程树和当前编辑对象控制器 | |||
| * @version 1.0.0 | |||
| * @author suyu | |||
| * @date 2026-08-22 | |||
| */ | |||
| #pragma once | |||
| #include "domain/hmi_model.h" | |||
| @@ -23,15 +31,21 @@ namespace Ui { | |||
| class MainWindow; | |||
| } | |||
| // 组织工程树、当前页面/逻辑选择和工程对象的界面刷新 | |||
| // 它不直接改 Project,所有增删改都转发给对应服务 | |||
| /** | |||
| * 组织工程树、当前页面/逻辑选择和工程对象的界面刷新 | |||
| * | |||
| * 它不直接改 Project,所有增删改都转发给对应服务 | |||
| */ | |||
| class ProjectWorkspaceController final | |||
| { | |||
| public: | |||
| /** 报告工程操作结果,参数依次为操作名、消息和是否成功 */ | |||
| using ResultReporter = std::function<void( | |||
| const QString &action, const QString &message, bool succeeded)>; | |||
| /** 向状态栏报告一条带超时的消息 */ | |||
| using StatusReporter = std::function<void( | |||
| const QString &message, int timeout_ms)>; | |||
| /** 通知主窗口编辑状态已经发生变化 */ | |||
| using EditStateChanged = std::function<void()>; | |||
| ProjectWorkspaceController( | |||
| @@ -53,21 +67,34 @@ public: | |||
| StatusReporter status_reporter, | |||
| EditStateChanged edit_state_changed); | |||
| /** 连接工程树动作和选择变化信号 */ | |||
| void configure(); | |||
| /** 根据当前工程重新生成工程树和编辑器内容 */ | |||
| void refresh(); | |||
| /** 根据当前选中对象更新工程树动作的可用状态 */ | |||
| void updateActions(); | |||
| /** 新增一个 HMI 页面 */ | |||
| void addPage(); | |||
| /** 新增一个控制逻辑 */ | |||
| void addLogic(); | |||
| /** 重命名当前选中的工程树项目 */ | |||
| void renameSelectedItem(); | |||
| /** 删除当前选中的工程树项目 */ | |||
| void deleteSelectedItem(); | |||
| /** 按偏移量移动当前选中的工程树项目 */ | |||
| void moveSelectedItem(int offset); | |||
| /** 把当前选中的 HMI 页面设为初始页面 */ | |||
| void setSelectedPageAsInitial(); | |||
| /** 启用或停用当前选中的控制逻辑 */ | |||
| void toggleSelectedLogicEnabled(); | |||
| /** 返回当前 HMI 页面标识 */ | |||
| const std::string ¤tHmiPageId() const; | |||
| /** 返回当前控制逻辑标识 */ | |||
| const std::string ¤tLogicId() const; | |||
| private: | |||
| /** 工程树项目的三种业务类型 */ | |||
| enum class ItemKind | |||
| { | |||
| Root = 0, | |||
| @@ -75,34 +102,61 @@ private: | |||
| ControlLogic = 2 | |||
| }; | |||
| /** 处理工程树选择变化并刷新当前编辑器 */ | |||
| void handleSelectionChanged(); | |||
| /** 统一报告工程操作失败 */ | |||
| void reportFailure( | |||
| const QString &action, const std::string &message) const; | |||
| /** 主窗口父对象,不由控制器拥有 */ | |||
| QWidget &parent_; | |||
| /** 主窗口的 Designer 界面对象 */ | |||
| Ui::MainWindow &ui_; | |||
| /** 工程服务,不由控制器拥有 */ | |||
| ProjectService &project_service_; | |||
| /** HMI 编辑服务,不由控制器拥有 */ | |||
| HmiEditorService &hmi_editor_service_; | |||
| /** 梯形图编辑服务,不由控制器拥有 */ | |||
| LogicEditorService &logic_editor_service_; | |||
| /** 运行模式服务,不由控制器拥有 */ | |||
| RuntimeModeService &runtime_mode_service_; | |||
| /** HMI 页面导航服务,不由控制器拥有 */ | |||
| HmiNavigationService &hmi_navigation_service_; | |||
| /** 主窗口中的 HMI 编辑器 */ | |||
| HmiEditorWidget &hmi_editor_widget_; | |||
| /** 主窗口中的梯形图编辑器 */ | |||
| LogicEditorWidget &logic_editor_widget_; | |||
| /** 运行监控控件 */ | |||
| RuntimeMonitorWidget &runtime_monitor_widget_; | |||
| /** 主窗口保存的当前 HMI 页面标识 */ | |||
| std::string ¤t_hmi_page_id_; | |||
| /** 主窗口保存的当前控制逻辑标识 */ | |||
| std::string ¤t_logic_id_; | |||
| /** 显示 HMI 控件属性的回调 */ | |||
| std::function<void(const std::string &)> show_control_properties_; | |||
| /** 显示梯形图节点属性的回调 */ | |||
| std::function<void(const std::string &)> show_logic_properties_; | |||
| /** 工程操作结果回调 */ | |||
| ResultReporter result_reporter_; | |||
| /** 状态栏消息回调 */ | |||
| StatusReporter status_reporter_; | |||
| /** 编辑状态变化回调 */ | |||
| EditStateChanged edit_state_changed_; | |||
| /** 新增页面动作 */ | |||
| QAction *add_page_action_ = nullptr; | |||
| /** 新增逻辑动作 */ | |||
| QAction *add_logic_action_ = nullptr; | |||
| /** 重命名动作 */ | |||
| QAction *rename_item_action_ = nullptr; | |||
| /** 删除工程树项目动作 */ | |||
| QAction *delete_item_action_ = nullptr; | |||
| /** 上移工程树项目动作 */ | |||
| QAction *move_item_up_action_ = nullptr; | |||
| /** 下移工程树项目动作 */ | |||
| QAction *move_item_down_action_ = nullptr; | |||
| /** 设置初始页面动作 */ | |||
| QAction *set_initial_page_action_ = nullptr; | |||
| /** 启停控制逻辑动作 */ | |||
| QAction *toggle_logic_enabled_action_ = nullptr; | |||
| }; | |||
| @@ -1,3 +1,11 @@ | |||
| /** | |||
| * @file property_panel_controller.h | |||
| * @brief 定义 HMI 和梯形图属性面板控制器 | |||
| * @version 1.0.0 | |||
| * @author suyu | |||
| * @date 2026-08-22 | |||
| */ | |||
| #pragma once | |||
| #include "domain/control_logic_model.h" | |||
| @@ -19,13 +27,18 @@ namespace Ui { | |||
| class MainWindow; | |||
| } | |||
| // 组织 HMI/梯形图选择状态、属性面板和编辑器信号 | |||
| // 属性提交统一经过编辑服务,以便校验、原子回滚和撤销/重做 | |||
| /** | |||
| * 组织 HMI/梯形图选择状态、属性面板和编辑器信号 | |||
| * | |||
| * 属性提交统一经过编辑服务,以便校验、原子回滚和撤销/重做 | |||
| */ | |||
| class PropertyPanelController final | |||
| { | |||
| public: | |||
| /** 报告属性操作结果,参数依次为操作名、消息和是否成功 */ | |||
| using ResultReporter = std::function<void( | |||
| const QString &action, const QString &message, bool succeeded)>; | |||
| /** 向状态栏报告一条带超时的消息 */ | |||
| using StatusReporter = std::function<void( | |||
| const QString &message, int timeout_ms)>; | |||
| @@ -43,39 +56,67 @@ public: | |||
| ResultReporter result_reporter, | |||
| StatusReporter status_reporter); | |||
| /** 连接属性面板按钮、输入框和服务信号 */ | |||
| void configure(); | |||
| /** 绑定 HMI 与梯形图编辑器,接收它们的选择变化 */ | |||
| void bindEditorWidgets( | |||
| HmiEditorWidget &hmi_editor_widget, | |||
| LogicEditorWidget &logic_editor_widget); | |||
| /** 显示指定 HMI 控件的属性 */ | |||
| void showControlProperties(const std::string &control_id); | |||
| /** 显示指定 HMI 页面尺寸属性 */ | |||
| void showPageProperties(const std::string &page_id); | |||
| /** 显示指定梯形图节点的属性 */ | |||
| void showLogicNodeProperties(const std::string &node_id); | |||
| /** 在当前页面添加指定类型的 HMI 控件 */ | |||
| void addHmiControl(HmiControlType type); | |||
| /** 删除当前选中的 HMI 控件 */ | |||
| void deleteSelectedControl(); | |||
| /** 提交当前 HMI 控件属性 */ | |||
| void applySelectedControlProperties(); | |||
| /** 提交当前 HMI 页面属性 */ | |||
| void applySelectedPageProperties(); | |||
| /** 提交当前梯形图节点属性 */ | |||
| void applySelectedLogicNodeProperties(); | |||
| private: | |||
| /** 显示 HMI 编辑服务返回的错误 */ | |||
| void handleHmiEditorError(const QString &message) const; | |||
| /** 显示梯形图编辑服务返回的错误 */ | |||
| void handleLogicEditorError(const QString &message) const; | |||
| /** 打开颜色选择器并写回属性输入框 */ | |||
| void chooseTextColor(); | |||
| /** 统一报告属性操作失败 */ | |||
| void reportFailure( | |||
| const QString &action, const std::string &message) const; | |||
| /** 主窗口父对象,不由控制器拥有 */ | |||
| QWidget &parent_; | |||
| /** 主窗口的 Designer 界面对象 */ | |||
| Ui::MainWindow &ui_; | |||
| /** 工程服务,不由控制器拥有 */ | |||
| ProjectService &project_service_; | |||
| /** HMI 编辑服务,不由控制器拥有 */ | |||
| HmiEditorService &hmi_editor_service_; | |||
| /** 梯形图编辑服务,不由控制器拥有 */ | |||
| LogicEditorService &logic_editor_service_; | |||
| /** 获取当前 HMI 页面标识的回调 */ | |||
| std::function<std::string()> current_page_id_; | |||
| /** 获取当前控制逻辑标识的回调 */ | |||
| std::function<std::string()> current_logic_id_; | |||
| /** 主窗口保存的当前控件标识 */ | |||
| std::string &selected_control_id_; | |||
| /** 主窗口保存的当前逻辑节点标识 */ | |||
| std::string &selected_logic_node_id_; | |||
| /** 请求主窗口刷新工程界面的回调 */ | |||
| std::function<void()> refresh_project_ui_; | |||
| /** 属性操作结果回调 */ | |||
| ResultReporter result_reporter_; | |||
| /** 状态栏消息回调 */ | |||
| StatusReporter status_reporter_; | |||
| /** 绑定后的 HMI 编辑器 */ | |||
| HmiEditorWidget *hmi_editor_widget_ = nullptr; | |||
| /** 绑定后的梯形图编辑器 */ | |||
| LogicEditorWidget *logic_editor_widget_ = nullptr; | |||
| }; | |||
| @@ -1,3 +1,11 @@ | |||
| /** | |||
| * @file register_comment_dialog.h | |||
| * @brief 定义 M/D 寄存器工程注释编辑对话框 | |||
| * @version 1.0.0 | |||
| * @author suyu | |||
| * @date 2026-08-22 | |||
| */ | |||
| #pragma once | |||
| #include "domain/register_address.h" | |||
| @@ -13,7 +21,7 @@ class RegisterCommentDialog; | |||
| class RegisterCommentService; | |||
| // 工程级 M/D 注释编辑对话框;注释不包含实时寄存器值 | |||
| /** 工程级 M/D 注释编辑对话框;注释不包含实时寄存器值 */ | |||
| class RegisterCommentDialog final : public QDialog | |||
| { | |||
| public: | |||
| @@ -23,13 +31,21 @@ public: | |||
| ~RegisterCommentDialog() override; | |||
| private: | |||
| /** 重新加载注释列表,并尽量保持原选中地址 */ | |||
| void reloadComments(const RegisterAddress *selected_address = nullptr); | |||
| /** 把当前选中地址的注释加载到编辑框 */ | |||
| void loadSelectedComment(); | |||
| /** 校验并保存当前地址的注释 */ | |||
| void saveComment(); | |||
| /** 删除当前选中地址的注释 */ | |||
| void removeComment(); | |||
| /** 从输入框读取寄存器地址 */ | |||
| RegisterAddress addressFromInputs() const; | |||
| /** 返回列表中当前选中的寄存器地址,没有选择时返回空值 */ | |||
| std::optional<RegisterAddress> selectedAddress() const; | |||
| /** Qt Designer 生成的界面对象 */ | |||
| std::unique_ptr<Ui::RegisterCommentDialog> ui_; | |||
| /** 注释读写服务,不由对话框拥有 */ | |||
| RegisterCommentService &service_; | |||
| }; | |||
| @@ -1,3 +1,11 @@ | |||
| /** | |||
| * @file runtime_monitor_widget.h | |||
| * @brief 定义工程师运行监控主控件 | |||
| * @version 1.0.0 | |||
| * @author suyu | |||
| * @date 2026-08-22 | |||
| */ | |||
| #pragma once | |||
| #include "domain/runtime_state.h" | |||
| @@ -24,7 +32,7 @@ class LogicEditorWidget; | |||
| class RegisterMonitorService; | |||
| class ProjectService; | |||
| // 唯一的工程师运行监控投影,组合 HMI、梯形图轨迹和自由监控 | |||
| /** 唯一的工程师运行监控投影,组合 HMI、梯形图轨迹和自由监控 */ | |||
| class RuntimeMonitorWidget final : public QWidget | |||
| { | |||
| Q_OBJECT | |||
| @@ -41,34 +49,55 @@ public: | |||
| QWidget *parent = nullptr); | |||
| ~RuntimeMonitorWidget() override; | |||
| /** 将当前页面和控制逻辑绑定到运行监控界面 */ | |||
| void setProjectObjects(const std::string &page_id, const std::string &logic_id); | |||
| /** 切换运行监控中显示的 HMI 页面 */ | |||
| void setRuntimePage(const std::string &page_id); | |||
| // 根据模式决定是否显示本地逻辑轨迹以及是否允许写入 | |||
| /** 根据模式决定是否显示本地逻辑轨迹以及是否允许写入 */ | |||
| void setMode(ApplicationMode mode, PlcConnectionState plc_state); | |||
| /** 从寄存器仓库刷新 HMI 和自由监控中的当前值 */ | |||
| void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); | |||
| /** 设置 HMI 控件是否允许写入运行值 */ | |||
| 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; | |||
| signals: | |||
| /** 页面跳转失败时发出可直接显示的错误信息 */ | |||
| void navigationFailed(const QString &message); | |||
| /** 用户请求退出运行态 */ | |||
| void exitRequested(); | |||
| private: | |||
| /** 显示指定的运行监控页面 */ | |||
| void showRuntimePage(const std::string &page_id); | |||
| /** 选中指定控制逻辑并刷新梯形图投影 */ | |||
| void selectRuntimeLogic(const std::string &logic_id); | |||
| /** Qt Designer 生成的界面对象 */ | |||
| std::unique_ptr<Ui::RuntimeMonitorWidget> ui_; | |||
| /** 工程服务,不由控件拥有 */ | |||
| ProjectService &project_service_; | |||
| /** HMI 页面导航服务,不由控件拥有 */ | |||
| HmiNavigationService &hmi_navigation_service_; | |||
| /** 运行监控中的 HMI 视图 */ | |||
| HmiEditorWidget *hmi_view_ = nullptr; | |||
| /** 运行监控中的梯形图视图 */ | |||
| LogicEditorWidget *logic_view_ = nullptr; | |||
| /** 运行监控中的自由监控控件 */ | |||
| FreeMonitorWidget *free_monitor_widget_ = nullptr; | |||
| /** 当前选中的控制逻辑标识 */ | |||
| std::string selected_logic_id_; | |||
| /** 最近一次收到的离线逻辑执行轨迹 */ | |||
| LogicTraceSnapshot latest_trace_; | |||
| /** 最近一次离线执行故障节点标识 */ | |||
| std::string latest_fault_node_id_; | |||
| /** 是否已经收到过可显示的逻辑轨迹 */ | |||
| bool has_logic_trace_ = false; | |||
| }; | |||
| @@ -1,3 +1,11 @@ | |||
| /** | |||
| * @file runtime_monitor_window.h | |||
| * @brief 定义独立的工程师运行监控窗口 | |||
| * @version 1.0.0 | |||
| * @author suyu | |||
| * @date 2026-08-22 | |||
| */ | |||
| #pragma once | |||
| #include <QMainWindow> | |||
| @@ -11,7 +19,7 @@ class RuntimeMonitorWindow; | |||
| class QCloseEvent; | |||
| class QWidget; | |||
| // 承载唯一工程师运行监控投影的独立顶层窗口 | |||
| /** 承载唯一工程师运行监控投影的独立顶层窗口 */ | |||
| class RuntimeMonitorWindow final : public QMainWindow | |||
| { | |||
| Q_OBJECT | |||
| @@ -20,22 +28,28 @@ public: | |||
| explicit RuntimeMonitorWindow(QWidget *parent = nullptr); | |||
| ~RuntimeMonitorWindow() override; | |||
| /** 将运行监控控件放入窗口中央区域 */ | |||
| void setMonitorWidget(QWidget &widget); | |||
| // 进入运行态时显示并最大化窗口 | |||
| /** 进入运行态时显示并最大化窗口 */ | |||
| void showForRuntime(); | |||
| // 返回编辑态时隐藏窗口但保留唯一监控控件 | |||
| /** 返回编辑态时隐藏窗口但保留唯一监控控件 */ | |||
| void hideForEditing(); | |||
| // 应用退出时允许真正关闭窗口 | |||
| /** 应用退出时允许真正关闭窗口 */ | |||
| void closeForApplicationExit(); | |||
| signals: | |||
| /** 用户点击窗口关闭按钮后请求退出运行态 */ | |||
| void exitRequested(); | |||
| protected: | |||
| /** 根据当前生命周期决定关闭是隐藏还是销毁窗口 */ | |||
| void closeEvent(QCloseEvent *event) override; | |||
| private: | |||
| /** Qt Designer 生成的窗口界面对象 */ | |||
| std::unique_ptr<Ui::RuntimeMonitorWindow> ui_; | |||
| /** 当前是否处于运行态显示阶段 */ | |||
| bool runtime_active_ = false; | |||
| /** 应用是否正在退出,决定关闭事件是否真正销毁窗口 */ | |||
| bool application_exit_ = false; | |||
| }; | |||
| @@ -1,3 +1,11 @@ | |||
| /** | |||
| * @file runtime_panel_controller.h | |||
| * @brief 定义运行监控面板控制器 | |||
| * @version 1.0.0 | |||
| * @author suyu | |||
| * @date 2026-08-22 | |||
| */ | |||
| #pragma once | |||
| #include "domain/runtime_state.h" | |||
| @@ -25,13 +33,16 @@ class RuntimeMonitorWindow; | |||
| class QTimer; | |||
| class QWidget; | |||
| // 组织运行监控工作台、运行刷新定时器和离线执行器反馈 | |||
| /** 组织运行监控窗口、刷新定时器和离线执行器反馈 */ | |||
| class RuntimePanelController final | |||
| { | |||
| public: | |||
| /** 向状态栏报告一条带超时的消息 */ | |||
| using StatusReporter = std::function<void( | |||
| const QString &message, int timeout_ms)>; | |||
| /** 向输出面板报告一条消息 */ | |||
| using OutputReporter = std::function<void(const QString &message)>; | |||
| /** 请求上层退出当前运行态 */ | |||
| using RuntimeExitRequester = std::function<void()>; | |||
| RuntimePanelController( | |||
| @@ -55,44 +66,73 @@ public: | |||
| RuntimeExitRequester runtime_exit_requester); | |||
| ~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); | |||
| /** 返回当前唯一的运行监控控件 */ | |||
| RuntimeMonitorWidget *runtimeMonitorWidget() const; | |||
| private: | |||
| /** 处理运行刷新定时器到期事件 */ | |||
| void handleRuntimeTimer(); | |||
| /** 处理离线执行器状态变化 */ | |||
| void handleSimulationStateChanged(); | |||
| /** 处理一次离线扫描完成事件 */ | |||
| void handleScanCompleted(); | |||
| /** 主窗口父对象,不由控制器拥有 */ | |||
| QWidget &parent_; | |||
| /** 运行模式服务,不由控制器拥有 */ | |||
| RuntimeModeService &runtime_mode_service_; | |||
| /** 工程服务,不由控制器拥有 */ | |||
| ProjectService &project_service_; | |||
| /** HMI 编辑服务,不由控制器拥有 */ | |||
| HmiEditorService &hmi_editor_service_; | |||
| /** HMI 运行服务,不由控制器拥有 */ | |||
| HmiRuntimeService &hmi_runtime_service_; | |||
| /** 梯形图编辑服务,不由控制器拥有 */ | |||
| LogicEditorService &logic_editor_service_; | |||
| /** HMI 页面导航服务,不由控制器拥有 */ | |||
| HmiNavigationService &hmi_navigation_service_; | |||
| /** 报警服务,不由控制器拥有 */ | |||
| AlarmService &alarm_service_; | |||
| /** 寄存器监控服务,不由控制器拥有 */ | |||
| RegisterMonitorService ®ister_monitor_service_; | |||
| /** 主窗口中的 HMI 编辑器,不由控制器拥有 */ | |||
| HmiEditorWidget &hmi_editor_widget_; | |||
| /** 主窗口中的梯形图编辑器,不由控制器拥有 */ | |||
| LogicEditorWidget &logic_editor_widget_; | |||
| /** 显示离线执行器状态的标签 */ | |||
| QLabel &executor_status_label_; | |||
| /** 获取当前控制逻辑标识的回调 */ | |||
| std::function<std::string()> current_logic_id_; | |||
| /** 切换当前控制逻辑的回调 */ | |||
| std::function<void(const std::string &)> select_logic_; | |||
| /** 显示控制逻辑属性的回调 */ | |||
| std::function<void(const std::string &)> show_logic_properties_; | |||
| /** 向状态栏报告消息的回调 */ | |||
| StatusReporter status_reporter_; | |||
| /** 向输出面板报告消息的回调 */ | |||
| OutputReporter output_reporter_; | |||
| /** 请求主窗口退出运行态的回调 */ | |||
| RuntimeExitRequester runtime_exit_requester_; | |||
| /** 唯一的运行监控顶层窗口 */ | |||
| std::unique_ptr<RuntimeMonitorWindow> runtime_monitor_window_; | |||
| /** 运行监控窗口中的主控件,窗口拥有它 */ | |||
| RuntimeMonitorWidget *runtime_monitor_widget_ = nullptr; | |||
| /** 定时刷新运行监控数据的计时器 */ | |||
| QTimer *runtime_refresh_timer_ = nullptr; | |||
| /** 当前是否已经进入运行监控会话 */ | |||
| bool runtime_session_active_ = false; | |||
| }; | |||
| @@ -1,8 +1,16 @@ | |||
| /** | |||
| * @file toolbar_icon_factory.h | |||
| * @brief 定义工具栏图标类型和图标创建函数 | |||
| * @version 1.0.0 | |||
| * @author suyu | |||
| * @date 2026-08-22 | |||
| */ | |||
| #pragma once | |||
| #include <QIcon> | |||
| // 工具栏语义图标;具体 QIcon 绘制集中在 toolbar_icon_factory.cpp | |||
| /** 工具栏中使用的语义图标类型 */ | |||
| enum class UiIcon | |||
| { | |||
| Edit, | |||
| @@ -56,5 +64,9 @@ enum class UiIcon | |||
| ClearList | |||
| }; | |||
| // 根据语义图标类型创建 Qt 图标 | |||
| /** | |||
| * @brief 根据语义图标类型创建 Qt 图标 | |||
| * @param icon 要创建的图标类型 | |||
| * @return 创建好的 Qt 图标 | |||
| */ | |||
| QIcon makeUiIcon(UiIcon icon); | |||
| @@ -18,6 +18,13 @@ ContactNodeConfig contact(int address) | |||
| return {RegisterAddress{RegisterArea::M, address}, ContactMode::NormallyOpen}; | |||
| } | |||
| std::string makeEmptyRung(LogicEditorService &service, const std::string &logic_id) | |||
| { | |||
| const LogicEditorResult result = service.addRung(logic_id); | |||
| require(result.succeeded, "test fixture must create an empty network"); | |||
| return result.id; | |||
| } | |||
| int conditionColumns(const ConditionExpression &expression) | |||
| { | |||
| if (expression.kind == ConditionExpressionKind::Node) | |||
| @@ -56,13 +63,82 @@ int wireColumns(const ConditionExpression &expression) | |||
| return columns; | |||
| } | |||
| void testEmptyLogicCreatesNetworksOnFirstEdit() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| require(service.findLogic(logic_id)->rungs.empty(), | |||
| "a default control logic must start without an empty network"); | |||
| const LogicEditorResult first_condition = service.appendCondition( | |||
| logic_id, {}, contact(0)); | |||
| require(first_condition.succeeded | |||
| && service.findLogic(logic_id)->rungs.size() == 1U, | |||
| "the first condition must create network 1 atomically"); | |||
| const LadderRung &condition_rung = service.findLogic(logic_id)->rungs.front(); | |||
| require(condition_rung.condition.has_value() | |||
| && condition_rung.condition->kind == ConditionExpressionKind::Node, | |||
| "the first condition must not create an implicit horizontal wire"); | |||
| require(service.removeRung(logic_id, condition_rung.id).succeeded | |||
| && service.findLogic(logic_id)->rungs.empty(), | |||
| "the last network must be removable back to an empty logic"); | |||
| const LogicEditorResult first_wire = service.appendWire(logic_id, {}, 1); | |||
| require(first_wire.succeeded | |||
| && service.findLogic(logic_id)->rungs.size() == 1U | |||
| && service.findLogic(logic_id)->rungs.front().condition->kind | |||
| == ConditionExpressionKind::Wire, | |||
| "the first horizontal wire must create a one-cell network"); | |||
| require(service.removeRung( | |||
| logic_id, service.findLogic(logic_id)->rungs.front().id).succeeded, | |||
| "the wire-only network must be removable"); | |||
| const LogicEditorResult first_output = service.setOutput( | |||
| logic_id, | |||
| {}, | |||
| CoilNodeConfig{RegisterAddress{RegisterArea::M, 10}, CoilMode::Normal}); | |||
| require(first_output.succeeded | |||
| && service.findLogic(logic_id)->rungs.size() == 1U | |||
| && !service.findLogic(logic_id)->rungs.front().condition.has_value() | |||
| && service.findLogic(logic_id)->rungs.front().output.has_value(), | |||
| "the first output must create an unconditional network"); | |||
| } | |||
| void testAppendingWireMovesToNextNetworkAfterTenColumns() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string rung_id = makeEmptyRung(service, logic_id); | |||
| for (int index = 0; index < ProjectLimits::kMaximumConditionColumns; ++index) | |||
| { | |||
| require(service.appendWire(logic_id, rung_id).succeeded, | |||
| "ten horizontal wire cells must fit in one network"); | |||
| } | |||
| const LogicEditorResult next = service.appendWire(logic_id, rung_id); | |||
| require(next.succeeded && service.findLogic(logic_id)->rungs.size() == 2U, | |||
| "the next appended wire must create the following network"); | |||
| const std::string next_rung_id = service.findLogic(logic_id)->rungs.back().id; | |||
| require(service.findRung(logic_id, rung_id)->condition.has_value() | |||
| && conditionColumns(*service.findRung(logic_id, rung_id)->condition) | |||
| == ProjectLimits::kMaximumConditionColumns | |||
| && service.findRung(logic_id, next_rung_id)->condition.has_value(), | |||
| "automatic network rollover must preserve both network contents"); | |||
| require(service.undo().succeeded && service.findLogic(logic_id)->rungs.size() == 1U, | |||
| "automatic network rollover must be undone as one edit"); | |||
| } | |||
| void testStructuredEditingAndNormalization() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string rung_id = service.firstRungId(logic_id); | |||
| const std::string rung_id = makeEmptyRung(service, logic_id); | |||
| const LogicEditorResult first = service.appendCondition(logic_id, rung_id, contact(0)); | |||
| const LogicEditorResult second = service.appendCondition(logic_id, rung_id, contact(1)); | |||
| @@ -130,7 +206,7 @@ void testRangeParallelInsertion() | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string rung_id = service.firstRungId(logic_id); | |||
| const std::string rung_id = makeEmptyRung(service, logic_id); | |||
| service.appendCondition(logic_id, rung_id, contact(0)); | |||
| service.appendCondition(logic_id, rung_id, contact(1)); | |||
| @@ -154,13 +230,66 @@ void testRangeParallelInsertion() | |||
| "a non-contiguous selection must be rejected"); | |||
| } | |||
| void testParallelBranchGridInsertion() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string rung_id = makeEmptyRung(service, logic_id); | |||
| const LogicEditorResult first = service.appendCondition( | |||
| logic_id, rung_id, contact(0)); | |||
| const LogicEditorResult second = service.appendCondition( | |||
| logic_id, rung_id, contact(1)); | |||
| const LogicEditorResult third = service.appendCondition( | |||
| logic_id, rung_id, contact(2)); | |||
| const LogicEditorResult branch = service.addParallelBranch( | |||
| logic_id, rung_id, {first.id, second.id, third.id}, contact(10)); | |||
| require(branch.succeeded, | |||
| "parallel grid insertion setup must create a short lower branch"); | |||
| service.clearHistory(); | |||
| const LogicEditorResult inserted = service.insertConditionInBranchAtColumn( | |||
| logic_id, rung_id, branch.id, 2, contact(12)); | |||
| require(inserted.succeeded, | |||
| "a visible parallel branch padding cell must accept a condition"); | |||
| const LadderRung *rung = service.findRung(logic_id, rung_id); | |||
| require(rung != nullptr && rung->condition.has_value() | |||
| && rung->condition->kind == ConditionExpressionKind::Parallel, | |||
| "branch grid insertion must preserve the surrounding parallel expression"); | |||
| const ConditionExpression &lower = rung->condition->children.at(1); | |||
| require(lower.kind == ConditionExpressionKind::Series | |||
| && lower.children.size() == 3U | |||
| && lower.children.at(0).node->id == branch.id | |||
| && lower.children.at(1).kind == ConditionExpressionKind::Wire | |||
| && lower.children.at(1).wire->columnSpan == 1 | |||
| && lower.children.at(2).node->id == inserted.id, | |||
| "a distant branch cell must persist only the required gap and new condition"); | |||
| require(service.undo().succeeded, | |||
| "parallel branch grid insertion must be one undoable edit"); | |||
| rung = service.findRung(logic_id, rung_id); | |||
| require(rung->condition->children.at(1).kind == ConditionExpressionKind::Node | |||
| && rung->condition->children.at(1).node->id == branch.id, | |||
| "undo must restore the original short parallel branch"); | |||
| service.clearHistory(); | |||
| const bool modified_before = project_service.isModified(); | |||
| const LogicEditorResult invalid = service.insertConditionInBranchAtColumn( | |||
| logic_id, rung_id, branch.id, 3, contact(13)); | |||
| require(!invalid.succeeded && !service.canUndo() | |||
| && project_service.isModified() == modified_before | |||
| && service.findNode(logic_id, inserted.id) == nullptr, | |||
| "a cell outside the visible branch padding must fail atomically"); | |||
| } | |||
| void testStructuredWireEditing() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string rung_id = service.firstRungId(logic_id); | |||
| const std::string rung_id = makeEmptyRung(service, logic_id); | |||
| service.appendCondition(logic_id, rung_id, contact(0)); | |||
| service.appendCondition(logic_id, rung_id, contact(1)); | |||
| service.appendCondition(logic_id, rung_id, contact(2)); | |||
| @@ -220,7 +349,7 @@ void testBatchDeleteAllNodesInParallelBranch() | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string rung_id = service.firstRungId(logic_id); | |||
| const std::string rung_id = makeEmptyRung(service, logic_id); | |||
| const LogicEditorResult first = service.appendCondition( | |||
| logic_id, rung_id, contact(0)); | |||
| @@ -274,7 +403,7 @@ void testConditionColumnLimit() | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string rung_id = service.firstRungId(logic_id); | |||
| const std::string rung_id = makeEmptyRung(service, logic_id); | |||
| for (int column = 0; column < ProjectLimits::kMaximumConditionColumns; ++column) | |||
| { | |||
| @@ -323,7 +452,7 @@ void testUnconditionalOutputEditing() | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string rung_id = service.firstRungId(logic_id); | |||
| const std::string rung_id = makeEmptyRung(service, logic_id); | |||
| require(service.setOutput( | |||
| logic_id, | |||
| @@ -345,7 +474,7 @@ void testColumnTargetedConditionInsertion() | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string first_rung_id = service.firstRungId(logic_id); | |||
| const std::string first_rung_id = makeEmptyRung(service, logic_id); | |||
| require(service.insertConditionAtColumn( | |||
| logic_id, first_rung_id, 4, contact(4)).succeeded, | |||
| @@ -429,7 +558,7 @@ void testWireColumnReplacement() | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string rung_id = service.firstRungId(logic_id); | |||
| const std::string rung_id = makeEmptyRung(service, logic_id); | |||
| const LogicEditorResult wire = service.appendWire(logic_id, rung_id, 4); | |||
| require(wire.succeeded, | |||
| "wire-column replacement test must create a four-column wire"); | |||
| @@ -575,7 +704,7 @@ void testSequentialConditionInsertionConsumesFollowingWire() | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string rung_id = service.firstRungId(logic_id); | |||
| const std::string rung_id = makeEmptyRung(service, logic_id); | |||
| const LogicEditorResult wire = service.appendWire(logic_id, rung_id, 10); | |||
| require(wire.succeeded, | |||
| "sequential wire replacement must start with a full-width wire"); | |||
| @@ -675,7 +804,7 @@ void testEdgeTimerNodesAndRungComments() | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string rung_id = service.firstRungId(logic_id); | |||
| const std::string rung_id = makeEmptyRung(service, logic_id); | |||
| const LogicEditorResult rising = service.appendCondition( | |||
| logic_id, | |||
| @@ -745,7 +874,7 @@ void testHistoryAndAtomicBatchDelete() | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string first_rung_id = service.firstRungId(logic_id); | |||
| const std::string first_rung_id = makeEmptyRung(service, logic_id); | |||
| const LogicEditorResult first = service.appendCondition( | |||
| logic_id, first_rung_id, contact(0)); | |||
| const LogicEditorResult second = service.appendCondition( | |||
| @@ -816,8 +945,11 @@ int main() | |||
| { | |||
| try | |||
| { | |||
| testEmptyLogicCreatesNetworksOnFirstEdit(); | |||
| testAppendingWireMovesToNextNetworkAfterTenColumns(); | |||
| testStructuredEditingAndNormalization(); | |||
| testRangeParallelInsertion(); | |||
| testParallelBranchGridInsertion(); | |||
| testStructuredWireEditing(); | |||
| testBatchDeleteAllNodesInParallelBranch(); | |||
| testConditionColumnLimit(); | |||
| @@ -30,6 +30,7 @@ | |||
| #include <QDialog> | |||
| #include <QDockWidget> | |||
| #include <QGraphicsItem> | |||
| #include <QGraphicsLineItem> | |||
| #include <QGraphicsScene> | |||
| #include <QImage> | |||
| #include <QLabel> | |||
| @@ -278,13 +279,20 @@ ContactNodeConfig logicContact(int address) | |||
| return {RegisterAddress{RegisterArea::M, address}, ContactMode::NormallyOpen}; | |||
| } | |||
| std::string makeEmptyRung(LogicEditorService &service, const std::string &logic_id) | |||
| { | |||
| const LogicEditorResult result = service.addRung(logic_id); | |||
| require(result.succeeded, "UI test fixture must create an empty network"); | |||
| return result.id; | |||
| } | |||
| void testLogicEditorBatchDeletesWholeParallelRow() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string rung_id = service.firstRungId(logic_id); | |||
| const std::string rung_id = makeEmptyRung(service, logic_id); | |||
| std::vector<std::string> top_ids; | |||
| for (int address = 0; address < 4; ++address) | |||
| @@ -688,7 +696,7 @@ void testLogicEmptyGridSlotInsertion() | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string rung_id = service.firstRungId(logic_id); | |||
| const std::string rung_id = makeEmptyRung(service, logic_id); | |||
| LogicEditorWidget editor(service); | |||
| editor.setLogicId(logic_id); | |||
| editor.resize(1400, 360); | |||
| @@ -792,13 +800,143 @@ void testLogicEmptyGridSlotInsertion() | |||
| "every newly added network must expose its own ten empty insertion slots"); | |||
| } | |||
| void testLogicParallelPaddingGridInsertion() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string rung_id = makeEmptyRung(service, logic_id); | |||
| std::vector<std::string> top_ids; | |||
| for (int address = 0; address < 3; ++address) | |||
| { | |||
| const LogicEditorResult result = service.appendCondition( | |||
| logic_id, rung_id, logicContact(address)); | |||
| require(result.succeeded, | |||
| "parallel grid UI setup must create the upper branch"); | |||
| top_ids.push_back(result.id); | |||
| } | |||
| const LogicEditorResult lower = service.addParallelBranch( | |||
| logic_id, rung_id, top_ids, logicContact(10)); | |||
| require(lower.succeeded, | |||
| "parallel grid UI setup must create a short lower branch"); | |||
| LogicEditorWidget editor(service); | |||
| editor.setLogicId(logic_id); | |||
| editor.resize(1700, 480); | |||
| editor.show(); | |||
| QApplication::processEvents(); | |||
| QList<QGraphicsItem *> branch_slots; | |||
| for (QGraphicsItem *item : editor.scene()->items()) | |||
| { | |||
| if (item->toolTip().startsWith(QStringLiteral("并联空网格"))) | |||
| { | |||
| branch_slots.push_back(item); | |||
| } | |||
| } | |||
| std::sort( | |||
| branch_slots.begin(), branch_slots.end(), | |||
| [](const QGraphicsItem *left, const QGraphicsItem *right) | |||
| { | |||
| return left->scenePos().x() < right->scenePos().x(); | |||
| }); | |||
| require(branch_slots.size() == 2, | |||
| "every visible padding cell in a short parallel branch must be selectable"); | |||
| QTest::mouseClick( | |||
| editor.viewport(), | |||
| Qt::LeftButton, | |||
| Qt::NoModifier, | |||
| editor.mapFromScene(branch_slots.back()->scenePos())); | |||
| require(editor.scene()->selectedItems().size() == 1 | |||
| && editor.scene()->selectedItems().front()->toolTip().startsWith( | |||
| QStringLiteral("并联空网格")), | |||
| "clicking a parallel padding line must select its grid cell, not the rung"); | |||
| const LogicEditorResult inserted = editor.addCondition(logicContact(12)); | |||
| require(inserted.succeeded, | |||
| "the normal condition action must insert into a selected parallel grid cell"); | |||
| const LadderRung *rung = service.findRung(logic_id, rung_id); | |||
| require(rung != nullptr && rung->condition.has_value() | |||
| && rung->condition->kind == ConditionExpressionKind::Parallel | |||
| && rung->condition->children.at(1).kind | |||
| == ConditionExpressionKind::Series | |||
| && rung->condition->children.at(1).children.size() == 3U, | |||
| "parallel grid insertion must update only the selected branch"); | |||
| int remaining_branch_slots = 0; | |||
| int persisted_wire_cells = 0; | |||
| for (QGraphicsItem *item : editor.scene()->items()) | |||
| { | |||
| remaining_branch_slots += item->toolTip().startsWith( | |||
| QStringLiteral("并联空网格")) ? 1 : 0; | |||
| persisted_wire_cells += item->toolTip().startsWith( | |||
| QStringLiteral("横线网格")) ? 1 : 0; | |||
| } | |||
| require(remaining_branch_slots == 0 && persisted_wire_cells == 1, | |||
| "using a distant branch cell must retain the skipped cell as a replaceable wire"); | |||
| } | |||
| void testLogicParallelBranchConnectsShortBranchToRightJoin() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string rung_id = makeEmptyRung(service, logic_id); | |||
| std::vector<std::string> selected_ids; | |||
| for (int address = 0; address < 5; ++address) | |||
| { | |||
| const LogicEditorResult result = service.appendCondition( | |||
| logic_id, rung_id, logicContact(address)); | |||
| require(result.succeeded, | |||
| "short parallel branch rendering setup must create the upper series"); | |||
| selected_ids.push_back(result.id); | |||
| } | |||
| const LogicEditorResult branch = service.addParallelBranch( | |||
| logic_id, rung_id, selected_ids, logicContact(10)); | |||
| require(branch.succeeded, | |||
| "parallel branch rendering setup must create a one-column lower branch"); | |||
| LogicEditorWidget editor(service); | |||
| editor.setLogicId(logic_id); | |||
| editor.resize(1700, 480); | |||
| editor.show(); | |||
| QApplication::processEvents(); | |||
| // The lower one-column node must be visibly wired to the parallel right join | |||
| const qreal expected_lower_y = 28.0 + 52.0 + 1.5 * 120.0; | |||
| const qreal expected_branch_output_x = 68.0 + 128.0; | |||
| const qreal expected_right_join_x = 68.0 + 5.0 * 128.0; | |||
| bool has_right_join_wire = false; | |||
| for (QGraphicsItem *item : editor.scene()->items()) | |||
| { | |||
| const QGraphicsLineItem *line = dynamic_cast<const QGraphicsLineItem *>(item); | |||
| if (line == nullptr || line->zValue() <= -1.0) | |||
| { | |||
| continue; | |||
| } | |||
| const QLineF segment = line->line(); | |||
| if (qAbs(segment.y1() - expected_lower_y) < 0.1 | |||
| && qAbs(segment.y2() - expected_lower_y) < 0.1 | |||
| && qAbs(segment.x1() - expected_branch_output_x) < 0.1 | |||
| && qAbs(segment.x2() - expected_right_join_x) < 0.1) | |||
| { | |||
| has_right_join_wire = true; | |||
| break; | |||
| } | |||
| } | |||
| require(has_right_join_wire, | |||
| "a short parallel branch must connect its output to the right join"); | |||
| } | |||
| void testLogicContinuousInsertionConsumesFullWire() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| LogicEditorService service(project_service); | |||
| const std::string logic_id = service.ensureDefaultLogic().id; | |||
| const std::string rung_id = service.firstRungId(logic_id); | |||
| const std::string rung_id = makeEmptyRung(service, logic_id); | |||
| LogicEditorWidget editor(service); | |||
| editor.setLogicId(logic_id); | |||
| editor.resize(1400, 360); | |||
| @@ -1301,11 +1439,14 @@ void testModeActionsControlEditingAvailability() | |||
| QApplication::processEvents(); | |||
| undo_action->trigger(); | |||
| require(logic_editor_service.findLogic(logic_editor_service.firstLogicId()) | |||
| ->rungs.front().condition.has_value() == false, | |||
| ->rungs.empty(), | |||
| "logic undo must use the ladder history when the ladder tab is active"); | |||
| redo_action->trigger(); | |||
| const LadderRung &restored_rung = logic_editor_service.findLogic( | |||
| logic_editor_service.firstLogicId())->rungs.front(); | |||
| const ControlLogic *restored_logic = logic_editor_service.findLogic( | |||
| logic_editor_service.firstLogicId()); | |||
| require(restored_logic != nullptr && restored_logic->rungs.size() == 1U, | |||
| "logic redo must restore the automatically created network"); | |||
| const LadderRung &restored_rung = restored_logic->rungs.front(); | |||
| require(restored_rung.condition.has_value(), | |||
| "logic redo must restore the ladder edit"); | |||
| std::vector<const LogicNode *> restored_nodes; | |||
| @@ -2204,8 +2345,8 @@ void testMultiPageAndLogicMainWindowIntegration() | |||
| require(window.currentLogicId() == second_logic_id, | |||
| "selecting a logic tree node must change the current logic id"); | |||
| requiredChild<QAction>(window, "addRungAction")->trigger(); | |||
| require(logic_editor_service.findLogic(second_logic_id)->rungs.size() == 2U | |||
| && logic_editor_service.findLogic(first_logic_id)->rungs.size() == 1U, | |||
| require(logic_editor_service.findLogic(second_logic_id)->rungs.size() == 1U | |||
| && logic_editor_service.findLogic(first_logic_id)->rungs.empty(), | |||
| "logic toolbar actions must edit the selected logic module"); | |||
| tree->setCurrentItem(tree->topLevelItem(0)->child(0)); | |||
| @@ -2276,6 +2417,8 @@ int main(int argc, char *argv[]) | |||
| testRuntimePageJumpDoesNotRequireRegisterWritePermission(); | |||
| testRuntimeAlarmListInteraction(); | |||
| testLogicEmptyGridSlotInsertion(); | |||
| testLogicParallelPaddingGridInsertion(); | |||
| testLogicParallelBranchConnectsShortBranchToRightJoin(); | |||
| testLogicContinuousInsertionConsumesFullWire(); | |||
| testWindowTitleTracksUnsavedProjectChanges(); | |||
| testModeActionsControlEditingAvailability(); | |||