diff --git a/app/integrated_platform.pro b/app/integrated_platform.pro index 26113e2..c896ba6 100644 --- a/app/integrated_platform.pro +++ b/app/integrated_platform.pro @@ -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 diff --git a/app/src/domain/hmi_control_registry.cpp b/app/src/domain/hmi_control_registry.cpp index aa76c14..03b82b6 100644 --- a/app/src/domain/hmi_control_registry.cpp +++ b/app/src/domain/hmi_control_registry.cpp @@ -66,7 +66,7 @@ constexpr std::array kControlDescriptors = { "报警列表", "alarm-list", "报警信息", - {0, 0, 360, 180}, + {0, 0, 360, 136}, HmiBindingKind::None, HmiRuntimeValueKind::None, false}, diff --git a/app/src/domain/project_limits.h b/app/src/domain/project_limits.h index cd9dd54..2a9885b 100644 --- a/app/src/domain/project_limits.h +++ b/app/src/domain/project_limits.h @@ -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; diff --git a/app/src/ui/alarm_configuration_dialog.cpp b/app/src/ui/alarm_configuration_dialog.cpp index d7a5169..168b40b 100644 --- a/app/src/ui/alarm_configuration_dialog.cpp +++ b/app/src/ui/alarm_configuration_dialog.cpp @@ -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(ProjectLimits::kMaximumAlarmMessageCharacters)); ui_->alarmTable->horizontalHeader()->setSectionResizeMode( 0, QHeaderView::ResizeToContents); ui_->alarmTable->horizontalHeader()->setSectionResizeMode( diff --git a/app/src/ui/hmi_editor_widget.cpp b/app/src/ui/hmi_editor_widget.cpp index 4e7ac64..2d164c1 100644 --- a/app/src/ui/hmi_editor_widget.cpp +++ b/app/src/ui/hmi_editor_widget.cpp @@ -5,6 +5,7 @@ #include "services/alarm_service.h" #include "domain/project_limits.h" +#include #include #include #include @@ -15,6 +16,8 @@ #include #include #include +#include +#include #include #include @@ -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(alarm_page_ + 1U)) + .arg(static_cast(page_count))); + } + const QString title = QString::fromUtf8( + control_.text.data(), static_cast(control_.text.size())); painter->drawText( - header.adjusted(7, 0, -7, 0), + title_rect, Qt::AlignVCenter | Qt::AlignLeft, - QString::fromUtf8( - control_.text.data(), static_cast(control_.text.size()))); + painter->fontMetrics().elidedText( + title, + Qt::ElideRight, + std::max(0, static_cast(title_rect.width())))); - const int maximum_rows = std::max( - 0, - static_cast((rect.height() - kAlarmHeaderHeight) - / kAlarmRowHeight)); - const int visible_rows = std::min( - maximum_rows, static_cast(alarm_records_.size())); + const int visible_rows = static_cast(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(row)]; + const AlarmRecord &record = alarm_records_[ + first_record + static_cast(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(record.message.size()))); + painter->fontMetrics().elidedText( + QString::fromUtf8( + record.message.data(), + static_cast(record.message.size())), + Qt::ElideRight, + std::max(0, static_cast(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 &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( (event->pos().y() - kAlarmHeaderHeight) / kAlarmRowHeight); - if (row >= 0 && row < static_cast(alarm_records_.size())) + const std::size_t record_index = alarmFirstRecordIndex() + + static_cast(std::max(0, row)); + if (row >= 0 + && row < static_cast(visibleAlarmRecordCount()) + && record_index < alarm_records_.size()) { - const AlarmRecord &record = alarm_records_[static_cast(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((rect.height() - kAlarmHeaderHeight) + / kAlarmRowHeight)); + return static_cast(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(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 page_navigation_; std::function alarm_acknowledge_; std::vector alarm_records_; + std::size_t alarm_page_ = 0U; bool editing_enabled_ = true; bool runtime_active_ = false; bool runtime_write_enabled_ = false; diff --git a/app/src/ui/hmi_runtime_window.cpp b/app/src/ui/hmi_runtime_window.cpp deleted file mode 100644 index 678b36c..0000000 --- a/app/src/ui/hmi_runtime_window.cpp +++ /dev/null @@ -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 -#include - -#include - -namespace { - -QString fromUtf8(const std::string &value) -{ - return QString::fromUtf8(value.data(), static_cast(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()), - 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); -} diff --git a/app/src/ui/hmi_runtime_window.h b/app/src/ui/hmi_runtime_window.h deleted file mode 100644 index 36d8f9d..0000000 --- a/app/src/ui/hmi_runtime_window.h +++ /dev/null @@ -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 - -#include -#include - -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_; - ProjectService &project_service_; - HmiEditorWidget *hmi_view_ = nullptr; - bool runtime_active_ = false; -}; diff --git a/app/src/ui/hmi_runtime_window.ui b/app/src/ui/hmi_runtime_window.ui deleted file mode 100644 index 89b557f..0000000 --- a/app/src/ui/hmi_runtime_window.ui +++ /dev/null @@ -1,49 +0,0 @@ - - - HmiRuntimeWindow - - - 001280800 - - HMI 运行 - - - 0 - 0 - 0 - 0 - 0 - - - 042 - 1677721542 - QFrame::StyledPanel - - 12 - 4 - 8 - 4 - HMI 运行font-weight: 600; - Qt::Horizontal4020 - 未选择页面Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - 1700当前未运行Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - 退出运行退出运行退出运行Qt::ToolButtonIconOnly - - - - - - - 0 - 0 - 0 - 0 - - - - - - - - - diff --git a/app/src/ui/logic_editor_widget.cpp b/app/src/ui/logic_editor_widget.cpp index 58add82..4d0aa61 100644 --- a/app/src/ui/logic_editor_widget.cpp +++ b/app/src/ui/logic_editor_widget.cpp @@ -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(&config_)) { diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 8ba350c..8ad1460 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -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( *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); diff --git a/app/src/ui/main_window.h b/app/src/ui/main_window.h index 611e5d3..666b3f0 100644 --- a/app/src/ui/main_window.h +++ b/app/src/ui/main_window.h @@ -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; }; diff --git a/app/src/ui/main_window.ui b/app/src/ui/main_window.ui index a9e76c2..14b89ad 100644 --- a/app/src/ui/main_window.ui +++ b/app/src/ui/main_window.ui @@ -207,18 +207,7 @@ - - - 运行监控 - - - 0 - 0 - 0 - 0 - - @@ -310,7 +299,7 @@ TopToolBarArea - false + true @@ -330,7 +319,7 @@ TopToolBarArea - false + true diff --git a/app/src/ui/property_panel_controller.cpp b/app/src/ui/property_panel_controller.cpp index 5d7bf92..8081622 100644 --- a/app/src/ui/property_panel_controller.cpp +++ b/app/src/ui/property_panel_controller.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,19 @@ std::string toUtf8(const QString &value) return std::string(bytes.constData(), static_cast(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(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); diff --git a/app/src/ui/runtime_monitor_widget.cpp b/app/src/ui/runtime_monitor_widget.cpp index 2d73cb2..1b227ed 100644 --- a/app/src/ui/runtime_monitor_widget.cpp +++ b/app/src/ui/runtime_monitor_widget.cpp @@ -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 #include #include +#include #include @@ -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::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) diff --git a/app/src/ui/runtime_monitor_widget.h b/app/src/ui/runtime_monitor_widget.h index 0313dfd..4862d8c 100644 --- a/app/src/ui/runtime_monitor_widget.h +++ b/app/src/ui/runtime_monitor_widget.h @@ -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); diff --git a/app/src/ui/runtime_monitor_widget.ui b/app/src/ui/runtime_monitor_widget.ui index 7cbfa09..8302caa 100644 --- a/app/src/ui/runtime_monitor_widget.ui +++ b/app/src/ui/runtime_monitor_widget.ui @@ -13,6 +13,7 @@ 运行监控font-weight: 600; Qt::Horizontal4020 当前未运行Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + 返回编辑态返回编辑态返回编辑态Qt::ToolButtonIconOnly diff --git a/app/src/ui/runtime_monitor_window.cpp b/app/src/ui/runtime_monitor_window.cpp new file mode 100644 index 0000000..3a0006b --- /dev/null +++ b/app/src/ui/runtime_monitor_window.cpp @@ -0,0 +1,54 @@ +#include "runtime_monitor_window.h" + +#include "ui_runtime_monitor_window.h" + +#include +#include + +RuntimeMonitorWindow::RuntimeMonitorWindow(QWidget *parent) + : QMainWindow(parent, Qt::Window), + ui_(std::make_unique()) +{ + 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); +} diff --git a/app/src/ui/runtime_monitor_window.h b/app/src/ui/runtime_monitor_window.h new file mode 100644 index 0000000..33623ba --- /dev/null +++ b/app/src/ui/runtime_monitor_window.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include + +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_; + bool runtime_active_ = false; + bool application_exit_ = false; +}; diff --git a/app/src/ui/runtime_monitor_window.ui b/app/src/ui/runtime_monitor_window.ui new file mode 100644 index 0000000..668b040 --- /dev/null +++ b/app/src/ui/runtime_monitor_window.ui @@ -0,0 +1,44 @@ + + + RuntimeMonitorWindow + + + + 0 + 0 + 1280 + 800 + + + + + 960 + 640 + + + + 运行监控 - 综合平台编程器 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + diff --git a/app/src/ui/runtime_panel_controller.cpp b/app/src/ui/runtime_panel_controller.cpp index 7ecf37e..4d1c41f 100644 --- a/app/src/ui/runtime_panel_controller.cpp +++ b/app/src/ui/runtime_panel_controller.cpp @@ -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 #include @@ -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(&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( - 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); } diff --git a/app/src/ui/runtime_panel_controller.h b/app/src/ui/runtime_panel_controller.h index cc8b853..66367db 100644 --- a/app/src/ui/runtime_panel_controller.h +++ b/app/src/ui/runtime_panel_controller.h @@ -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 runtime_monitor_window_; RuntimeMonitorWidget *runtime_monitor_widget_ = nullptr; - std::unique_ptr hmi_runtime_window_; QTimer *runtime_refresh_timer_ = nullptr; bool runtime_session_active_ = false; }; diff --git a/app/tests/main_window_tests.cpp b/app/tests/main_window_tests.cpp index 8815972..801ae41 100644 --- a/app/tests/main_window_tests.cpp +++ b/app/tests/main_window_tests.cpp @@ -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 #include @@ -39,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -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(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 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( + QStringLiteral("messageEdit")); + require(message_edit != nullptr + && message_edit->maxLength() + == static_cast( + 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( + 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(window, "projectDock"); QDockWidget *properties_dock = requiredChild(window, "propertiesDock"); + QDockWidget *output_dock = requiredChild(window, "outputDock"); QLabel *selection = requiredChild(window, "selectionValueLabel"); QLineEdit *text_edit = requiredChild(window, "controlTextEdit"); QLineEdit *text_color_edit = requiredChild(window, "textColorEdit"); @@ -988,14 +1027,21 @@ void testModeActionsControlEditingAvailability() LogicEditorWidget *logic_editor = requiredChild( window, "logicEditorWidget"); QLabel *executor_status = requiredChild(window, "executorStatusLabel"); - QWidget *runtime_tab = requiredChild(window, "runtimeMonitorTab"); + RuntimeMonitorWindow *runtime_window = requiredChild( + window, "runtimeMonitorWindow"); QWidget *runtime_hmi = requiredChild(window, "runtimeHmiView"); QWidget *runtime_logic = requiredChild(window, "runtimeLogicView"); QWidget *free_monitor = requiredChild(window, "freeMonitorWidget"); QTabWidget *editor_tabs = requiredChild(window, "editorTabWidget"); QTreeWidget *project_tree = requiredChild(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(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( QStringLiteral("removeButton")); QToolButton *monitor_clear = free_monitor->findChild( @@ -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(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( + 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(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(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( - QStringLiteral("hmiRuntimeHmiView")) != nullptr, - "independent HMI window must contain a runtime-only HMI view"); - HmiEditorWidget *runtime_hmi = runtime_window->findChild( - 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( + window, "runtimeMonitorWindow"); + HmiEditorWidget *runtime_hmi = requiredChild( + window, "runtimeHmiView"); + require(runtime_window->isWindow() && runtime_window->isVisible() + && runtime_window->isMaximized(), + "offline running must show a maximized runtime monitor window"); + require(window.findChildren().size() == 1, + "the application must keep exactly one runtime monitor projection"); + require(window.findChild(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(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(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(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(window, "editingModeAction")->trigger(); + requiredChild(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(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(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( - 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(window, "runtimeMonitorWidget") - ->findChild(QStringLiteral("runtimeHmiView")) - ->scene()->items().size() == 3, - "the diagnostic HMI must keep projecting the edited page"); + runtime_hmi = requiredChild(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(window, "offlineModeAction")->trigger(); + QApplication::processEvents(); + RuntimeMonitorWindow *runtime_window = requiredChild( + window, "runtimeMonitorWindow"); + require(runtime_window->isVisible(), + "application-exit fixture must start with a visible runtime window"); + + requiredChild(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(window, "runtimeMonitorTab"); + RuntimeMonitorWindow *runtime_window = requiredChild( + window, "runtimeMonitorWindow"); QWidget *runtime_hmi = requiredChild(window, "runtimeHmiView"); QWidget *runtime_logic = requiredChild(window, "runtimeLogicView"); QWidget *free_monitor = requiredChild(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(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(window, "addressEdit"); QPushButton *monitor_add = requiredChild(window, "addButton"); QTableWidget *monitor_table = requiredChild(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) { diff --git a/app/tests/main_window_tests.pro b/app/tests/main_window_tests.pro index 638720b..6c3596b 100644 --- a/app/tests/main_window_tests.pro +++ b/app/tests/main_window_tests.pro @@ -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