| @@ -22,6 +22,7 @@ SOURCES += \ | |||||
| src/ui/register_comment_dialog.cpp \ | src/ui/register_comment_dialog.cpp \ | ||||
| src/ui/plc_connection_dialog.cpp \ | src/ui/plc_connection_dialog.cpp \ | ||||
| src/ui/free_monitor_widget.cpp \ | src/ui/free_monitor_widget.cpp \ | ||||
| src/ui/hmi_runtime_window.cpp \ | |||||
| src/ui/runtime_monitor_widget.cpp \ | src/ui/runtime_monitor_widget.cpp \ | ||||
| src/ui/toolbar_icon_factory.cpp \ | src/ui/toolbar_icon_factory.cpp \ | ||||
| src/ui/project_workspace_controller.cpp \ | src/ui/project_workspace_controller.cpp \ | ||||
| @@ -63,6 +64,7 @@ HEADERS += \ | |||||
| src/ui/register_comment_dialog.h \ | src/ui/register_comment_dialog.h \ | ||||
| src/ui/plc_connection_dialog.h \ | src/ui/plc_connection_dialog.h \ | ||||
| src/ui/free_monitor_widget.h \ | src/ui/free_monitor_widget.h \ | ||||
| src/ui/hmi_runtime_window.h \ | |||||
| src/ui/runtime_monitor_widget.h \ | src/ui/runtime_monitor_widget.h \ | ||||
| src/ui/toolbar_icon_factory.h \ | src/ui/toolbar_icon_factory.h \ | ||||
| src/ui/project_workspace_controller.h \ | src/ui/project_workspace_controller.h \ | ||||
| @@ -108,4 +110,5 @@ FORMS += \ | |||||
| src/ui/register_comment_dialog.ui \ | src/ui/register_comment_dialog.ui \ | ||||
| src/ui/plc_connection_dialog.ui \ | src/ui/plc_connection_dialog.ui \ | ||||
| src/ui/free_monitor_widget.ui \ | src/ui/free_monitor_widget.ui \ | ||||
| src/ui/hmi_runtime_window.ui \ | |||||
| src/ui/runtime_monitor_widget.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(); | event->ignore(); | ||||
| return; | return; | ||||
| } | } | ||||
| if (runtime_panel_controller_ != nullptr) | |||||
| { | |||||
| runtime_panel_controller_->closeForApplicationExit(); | |||||
| } | |||||
| QMainWindow::closeEvent(event); | QMainWindow::closeEvent(event); | ||||
| } | } | ||||
| @@ -854,6 +858,10 @@ void MainWindow::configureRuntimeMonitor() | |||||
| [this](const QString &message) | [this](const QString &message) | ||||
| { | { | ||||
| appendOutputMessage(message); | appendOutputMessage(message); | ||||
| }, | |||||
| [this] | |||||
| { | |||||
| requestMode(ApplicationMode::Editing); | |||||
| }); | }); | ||||
| runtime_panel_controller_->configure(); | runtime_panel_controller_->configure(); | ||||
| runtime_monitor_widget_ = runtime_panel_controller_->runtimeMonitorWidget(); | runtime_monitor_widget_ = runtime_panel_controller_->runtimeMonitorWidget(); | ||||
| @@ -1552,15 +1560,17 @@ void MainWindow::updateModeUi(const QString &message) | |||||
| statusBar()->showMessage(fromUtf8(navigation.message), 5000); | 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); | ui_->editorTabWidget->setCurrentWidget(ui_->runtimeMonitorTab); | ||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| hmi_navigation_service_->stop(); | hmi_navigation_service_->stop(); | ||||
| runtime_monitor_widget_->setMode( | |||||
| runtime_panel_controller_->leaveRuntime( | |||||
| mode, runtime_mode_service_.plcConnectionState()); | mode, runtime_mode_service_.plcConnectionState()); | ||||
| if (ui_->editorTabWidget->currentWidget() == ui_->runtimeMonitorTab) | if (ui_->editorTabWidget->currentWidget() == ui_->runtimeMonitorTab) | ||||
| { | { | ||||
| @@ -97,7 +97,7 @@ 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) | ||||
| { | { | ||||
| showRuntimePage(page_id); | |||||
| setRuntimePage(page_id); | |||||
| const QSignalBlocker blocker(ui_->runtimeLogicComboBox); | const QSignalBlocker blocker(ui_->runtimeLogicComboBox); | ||||
| ui_->runtimeLogicComboBox->clear(); | ui_->runtimeLogicComboBox->clear(); | ||||
| @@ -120,6 +120,11 @@ void RuntimeMonitorWidget::setProjectObjects( | |||||
| : std::string{}); | : std::string{}); | ||||
| } | } | ||||
| void RuntimeMonitorWidget::setRuntimePage(const std::string &page_id) | |||||
| { | |||||
| showRuntimePage(page_id); | |||||
| } | |||||
| void RuntimeMonitorWidget::setMode( | void RuntimeMonitorWidget::setMode( | ||||
| ApplicationMode mode, PlcConnectionState plc_state) | ApplicationMode mode, PlcConnectionState plc_state) | ||||
| { | { | ||||
| @@ -127,7 +132,10 @@ void RuntimeMonitorWidget::setMode( | |||||
| const bool online = mode == ApplicationMode::OnlineRunning; | const bool online = mode == ApplicationMode::OnlineRunning; | ||||
| ui_->logicFrame->setVisible(offline); | ui_->logicFrame->setVisible(offline); | ||||
| hmi_view_->setRuntimeActive(offline || online); | 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) | if (!offline) | ||||
| { | { | ||||
| logic_view_->clearRuntimeTrace(); | logic_view_->clearRuntimeTrace(); | ||||
| @@ -144,6 +152,10 @@ void RuntimeMonitorWidget::setMode( | |||||
| void RuntimeMonitorWidget::setHmiWriteEnabled(bool enabled) | void RuntimeMonitorWidget::setHmiWriteEnabled(bool enabled) | ||||
| { | { | ||||
| hmi_view_->setRuntimeWriteEnabled(enabled); | hmi_view_->setRuntimeWriteEnabled(enabled); | ||||
| } | |||||
| void RuntimeMonitorWidget::setFreeMonitorWriteEnabled(bool enabled) | |||||
| { | |||||
| free_monitor_widget_->setWriteEnabled(enabled); | free_monitor_widget_->setWriteEnabled(enabled); | ||||
| } | } | ||||
| @@ -41,9 +41,11 @@ public: | |||||
| ~RuntimeMonitorWidget() override; | ~RuntimeMonitorWidget() override; | ||||
| void setProjectObjects(const std::string &page_id, const std::string &logic_id); | void setProjectObjects(const std::string &page_id, const std::string &logic_id); | ||||
| void setRuntimePage(const std::string &page_id); | |||||
| void setMode(ApplicationMode mode, PlcConnectionState plc_state); | void setMode(ApplicationMode mode, PlcConnectionState plc_state); | ||||
| void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); | void refreshValues(ApplicationMode mode, PlcConnectionState plc_state); | ||||
| void setHmiWriteEnabled(bool enabled); | void setHmiWriteEnabled(bool enabled); | ||||
| void setFreeMonitorWriteEnabled(bool enabled); | |||||
| void setLogicTrace(const LogicTraceSnapshot &trace, const std::string &fault_node_id = {}); | void setLogicTrace(const LogicTraceSnapshot &trace, const std::string &fault_node_id = {}); | ||||
| FreeMonitorWidget *freeMonitorWidget() const; | FreeMonitorWidget *freeMonitorWidget() const; | ||||
| std::string selectedLogicId() const; | std::string selectedLogicId() const; | ||||
| @@ -2,6 +2,7 @@ | |||||
| #include "free_monitor_widget.h" | #include "free_monitor_widget.h" | ||||
| #include "hmi_editor_widget.h" | #include "hmi_editor_widget.h" | ||||
| #include "hmi_runtime_window.h" | |||||
| #include "logic_editor_widget.h" | #include "logic_editor_widget.h" | ||||
| #include "runtime_monitor_widget.h" | #include "runtime_monitor_widget.h" | ||||
| #include "services/alarm_service.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 &)> select_logic, | ||||
| std::function<void(const std::string &)> show_logic_properties, | std::function<void(const std::string &)> show_logic_properties, | ||||
| StatusReporter status_reporter, | StatusReporter status_reporter, | ||||
| OutputReporter output_reporter) | |||||
| OutputReporter output_reporter, | |||||
| RuntimeExitRequester runtime_exit_requester) | |||||
| : parent_(parent), | : parent_(parent), | ||||
| ui_(ui), | ui_(ui), | ||||
| runtime_mode_service_(runtime_mode_service), | runtime_mode_service_(runtime_mode_service), | ||||
| @@ -68,10 +70,13 @@ RuntimePanelController::RuntimePanelController( | |||||
| select_logic_(std::move(select_logic)), | select_logic_(std::move(select_logic)), | ||||
| show_logic_properties_(std::move(show_logic_properties)), | show_logic_properties_(std::move(show_logic_properties)), | ||||
| status_reporter_(std::move(status_reporter)), | 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() | void RuntimePanelController::configure() | ||||
| { | { | ||||
| runtime_monitor_widget_ = new RuntimeMonitorWidget( | runtime_monitor_widget_ = new RuntimeMonitorWidget( | ||||
| @@ -85,6 +90,35 @@ void RuntimePanelController::configure() | |||||
| ui_.runtimeMonitorTab); | ui_.runtimeMonitorTab); | ||||
| runtime_monitor_widget_->setObjectName(QStringLiteral("runtimeMonitorWidget")); | runtime_monitor_widget_->setObjectName(QStringLiteral("runtimeMonitorWidget")); | ||||
| ui_.runtimeMonitorLayout->addWidget(runtime_monitor_widget_); | 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(), | QObject::connect(runtime_monitor_widget_->freeMonitorWidget(), | ||||
| &FreeMonitorWidget::monitorAddressesChanged, | &FreeMonitorWidget::monitorAddressesChanged, | ||||
| &parent_, [this] | &parent_, [this] | ||||
| @@ -107,6 +141,15 @@ void RuntimePanelController::configure() | |||||
| status_reporter_(message, 5000); | status_reporter_(message, 5000); | ||||
| output_reporter_(QObject::tr("页面导航失败:%1").arg(message)); | 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); | ui_.editorTabWidget->setTabVisible(2, false); | ||||
| runtime_refresh_timer_ = new QTimer(&parent_); | runtime_refresh_timer_ = new QTimer(&parent_); | ||||
| @@ -129,6 +172,46 @@ RuntimeMonitorWidget *RuntimePanelController::runtimeMonitorWidget() const | |||||
| return runtime_monitor_widget_; | 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) | void RuntimePanelController::updateSimulationUi(bool report_fault) | ||||
| { | { | ||||
| const bool offline = runtime_mode_service_.mode() | 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 SimulationState state = runtime_mode_service_.simulationState(); | ||||
| const bool write_enabled = (online && plc_connected) | const bool write_enabled = (online && plc_connected) | ||||
| || (offline && state == SimulationState::Running); | || (offline && state == SimulationState::Running); | ||||
| hmi_editor_widget_.setRuntimeWriteEnabled(write_enabled); | |||||
| // 操作员输入只允许从独立 HMI 窗口发出,主窗口的画布全部只读 | |||||
| hmi_editor_widget_.setRuntimeWriteEnabled(false); | |||||
| if (runtime_monitor_widget_ != nullptr) | 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_monitor_widget_->refreshValues( | ||||
| runtime_mode_service_.mode(), | runtime_mode_service_.mode(), | ||||
| runtime_mode_service_.plcConnectionState()); | runtime_mode_service_.plcConnectionState()); | ||||
| } | } | ||||
| if (hmi_runtime_window_ != nullptr) | |||||
| { | |||||
| hmi_runtime_window_->setHmiWriteEnabled(write_enabled); | |||||
| hmi_runtime_window_->refreshValues(); | |||||
| } | |||||
| switch (state) | switch (state) | ||||
| { | { | ||||
| @@ -178,6 +268,11 @@ void RuntimePanelController::updateSimulationUi(bool report_fault) | |||||
| runtime_monitor_widget_->setProjectObjects( | runtime_monitor_widget_->setProjectObjects( | ||||
| hmi_navigation_service_.currentPageId(), logic_id); | hmi_navigation_service_.currentPageId(), logic_id); | ||||
| } | } | ||||
| if (hmi_runtime_window_ != nullptr) | |||||
| { | |||||
| hmi_runtime_window_->setRuntimePage( | |||||
| hmi_navigation_service_.currentPageId()); | |||||
| } | |||||
| logic_editor_widget_.setRuntimeTrace( | logic_editor_widget_.setRuntimeTrace( | ||||
| runtime_mode_service_.offlineSimulationService() | runtime_mode_service_.offlineSimulationService() | ||||
| .traceSnapshot().forLogic(logic_id), | .traceSnapshot().forLogic(logic_id), | ||||
| @@ -233,6 +328,10 @@ void RuntimePanelController::handleRuntimeTimer() | |||||
| runtime_mode_service_.mode(), | runtime_mode_service_.mode(), | ||||
| runtime_mode_service_.plcConnectionState()); | runtime_mode_service_.plcConnectionState()); | ||||
| } | } | ||||
| if (hmi_runtime_window_ != nullptr) | |||||
| { | |||||
| hmi_runtime_window_->refreshValues(); | |||||
| } | |||||
| updateSimulationUi(false); | updateSimulationUi(false); | ||||
| } | } | ||||
| @@ -12,6 +12,7 @@ | |||||
| class AlarmService; | class AlarmService; | ||||
| class HmiEditorService; | class HmiEditorService; | ||||
| class HmiEditorWidget; | class HmiEditorWidget; | ||||
| class HmiRuntimeWindow; | |||||
| class HmiNavigationService; | class HmiNavigationService; | ||||
| class HmiRuntimeService; | class HmiRuntimeService; | ||||
| class LogicEditorService; | class LogicEditorService; | ||||
| @@ -35,6 +36,7 @@ public: | |||||
| using StatusReporter = std::function<void( | using StatusReporter = std::function<void( | ||||
| const QString &message, int timeout_ms)>; | const QString &message, int timeout_ms)>; | ||||
| using OutputReporter = std::function<void(const QString &message)>; | using OutputReporter = std::function<void(const QString &message)>; | ||||
| using RuntimeExitRequester = std::function<void()>; | |||||
| RuntimePanelController( | RuntimePanelController( | ||||
| QWidget &parent, | QWidget &parent, | ||||
| @@ -54,11 +56,21 @@ public: | |||||
| std::function<void(const std::string &)> select_logic, | std::function<void(const std::string &)> select_logic, | ||||
| std::function<void(const std::string &)> show_logic_properties, | std::function<void(const std::string &)> show_logic_properties, | ||||
| StatusReporter status_reporter, | StatusReporter status_reporter, | ||||
| OutputReporter output_reporter); | |||||
| OutputReporter output_reporter, | |||||
| RuntimeExitRequester runtime_exit_requester); | |||||
| ~RuntimePanelController(); | |||||
| void configure(); | 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); | void updateSimulationUi(bool report_fault); | ||||
| RuntimeMonitorWidget *runtimeMonitorWidget() const; | RuntimeMonitorWidget *runtimeMonitorWidget() const; | ||||
| HmiRuntimeWindow *hmiRuntimeWindow() const; | |||||
| private: | private: | ||||
| void handleRuntimeTimer(); | void handleRuntimeTimer(); | ||||
| @@ -83,6 +95,9 @@ private: | |||||
| std::function<void(const std::string &)> show_logic_properties_; | std::function<void(const std::string &)> show_logic_properties_; | ||||
| StatusReporter status_reporter_; | StatusReporter status_reporter_; | ||||
| OutputReporter output_reporter_; | OutputReporter output_reporter_; | ||||
| RuntimeExitRequester runtime_exit_requester_; | |||||
| RuntimeMonitorWidget *runtime_monitor_widget_ = nullptr; | RuntimeMonitorWidget *runtime_monitor_widget_ = nullptr; | ||||
| std::unique_ptr<HmiRuntimeWindow> hmi_runtime_window_; | |||||
| QTimer *runtime_refresh_timer_ = nullptr; | QTimer *runtime_refresh_timer_ = nullptr; | ||||
| bool runtime_session_active_ = false; | |||||
| }; | }; | ||||
| @@ -14,6 +14,7 @@ | |||||
| #include "services/register_comment_service.h" | #include "services/register_comment_service.h" | ||||
| #include "ui/hmi_editor_widget.h" | #include "ui/hmi_editor_widget.h" | ||||
| #include "ui/free_monitor_widget.h" | #include "ui/free_monitor_widget.h" | ||||
| #include "ui/hmi_runtime_window.h" | |||||
| #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" | ||||
| @@ -278,6 +279,19 @@ ObjectType *requiredChild(MainWindow &window, const char *name) | |||||
| return child; | 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) | QColor renderedColorAt(HmiEditorWidget &view, const QPoint &viewport_position) | ||||
| { | { | ||||
| QImage image(view.viewport()->size(), QImage::Format_ARGB32_Premultiplied); | QImage image(view.viewport()->size(), QImage::Format_ARGB32_Premultiplied); | ||||
| @@ -1207,6 +1221,142 @@ void testModeActionsControlEditingAvailability() | |||||
| "rejected online running must restore the editing action"); | "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() | void testEdgeTimerPropertyEditingAndParallelMenu() | ||||
| { | { | ||||
| TestProjectStorage storage; | TestProjectStorage storage; | ||||
| @@ -1733,6 +1883,7 @@ int main(int argc, char *argv[]) | |||||
| testRuntimeAlarmListInteraction(); | testRuntimeAlarmListInteraction(); | ||||
| testWindowTitleTracksUnsavedProjectChanges(); | testWindowTitleTracksUnsavedProjectChanges(); | ||||
| testModeActionsControlEditingAvailability(); | testModeActionsControlEditingAvailability(); | ||||
| testIndependentHmiRuntimeWindowLifecycle(); | |||||
| testEdgeTimerPropertyEditingAndParallelMenu(); | testEdgeTimerPropertyEditingAndParallelMenu(); | ||||
| testPlcConfigurationUsesDialog(); | testPlcConfigurationUsesDialog(); | ||||
| testRepeatedPlcStatusNotificationsAreCoalesced(); | testRepeatedPlcStatusNotificationsAreCoalesced(); | ||||
| @@ -15,6 +15,7 @@ SOURCES += \ | |||||
| ../src/ui/register_comment_dialog.cpp \ | ../src/ui/register_comment_dialog.cpp \ | ||||
| ../src/ui/plc_connection_dialog.cpp \ | ../src/ui/plc_connection_dialog.cpp \ | ||||
| ../src/ui/free_monitor_widget.cpp \ | ../src/ui/free_monitor_widget.cpp \ | ||||
| ../src/ui/hmi_runtime_window.cpp \ | |||||
| ../src/ui/runtime_monitor_widget.cpp \ | ../src/ui/runtime_monitor_widget.cpp \ | ||||
| ../src/ui/toolbar_icon_factory.cpp \ | ../src/ui/toolbar_icon_factory.cpp \ | ||||
| ../src/ui/project_workspace_controller.cpp \ | ../src/ui/project_workspace_controller.cpp \ | ||||
| @@ -52,6 +53,7 @@ HEADERS += \ | |||||
| ../src/ui/register_comment_dialog.h \ | ../src/ui/register_comment_dialog.h \ | ||||
| ../src/ui/plc_connection_dialog.h \ | ../src/ui/plc_connection_dialog.h \ | ||||
| ../src/ui/free_monitor_widget.h \ | ../src/ui/free_monitor_widget.h \ | ||||
| ../src/ui/hmi_runtime_window.h \ | |||||
| ../src/ui/runtime_monitor_widget.h \ | ../src/ui/runtime_monitor_widget.h \ | ||||
| ../src/ui/toolbar_icon_factory.h \ | ../src/ui/toolbar_icon_factory.h \ | ||||
| ../src/ui/project_workspace_controller.h \ | ../src/ui/project_workspace_controller.h \ | ||||
| @@ -94,5 +96,6 @@ FORMS += \ | |||||
| ../src/ui/register_comment_dialog.ui \ | ../src/ui/register_comment_dialog.ui \ | ||||
| ../src/ui/plc_connection_dialog.ui \ | ../src/ui/plc_connection_dialog.ui \ | ||||
| ../src/ui/free_monitor_widget.ui \ | ../src/ui/free_monitor_widget.ui \ | ||||
| ../src/ui/hmi_runtime_window.ui \ | |||||
| ../src/ui/runtime_monitor_widget.ui \ | ../src/ui/runtime_monitor_widget.ui \ | ||||
| ../src/ui/logic_instruction_dialog.ui | ../src/ui/logic_instruction_dialog.ui | ||||