| @@ -22,7 +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_window.cpp \ | |||
| src/ui/runtime_monitor_widget.cpp \ | |||
| src/ui/toolbar_icon_factory.cpp \ | |||
| src/ui/project_workspace_controller.cpp \ | |||
| @@ -64,7 +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_window.h \ | |||
| src/ui/runtime_monitor_widget.h \ | |||
| src/ui/toolbar_icon_factory.h \ | |||
| src/ui/project_workspace_controller.h \ | |||
| @@ -110,5 +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_window.ui \ | |||
| src/ui/runtime_monitor_widget.ui | |||
| @@ -66,7 +66,7 @@ constexpr std::array kControlDescriptors = { | |||
| "报警列表", | |||
| "alarm-list", | |||
| "报警信息", | |||
| {0, 0, 360, 180}, | |||
| {0, 0, 360, 136}, | |||
| HmiBindingKind::None, | |||
| HmiRuntimeValueKind::None, | |||
| false}, | |||
| @@ -25,6 +25,8 @@ static_assert(kMaximumLadderColumns == kMaximumConditionColumns + 1); | |||
| constexpr std::size_t kMaximumHmiProperties = 64U; | |||
| constexpr std::size_t kMaximumIdBytes = 128U; | |||
| constexpr std::size_t kMaximumTextBytes = 4096U; | |||
| constexpr std::size_t kMaximumAlarmTitleCharacters = 12U; | |||
| constexpr std::size_t kMaximumAlarmMessageCharacters = 20U; | |||
| constexpr std::size_t kMaximumPropertyKeyBytes = 128U; | |||
| constexpr std::size_t kMaximumPropertyValueBytes = 4096U; | |||
| constexpr std::size_t kMaximumRegisterCommentBytes = 64U; | |||
| @@ -36,6 +38,8 @@ constexpr int kMaximumHmiControlWidth = 8192; | |||
| constexpr int kMaximumHmiControlHeight = 8192; | |||
| constexpr int kMinimumHmiFontPointSize = 6; | |||
| constexpr int kMaximumHmiFontPointSize = 72; | |||
| constexpr int kAlarmDefaultFontPointReduction = 2; | |||
| constexpr int kMaximumVisibleAlarmRows = 5; | |||
| constexpr std::size_t kMaximumPollAddresses = 1024U; | |||
| constexpr std::size_t kMaximumPollBlocks = 64U; | |||
| @@ -1,5 +1,6 @@ | |||
| #include "alarm_configuration_dialog.h" | |||
| #include "domain/project_limits.h" | |||
| #include "services/alarm_editor_service.h" | |||
| #include "ui_alarm_configuration_dialog.h" | |||
| @@ -53,6 +54,8 @@ AlarmConfigurationDialog::AlarmConfigurationDialog( | |||
| service_(service) | |||
| { | |||
| ui_->setupUi(this); | |||
| ui_->messageEdit->setMaxLength( | |||
| static_cast<int>(ProjectLimits::kMaximumAlarmMessageCharacters)); | |||
| ui_->alarmTable->horizontalHeader()->setSectionResizeMode( | |||
| 0, QHeaderView::ResizeToContents); | |||
| ui_->alarmTable->horizontalHeader()->setSectionResizeMode( | |||
| @@ -5,6 +5,7 @@ | |||
| #include "services/alarm_service.h" | |||
| #include "domain/project_limits.h" | |||
| #include <QApplication> | |||
| #include <QDateTime> | |||
| #include <QFont> | |||
| #include <QGraphicsItem> | |||
| @@ -15,6 +16,8 @@ | |||
| #include <QInputDialog> | |||
| #include <QPainter> | |||
| #include <QResizeEvent> | |||
| #include <QStyle> | |||
| #include <QStyleOption> | |||
| #include <QStyleOptionGraphicsItem> | |||
| #include <algorithm> | |||
| @@ -27,6 +30,13 @@ namespace { | |||
| constexpr qreal kAlarmHeaderHeight = 24.0; | |||
| constexpr qreal kAlarmRowHeight = 22.0; | |||
| constexpr qreal kAlarmCellLeftPadding = 5.0; | |||
| constexpr qreal kAlarmCellRightPadding = 5.0; | |||
| constexpr qreal kAlarmColumnSpacing = 3.0; | |||
| constexpr qreal kAlarmTimeColumnWidth = 76.0; | |||
| constexpr qreal kAlarmStateColumnWidth = 52.0; | |||
| constexpr qreal kAlarmPageButtonSize = 16.0; | |||
| constexpr qreal kAlarmPageIndicatorWidth = 34.0; | |||
| QString alarmTimeText(const std::chrono::system_clock::time_point &time) | |||
| { | |||
| @@ -73,9 +83,9 @@ public: | |||
| { | |||
| if (!control_.binding.has_value()) | |||
| { | |||
| return controlRect(); | |||
| return visibleControlRect(); | |||
| } | |||
| return controlRect().united(addressRect()); | |||
| return visibleControlRect().united(addressRect()); | |||
| } | |||
| void paint( | |||
| @@ -84,7 +94,7 @@ public: | |||
| QWidget *) override | |||
| { | |||
| // 留出一个像素边距,避免描边被图元边界裁剪 | |||
| const QRectF rect = controlRect().adjusted(1, 1, -1, -1); | |||
| const QRectF rect = visibleControlRect().adjusted(1, 1, -1, -1); | |||
| painter->setRenderHint(QPainter::Antialiasing, true); | |||
| painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1)); | |||
| @@ -247,18 +257,51 @@ public: | |||
| rect.left(), rect.top(), rect.width(), kAlarmHeaderHeight); | |||
| painter->fillRect(header, QColor(QStringLiteral("#a63f3f"))); | |||
| painter->setPen(Qt::white); | |||
| QRectF title_rect = header.adjusted(7, 0, -7, 0); | |||
| const std::size_t page_count = alarmPageCount(); | |||
| if (runtime_active_ && page_count > 1U) | |||
| { | |||
| const QRectF previous_rect = alarmPreviousPageRect(header); | |||
| const QRectF next_rect = alarmNextPageRect(header); | |||
| const QRectF indicator_rect = alarmPageIndicatorRect(header); | |||
| title_rect.setRight(previous_rect.left() - kAlarmColumnSpacing); | |||
| QStyleOption previous_option; | |||
| previous_option.rect = previous_rect.toAlignedRect(); | |||
| previous_option.state = alarm_page_ > 0U | |||
| ? QStyle::State_Enabled : QStyle::State_None; | |||
| QApplication::style()->drawPrimitive( | |||
| QStyle::PE_IndicatorArrowLeft, | |||
| &previous_option, | |||
| painter); | |||
| QStyleOption next_option; | |||
| next_option.rect = next_rect.toAlignedRect(); | |||
| next_option.state = alarm_page_ + 1U < page_count | |||
| ? QStyle::State_Enabled : QStyle::State_None; | |||
| QApplication::style()->drawPrimitive( | |||
| QStyle::PE_IndicatorArrowRight, | |||
| &next_option, | |||
| painter); | |||
| painter->setPen(Qt::white); | |||
| painter->drawText( | |||
| indicator_rect, | |||
| Qt::AlignCenter, | |||
| QStringLiteral("%1/%2") | |||
| .arg(static_cast<qulonglong>(alarm_page_ + 1U)) | |||
| .arg(static_cast<qulonglong>(page_count))); | |||
| } | |||
| const QString title = QString::fromUtf8( | |||
| control_.text.data(), static_cast<int>(control_.text.size())); | |||
| painter->drawText( | |||
| header.adjusted(7, 0, -7, 0), | |||
| title_rect, | |||
| Qt::AlignVCenter | Qt::AlignLeft, | |||
| QString::fromUtf8( | |||
| control_.text.data(), static_cast<int>(control_.text.size()))); | |||
| painter->fontMetrics().elidedText( | |||
| title, | |||
| Qt::ElideRight, | |||
| std::max(0, static_cast<int>(title_rect.width())))); | |||
| const int maximum_rows = std::max( | |||
| 0, | |||
| static_cast<int>((rect.height() - kAlarmHeaderHeight) | |||
| / kAlarmRowHeight)); | |||
| const int visible_rows = std::min( | |||
| maximum_rows, static_cast<int>(alarm_records_.size())); | |||
| const int visible_rows = static_cast<int>(visibleAlarmRecordCount()); | |||
| if (visible_rows == 0) | |||
| { | |||
| painter->setPen(configuredTextColor(QColor(QStringLiteral("#6f7a82")))); | |||
| @@ -273,9 +316,11 @@ public: | |||
| : QObject::tr("运行时显示当前报警")); | |||
| break; | |||
| } | |||
| const std::size_t first_record = alarmFirstRecordIndex(); | |||
| for (int row = 0; row < visible_rows; ++row) | |||
| { | |||
| const AlarmRecord &record = alarm_records_[static_cast<std::size_t>(row)]; | |||
| const AlarmRecord &record = alarm_records_[ | |||
| first_record + static_cast<std::size_t>(row)]; | |||
| const QRectF row_rect( | |||
| rect.left(), | |||
| header.bottom() + row * kAlarmRowHeight, | |||
| @@ -289,23 +334,43 @@ public: | |||
| painter->setPen(configuredTextColor(QColor(QStringLiteral("#8d1f1f")))); | |||
| const QString state = record.acknowledged | |||
| ? QObject::tr("已确认") : QObject::tr("未确认"); | |||
| const QRectF time_rect( | |||
| row_rect.left() + kAlarmCellLeftPadding, | |||
| row_rect.top(), | |||
| kAlarmTimeColumnWidth, | |||
| row_rect.height()); | |||
| const QRectF state_rect( | |||
| time_rect.right() + kAlarmColumnSpacing, | |||
| row_rect.top(), | |||
| kAlarmStateColumnWidth, | |||
| row_rect.height()); | |||
| const qreal message_left = | |||
| state_rect.right() + kAlarmColumnSpacing; | |||
| const QRectF message_rect( | |||
| message_left, | |||
| row_rect.top(), | |||
| std::max( | |||
| 0.0, | |||
| row_rect.right() - kAlarmCellRightPadding | |||
| - message_left), | |||
| row_rect.height()); | |||
| painter->drawText( | |||
| QRectF(row_rect.left() + 5, row_rect.top(), 58, row_rect.height()), | |||
| time_rect, | |||
| Qt::AlignVCenter | Qt::AlignLeft, | |||
| alarmTimeText(record.occurredAt)); | |||
| painter->drawText( | |||
| QRectF(row_rect.left() + 66, row_rect.top(), 52, row_rect.height()), | |||
| state_rect, | |||
| Qt::AlignVCenter | Qt::AlignLeft, | |||
| state); | |||
| painter->drawText( | |||
| QRectF( | |||
| row_rect.left() + 120, | |||
| row_rect.top(), | |||
| std::max(0.0, row_rect.width() - 125), | |||
| row_rect.height()), | |||
| message_rect, | |||
| Qt::AlignVCenter | Qt::AlignLeft, | |||
| QString::fromUtf8( | |||
| record.message.data(), static_cast<int>(record.message.size()))); | |||
| painter->fontMetrics().elidedText( | |||
| QString::fromUtf8( | |||
| record.message.data(), | |||
| static_cast<int>(record.message.size())), | |||
| Qt::ElideRight, | |||
| std::max(0, static_cast<int>(message_rect.width())))); | |||
| } | |||
| break; | |||
| } | |||
| @@ -340,6 +405,14 @@ public: | |||
| void setInteractionState( | |||
| bool editable, bool runtime_active, bool runtime_write_enabled) | |||
| { | |||
| if (control_.type == HmiControlType::AlarmList) | |||
| { | |||
| prepareGeometryChange(); | |||
| if (!runtime_active) | |||
| { | |||
| alarm_page_ = 0U; | |||
| } | |||
| } | |||
| editing_enabled_ = editable; | |||
| runtime_active_ = runtime_active; | |||
| runtime_write_enabled_ = runtime_active && runtime_write_enabled; | |||
| @@ -396,7 +469,14 @@ public: | |||
| void setAlarmRecords(const std::vector<AlarmRecord> &records) | |||
| { | |||
| if (control_.type == HmiControlType::AlarmList) | |||
| { | |||
| prepareGeometryChange(); | |||
| } | |||
| alarm_records_ = records; | |||
| const std::size_t page_count = alarmPageCount(); | |||
| alarm_page_ = page_count == 0U | |||
| ? 0U : std::min(alarm_page_, page_count - 1U); | |||
| updateAlarmVisibility(); | |||
| update(); | |||
| } | |||
| @@ -428,14 +508,40 @@ protected: | |||
| { | |||
| if (event->pos().y() < kAlarmHeaderHeight) | |||
| { | |||
| const QRectF content_rect = | |||
| visibleControlRect().adjusted(1, 1, -1, -1); | |||
| const QRectF header( | |||
| content_rect.left(), | |||
| content_rect.top(), | |||
| content_rect.width(), | |||
| kAlarmHeaderHeight); | |||
| const std::size_t page_count = alarmPageCount(); | |||
| if (alarmPreviousPageRect(header).contains(event->pos()) | |||
| && alarm_page_ > 0U) | |||
| { | |||
| prepareGeometryChange(); | |||
| --alarm_page_; | |||
| update(); | |||
| } | |||
| else if (alarmNextPageRect(header).contains(event->pos()) | |||
| && alarm_page_ + 1U < page_count) | |||
| { | |||
| prepareGeometryChange(); | |||
| ++alarm_page_; | |||
| update(); | |||
| } | |||
| event->accept(); | |||
| return; | |||
| } | |||
| const int row = static_cast<int>( | |||
| (event->pos().y() - kAlarmHeaderHeight) / kAlarmRowHeight); | |||
| if (row >= 0 && row < static_cast<int>(alarm_records_.size())) | |||
| const std::size_t record_index = alarmFirstRecordIndex() | |||
| + static_cast<std::size_t>(std::max(0, row)); | |||
| if (row >= 0 | |||
| && row < static_cast<int>(visibleAlarmRecordCount()) | |||
| && record_index < alarm_records_.size()) | |||
| { | |||
| const AlarmRecord &record = alarm_records_[static_cast<std::size_t>(row)]; | |||
| const AlarmRecord &record = alarm_records_[record_index]; | |||
| if (!record.acknowledged && alarm_acknowledge_) | |||
| { | |||
| alarm_acknowledge_(record.definitionId); | |||
| @@ -543,6 +649,86 @@ protected: | |||
| } | |||
| private: | |||
| std::size_t alarmPageSize() const | |||
| { | |||
| const QRectF rect = controlRect().adjusted(1, 1, -1, -1); | |||
| const int rows_by_height = std::max( | |||
| 0, | |||
| static_cast<int>((rect.height() - kAlarmHeaderHeight) | |||
| / kAlarmRowHeight)); | |||
| return static_cast<std::size_t>(std::min( | |||
| rows_by_height, ProjectLimits::kMaximumVisibleAlarmRows)); | |||
| } | |||
| std::size_t alarmPageCount() const | |||
| { | |||
| const std::size_t page_size = alarmPageSize(); | |||
| return page_size == 0U || alarm_records_.empty() | |||
| ? 0U | |||
| : (alarm_records_.size() + page_size - 1U) / page_size; | |||
| } | |||
| std::size_t alarmFirstRecordIndex() const | |||
| { | |||
| return alarm_page_ * alarmPageSize(); | |||
| } | |||
| std::size_t visibleAlarmRecordCount() const | |||
| { | |||
| const std::size_t first_record = alarmFirstRecordIndex(); | |||
| if (first_record >= alarm_records_.size()) | |||
| { | |||
| return 0U; | |||
| } | |||
| return std::min( | |||
| alarmPageSize(), alarm_records_.size() - first_record); | |||
| } | |||
| QRectF visibleControlRect() const | |||
| { | |||
| const QRectF full_rect = controlRect(); | |||
| if (control_.type != HmiControlType::AlarmList || !runtime_active_) | |||
| { | |||
| return full_rect; | |||
| } | |||
| const qreal content_height = kAlarmHeaderHeight | |||
| + static_cast<qreal>(visibleAlarmRecordCount()) * kAlarmRowHeight | |||
| + 2.0; | |||
| return {full_rect.left(), | |||
| full_rect.top(), | |||
| full_rect.width(), | |||
| std::min(full_rect.height(), content_height)}; | |||
| } | |||
| QRectF alarmNextPageRect(const QRectF &header) const | |||
| { | |||
| return { | |||
| header.right() - kAlarmCellRightPadding - kAlarmPageButtonSize, | |||
| header.center().y() - kAlarmPageButtonSize / 2.0, | |||
| kAlarmPageButtonSize, | |||
| kAlarmPageButtonSize}; | |||
| } | |||
| QRectF alarmPageIndicatorRect(const QRectF &header) const | |||
| { | |||
| const QRectF next_rect = alarmNextPageRect(header); | |||
| return { | |||
| next_rect.left() - kAlarmPageIndicatorWidth, | |||
| header.top(), | |||
| kAlarmPageIndicatorWidth, | |||
| header.height()}; | |||
| } | |||
| QRectF alarmPreviousPageRect(const QRectF &header) const | |||
| { | |||
| const QRectF indicator_rect = alarmPageIndicatorRect(header); | |||
| return { | |||
| indicator_rect.left() - kAlarmPageButtonSize, | |||
| header.center().y() - kAlarmPageButtonSize / 2.0, | |||
| kAlarmPageButtonSize, | |||
| kAlarmPageButtonSize}; | |||
| } | |||
| void updateAlarmVisibility() | |||
| { | |||
| if (control_.type != HmiControlType::AlarmList) | |||
| @@ -616,6 +802,14 @@ private: | |||
| font.setPointSize(point_size); | |||
| } | |||
| } | |||
| else if (control_.type == HmiControlType::AlarmList | |||
| && font.pointSize() > 0) | |||
| { | |||
| font.setPointSize(std::max( | |||
| ProjectLimits::kMinimumHmiFontPointSize, | |||
| font.pointSize() | |||
| - ProjectLimits::kAlarmDefaultFontPointReduction)); | |||
| } | |||
| const auto font_bold = control_.properties.find( | |||
| HmiAppearanceProperty::kFontBold); | |||
| if (font_bold != control_.properties.cend()) | |||
| @@ -661,6 +855,7 @@ private: | |||
| std::function<void(const std::string &)> page_navigation_; | |||
| std::function<void(const std::string &)> alarm_acknowledge_; | |||
| std::vector<AlarmRecord> alarm_records_; | |||
| std::size_t alarm_page_ = 0U; | |||
| bool editing_enabled_ = true; | |||
| bool runtime_active_ = false; | |||
| bool runtime_write_enabled_ = false; | |||
| @@ -1,134 +0,0 @@ | |||
| #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); | |||
| } | |||
| @@ -1,63 +0,0 @@ | |||
| /** | |||
| * @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; | |||
| }; | |||
| @@ -1,49 +0,0 @@ | |||
| <?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> | |||
| @@ -475,12 +475,17 @@ public: | |||
| configured_ ? counterAddressText(counter->address) | |||
| : tr("< C 地址 >")); | |||
| painter->drawText( | |||
| QRectF(-kCellWidth / 2.0, 22, kCellWidth, 18), | |||
| Qt::AlignCenter, | |||
| configured_ ? QStringLiteral("CV %1 / PV %2") | |||
| QRectF(-kCellWidth / 2.0 + 4.0, 20, kCellWidth - 8.0, 18), | |||
| Qt::AlignCenter | Qt::TextSingleLine, | |||
| configured_ ? QStringLiteral("CV %1") | |||
| .arg(registerAddressText(counter->currentValueAddress)) | |||
| : tr("< CV >")); | |||
| painter->drawText( | |||
| QRectF(-kCellWidth / 2.0 + 4.0, 38, kCellWidth - 8.0, 18), | |||
| Qt::AlignCenter | Qt::TextSingleLine, | |||
| configured_ ? QStringLiteral("PV %1") | |||
| .arg(wordOperandText(counter->preset)) | |||
| : tr("< CV / PV >")); | |||
| : tr("< PV >")); | |||
| } | |||
| else if (const auto *move = std::get_if<MoveNodeConfig>(&config_)) | |||
| { | |||
| @@ -356,7 +356,7 @@ void MainWindow::configureActions() | |||
| [this] { requestMode(ApplicationMode::OfflineRunning); }); | |||
| connect(ui_->onlineModeAction, &QAction::triggered, this, | |||
| [this] { requestMode(ApplicationMode::OnlineRunning); }); | |||
| connect(ui_->exitAction, &QAction::triggered, qApp, &QApplication::closeAllWindows); | |||
| connect(ui_->exitAction, &QAction::triggered, this, [this] { close(); }); | |||
| connect(ui_->newProjectAction, &QAction::triggered, this, &MainWindow::createNewProject); | |||
| connect(ui_->saveProjectAction, &QAction::triggered, this, &MainWindow::saveProject); | |||
| connect(ui_->saveAsProjectAction, &QAction::triggered, this, &MainWindow::saveProjectAs); | |||
| @@ -691,7 +691,6 @@ void MainWindow::configureActions() | |||
| } | |||
| if (index == 0 || index == 1) | |||
| { | |||
| last_editing_tab_index_ = index; | |||
| refreshProjectUi(); | |||
| } | |||
| }); | |||
| @@ -765,7 +764,6 @@ void MainWindow::configureAppearance() | |||
| ui_->deleteLogicAction->setIcon(makeUiIcon(UiIcon::Delete)); | |||
| ui_->editorTabWidget->setTabIcon(0, makeUiIcon(UiIcon::HmiPage)); | |||
| ui_->editorTabWidget->setTabIcon(1, makeUiIcon(UiIcon::Logic)); | |||
| ui_->editorTabWidget->setTabIcon(2, makeUiIcon(UiIcon::Runtime)); | |||
| ui_->outputDock->setMaximumHeight(220); | |||
| mode_status_label_ = new QLabel(this); | |||
| @@ -833,7 +831,6 @@ void MainWindow::configureRuntimeMonitor() | |||
| { | |||
| runtime_panel_controller_ = std::make_unique<RuntimePanelController>( | |||
| *this, | |||
| *ui_, | |||
| runtime_mode_service_, | |||
| project_service_, | |||
| hmi_editor_service_, | |||
| @@ -1550,7 +1547,6 @@ void MainWindow::updateModeUi(const QString &message) | |||
| const ModePolicy policy = runtime_mode_service_.policy(); | |||
| restoreCurrentModeAction(); | |||
| const bool running = mode != ApplicationMode::Editing; | |||
| ui_->editorTabWidget->setTabVisible(2, running); | |||
| if (running) | |||
| { | |||
| if (hmi_navigation_service_->currentPageId().empty()) | |||
| @@ -1566,17 +1562,12 @@ void MainWindow::updateModeUi(const QString &message) | |||
| current_logic_id_, | |||
| mode, | |||
| runtime_mode_service_.plcConnectionState()); | |||
| ui_->editorTabWidget->setCurrentWidget(ui_->runtimeMonitorTab); | |||
| } | |||
| else | |||
| { | |||
| hmi_navigation_service_->stop(); | |||
| runtime_panel_controller_->leaveRuntime( | |||
| mode, runtime_mode_service_.plcConnectionState()); | |||
| if (ui_->editorTabWidget->currentWidget() == ui_->runtimeMonitorTab) | |||
| { | |||
| ui_->editorTabWidget->setCurrentIndex(last_editing_tab_index_); | |||
| } | |||
| } | |||
| // 将同一份模式策略同步到所有可编辑入口,避免只禁用部分操作 | |||
| ui_->projectDock->setEnabled(policy.allowsProjectEditing); | |||
| @@ -220,5 +220,4 @@ private: | |||
| std::string current_hmi_page_id_; | |||
| std::string current_logic_id_; | |||
| bool plc_status_update_pending_ = false; | |||
| int last_editing_tab_index_ = 0; | |||
| }; | |||
| @@ -207,18 +207,7 @@ | |||
| </item> | |||
| </layout> | |||
| </widget> | |||
| <widget class="QWidget" name="runtimeMonitorTab"> | |||
| <attribute name="title"> | |||
| <string>运行监控</string> | |||
| </attribute> | |||
| <layout class="QVBoxLayout" name="runtimeMonitorLayout"> | |||
| <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> | |||
| </widget> | |||
| </item> | |||
| </layout> | |||
| </widget> | |||
| @@ -310,7 +299,7 @@ | |||
| <enum>TopToolBarArea</enum> | |||
| </attribute> | |||
| <attribute name="toolBarBreak"> | |||
| <bool>false</bool> | |||
| <bool>true</bool> | |||
| </attribute> | |||
| <addaction name="addButtonAction"/> | |||
| <addaction name="addIndicatorAction"/> | |||
| @@ -330,7 +319,7 @@ | |||
| <enum>TopToolBarArea</enum> | |||
| </attribute> | |||
| <attribute name="toolBarBreak"> | |||
| <bool>false</bool> | |||
| <bool>true</bool> | |||
| </attribute> | |||
| <addaction name="addRungAction"/> | |||
| <addaction name="insertHorizontalWireAction"/> | |||
| @@ -15,6 +15,7 @@ | |||
| #include <QColorDialog> | |||
| #include <QComboBox> | |||
| #include <QLabel> | |||
| #include <QLineEdit> | |||
| #include <QPushButton> | |||
| #include <QDialog> | |||
| #include <QObject> | |||
| @@ -40,6 +41,19 @@ std::string toUtf8(const QString &value) | |||
| return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size())); | |||
| } | |||
| int defaultFontPointSize(HmiControlType type) | |||
| { | |||
| int point_size = QApplication::font().pointSize(); | |||
| if (type == HmiControlType::AlarmList) | |||
| { | |||
| point_size -= ProjectLimits::kAlarmDefaultFontPointReduction; | |||
| } | |||
| return std::clamp( | |||
| point_size, | |||
| ProjectLimits::kMinimumHmiFontPointSize, | |||
| ProjectLimits::kMaximumHmiFontPointSize); | |||
| } | |||
| } // namespace | |||
| PropertyPanelController::PropertyPanelController( | |||
| @@ -181,6 +195,7 @@ void PropertyPanelController::showControlProperties(const std::string &control_i | |||
| if (!has_control) | |||
| { | |||
| ui_.controlTextEdit->setMaxLength(32767); | |||
| ui_.bindingAreaLabel->setVisible(false); | |||
| ui_.bindingAreaComboBox->setVisible(false); | |||
| ui_.bindingIndexLabel->setVisible(false); | |||
| @@ -207,6 +222,10 @@ void PropertyPanelController::showControlProperties(const std::string &control_i | |||
| return; | |||
| } | |||
| ui_.controlTextEdit->setMaxLength( | |||
| control->type == HmiControlType::AlarmList | |||
| ? static_cast<int>(ProjectLimits::kMaximumAlarmTitleCharacters) | |||
| : 32767); | |||
| ui_.controlIdEdit->setText(fromUtf8(control->id)); | |||
| ui_.controlTextEdit->setText(fromUtf8(control->text)); | |||
| ui_.controlXSpinBox->setValue(control->bounds.x); | |||
| @@ -278,10 +297,7 @@ void PropertyPanelController::showControlProperties(const std::string &control_i | |||
| swatch_color.name(QColor::HexRgb)) | |||
| : QString{}); | |||
| const int default_font_size = std::clamp( | |||
| QApplication::font().pointSize(), | |||
| ProjectLimits::kMinimumHmiFontPointSize, | |||
| ProjectLimits::kMaximumHmiFontPointSize); | |||
| const int default_font_size = defaultFontPointSize(control->type); | |||
| int font_size = default_font_size; | |||
| const auto font_size_property = control->properties.find( | |||
| HmiAppearanceProperty::kFontSize); | |||
| @@ -6,11 +6,13 @@ | |||
| #include "services/hmi_navigation_service.h" | |||
| #include "services/alarm_service.h" | |||
| #include "services/project_service.h" | |||
| #include "toolbar_icon_factory.h" | |||
| #include "ui_runtime_monitor_widget.h" | |||
| #include <QSplitter> | |||
| #include <QComboBox> | |||
| #include <QSignalBlocker> | |||
| #include <QToolButton> | |||
| #include <algorithm> | |||
| @@ -44,6 +46,7 @@ RuntimeMonitorWidget::RuntimeMonitorWidget( | |||
| hmi_navigation_service_(hmi_navigation_service) | |||
| { | |||
| ui_->setupUi(this); | |||
| ui_->exitRuntimeButton->setIcon(makeUiIcon(UiIcon::Exit)); | |||
| hmi_view_ = new HmiEditorWidget( | |||
| hmi_editor_service, | |||
| hmi_runtime_service, | |||
| @@ -77,8 +80,9 @@ RuntimeMonitorWidget::RuntimeMonitorWidget( | |||
| return; | |||
| } | |||
| showRuntimePage(result.pageId); | |||
| emit runtimePageChanged(fromUtf8(result.pageId)); | |||
| }); | |||
| connect(ui_->exitRuntimeButton, &QToolButton::clicked, | |||
| this, &RuntimeMonitorWidget::exitRequested); | |||
| connect(ui_->runtimeLogicComboBox, | |||
| QOverload<int>::of(&QComboBox::currentIndexChanged), | |||
| this, | |||
| @@ -132,8 +136,10 @@ void RuntimeMonitorWidget::setMode( | |||
| const bool online = mode == ApplicationMode::OnlineRunning; | |||
| ui_->logicFrame->setVisible(offline); | |||
| hmi_view_->setRuntimeActive(offline || online); | |||
| // 工程师诊断视图只投影 HMI,实际运行输入统一由独立 HMI 窗口处理 | |||
| setHmiWriteEnabled(false); | |||
| if (!offline && !online) | |||
| { | |||
| setHmiWriteEnabled(false); | |||
| } | |||
| setFreeMonitorWriteEnabled(offline | |||
| || (online && plc_state == PlcConnectionState::Connected)); | |||
| if (!offline) | |||
| @@ -52,7 +52,7 @@ public: | |||
| signals: | |||
| void navigationFailed(const QString &message); | |||
| void runtimePageChanged(const QString &page_id); | |||
| void exitRequested(); | |||
| private: | |||
| void showRuntimePage(const std::string &page_id); | |||
| @@ -13,6 +13,7 @@ | |||
| <item><widget class="QLabel" name="titleLabel"><property name="text"><string>运行监控</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="modeLabel"><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> | |||
| </item> | |||
| <item> | |||
| @@ -0,0 +1,54 @@ | |||
| #include "runtime_monitor_window.h" | |||
| #include "ui_runtime_monitor_window.h" | |||
| #include <QCloseEvent> | |||
| #include <QWidget> | |||
| RuntimeMonitorWindow::RuntimeMonitorWindow(QWidget *parent) | |||
| : QMainWindow(parent, Qt::Window), | |||
| ui_(std::make_unique<Ui::RuntimeMonitorWindow>()) | |||
| { | |||
| ui_->setupUi(this); | |||
| } | |||
| RuntimeMonitorWindow::~RuntimeMonitorWindow() = default; | |||
| void RuntimeMonitorWindow::setMonitorWidget(QWidget &widget) | |||
| { | |||
| widget.setParent(ui_->centralWidget); | |||
| ui_->runtimeMonitorWindowLayout->addWidget(&widget); | |||
| } | |||
| void RuntimeMonitorWindow::showForRuntime() | |||
| { | |||
| application_exit_ = false; | |||
| runtime_active_ = true; | |||
| showMaximized(); | |||
| raise(); | |||
| activateWindow(); | |||
| } | |||
| void RuntimeMonitorWindow::hideForEditing() | |||
| { | |||
| runtime_active_ = false; | |||
| hide(); | |||
| } | |||
| void RuntimeMonitorWindow::closeForApplicationExit() | |||
| { | |||
| application_exit_ = true; | |||
| runtime_active_ = false; | |||
| close(); | |||
| } | |||
| void RuntimeMonitorWindow::closeEvent(QCloseEvent *event) | |||
| { | |||
| if (runtime_active_ && !application_exit_) | |||
| { | |||
| emit exitRequested(); | |||
| event->ignore(); | |||
| return; | |||
| } | |||
| QMainWindow::closeEvent(event); | |||
| } | |||
| @@ -0,0 +1,38 @@ | |||
| #pragma once | |||
| #include <QMainWindow> | |||
| #include <memory> | |||
| namespace Ui { | |||
| class RuntimeMonitorWindow; | |||
| } | |||
| class QCloseEvent; | |||
| class QWidget; | |||
| // 承载唯一工程师运行监控投影的独立顶层窗口 | |||
| class RuntimeMonitorWindow final : public QMainWindow | |||
| { | |||
| Q_OBJECT | |||
| public: | |||
| explicit RuntimeMonitorWindow(QWidget *parent = nullptr); | |||
| ~RuntimeMonitorWindow() override; | |||
| void setMonitorWidget(QWidget &widget); | |||
| void showForRuntime(); | |||
| void hideForEditing(); | |||
| void closeForApplicationExit(); | |||
| signals: | |||
| void exitRequested(); | |||
| protected: | |||
| void closeEvent(QCloseEvent *event) override; | |||
| private: | |||
| std::unique_ptr<Ui::RuntimeMonitorWindow> ui_; | |||
| bool runtime_active_ = false; | |||
| bool application_exit_ = false; | |||
| }; | |||
| @@ -0,0 +1,44 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <ui version="4.0"> | |||
| <class>RuntimeMonitorWindow</class> | |||
| <widget class="QMainWindow" name="RuntimeMonitorWindow"> | |||
| <property name="geometry"> | |||
| <rect> | |||
| <x>0</x> | |||
| <y>0</y> | |||
| <width>1280</width> | |||
| <height>800</height> | |||
| </rect> | |||
| </property> | |||
| <property name="minimumSize"> | |||
| <size> | |||
| <width>960</width> | |||
| <height>640</height> | |||
| </size> | |||
| </property> | |||
| <property name="windowTitle"> | |||
| <string>运行监控 - 综合平台编程器</string> | |||
| </property> | |||
| <widget class="QWidget" name="centralWidget"> | |||
| <layout class="QVBoxLayout" name="runtimeMonitorWindowLayout"> | |||
| <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> | |||
| </layout> | |||
| </widget> | |||
| </widget> | |||
| <resources/> | |||
| <connections/> | |||
| </ui> | |||
| @@ -2,9 +2,9 @@ | |||
| #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 "runtime_monitor_window.h" | |||
| #include "services/alarm_service.h" | |||
| #include "services/hmi_editor_service.h" | |||
| #include "services/hmi_navigation_service.h" | |||
| @@ -14,7 +14,6 @@ | |||
| #include "services/project_service.h" | |||
| #include "services/register_monitor_service.h" | |||
| #include "services/runtime_mode_service.h" | |||
| #include "ui_main_window.h" | |||
| #include <QListWidget> | |||
| #include <QLabel> | |||
| @@ -35,7 +34,6 @@ QString fromUtf8(const std::string &value) | |||
| RuntimePanelController::RuntimePanelController( | |||
| QWidget &parent, | |||
| Ui::MainWindow &ui, | |||
| RuntimeModeService &runtime_mode_service, | |||
| ProjectService &project_service, | |||
| HmiEditorService &hmi_editor_service, | |||
| @@ -54,7 +52,6 @@ RuntimePanelController::RuntimePanelController( | |||
| OutputReporter output_reporter, | |||
| RuntimeExitRequester runtime_exit_requester) | |||
| : parent_(parent), | |||
| ui_(ui), | |||
| runtime_mode_service_(runtime_mode_service), | |||
| project_service_(project_service), | |||
| hmi_editor_service_(hmi_editor_service), | |||
| @@ -79,6 +76,9 @@ RuntimePanelController::~RuntimePanelController() = default; | |||
| void RuntimePanelController::configure() | |||
| { | |||
| runtime_monitor_window_ = std::make_unique<RuntimeMonitorWindow>(&parent_); | |||
| runtime_monitor_window_->setObjectName( | |||
| QStringLiteral("runtimeMonitorWindow")); | |||
| runtime_monitor_widget_ = new RuntimeMonitorWidget( | |||
| hmi_editor_service_, | |||
| hmi_runtime_service_, | |||
| @@ -87,31 +87,19 @@ void RuntimePanelController::configure() | |||
| hmi_navigation_service_, | |||
| alarm_service_, | |||
| register_monitor_service_, | |||
| ui_.runtimeMonitorTab); | |||
| runtime_monitor_window_.get()); | |||
| 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) | |||
| runtime_monitor_window_->setMonitorWidget(*runtime_monitor_widget_); | |||
| QObject::connect(runtime_monitor_widget_, &RuntimeMonitorWidget::exitRequested, | |||
| &parent_, [this] | |||
| { | |||
| const HmiNavigationResult result = hmi_navigation_service_.navigateTo( | |||
| target_page_id.toUtf8().toStdString()); | |||
| if (!result.succeeded) | |||
| if (runtime_exit_requester_) | |||
| { | |||
| const QString message = fromUtf8(result.message); | |||
| status_reporter_(message, 5000); | |||
| output_reporter_(QObject::tr("页面导航失败:%1").arg(message)); | |||
| return; | |||
| runtime_exit_requester_(); | |||
| } | |||
| runtime_monitor_widget_->setRuntimePage(result.pageId); | |||
| hmi_runtime_window_->setRuntimePage(result.pageId); | |||
| }); | |||
| QObject::connect(hmi_runtime_window_.get(), &HmiRuntimeWindow::exitRequested, | |||
| QObject::connect(runtime_monitor_window_.get(), | |||
| &RuntimeMonitorWindow::exitRequested, | |||
| &parent_, [this] | |||
| { | |||
| if (runtime_exit_requester_) | |||
| @@ -141,17 +129,6 @@ 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_); | |||
| runtime_refresh_timer_->setInterval(150); | |||
| QObject::connect(runtime_refresh_timer_, &QTimer::timeout, | |||
| @@ -172,11 +149,6 @@ 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, | |||
| @@ -186,13 +158,10 @@ void RuntimePanelController::enterRuntime( | |||
| 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(); | |||
| runtime_monitor_window_->showForRuntime(); | |||
| } | |||
| void RuntimePanelController::leaveRuntime( | |||
| @@ -200,15 +169,14 @@ void RuntimePanelController::leaveRuntime( | |||
| { | |||
| runtime_session_active_ = false; | |||
| runtime_monitor_widget_->setMode(mode, plc_state); | |||
| hmi_runtime_window_->setMode(mode, plc_state); | |||
| hmi_runtime_window_->hideForEditing(); | |||
| runtime_monitor_window_->hideForEditing(); | |||
| } | |||
| void RuntimePanelController::closeForApplicationExit() | |||
| { | |||
| if (hmi_runtime_window_ != nullptr) | |||
| if (runtime_monitor_window_ != nullptr) | |||
| { | |||
| hmi_runtime_window_->closeForApplicationExit(); | |||
| runtime_monitor_window_->closeForApplicationExit(); | |||
| } | |||
| } | |||
| @@ -223,22 +191,16 @@ 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 窗口发出,主窗口的画布全部只读 | |||
| // 编辑画布始终只读,运行操作统一由运行监控中的 HMI 发出 | |||
| hmi_editor_widget_.setRuntimeWriteEnabled(false); | |||
| if (runtime_monitor_widget_ != nullptr) | |||
| { | |||
| runtime_monitor_widget_->setHmiWriteEnabled(false); | |||
| runtime_monitor_widget_->setHmiWriteEnabled(write_enabled); | |||
| 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) | |||
| { | |||
| case SimulationState::Running: | |||
| @@ -268,11 +230,6 @@ 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), | |||
| @@ -328,10 +285,6 @@ void RuntimePanelController::handleRuntimeTimer() | |||
| runtime_mode_service_.mode(), | |||
| runtime_mode_service_.plcConnectionState()); | |||
| } | |||
| if (hmi_runtime_window_ != nullptr) | |||
| { | |||
| hmi_runtime_window_->refreshValues(); | |||
| } | |||
| updateSimulationUi(false); | |||
| } | |||
| @@ -12,7 +12,6 @@ | |||
| class AlarmService; | |||
| class HmiEditorService; | |||
| class HmiEditorWidget; | |||
| class HmiRuntimeWindow; | |||
| class HmiNavigationService; | |||
| class HmiRuntimeService; | |||
| class LogicEditorService; | |||
| @@ -22,13 +21,10 @@ class ProjectService; | |||
| class RegisterMonitorService; | |||
| class RuntimeModeService; | |||
| class RuntimeMonitorWidget; | |||
| class RuntimeMonitorWindow; | |||
| class QTimer; | |||
| class QWidget; | |||
| namespace Ui { | |||
| class MainWindow; | |||
| } | |||
| // 组织运行监控工作台、运行刷新定时器和离线执行器反馈 | |||
| class RuntimePanelController final | |||
| { | |||
| @@ -40,7 +36,6 @@ public: | |||
| RuntimePanelController( | |||
| QWidget &parent, | |||
| Ui::MainWindow &ui, | |||
| RuntimeModeService &runtime_mode_service, | |||
| ProjectService &project_service, | |||
| HmiEditorService &hmi_editor_service, | |||
| @@ -70,7 +65,6 @@ public: | |||
| void closeForApplicationExit(); | |||
| void updateSimulationUi(bool report_fault); | |||
| RuntimeMonitorWidget *runtimeMonitorWidget() const; | |||
| HmiRuntimeWindow *hmiRuntimeWindow() const; | |||
| private: | |||
| void handleRuntimeTimer(); | |||
| @@ -78,7 +72,6 @@ private: | |||
| void handleScanCompleted(); | |||
| QWidget &parent_; | |||
| Ui::MainWindow &ui_; | |||
| RuntimeModeService &runtime_mode_service_; | |||
| ProjectService &project_service_; | |||
| HmiEditorService &hmi_editor_service_; | |||
| @@ -96,8 +89,8 @@ private: | |||
| StatusReporter status_reporter_; | |||
| OutputReporter output_reporter_; | |||
| RuntimeExitRequester runtime_exit_requester_; | |||
| std::unique_ptr<RuntimeMonitorWindow> runtime_monitor_window_; | |||
| RuntimeMonitorWidget *runtime_monitor_widget_ = nullptr; | |||
| std::unique_ptr<HmiRuntimeWindow> hmi_runtime_window_; | |||
| QTimer *runtime_refresh_timer_ = nullptr; | |||
| bool runtime_session_active_ = false; | |||
| }; | |||
| @@ -13,13 +13,14 @@ | |||
| #include "services/runtime_mode_service.h" | |||
| #include "services/register_monitor_service.h" | |||
| #include "services/register_comment_service.h" | |||
| #include "ui/alarm_configuration_dialog.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" | |||
| #include "ui/runtime_monitor_widget.h" | |||
| #include "ui/runtime_monitor_window.h" | |||
| #include <QAction> | |||
| #include <QApplication> | |||
| @@ -39,6 +40,7 @@ | |||
| #include <QPushButton> | |||
| #include <QStringList> | |||
| #include <QTableWidget> | |||
| #include <QTabBar> | |||
| #include <QTabWidget> | |||
| #include <QTimer> | |||
| #include <QTest> | |||
| @@ -280,19 +282,6 @@ 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); | |||
| @@ -591,14 +580,28 @@ void testRuntimeAlarmListInteraction() | |||
| require(alarm_list != nullptr && !alarm_list->binding.has_value(), | |||
| "AlarmList must not require a register binding"); | |||
| AlarmDefinition definition; | |||
| definition.address = RegisterAddress{RegisterArea::M, 0}; | |||
| definition.condition = AlarmCondition::MOn; | |||
| definition.message = "Emergency stop"; | |||
| const AlarmEditorResult alarm_result = | |||
| alarm_editor_service.addDefinition(definition); | |||
| require(alarm_result.succeeded, | |||
| "runtime alarm test must create an M alarm definition"); | |||
| std::vector<std::string> alarm_ids; | |||
| for (int index = 0; index <= ProjectLimits::kMaximumVisibleAlarmRows; ++index) | |||
| { | |||
| AlarmDefinition definition; | |||
| definition.address = RegisterAddress{RegisterArea::M, index}; | |||
| definition.condition = AlarmCondition::MOn; | |||
| definition.message = "Alarm " + std::to_string(index); | |||
| const AlarmEditorResult alarm_result = | |||
| alarm_editor_service.addDefinition(definition); | |||
| require(alarm_result.succeeded, | |||
| "runtime alarm test must create paged M alarm definitions"); | |||
| alarm_ids.push_back(alarm_result.id); | |||
| } | |||
| AlarmConfigurationDialog configuration_dialog(alarm_editor_service); | |||
| QLineEdit *message_edit = configuration_dialog.findChild<QLineEdit *>( | |||
| QStringLiteral("messageEdit")); | |||
| require(message_edit != nullptr | |||
| && message_edit->maxLength() | |||
| == static_cast<int>( | |||
| ProjectLimits::kMaximumAlarmMessageCharacters), | |||
| "alarm configuration must limit messages to 20 characters"); | |||
| HmiEditorWidget view(editor_service, runtime_service, alarm_service); | |||
| view.setPageId(page_id); | |||
| @@ -632,6 +635,8 @@ void testRuntimeAlarmListInteraction() | |||
| "an active M alarm must be available to AlarmList"); | |||
| require(alarm_item->isVisible() && alarm_item->zValue() > 0.0, | |||
| "AlarmList must appear above the page for an active alarm"); | |||
| require(alarm_item->boundingRect().height() < alarm_list->bounds.height, | |||
| "a runtime AlarmList with one record must remove unused row space"); | |||
| const QPoint header_position = view.mapFromScene(QPointF( | |||
| alarm_list->bounds.x + 20.0, alarm_list->bounds.y + 10.0)); | |||
| @@ -647,7 +652,40 @@ void testRuntimeAlarmListInteraction() | |||
| require(alarm_service.records().front().acknowledged, | |||
| "clicking an active AlarmList row must acknowledge the alarm"); | |||
| repository.writeBit(RegisterAddress{RegisterArea::M, 0}, false); | |||
| for (int index = 1; index <= ProjectLimits::kMaximumVisibleAlarmRows; ++index) | |||
| { | |||
| repository.writeBit(RegisterAddress{RegisterArea::M, index}, true); | |||
| } | |||
| alarm_service.reset(); | |||
| alarm_service.refresh(); | |||
| view.refreshRuntimeValues(); | |||
| QApplication::processEvents(); | |||
| require(alarm_service.records().size() | |||
| == static_cast<std::size_t>( | |||
| ProjectLimits::kMaximumVisibleAlarmRows + 1), | |||
| "AlarmList pagination must retain records beyond the visible row limit"); | |||
| require(alarm_item->boundingRect().height() == alarm_list->bounds.height, | |||
| "a full AlarmList page must use its configured maximum height"); | |||
| const QPoint next_page_position = view.mapFromScene(QPointF( | |||
| alarm_list->bounds.x + alarm_list->bounds.width - 14.0, | |||
| alarm_list->bounds.y + 10.0)); | |||
| QTest::mouseClick( | |||
| view.viewport(), Qt::LeftButton, Qt::NoModifier, next_page_position); | |||
| QApplication::processEvents(); | |||
| require(alarm_item->boundingRect().height() < alarm_list->bounds.height, | |||
| "the final partial alarm page must shrink to its visible rows"); | |||
| QTest::mouseClick( | |||
| view.viewport(), Qt::LeftButton, Qt::NoModifier, first_row_position); | |||
| require(alarm_service.records().back().definitionId == alarm_ids.front() | |||
| && alarm_service.records().back().acknowledged, | |||
| "the next AlarmList page must acknowledge its own first record"); | |||
| for (int index = 0; index <= ProjectLimits::kMaximumVisibleAlarmRows; ++index) | |||
| { | |||
| repository.writeBit(RegisterAddress{RegisterArea::M, index}, false); | |||
| } | |||
| alarm_service.refresh(); | |||
| view.refreshRuntimeValues(); | |||
| QApplication::processEvents(); | |||
| @@ -963,6 +1001,7 @@ void testModeActionsControlEditingAvailability() | |||
| window, "deleteVerticalWireAction"); | |||
| QDockWidget *project_dock = requiredChild<QDockWidget>(window, "projectDock"); | |||
| QDockWidget *properties_dock = requiredChild<QDockWidget>(window, "propertiesDock"); | |||
| QDockWidget *output_dock = requiredChild<QDockWidget>(window, "outputDock"); | |||
| QLabel *selection = requiredChild<QLabel>(window, "selectionValueLabel"); | |||
| QLineEdit *text_edit = requiredChild<QLineEdit>(window, "controlTextEdit"); | |||
| QLineEdit *text_color_edit = requiredChild<QLineEdit>(window, "textColorEdit"); | |||
| @@ -988,14 +1027,21 @@ void testModeActionsControlEditingAvailability() | |||
| LogicEditorWidget *logic_editor = requiredChild<LogicEditorWidget>( | |||
| window, "logicEditorWidget"); | |||
| QLabel *executor_status = requiredChild<QLabel>(window, "executorStatusLabel"); | |||
| QWidget *runtime_tab = requiredChild<QWidget>(window, "runtimeMonitorTab"); | |||
| RuntimeMonitorWindow *runtime_window = requiredChild<RuntimeMonitorWindow>( | |||
| window, "runtimeMonitorWindow"); | |||
| QWidget *runtime_hmi = requiredChild<QWidget>(window, "runtimeHmiView"); | |||
| QWidget *runtime_logic = requiredChild<QWidget>(window, "runtimeLogicView"); | |||
| QWidget *free_monitor = requiredChild<QWidget>(window, "freeMonitorWidget"); | |||
| QTabWidget *editor_tabs = requiredChild<QTabWidget>(window, "editorTabWidget"); | |||
| QTreeWidget *project_tree = requiredChild<QTreeWidget>(window, "projectTree"); | |||
| require(!runtime_tab->isVisible(), | |||
| "runtime monitor workspace must be hidden while editing"); | |||
| const bool initially_maximized = window.isMaximized(); | |||
| const bool project_dock_initially_visible = project_dock->isVisible(); | |||
| const bool properties_dock_initially_visible = properties_dock->isVisible(); | |||
| const bool output_dock_initially_visible = output_dock->isVisible(); | |||
| require(!runtime_window->isVisible() | |||
| && window.findChild<QWidget *>(QStringLiteral("runtimeMonitorTab")) | |||
| == nullptr, | |||
| "runtime monitor window must be hidden while editing and absent from editor tabs"); | |||
| for (const char *action_name : { | |||
| "exitAction", | |||
| "newProjectAction", | |||
| @@ -1056,11 +1102,10 @@ void testModeActionsControlEditingAvailability() | |||
| && !logic_timer_counter_menu->icon().isNull() | |||
| && !logic_data_menu->icon().isNull(), | |||
| "every grouped toolbar menu must have an icon"); | |||
| require(editor_tabs->count() == 3 | |||
| require(editor_tabs->count() == 2 | |||
| && !editor_tabs->tabIcon(0).isNull() | |||
| && !editor_tabs->tabIcon(1).isNull() | |||
| && !editor_tabs->tabIcon(2).isNull(), | |||
| "every editor workspace tab must have an icon"); | |||
| && !editor_tabs->tabIcon(1).isNull(), | |||
| "the two editor tabs must remain present and have icons"); | |||
| QToolButton *monitor_remove = free_monitor->findChild<QToolButton *>( | |||
| QStringLiteral("removeButton")); | |||
| QToolButton *monitor_clear = free_monitor->findChild<QToolButton *>( | |||
| @@ -1237,6 +1282,26 @@ void testModeActionsControlEditingAvailability() | |||
| require(editor_service.findPage(page_id)->controls.empty(), | |||
| "deleting a ProgressBar must remove it from the HMI page"); | |||
| requiredChild<QAction>(window, "addAlarmListAction")->trigger(); | |||
| const HmiControl *default_alarm_list = editor_service.findControl( | |||
| page_id, "alarm-list-1"); | |||
| const int expected_alarm_font_size = std::clamp( | |||
| QApplication::font().pointSize() | |||
| - ProjectLimits::kAlarmDefaultFontPointReduction, | |||
| ProjectLimits::kMinimumHmiFontPointSize, | |||
| ProjectLimits::kMaximumHmiFontPointSize); | |||
| require(default_alarm_list != nullptr | |||
| && default_alarm_list->bounds.width == 360 | |||
| && default_alarm_list->bounds.height == 136 | |||
| && text_edit->maxLength() | |||
| == static_cast<int>( | |||
| ProjectLimits::kMaximumAlarmTitleCharacters) | |||
| && font_size->value() == expected_alarm_font_size, | |||
| "AlarmList defaults must expose five rows and the reduced font size"); | |||
| delete_control_action->trigger(); | |||
| require(editor_service.findPage(page_id)->controls.empty(), | |||
| "deleting an AlarmList must remove it from the HMI page"); | |||
| add_indicator_action->trigger(); | |||
| HmiControl runtime_indicator = *editor_service.findControl(page_id, "indicator-1"); | |||
| runtime_indicator.binding = RegisterAddress{RegisterArea::M, 3}; | |||
| @@ -1415,6 +1480,14 @@ void testModeActionsControlEditingAvailability() | |||
| "project dock must be disabled while running"); | |||
| require(!properties_dock->isEnabled(), | |||
| "properties dock must be disabled while running"); | |||
| require(runtime_window->isVisible() && runtime_window->isMaximized() | |||
| && project_dock->isVisible() == project_dock_initially_visible | |||
| && properties_dock->isVisible() | |||
| == properties_dock_initially_visible | |||
| && output_dock->isVisible() == output_dock_initially_visible | |||
| && editor_tabs->tabBar()->isVisible() | |||
| && window.isMaximized() == initially_maximized, | |||
| "runtime monitoring must open maximized without changing the editor layout"); | |||
| require(!add_button_action->isEnabled(), | |||
| "HMI add controls must be disabled while running"); | |||
| require(!add_progress_bar_action->isEnabled(), | |||
| @@ -1424,7 +1497,7 @@ void testModeActionsControlEditingAvailability() | |||
| require(!undo_action->isEnabled() && !redo_action->isEnabled() | |||
| && !delete_selection_action->isEnabled(), | |||
| "undo, redo and delete must be disabled while running"); | |||
| require(runtime_tab->isVisible() && runtime_hmi->isVisible() | |||
| require(runtime_window->isVisible() && runtime_hmi->isVisible() | |||
| && runtime_logic->isVisible() && free_monitor->isVisible(), | |||
| "offline running must show HMI, ladder trace and free monitor together"); | |||
| auto *runtime_hmi_view = qobject_cast<HmiEditorWidget *>(runtime_hmi); | |||
| @@ -1461,14 +1534,21 @@ void testModeActionsControlEditingAvailability() | |||
| "project dock must be restored after returning to editing"); | |||
| require(properties_dock->isEnabled(), | |||
| "properties dock must be restored after returning to editing"); | |||
| require(project_dock->isVisible() == project_dock_initially_visible | |||
| && properties_dock->isVisible() | |||
| == properties_dock_initially_visible | |||
| && output_dock->isVisible() == output_dock_initially_visible | |||
| && editor_tabs->tabBar()->isVisible() | |||
| && window.isMaximized() == initially_maximized, | |||
| "returning to editing must restore the previous workspace layout"); | |||
| require(add_button_action->isEnabled(), | |||
| "HMI add controls must be restored after returning to editing"); | |||
| require(add_progress_bar_action->isEnabled(), | |||
| "ProgressBar creation must be restored after returning to editing"); | |||
| require(add_normally_open_action->isEnabled(), | |||
| "logic add nodes must be restored after returning to editing"); | |||
| require(!runtime_tab->isVisible(), | |||
| "returning to editing must hide the runtime monitor workspace"); | |||
| require(!runtime_window->isVisible(), | |||
| "returning to editing must hide the runtime monitor window"); | |||
| online_action->trigger(); | |||
| require(mode_service.mode() == ApplicationMode::Editing, | |||
| @@ -1477,7 +1557,7 @@ void testModeActionsControlEditingAvailability() | |||
| "rejected online running must restore the editing action"); | |||
| } | |||
| void testIndependentHmiRuntimeWindowLifecycle() | |||
| void testRuntimeMonitorWindowLifecycle() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| @@ -1491,7 +1571,17 @@ void testIndependentHmiRuntimeWindowLifecycle() | |||
| 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"); | |||
| "runtime-workspace fixture must configure a PageJump target"); | |||
| const HmiEditorResult button_result = editor_service.addControl( | |||
| main_page_id, HmiControlType::Button); | |||
| HmiControl button = *editor_service.findControl( | |||
| main_page_id, button_result.id); | |||
| button.binding = RegisterAddress{RegisterArea::M, 0}; | |||
| button.buttonOperation = HmiButtonOperation::Toggle; | |||
| button.bounds = HmiRect{200, 20, 120, 40}; | |||
| require(editor_service.updateControl( | |||
| main_page_id, button_result.id, button).succeeded, | |||
| "runtime-workspace fixture must configure an interactive HMI button"); | |||
| const HmiEditorResult return_jump_result = editor_service.addControl( | |||
| settings_page_id, HmiControlType::PageJump); | |||
| HmiControl return_jump = *editor_service.findControl( | |||
| @@ -1499,7 +1589,7 @@ void testIndependentHmiRuntimeWindowLifecycle() | |||
| 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"); | |||
| "runtime-workspace fixture must configure a PageJump back to the initial page"); | |||
| VirtualRegisterRepository repository; | |||
| HmiRuntimeService runtime_service(repository); | |||
| AlarmEditorService alarm_editor_service(project_service); | |||
| @@ -1525,92 +1615,114 @@ void testIndependentHmiRuntimeWindowLifecycle() | |||
| 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"); | |||
| RuntimeMonitorWindow *runtime_window = requiredChild<RuntimeMonitorWindow>( | |||
| window, "runtimeMonitorWindow"); | |||
| HmiEditorWidget *runtime_hmi = requiredChild<HmiEditorWidget>( | |||
| window, "runtimeHmiView"); | |||
| require(runtime_window->isWindow() && runtime_window->isVisible() | |||
| && runtime_window->isMaximized(), | |||
| "offline running must show a maximized runtime monitor window"); | |||
| require(window.findChildren<RuntimeMonitorWidget *>().size() == 1, | |||
| "the application must keep exactly one runtime monitor projection"); | |||
| require(window.findChild<QWidget *>(QStringLiteral("runtimeMonitorTab")) == nullptr, | |||
| "the editor must not retain an embedded runtime monitor tab"); | |||
| QTest::mouseClick( | |||
| runtime_hmi->viewport(), Qt::LeftButton, Qt::NoModifier, | |||
| runtime_hmi->mapFromScene(QPointF( | |||
| button.bounds.x + button.bounds.width / 2.0, | |||
| button.bounds.y + button.bounds.height / 2.0))); | |||
| QApplication::processEvents(); | |||
| require(repository.readBit({RegisterArea::M, 0}).succeeded | |||
| && repository.readBit({RegisterArea::M, 0}).value, | |||
| "the unified runtime HMI must write to the active virtual repository"); | |||
| QTest::mouseClick(runtime_hmi->viewport(), Qt::LeftButton, Qt::NoModifier, | |||
| runtime_hmi->mapFromScene( | |||
| jump_item->mapToScene(jump_item->boundingRect().center()))); | |||
| runtime_hmi->mapFromScene(QPointF( | |||
| jump.bounds.x + jump.bounds.width / 2.0, | |||
| jump.bounds.y + jump.bounds.height / 2.0))); | |||
| 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"); | |||
| "PageJump in the unified runtime HMI must update the navigation session"); | |||
| 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"); | |||
| "the runtime workspace header must follow the navigated HMI page"); | |||
| QTest::mouseClick(runtime_hmi->viewport(), Qt::LeftButton, Qt::NoModifier, | |||
| runtime_hmi->mapFromScene( | |||
| return_jump_item->mapToScene( | |||
| return_jump_item->boundingRect().center()))); | |||
| runtime_hmi->mapFromScene(QPointF( | |||
| return_jump.bounds.x + return_jump.bounds.width / 2.0, | |||
| return_jump.bounds.y + return_jump.bounds.height / 2.0))); | |||
| 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"); | |||
| "the unified runtime HMI must return to the initial page before editing"); | |||
| requiredChild<QAction>(window, "editingModeAction")->trigger(); | |||
| requiredChild<QToolButton>(window, "exitRuntimeButton")->click(); | |||
| QApplication::processEvents(); | |||
| require(mode_service.mode() == ApplicationMode::Editing, | |||
| "editing action must return the runtime mode to editing"); | |||
| "the runtime monitor exit button must return to editing"); | |||
| require(!runtime_window->isVisible(), | |||
| "returning to editing must hide the independent HMI window"); | |||
| "returning to editing must hide the runtime monitor window"); | |||
| requiredChild<QAction>(window, "addLabelAction")->trigger(); | |||
| QApplication::processEvents(); | |||
| require(editor_service.findPage(main_page_id)->controls.size() == 2U, | |||
| require(editor_service.findPage(main_page_id)->controls.size() == 3U, | |||
| "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_hmi = requiredChild<HmiEditorWidget>(window, "runtimeHmiView"); | |||
| require(runtime_window->isVisible() && runtime_window->isMaximized() | |||
| && runtime_hmi->scene()->items().size() == 4, | |||
| "a later runtime session must reopen and refresh the monitor window"); | |||
| runtime_window->close(); | |||
| QApplication::processEvents(); | |||
| require(mode_service.mode() == ApplicationMode::Editing, | |||
| "closing the HMI window must request a transition to editing"); | |||
| "closing the runtime monitor window must request editing mode"); | |||
| require(!runtime_window->isVisible(), | |||
| "closing the HMI window must not leave a visible runtime surface"); | |||
| "closing the runtime monitor window must leave no visible runtime surface"); | |||
| } | |||
| void testApplicationExitClosesRuntimeMonitorWindow() | |||
| { | |||
| TestProjectStorage storage; | |||
| ProjectService project_service(storage); | |||
| HmiEditorService editor_service(project_service); | |||
| LogicEditorService logic_editor_service(project_service); | |||
| 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); | |||
| MainWindow window( | |||
| mode_service, | |||
| project_service, | |||
| editor_service, | |||
| logic_editor_service, | |||
| runtime_service, | |||
| alarm_editor_service, | |||
| alarm_service, | |||
| register_comment_service, | |||
| monitor_service); | |||
| window.show(); | |||
| QApplication::processEvents(); | |||
| require(project_service.saveAs("runtime-window-exit-test.json").succeeded, | |||
| "application-exit fixture must clear the unsaved-project prompt"); | |||
| requiredChild<QAction>(window, "offlineModeAction")->trigger(); | |||
| QApplication::processEvents(); | |||
| RuntimeMonitorWindow *runtime_window = requiredChild<RuntimeMonitorWindow>( | |||
| window, "runtimeMonitorWindow"); | |||
| require(runtime_window->isVisible(), | |||
| "application-exit fixture must start with a visible runtime window"); | |||
| requiredChild<QAction>(window, "exitAction")->trigger(); | |||
| QApplication::processEvents(); | |||
| require(!window.isVisible() && !runtime_window->isVisible(), | |||
| "application exit must close both programmer and runtime windows"); | |||
| } | |||
| void testEdgeTimerPropertyEditingAndParallelMenu() | |||
| @@ -1881,6 +1993,15 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() | |||
| ProjectService project_service(storage); | |||
| HmiEditorService editor_service(project_service); | |||
| LogicEditorService logic_editor_service(project_service); | |||
| const std::string page_id = editor_service.ensureDefaultPage().id; | |||
| const HmiEditorResult button_result = editor_service.addControl( | |||
| page_id, HmiControlType::Button); | |||
| HmiControl button = *editor_service.findControl(page_id, button_result.id); | |||
| button.binding = RegisterAddress{RegisterArea::M, 5}; | |||
| button.buttonOperation = HmiButtonOperation::Toggle; | |||
| require(editor_service.updateControl( | |||
| page_id, button_result.id, button).succeeded, | |||
| "online workspace fixture must configure an interactive HMI button"); | |||
| VirtualRegisterRepository virtual_repository; | |||
| VirtualRegisterRepository plc_repository; | |||
| ActiveRegisterRepository active_repository(virtual_repository); | |||
| @@ -1916,17 +2037,31 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() | |||
| online_action->trigger(); | |||
| QApplication::processEvents(); | |||
| QWidget *runtime_tab = requiredChild<QWidget>(window, "runtimeMonitorTab"); | |||
| RuntimeMonitorWindow *runtime_window = requiredChild<RuntimeMonitorWindow>( | |||
| window, "runtimeMonitorWindow"); | |||
| QWidget *runtime_hmi = requiredChild<QWidget>(window, "runtimeHmiView"); | |||
| QWidget *runtime_logic = requiredChild<QWidget>(window, "runtimeLogicView"); | |||
| QWidget *free_monitor = requiredChild<QWidget>(window, "freeMonitorWidget"); | |||
| require(mode_service.mode() == ApplicationMode::OnlineRunning, | |||
| "completed PLC initial read must allow online running"); | |||
| require(runtime_tab->isVisible() && runtime_hmi->isVisible() | |||
| require(runtime_window->isVisible() && runtime_window->isMaximized() | |||
| && runtime_hmi->isVisible() | |||
| && free_monitor->isVisible(), | |||
| "online running must show HMI and free monitor together"); | |||
| "online running must show HMI and free monitor in the maximized window"); | |||
| require(!runtime_logic->isVisible(), | |||
| "online running must not show a misleading local ladder runtime trace"); | |||
| auto *runtime_hmi_view = qobject_cast<HmiEditorWidget *>(runtime_hmi); | |||
| require(runtime_hmi_view != nullptr, | |||
| "online running must expose the unified interactive HMI view"); | |||
| QTest::mouseClick( | |||
| runtime_hmi_view->viewport(), Qt::LeftButton, Qt::NoModifier, | |||
| runtime_hmi_view->mapFromScene(QPointF( | |||
| button.bounds.x + button.bounds.width / 2.0, | |||
| button.bounds.y + button.bounds.height / 2.0))); | |||
| QApplication::processEvents(); | |||
| require(plc_repository.readBit({RegisterArea::M, 5}).succeeded | |||
| && plc_repository.readBit({RegisterArea::M, 5}).value, | |||
| "the unified online HMI must write through the active PLC repository"); | |||
| QLineEdit *monitor_address = requiredChild<QLineEdit>(window, "addressEdit"); | |||
| QPushButton *monitor_add = requiredChild<QPushButton>(window, "addButton"); | |||
| QTableWidget *monitor_table = requiredChild<QTableWidget>(window, "monitorTable"); | |||
| @@ -1950,6 +2085,8 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly() | |||
| require(mode_service.mode() == ApplicationMode::Editing, | |||
| "a PLC timeout must return online running to editing"); | |||
| require(!runtime_window->isVisible(), | |||
| "a PLC timeout must hide the runtime monitor window"); | |||
| require(!monitor_write->isEnabled(), | |||
| "a PLC communication fault must disable free monitor writes"); | |||
| require(disconnect_action->isEnabled(), | |||
| @@ -2141,13 +2278,14 @@ int main(int argc, char *argv[]) | |||
| testLogicContinuousInsertionConsumesFullWire(); | |||
| testWindowTitleTracksUnsavedProjectChanges(); | |||
| testModeActionsControlEditingAvailability(); | |||
| testIndependentHmiRuntimeWindowLifecycle(); | |||
| testRuntimeMonitorWindowLifecycle(); | |||
| testEdgeTimerPropertyEditingAndParallelMenu(); | |||
| testPlcConfigurationUsesDialog(); | |||
| testRepeatedPlcStatusNotificationsAreCoalesced(); | |||
| testFreeMonitorWritesSingleValues(); | |||
| testOnlineWorkspaceShowsHmiAndFreeMonitorOnly(); | |||
| testMultiPageAndLogicMainWindowIntegration(); | |||
| testApplicationExitClosesRuntimeMonitorWindow(); | |||
| } | |||
| catch (const std::exception &error) | |||
| { | |||
| @@ -15,7 +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_window.cpp \ | |||
| ../src/ui/runtime_monitor_widget.cpp \ | |||
| ../src/ui/toolbar_icon_factory.cpp \ | |||
| ../src/ui/project_workspace_controller.cpp \ | |||
| @@ -53,7 +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_window.h \ | |||
| ../src/ui/runtime_monitor_widget.h \ | |||
| ../src/ui/toolbar_icon_factory.h \ | |||
| ../src/ui/project_workspace_controller.h \ | |||
| @@ -96,6 +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_window.ui \ | |||
| ../src/ui/runtime_monitor_widget.ui \ | |||
| ../src/ui/logic_instruction_dialog.ui | |||