Просмотр исходного кода

feat: 完善梯形图网格编辑与运行界面

main
suyu 4 недель назад
Родитель
Сommit
88cacc0f57
20 измененных файлов: 1035 добавлений и 143 удалений
  1. +5
    -2
      app/src/domain/hmi_model.cpp
  2. +207
    -23
      app/src/services/logic_editor_service.cpp
  3. +7
    -1
      app/src/services/logic_editor_service.h
  4. +19
    -2
      app/src/ui/alarm_configuration_dialog.h
  5. +23
    -2
      app/src/ui/free_monitor_widget.h
  6. +13
    -8
      app/src/ui/hmi_editor_widget.h
  7. +87
    -39
      app/src/ui/logic_editor_widget.cpp
  8. +52
    -4
      app/src/ui/logic_editor_widget.h
  9. +18
    -2
      app/src/ui/logic_instruction_dialog.h
  10. +76
    -22
      app/src/ui/main_window.h
  11. +11
    -2
      app/src/ui/plc_connection_dialog.h
  12. +56
    -2
      app/src/ui/project_workspace_controller.h
  13. +43
    -2
      app/src/ui/property_panel_controller.h
  14. +17
    -1
      app/src/ui/register_comment_dialog.h
  15. +32
    -3
      app/src/ui/runtime_monitor_widget.h
  16. +18
    -4
      app/src/ui/runtime_monitor_window.h
  17. +43
    -3
      app/src/ui/runtime_panel_controller.h
  18. +14
    -2
      app/src/ui/toolbar_icon_factory.h
  19. +143
    -11
      app/tests/logic_editor_service_tests.cpp
  20. +151
    -8
      app/tests/main_window_tests.cpp

+ 5
- 2
app/src/domain/hmi_model.cpp Просмотреть файл

@@ -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)


+ 207
- 23
app/src/services/logic_editor_service.cpp Просмотреть файл

@@ -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};
} }


+ 7
- 1
app/src/services/logic_editor_service.h Просмотреть файл

@@ -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,


+ 19
- 2
app/src/ui/alarm_configuration_dialog.h Просмотреть файл

@@ -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_;
}; };

+ 23
- 2
app/src/ui/free_monitor_widget.h Просмотреть файл

@@ -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;
}; };

+ 13
- 8
app/src/ui/hmi_editor_widget.h Просмотреть файл

@@ -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;
}; };

+ 87
- 39
app/src/ui/logic_editor_widget.cpp Просмотреть файл

@@ -649,14 +649,24 @@ public:
const std::string &rung_id, const std::string &rung_id,
int column, int column,
const QPointF &center, const QPointF &center,
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();


+ 52
- 4
app/src/ui/logic_editor_widget.h Просмотреть файл

@@ -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;
}; };

+ 18
- 2
app/src/ui/logic_instruction_dialog.h Просмотреть файл

@@ -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_;
}; };

+ 76
- 22
app/src/ui/main_window.h Просмотреть файл

@@ -91,78 +91,105 @@ public:


~MainWindow() override; ~MainWindow() override;


/** 返回当前选中的 HMI 页面标识 */
const std::string &currentHmiPageId() const; const std::string &currentHmiPageId() const;
/** 返回当前选中的控制逻辑标识 */
const std::string &currentLogicId() const; const std::string &currentLogicId() 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 &register_comment_service_; RegisterCommentService &register_comment_service_;
/** 非拥有的寄存器监控服务依赖 */
RegisterMonitorService &register_monitor_service_; RegisterMonitorService &register_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;
}; };

+ 11
- 2
app/src/ui/plc_connection_dialog.h Просмотреть файл

@@ -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_;
}; };

+ 56
- 2
app/src/ui/project_workspace_controller.h Просмотреть файл

@@ -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 &currentHmiPageId() const; const std::string &currentHmiPageId() const;
/** 返回当前控制逻辑标识 */
const std::string &currentLogicId() const; const std::string &currentLogicId() 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 &current_hmi_page_id_; std::string &current_hmi_page_id_;
/** 主窗口保存的当前控制逻辑标识 */
std::string &current_logic_id_; std::string &current_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;
}; };

+ 43
- 2
app/src/ui/property_panel_controller.h Просмотреть файл

@@ -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;
}; };

+ 17
- 1
app/src/ui/register_comment_dialog.h Просмотреть файл

@@ -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_;
}; };

+ 32
- 3
app/src/ui/runtime_monitor_widget.h Просмотреть файл

@@ -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;
}; };

+ 18
- 4
app/src/ui/runtime_monitor_window.h Просмотреть файл

@@ -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;
}; };

+ 43
- 3
app/src/ui/runtime_panel_controller.h Просмотреть файл

@@ -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 &register_monitor_service_; RegisterMonitorService &register_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;
}; };

+ 14
- 2
app/src/ui/toolbar_icon_factory.h Просмотреть файл

@@ -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);

+ 143
- 11
app/tests/logic_editor_service_tests.cpp Просмотреть файл

@@ -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();


+ 151
- 8
app/tests/main_window_tests.cpp Просмотреть файл

@@ -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();


Загрузка…
Отмена
Сохранить