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