| @@ -30,6 +30,7 @@ SOURCES += \ | |||||
| src/domain/runtime_state.cpp \ | src/domain/runtime_state.cpp \ | ||||
| src/services/project_service.cpp \ | src/services/project_service.cpp \ | ||||
| src/services/hmi_editor_service.cpp \ | src/services/hmi_editor_service.cpp \ | ||||
| src/services/hmi_navigation_service.cpp \ | |||||
| src/services/logic_editor_service.cpp \ | src/services/logic_editor_service.cpp \ | ||||
| src/services/hmi_runtime_service.cpp \ | src/services/hmi_runtime_service.cpp \ | ||||
| src/services/software_logic_executor.cpp \ | src/services/software_logic_executor.cpp \ | ||||
| @@ -59,6 +60,7 @@ HEADERS += \ | |||||
| src/domain/project_storage.h \ | src/domain/project_storage.h \ | ||||
| src/services/project_service.h \ | src/services/project_service.h \ | ||||
| src/services/hmi_editor_service.h \ | src/services/hmi_editor_service.h \ | ||||
| src/services/hmi_navigation_service.h \ | |||||
| src/services/logic_editor_service.h \ | src/services/logic_editor_service.h \ | ||||
| src/services/hmi_runtime_service.h \ | src/services/hmi_runtime_service.h \ | ||||
| src/services/software_logic_executor.h \ | src/services/software_logic_executor.h \ | ||||
| @@ -14,6 +14,7 @@ | |||||
| #include "domain/active_register_repository.h" | #include "domain/active_register_repository.h" | ||||
| #include "services/hmi_editor_service.h" | #include "services/hmi_editor_service.h" | ||||
| #include "services/hmi_runtime_service.h" | #include "services/hmi_runtime_service.h" | ||||
| #include "services/hmi_navigation_service.h" | |||||
| #include "services/logic_editor_service.h" | #include "services/logic_editor_service.h" | ||||
| #include "services/offline_simulation_service.h" | #include "services/offline_simulation_service.h" | ||||
| #include "services/project_service.h" | #include "services/project_service.h" | ||||
| @@ -31,6 +32,7 @@ int main(int argc, char *argv[]) | |||||
| JsonProjectStorage project_storage; | JsonProjectStorage project_storage; | ||||
| ProjectService project_service(project_storage); | ProjectService project_service(project_storage); | ||||
| HmiEditorService hmi_editor_service(project_service); | HmiEditorService hmi_editor_service(project_service); | ||||
| HmiNavigationService hmi_navigation_service(project_service); | |||||
| LogicEditorService logic_editor_service(project_service); | LogicEditorService logic_editor_service(project_service); | ||||
| // 当前离线模式使用内存仓库,后续真机模式替换为 PLC 缓存实现 | // 当前离线模式使用内存仓库,后续真机模式替换为 PLC 缓存实现 | ||||
| VirtualRegisterRepository virtual_register_repository; | VirtualRegisterRepository virtual_register_repository; | ||||
| @@ -53,6 +55,7 @@ int main(int argc, char *argv[]) | |||||
| hmi_editor_service, | hmi_editor_service, | ||||
| logic_editor_service, | logic_editor_service, | ||||
| hmi_runtime_service, | hmi_runtime_service, | ||||
| hmi_navigation_service, | |||||
| register_monitor_service); | register_monitor_service); | ||||
| main_window.show(); | main_window.show(); | ||||
| @@ -32,13 +32,15 @@ public: | |||||
| int page_height, | int page_height, | ||||
| std::function<void(const std::string &, const QPointF &)> moved, | std::function<void(const std::string &, const QPointF &)> moved, | ||||
| std::function<void(const std::string &, HmiButtonEvent)> button_event, | std::function<void(const std::string &, HmiButtonEvent)> button_event, | ||||
| std::function<void(const std::string &)> numeric_input_activated) | |||||
| std::function<void(const std::string &)> numeric_input_activated, | |||||
| std::function<void(const std::string &)> page_navigation) | |||||
| : control_(control), | : control_(control), | ||||
| page_width_(page_width), | page_width_(page_width), | ||||
| page_height_(page_height), | page_height_(page_height), | ||||
| moved_(std::move(moved)), | moved_(std::move(moved)), | ||||
| button_event_(std::move(button_event)), | 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); | setPos(control_.bounds.x, control_.bounds.y); | ||||
| @@ -164,6 +166,18 @@ public: | |||||
| textWithValue()); | textWithValue()); | ||||
| break; | 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: | case HmiControlType::Label: | ||||
| default: | default: | ||||
| { | { | ||||
| @@ -207,15 +221,19 @@ public: | |||||
| const bool runtime_input = runtime_active_ | const bool runtime_input = runtime_active_ | ||||
| && (control_.type == HmiControlType::Button | && (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( | setAcceptedMouseButtons( | ||||
| editable || runtime_input_enabled ? Qt::LeftButton : Qt::NoButton); | editable || runtime_input_enabled ? Qt::LeftButton : Qt::NoButton); | ||||
| const bool runtime_button_enabled = runtime_active_ | const bool runtime_button_enabled = runtime_active_ | ||||
| && control_.type == HmiControlType::Button && runtime_write_enabled_; | && 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); | setCursor(Qt::PointingHandCursor); | ||||
| } | } | ||||
| @@ -223,6 +241,7 @@ public: | |||||
| { | { | ||||
| unsetCursor(); | unsetCursor(); | ||||
| button_hovered_ = false; | button_hovered_ = false; | ||||
| page_hovered_ = false; | |||||
| button_pressed_ = false; | button_pressed_ = false; | ||||
| } | } | ||||
| update(); | update(); | ||||
| @@ -260,6 +279,13 @@ protected: | |||||
| // 运行态按钮按下时通知外层执行配置的 M 位操作 | // 运行态按钮按下时通知外层执行配置的 M 位操作 | ||||
| void mousePressEvent(QGraphicsSceneMouseEvent *event) override | 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_ | if (runtime_active_ && runtime_write_enabled_ | ||||
| && control_.type == HmiControlType::Button && button_event_) | && control_.type == HmiControlType::Button && button_event_) | ||||
| { | { | ||||
| @@ -291,6 +317,18 @@ protected: | |||||
| // 拖拽结束后才提交最终坐标,避免移动过程频繁修改领域模型 | // 拖拽结束后才提交最终坐标,避免移动过程频繁修改领域模型 | ||||
| void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override | 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_ | if (runtime_active_ && runtime_write_enabled_ | ||||
| && control_.type == HmiControlType::Button && button_pressed_) | && control_.type == HmiControlType::Button && button_pressed_) | ||||
| { | { | ||||
| @@ -316,6 +354,11 @@ protected: | |||||
| button_hovered_ = true; | button_hovered_ = true; | ||||
| update(); | update(); | ||||
| } | } | ||||
| if (runtime_active_ && control_.type == HmiControlType::PageJump) | |||||
| { | |||||
| page_hovered_ = true; | |||||
| update(); | |||||
| } | |||||
| QGraphicsItem::hoverEnterEvent(event); | QGraphicsItem::hoverEnterEvent(event); | ||||
| } | } | ||||
| @@ -326,6 +369,11 @@ protected: | |||||
| button_hovered_ = false; | button_hovered_ = false; | ||||
| update(); | update(); | ||||
| } | } | ||||
| if (page_hovered_) | |||||
| { | |||||
| page_hovered_ = false; | |||||
| update(); | |||||
| } | |||||
| QGraphicsItem::hoverLeaveEvent(event); | QGraphicsItem::hoverLeaveEvent(event); | ||||
| } | } | ||||
| @@ -384,11 +432,14 @@ private: | |||||
| std::function<void(const std::string &, const QPointF &)> moved_; | std::function<void(const std::string &, const QPointF &)> moved_; | ||||
| std::function<void(const std::string &, HmiButtonEvent)> button_event_; | std::function<void(const std::string &, HmiButtonEvent)> button_event_; | ||||
| std::function<void(const std::string &)> numeric_input_activated_; | std::function<void(const std::string &)> numeric_input_activated_; | ||||
| std::function<void(const std::string &)> page_navigation_; | |||||
| 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; | ||||
| bool button_hovered_ = false; | bool button_hovered_ = false; | ||||
| bool button_pressed_ = false; | bool button_pressed_ = false; | ||||
| bool page_hovered_ = false; | |||||
| bool page_pressed_ = false; | |||||
| // 指示灯读取到的 M 位值 | // 指示灯读取到的 M 位值 | ||||
| bool bit_value_ = false; | bool bit_value_ = false; | ||||
| // 数值控件读取到的 D 字值 | // 数值控件读取到的 D 字值 | ||||
| @@ -501,6 +552,11 @@ void HmiEditorWidget::reloadPage() | |||||
| [this](const std::string &control_id) | [this](const std::string &control_id) | ||||
| { | { | ||||
| handleNumericInputActivated(control_id); | handleNumericInputActivated(control_id); | ||||
| }, | |||||
| [this](const std::string &target_page_id) | |||||
| { | |||||
| emit pageNavigationRequested( | |||||
| QString::fromStdString(target_page_id)); | |||||
| }); | }); | ||||
| scene_->addItem(item); | scene_->addItem(item); | ||||
| item->setInteractionState( | item->setInteractionState( | ||||
| @@ -90,6 +90,7 @@ signals: | |||||
| * @param message 可直接显示的错误信息 | * @param message 可直接显示的错误信息 | ||||
| */ | */ | ||||
| void editorError(const QString &message); | void editorError(const QString &message); | ||||
| void pageNavigationRequested(const QString &target_page_id); | |||||
| protected: | protected: | ||||
| void resizeEvent(QResizeEvent *event) override; | void resizeEvent(QResizeEvent *event) override; | ||||
| @@ -6,6 +6,7 @@ | |||||
| #include "runtime_monitor_widget.h" | #include "runtime_monitor_widget.h" | ||||
| #include "free_monitor_widget.h" | #include "free_monitor_widget.h" | ||||
| #include "services/hmi_editor_service.h" | #include "services/hmi_editor_service.h" | ||||
| #include "services/hmi_navigation_service.h" | |||||
| #include "services/hmi_runtime_service.h" | #include "services/hmi_runtime_service.h" | ||||
| #include "services/logic_editor_service.h" | #include "services/logic_editor_service.h" | ||||
| #include "services/project_service.h" | #include "services/project_service.h" | ||||
| @@ -23,6 +24,7 @@ | |||||
| #include <QMessageBox> | #include <QMessageBox> | ||||
| #include <QMenu> | #include <QMenu> | ||||
| #include <QPushButton> | #include <QPushButton> | ||||
| #include <QSignalBlocker> | |||||
| #include <QSpinBox> | #include <QSpinBox> | ||||
| #include <QStatusBar> | #include <QStatusBar> | ||||
| #include <QStyle> | #include <QStyle> | ||||
| @@ -37,6 +39,16 @@ | |||||
| namespace { | 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) | QString modeText(ApplicationMode mode) | ||||
| { | { | ||||
| switch (mode) | switch (mode) | ||||
| @@ -157,10 +169,17 @@ QString controlTypeText(HmiControlType type) | |||||
| return MainWindow::tr("数值输入"); | return MainWindow::tr("数值输入"); | ||||
| } | } | ||||
| case HmiControlType::Label: | case HmiControlType::Label: | ||||
| default: | |||||
| { | { | ||||
| return MainWindow::tr("文本"); | 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), | hmi_editor_service_(hmi_editor_service), | ||||
| logic_editor_service_(logic_editor_service), | logic_editor_service_(logic_editor_service), | ||||
| hmi_runtime_service_(hmi_runtime_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<HmiNavigationService>(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<Ui::MainWindow>()), | |||||
| 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); | ui_->setupUi(this); | ||||
| configureAppearance(); | configureAppearance(); | ||||
| @@ -191,10 +240,13 @@ MainWindow::MainWindow( | |||||
| configureHmiEditor(); | configureHmiEditor(); | ||||
| configureLogicEditor(); | configureLogicEditor(); | ||||
| configureRuntimeMonitor(); | configureRuntimeMonitor(); | ||||
| configureProjectTree(); | |||||
| runtime_mode_service_.setPlcStatusChangedCallback( | runtime_mode_service_.setPlcStatusChangedCallback( | ||||
| [this] { schedulePlcStatusUpdate(); }); | [this] { schedulePlcStatusUpdate(); }); | ||||
| hmi_editor_service_.ensureDefaultPage(); | hmi_editor_service_.ensureDefaultPage(); | ||||
| logic_editor_service_.ensureDefaultLogic(); | logic_editor_service_.ensureDefaultLogic(); | ||||
| current_hmi_page_id_ = hmi_editor_service_.firstPageId(); | |||||
| current_logic_id_ = logic_editor_service_.firstLogicId(); | |||||
| refreshProjectUi(); | refreshProjectUi(); | ||||
| updateModeUi(tr("系统已进入编辑态")); | updateModeUi(tr("系统已进入编辑态")); | ||||
| } | } | ||||
| @@ -204,6 +256,16 @@ MainWindow::~MainWindow() | |||||
| runtime_mode_service_.setPlcStatusChangedCallback({}); | 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() | void MainWindow::configureActions() | ||||
| { | { | ||||
| mode_action_group_ = new QActionGroup(this); | mode_action_group_ = new QActionGroup(this); | ||||
| @@ -232,6 +294,10 @@ void MainWindow::configureActions() | |||||
| [this] { addHmiControl(HmiControlType::NumericDisplay); }); | [this] { addHmiControl(HmiControlType::NumericDisplay); }); | ||||
| connect(ui_->addNumericInputAction, &QAction::triggered, this, | connect(ui_->addNumericInputAction, &QAction::triggered, this, | ||||
| [this] { addHmiControl(HmiControlType::NumericInput); }); | [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, | connect(ui_->deleteControlAction, &QAction::triggered, | ||||
| this, &MainWindow::deleteSelectedControl); | this, &MainWindow::deleteSelectedControl); | ||||
| @@ -349,6 +415,7 @@ void MainWindow::configureActions() | |||||
| if (index == 0 || index == 1) | if (index == 0 || index == 1) | ||||
| { | { | ||||
| last_editing_tab_index_ = index; | last_editing_tab_index_ = index; | ||||
| refreshProjectUi(); | |||||
| } | } | ||||
| }); | }); | ||||
| ui_->hmiToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 0); | ui_->hmiToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 0); | ||||
| @@ -383,6 +450,10 @@ void MainWindow::configureAppearance() | |||||
| style()->standardIcon(QStyle::SP_FileDialogInfoView)); | style()->standardIcon(QStyle::SP_FileDialogInfoView)); | ||||
| ui_->addNumericInputAction->setIcon( | ui_->addNumericInputAction->setIcon( | ||||
| style()->standardIcon(QStyle::SP_FileDialogContentsView)); | 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_->deleteControlAction->setIcon(style()->standardIcon(QStyle::SP_TrashIcon)); | ||||
| ui_->addRungAction->setIcon(style()->standardIcon(QStyle::SP_FileIcon)); | ui_->addRungAction->setIcon(style()->standardIcon(QStyle::SP_FileIcon)); | ||||
| ui_->parallelInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowDown)); | ui_->parallelInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowDown)); | ||||
| @@ -483,7 +554,8 @@ void MainWindow::configureHmiEditor() | |||||
| [this] | [this] | ||||
| { | { | ||||
| logic_editor_widget_->setRuntimeTrace( | logic_editor_widget_->setRuntimeTrace( | ||||
| runtime_mode_service_.offlineSimulationService().traceSnapshot()); | |||||
| runtime_mode_service_.offlineSimulationService() | |||||
| .traceSnapshot().forLogic(current_logic_id_)); | |||||
| runtime_monitor_widget_->setLogicTrace( | runtime_monitor_widget_->setLogicTrace( | ||||
| runtime_mode_service_.offlineSimulationService().traceSnapshot()); | runtime_mode_service_.offlineSimulationService().traceSnapshot()); | ||||
| }, | }, | ||||
| @@ -521,6 +593,8 @@ void MainWindow::configureRuntimeMonitor() | |||||
| hmi_editor_service_, | hmi_editor_service_, | ||||
| hmi_runtime_service_, | hmi_runtime_service_, | ||||
| logic_editor_service_, | logic_editor_service_, | ||||
| project_service_, | |||||
| *hmi_navigation_service_, | |||||
| register_monitor_service_, | register_monitor_service_, | ||||
| ui_->runtimeMonitorTab); | ui_->runtimeMonitorTab); | ||||
| runtime_monitor_widget_->setObjectName(QStringLiteral("runtimeMonitorWidget")); | runtime_monitor_widget_->setObjectName(QStringLiteral("runtimeMonitorWidget")); | ||||
| @@ -543,9 +617,76 @@ void MainWindow::configureRuntimeMonitor() | |||||
| { | { | ||||
| statusBar()->showMessage(message, 4000); | 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); | 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() | void MainWindow::configurePropertyEditor() | ||||
| { | { | ||||
| ui_->bindingAreaComboBox->setItemData(0, -1); | ui_->bindingAreaComboBox->setItemData(0, -1); | ||||
| @@ -567,6 +708,8 @@ void MainWindow::configurePropertyEditor() | |||||
| this, &MainWindow::applySelectedControlProperties); | this, &MainWindow::applySelectedControlProperties); | ||||
| connect(ui_->applyLogicPropertiesButton, &QPushButton::clicked, | connect(ui_->applyLogicPropertiesButton, &QPushButton::clicked, | ||||
| this, &MainWindow::applySelectedLogicNodeProperties); | this, &MainWindow::applySelectedLogicNodeProperties); | ||||
| ui_->targetPageLabel->setVisible(false); | |||||
| ui_->targetPageComboBox->setVisible(false); | |||||
| showControlProperties({}); | showControlProperties({}); | ||||
| } | } | ||||
| @@ -578,28 +721,61 @@ void MainWindow::configurePlcConnection() | |||||
| void MainWindow::refreshProjectUi() | 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) | 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) | 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(); | runtime_mode_service_.refreshPlcPollAddresses(); | ||||
| const QSignalBlocker blocker(ui_->projectTree); | |||||
| ui_->projectTree->clear(); | ui_->projectTree->clear(); | ||||
| auto *hmi_root = new QTreeWidgetItem(ui_->projectTree, {tr("HMI 页面")}); | auto *hmi_root = new QTreeWidgetItem(ui_->projectTree, {tr("HMI 页面")}); | ||||
| hmi_root->setData(0, kProjectTreeKindRole, | |||||
| static_cast<int>(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<int>(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) | if (page != nullptr) | ||||
| { | { | ||||
| hmi_root->addChild(new QTreeWidgetItem(hmi_root, {fromUtf8(page->name)})); | |||||
| ui_->hmiPageTitleLabel->setText(fromUtf8(page->name)); | ui_->hmiPageTitleLabel->setText(fromUtf8(page->name)); | ||||
| ui_->hmiPageSizeLabel->setText( | ui_->hmiPageSizeLabel->setText( | ||||
| tr("%1 x %2").arg(page->width).arg(page->height)); | tr("%1 x %2").arg(page->width).arg(page->height)); | ||||
| @@ -610,13 +786,311 @@ void MainWindow::refreshProjectUi() | |||||
| ui_->hmiPageSizeLabel->clear(); | ui_->hmiPageSizeLabel->clear(); | ||||
| } | } | ||||
| auto *logic_root = new QTreeWidgetItem(ui_->projectTree, {tr("控制逻辑")}); | auto *logic_root = new QTreeWidgetItem(ui_->projectTree, {tr("控制逻辑")}); | ||||
| logic_root->setData(0, kProjectTreeKindRole, | |||||
| static_cast<int>(ProjectTreeItemKind::Root)); | |||||
| for (const ControlLogic &logic : project_service_.project().controlLogics) | for (const ControlLogic &logic : project_service_.project().controlLogics) | ||||
| { | { | ||||
| const QString suffix = logic.enabled ? QString{} : tr("(已禁用)"); | 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<int>(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(); | 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<ProjectTreeItemKind>( | |||||
| 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<ProjectTreeItemKind>( | |||||
| 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<ProjectTreeItemKind>( | |||||
| 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<ProjectTreeItemKind>( | |||||
| 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<ProjectTreeItemKind>( | |||||
| 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<ProjectTreeItemKind>( | |||||
| 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<ProjectTreeItemKind>( | |||||
| 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() | void MainWindow::connectPlc() | ||||
| @@ -682,14 +1156,16 @@ void MainWindow::showControlProperties(const std::string &control_id) | |||||
| // 在第一页画面中,根据ID查找目标控件,获取只读模型指针 | // 在第一页画面中,根据ID查找目标控件,获取只读模型指针 | ||||
| const HmiControl *control = hmi_editor_service_.findControl( | 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; | const bool has_control = control != nullptr; | ||||
| // 更新属性面板顶部状态标签,显示选中控件ID或提示未选择 | // 更新属性面板顶部状态标签,显示选中控件ID或提示未选择 | ||||
| ui_->selectionValueLabel->setText( | ui_->selectionValueLabel->setText( | ||||
| has_control ? fromUtf8(control->id) | has_control ? fromUtf8(control->id) | ||||
| + (control->isConfigured() ? QString{} : tr("(未绑定)")) | |||||
| + (control->isConfigured() ? QString{} | |||||
| : control->type == HmiControlType::PageJump | |||||
| ? tr("(未配置)") : tr("(未绑定)")) | |||||
| : tr("未选择")); | : tr("未选择")); | ||||
| // 批量控制所有属性编辑组件可用性:无选中控件时整体置灰,禁止编辑 | // 批量控制所有属性编辑组件可用性:无选中控件时整体置灰,禁止编辑 | ||||
| @@ -701,6 +1177,7 @@ void MainWindow::showControlProperties(const std::string &control_id) | |||||
| static_cast<QWidget *>(ui_->controlHeightSpinBox), | static_cast<QWidget *>(ui_->controlHeightSpinBox), | ||||
| static_cast<QWidget *>(ui_->bindingAreaComboBox), | static_cast<QWidget *>(ui_->bindingAreaComboBox), | ||||
| static_cast<QWidget *>(ui_->bindingIndexSpinBox), | static_cast<QWidget *>(ui_->bindingIndexSpinBox), | ||||
| static_cast<QWidget *>(ui_->targetPageComboBox), | |||||
| static_cast<QWidget *>(ui_->buttonOperationComboBox), | static_cast<QWidget *>(ui_->buttonOperationComboBox), | ||||
| static_cast<QWidget *>(ui_->applyPropertiesButton)}) | static_cast<QWidget *>(ui_->applyPropertiesButton)}) | ||||
| { | { | ||||
| @@ -710,6 +1187,12 @@ void MainWindow::showControlProperties(const std::string &control_id) | |||||
| // 未选中有效控件,无需填充属性,直接退出 | // 未选中有效控件,无需填充属性,直接退出 | ||||
| if (!has_control) | 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_->buttonOperationLabel->setVisible(false); | ||||
| ui_->buttonOperationComboBox->setVisible(false); | ui_->buttonOperationComboBox->setVisible(false); | ||||
| return; | return; | ||||
| @@ -731,6 +1214,12 @@ void MainWindow::showControlProperties(const std::string &control_id) | |||||
| : control->binding->area() == RegisterArea::M ? 1 : 2); | : control->binding->area() == RegisterArea::M ? 1 : 2); | ||||
| ui_->bindingIndexSpinBox->setValue( | ui_->bindingIndexSpinBox->setValue( | ||||
| control->binding.has_value() ? control->binding->index() : 0); | 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; | const bool is_button = control->type == HmiControlType::Button; | ||||
| ui_->buttonOperationLabel->setVisible(is_button); | ui_->buttonOperationLabel->setVisible(is_button); | ||||
| ui_->buttonOperationComboBox->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->setCurrentIndex( | ||||
| ui_->buttonOperationComboBox->findData( | ui_->buttonOperationComboBox->findData( | ||||
| static_cast<int>(control->buttonOperation))); | static_cast<int>(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) | 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); | ui_->propertyStack->setCurrentWidget(ui_->logicPropertiesPage); | ||||
| selected_logic_node_id_ = node_id; | selected_logic_node_id_ = node_id; | ||||
| const LogicNode *node = logic_editor_service_.findNode( | const LogicNode *node = logic_editor_service_.findNode( | ||||
| logic_editor_service_.firstLogicId(), node_id); | |||||
| current_logic_id_, node_id); | |||||
| const bool has_node = node != nullptr; | const bool has_node = node != nullptr; | ||||
| ui_->selectionValueLabel->setText( | ui_->selectionValueLabel->setText( | ||||
| has_node ? fromUtf8(node->id) : tr("未选择")); | has_node ? fromUtf8(node->id) : tr("未选择")); | ||||
| @@ -827,8 +1333,8 @@ void MainWindow::showLogicNodeProperties(const std::string &node_id) | |||||
| void MainWindow::addHmiControl(HmiControlType type) | 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) | if (!result.succeeded) | ||||
| { | { | ||||
| showProjectResult(tr("添加控件"), fromUtf8(result.message), false); | showProjectResult(tr("添加控件"), fromUtf8(result.message), false); | ||||
| @@ -848,7 +1354,7 @@ void MainWindow::deleteSelectedControl() | |||||
| return; | return; | ||||
| } | } | ||||
| const HmiEditorResult result = hmi_editor_service_.removeControl( | const HmiEditorResult result = hmi_editor_service_.removeControl( | ||||
| hmi_editor_service_.firstPageId(), selected_control_id_); | |||||
| current_hmi_page_id_, selected_control_id_); | |||||
| if (!result.succeeded) | if (!result.succeeded) | ||||
| { | { | ||||
| showProjectResult(tr("删除控件"), fromUtf8(result.message), false); | showProjectResult(tr("删除控件"), fromUtf8(result.message), false); | ||||
| @@ -863,7 +1369,7 @@ void MainWindow::deleteSelectedControl() | |||||
| void MainWindow::applySelectedControlProperties() | void MainWindow::applySelectedControlProperties() | ||||
| { | { | ||||
| const HmiControl *old_control = hmi_editor_service_.findControl( | 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) | if (old_control == nullptr) | ||||
| { | { | ||||
| return; | return; | ||||
| @@ -876,23 +1382,38 @@ void MainWindow::applySelectedControlProperties() | |||||
| control.bounds.y = ui_->controlYSpinBox->value(); | control.bounds.y = ui_->controlYSpinBox->value(); | ||||
| control.bounds.width = ui_->controlWidthSpinBox->value(); | control.bounds.width = ui_->controlWidthSpinBox->value(); | ||||
| control.bounds.height = ui_->controlHeightSpinBox->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(); | control.binding.reset(); | ||||
| } | } | ||||
| else | 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) | if (control.type == HmiControlType::Button) | ||||
| { | { | ||||
| control.buttonOperation = static_cast<HmiButtonOperation>( | control.buttonOperation = static_cast<HmiButtonOperation>( | ||||
| ui_->buttonOperationComboBox->currentData().toInt()); | ui_->buttonOperationComboBox->currentData().toInt()); | ||||
| } | } | ||||
| if (control.type == HmiControlType::PageJump) | |||||
| { | |||||
| control.pageJump = HmiPageJumpConfig{ | |||||
| toUtf8(ui_->targetPageComboBox->currentData().toString())}; | |||||
| } | |||||
| const HmiEditorResult result = hmi_editor_service_.updateControl( | 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) | if (!result.succeeded) | ||||
| { | { | ||||
| showProjectResult(tr("应用属性"), fromUtf8(result.message), false); | showProjectResult(tr("应用属性"), fromUtf8(result.message), false); | ||||
| @@ -975,9 +1496,8 @@ void MainWindow::deleteSelectedLogicObject() | |||||
| void MainWindow::applySelectedLogicNodeProperties() | void MainWindow::applySelectedLogicNodeProperties() | ||||
| { | { | ||||
| const std::string logic_id = logic_editor_service_.firstLogicId(); | |||||
| const LogicNode *node = logic_editor_service_.findNode( | const LogicNode *node = logic_editor_service_.findNode( | ||||
| logic_id, selected_logic_node_id_); | |||||
| current_logic_id_, selected_logic_node_id_); | |||||
| if (node == nullptr) | if (node == nullptr) | ||||
| { | { | ||||
| return; | return; | ||||
| @@ -1006,7 +1526,7 @@ void MainWindow::applySelectedLogicNodeProperties() | |||||
| } | } | ||||
| const LogicEditorResult result = logic_editor_service_.updateNodeConfig( | 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) | if (!result.succeeded) | ||||
| { | { | ||||
| showProjectResult(tr("应用逻辑属性"), fromUtf8(result.message), false); | showProjectResult(tr("应用逻辑属性"), fromUtf8(result.message), false); | ||||
| @@ -1037,6 +1557,8 @@ void MainWindow::createNewProject() | |||||
| logic_editor_service_.ensureDefaultLogic(); | logic_editor_service_.ensureDefaultLogic(); | ||||
| selected_control_id_.clear(); | selected_control_id_.clear(); | ||||
| selected_logic_node_id_.clear(); | selected_logic_node_id_.clear(); | ||||
| current_hmi_page_id_ = project_service_.project().initialHmiPageId; | |||||
| current_logic_id_ = logic_editor_service_.firstLogicId(); | |||||
| refreshProjectUi(); | refreshProjectUi(); | ||||
| hmi_editor_widget_->reloadPage(); | hmi_editor_widget_->reloadPage(); | ||||
| logic_editor_widget_->reloadLogic(); | logic_editor_widget_->reloadLogic(); | ||||
| @@ -1086,6 +1608,8 @@ void MainWindow::loadProject() | |||||
| } | } | ||||
| selected_control_id_.clear(); | selected_control_id_.clear(); | ||||
| selected_logic_node_id_.clear(); | selected_logic_node_id_.clear(); | ||||
| current_hmi_page_id_ = project_service_.project().initialHmiPageId; | |||||
| current_logic_id_ = logic_editor_service_.firstLogicId(); | |||||
| refreshProjectUi(); | refreshProjectUi(); | ||||
| hmi_editor_widget_->reloadPage(); | hmi_editor_widget_->reloadPage(); | ||||
| logic_editor_widget_->reloadLogic(); | logic_editor_widget_->reloadLogic(); | ||||
| @@ -1165,14 +1689,28 @@ void MainWindow::updateModeUi(const QString &message) | |||||
| ui_->editorTabWidget->setTabVisible(2, running); | ui_->editorTabWidget->setTabVisible(2, running); | ||||
| if (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( | 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()); | runtime_monitor_widget_->setMode(mode, runtime_mode_service_.plcConnectionState()); | ||||
| ui_->editorTabWidget->setCurrentWidget(ui_->runtimeMonitorTab); | 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); | ui_->projectDock->setEnabled(policy.allowsProjectEditing); | ||||
| @@ -1191,6 +1729,8 @@ void MainWindow::updateModeUi(const QString &message) | |||||
| ui_->addIndicatorAction->setEnabled(policy.allowsProjectEditing); | ui_->addIndicatorAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addNumericDisplayAction->setEnabled(policy.allowsProjectEditing); | ui_->addNumericDisplayAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addNumericInputAction->setEnabled(policy.allowsProjectEditing); | ui_->addNumericInputAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addLabelAction->setEnabled(policy.allowsProjectEditing); | |||||
| ui_->addPageJumpAction->setEnabled(policy.allowsProjectEditing); | |||||
| ui_->deleteControlAction->setEnabled(policy.allowsProjectEditing); | ui_->deleteControlAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addNormallyOpenAction->setEnabled(policy.allowsProjectEditing); | ui_->addNormallyOpenAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->addNormallyClosedAction->setEnabled(policy.allowsProjectEditing); | ui_->addNormallyClosedAction->setEnabled(policy.allowsProjectEditing); | ||||
| @@ -1203,6 +1743,7 @@ void MainWindow::updateModeUi(const QString &message) | |||||
| ui_->saveProjectAction->setEnabled(policy.allowsProjectEditing); | ui_->saveProjectAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->saveAsProjectAction->setEnabled(policy.allowsProjectEditing); | ui_->saveAsProjectAction->setEnabled(policy.allowsProjectEditing); | ||||
| ui_->loadProjectAction->setEnabled(policy.allowsProjectEditing); | ui_->loadProjectAction->setEnabled(policy.allowsProjectEditing); | ||||
| updateProjectTreeActions(); | |||||
| mode_status_label_->setText(modeText(mode)); | mode_status_label_->setText(modeText(mode)); | ||||
| if (mode == ApplicationMode::Editing) | if (mode == ApplicationMode::Editing) | ||||
| @@ -1300,8 +1841,17 @@ void MainWindow::updateSimulationUi(bool report_fault) | |||||
| if (report_fault) | if (report_fault) | ||||
| { | { | ||||
| const LogicScanResult &error = runtime_mode_service_.simulationError(); | 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( | logic_editor_widget_->setRuntimeTrace( | ||||
| runtime_mode_service_.offlineSimulationService().traceSnapshot(), | |||||
| runtime_mode_service_.offlineSimulationService() | |||||
| .traceSnapshot().forLogic(current_logic_id_), | |||||
| error.nodeId); | error.nodeId); | ||||
| runtime_monitor_widget_->setLogicTrace( | runtime_monitor_widget_->setLogicTrace( | ||||
| runtime_mode_service_.offlineSimulationService().traceSnapshot(), | runtime_mode_service_.offlineSimulationService().traceSnapshot(), | ||||
| @@ -18,6 +18,7 @@ | |||||
| #include <memory> | #include <memory> | ||||
| QT_BEGIN_NAMESPACE | QT_BEGIN_NAMESPACE | ||||
| class QAction; | |||||
| class QActionGroup; | class QActionGroup; | ||||
| class QLabel; | class QLabel; | ||||
| class QTimer; | class QTimer; | ||||
| @@ -31,6 +32,7 @@ class RuntimeModeService; | |||||
| class ProjectService; | class ProjectService; | ||||
| class HmiEditorService; | class HmiEditorService; | ||||
| class HmiRuntimeService; | class HmiRuntimeService; | ||||
| class HmiNavigationService; | |||||
| class HmiEditorWidget; | class HmiEditorWidget; | ||||
| class LogicEditorService; | class LogicEditorService; | ||||
| class LogicEditorWidget; | class LogicEditorWidget; | ||||
| @@ -63,9 +65,21 @@ public: | |||||
| HmiRuntimeService &hmi_runtime_service, | HmiRuntimeService &hmi_runtime_service, | ||||
| RegisterMonitorService ®ister_monitor_service, | RegisterMonitorService ®ister_monitor_service, | ||||
| QWidget *parent = nullptr); | 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; | ~MainWindow() override; | ||||
| const std::string ¤tHmiPageId() const; | |||||
| const std::string ¤tLogicId() const; | |||||
| private: | private: | ||||
| // 配置菜单和工具栏动作 | // 配置菜单和工具栏动作 | ||||
| void configureActions(); | void configureActions(); | ||||
| @@ -76,11 +90,21 @@ private: | |||||
| // 创建并连接控制逻辑编辑画布 | // 创建并连接控制逻辑编辑画布 | ||||
| void configureLogicEditor(); | void configureLogicEditor(); | ||||
| void configureRuntimeMonitor(); | void configureRuntimeMonitor(); | ||||
| void configureProjectTree(); | |||||
| // 创建并连接控件属性编辑表单 | // 创建并连接控件属性编辑表单 | ||||
| void configurePropertyEditor(); | void configurePropertyEditor(); | ||||
| void configurePlcConnection(); | void configurePlcConnection(); | ||||
| // 刷新工程树和当前 HMI 页面信息 | // 刷新工程树和当前 HMI 页面信息 | ||||
| void refreshProjectUi(); | void refreshProjectUi(); | ||||
| void handleProjectTreeSelection(); | |||||
| void updateProjectTreeActions(); | |||||
| void addPage(); | |||||
| void addLogic(); | |||||
| void renameProjectItem(); | |||||
| void deleteProjectItem(); | |||||
| void moveProjectItem(int offset); | |||||
| void setSelectedPageAsInitial(); | |||||
| void toggleSelectedLogicEnabled(); | |||||
| // 显示指定 HMI 控件的可编辑属性 | // 显示指定 HMI 控件的可编辑属性 | ||||
| void showControlProperties(const std::string &control_id); | void showControlProperties(const std::string &control_id); | ||||
| // 显示指定控制逻辑节点的可编辑属性 | // 显示指定控制逻辑节点的可编辑属性 | ||||
| @@ -138,6 +162,7 @@ private: | |||||
| * @brief 将互斥模式动作同步到服务层当前状态 | * @brief 将互斥模式动作同步到服务层当前状态 | ||||
| */ | */ | ||||
| void restoreCurrentModeAction(); | void restoreCurrentModeAction(); | ||||
| void initializeUi(); | |||||
| std::unique_ptr<Ui::MainWindow> ui_; | std::unique_ptr<Ui::MainWindow> ui_; | ||||
| /** | /** | ||||
| @@ -152,6 +177,8 @@ private: | |||||
| LogicEditorService &logic_editor_service_; | LogicEditorService &logic_editor_service_; | ||||
| HmiRuntimeService &hmi_runtime_service_; | HmiRuntimeService &hmi_runtime_service_; | ||||
| RegisterMonitorService ®ister_monitor_service_; | RegisterMonitorService ®ister_monitor_service_; | ||||
| std::unique_ptr<HmiNavigationService> owned_hmi_navigation_service_; | |||||
| HmiNavigationService *hmi_navigation_service_ = nullptr; | |||||
| QActionGroup *mode_action_group_ = nullptr; | QActionGroup *mode_action_group_ = nullptr; | ||||
| HmiEditorWidget *hmi_editor_widget_ = nullptr; | HmiEditorWidget *hmi_editor_widget_ = nullptr; | ||||
| LogicEditorWidget *logic_editor_widget_ = nullptr; | LogicEditorWidget *logic_editor_widget_ = nullptr; | ||||
| @@ -160,9 +187,19 @@ private: | |||||
| QLabel *mode_status_label_ = nullptr; | QLabel *mode_status_label_ = nullptr; | ||||
| QLabel *register_status_label_ = nullptr; | QLabel *register_status_label_ = nullptr; | ||||
| QLabel *executor_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_; | PlcSerialConfiguration plc_configuration_; | ||||
| std::string selected_control_id_; | std::string selected_control_id_; | ||||
| std::string selected_logic_node_id_; | std::string selected_logic_node_id_; | ||||
| std::string current_hmi_page_id_; | |||||
| std::string current_logic_id_; | |||||
| bool plc_status_update_pending_ = false; | bool plc_status_update_pending_ = false; | ||||
| int last_editing_tab_index_ = 0; | int last_editing_tab_index_ = 0; | ||||
| }; | }; | ||||
| @@ -302,6 +302,8 @@ | |||||
| <addaction name="addIndicatorAction"/> | <addaction name="addIndicatorAction"/> | ||||
| <addaction name="addNumericDisplayAction"/> | <addaction name="addNumericDisplayAction"/> | ||||
| <addaction name="addNumericInputAction"/> | <addaction name="addNumericInputAction"/> | ||||
| <addaction name="addLabelAction"/> | |||||
| <addaction name="addPageJumpAction"/> | |||||
| <addaction name="deleteControlAction"/> | <addaction name="deleteControlAction"/> | ||||
| </widget> | </widget> | ||||
| <widget class="QToolBar" name="logicToolBar"> | <widget class="QToolBar" name="logicToolBar"> | ||||
| @@ -575,14 +577,24 @@ | |||||
| </property> | </property> | ||||
| </widget> | </widget> | ||||
| </item> | </item> | ||||
| <item row="8" column="0"> | |||||
| <item row="8" column="0"> | |||||
| <widget class="QLabel" name="targetPageLabel"> | |||||
| <property name="text"> | |||||
| <string>目标页面</string> | |||||
| </property> | |||||
| </widget> | |||||
| </item> | |||||
| <item row="8" column="1"> | |||||
| <widget class="QComboBox" name="targetPageComboBox"/> | |||||
| </item> | |||||
| <item row="9" column="0"> | |||||
| <widget class="QLabel" name="buttonOperationLabel"> | <widget class="QLabel" name="buttonOperationLabel"> | ||||
| <property name="text"> | <property name="text"> | ||||
| <string>按钮操作</string> | <string>按钮操作</string> | ||||
| </property> | </property> | ||||
| </widget> | </widget> | ||||
| </item> | </item> | ||||
| <item row="8" column="1"> | |||||
| <item row="9" column="1"> | |||||
| <widget class="QComboBox" name="buttonOperationComboBox"> | <widget class="QComboBox" name="buttonOperationComboBox"> | ||||
| <item> | <item> | ||||
| <property name="text"> | <property name="text"> | ||||
| @@ -606,7 +618,7 @@ | |||||
| </item> | </item> | ||||
| </widget> | </widget> | ||||
| </item> | </item> | ||||
| <item row="9" column="0" colspan="2"> | |||||
| <item row="10" column="0" colspan="2"> | |||||
| <widget class="QPushButton" name="applyPropertiesButton"> | <widget class="QPushButton" name="applyPropertiesButton"> | ||||
| <property name="text"> | <property name="text"> | ||||
| <string>应用属性</string> | <string>应用属性</string> | ||||
| @@ -874,6 +886,22 @@ | |||||
| <string>添加数值输入控件</string> | <string>添加数值输入控件</string> | ||||
| </property> | </property> | ||||
| </action> | </action> | ||||
| <action name="addLabelAction"> | |||||
| <property name="text"> | |||||
| <string>文本</string> | |||||
| </property> | |||||
| <property name="toolTip"> | |||||
| <string>添加固定文本控件</string> | |||||
| </property> | |||||
| </action> | |||||
| <action name="addPageJumpAction"> | |||||
| <property name="text"> | |||||
| <string>页面跳转</string> | |||||
| </property> | |||||
| <property name="toolTip"> | |||||
| <string>添加页面跳转控件</string> | |||||
| </property> | |||||
| </action> | |||||
| <action name="deleteControlAction"> | <action name="deleteControlAction"> | ||||
| <property name="text"> | <property name="text"> | ||||
| <string>删除</string> | <string>删除</string> | ||||
| @@ -3,18 +3,43 @@ | |||||
| #include "free_monitor_widget.h" | #include "free_monitor_widget.h" | ||||
| #include "hmi_editor_widget.h" | #include "hmi_editor_widget.h" | ||||
| #include "logic_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 "ui_runtime_monitor_widget.h" | ||||
| #include <QSplitter> | #include <QSplitter> | ||||
| #include <QComboBox> | |||||
| #include <QSignalBlocker> | |||||
| #include <algorithm> | |||||
| namespace { | |||||
| QString fromUtf8(const std::string &value) | |||||
| { | |||||
| return QString::fromUtf8(value.data(), static_cast<int>(value.size())); | |||||
| } | |||||
| std::string toUtf8(const QString &value) | |||||
| { | |||||
| const QByteArray bytes = value.toUtf8(); | |||||
| return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size())); | |||||
| } | |||||
| } // namespace | |||||
| RuntimeMonitorWidget::RuntimeMonitorWidget( | RuntimeMonitorWidget::RuntimeMonitorWidget( | ||||
| HmiEditorService &hmi_editor_service, | HmiEditorService &hmi_editor_service, | ||||
| HmiRuntimeService &hmi_runtime_service, | HmiRuntimeService &hmi_runtime_service, | ||||
| LogicEditorService &logic_editor_service, | LogicEditorService &logic_editor_service, | ||||
| ProjectService &project_service, | |||||
| HmiNavigationService &hmi_navigation_service, | |||||
| RegisterMonitorService ®ister_monitor_service, | RegisterMonitorService ®ister_monitor_service, | ||||
| QWidget *parent) | QWidget *parent) | ||||
| : QWidget(parent), | : QWidget(parent), | ||||
| ui_(std::make_unique<Ui::RuntimeMonitorWidget>()) | |||||
| ui_(std::make_unique<Ui::RuntimeMonitorWidget>()), | |||||
| project_service_(project_service), | |||||
| hmi_navigation_service_(hmi_navigation_service) | |||||
| { | { | ||||
| ui_->setupUi(this); | ui_->setupUi(this); | ||||
| hmi_view_ = new HmiEditorWidget( | hmi_view_ = new HmiEditorWidget( | ||||
| @@ -35,6 +60,31 @@ RuntimeMonitorWidget::RuntimeMonitorWidget( | |||||
| ui_->monitorContainerLayout->addWidget(free_monitor_widget_); | ui_->monitorContainerLayout->addWidget(free_monitor_widget_); | ||||
| ui_->upperSplitter->setSizes({600, 600}); | ui_->upperSplitter->setSizes({600, 600}); | ||||
| ui_->verticalSplitter->setSizes({440, 260}); | 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<int>::of(&QComboBox::currentIndexChanged), | |||||
| this, | |||||
| [this](int index) | |||||
| { | |||||
| if (index >= 0) | |||||
| { | |||||
| selectRuntimeLogic(toUtf8( | |||||
| ui_->runtimeLogicComboBox->itemData(index).toString())); | |||||
| } | |||||
| }); | |||||
| } | } | ||||
| RuntimeMonitorWidget::~RuntimeMonitorWidget() = default; | RuntimeMonitorWidget::~RuntimeMonitorWidget() = default; | ||||
| @@ -42,12 +92,27 @@ RuntimeMonitorWidget::~RuntimeMonitorWidget() = default; | |||||
| void RuntimeMonitorWidget::setProjectObjects( | void RuntimeMonitorWidget::setProjectObjects( | ||||
| const std::string &page_id, const std::string &logic_id) | 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( | void RuntimeMonitorWidget::setMode( | ||||
| @@ -62,6 +127,9 @@ void RuntimeMonitorWidget::setMode( | |||||
| if (!offline) | if (!offline) | ||||
| { | { | ||||
| logic_view_->clearRuntimeTrace(); | logic_view_->clearRuntimeTrace(); | ||||
| latest_trace_.clear(); | |||||
| latest_fault_node_id_.clear(); | |||||
| has_logic_trace_ = false; | |||||
| } | } | ||||
| ui_->modeLabel->setText( | ui_->modeLabel->setText( | ||||
| offline ? tr("离线仿真 · 虚拟 M/D") | offline ? tr("离线仿真 · 虚拟 M/D") | ||||
| @@ -84,10 +152,44 @@ void RuntimeMonitorWidget::refreshValues( | |||||
| void RuntimeMonitorWidget::setLogicTrace( | void RuntimeMonitorWidget::setLogicTrace( | ||||
| const LogicTraceSnapshot &trace, const std::string &fault_node_id) | 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 | FreeMonitorWidget *RuntimeMonitorWidget::freeMonitorWidget() const | ||||
| { | { | ||||
| return free_monitor_widget_; | 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_); | |||||
| } | |||||
| } | |||||
| @@ -2,6 +2,7 @@ | |||||
| #include "domain/runtime_state.h" | #include "domain/runtime_state.h" | ||||
| #include "services/plc_communication_gateway.h" | #include "services/plc_communication_gateway.h" | ||||
| #include "services/software_logic_executor.h" | |||||
| #include <QWidget> | #include <QWidget> | ||||
| @@ -16,10 +17,11 @@ class FreeMonitorWidget; | |||||
| class HmiEditorService; | class HmiEditorService; | ||||
| class HmiEditorWidget; | class HmiEditorWidget; | ||||
| class HmiRuntimeService; | class HmiRuntimeService; | ||||
| class HmiNavigationService; | |||||
| class LogicEditorService; | class LogicEditorService; | ||||
| class LogicEditorWidget; | class LogicEditorWidget; | ||||
| class RegisterMonitorService; | class RegisterMonitorService; | ||||
| struct LogicTraceSnapshot; | |||||
| class ProjectService; | |||||
| class RuntimeMonitorWidget final : public QWidget | class RuntimeMonitorWidget final : public QWidget | ||||
| { | { | ||||
| @@ -30,6 +32,8 @@ public: | |||||
| HmiEditorService &hmi_editor_service, | HmiEditorService &hmi_editor_service, | ||||
| HmiRuntimeService &hmi_runtime_service, | HmiRuntimeService &hmi_runtime_service, | ||||
| LogicEditorService &logic_editor_service, | LogicEditorService &logic_editor_service, | ||||
| ProjectService &project_service, | |||||
| HmiNavigationService &hmi_navigation_service, | |||||
| RegisterMonitorService ®ister_monitor_service, | RegisterMonitorService ®ister_monitor_service, | ||||
| QWidget *parent = nullptr); | QWidget *parent = nullptr); | ||||
| ~RuntimeMonitorWidget() override; | ~RuntimeMonitorWidget() override; | ||||
| @@ -40,10 +44,24 @@ public: | |||||
| void setHmiWriteEnabled(bool enabled); | void setHmiWriteEnabled(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; | |||||
| signals: | |||||
| void navigationFailed(const QString &message); | |||||
| void runtimePageChanged(const QString &page_id); | |||||
| private: | private: | ||||
| void showRuntimePage(const std::string &page_id); | |||||
| void selectRuntimeLogic(const std::string &logic_id); | |||||
| std::unique_ptr<Ui::RuntimeMonitorWidget> ui_; | std::unique_ptr<Ui::RuntimeMonitorWidget> ui_; | ||||
| ProjectService &project_service_; | |||||
| HmiNavigationService &hmi_navigation_service_; | |||||
| 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_; | |||||
| LogicTraceSnapshot latest_trace_; | |||||
| std::string latest_fault_node_id_; | |||||
| bool has_logic_trace_ = false; | |||||
| }; | }; | ||||
| @@ -24,7 +24,13 @@ | |||||
| <property name="frameShape"><enum>QFrame::StyledPanel</enum></property> | <property name="frameShape"><enum>QFrame::StyledPanel</enum></property> | ||||
| <layout class="QVBoxLayout" name="hmiLayout"> | <layout class="QVBoxLayout" name="hmiLayout"> | ||||
| <property name="leftMargin"><number>4</number></property><property name="topMargin"><number>4</number></property><property name="rightMargin"><number>4</number></property><property name="bottomMargin"><number>4</number></property> | <property name="leftMargin"><number>4</number></property><property name="topMargin"><number>4</number></property><property name="rightMargin"><number>4</number></property><property name="bottomMargin"><number>4</number></property> | ||||
| <item><widget class="QLabel" name="hmiLabel"><property name="text"><string>HMI 运行视图</string></property></widget></item> | |||||
| <item> | |||||
| <layout class="QHBoxLayout" name="hmiHeaderLayout"> | |||||
| <item><widget class="QLabel" name="hmiLabel"><property name="text"><string>HMI 运行视图</string></property></widget></item> | |||||
| <item><spacer name="hmiHeaderSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>24</width><height>20</height></size></property></spacer></item> | |||||
| <item><widget class="QLabel" name="runtimePageLabel"><property name="text"><string>未选择页面</string></property><property name="alignment"><set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set></property></widget></item> | |||||
| </layout> | |||||
| </item> | |||||
| <item><widget class="QWidget" name="hmiViewContainer" native="true"><layout class="QVBoxLayout" name="hmiViewLayout"><property name="leftMargin"><number>0</number></property><property name="topMargin"><number>0</number></property><property name="rightMargin"><number>0</number></property><property name="bottomMargin"><number>0</number></property></layout></widget></item> | <item><widget class="QWidget" name="hmiViewContainer" native="true"><layout class="QVBoxLayout" name="hmiViewLayout"><property name="leftMargin"><number>0</number></property><property name="topMargin"><number>0</number></property><property name="rightMargin"><number>0</number></property><property name="bottomMargin"><number>0</number></property></layout></widget></item> | ||||
| </layout> | </layout> | ||||
| </widget> | </widget> | ||||
| @@ -32,7 +38,13 @@ | |||||
| <property name="frameShape"><enum>QFrame::StyledPanel</enum></property> | <property name="frameShape"><enum>QFrame::StyledPanel</enum></property> | ||||
| <layout class="QVBoxLayout" name="logicLayout"> | <layout class="QVBoxLayout" name="logicLayout"> | ||||
| <property name="leftMargin"><number>4</number></property><property name="topMargin"><number>4</number></property><property name="rightMargin"><number>4</number></property><property name="bottomMargin"><number>4</number></property> | <property name="leftMargin"><number>4</number></property><property name="topMargin"><number>4</number></property><property name="rightMargin"><number>4</number></property><property name="bottomMargin"><number>4</number></property> | ||||
| <item><widget class="QLabel" name="logicLabel"><property name="text"><string>梯形图运行状态</string></property></widget></item> | |||||
| <item> | |||||
| <layout class="QHBoxLayout" name="logicHeaderLayout"> | |||||
| <item><widget class="QLabel" name="logicLabel"><property name="text"><string>梯形图运行状态</string></property></widget></item> | |||||
| <item><spacer name="logicHeaderSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>24</width><height>20</height></size></property></spacer></item> | |||||
| <item><widget class="QComboBox" name="runtimeLogicComboBox"><property name="minimumSize"><size><width>150</width><height>0</height></size></property></widget></item> | |||||
| </layout> | |||||
| </item> | |||||
| <item><widget class="QWidget" name="logicViewContainer" native="true"><layout class="QVBoxLayout" name="logicViewLayout"><property name="leftMargin"><number>0</number></property><property name="topMargin"><number>0</number></property><property name="rightMargin"><number>0</number></property><property name="bottomMargin"><number>0</number></property></layout></widget></item> | <item><widget class="QWidget" name="logicViewContainer" native="true"><layout class="QVBoxLayout" name="logicViewLayout"><property name="leftMargin"><number>0</number></property><property name="topMargin"><number>0</number></property><property name="rightMargin"><number>0</number></property><property name="bottomMargin"><number>0</number></property></layout></widget></item> | ||||
| </layout> | </layout> | ||||
| </widget> | </widget> | ||||
| @@ -3,6 +3,7 @@ | |||||
| #include "domain/register_repository.h" | #include "domain/register_repository.h" | ||||
| #include "services/hmi_editor_service.h" | #include "services/hmi_editor_service.h" | ||||
| #include "services/hmi_runtime_service.h" | #include "services/hmi_runtime_service.h" | ||||
| #include "services/hmi_navigation_service.h" | |||||
| #include "services/logic_editor_service.h" | #include "services/logic_editor_service.h" | ||||
| #include "services/offline_simulation_service.h" | #include "services/offline_simulation_service.h" | ||||
| #include "services/project_service.h" | #include "services/project_service.h" | ||||
| @@ -13,6 +14,7 @@ | |||||
| #include "ui/logic_editor_widget.h" | #include "ui/logic_editor_widget.h" | ||||
| #include "ui/main_window.h" | #include "ui/main_window.h" | ||||
| #include "ui/plc_connection_dialog.h" | #include "ui/plc_connection_dialog.h" | ||||
| #include "ui/runtime_monitor_widget.h" | |||||
| #include <QAction> | #include <QAction> | ||||
| #include <QApplication> | #include <QApplication> | ||||
| @@ -31,6 +33,7 @@ | |||||
| #include <QTableWidget> | #include <QTableWidget> | ||||
| #include <QTimer> | #include <QTimer> | ||||
| #include <QTest> | #include <QTest> | ||||
| #include <QTreeWidget> | |||||
| #include <iostream> | #include <iostream> | ||||
| #include <stdexcept> | #include <stdexcept> | ||||
| @@ -341,6 +344,56 @@ void testRuntimeButtonMouseInteraction() | |||||
| "a disabled runtime button must not write its register"); | "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() | void testModeActionsControlEditingAvailability() | ||||
| { | { | ||||
| // 验证模式动作会同步禁用编辑入口,并在失败时恢复当前选择 | // 验证模式动作会同步禁用编辑入口,并在失败时恢复当前选择 | ||||
| @@ -776,6 +829,125 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() | |||||
| "faulted PLC state must not re-enter online running without a fresh read"); | "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<QTreeWidget>(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<QAction>(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<QComboBox>(window, "bindingAreaComboBox"); | |||||
| QComboBox *target_page = requiredChild<QComboBox>(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<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, | |||||
| "logic toolbar actions must edit the selected logic module"); | |||||
| tree->setCurrentItem(tree->topLevelItem(0)->child(0)); | |||||
| QApplication::processEvents(); | |||||
| HmiEditorWidget *editor = requiredChild<HmiEditorWidget>(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<QAction>(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<HmiEditorWidget>( | |||||
| 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<QLabel>(window, "runtimePageLabel")->text() | |||||
| == QStringLiteral("Settings"), | |||||
| "clicking PageJump at runtime must navigate locally to its target page"); | |||||
| QComboBox *logic_selector = requiredChild<QComboBox>( | |||||
| 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<RuntimeMonitorWidget>( | |||||
| window, "runtimeMonitorWidget"); | |||||
| require(runtime_monitor->selectedLogicId() == second_logic_id, | |||||
| "runtime logic selection must change only the visible trace projection"); | |||||
| requiredChild<QAction>(window, "editingModeAction")->trigger(); | |||||
| } | |||||
| } // namespace | } // namespace | ||||
| int main(int argc, char *argv[]) | int main(int argc, char *argv[]) | ||||
| @@ -787,10 +959,12 @@ int main(int argc, char *argv[]) | |||||
| try | try | ||||
| { | { | ||||
| testRuntimeButtonMouseInteraction(); | testRuntimeButtonMouseInteraction(); | ||||
| testRuntimePageJumpDoesNotRequireRegisterWritePermission(); | |||||
| testModeActionsControlEditingAvailability(); | testModeActionsControlEditingAvailability(); | ||||
| testPlcConfigurationUsesDialog(); | testPlcConfigurationUsesDialog(); | ||||
| testRepeatedPlcStatusNotificationsAreCoalesced(); | testRepeatedPlcStatusNotificationsAreCoalesced(); | ||||
| testOnlineWorkspaceShowsHmiAndFreeMonitorOnly(); | testOnlineWorkspaceShowsHmiAndFreeMonitorOnly(); | ||||
| testMultiPageAndLogicMainWindowIntegration(); | |||||
| } | } | ||||
| catch (const std::exception &error) | catch (const std::exception &error) | ||||
| { | { | ||||
| @@ -26,6 +26,7 @@ SOURCES += \ | |||||
| ../src/domain/runtime_state.cpp \ | ../src/domain/runtime_state.cpp \ | ||||
| ../src/services/project_service.cpp \ | ../src/services/project_service.cpp \ | ||||
| ../src/services/hmi_editor_service.cpp \ | ../src/services/hmi_editor_service.cpp \ | ||||
| ../src/services/hmi_navigation_service.cpp \ | |||||
| ../src/services/logic_editor_service.cpp \ | ../src/services/logic_editor_service.cpp \ | ||||
| ../src/services/hmi_runtime_service.cpp \ | ../src/services/hmi_runtime_service.cpp \ | ||||
| ../src/services/software_logic_executor.cpp \ | ../src/services/software_logic_executor.cpp \ | ||||
| @@ -51,6 +52,7 @@ HEADERS += \ | |||||
| ../src/domain/runtime_state.h \ | ../src/domain/runtime_state.h \ | ||||
| ../src/services/project_service.h \ | ../src/services/project_service.h \ | ||||
| ../src/services/hmi_editor_service.h \ | ../src/services/hmi_editor_service.h \ | ||||
| ../src/services/hmi_navigation_service.h \ | |||||
| ../src/services/logic_editor_service.h \ | ../src/services/logic_editor_service.h \ | ||||
| ../src/services/hmi_runtime_service.h \ | ../src/services/hmi_runtime_service.h \ | ||||
| ../src/services/software_logic_executor.h \ | ../src/services/software_logic_executor.h \ | ||||