Bladeren bron

feat: 完善工程师运行监控与界面交互

main
suyu 1 maand geleden
bovenliggende
commit
0e4a2c3ecd
23 gewijzigde bestanden met toevoegingen van 671 en 488 verwijderingen
  1. +3
    -3
      app/integrated_platform.pro
  2. +1
    -1
      app/src/domain/hmi_control_registry.cpp
  3. +4
    -0
      app/src/domain/project_limits.h
  4. +3
    -0
      app/src/ui/alarm_configuration_dialog.cpp
  5. +219
    -24
      app/src/ui/hmi_editor_widget.cpp
  6. +0
    -134
      app/src/ui/hmi_runtime_window.cpp
  7. +0
    -63
      app/src/ui/hmi_runtime_window.h
  8. +0
    -49
      app/src/ui/hmi_runtime_window.ui
  9. +9
    -4
      app/src/ui/logic_editor_widget.cpp
  10. +1
    -10
      app/src/ui/main_window.cpp
  11. +0
    -1
      app/src/ui/main_window.h
  12. +2
    -13
      app/src/ui/main_window.ui
  13. +20
    -4
      app/src/ui/property_panel_controller.cpp
  14. +9
    -3
      app/src/ui/runtime_monitor_widget.cpp
  15. +1
    -1
      app/src/ui/runtime_monitor_widget.h
  16. +1
    -0
      app/src/ui/runtime_monitor_widget.ui
  17. +54
    -0
      app/src/ui/runtime_monitor_window.cpp
  18. +38
    -0
      app/src/ui/runtime_monitor_window.h
  19. +44
    -0
      app/src/ui/runtime_monitor_window.ui
  20. +18
    -65
      app/src/ui/runtime_panel_controller.cpp
  21. +2
    -9
      app/src/ui/runtime_panel_controller.h
  22. +239
    -101
      app/tests/main_window_tests.cpp
  23. +3
    -3
      app/tests/main_window_tests.pro

+ 3
- 3
app/integrated_platform.pro Bestand weergeven

@@ -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

+ 1
- 1
app/src/domain/hmi_control_registry.cpp Bestand weergeven

@@ -66,7 +66,7 @@ constexpr std::array kControlDescriptors = {
"报警列表",
"alarm-list",
"报警信息",
{0, 0, 360, 180},
{0, 0, 360, 136},
HmiBindingKind::None,
HmiRuntimeValueKind::None,
false},


+ 4
- 0
app/src/domain/project_limits.h Bestand weergeven

@@ -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;


+ 3
- 0
app/src/ui/alarm_configuration_dialog.cpp Bestand weergeven

@@ -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(


+ 219
- 24
app/src/ui/hmi_editor_widget.cpp Bestand weergeven

@@ -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;


+ 0
- 134
app/src/ui/hmi_runtime_window.cpp Bestand weergeven

@@ -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);
}

+ 0
- 63
app/src/ui/hmi_runtime_window.h Bestand weergeven

@@ -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;
};

+ 0
- 49
app/src/ui/hmi_runtime_window.ui Bestand weergeven

@@ -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>

+ 9
- 4
app/src/ui/logic_editor_widget.cpp Bestand weergeven

@@ -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_))
{


+ 1
- 10
app/src/ui/main_window.cpp Bestand weergeven

@@ -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);


+ 0
- 1
app/src/ui/main_window.h Bestand weergeven

@@ -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;
};

+ 2
- 13
app/src/ui/main_window.ui Bestand weergeven

@@ -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"/>


+ 20
- 4
app/src/ui/property_panel_controller.cpp Bestand weergeven

@@ -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);


+ 9
- 3
app/src/ui/runtime_monitor_widget.cpp Bestand weergeven

@@ -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)


+ 1
- 1
app/src/ui/runtime_monitor_widget.h Bestand weergeven

@@ -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);


+ 1
- 0
app/src/ui/runtime_monitor_widget.ui Bestand weergeven

@@ -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>


+ 54
- 0
app/src/ui/runtime_monitor_window.cpp Bestand weergeven

@@ -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);
}

+ 38
- 0
app/src/ui/runtime_monitor_window.h Bestand weergeven

@@ -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;
};

+ 44
- 0
app/src/ui/runtime_monitor_window.ui Bestand weergeven

@@ -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>

+ 18
- 65
app/src/ui/runtime_panel_controller.cpp Bestand weergeven

@@ -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);
}



+ 2
- 9
app/src/ui/runtime_panel_controller.h Bestand weergeven

@@ -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;
};

+ 239
- 101
app/tests/main_window_tests.cpp Bestand weergeven

@@ -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)
{


+ 3
- 3
app/tests/main_window_tests.pro Bestand weergeven

@@ -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

Laden…
Annuleren
Opslaan