| @@ -22,6 +22,7 @@ SOURCES += \ | |||
| src/ui/register_comment_dialog.cpp \ | |||
| src/ui/plc_connection_dialog.cpp \ | |||
| src/ui/free_monitor_widget.cpp \ | |||
| src/ui/hmi_runtime_window.cpp \ | |||
| src/ui/runtime_monitor_widget.cpp \ | |||
| src/ui/toolbar_icon_factory.cpp \ | |||
| src/ui/project_workspace_controller.cpp \ | |||
| @@ -63,6 +64,7 @@ HEADERS += \ | |||
| src/ui/register_comment_dialog.h \ | |||
| src/ui/plc_connection_dialog.h \ | |||
| src/ui/free_monitor_widget.h \ | |||
| src/ui/hmi_runtime_window.h \ | |||
| src/ui/runtime_monitor_widget.h \ | |||
| src/ui/toolbar_icon_factory.h \ | |||
| src/ui/project_workspace_controller.h \ | |||
| @@ -108,4 +110,5 @@ FORMS += \ | |||
| src/ui/register_comment_dialog.ui \ | |||
| src/ui/plc_connection_dialog.ui \ | |||
| src/ui/free_monitor_widget.ui \ | |||
| src/ui/hmi_runtime_window.ui \ | |||
| src/ui/runtime_monitor_widget.ui | |||
| @@ -0,0 +1,134 @@ | |||
| #include "hmi_runtime_window.h" | |||
| #include "hmi_editor_widget.h" | |||
| #include "services/alarm_service.h" | |||
| #include "services/hmi_editor_service.h" | |||
| #include "services/hmi_runtime_service.h" | |||
| #include "services/project_service.h" | |||
| #include "toolbar_icon_factory.h" | |||
| #include "ui_hmi_runtime_window.h" | |||
| #include <QCloseEvent> | |||
| #include <QToolButton> | |||
| #include <algorithm> | |||
| namespace { | |||
| QString fromUtf8(const std::string &value) | |||
| { | |||
| return QString::fromUtf8(value.data(), static_cast<int>(value.size())); | |||
| } | |||
| } // namespace | |||
| HmiRuntimeWindow::HmiRuntimeWindow( | |||
| HmiEditorService &hmi_editor_service, | |||
| HmiRuntimeService &hmi_runtime_service, | |||
| AlarmService &alarm_service, | |||
| ProjectService &project_service, | |||
| QWidget *parent) | |||
| : QMainWindow(parent), | |||
| ui_(std::make_unique<Ui::HmiRuntimeWindow>()), | |||
| project_service_(project_service) | |||
| { | |||
| ui_->setupUi(this); | |||
| setObjectName(QStringLiteral("hmiRuntimeWindow")); | |||
| ui_->exitRuntimeButton->setIcon(makeUiIcon(UiIcon::Exit)); | |||
| hmi_view_ = new HmiEditorWidget( | |||
| hmi_editor_service, | |||
| hmi_runtime_service, | |||
| alarm_service, | |||
| ui_->hmiViewContainer); | |||
| hmi_view_->setObjectName(QStringLiteral("hmiRuntimeHmiView")); | |||
| hmi_view_->setEditingEnabled(false); | |||
| ui_->hmiViewLayout->addWidget(hmi_view_); | |||
| connect(hmi_view_, &HmiEditorWidget::pageNavigationRequested, | |||
| this, &HmiRuntimeWindow::pageNavigationRequested); | |||
| connect(ui_->exitRuntimeButton, &QToolButton::clicked, | |||
| this, &HmiRuntimeWindow::exitRequested); | |||
| } | |||
| HmiRuntimeWindow::~HmiRuntimeWindow() = default; | |||
| void HmiRuntimeWindow::setRuntimePage(const std::string &page_id) | |||
| { | |||
| hmi_view_->setPageId(page_id); | |||
| 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 HmiRuntimeWindow::reloadRuntimePage() | |||
| { | |||
| hmi_view_->reloadPage(); | |||
| } | |||
| void HmiRuntimeWindow::setMode( | |||
| ApplicationMode mode, PlcConnectionState plc_state) | |||
| { | |||
| const bool offline = mode == ApplicationMode::OfflineRunning; | |||
| const bool online = mode == ApplicationMode::OnlineRunning; | |||
| runtime_active_ = offline || online; | |||
| hmi_view_->setRuntimeActive(runtime_active_); | |||
| if (!runtime_active_) | |||
| { | |||
| hmi_view_->setRuntimeWriteEnabled(false); | |||
| } | |||
| ui_->modeLabel->setText( | |||
| offline ? tr("离线仿真 · 虚拟 M/D") | |||
| : online && plc_state == PlcConnectionState::Connected | |||
| ? tr("真机运行 · PLC 已连接") | |||
| : online ? tr("真机运行 · PLC 通信不可用") | |||
| : tr("当前未运行")); | |||
| } | |||
| void HmiRuntimeWindow::setHmiWriteEnabled(bool enabled) | |||
| { | |||
| hmi_view_->setRuntimeWriteEnabled(runtime_active_ && enabled); | |||
| } | |||
| void HmiRuntimeWindow::refreshValues() | |||
| { | |||
| hmi_view_->refreshRuntimeValues(); | |||
| } | |||
| void HmiRuntimeWindow::showForRuntime() | |||
| { | |||
| if (!isVisible()) | |||
| { | |||
| showMaximized(); | |||
| } | |||
| else | |||
| { | |||
| raise(); | |||
| activateWindow(); | |||
| } | |||
| } | |||
| void HmiRuntimeWindow::hideForEditing() | |||
| { | |||
| hide(); | |||
| } | |||
| void HmiRuntimeWindow::closeForApplicationExit() | |||
| { | |||
| runtime_active_ = false; | |||
| close(); | |||
| } | |||
| void HmiRuntimeWindow::closeEvent(QCloseEvent *event) | |||
| { | |||
| if (runtime_active_) | |||
| { | |||
| emit exitRequested(); | |||
| event->ignore(); | |||
| return; | |||
| } | |||
| QMainWindow::closeEvent(event); | |||
| } | |||
| @@ -0,0 +1,63 @@ | |||
| /** | |||
| * @file hmi_runtime_window.h | |||
| * @brief 定义面向操作员的独立 HMI 运行窗口 | |||
| * @author suyu | |||
| */ | |||
| #pragma once | |||
| #include "domain/runtime_state.h" | |||
| #include "services/plc_communication_gateway.h" | |||
| #include <QMainWindow> | |||
| #include <memory> | |||
| #include <string> | |||
| namespace Ui { | |||
| class HmiRuntimeWindow; | |||
| } | |||
| class AlarmService; | |||
| class HmiEditorService; | |||
| class HmiEditorWidget; | |||
| class HmiRuntimeService; | |||
| class ProjectService; | |||
| class QCloseEvent; | |||
| // 独立显示运行 HMI,工程编辑和运行模式切换仍由外层控制器负责 | |||
| class HmiRuntimeWindow final : public QMainWindow | |||
| { | |||
| Q_OBJECT | |||
| public: | |||
| HmiRuntimeWindow( | |||
| HmiEditorService &hmi_editor_service, | |||
| HmiRuntimeService &hmi_runtime_service, | |||
| AlarmService &alarm_service, | |||
| ProjectService &project_service, | |||
| QWidget *parent = nullptr); | |||
| ~HmiRuntimeWindow() override; | |||
| void setRuntimePage(const std::string &page_id); | |||
| void reloadRuntimePage(); | |||
| void setMode(ApplicationMode mode, PlcConnectionState plc_state); | |||
| void setHmiWriteEnabled(bool enabled); | |||
| void refreshValues(); | |||
| void showForRuntime(); | |||
| void hideForEditing(); | |||
| void closeForApplicationExit(); | |||
| signals: | |||
| void pageNavigationRequested(const QString &target_page_id); | |||
| void exitRequested(); | |||
| protected: | |||
| void closeEvent(QCloseEvent *event) override; | |||
| private: | |||
| std::unique_ptr<Ui::HmiRuntimeWindow> ui_; | |||
| ProjectService &project_service_; | |||
| HmiEditorWidget *hmi_view_ = nullptr; | |||
| bool runtime_active_ = false; | |||
| }; | |||
| @@ -0,0 +1,49 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <ui version="4.0"> | |||
| <class>HmiRuntimeWindow</class> | |||
| <widget class="QMainWindow" name="HmiRuntimeWindow"> | |||
| <property name="geometry"> | |||
| <rect><x>0</x><y>0</y><width>1280</width><height>800</height></rect> | |||
| </property> | |||
| <property name="windowTitle"><string>HMI 运行</string></property> | |||
| <widget class="QWidget" name="centralWidget"> | |||
| <layout class="QVBoxLayout" name="centralLayout"> | |||
| <property name="spacing"><number>0</number></property> | |||
| <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> | |||
| <item> | |||
| <widget class="QFrame" name="runtimeHeaderFrame"> | |||
| <property name="minimumSize"><size><width>0</width><height>42</height></size></property> | |||
| <property name="maximumSize"><size><width>16777215</width><height>42</height></size></property> | |||
| <property name="frameShape"><enum>QFrame::StyledPanel</enum></property> | |||
| <layout class="QHBoxLayout" name="runtimeHeaderLayout"> | |||
| <property name="leftMargin"><number>12</number></property> | |||
| <property name="topMargin"><number>4</number></property> | |||
| <property name="rightMargin"><number>8</number></property> | |||
| <property name="bottomMargin"><number>4</number></property> | |||
| <item><widget class="QLabel" name="titleLabel"><property name="text"><string>HMI 运行</string></property><property name="styleSheet"><string notr="true">font-weight: 600;</string></property></widget></item> | |||
| <item><spacer name="headerSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</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> | |||
| <item><widget class="QLabel" name="modeLabel"><property name="minimumSize"><size><width>170</width><height>0</height></size></property><property name="text"><string>当前未运行</string></property><property name="alignment"><set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set></property></widget></item> | |||
| <item><widget class="QToolButton" name="exitRuntimeButton"><property name="toolTip"><string>退出运行</string></property><property name="accessibleName"><string>退出运行</string></property><property name="text"><string>退出运行</string></property><property name="toolButtonStyle"><enum>Qt::ToolButtonIconOnly</enum></property></widget></item> | |||
| </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> | |||
| </widget> | |||
| </widget> | |||
| <resources/> | |||
| <connections/> | |||
| </ui> | |||
| @@ -325,6 +325,10 @@ void MainWindow::closeEvent(QCloseEvent *event) | |||
| event->ignore(); | |||
| return; | |||
| } | |||
| if (runtime_panel_controller_ != nullptr) | |||
| { | |||
| runtime_panel_controller_->closeForApplicationExit(); | |||
| } | |||
| QMainWindow::closeEvent(event); | |||
| } | |||
| @@ -854,6 +858,10 @@ void MainWindow::configureRuntimeMonitor() | |||
| [this](const QString &message) | |||
| { | |||
| appendOutputMessage(message); | |||
| }, | |||
| [this] | |||
| { | |||
| requestMode(ApplicationMode::Editing); | |||
| }); | |||
| runtime_panel_controller_->configure(); | |||
| runtime_monitor_widget_ = runtime_panel_controller_->runtimeMonitorWidget(); | |||
| @@ -1552,15 +1560,17 @@ void MainWindow::updateModeUi(const QString &message) | |||
| statusBar()->showMessage(fromUtf8(navigation.message), 5000); | |||
| } | |||
| } | |||
| runtime_monitor_widget_->setProjectObjects( | |||
| hmi_navigation_service_->currentPageId(), current_logic_id_); | |||
| runtime_monitor_widget_->setMode(mode, runtime_mode_service_.plcConnectionState()); | |||
| runtime_panel_controller_->enterRuntime( | |||
| hmi_navigation_service_->currentPageId(), | |||
| current_logic_id_, | |||
| mode, | |||
| runtime_mode_service_.plcConnectionState()); | |||
| ui_->editorTabWidget->setCurrentWidget(ui_->runtimeMonitorTab); | |||
| } | |||
| else | |||
| { | |||
| hmi_navigation_service_->stop(); | |||
| runtime_monitor_widget_->setMode( | |||
| runtime_panel_controller_->leaveRuntime( | |||
| mode, runtime_mode_service_.plcConnectionState()); | |||
| if (ui_->editorTabWidget->currentWidget() == ui_->runtimeMonitorTab) | |||
| { | |||
| @@ -97,7 +97,7 @@ RuntimeMonitorWidget::~RuntimeMonitorWidget() = default; | |||
| void RuntimeMonitorWidget::setProjectObjects( | |||
| const std::string &page_id, const std::string &logic_id) | |||
| { | |||
| showRuntimePage(page_id); | |||
| setRuntimePage(page_id); | |||
| const QSignalBlocker blocker(ui_->runtimeLogicComboBox); | |||
| ui_->runtimeLogicComboBox->clear(); | |||
| @@ -120,6 +120,11 @@ void RuntimeMonitorWidget::setProjectObjects( | |||
| : std::string{}); | |||
| } | |||
| void RuntimeMonitorWidget::setRuntimePage(const std::string &page_id) | |||
| { | |||
| showRuntimePage(page_id); | |||
| } | |||
| void RuntimeMonitorWidget::setMode( | |||
| ApplicationMode mode, PlcConnectionState plc_state) | |||
| { | |||
| @@ -127,7 +132,10 @@ void RuntimeMonitorWidget::setMode( | |||
| const bool online = mode == ApplicationMode::OnlineRunning; | |||
| ui_->logicFrame->setVisible(offline); | |||
| hmi_view_->setRuntimeActive(offline || online); | |||
| setHmiWriteEnabled(offline || (online && plc_state == PlcConnectionState::Connected)); | |||
| // 工程师诊断视图只投影 HMI,实际运行输入统一由独立 HMI 窗口处理 | |||
| setHmiWriteEnabled(false); | |||
| setFreeMonitorWriteEnabled(offline | |||
| || (online && plc_state == PlcConnectionState::Connected)); | |||
| if (!offline) | |||
| { | |||
| logic_view_->clearRuntimeTrace(); | |||
| @@ -144,6 +152,10 @@ void RuntimeMonitorWidget::setMode( | |||
| void RuntimeMonitorWidget::setHmiWriteEnabled(bool enabled) | |||
| { | |||
| hmi_view_->setRuntimeWriteEnabled(enabled); | |||
| } | |||
| void RuntimeMonitorWidget::setFreeMonitorWriteEnabled(bool enabled) | |||
| { | |||
| free_monitor_widget_->setWriteEnabled(enabled); | |||
| } | |||
| @@ -41,9 +41,11 @@ public: | |||
| ~RuntimeMonitorWidget() override; | |||
| void setProjectObjects(const std::string &page_id, const std::string &logic_id); | |||
| void setRuntimePage(const std::string &page_id); | |||
| void setMode(ApplicationMode mode, PlcConnectionState plc_state); | |||
| void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); | |||
| void setHmiWriteEnabled(bool enabled); | |||
| void setFreeMonitorWriteEnabled(bool enabled); | |||
| void setLogicTrace(const LogicTraceSnapshot &trace, const std::string &fault_node_id = {}); | |||
| FreeMonitorWidget *freeMonitorWidget() const; | |||
| std::string selectedLogicId() const; | |||
| @@ -2,6 +2,7 @@ | |||
| #include "free_monitor_widget.h" | |||
| #include "hmi_editor_widget.h" | |||
| #include "hmi_runtime_window.h" | |||
| #include "logic_editor_widget.h" | |||
| #include "runtime_monitor_widget.h" | |||
| #include "services/alarm_service.h" | |||
| @@ -50,7 +51,8 @@ RuntimePanelController::RuntimePanelController( | |||
| std::function<void(const std::string &)> select_logic, | |||
| std::function<void(const std::string &)> show_logic_properties, | |||
| StatusReporter status_reporter, | |||
| OutputReporter output_reporter) | |||
| OutputReporter output_reporter, | |||
| RuntimeExitRequester runtime_exit_requester) | |||
| : parent_(parent), | |||
| ui_(ui), | |||
| runtime_mode_service_(runtime_mode_service), | |||
| @@ -68,10 +70,13 @@ RuntimePanelController::RuntimePanelController( | |||
| select_logic_(std::move(select_logic)), | |||
| show_logic_properties_(std::move(show_logic_properties)), | |||
| status_reporter_(std::move(status_reporter)), | |||
| output_reporter_(std::move(output_reporter)) | |||
| output_reporter_(std::move(output_reporter)), | |||
| runtime_exit_requester_(std::move(runtime_exit_requester)) | |||
| { | |||
| } | |||
| RuntimePanelController::~RuntimePanelController() = default; | |||
| void RuntimePanelController::configure() | |||
| { | |||
| runtime_monitor_widget_ = new RuntimeMonitorWidget( | |||
| @@ -85,6 +90,35 @@ void RuntimePanelController::configure() | |||
| ui_.runtimeMonitorTab); | |||
| runtime_monitor_widget_->setObjectName(QStringLiteral("runtimeMonitorWidget")); | |||
| ui_.runtimeMonitorLayout->addWidget(runtime_monitor_widget_); | |||
| hmi_runtime_window_ = std::make_unique<HmiRuntimeWindow>( | |||
| hmi_editor_service_, | |||
| hmi_runtime_service_, | |||
| alarm_service_, | |||
| project_service_); | |||
| QObject::connect(hmi_runtime_window_.get(), | |||
| &HmiRuntimeWindow::pageNavigationRequested, | |||
| &parent_, [this](const QString &target_page_id) | |||
| { | |||
| const HmiNavigationResult result = hmi_navigation_service_.navigateTo( | |||
| target_page_id.toUtf8().toStdString()); | |||
| if (!result.succeeded) | |||
| { | |||
| const QString message = fromUtf8(result.message); | |||
| status_reporter_(message, 5000); | |||
| output_reporter_(QObject::tr("页面导航失败:%1").arg(message)); | |||
| return; | |||
| } | |||
| runtime_monitor_widget_->setRuntimePage(result.pageId); | |||
| hmi_runtime_window_->setRuntimePage(result.pageId); | |||
| }); | |||
| QObject::connect(hmi_runtime_window_.get(), &HmiRuntimeWindow::exitRequested, | |||
| &parent_, [this] | |||
| { | |||
| if (runtime_exit_requester_) | |||
| { | |||
| runtime_exit_requester_(); | |||
| } | |||
| }); | |||
| QObject::connect(runtime_monitor_widget_->freeMonitorWidget(), | |||
| &FreeMonitorWidget::monitorAddressesChanged, | |||
| &parent_, [this] | |||
| @@ -107,6 +141,15 @@ void RuntimePanelController::configure() | |||
| status_reporter_(message, 5000); | |||
| output_reporter_(QObject::tr("页面导航失败:%1").arg(message)); | |||
| }); | |||
| QObject::connect(runtime_monitor_widget_, &RuntimeMonitorWidget::runtimePageChanged, | |||
| &parent_, [this](const QString &page_id) | |||
| { | |||
| if (hmi_runtime_window_ != nullptr) | |||
| { | |||
| hmi_runtime_window_->setRuntimePage( | |||
| page_id.toUtf8().toStdString()); | |||
| } | |||
| }); | |||
| ui_.editorTabWidget->setTabVisible(2, false); | |||
| runtime_refresh_timer_ = new QTimer(&parent_); | |||
| @@ -129,6 +172,46 @@ RuntimeMonitorWidget *RuntimePanelController::runtimeMonitorWidget() const | |||
| return runtime_monitor_widget_; | |||
| } | |||
| HmiRuntimeWindow *RuntimePanelController::hmiRuntimeWindow() const | |||
| { | |||
| return hmi_runtime_window_.get(); | |||
| } | |||
| void RuntimePanelController::enterRuntime( | |||
| const std::string &page_id, | |||
| const std::string &logic_id, | |||
| ApplicationMode mode, | |||
| PlcConnectionState plc_state) | |||
| { | |||
| if (!runtime_session_active_) | |||
| { | |||
| runtime_monitor_widget_->setProjectObjects(page_id, logic_id); | |||
| hmi_runtime_window_->setRuntimePage(page_id); | |||
| hmi_runtime_window_->reloadRuntimePage(); | |||
| runtime_session_active_ = true; | |||
| } | |||
| runtime_monitor_widget_->setMode(mode, plc_state); | |||
| hmi_runtime_window_->setMode(mode, plc_state); | |||
| hmi_runtime_window_->showForRuntime(); | |||
| } | |||
| void RuntimePanelController::leaveRuntime( | |||
| ApplicationMode mode, PlcConnectionState plc_state) | |||
| { | |||
| runtime_session_active_ = false; | |||
| runtime_monitor_widget_->setMode(mode, plc_state); | |||
| hmi_runtime_window_->setMode(mode, plc_state); | |||
| hmi_runtime_window_->hideForEditing(); | |||
| } | |||
| void RuntimePanelController::closeForApplicationExit() | |||
| { | |||
| if (hmi_runtime_window_ != nullptr) | |||
| { | |||
| hmi_runtime_window_->closeForApplicationExit(); | |||
| } | |||
| } | |||
| void RuntimePanelController::updateSimulationUi(bool report_fault) | |||
| { | |||
| const bool offline = runtime_mode_service_.mode() | |||
| @@ -140,14 +223,21 @@ void RuntimePanelController::updateSimulationUi(bool report_fault) | |||
| const SimulationState state = runtime_mode_service_.simulationState(); | |||
| const bool write_enabled = (online && plc_connected) | |||
| || (offline && state == SimulationState::Running); | |||
| hmi_editor_widget_.setRuntimeWriteEnabled(write_enabled); | |||
| // 操作员输入只允许从独立 HMI 窗口发出,主窗口的画布全部只读 | |||
| hmi_editor_widget_.setRuntimeWriteEnabled(false); | |||
| if (runtime_monitor_widget_ != nullptr) | |||
| { | |||
| runtime_monitor_widget_->setHmiWriteEnabled(write_enabled); | |||
| runtime_monitor_widget_->setHmiWriteEnabled(false); | |||
| runtime_monitor_widget_->setFreeMonitorWriteEnabled(write_enabled); | |||
| runtime_monitor_widget_->refreshValues( | |||
| runtime_mode_service_.mode(), | |||
| runtime_mode_service_.plcConnectionState()); | |||
| } | |||
| if (hmi_runtime_window_ != nullptr) | |||
| { | |||
| hmi_runtime_window_->setHmiWriteEnabled(write_enabled); | |||
| hmi_runtime_window_->refreshValues(); | |||
| } | |||
| switch (state) | |||
| { | |||
| @@ -178,6 +268,11 @@ void RuntimePanelController::updateSimulationUi(bool report_fault) | |||
| runtime_monitor_widget_->setProjectObjects( | |||
| hmi_navigation_service_.currentPageId(), logic_id); | |||
| } | |||
| if (hmi_runtime_window_ != nullptr) | |||
| { | |||
| hmi_runtime_window_->setRuntimePage( | |||
| hmi_navigation_service_.currentPageId()); | |||
| } | |||
| logic_editor_widget_.setRuntimeTrace( | |||
| runtime_mode_service_.offlineSimulationService() | |||
| .traceSnapshot().forLogic(logic_id), | |||
| @@ -233,6 +328,10 @@ void RuntimePanelController::handleRuntimeTimer() | |||
| runtime_mode_service_.mode(), | |||
| runtime_mode_service_.plcConnectionState()); | |||
| } | |||
| if (hmi_runtime_window_ != nullptr) | |||
| { | |||
| hmi_runtime_window_->refreshValues(); | |||
| } | |||
| updateSimulationUi(false); | |||
| } | |||
| @@ -12,6 +12,7 @@ | |||
| class AlarmService; | |||
| class HmiEditorService; | |||
| class HmiEditorWidget; | |||
| class HmiRuntimeWindow; | |||
| class HmiNavigationService; | |||
| class HmiRuntimeService; | |||
| class LogicEditorService; | |||
| @@ -35,6 +36,7 @@ public: | |||
| using StatusReporter = std::function<void( | |||
| const QString &message, int timeout_ms)>; | |||
| using OutputReporter = std::function<void(const QString &message)>; | |||
| using RuntimeExitRequester = std::function<void()>; | |||
| RuntimePanelController( | |||
| QWidget &parent, | |||
| @@ -54,11 +56,21 @@ public: | |||
| std::function<void(const std::string &)> select_logic, | |||
| std::function<void(const std::string &)> show_logic_properties, | |||
| StatusReporter status_reporter, | |||
| OutputReporter output_reporter); | |||
| OutputReporter output_reporter, | |||
| RuntimeExitRequester runtime_exit_requester); | |||
| ~RuntimePanelController(); | |||
| void configure(); | |||
| void enterRuntime( | |||
| const std::string &page_id, | |||
| const std::string &logic_id, | |||
| ApplicationMode mode, | |||
| PlcConnectionState plc_state); | |||
| void leaveRuntime(ApplicationMode mode, PlcConnectionState plc_state); | |||
| void closeForApplicationExit(); | |||
| void updateSimulationUi(bool report_fault); | |||
| RuntimeMonitorWidget *runtimeMonitorWidget() const; | |||
| HmiRuntimeWindow *hmiRuntimeWindow() const; | |||
| private: | |||
| void handleRuntimeTimer(); | |||
| @@ -83,6 +95,9 @@ private: | |||
| std::function<void(const std::string &)> show_logic_properties_; | |||
| StatusReporter status_reporter_; | |||
| OutputReporter output_reporter_; | |||
| RuntimeExitRequester runtime_exit_requester_; | |||
| RuntimeMonitorWidget *runtime_monitor_widget_ = nullptr; | |||
| std::unique_ptr<HmiRuntimeWindow> hmi_runtime_window_; | |||
| QTimer *runtime_refresh_timer_ = nullptr; | |||
| bool runtime_session_active_ = false; | |||
| }; | |||
| @@ -14,6 +14,7 @@ | |||
| #include "services/register_comment_service.h" | |||
| #include "ui/hmi_editor_widget.h" | |||
| #include "ui/free_monitor_widget.h" | |||
| #include "ui/hmi_runtime_window.h" | |||
| #include "ui/logic_editor_widget.h" | |||
| #include "ui/main_window.h" | |||
| #include "ui/plc_connection_dialog.h" | |||
| @@ -278,6 +279,19 @@ ObjectType *requiredChild(MainWindow &window, const char *name) | |||
| return child; | |||
| } | |||
| HmiRuntimeWindow *findHmiRuntimeWindow() | |||
| { | |||
| for (QWidget *widget : QApplication::topLevelWidgets()) | |||
| { | |||
| auto *runtime_window = qobject_cast<HmiRuntimeWindow *>(widget); | |||
| if (runtime_window != nullptr) | |||
| { | |||
| return runtime_window; | |||
| } | |||
| } | |||
| return nullptr; | |||
| } | |||
| QColor renderedColorAt(HmiEditorWidget &view, const QPoint &viewport_position) | |||
| { | |||
| QImage image(view.viewport()->size(), QImage::Format_ARGB32_Premultiplied); | |||
| @@ -1207,6 +1221,142 @@ void testModeActionsControlEditingAvailability() | |||
| "rejected online running must restore the editing action"); | |||
| } | |||
| void testIndependentHmiRuntimeWindowLifecycle() | |||
| { | |||
| 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 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, | |||
| "runtime-window fixture must configure a PageJump target"); | |||
| const HmiEditorResult return_jump_result = editor_service.addControl( | |||
| settings_page_id, HmiControlType::PageJump); | |||
| HmiControl return_jump = *editor_service.findControl( | |||
| settings_page_id, return_jump_result.id); | |||
| return_jump.pageJump = HmiPageJumpConfig{main_page_id}; | |||
| require(editor_service.updateControl( | |||
| settings_page_id, return_jump_result.id, return_jump).succeeded, | |||
| "runtime-window fixture must configure a PageJump back to the initial page"); | |||
| VirtualRegisterRepository repository; | |||
| HmiRuntimeService runtime_service(repository); | |||
| AlarmEditorService alarm_editor_service(project_service); | |||
| AlarmService alarm_service(project_service, repository); | |||
| RegisterCommentService register_comment_service(project_service); | |||
| 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, | |||
| alarm_editor_service, | |||
| alarm_service, | |||
| navigation_service, | |||
| register_comment_service, | |||
| monitor_service); | |||
| window.show(); | |||
| QApplication::processEvents(); | |||
| requiredChild<QAction>(window, "offlineModeAction")->trigger(); | |||
| QApplication::processEvents(); | |||
| HmiRuntimeWindow *runtime_window = findHmiRuntimeWindow(); | |||
| require(mode_service.mode() == ApplicationMode::OfflineRunning, | |||
| "offline mode must remain the authoritative runtime state"); | |||
| require(runtime_window != nullptr && runtime_window->isVisible(), | |||
| "entering offline running must show an independent HMI window"); | |||
| require(runtime_window->findChild<HmiEditorWidget *>( | |||
| QStringLiteral("hmiRuntimeHmiView")) != nullptr, | |||
| "independent HMI window must contain a runtime-only HMI view"); | |||
| HmiEditorWidget *runtime_hmi = runtime_window->findChild<HmiEditorWidget *>( | |||
| QStringLiteral("hmiRuntimeHmiView")); | |||
| QGraphicsItem *jump_item = nullptr; | |||
| for (QGraphicsItem *item : runtime_hmi->scene()->items()) | |||
| { | |||
| if (item->zValue() >= 0.0) | |||
| { | |||
| jump_item = item; | |||
| break; | |||
| } | |||
| } | |||
| require(jump_item != nullptr, | |||
| "independent HMI window must project the configured PageJump"); | |||
| QTest::mouseClick(runtime_hmi->viewport(), Qt::LeftButton, Qt::NoModifier, | |||
| runtime_hmi->mapFromScene( | |||
| jump_item->mapToScene(jump_item->boundingRect().center()))); | |||
| QApplication::processEvents(); | |||
| require(navigation_service.currentPageId() == settings_page_id, | |||
| "PageJump in the independent window must update the shared navigation session"); | |||
| require(runtime_window->findChild<QLabel *>(QStringLiteral("runtimePageLabel")) | |||
| ->text() == QStringLiteral("Settings"), | |||
| "independent window title must follow the navigated runtime page"); | |||
| require(requiredChild<QLabel>(window, "runtimePageLabel")->text() | |||
| == QStringLiteral("Settings"), | |||
| "diagnostic runtime view must stay synchronized with the independent HMI page"); | |||
| QGraphicsItem *return_jump_item = nullptr; | |||
| for (QGraphicsItem *item : runtime_hmi->scene()->items()) | |||
| { | |||
| if (item->zValue() >= 0.0) | |||
| { | |||
| return_jump_item = item; | |||
| break; | |||
| } | |||
| } | |||
| require(return_jump_item != nullptr, | |||
| "the destination page must expose a PageJump back to the initial page"); | |||
| QTest::mouseClick(runtime_hmi->viewport(), Qt::LeftButton, Qt::NoModifier, | |||
| runtime_hmi->mapFromScene( | |||
| return_jump_item->mapToScene( | |||
| return_jump_item->boundingRect().center()))); | |||
| QApplication::processEvents(); | |||
| require(navigation_service.currentPageId() == main_page_id, | |||
| "the independent HMI must return to the initial page before editing"); | |||
| require(runtime_window->findChild<QLabel *>(QStringLiteral("runtimePageLabel")) | |||
| ->text() == QStringLiteral("主操作页面"), | |||
| "the independent window must keep the initial page selected across sessions"); | |||
| requiredChild<QAction>(window, "editingModeAction")->trigger(); | |||
| QApplication::processEvents(); | |||
| require(mode_service.mode() == ApplicationMode::Editing, | |||
| "editing action must return the runtime mode to editing"); | |||
| require(!runtime_window->isVisible(), | |||
| "returning to editing must hide the independent HMI window"); | |||
| requiredChild<QAction>(window, "addLabelAction")->trigger(); | |||
| QApplication::processEvents(); | |||
| require(editor_service.findPage(main_page_id)->controls.size() == 2U, | |||
| "editing after a runtime session must add the new control to the page"); | |||
| requiredChild<QAction>(window, "offlineModeAction")->trigger(); | |||
| QApplication::processEvents(); | |||
| runtime_window = findHmiRuntimeWindow(); | |||
| require(runtime_window != nullptr && runtime_window->isVisible(), | |||
| "the HMI window must be reusable for a later runtime session"); | |||
| runtime_hmi = runtime_window->findChild<HmiEditorWidget *>( | |||
| QStringLiteral("hmiRuntimeHmiView")); | |||
| require(runtime_hmi != nullptr && runtime_hmi->scene()->items().size() == 3, | |||
| "a later runtime session must refresh the independent HMI page after an edit"); | |||
| require(requiredChild<RuntimeMonitorWidget>(window, "runtimeMonitorWidget") | |||
| ->findChild<HmiEditorWidget *>(QStringLiteral("runtimeHmiView")) | |||
| ->scene()->items().size() == 3, | |||
| "the diagnostic HMI must keep projecting the edited page"); | |||
| runtime_window->close(); | |||
| QApplication::processEvents(); | |||
| require(mode_service.mode() == ApplicationMode::Editing, | |||
| "closing the HMI window must request a transition to editing"); | |||
| require(!runtime_window->isVisible(), | |||
| "closing the HMI window must not leave a visible runtime surface"); | |||
| } | |||
| void testEdgeTimerPropertyEditingAndParallelMenu() | |||
| { | |||
| TestProjectStorage storage; | |||
| @@ -1733,6 +1883,7 @@ int main(int argc, char *argv[]) | |||
| testRuntimeAlarmListInteraction(); | |||
| testWindowTitleTracksUnsavedProjectChanges(); | |||
| testModeActionsControlEditingAvailability(); | |||
| testIndependentHmiRuntimeWindowLifecycle(); | |||
| testEdgeTimerPropertyEditingAndParallelMenu(); | |||
| testPlcConfigurationUsesDialog(); | |||
| testRepeatedPlcStatusNotificationsAreCoalesced(); | |||
| @@ -15,6 +15,7 @@ SOURCES += \ | |||
| ../src/ui/register_comment_dialog.cpp \ | |||
| ../src/ui/plc_connection_dialog.cpp \ | |||
| ../src/ui/free_monitor_widget.cpp \ | |||
| ../src/ui/hmi_runtime_window.cpp \ | |||
| ../src/ui/runtime_monitor_widget.cpp \ | |||
| ../src/ui/toolbar_icon_factory.cpp \ | |||
| ../src/ui/project_workspace_controller.cpp \ | |||
| @@ -52,6 +53,7 @@ HEADERS += \ | |||
| ../src/ui/register_comment_dialog.h \ | |||
| ../src/ui/plc_connection_dialog.h \ | |||
| ../src/ui/free_monitor_widget.h \ | |||
| ../src/ui/hmi_runtime_window.h \ | |||
| ../src/ui/runtime_monitor_widget.h \ | |||
| ../src/ui/toolbar_icon_factory.h \ | |||
| ../src/ui/project_workspace_controller.h \ | |||
| @@ -94,5 +96,6 @@ FORMS += \ | |||
| ../src/ui/register_comment_dialog.ui \ | |||
| ../src/ui/plc_connection_dialog.ui \ | |||
| ../src/ui/free_monitor_widget.ui \ | |||
| ../src/ui/hmi_runtime_window.ui \ | |||
| ../src/ui/runtime_monitor_widget.ui \ | |||
| ../src/ui/logic_instruction_dialog.ui | |||