From d8f18ef5011b28a696a88ec81982cd0cfec9d738 Mon Sep 17 00:00:00 2001 From: suyu <1643689728@qq.com> Date: Fri, 14 Aug 2026 15:36:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=9B=86=E6=88=90=E5=A4=9A=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=A4=9A=E9=80=BB=E8=BE=91=E5=B7=A5=E4=BD=9C=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/integrated_platform.pro | 2 + app/src/main.cpp | 3 + app/src/ui/hmi_editor_widget.cpp | 68 ++- app/src/ui/hmi_editor_widget.h | 1 + app/src/ui/main_window.cpp | 616 ++++++++++++++++++++++++-- app/src/ui/main_window.h | 37 ++ app/src/ui/main_window.ui | 34 +- app/src/ui/runtime_monitor_widget.cpp | 118 ++++- app/src/ui/runtime_monitor_widget.h | 20 +- app/src/ui/runtime_monitor_widget.ui | 16 +- app/tests/main_window_tests.cpp | 174 ++++++++ app/tests/main_window_tests.pro | 2 + 12 files changed, 1038 insertions(+), 53 deletions(-) diff --git a/app/integrated_platform.pro b/app/integrated_platform.pro index 8986fa5..e01a980 100644 --- a/app/integrated_platform.pro +++ b/app/integrated_platform.pro @@ -30,6 +30,7 @@ SOURCES += \ src/domain/runtime_state.cpp \ src/services/project_service.cpp \ src/services/hmi_editor_service.cpp \ + src/services/hmi_navigation_service.cpp \ src/services/logic_editor_service.cpp \ src/services/hmi_runtime_service.cpp \ src/services/software_logic_executor.cpp \ @@ -59,6 +60,7 @@ HEADERS += \ src/domain/project_storage.h \ src/services/project_service.h \ src/services/hmi_editor_service.h \ + src/services/hmi_navigation_service.h \ src/services/logic_editor_service.h \ src/services/hmi_runtime_service.h \ src/services/software_logic_executor.h \ diff --git a/app/src/main.cpp b/app/src/main.cpp index 02721b5..60a323b 100644 --- a/app/src/main.cpp +++ b/app/src/main.cpp @@ -14,6 +14,7 @@ #include "domain/active_register_repository.h" #include "services/hmi_editor_service.h" #include "services/hmi_runtime_service.h" +#include "services/hmi_navigation_service.h" #include "services/logic_editor_service.h" #include "services/offline_simulation_service.h" #include "services/project_service.h" @@ -31,6 +32,7 @@ int main(int argc, char *argv[]) JsonProjectStorage project_storage; ProjectService project_service(project_storage); HmiEditorService hmi_editor_service(project_service); + HmiNavigationService hmi_navigation_service(project_service); LogicEditorService logic_editor_service(project_service); // 当前离线模式使用内存仓库,后续真机模式替换为 PLC 缓存实现 VirtualRegisterRepository virtual_register_repository; @@ -53,6 +55,7 @@ int main(int argc, char *argv[]) hmi_editor_service, logic_editor_service, hmi_runtime_service, + hmi_navigation_service, register_monitor_service); main_window.show(); diff --git a/app/src/ui/hmi_editor_widget.cpp b/app/src/ui/hmi_editor_widget.cpp index 87ffb54..e0f862b 100644 --- a/app/src/ui/hmi_editor_widget.cpp +++ b/app/src/ui/hmi_editor_widget.cpp @@ -32,13 +32,15 @@ public: int page_height, std::function moved, std::function button_event, - std::function numeric_input_activated) + std::function numeric_input_activated, + std::function page_navigation) : control_(control), page_width_(page_width), page_height_(page_height), moved_(std::move(moved)), button_event_(std::move(button_event)), - numeric_input_activated_(std::move(numeric_input_activated)) + numeric_input_activated_(std::move(numeric_input_activated)), + page_navigation_(std::move(page_navigation)) { // 领域坐标直接作为图元在场景中的初始位置 setPos(control_.bounds.x, control_.bounds.y); @@ -164,6 +166,18 @@ public: textWithValue()); break; } + case HmiControlType::PageJump: + { + const QColor fill = runtime_active_ && page_hovered_ + ? QColor(QStringLiteral("#d8eafa")) + : QColor(QStringLiteral("#e8f1fa")); + painter->setBrush(fill); + painter->setPen(QPen(QColor(QStringLiteral("#4e789f")), 1)); + painter->drawRoundedRect(rect, 4, 4); + painter->setPen(QColor(QStringLiteral("#244b6b"))); + painter->drawText(rect, Qt::AlignCenter, textWithValue()); + break; + } case HmiControlType::Label: default: { @@ -207,15 +221,19 @@ public: const bool runtime_input = runtime_active_ && (control_.type == HmiControlType::Button - || control_.type == HmiControlType::NumericInput); - const bool runtime_input_enabled = runtime_input && runtime_write_enabled_; + || control_.type == HmiControlType::NumericInput + || control_.type == HmiControlType::PageJump); + const bool runtime_input_enabled = runtime_input + && (control_.type == HmiControlType::PageJump || runtime_write_enabled_); setAcceptedMouseButtons( editable || runtime_input_enabled ? Qt::LeftButton : Qt::NoButton); const bool runtime_button_enabled = runtime_active_ && control_.type == HmiControlType::Button && runtime_write_enabled_; - setAcceptHoverEvents(runtime_button_enabled); - if (runtime_button_enabled) + const bool runtime_page_jump_enabled = runtime_active_ + && control_.type == HmiControlType::PageJump; + setAcceptHoverEvents(runtime_button_enabled || runtime_page_jump_enabled); + if (runtime_button_enabled || runtime_page_jump_enabled) { setCursor(Qt::PointingHandCursor); } @@ -223,6 +241,7 @@ public: { unsetCursor(); button_hovered_ = false; + page_hovered_ = false; button_pressed_ = false; } update(); @@ -260,6 +279,13 @@ protected: // 运行态按钮按下时通知外层执行配置的 M 位操作 void mousePressEvent(QGraphicsSceneMouseEvent *event) override { + if (runtime_active_ && control_.type == HmiControlType::PageJump) + { + page_pressed_ = true; + update(); + event->accept(); + return; + } if (runtime_active_ && runtime_write_enabled_ && control_.type == HmiControlType::Button && button_event_) { @@ -291,6 +317,18 @@ protected: // 拖拽结束后才提交最终坐标,避免移动过程频繁修改领域模型 void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override { + if (runtime_active_ && control_.type == HmiControlType::PageJump + && page_pressed_) + { + page_pressed_ = false; + update(); + if (page_navigation_ && control_.pageJump.has_value()) + { + page_navigation_(control_.pageJump->targetPageId); + } + event->accept(); + return; + } if (runtime_active_ && runtime_write_enabled_ && control_.type == HmiControlType::Button && button_pressed_) { @@ -316,6 +354,11 @@ protected: button_hovered_ = true; update(); } + if (runtime_active_ && control_.type == HmiControlType::PageJump) + { + page_hovered_ = true; + update(); + } QGraphicsItem::hoverEnterEvent(event); } @@ -326,6 +369,11 @@ protected: button_hovered_ = false; update(); } + if (page_hovered_) + { + page_hovered_ = false; + update(); + } QGraphicsItem::hoverLeaveEvent(event); } @@ -384,11 +432,14 @@ private: std::function moved_; std::function button_event_; std::function numeric_input_activated_; + std::function page_navigation_; bool editing_enabled_ = true; bool runtime_active_ = false; bool runtime_write_enabled_ = false; bool button_hovered_ = false; bool button_pressed_ = false; + bool page_hovered_ = false; + bool page_pressed_ = false; // 指示灯读取到的 M 位值 bool bit_value_ = false; // 数值控件读取到的 D 字值 @@ -501,6 +552,11 @@ void HmiEditorWidget::reloadPage() [this](const std::string &control_id) { handleNumericInputActivated(control_id); + }, + [this](const std::string &target_page_id) + { + emit pageNavigationRequested( + QString::fromStdString(target_page_id)); }); scene_->addItem(item); item->setInteractionState( diff --git a/app/src/ui/hmi_editor_widget.h b/app/src/ui/hmi_editor_widget.h index d123c61..15b41a5 100644 --- a/app/src/ui/hmi_editor_widget.h +++ b/app/src/ui/hmi_editor_widget.h @@ -90,6 +90,7 @@ signals: * @param message 可直接显示的错误信息 */ void editorError(const QString &message); + void pageNavigationRequested(const QString &target_page_id); protected: void resizeEvent(QResizeEvent *event) override; diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 9988d00..5504ef4 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -6,6 +6,7 @@ #include "runtime_monitor_widget.h" #include "free_monitor_widget.h" #include "services/hmi_editor_service.h" +#include "services/hmi_navigation_service.h" #include "services/hmi_runtime_service.h" #include "services/logic_editor_service.h" #include "services/project_service.h" @@ -23,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +39,16 @@ namespace { +enum class ProjectTreeItemKind +{ + Root = 0, + HmiPage = 1, + ControlLogic = 2 +}; + +constexpr int kProjectTreeKindRole = Qt::UserRole; +constexpr int kProjectTreeIdRole = Qt::UserRole + 1; + QString modeText(ApplicationMode mode) { switch (mode) @@ -157,10 +169,17 @@ QString controlTypeText(HmiControlType type) return MainWindow::tr("数值输入"); } case HmiControlType::Label: - default: { return MainWindow::tr("文本"); } + case HmiControlType::PageJump: + { + return MainWindow::tr("页面跳转"); + } + default: + { + return MainWindow::tr("未知控件"); + } } } @@ -181,7 +200,37 @@ MainWindow::MainWindow( hmi_editor_service_(hmi_editor_service), logic_editor_service_(logic_editor_service), hmi_runtime_service_(hmi_runtime_service), - register_monitor_service_(register_monitor_service) + register_monitor_service_(register_monitor_service), + owned_hmi_navigation_service_( + std::make_unique(project_service)), + hmi_navigation_service_(owned_hmi_navigation_service_.get()) +{ + initializeUi(); +} + +MainWindow::MainWindow( + RuntimeModeService &runtime_mode_service, + ProjectService &project_service, + HmiEditorService &hmi_editor_service, + LogicEditorService &logic_editor_service, + HmiRuntimeService &hmi_runtime_service, + HmiNavigationService &hmi_navigation_service, + RegisterMonitorService ®ister_monitor_service, + QWidget *parent) + : QMainWindow(parent), + ui_(std::make_unique()), + runtime_mode_service_(runtime_mode_service), + project_service_(project_service), + hmi_editor_service_(hmi_editor_service), + logic_editor_service_(logic_editor_service), + hmi_runtime_service_(hmi_runtime_service), + register_monitor_service_(register_monitor_service), + hmi_navigation_service_(&hmi_navigation_service) +{ + initializeUi(); +} + +void MainWindow::initializeUi() { ui_->setupUi(this); configureAppearance(); @@ -191,10 +240,13 @@ MainWindow::MainWindow( configureHmiEditor(); configureLogicEditor(); configureRuntimeMonitor(); + configureProjectTree(); runtime_mode_service_.setPlcStatusChangedCallback( [this] { schedulePlcStatusUpdate(); }); hmi_editor_service_.ensureDefaultPage(); logic_editor_service_.ensureDefaultLogic(); + current_hmi_page_id_ = hmi_editor_service_.firstPageId(); + current_logic_id_ = logic_editor_service_.firstLogicId(); refreshProjectUi(); updateModeUi(tr("系统已进入编辑态")); } @@ -204,6 +256,16 @@ MainWindow::~MainWindow() runtime_mode_service_.setPlcStatusChangedCallback({}); } +const std::string &MainWindow::currentHmiPageId() const +{ + return current_hmi_page_id_; +} + +const std::string &MainWindow::currentLogicId() const +{ + return current_logic_id_; +} + void MainWindow::configureActions() { mode_action_group_ = new QActionGroup(this); @@ -232,6 +294,10 @@ void MainWindow::configureActions() [this] { addHmiControl(HmiControlType::NumericDisplay); }); connect(ui_->addNumericInputAction, &QAction::triggered, this, [this] { addHmiControl(HmiControlType::NumericInput); }); + connect(ui_->addLabelAction, &QAction::triggered, this, + [this] { addHmiControl(HmiControlType::Label); }); + connect(ui_->addPageJumpAction, &QAction::triggered, this, + [this] { addHmiControl(HmiControlType::PageJump); }); connect(ui_->deleteControlAction, &QAction::triggered, this, &MainWindow::deleteSelectedControl); @@ -349,6 +415,7 @@ void MainWindow::configureActions() if (index == 0 || index == 1) { last_editing_tab_index_ = index; + refreshProjectUi(); } }); ui_->hmiToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 0); @@ -383,6 +450,10 @@ void MainWindow::configureAppearance() style()->standardIcon(QStyle::SP_FileDialogInfoView)); ui_->addNumericInputAction->setIcon( style()->standardIcon(QStyle::SP_FileDialogContentsView)); + ui_->addLabelAction->setIcon( + style()->standardIcon(QStyle::SP_FileDialogInfoView)); + ui_->addPageJumpAction->setIcon( + style()->standardIcon(QStyle::SP_ArrowForward)); ui_->deleteControlAction->setIcon(style()->standardIcon(QStyle::SP_TrashIcon)); ui_->addRungAction->setIcon(style()->standardIcon(QStyle::SP_FileIcon)); ui_->parallelInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowDown)); @@ -483,7 +554,8 @@ void MainWindow::configureHmiEditor() [this] { logic_editor_widget_->setRuntimeTrace( - runtime_mode_service_.offlineSimulationService().traceSnapshot()); + runtime_mode_service_.offlineSimulationService() + .traceSnapshot().forLogic(current_logic_id_)); runtime_monitor_widget_->setLogicTrace( runtime_mode_service_.offlineSimulationService().traceSnapshot()); }, @@ -521,6 +593,8 @@ void MainWindow::configureRuntimeMonitor() hmi_editor_service_, hmi_runtime_service_, logic_editor_service_, + project_service_, + *hmi_navigation_service_, register_monitor_service_, ui_->runtimeMonitorTab); runtime_monitor_widget_->setObjectName(QStringLiteral("runtimeMonitorWidget")); @@ -543,9 +617,76 @@ void MainWindow::configureRuntimeMonitor() { statusBar()->showMessage(message, 4000); }); + connect(runtime_monitor_widget_, &RuntimeMonitorWidget::navigationFailed, + this, + [this](const QString &message) + { + statusBar()->showMessage(message, 5000); + ui_->outputList->addItem(tr("页面导航失败:%1").arg(message)); + }); ui_->editorTabWidget->setTabVisible(2, false); } +void MainWindow::configureProjectTree() +{ + ui_->projectTree->setContextMenuPolicy(Qt::ActionsContextMenu); + + add_page_action_ = new QAction(tr("新建页面"), this); + add_page_action_->setObjectName(QStringLiteral("addPageAction")); + add_logic_action_ = new QAction(tr("新建控制逻辑"), this); + add_logic_action_->setObjectName(QStringLiteral("addLogicAction")); + rename_project_item_action_ = new QAction(tr("重命名"), this); + rename_project_item_action_->setObjectName( + QStringLiteral("renameProjectItemAction")); + delete_project_item_action_ = new QAction(tr("删除"), this); + delete_project_item_action_->setObjectName( + QStringLiteral("deleteProjectItemAction")); + move_project_item_up_action_ = new QAction(tr("上移"), this); + move_project_item_up_action_->setObjectName( + QStringLiteral("moveProjectItemUpAction")); + move_project_item_down_action_ = new QAction(tr("下移"), this); + move_project_item_down_action_->setObjectName( + QStringLiteral("moveProjectItemDownAction")); + set_initial_page_action_ = new QAction(tr("设为初始页面"), this); + set_initial_page_action_->setObjectName( + QStringLiteral("setInitialPageAction")); + toggle_logic_enabled_action_ = new QAction(tr("禁用控制逻辑"), this); + toggle_logic_enabled_action_->setObjectName( + QStringLiteral("toggleLogicEnabledAction")); + + ui_->projectTree->addActions({ + add_page_action_, + add_logic_action_, + rename_project_item_action_, + delete_project_item_action_, + move_project_item_up_action_, + move_project_item_down_action_, + set_initial_page_action_, + toggle_logic_enabled_action_}); + + connect(add_page_action_, &QAction::triggered, this, &MainWindow::addPage); + connect(add_logic_action_, &QAction::triggered, this, &MainWindow::addLogic); + connect(rename_project_item_action_, &QAction::triggered, + this, &MainWindow::renameProjectItem); + connect(delete_project_item_action_, &QAction::triggered, + this, &MainWindow::deleteProjectItem); + connect(move_project_item_up_action_, &QAction::triggered, + this, [this] { moveProjectItem(-1); }); + connect(move_project_item_down_action_, &QAction::triggered, + this, [this] { moveProjectItem(1); }); + connect(set_initial_page_action_, &QAction::triggered, + this, &MainWindow::setSelectedPageAsInitial); + connect(toggle_logic_enabled_action_, &QAction::triggered, + this, &MainWindow::toggleSelectedLogicEnabled); + connect(ui_->projectTree, &QTreeWidget::currentItemChanged, + this, + [this](QTreeWidgetItem *, QTreeWidgetItem *) + { + handleProjectTreeSelection(); + }); + updateProjectTreeActions(); +} + void MainWindow::configurePropertyEditor() { ui_->bindingAreaComboBox->setItemData(0, -1); @@ -567,6 +708,8 @@ void MainWindow::configurePropertyEditor() this, &MainWindow::applySelectedControlProperties); connect(ui_->applyLogicPropertiesButton, &QPushButton::clicked, this, &MainWindow::applySelectedLogicNodeProperties); + ui_->targetPageLabel->setVisible(false); + ui_->targetPageComboBox->setVisible(false); showControlProperties({}); } @@ -578,28 +721,61 @@ void MainWindow::configurePlcConnection() void MainWindow::refreshProjectUi() { - // 工程树只展示模型摘要,画布内容由 HmiEditorWidget 单独投影 - const std::string current_page_id = hmi_editor_service_.firstPageId(); + // 仅在当前对象消失时回退,普通刷新必须保留用户的会话选择 + if (hmi_editor_service_.findPage(current_hmi_page_id_) == nullptr) + { + const std::string &initial_id = project_service_.project().initialHmiPageId; + current_hmi_page_id_ = hmi_editor_service_.findPage(initial_id) != nullptr + ? initial_id : hmi_editor_service_.firstPageId(); + selected_control_id_.clear(); + } + if (logic_editor_service_.findLogic(current_logic_id_) == nullptr) + { + current_logic_id_ = logic_editor_service_.firstLogicId(); + selected_logic_node_id_.clear(); + } + if (hmi_editor_widget_ != nullptr) { - hmi_editor_widget_->setPageId(current_page_id); + hmi_editor_widget_->setPageId(current_hmi_page_id_); } - const HmiPage *page = hmi_editor_service_.findPage(current_page_id); - const std::string current_logic_id = logic_editor_service_.firstLogicId(); + const HmiPage *page = hmi_editor_service_.findPage(current_hmi_page_id_); if (logic_editor_widget_ != nullptr) { - logic_editor_widget_->setLogicId(current_logic_id); + logic_editor_widget_->setLogicId(current_logic_id_); } - if (runtime_monitor_widget_ != nullptr) + if (runtime_monitor_widget_ != nullptr + && runtime_mode_service_.mode() != ApplicationMode::Editing) { - runtime_monitor_widget_->setProjectObjects(current_page_id, current_logic_id); + runtime_monitor_widget_->setProjectObjects( + hmi_navigation_service_->currentPageId(), current_logic_id_); } runtime_mode_service_.refreshPlcPollAddresses(); + + const QSignalBlocker blocker(ui_->projectTree); ui_->projectTree->clear(); auto *hmi_root = new QTreeWidgetItem(ui_->projectTree, {tr("HMI 页面")}); + hmi_root->setData(0, kProjectTreeKindRole, + static_cast(ProjectTreeItemKind::Root)); + QTreeWidgetItem *selected_tree_item = nullptr; + for (const HmiPage &candidate : project_service_.project().hmiPages) + { + const QString initial_suffix = candidate.id + == project_service_.project().initialHmiPageId + ? tr("(初始)") : QString{}; + auto *item = new QTreeWidgetItem( + hmi_root, {fromUtf8(candidate.name) + initial_suffix}); + item->setData(0, kProjectTreeKindRole, + static_cast(ProjectTreeItemKind::HmiPage)); + item->setData(0, kProjectTreeIdRole, fromUtf8(candidate.id)); + if (candidate.id == current_hmi_page_id_ + && ui_->editorTabWidget->currentIndex() == 0) + { + selected_tree_item = item; + } + } if (page != nullptr) { - hmi_root->addChild(new QTreeWidgetItem(hmi_root, {fromUtf8(page->name)})); ui_->hmiPageTitleLabel->setText(fromUtf8(page->name)); ui_->hmiPageSizeLabel->setText( tr("%1 x %2").arg(page->width).arg(page->height)); @@ -610,13 +786,311 @@ void MainWindow::refreshProjectUi() ui_->hmiPageSizeLabel->clear(); } auto *logic_root = new QTreeWidgetItem(ui_->projectTree, {tr("控制逻辑")}); + logic_root->setData(0, kProjectTreeKindRole, + static_cast(ProjectTreeItemKind::Root)); for (const ControlLogic &logic : project_service_.project().controlLogics) { const QString suffix = logic.enabled ? QString{} : tr("(已禁用)"); - logic_root->addChild(new QTreeWidgetItem( - logic_root, {fromUtf8(logic.name) + suffix})); + auto *item = new QTreeWidgetItem( + logic_root, {fromUtf8(logic.name) + suffix}); + item->setData(0, kProjectTreeKindRole, + static_cast(ProjectTreeItemKind::ControlLogic)); + item->setData(0, kProjectTreeIdRole, fromUtf8(logic.id)); + if (logic.id == current_logic_id_ + && ui_->editorTabWidget->currentIndex() == 1) + { + selected_tree_item = item; + } } ui_->projectTree->expandAll(); + if (selected_tree_item != nullptr) + { + ui_->projectTree->setCurrentItem(selected_tree_item); + } + updateProjectTreeActions(); +} + +void MainWindow::handleProjectTreeSelection() +{ + QTreeWidgetItem *item = ui_->projectTree->currentItem(); + if (item == nullptr) + { + updateProjectTreeActions(); + return; + } + const auto kind = static_cast( + item->data(0, kProjectTreeKindRole).toInt()); + const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString()); + if (kind == ProjectTreeItemKind::HmiPage + && hmi_editor_service_.findPage(id) != nullptr) + { + current_hmi_page_id_ = id; + selected_control_id_.clear(); + hmi_editor_widget_->setPageId(id); + hmi_editor_widget_->reloadPage(); + ui_->editorTabWidget->setCurrentWidget(ui_->hmiEditorTab); + showControlProperties({}); + const HmiPage *page = hmi_editor_service_.findPage(id); + ui_->hmiPageTitleLabel->setText(fromUtf8(page->name)); + ui_->hmiPageSizeLabel->setText( + tr("%1 x %2").arg(page->width).arg(page->height)); + } + else if (kind == ProjectTreeItemKind::ControlLogic + && logic_editor_service_.findLogic(id) != nullptr) + { + current_logic_id_ = id; + selected_logic_node_id_.clear(); + logic_editor_widget_->setLogicId(id); + logic_editor_widget_->reloadLogic(); + ui_->editorTabWidget->setCurrentWidget(ui_->logicEditorTab); + showLogicNodeProperties({}); + } + updateProjectTreeActions(); +} + +void MainWindow::updateProjectTreeActions() +{ + if (add_page_action_ == nullptr) + { + return; + } + QTreeWidgetItem *item = ui_->projectTree->currentItem(); + const auto kind = item == nullptr ? ProjectTreeItemKind::Root + : static_cast( + item->data(0, kProjectTreeKindRole).toInt()); + const bool page_selected = kind == ProjectTreeItemKind::HmiPage; + const bool logic_selected = kind == ProjectTreeItemKind::ControlLogic; + const bool editable = runtime_mode_service_.policy().allowsProjectEditing; + add_page_action_->setEnabled(editable); + add_logic_action_->setEnabled(editable); + rename_project_item_action_->setEnabled( + editable && (page_selected || logic_selected)); + delete_project_item_action_->setEnabled( + editable && (page_selected || logic_selected)); + move_project_item_up_action_->setEnabled( + editable && (page_selected || logic_selected)); + move_project_item_down_action_->setEnabled( + editable && (page_selected || logic_selected)); + set_initial_page_action_->setEnabled(editable && page_selected); + toggle_logic_enabled_action_->setEnabled(editable && logic_selected); + if (logic_selected) + { + const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString()); + const ControlLogic *logic = logic_editor_service_.findLogic(id); + toggle_logic_enabled_action_->setText( + logic != nullptr && logic->enabled + ? tr("禁用控制逻辑") : tr("启用控制逻辑")); + } +} + +void MainWindow::addPage() +{ + const QString name = QInputDialog::getText( + this, tr("新建页面"), tr("页面名称")); + if (name.isEmpty()) + { + return; + } + const HmiEditorResult result = hmi_editor_service_.addPage(toUtf8(name)); + if (!result.succeeded) + { + showProjectResult(tr("新建页面"), fromUtf8(result.message), false); + return; + } + current_hmi_page_id_ = result.id; + selected_control_id_.clear(); + ui_->editorTabWidget->setCurrentWidget(ui_->hmiEditorTab); + refreshProjectUi(); + hmi_editor_widget_->reloadPage(); +} + +void MainWindow::addLogic() +{ + const QString name = QInputDialog::getText( + this, tr("新建控制逻辑"), tr("逻辑名称")); + if (name.isEmpty()) + { + return; + } + const LogicEditorResult result = logic_editor_service_.addLogic(toUtf8(name)); + if (!result.succeeded) + { + showProjectResult(tr("新建控制逻辑"), fromUtf8(result.message), false); + return; + } + current_logic_id_ = result.id; + selected_logic_node_id_.clear(); + ui_->editorTabWidget->setCurrentWidget(ui_->logicEditorTab); + refreshProjectUi(); + logic_editor_widget_->reloadLogic(); +} + +void MainWindow::renameProjectItem() +{ + QTreeWidgetItem *item = ui_->projectTree->currentItem(); + if (item == nullptr) + { + return; + } + const auto kind = static_cast( + item->data(0, kProjectTreeKindRole).toInt()); + const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString()); + QString current_name; + if (kind == ProjectTreeItemKind::HmiPage) + { + const HmiPage *page = hmi_editor_service_.findPage(id); + current_name = page == nullptr ? QString{} : fromUtf8(page->name); + } + else if (kind == ProjectTreeItemKind::ControlLogic) + { + const ControlLogic *logic = logic_editor_service_.findLogic(id); + current_name = logic == nullptr ? QString{} : fromUtf8(logic->name); + } + else + { + return; + } + bool accepted = false; + const QString name = QInputDialog::getText( + this, tr("重命名"), tr("新名称"), QLineEdit::Normal, + current_name, &accepted); + if (!accepted) + { + return; + } + if (kind == ProjectTreeItemKind::HmiPage) + { + const HmiEditorResult result = hmi_editor_service_.renamePage(id, toUtf8(name)); + if (!result.succeeded) + { + showProjectResult(tr("重命名页面"), fromUtf8(result.message), false); + return; + } + } + else + { + const LogicEditorResult result = logic_editor_service_.renameLogic(id, toUtf8(name)); + if (!result.succeeded) + { + showProjectResult(tr("重命名控制逻辑"), fromUtf8(result.message), false); + return; + } + } + refreshProjectUi(); +} + +void MainWindow::deleteProjectItem() +{ + QTreeWidgetItem *item = ui_->projectTree->currentItem(); + if (item == nullptr) + { + return; + } + const auto kind = static_cast( + item->data(0, kProjectTreeKindRole).toInt()); + const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString()); + if (kind == ProjectTreeItemKind::HmiPage) + { + const HmiEditorResult result = hmi_editor_service_.removePage(id); + if (!result.succeeded) + { + showProjectResult(tr("删除页面"), fromUtf8(result.message), false); + return; + } + } + else if (kind == ProjectTreeItemKind::ControlLogic) + { + const LogicEditorResult result = logic_editor_service_.removeLogic(id); + if (!result.succeeded) + { + showProjectResult(tr("删除控制逻辑"), fromUtf8(result.message), false); + return; + } + } + else + { + return; + } + refreshProjectUi(); + showControlProperties({}); + showLogicNodeProperties({}); +} + +void MainWindow::moveProjectItem(int offset) +{ + QTreeWidgetItem *item = ui_->projectTree->currentItem(); + if (item == nullptr) + { + return; + } + const auto kind = static_cast( + item->data(0, kProjectTreeKindRole).toInt()); + const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString()); + if (kind == ProjectTreeItemKind::HmiPage) + { + const HmiEditorResult result = hmi_editor_service_.movePage(id, offset); + if (!result.succeeded) + { + statusBar()->showMessage(fromUtf8(result.message), 4000); + return; + } + } + else if (kind == ProjectTreeItemKind::ControlLogic) + { + const LogicEditorResult result = logic_editor_service_.moveLogic(id, offset); + if (!result.succeeded) + { + statusBar()->showMessage(fromUtf8(result.message), 4000); + return; + } + } + refreshProjectUi(); +} + +void MainWindow::setSelectedPageAsInitial() +{ + QTreeWidgetItem *item = ui_->projectTree->currentItem(); + if (item == nullptr + || static_cast( + item->data(0, kProjectTreeKindRole).toInt()) + != ProjectTreeItemKind::HmiPage) + { + return; + } + const HmiEditorResult result = hmi_editor_service_.setInitialPage( + toUtf8(item->data(0, kProjectTreeIdRole).toString())); + if (!result.succeeded) + { + showProjectResult(tr("设置初始页面"), fromUtf8(result.message), false); + return; + } + refreshProjectUi(); +} + +void MainWindow::toggleSelectedLogicEnabled() +{ + QTreeWidgetItem *item = ui_->projectTree->currentItem(); + if (item == nullptr + || static_cast( + item->data(0, kProjectTreeKindRole).toInt()) + != ProjectTreeItemKind::ControlLogic) + { + return; + } + const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString()); + const ControlLogic *logic = logic_editor_service_.findLogic(id); + if (logic == nullptr) + { + return; + } + const LogicEditorResult result = logic_editor_service_.setLogicEnabled( + id, !logic->enabled); + if (!result.succeeded) + { + showProjectResult(tr("启停控制逻辑"), fromUtf8(result.message), false); + return; + } + refreshProjectUi(); } void MainWindow::connectPlc() @@ -682,14 +1156,16 @@ void MainWindow::showControlProperties(const std::string &control_id) // 在第一页画面中,根据ID查找目标控件,获取只读模型指针 const HmiControl *control = hmi_editor_service_.findControl( - hmi_editor_service_.firstPageId(), control_id); + current_hmi_page_id_, control_id); // 判断是否查询到有效控件 const bool has_control = control != nullptr; // 更新属性面板顶部状态标签,显示选中控件ID或提示未选择 ui_->selectionValueLabel->setText( has_control ? fromUtf8(control->id) - + (control->isConfigured() ? QString{} : tr("(未绑定)")) + + (control->isConfigured() ? QString{} + : control->type == HmiControlType::PageJump + ? tr("(未配置)") : tr("(未绑定)")) : tr("未选择")); // 批量控制所有属性编辑组件可用性:无选中控件时整体置灰,禁止编辑 @@ -701,6 +1177,7 @@ void MainWindow::showControlProperties(const std::string &control_id) static_cast(ui_->controlHeightSpinBox), static_cast(ui_->bindingAreaComboBox), static_cast(ui_->bindingIndexSpinBox), + static_cast(ui_->targetPageComboBox), static_cast(ui_->buttonOperationComboBox), static_cast(ui_->applyPropertiesButton)}) { @@ -710,6 +1187,12 @@ void MainWindow::showControlProperties(const std::string &control_id) // 未选中有效控件,无需填充属性,直接退出 if (!has_control) { + ui_->bindingAreaLabel->setVisible(false); + ui_->bindingAreaComboBox->setVisible(false); + ui_->bindingIndexLabel->setVisible(false); + ui_->bindingIndexSpinBox->setVisible(false); + ui_->targetPageLabel->setVisible(false); + ui_->targetPageComboBox->setVisible(false); ui_->buttonOperationLabel->setVisible(false); ui_->buttonOperationComboBox->setVisible(false); return; @@ -731,6 +1214,12 @@ void MainWindow::showControlProperties(const std::string &control_id) : control->binding->area() == RegisterArea::M ? 1 : 2); ui_->bindingIndexSpinBox->setValue( control->binding.has_value() ? control->binding->index() : 0); + const bool has_binding = control->type != HmiControlType::Label + && control->type != HmiControlType::PageJump; + ui_->bindingAreaLabel->setVisible(has_binding); + ui_->bindingAreaComboBox->setVisible(has_binding); + ui_->bindingIndexLabel->setVisible(has_binding); + ui_->bindingIndexSpinBox->setVisible(has_binding); const bool is_button = control->type == HmiControlType::Button; ui_->buttonOperationLabel->setVisible(is_button); ui_->buttonOperationComboBox->setVisible(is_button); @@ -738,6 +1227,23 @@ void MainWindow::showControlProperties(const std::string &control_id) ui_->buttonOperationComboBox->setCurrentIndex( ui_->buttonOperationComboBox->findData( static_cast(control->buttonOperation))); + const bool is_page_jump = control->type == HmiControlType::PageJump; + ui_->targetPageLabel->setVisible(is_page_jump); + ui_->targetPageComboBox->setVisible(is_page_jump); + ui_->targetPageComboBox->setEnabled(is_page_jump); + ui_->targetPageComboBox->clear(); + if (is_page_jump) + { + for (const HmiPage &page : project_service_.project().hmiPages) + { + ui_->targetPageComboBox->addItem( + fromUtf8(page.name), fromUtf8(page.id)); + } + const std::string target_id = control->pageJump.has_value() + ? control->pageJump->targetPageId : std::string{}; + ui_->targetPageComboBox->setCurrentIndex( + ui_->targetPageComboBox->findData(fromUtf8(target_id))); + } } void MainWindow::showLogicNodeProperties(const std::string &node_id) @@ -745,7 +1251,7 @@ void MainWindow::showLogicNodeProperties(const std::string &node_id) ui_->propertyStack->setCurrentWidget(ui_->logicPropertiesPage); selected_logic_node_id_ = node_id; const LogicNode *node = logic_editor_service_.findNode( - logic_editor_service_.firstLogicId(), node_id); + current_logic_id_, node_id); const bool has_node = node != nullptr; ui_->selectionValueLabel->setText( has_node ? fromUtf8(node->id) : tr("未选择")); @@ -827,8 +1333,8 @@ void MainWindow::showLogicNodeProperties(const std::string &node_id) void MainWindow::addHmiControl(HmiControlType type) { - const std::string page_id = hmi_editor_service_.firstPageId(); - const HmiEditorResult result = hmi_editor_service_.addControl(page_id, type); + const HmiEditorResult result = hmi_editor_service_.addControl( + current_hmi_page_id_, type); if (!result.succeeded) { showProjectResult(tr("添加控件"), fromUtf8(result.message), false); @@ -848,7 +1354,7 @@ void MainWindow::deleteSelectedControl() return; } const HmiEditorResult result = hmi_editor_service_.removeControl( - hmi_editor_service_.firstPageId(), selected_control_id_); + current_hmi_page_id_, selected_control_id_); if (!result.succeeded) { showProjectResult(tr("删除控件"), fromUtf8(result.message), false); @@ -863,7 +1369,7 @@ void MainWindow::deleteSelectedControl() void MainWindow::applySelectedControlProperties() { const HmiControl *old_control = hmi_editor_service_.findControl( - hmi_editor_service_.firstPageId(), selected_control_id_); + current_hmi_page_id_, selected_control_id_); if (old_control == nullptr) { return; @@ -876,23 +1382,38 @@ void MainWindow::applySelectedControlProperties() control.bounds.y = ui_->controlYSpinBox->value(); control.bounds.width = ui_->controlWidthSpinBox->value(); control.bounds.height = ui_->controlHeightSpinBox->value(); - const int binding_area = ui_->bindingAreaComboBox->currentData().toInt(); - if (binding_area < 0) + if (control.type == HmiControlType::Label + || control.type == HmiControlType::PageJump) { control.binding.reset(); } else { - const RegisterArea area = binding_area == 0 ? RegisterArea::M : RegisterArea::D; - control.binding = RegisterAddress{area, ui_->bindingIndexSpinBox->value()}; + const int binding_area = ui_->bindingAreaComboBox->currentData().toInt(); + if (binding_area < 0) + { + control.binding.reset(); + } + else + { + const RegisterArea area = binding_area == 0 + ? RegisterArea::M : RegisterArea::D; + control.binding = RegisterAddress{ + area, ui_->bindingIndexSpinBox->value()}; + } } if (control.type == HmiControlType::Button) { control.buttonOperation = static_cast( ui_->buttonOperationComboBox->currentData().toInt()); } + if (control.type == HmiControlType::PageJump) + { + control.pageJump = HmiPageJumpConfig{ + toUtf8(ui_->targetPageComboBox->currentData().toString())}; + } const HmiEditorResult result = hmi_editor_service_.updateControl( - hmi_editor_service_.firstPageId(), selected_control_id_, control); + current_hmi_page_id_, selected_control_id_, control); if (!result.succeeded) { showProjectResult(tr("应用属性"), fromUtf8(result.message), false); @@ -975,9 +1496,8 @@ void MainWindow::deleteSelectedLogicObject() void MainWindow::applySelectedLogicNodeProperties() { - const std::string logic_id = logic_editor_service_.firstLogicId(); const LogicNode *node = logic_editor_service_.findNode( - logic_id, selected_logic_node_id_); + current_logic_id_, selected_logic_node_id_); if (node == nullptr) { return; @@ -1006,7 +1526,7 @@ void MainWindow::applySelectedLogicNodeProperties() } const LogicEditorResult result = logic_editor_service_.updateNodeConfig( - logic_id, selected_logic_node_id_, config); + current_logic_id_, selected_logic_node_id_, config); if (!result.succeeded) { showProjectResult(tr("应用逻辑属性"), fromUtf8(result.message), false); @@ -1037,6 +1557,8 @@ void MainWindow::createNewProject() logic_editor_service_.ensureDefaultLogic(); selected_control_id_.clear(); selected_logic_node_id_.clear(); + current_hmi_page_id_ = project_service_.project().initialHmiPageId; + current_logic_id_ = logic_editor_service_.firstLogicId(); refreshProjectUi(); hmi_editor_widget_->reloadPage(); logic_editor_widget_->reloadLogic(); @@ -1086,6 +1608,8 @@ void MainWindow::loadProject() } selected_control_id_.clear(); selected_logic_node_id_.clear(); + current_hmi_page_id_ = project_service_.project().initialHmiPageId; + current_logic_id_ = logic_editor_service_.firstLogicId(); refreshProjectUi(); hmi_editor_widget_->reloadPage(); logic_editor_widget_->reloadLogic(); @@ -1165,14 +1689,28 @@ void MainWindow::updateModeUi(const QString &message) ui_->editorTabWidget->setTabVisible(2, running); if (running) { + if (hmi_navigation_service_->currentPageId().empty()) + { + const HmiNavigationResult navigation = hmi_navigation_service_->start(); + if (!navigation.succeeded) + { + statusBar()->showMessage(fromUtf8(navigation.message), 5000); + } + } runtime_monitor_widget_->setProjectObjects( - hmi_editor_service_.firstPageId(), logic_editor_service_.firstLogicId()); + hmi_navigation_service_->currentPageId(), current_logic_id_); runtime_monitor_widget_->setMode(mode, runtime_mode_service_.plcConnectionState()); ui_->editorTabWidget->setCurrentWidget(ui_->runtimeMonitorTab); } - else if (ui_->editorTabWidget->currentWidget() == ui_->runtimeMonitorTab) + else { - ui_->editorTabWidget->setCurrentIndex(last_editing_tab_index_); + hmi_navigation_service_->stop(); + runtime_monitor_widget_->setMode( + mode, runtime_mode_service_.plcConnectionState()); + if (ui_->editorTabWidget->currentWidget() == ui_->runtimeMonitorTab) + { + ui_->editorTabWidget->setCurrentIndex(last_editing_tab_index_); + } } // 将同一份模式策略同步到所有可编辑入口,避免只禁用部分操作 ui_->projectDock->setEnabled(policy.allowsProjectEditing); @@ -1191,6 +1729,8 @@ void MainWindow::updateModeUi(const QString &message) ui_->addIndicatorAction->setEnabled(policy.allowsProjectEditing); ui_->addNumericDisplayAction->setEnabled(policy.allowsProjectEditing); ui_->addNumericInputAction->setEnabled(policy.allowsProjectEditing); + ui_->addLabelAction->setEnabled(policy.allowsProjectEditing); + ui_->addPageJumpAction->setEnabled(policy.allowsProjectEditing); ui_->deleteControlAction->setEnabled(policy.allowsProjectEditing); ui_->addNormallyOpenAction->setEnabled(policy.allowsProjectEditing); ui_->addNormallyClosedAction->setEnabled(policy.allowsProjectEditing); @@ -1203,6 +1743,7 @@ void MainWindow::updateModeUi(const QString &message) ui_->saveProjectAction->setEnabled(policy.allowsProjectEditing); ui_->saveAsProjectAction->setEnabled(policy.allowsProjectEditing); ui_->loadProjectAction->setEnabled(policy.allowsProjectEditing); + updateProjectTreeActions(); mode_status_label_->setText(modeText(mode)); if (mode == ApplicationMode::Editing) @@ -1300,8 +1841,17 @@ void MainWindow::updateSimulationUi(bool report_fault) if (report_fault) { const LogicScanResult &error = runtime_mode_service_.simulationError(); + if (!error.logicId.empty() + && logic_editor_service_.findLogic(error.logicId) != nullptr) + { + current_logic_id_ = error.logicId; + logic_editor_widget_->setLogicId(current_logic_id_); + runtime_monitor_widget_->setProjectObjects( + hmi_navigation_service_->currentPageId(), current_logic_id_); + } logic_editor_widget_->setRuntimeTrace( - runtime_mode_service_.offlineSimulationService().traceSnapshot(), + runtime_mode_service_.offlineSimulationService() + .traceSnapshot().forLogic(current_logic_id_), error.nodeId); runtime_monitor_widget_->setLogicTrace( runtime_mode_service_.offlineSimulationService().traceSnapshot(), diff --git a/app/src/ui/main_window.h b/app/src/ui/main_window.h index 4a39000..2219308 100644 --- a/app/src/ui/main_window.h +++ b/app/src/ui/main_window.h @@ -18,6 +18,7 @@ #include QT_BEGIN_NAMESPACE +class QAction; class QActionGroup; class QLabel; class QTimer; @@ -31,6 +32,7 @@ class RuntimeModeService; class ProjectService; class HmiEditorService; class HmiRuntimeService; +class HmiNavigationService; class HmiEditorWidget; class LogicEditorService; class LogicEditorWidget; @@ -63,9 +65,21 @@ public: HmiRuntimeService &hmi_runtime_service, RegisterMonitorService ®ister_monitor_service, QWidget *parent = nullptr); + MainWindow( + RuntimeModeService &runtime_mode_service, + ProjectService &project_service, + HmiEditorService &hmi_editor_service, + LogicEditorService &logic_editor_service, + HmiRuntimeService &hmi_runtime_service, + HmiNavigationService &hmi_navigation_service, + RegisterMonitorService ®ister_monitor_service, + QWidget *parent = nullptr); ~MainWindow() override; + const std::string ¤tHmiPageId() const; + const std::string ¤tLogicId() const; + private: // 配置菜单和工具栏动作 void configureActions(); @@ -76,11 +90,21 @@ private: // 创建并连接控制逻辑编辑画布 void configureLogicEditor(); void configureRuntimeMonitor(); + void configureProjectTree(); // 创建并连接控件属性编辑表单 void configurePropertyEditor(); void configurePlcConnection(); // 刷新工程树和当前 HMI 页面信息 void refreshProjectUi(); + void handleProjectTreeSelection(); + void updateProjectTreeActions(); + void addPage(); + void addLogic(); + void renameProjectItem(); + void deleteProjectItem(); + void moveProjectItem(int offset); + void setSelectedPageAsInitial(); + void toggleSelectedLogicEnabled(); // 显示指定 HMI 控件的可编辑属性 void showControlProperties(const std::string &control_id); // 显示指定控制逻辑节点的可编辑属性 @@ -138,6 +162,7 @@ private: * @brief 将互斥模式动作同步到服务层当前状态 */ void restoreCurrentModeAction(); + void initializeUi(); std::unique_ptr ui_; /** @@ -152,6 +177,8 @@ private: LogicEditorService &logic_editor_service_; HmiRuntimeService &hmi_runtime_service_; RegisterMonitorService ®ister_monitor_service_; + std::unique_ptr owned_hmi_navigation_service_; + HmiNavigationService *hmi_navigation_service_ = nullptr; QActionGroup *mode_action_group_ = nullptr; HmiEditorWidget *hmi_editor_widget_ = nullptr; LogicEditorWidget *logic_editor_widget_ = nullptr; @@ -160,9 +187,19 @@ private: QLabel *mode_status_label_ = nullptr; QLabel *register_status_label_ = nullptr; QLabel *executor_status_label_ = nullptr; + QAction *add_page_action_ = nullptr; + QAction *add_logic_action_ = nullptr; + QAction *rename_project_item_action_ = nullptr; + QAction *delete_project_item_action_ = nullptr; + QAction *move_project_item_up_action_ = nullptr; + QAction *move_project_item_down_action_ = nullptr; + QAction *set_initial_page_action_ = nullptr; + QAction *toggle_logic_enabled_action_ = nullptr; PlcSerialConfiguration plc_configuration_; std::string selected_control_id_; std::string selected_logic_node_id_; + std::string current_hmi_page_id_; + std::string current_logic_id_; bool plc_status_update_pending_ = false; int last_editing_tab_index_ = 0; }; diff --git a/app/src/ui/main_window.ui b/app/src/ui/main_window.ui index dc396e1..13aa4c8 100644 --- a/app/src/ui/main_window.ui +++ b/app/src/ui/main_window.ui @@ -302,6 +302,8 @@ + + @@ -575,14 +577,24 @@ - + + + + 目标页面 + + + + + + + 按钮操作 - + @@ -606,7 +618,7 @@ - + 应用属性 @@ -874,6 +886,22 @@ 添加数值输入控件 + + + 文本 + + + 添加固定文本控件 + + + + + 页面跳转 + + + 添加页面跳转控件 + + 删除 diff --git a/app/src/ui/runtime_monitor_widget.cpp b/app/src/ui/runtime_monitor_widget.cpp index 6aad488..bafcaa2 100644 --- a/app/src/ui/runtime_monitor_widget.cpp +++ b/app/src/ui/runtime_monitor_widget.cpp @@ -3,18 +3,43 @@ #include "free_monitor_widget.h" #include "hmi_editor_widget.h" #include "logic_editor_widget.h" +#include "services/hmi_navigation_service.h" +#include "services/project_service.h" #include "ui_runtime_monitor_widget.h" #include +#include +#include + +#include + +namespace { + +QString fromUtf8(const std::string &value) +{ + return QString::fromUtf8(value.data(), static_cast(value.size())); +} + +std::string toUtf8(const QString &value) +{ + const QByteArray bytes = value.toUtf8(); + return std::string(bytes.constData(), static_cast(bytes.size())); +} + +} // namespace RuntimeMonitorWidget::RuntimeMonitorWidget( HmiEditorService &hmi_editor_service, HmiRuntimeService &hmi_runtime_service, LogicEditorService &logic_editor_service, + ProjectService &project_service, + HmiNavigationService &hmi_navigation_service, RegisterMonitorService ®ister_monitor_service, QWidget *parent) : QWidget(parent), - ui_(std::make_unique()) + ui_(std::make_unique()), + project_service_(project_service), + hmi_navigation_service_(hmi_navigation_service) { ui_->setupUi(this); hmi_view_ = new HmiEditorWidget( @@ -35,6 +60,31 @@ RuntimeMonitorWidget::RuntimeMonitorWidget( ui_->monitorContainerLayout->addWidget(free_monitor_widget_); ui_->upperSplitter->setSizes({600, 600}); ui_->verticalSplitter->setSizes({440, 260}); + connect(hmi_view_, &HmiEditorWidget::pageNavigationRequested, + this, + [this](const QString &target_page_id) + { + const HmiNavigationResult result = + hmi_navigation_service_.navigateTo(toUtf8(target_page_id)); + if (!result.succeeded) + { + emit navigationFailed(fromUtf8(result.message)); + return; + } + showRuntimePage(result.pageId); + emit runtimePageChanged(fromUtf8(result.pageId)); + }); + connect(ui_->runtimeLogicComboBox, + QOverload::of(&QComboBox::currentIndexChanged), + this, + [this](int index) + { + if (index >= 0) + { + selectRuntimeLogic(toUtf8( + ui_->runtimeLogicComboBox->itemData(index).toString())); + } + }); } RuntimeMonitorWidget::~RuntimeMonitorWidget() = default; @@ -42,12 +92,27 @@ RuntimeMonitorWidget::~RuntimeMonitorWidget() = default; void RuntimeMonitorWidget::setProjectObjects( const std::string &page_id, const std::string &logic_id) { - hmi_view_->setPageId(page_id); - // 运行视图是独立投影,同一页面的控件也可能已在编辑视图中发生变化 - hmi_view_->reloadPage(); - logic_view_->setLogicId(logic_id); - // 同一逻辑 ID 下的网络结构变化同样需要在首次扫描前立即可见 - logic_view_->reloadLogic(); + showRuntimePage(page_id); + + const QSignalBlocker blocker(ui_->runtimeLogicComboBox); + ui_->runtimeLogicComboBox->clear(); + for (const ControlLogic &logic : project_service_.project().controlLogics) + { + if (logic.enabled) + { + ui_->runtimeLogicComboBox->addItem( + fromUtf8(logic.name), fromUtf8(logic.id)); + } + } + int selected_index = ui_->runtimeLogicComboBox->findData(fromUtf8(logic_id)); + if (selected_index < 0 && ui_->runtimeLogicComboBox->count() > 0) + { + selected_index = 0; + } + ui_->runtimeLogicComboBox->setCurrentIndex(selected_index); + selectRuntimeLogic(selected_index >= 0 + ? toUtf8(ui_->runtimeLogicComboBox->itemData(selected_index).toString()) + : std::string{}); } void RuntimeMonitorWidget::setMode( @@ -62,6 +127,9 @@ void RuntimeMonitorWidget::setMode( if (!offline) { logic_view_->clearRuntimeTrace(); + latest_trace_.clear(); + latest_fault_node_id_.clear(); + has_logic_trace_ = false; } ui_->modeLabel->setText( offline ? tr("离线仿真 · 虚拟 M/D") @@ -84,10 +152,44 @@ void RuntimeMonitorWidget::refreshValues( void RuntimeMonitorWidget::setLogicTrace( const LogicTraceSnapshot &trace, const std::string &fault_node_id) { - logic_view_->setRuntimeTrace(trace, fault_node_id); + latest_trace_ = trace; + latest_fault_node_id_ = fault_node_id; + has_logic_trace_ = true; + logic_view_->setRuntimeTrace( + trace.forLogic(selected_logic_id_), fault_node_id); } FreeMonitorWidget *RuntimeMonitorWidget::freeMonitorWidget() const { return free_monitor_widget_; } + +std::string RuntimeMonitorWidget::selectedLogicId() const +{ + return selected_logic_id_; +} + +void RuntimeMonitorWidget::showRuntimePage(const std::string &page_id) +{ + hmi_view_->setPageId(page_id); + hmi_view_->reloadPage(); + const auto page = std::find_if( + project_service_.project().hmiPages.cbegin(), + project_service_.project().hmiPages.cend(), + [&page_id](const HmiPage &candidate) { return candidate.id == page_id; }); + ui_->runtimePageLabel->setText( + page == project_service_.project().hmiPages.cend() + ? tr("未选择页面") : fromUtf8(page->name)); +} + +void RuntimeMonitorWidget::selectRuntimeLogic(const std::string &logic_id) +{ + selected_logic_id_ = logic_id; + logic_view_->setLogicId(logic_id); + logic_view_->reloadLogic(); + if (has_logic_trace_) + { + logic_view_->setRuntimeTrace( + latest_trace_.forLogic(logic_id), latest_fault_node_id_); + } +} diff --git a/app/src/ui/runtime_monitor_widget.h b/app/src/ui/runtime_monitor_widget.h index 5147833..a6ac8ea 100644 --- a/app/src/ui/runtime_monitor_widget.h +++ b/app/src/ui/runtime_monitor_widget.h @@ -2,6 +2,7 @@ #include "domain/runtime_state.h" #include "services/plc_communication_gateway.h" +#include "services/software_logic_executor.h" #include @@ -16,10 +17,11 @@ class FreeMonitorWidget; class HmiEditorService; class HmiEditorWidget; class HmiRuntimeService; +class HmiNavigationService; class LogicEditorService; class LogicEditorWidget; class RegisterMonitorService; -struct LogicTraceSnapshot; +class ProjectService; class RuntimeMonitorWidget final : public QWidget { @@ -30,6 +32,8 @@ public: HmiEditorService &hmi_editor_service, HmiRuntimeService &hmi_runtime_service, LogicEditorService &logic_editor_service, + ProjectService &project_service, + HmiNavigationService &hmi_navigation_service, RegisterMonitorService ®ister_monitor_service, QWidget *parent = nullptr); ~RuntimeMonitorWidget() override; @@ -40,10 +44,24 @@ public: void setHmiWriteEnabled(bool enabled); void setLogicTrace(const LogicTraceSnapshot &trace, const std::string &fault_node_id = {}); FreeMonitorWidget *freeMonitorWidget() const; + std::string selectedLogicId() const; + +signals: + void navigationFailed(const QString &message); + void runtimePageChanged(const QString &page_id); private: + void showRuntimePage(const std::string &page_id); + void selectRuntimeLogic(const std::string &logic_id); + std::unique_ptr ui_; + ProjectService &project_service_; + HmiNavigationService &hmi_navigation_service_; HmiEditorWidget *hmi_view_ = nullptr; LogicEditorWidget *logic_view_ = nullptr; FreeMonitorWidget *free_monitor_widget_ = nullptr; + std::string selected_logic_id_; + LogicTraceSnapshot latest_trace_; + std::string latest_fault_node_id_; + bool has_logic_trace_ = false; }; diff --git a/app/src/ui/runtime_monitor_widget.ui b/app/src/ui/runtime_monitor_widget.ui index 9f7f4b2..7cbfa09 100644 --- a/app/src/ui/runtime_monitor_widget.ui +++ b/app/src/ui/runtime_monitor_widget.ui @@ -24,7 +24,13 @@ QFrame::StyledPanel 4444 - HMI 运行视图 + + + HMI 运行视图 + Qt::Horizontal2420 + 未选择页面Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + 0000 @@ -32,7 +38,13 @@ QFrame::StyledPanel 4444 - 梯形图运行状态 + + + 梯形图运行状态 + Qt::Horizontal2420 + 1500 + + 0000 diff --git a/app/tests/main_window_tests.cpp b/app/tests/main_window_tests.cpp index 443758c..e37bb80 100644 --- a/app/tests/main_window_tests.cpp +++ b/app/tests/main_window_tests.cpp @@ -3,6 +3,7 @@ #include "domain/register_repository.h" #include "services/hmi_editor_service.h" #include "services/hmi_runtime_service.h" +#include "services/hmi_navigation_service.h" #include "services/logic_editor_service.h" #include "services/offline_simulation_service.h" #include "services/project_service.h" @@ -13,6 +14,7 @@ #include "ui/logic_editor_widget.h" #include "ui/main_window.h" #include "ui/plc_connection_dialog.h" +#include "ui/runtime_monitor_widget.h" #include #include @@ -31,6 +33,7 @@ #include #include #include +#include #include #include @@ -341,6 +344,56 @@ void testRuntimeButtonMouseInteraction() "a disabled runtime button must not write its register"); } +void testRuntimePageJumpDoesNotRequireRegisterWritePermission() +{ + TestProjectStorage storage; + ProjectService project_service(storage); + HmiEditorService editor_service(project_service); + const std::string source_page = editor_service.ensureDefaultPage().id; + const std::string target_page = editor_service.addPage("Target").id; + const HmiEditorResult jump_result = editor_service.addControl( + source_page, HmiControlType::PageJump); + HmiControl jump = *editor_service.findControl(source_page, jump_result.id); + jump.pageJump = HmiPageJumpConfig{target_page}; + require(editor_service.updateControl( + source_page, jump_result.id, jump).succeeded, + "PageJump fixture setup must succeed"); + + VirtualRegisterRepository repository; + HmiRuntimeService runtime_service(repository); + HmiEditorWidget view(editor_service, runtime_service); + view.setPageId(source_page); + view.setEditingEnabled(false); + view.setRuntimeActive(true); + view.setRuntimeWriteEnabled(false); + view.resize(900, 560); + view.show(); + QApplication::processEvents(); + + QString requested_page; + QObject::connect( + &view, &HmiEditorWidget::pageNavigationRequested, + [&requested_page](const QString &page_id) + { + requested_page = page_id; + }); + QGraphicsItem *jump_item = nullptr; + for (QGraphicsItem *item : view.scene()->items()) + { + if (item->zValue() >= 0.0) + { + jump_item = item; + break; + } + } + require(jump_item != nullptr, "PageJump graphics item must be rendered"); + const QPoint center = view.mapFromScene( + jump_item->mapToScene(jump_item->boundingRect().center())); + QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, center); + require(requested_page == QString::fromStdString(target_page), + "PageJump must remain active when register writes are disabled"); +} + void testModeActionsControlEditingAvailability() { // 验证模式动作会同步禁用编辑入口,并在失败时恢复当前选择 @@ -776,6 +829,125 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() "faulted PLC state must not re-enter online running without a fresh read"); } +void testMultiPageAndLogicMainWindowIntegration() +{ + TestProjectStorage storage; + ProjectService project_service(storage); + HmiEditorService editor_service(project_service); + LogicEditorService logic_editor_service(project_service); + const std::string main_page_id = editor_service.ensureDefaultPage().id; + const std::string settings_page_id = editor_service.addPage("Settings").id; + const std::string first_logic_id = logic_editor_service.ensureDefaultLogic().id; + const std::string second_logic_id = + logic_editor_service.addLogic("Safety logic").id; + + const HmiEditorResult jump_result = editor_service.addControl( + main_page_id, HmiControlType::PageJump); + HmiControl jump = *editor_service.findControl(main_page_id, jump_result.id); + jump.pageJump = HmiPageJumpConfig{settings_page_id}; + require(editor_service.updateControl( + main_page_id, jump_result.id, jump).succeeded, + "the integration fixture must configure PageJump"); + + VirtualRegisterRepository repository; + HmiRuntimeService runtime_service(repository); + OfflineSimulationService simulation_service(repository); + RuntimeModeService mode_service(project_service, simulation_service); + RegisterMonitorService monitor_service(repository); + HmiNavigationService navigation_service(project_service); + MainWindow window( + mode_service, + project_service, + editor_service, + logic_editor_service, + runtime_service, + navigation_service, + monitor_service); + window.resize(1400, 820); + window.show(); + QApplication::processEvents(); + + QTreeWidget *tree = requiredChild(window, "projectTree"); + require(tree->topLevelItemCount() == 2 + && tree->topLevelItem(0)->childCount() == 2 + && tree->topLevelItem(1)->childCount() == 2, + "the project tree must list every page and logic module"); + + tree->setCurrentItem(tree->topLevelItem(0)->child(1)); + QApplication::processEvents(); + require(window.currentHmiPageId() == settings_page_id, + "selecting a page tree node must change the current HMI page id"); + QAction *add_label = requiredChild(window, "addLabelAction"); + add_label->trigger(); + require(editor_service.findPage(settings_page_id)->controls.size() == 1U + && editor_service.findPage(main_page_id)->controls.size() == 1U, + "HMI toolbar actions must edit the selected page rather than the first page"); + QComboBox *binding_area = requiredChild(window, "bindingAreaComboBox"); + QComboBox *target_page = requiredChild(window, "targetPageComboBox"); + require(!binding_area->isVisible() && !target_page->isVisible(), + "Label properties must hide both register binding and PageJump target fields"); + + tree->setCurrentItem(tree->topLevelItem(1)->child(1)); + QApplication::processEvents(); + require(window.currentLogicId() == second_logic_id, + "selecting a logic tree node must change the current logic id"); + requiredChild(window, "addRungAction")->trigger(); + require(logic_editor_service.findLogic(second_logic_id)->rungs.size() == 2U + && logic_editor_service.findLogic(first_logic_id)->rungs.size() == 1U, + "logic toolbar actions must edit the selected logic module"); + + tree->setCurrentItem(tree->topLevelItem(0)->child(0)); + QApplication::processEvents(); + HmiEditorWidget *editor = requiredChild(window, "hmiEditorWidget"); + editor->selectControl(jump_result.id); + QApplication::processEvents(); + require(target_page->isVisible() && !binding_area->isVisible() + && target_page->currentData().toString() + == QString::fromStdString(settings_page_id), + "PageJump properties must show a target-page selector without register fields"); + + requiredChild(window, "offlineModeAction")->trigger(); + QApplication::processEvents(); + require(mode_service.mode() == ApplicationMode::OfflineRunning + && navigation_service.currentPageId() == main_page_id, + "runtime HMI navigation must start on the persisted initial page"); + HmiEditorWidget *runtime_hmi = requiredChild( + window, "runtimeHmiView"); + QGraphicsItem *jump_item = nullptr; + for (QGraphicsItem *item : runtime_hmi->scene()->items()) + { + if (item->zValue() >= 0.0) + { + jump_item = item; + break; + } + } + require(jump_item != nullptr, "runtime HMI must render the PageJump control"); + const QPoint jump_center = runtime_hmi->mapFromScene( + jump_item->mapToScene(jump_item->boundingRect().center())); + QTest::mouseClick(runtime_hmi->viewport(), Qt::LeftButton, + Qt::NoModifier, jump_center); + QApplication::processEvents(); + require(navigation_service.currentPageId() == settings_page_id + && requiredChild(window, "runtimePageLabel")->text() + == QStringLiteral("Settings"), + "clicking PageJump at runtime must navigate locally to its target page"); + + QComboBox *logic_selector = requiredChild( + window, "runtimeLogicComboBox"); + require(logic_selector->count() == 2, + "runtime logic selector must list all enabled logic modules"); + logic_selector->setCurrentIndex( + logic_selector->findData(QString::fromStdString(second_logic_id))); + QApplication::processEvents(); + auto *runtime_monitor = requiredChild( + window, "runtimeMonitorWidget"); + require(runtime_monitor->selectedLogicId() == second_logic_id, + "runtime logic selection must change only the visible trace projection"); + + requiredChild(window, "editingModeAction")->trigger(); +} + } // namespace int main(int argc, char *argv[]) @@ -787,10 +959,12 @@ int main(int argc, char *argv[]) try { testRuntimeButtonMouseInteraction(); + testRuntimePageJumpDoesNotRequireRegisterWritePermission(); testModeActionsControlEditingAvailability(); testPlcConfigurationUsesDialog(); testRepeatedPlcStatusNotificationsAreCoalesced(); testOnlineWorkspaceShowsHmiAndFreeMonitorOnly(); + testMultiPageAndLogicMainWindowIntegration(); } catch (const std::exception &error) { diff --git a/app/tests/main_window_tests.pro b/app/tests/main_window_tests.pro index 4da01b9..dd90cb4 100644 --- a/app/tests/main_window_tests.pro +++ b/app/tests/main_window_tests.pro @@ -26,6 +26,7 @@ SOURCES += \ ../src/domain/runtime_state.cpp \ ../src/services/project_service.cpp \ ../src/services/hmi_editor_service.cpp \ + ../src/services/hmi_navigation_service.cpp \ ../src/services/logic_editor_service.cpp \ ../src/services/hmi_runtime_service.cpp \ ../src/services/software_logic_executor.cpp \ @@ -51,6 +52,7 @@ HEADERS += \ ../src/domain/runtime_state.h \ ../src/services/project_service.h \ ../src/services/hmi_editor_service.h \ + ../src/services/hmi_navigation_service.h \ ../src/services/logic_editor_service.h \ ../src/services/hmi_runtime_service.h \ ../src/services/software_logic_executor.h \