Quellcode durchsuchen

feat: 集成多页面多逻辑工作区

main
suyu vor 1 Monat
Ursprung
Commit
d8f18ef501
12 geänderte Dateien mit 1038 neuen und 53 gelöschten Zeilen
  1. +2
    -0
      app/integrated_platform.pro
  2. +3
    -0
      app/src/main.cpp
  3. +62
    -6
      app/src/ui/hmi_editor_widget.cpp
  4. +1
    -0
      app/src/ui/hmi_editor_widget.h
  5. +583
    -33
      app/src/ui/main_window.cpp
  6. +37
    -0
      app/src/ui/main_window.h
  7. +31
    -3
      app/src/ui/main_window.ui
  8. +110
    -8
      app/src/ui/runtime_monitor_widget.cpp
  9. +19
    -1
      app/src/ui/runtime_monitor_widget.h
  10. +14
    -2
      app/src/ui/runtime_monitor_widget.ui
  11. +174
    -0
      app/tests/main_window_tests.cpp
  12. +2
    -0
      app/tests/main_window_tests.pro

+ 2
- 0
app/integrated_platform.pro Datei anzeigen

@@ -30,6 +30,7 @@ SOURCES += \
src/domain/runtime_state.cpp \
src/services/project_service.cpp \
src/services/hmi_editor_service.cpp \
src/services/hmi_navigation_service.cpp \
src/services/logic_editor_service.cpp \
src/services/hmi_runtime_service.cpp \
src/services/software_logic_executor.cpp \
@@ -59,6 +60,7 @@ HEADERS += \
src/domain/project_storage.h \
src/services/project_service.h \
src/services/hmi_editor_service.h \
src/services/hmi_navigation_service.h \
src/services/logic_editor_service.h \
src/services/hmi_runtime_service.h \
src/services/software_logic_executor.h \


+ 3
- 0
app/src/main.cpp Datei anzeigen

@@ -14,6 +14,7 @@
#include "domain/active_register_repository.h"
#include "services/hmi_editor_service.h"
#include "services/hmi_runtime_service.h"
#include "services/hmi_navigation_service.h"
#include "services/logic_editor_service.h"
#include "services/offline_simulation_service.h"
#include "services/project_service.h"
@@ -31,6 +32,7 @@ int main(int argc, char *argv[])
JsonProjectStorage project_storage;
ProjectService project_service(project_storage);
HmiEditorService hmi_editor_service(project_service);
HmiNavigationService hmi_navigation_service(project_service);
LogicEditorService logic_editor_service(project_service);
// 当前离线模式使用内存仓库,后续真机模式替换为 PLC 缓存实现
VirtualRegisterRepository virtual_register_repository;
@@ -53,6 +55,7 @@ int main(int argc, char *argv[])
hmi_editor_service,
logic_editor_service,
hmi_runtime_service,
hmi_navigation_service,
register_monitor_service);
main_window.show();



+ 62
- 6
app/src/ui/hmi_editor_widget.cpp Datei anzeigen

@@ -32,13 +32,15 @@ public:
int page_height,
std::function<void(const std::string &, const QPointF &)> moved,
std::function<void(const std::string &, HmiButtonEvent)> button_event,
std::function<void(const std::string &)> numeric_input_activated)
std::function<void(const std::string &)> numeric_input_activated,
std::function<void(const std::string &)> page_navigation)
: control_(control),
page_width_(page_width),
page_height_(page_height),
moved_(std::move(moved)),
button_event_(std::move(button_event)),
numeric_input_activated_(std::move(numeric_input_activated))
numeric_input_activated_(std::move(numeric_input_activated)),
page_navigation_(std::move(page_navigation))
{
// 领域坐标直接作为图元在场景中的初始位置
setPos(control_.bounds.x, control_.bounds.y);
@@ -164,6 +166,18 @@ public:
textWithValue());
break;
}
case HmiControlType::PageJump:
{
const QColor fill = runtime_active_ && page_hovered_
? QColor(QStringLiteral("#d8eafa"))
: QColor(QStringLiteral("#e8f1fa"));
painter->setBrush(fill);
painter->setPen(QPen(QColor(QStringLiteral("#4e789f")), 1));
painter->drawRoundedRect(rect, 4, 4);
painter->setPen(QColor(QStringLiteral("#244b6b")));
painter->drawText(rect, Qt::AlignCenter, textWithValue());
break;
}
case HmiControlType::Label:
default:
{
@@ -207,15 +221,19 @@ public:

const bool runtime_input = runtime_active_
&& (control_.type == HmiControlType::Button
|| control_.type == HmiControlType::NumericInput);
const bool runtime_input_enabled = runtime_input && runtime_write_enabled_;
|| control_.type == HmiControlType::NumericInput
|| control_.type == HmiControlType::PageJump);
const bool runtime_input_enabled = runtime_input
&& (control_.type == HmiControlType::PageJump || runtime_write_enabled_);
setAcceptedMouseButtons(
editable || runtime_input_enabled ? Qt::LeftButton : Qt::NoButton);

const bool runtime_button_enabled = runtime_active_
&& control_.type == HmiControlType::Button && runtime_write_enabled_;
setAcceptHoverEvents(runtime_button_enabled);
if (runtime_button_enabled)
const bool runtime_page_jump_enabled = runtime_active_
&& control_.type == HmiControlType::PageJump;
setAcceptHoverEvents(runtime_button_enabled || runtime_page_jump_enabled);
if (runtime_button_enabled || runtime_page_jump_enabled)
{
setCursor(Qt::PointingHandCursor);
}
@@ -223,6 +241,7 @@ public:
{
unsetCursor();
button_hovered_ = false;
page_hovered_ = false;
button_pressed_ = false;
}
update();
@@ -260,6 +279,13 @@ protected:
// 运行态按钮按下时通知外层执行配置的 M 位操作
void mousePressEvent(QGraphicsSceneMouseEvent *event) override
{
if (runtime_active_ && control_.type == HmiControlType::PageJump)
{
page_pressed_ = true;
update();
event->accept();
return;
}
if (runtime_active_ && runtime_write_enabled_
&& control_.type == HmiControlType::Button && button_event_)
{
@@ -291,6 +317,18 @@ protected:
// 拖拽结束后才提交最终坐标,避免移动过程频繁修改领域模型
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override
{
if (runtime_active_ && control_.type == HmiControlType::PageJump
&& page_pressed_)
{
page_pressed_ = false;
update();
if (page_navigation_ && control_.pageJump.has_value())
{
page_navigation_(control_.pageJump->targetPageId);
}
event->accept();
return;
}
if (runtime_active_ && runtime_write_enabled_
&& control_.type == HmiControlType::Button && button_pressed_)
{
@@ -316,6 +354,11 @@ protected:
button_hovered_ = true;
update();
}
if (runtime_active_ && control_.type == HmiControlType::PageJump)
{
page_hovered_ = true;
update();
}
QGraphicsItem::hoverEnterEvent(event);
}

@@ -326,6 +369,11 @@ protected:
button_hovered_ = false;
update();
}
if (page_hovered_)
{
page_hovered_ = false;
update();
}
QGraphicsItem::hoverLeaveEvent(event);
}

@@ -384,11 +432,14 @@ private:
std::function<void(const std::string &, const QPointF &)> moved_;
std::function<void(const std::string &, HmiButtonEvent)> button_event_;
std::function<void(const std::string &)> numeric_input_activated_;
std::function<void(const std::string &)> page_navigation_;
bool editing_enabled_ = true;
bool runtime_active_ = false;
bool runtime_write_enabled_ = false;
bool button_hovered_ = false;
bool button_pressed_ = false;
bool page_hovered_ = false;
bool page_pressed_ = false;
// 指示灯读取到的 M 位值
bool bit_value_ = false;
// 数值控件读取到的 D 字值
@@ -501,6 +552,11 @@ void HmiEditorWidget::reloadPage()
[this](const std::string &control_id)
{
handleNumericInputActivated(control_id);
},
[this](const std::string &target_page_id)
{
emit pageNavigationRequested(
QString::fromStdString(target_page_id));
});
scene_->addItem(item);
item->setInteractionState(


+ 1
- 0
app/src/ui/hmi_editor_widget.h Datei anzeigen

@@ -90,6 +90,7 @@ signals:
* @param message 可直接显示的错误信息
*/
void editorError(const QString &message);
void pageNavigationRequested(const QString &target_page_id);

protected:
void resizeEvent(QResizeEvent *event) override;


+ 583
- 33
app/src/ui/main_window.cpp Datei anzeigen

@@ -6,6 +6,7 @@
#include "runtime_monitor_widget.h"
#include "free_monitor_widget.h"
#include "services/hmi_editor_service.h"
#include "services/hmi_navigation_service.h"
#include "services/hmi_runtime_service.h"
#include "services/logic_editor_service.h"
#include "services/project_service.h"
@@ -23,6 +24,7 @@
#include <QMessageBox>
#include <QMenu>
#include <QPushButton>
#include <QSignalBlocker>
#include <QSpinBox>
#include <QStatusBar>
#include <QStyle>
@@ -37,6 +39,16 @@

namespace {

enum class ProjectTreeItemKind
{
Root = 0,
HmiPage = 1,
ControlLogic = 2
};

constexpr int kProjectTreeKindRole = Qt::UserRole;
constexpr int kProjectTreeIdRole = Qt::UserRole + 1;

QString modeText(ApplicationMode mode)
{
switch (mode)
@@ -157,10 +169,17 @@ QString controlTypeText(HmiControlType type)
return MainWindow::tr("数值输入");
}
case HmiControlType::Label:
default:
{
return MainWindow::tr("文本");
}
case HmiControlType::PageJump:
{
return MainWindow::tr("页面跳转");
}
default:
{
return MainWindow::tr("未知控件");
}
}
}

@@ -181,7 +200,37 @@ MainWindow::MainWindow(
hmi_editor_service_(hmi_editor_service),
logic_editor_service_(logic_editor_service),
hmi_runtime_service_(hmi_runtime_service),
register_monitor_service_(register_monitor_service)
register_monitor_service_(register_monitor_service),
owned_hmi_navigation_service_(
std::make_unique<HmiNavigationService>(project_service)),
hmi_navigation_service_(owned_hmi_navigation_service_.get())
{
initializeUi();
}

MainWindow::MainWindow(
RuntimeModeService &runtime_mode_service,
ProjectService &project_service,
HmiEditorService &hmi_editor_service,
LogicEditorService &logic_editor_service,
HmiRuntimeService &hmi_runtime_service,
HmiNavigationService &hmi_navigation_service,
RegisterMonitorService &register_monitor_service,
QWidget *parent)
: QMainWindow(parent),
ui_(std::make_unique<Ui::MainWindow>()),
runtime_mode_service_(runtime_mode_service),
project_service_(project_service),
hmi_editor_service_(hmi_editor_service),
logic_editor_service_(logic_editor_service),
hmi_runtime_service_(hmi_runtime_service),
register_monitor_service_(register_monitor_service),
hmi_navigation_service_(&hmi_navigation_service)
{
initializeUi();
}

void MainWindow::initializeUi()
{
ui_->setupUi(this);
configureAppearance();
@@ -191,10 +240,13 @@ MainWindow::MainWindow(
configureHmiEditor();
configureLogicEditor();
configureRuntimeMonitor();
configureProjectTree();
runtime_mode_service_.setPlcStatusChangedCallback(
[this] { schedulePlcStatusUpdate(); });
hmi_editor_service_.ensureDefaultPage();
logic_editor_service_.ensureDefaultLogic();
current_hmi_page_id_ = hmi_editor_service_.firstPageId();
current_logic_id_ = logic_editor_service_.firstLogicId();
refreshProjectUi();
updateModeUi(tr("系统已进入编辑态"));
}
@@ -204,6 +256,16 @@ MainWindow::~MainWindow()
runtime_mode_service_.setPlcStatusChangedCallback({});
}

const std::string &MainWindow::currentHmiPageId() const
{
return current_hmi_page_id_;
}

const std::string &MainWindow::currentLogicId() const
{
return current_logic_id_;
}

void MainWindow::configureActions()
{
mode_action_group_ = new QActionGroup(this);
@@ -232,6 +294,10 @@ void MainWindow::configureActions()
[this] { addHmiControl(HmiControlType::NumericDisplay); });
connect(ui_->addNumericInputAction, &QAction::triggered, this,
[this] { addHmiControl(HmiControlType::NumericInput); });
connect(ui_->addLabelAction, &QAction::triggered, this,
[this] { addHmiControl(HmiControlType::Label); });
connect(ui_->addPageJumpAction, &QAction::triggered, this,
[this] { addHmiControl(HmiControlType::PageJump); });
connect(ui_->deleteControlAction, &QAction::triggered,
this, &MainWindow::deleteSelectedControl);

@@ -349,6 +415,7 @@ void MainWindow::configureActions()
if (index == 0 || index == 1)
{
last_editing_tab_index_ = index;
refreshProjectUi();
}
});
ui_->hmiToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 0);
@@ -383,6 +450,10 @@ void MainWindow::configureAppearance()
style()->standardIcon(QStyle::SP_FileDialogInfoView));
ui_->addNumericInputAction->setIcon(
style()->standardIcon(QStyle::SP_FileDialogContentsView));
ui_->addLabelAction->setIcon(
style()->standardIcon(QStyle::SP_FileDialogInfoView));
ui_->addPageJumpAction->setIcon(
style()->standardIcon(QStyle::SP_ArrowForward));
ui_->deleteControlAction->setIcon(style()->standardIcon(QStyle::SP_TrashIcon));
ui_->addRungAction->setIcon(style()->standardIcon(QStyle::SP_FileIcon));
ui_->parallelInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowDown));
@@ -483,7 +554,8 @@ void MainWindow::configureHmiEditor()
[this]
{
logic_editor_widget_->setRuntimeTrace(
runtime_mode_service_.offlineSimulationService().traceSnapshot());
runtime_mode_service_.offlineSimulationService()
.traceSnapshot().forLogic(current_logic_id_));
runtime_monitor_widget_->setLogicTrace(
runtime_mode_service_.offlineSimulationService().traceSnapshot());
},
@@ -521,6 +593,8 @@ void MainWindow::configureRuntimeMonitor()
hmi_editor_service_,
hmi_runtime_service_,
logic_editor_service_,
project_service_,
*hmi_navigation_service_,
register_monitor_service_,
ui_->runtimeMonitorTab);
runtime_monitor_widget_->setObjectName(QStringLiteral("runtimeMonitorWidget"));
@@ -543,9 +617,76 @@ void MainWindow::configureRuntimeMonitor()
{
statusBar()->showMessage(message, 4000);
});
connect(runtime_monitor_widget_, &RuntimeMonitorWidget::navigationFailed,
this,
[this](const QString &message)
{
statusBar()->showMessage(message, 5000);
ui_->outputList->addItem(tr("页面导航失败:%1").arg(message));
});
ui_->editorTabWidget->setTabVisible(2, false);
}

void MainWindow::configureProjectTree()
{
ui_->projectTree->setContextMenuPolicy(Qt::ActionsContextMenu);

add_page_action_ = new QAction(tr("新建页面"), this);
add_page_action_->setObjectName(QStringLiteral("addPageAction"));
add_logic_action_ = new QAction(tr("新建控制逻辑"), this);
add_logic_action_->setObjectName(QStringLiteral("addLogicAction"));
rename_project_item_action_ = new QAction(tr("重命名"), this);
rename_project_item_action_->setObjectName(
QStringLiteral("renameProjectItemAction"));
delete_project_item_action_ = new QAction(tr("删除"), this);
delete_project_item_action_->setObjectName(
QStringLiteral("deleteProjectItemAction"));
move_project_item_up_action_ = new QAction(tr("上移"), this);
move_project_item_up_action_->setObjectName(
QStringLiteral("moveProjectItemUpAction"));
move_project_item_down_action_ = new QAction(tr("下移"), this);
move_project_item_down_action_->setObjectName(
QStringLiteral("moveProjectItemDownAction"));
set_initial_page_action_ = new QAction(tr("设为初始页面"), this);
set_initial_page_action_->setObjectName(
QStringLiteral("setInitialPageAction"));
toggle_logic_enabled_action_ = new QAction(tr("禁用控制逻辑"), this);
toggle_logic_enabled_action_->setObjectName(
QStringLiteral("toggleLogicEnabledAction"));

ui_->projectTree->addActions({
add_page_action_,
add_logic_action_,
rename_project_item_action_,
delete_project_item_action_,
move_project_item_up_action_,
move_project_item_down_action_,
set_initial_page_action_,
toggle_logic_enabled_action_});

connect(add_page_action_, &QAction::triggered, this, &MainWindow::addPage);
connect(add_logic_action_, &QAction::triggered, this, &MainWindow::addLogic);
connect(rename_project_item_action_, &QAction::triggered,
this, &MainWindow::renameProjectItem);
connect(delete_project_item_action_, &QAction::triggered,
this, &MainWindow::deleteProjectItem);
connect(move_project_item_up_action_, &QAction::triggered,
this, [this] { moveProjectItem(-1); });
connect(move_project_item_down_action_, &QAction::triggered,
this, [this] { moveProjectItem(1); });
connect(set_initial_page_action_, &QAction::triggered,
this, &MainWindow::setSelectedPageAsInitial);
connect(toggle_logic_enabled_action_, &QAction::triggered,
this, &MainWindow::toggleSelectedLogicEnabled);
connect(ui_->projectTree, &QTreeWidget::currentItemChanged,
this,
[this](QTreeWidgetItem *, QTreeWidgetItem *)
{
handleProjectTreeSelection();
});
updateProjectTreeActions();
}

void MainWindow::configurePropertyEditor()
{
ui_->bindingAreaComboBox->setItemData(0, -1);
@@ -567,6 +708,8 @@ void MainWindow::configurePropertyEditor()
this, &MainWindow::applySelectedControlProperties);
connect(ui_->applyLogicPropertiesButton, &QPushButton::clicked,
this, &MainWindow::applySelectedLogicNodeProperties);
ui_->targetPageLabel->setVisible(false);
ui_->targetPageComboBox->setVisible(false);
showControlProperties({});
}

@@ -578,28 +721,61 @@ void MainWindow::configurePlcConnection()

void MainWindow::refreshProjectUi()
{
// 工程树只展示模型摘要,画布内容由 HmiEditorWidget 单独投影
const std::string current_page_id = hmi_editor_service_.firstPageId();
// 仅在当前对象消失时回退,普通刷新必须保留用户的会话选择
if (hmi_editor_service_.findPage(current_hmi_page_id_) == nullptr)
{
const std::string &initial_id = project_service_.project().initialHmiPageId;
current_hmi_page_id_ = hmi_editor_service_.findPage(initial_id) != nullptr
? initial_id : hmi_editor_service_.firstPageId();
selected_control_id_.clear();
}
if (logic_editor_service_.findLogic(current_logic_id_) == nullptr)
{
current_logic_id_ = logic_editor_service_.firstLogicId();
selected_logic_node_id_.clear();
}

if (hmi_editor_widget_ != nullptr)
{
hmi_editor_widget_->setPageId(current_page_id);
hmi_editor_widget_->setPageId(current_hmi_page_id_);
}
const HmiPage *page = hmi_editor_service_.findPage(current_page_id);
const std::string current_logic_id = logic_editor_service_.firstLogicId();
const HmiPage *page = hmi_editor_service_.findPage(current_hmi_page_id_);
if (logic_editor_widget_ != nullptr)
{
logic_editor_widget_->setLogicId(current_logic_id);
logic_editor_widget_->setLogicId(current_logic_id_);
}
if (runtime_monitor_widget_ != nullptr)
if (runtime_monitor_widget_ != nullptr
&& runtime_mode_service_.mode() != ApplicationMode::Editing)
{
runtime_monitor_widget_->setProjectObjects(current_page_id, current_logic_id);
runtime_monitor_widget_->setProjectObjects(
hmi_navigation_service_->currentPageId(), current_logic_id_);
}
runtime_mode_service_.refreshPlcPollAddresses();

const QSignalBlocker blocker(ui_->projectTree);
ui_->projectTree->clear();
auto *hmi_root = new QTreeWidgetItem(ui_->projectTree, {tr("HMI 页面")});
hmi_root->setData(0, kProjectTreeKindRole,
static_cast<int>(ProjectTreeItemKind::Root));
QTreeWidgetItem *selected_tree_item = nullptr;
for (const HmiPage &candidate : project_service_.project().hmiPages)
{
const QString initial_suffix = candidate.id
== project_service_.project().initialHmiPageId
? tr("(初始)") : QString{};
auto *item = new QTreeWidgetItem(
hmi_root, {fromUtf8(candidate.name) + initial_suffix});
item->setData(0, kProjectTreeKindRole,
static_cast<int>(ProjectTreeItemKind::HmiPage));
item->setData(0, kProjectTreeIdRole, fromUtf8(candidate.id));
if (candidate.id == current_hmi_page_id_
&& ui_->editorTabWidget->currentIndex() == 0)
{
selected_tree_item = item;
}
}
if (page != nullptr)
{
hmi_root->addChild(new QTreeWidgetItem(hmi_root, {fromUtf8(page->name)}));
ui_->hmiPageTitleLabel->setText(fromUtf8(page->name));
ui_->hmiPageSizeLabel->setText(
tr("%1 x %2").arg(page->width).arg(page->height));
@@ -610,13 +786,311 @@ void MainWindow::refreshProjectUi()
ui_->hmiPageSizeLabel->clear();
}
auto *logic_root = new QTreeWidgetItem(ui_->projectTree, {tr("控制逻辑")});
logic_root->setData(0, kProjectTreeKindRole,
static_cast<int>(ProjectTreeItemKind::Root));
for (const ControlLogic &logic : project_service_.project().controlLogics)
{
const QString suffix = logic.enabled ? QString{} : tr("(已禁用)");
logic_root->addChild(new QTreeWidgetItem(
logic_root, {fromUtf8(logic.name) + suffix}));
auto *item = new QTreeWidgetItem(
logic_root, {fromUtf8(logic.name) + suffix});
item->setData(0, kProjectTreeKindRole,
static_cast<int>(ProjectTreeItemKind::ControlLogic));
item->setData(0, kProjectTreeIdRole, fromUtf8(logic.id));
if (logic.id == current_logic_id_
&& ui_->editorTabWidget->currentIndex() == 1)
{
selected_tree_item = item;
}
}
ui_->projectTree->expandAll();
if (selected_tree_item != nullptr)
{
ui_->projectTree->setCurrentItem(selected_tree_item);
}
updateProjectTreeActions();
}

void MainWindow::handleProjectTreeSelection()
{
QTreeWidgetItem *item = ui_->projectTree->currentItem();
if (item == nullptr)
{
updateProjectTreeActions();
return;
}
const auto kind = static_cast<ProjectTreeItemKind>(
item->data(0, kProjectTreeKindRole).toInt());
const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString());
if (kind == ProjectTreeItemKind::HmiPage
&& hmi_editor_service_.findPage(id) != nullptr)
{
current_hmi_page_id_ = id;
selected_control_id_.clear();
hmi_editor_widget_->setPageId(id);
hmi_editor_widget_->reloadPage();
ui_->editorTabWidget->setCurrentWidget(ui_->hmiEditorTab);
showControlProperties({});
const HmiPage *page = hmi_editor_service_.findPage(id);
ui_->hmiPageTitleLabel->setText(fromUtf8(page->name));
ui_->hmiPageSizeLabel->setText(
tr("%1 x %2").arg(page->width).arg(page->height));
}
else if (kind == ProjectTreeItemKind::ControlLogic
&& logic_editor_service_.findLogic(id) != nullptr)
{
current_logic_id_ = id;
selected_logic_node_id_.clear();
logic_editor_widget_->setLogicId(id);
logic_editor_widget_->reloadLogic();
ui_->editorTabWidget->setCurrentWidget(ui_->logicEditorTab);
showLogicNodeProperties({});
}
updateProjectTreeActions();
}

void MainWindow::updateProjectTreeActions()
{
if (add_page_action_ == nullptr)
{
return;
}
QTreeWidgetItem *item = ui_->projectTree->currentItem();
const auto kind = item == nullptr ? ProjectTreeItemKind::Root
: static_cast<ProjectTreeItemKind>(
item->data(0, kProjectTreeKindRole).toInt());
const bool page_selected = kind == ProjectTreeItemKind::HmiPage;
const bool logic_selected = kind == ProjectTreeItemKind::ControlLogic;
const bool editable = runtime_mode_service_.policy().allowsProjectEditing;
add_page_action_->setEnabled(editable);
add_logic_action_->setEnabled(editable);
rename_project_item_action_->setEnabled(
editable && (page_selected || logic_selected));
delete_project_item_action_->setEnabled(
editable && (page_selected || logic_selected));
move_project_item_up_action_->setEnabled(
editable && (page_selected || logic_selected));
move_project_item_down_action_->setEnabled(
editable && (page_selected || logic_selected));
set_initial_page_action_->setEnabled(editable && page_selected);
toggle_logic_enabled_action_->setEnabled(editable && logic_selected);
if (logic_selected)
{
const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString());
const ControlLogic *logic = logic_editor_service_.findLogic(id);
toggle_logic_enabled_action_->setText(
logic != nullptr && logic->enabled
? tr("禁用控制逻辑") : tr("启用控制逻辑"));
}
}

void MainWindow::addPage()
{
const QString name = QInputDialog::getText(
this, tr("新建页面"), tr("页面名称"));
if (name.isEmpty())
{
return;
}
const HmiEditorResult result = hmi_editor_service_.addPage(toUtf8(name));
if (!result.succeeded)
{
showProjectResult(tr("新建页面"), fromUtf8(result.message), false);
return;
}
current_hmi_page_id_ = result.id;
selected_control_id_.clear();
ui_->editorTabWidget->setCurrentWidget(ui_->hmiEditorTab);
refreshProjectUi();
hmi_editor_widget_->reloadPage();
}

void MainWindow::addLogic()
{
const QString name = QInputDialog::getText(
this, tr("新建控制逻辑"), tr("逻辑名称"));
if (name.isEmpty())
{
return;
}
const LogicEditorResult result = logic_editor_service_.addLogic(toUtf8(name));
if (!result.succeeded)
{
showProjectResult(tr("新建控制逻辑"), fromUtf8(result.message), false);
return;
}
current_logic_id_ = result.id;
selected_logic_node_id_.clear();
ui_->editorTabWidget->setCurrentWidget(ui_->logicEditorTab);
refreshProjectUi();
logic_editor_widget_->reloadLogic();
}

void MainWindow::renameProjectItem()
{
QTreeWidgetItem *item = ui_->projectTree->currentItem();
if (item == nullptr)
{
return;
}
const auto kind = static_cast<ProjectTreeItemKind>(
item->data(0, kProjectTreeKindRole).toInt());
const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString());
QString current_name;
if (kind == ProjectTreeItemKind::HmiPage)
{
const HmiPage *page = hmi_editor_service_.findPage(id);
current_name = page == nullptr ? QString{} : fromUtf8(page->name);
}
else if (kind == ProjectTreeItemKind::ControlLogic)
{
const ControlLogic *logic = logic_editor_service_.findLogic(id);
current_name = logic == nullptr ? QString{} : fromUtf8(logic->name);
}
else
{
return;
}
bool accepted = false;
const QString name = QInputDialog::getText(
this, tr("重命名"), tr("新名称"), QLineEdit::Normal,
current_name, &accepted);
if (!accepted)
{
return;
}
if (kind == ProjectTreeItemKind::HmiPage)
{
const HmiEditorResult result = hmi_editor_service_.renamePage(id, toUtf8(name));
if (!result.succeeded)
{
showProjectResult(tr("重命名页面"), fromUtf8(result.message), false);
return;
}
}
else
{
const LogicEditorResult result = logic_editor_service_.renameLogic(id, toUtf8(name));
if (!result.succeeded)
{
showProjectResult(tr("重命名控制逻辑"), fromUtf8(result.message), false);
return;
}
}
refreshProjectUi();
}

void MainWindow::deleteProjectItem()
{
QTreeWidgetItem *item = ui_->projectTree->currentItem();
if (item == nullptr)
{
return;
}
const auto kind = static_cast<ProjectTreeItemKind>(
item->data(0, kProjectTreeKindRole).toInt());
const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString());
if (kind == ProjectTreeItemKind::HmiPage)
{
const HmiEditorResult result = hmi_editor_service_.removePage(id);
if (!result.succeeded)
{
showProjectResult(tr("删除页面"), fromUtf8(result.message), false);
return;
}
}
else if (kind == ProjectTreeItemKind::ControlLogic)
{
const LogicEditorResult result = logic_editor_service_.removeLogic(id);
if (!result.succeeded)
{
showProjectResult(tr("删除控制逻辑"), fromUtf8(result.message), false);
return;
}
}
else
{
return;
}
refreshProjectUi();
showControlProperties({});
showLogicNodeProperties({});
}

void MainWindow::moveProjectItem(int offset)
{
QTreeWidgetItem *item = ui_->projectTree->currentItem();
if (item == nullptr)
{
return;
}
const auto kind = static_cast<ProjectTreeItemKind>(
item->data(0, kProjectTreeKindRole).toInt());
const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString());
if (kind == ProjectTreeItemKind::HmiPage)
{
const HmiEditorResult result = hmi_editor_service_.movePage(id, offset);
if (!result.succeeded)
{
statusBar()->showMessage(fromUtf8(result.message), 4000);
return;
}
}
else if (kind == ProjectTreeItemKind::ControlLogic)
{
const LogicEditorResult result = logic_editor_service_.moveLogic(id, offset);
if (!result.succeeded)
{
statusBar()->showMessage(fromUtf8(result.message), 4000);
return;
}
}
refreshProjectUi();
}

void MainWindow::setSelectedPageAsInitial()
{
QTreeWidgetItem *item = ui_->projectTree->currentItem();
if (item == nullptr
|| static_cast<ProjectTreeItemKind>(
item->data(0, kProjectTreeKindRole).toInt())
!= ProjectTreeItemKind::HmiPage)
{
return;
}
const HmiEditorResult result = hmi_editor_service_.setInitialPage(
toUtf8(item->data(0, kProjectTreeIdRole).toString()));
if (!result.succeeded)
{
showProjectResult(tr("设置初始页面"), fromUtf8(result.message), false);
return;
}
refreshProjectUi();
}

void MainWindow::toggleSelectedLogicEnabled()
{
QTreeWidgetItem *item = ui_->projectTree->currentItem();
if (item == nullptr
|| static_cast<ProjectTreeItemKind>(
item->data(0, kProjectTreeKindRole).toInt())
!= ProjectTreeItemKind::ControlLogic)
{
return;
}
const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString());
const ControlLogic *logic = logic_editor_service_.findLogic(id);
if (logic == nullptr)
{
return;
}
const LogicEditorResult result = logic_editor_service_.setLogicEnabled(
id, !logic->enabled);
if (!result.succeeded)
{
showProjectResult(tr("启停控制逻辑"), fromUtf8(result.message), false);
return;
}
refreshProjectUi();
}

void MainWindow::connectPlc()
@@ -682,14 +1156,16 @@ void MainWindow::showControlProperties(const std::string &control_id)

// 在第一页画面中,根据ID查找目标控件,获取只读模型指针
const HmiControl *control = hmi_editor_service_.findControl(
hmi_editor_service_.firstPageId(), control_id);
current_hmi_page_id_, control_id);
// 判断是否查询到有效控件
const bool has_control = control != nullptr;

// 更新属性面板顶部状态标签,显示选中控件ID或提示未选择
ui_->selectionValueLabel->setText(
has_control ? fromUtf8(control->id)
+ (control->isConfigured() ? QString{} : tr("(未绑定)"))
+ (control->isConfigured() ? QString{}
: control->type == HmiControlType::PageJump
? tr("(未配置)") : tr("(未绑定)"))
: tr("未选择"));

// 批量控制所有属性编辑组件可用性:无选中控件时整体置灰,禁止编辑
@@ -701,6 +1177,7 @@ void MainWindow::showControlProperties(const std::string &control_id)
static_cast<QWidget *>(ui_->controlHeightSpinBox),
static_cast<QWidget *>(ui_->bindingAreaComboBox),
static_cast<QWidget *>(ui_->bindingIndexSpinBox),
static_cast<QWidget *>(ui_->targetPageComboBox),
static_cast<QWidget *>(ui_->buttonOperationComboBox),
static_cast<QWidget *>(ui_->applyPropertiesButton)})
{
@@ -710,6 +1187,12 @@ void MainWindow::showControlProperties(const std::string &control_id)
// 未选中有效控件,无需填充属性,直接退出
if (!has_control)
{
ui_->bindingAreaLabel->setVisible(false);
ui_->bindingAreaComboBox->setVisible(false);
ui_->bindingIndexLabel->setVisible(false);
ui_->bindingIndexSpinBox->setVisible(false);
ui_->targetPageLabel->setVisible(false);
ui_->targetPageComboBox->setVisible(false);
ui_->buttonOperationLabel->setVisible(false);
ui_->buttonOperationComboBox->setVisible(false);
return;
@@ -731,6 +1214,12 @@ void MainWindow::showControlProperties(const std::string &control_id)
: control->binding->area() == RegisterArea::M ? 1 : 2);
ui_->bindingIndexSpinBox->setValue(
control->binding.has_value() ? control->binding->index() : 0);
const bool has_binding = control->type != HmiControlType::Label
&& control->type != HmiControlType::PageJump;
ui_->bindingAreaLabel->setVisible(has_binding);
ui_->bindingAreaComboBox->setVisible(has_binding);
ui_->bindingIndexLabel->setVisible(has_binding);
ui_->bindingIndexSpinBox->setVisible(has_binding);
const bool is_button = control->type == HmiControlType::Button;
ui_->buttonOperationLabel->setVisible(is_button);
ui_->buttonOperationComboBox->setVisible(is_button);
@@ -738,6 +1227,23 @@ void MainWindow::showControlProperties(const std::string &control_id)
ui_->buttonOperationComboBox->setCurrentIndex(
ui_->buttonOperationComboBox->findData(
static_cast<int>(control->buttonOperation)));
const bool is_page_jump = control->type == HmiControlType::PageJump;
ui_->targetPageLabel->setVisible(is_page_jump);
ui_->targetPageComboBox->setVisible(is_page_jump);
ui_->targetPageComboBox->setEnabled(is_page_jump);
ui_->targetPageComboBox->clear();
if (is_page_jump)
{
for (const HmiPage &page : project_service_.project().hmiPages)
{
ui_->targetPageComboBox->addItem(
fromUtf8(page.name), fromUtf8(page.id));
}
const std::string target_id = control->pageJump.has_value()
? control->pageJump->targetPageId : std::string{};
ui_->targetPageComboBox->setCurrentIndex(
ui_->targetPageComboBox->findData(fromUtf8(target_id)));
}
}

void MainWindow::showLogicNodeProperties(const std::string &node_id)
@@ -745,7 +1251,7 @@ void MainWindow::showLogicNodeProperties(const std::string &node_id)
ui_->propertyStack->setCurrentWidget(ui_->logicPropertiesPage);
selected_logic_node_id_ = node_id;
const LogicNode *node = logic_editor_service_.findNode(
logic_editor_service_.firstLogicId(), node_id);
current_logic_id_, node_id);
const bool has_node = node != nullptr;
ui_->selectionValueLabel->setText(
has_node ? fromUtf8(node->id) : tr("未选择"));
@@ -827,8 +1333,8 @@ void MainWindow::showLogicNodeProperties(const std::string &node_id)

void MainWindow::addHmiControl(HmiControlType type)
{
const std::string page_id = hmi_editor_service_.firstPageId();
const HmiEditorResult result = hmi_editor_service_.addControl(page_id, type);
const HmiEditorResult result = hmi_editor_service_.addControl(
current_hmi_page_id_, type);
if (!result.succeeded)
{
showProjectResult(tr("添加控件"), fromUtf8(result.message), false);
@@ -848,7 +1354,7 @@ void MainWindow::deleteSelectedControl()
return;
}
const HmiEditorResult result = hmi_editor_service_.removeControl(
hmi_editor_service_.firstPageId(), selected_control_id_);
current_hmi_page_id_, selected_control_id_);
if (!result.succeeded)
{
showProjectResult(tr("删除控件"), fromUtf8(result.message), false);
@@ -863,7 +1369,7 @@ void MainWindow::deleteSelectedControl()
void MainWindow::applySelectedControlProperties()
{
const HmiControl *old_control = hmi_editor_service_.findControl(
hmi_editor_service_.firstPageId(), selected_control_id_);
current_hmi_page_id_, selected_control_id_);
if (old_control == nullptr)
{
return;
@@ -876,23 +1382,38 @@ void MainWindow::applySelectedControlProperties()
control.bounds.y = ui_->controlYSpinBox->value();
control.bounds.width = ui_->controlWidthSpinBox->value();
control.bounds.height = ui_->controlHeightSpinBox->value();
const int binding_area = ui_->bindingAreaComboBox->currentData().toInt();
if (binding_area < 0)
if (control.type == HmiControlType::Label
|| control.type == HmiControlType::PageJump)
{
control.binding.reset();
}
else
{
const RegisterArea area = binding_area == 0 ? RegisterArea::M : RegisterArea::D;
control.binding = RegisterAddress{area, ui_->bindingIndexSpinBox->value()};
const int binding_area = ui_->bindingAreaComboBox->currentData().toInt();
if (binding_area < 0)
{
control.binding.reset();
}
else
{
const RegisterArea area = binding_area == 0
? RegisterArea::M : RegisterArea::D;
control.binding = RegisterAddress{
area, ui_->bindingIndexSpinBox->value()};
}
}
if (control.type == HmiControlType::Button)
{
control.buttonOperation = static_cast<HmiButtonOperation>(
ui_->buttonOperationComboBox->currentData().toInt());
}
if (control.type == HmiControlType::PageJump)
{
control.pageJump = HmiPageJumpConfig{
toUtf8(ui_->targetPageComboBox->currentData().toString())};
}
const HmiEditorResult result = hmi_editor_service_.updateControl(
hmi_editor_service_.firstPageId(), selected_control_id_, control);
current_hmi_page_id_, selected_control_id_, control);
if (!result.succeeded)
{
showProjectResult(tr("应用属性"), fromUtf8(result.message), false);
@@ -975,9 +1496,8 @@ void MainWindow::deleteSelectedLogicObject()

void MainWindow::applySelectedLogicNodeProperties()
{
const std::string logic_id = logic_editor_service_.firstLogicId();
const LogicNode *node = logic_editor_service_.findNode(
logic_id, selected_logic_node_id_);
current_logic_id_, selected_logic_node_id_);
if (node == nullptr)
{
return;
@@ -1006,7 +1526,7 @@ void MainWindow::applySelectedLogicNodeProperties()
}

const LogicEditorResult result = logic_editor_service_.updateNodeConfig(
logic_id, selected_logic_node_id_, config);
current_logic_id_, selected_logic_node_id_, config);
if (!result.succeeded)
{
showProjectResult(tr("应用逻辑属性"), fromUtf8(result.message), false);
@@ -1037,6 +1557,8 @@ void MainWindow::createNewProject()
logic_editor_service_.ensureDefaultLogic();
selected_control_id_.clear();
selected_logic_node_id_.clear();
current_hmi_page_id_ = project_service_.project().initialHmiPageId;
current_logic_id_ = logic_editor_service_.firstLogicId();
refreshProjectUi();
hmi_editor_widget_->reloadPage();
logic_editor_widget_->reloadLogic();
@@ -1086,6 +1608,8 @@ void MainWindow::loadProject()
}
selected_control_id_.clear();
selected_logic_node_id_.clear();
current_hmi_page_id_ = project_service_.project().initialHmiPageId;
current_logic_id_ = logic_editor_service_.firstLogicId();
refreshProjectUi();
hmi_editor_widget_->reloadPage();
logic_editor_widget_->reloadLogic();
@@ -1165,14 +1689,28 @@ void MainWindow::updateModeUi(const QString &message)
ui_->editorTabWidget->setTabVisible(2, running);
if (running)
{
if (hmi_navigation_service_->currentPageId().empty())
{
const HmiNavigationResult navigation = hmi_navigation_service_->start();
if (!navigation.succeeded)
{
statusBar()->showMessage(fromUtf8(navigation.message), 5000);
}
}
runtime_monitor_widget_->setProjectObjects(
hmi_editor_service_.firstPageId(), logic_editor_service_.firstLogicId());
hmi_navigation_service_->currentPageId(), current_logic_id_);
runtime_monitor_widget_->setMode(mode, runtime_mode_service_.plcConnectionState());
ui_->editorTabWidget->setCurrentWidget(ui_->runtimeMonitorTab);
}
else if (ui_->editorTabWidget->currentWidget() == ui_->runtimeMonitorTab)
else
{
ui_->editorTabWidget->setCurrentIndex(last_editing_tab_index_);
hmi_navigation_service_->stop();
runtime_monitor_widget_->setMode(
mode, runtime_mode_service_.plcConnectionState());
if (ui_->editorTabWidget->currentWidget() == ui_->runtimeMonitorTab)
{
ui_->editorTabWidget->setCurrentIndex(last_editing_tab_index_);
}
}
// 将同一份模式策略同步到所有可编辑入口,避免只禁用部分操作
ui_->projectDock->setEnabled(policy.allowsProjectEditing);
@@ -1191,6 +1729,8 @@ void MainWindow::updateModeUi(const QString &message)
ui_->addIndicatorAction->setEnabled(policy.allowsProjectEditing);
ui_->addNumericDisplayAction->setEnabled(policy.allowsProjectEditing);
ui_->addNumericInputAction->setEnabled(policy.allowsProjectEditing);
ui_->addLabelAction->setEnabled(policy.allowsProjectEditing);
ui_->addPageJumpAction->setEnabled(policy.allowsProjectEditing);
ui_->deleteControlAction->setEnabled(policy.allowsProjectEditing);
ui_->addNormallyOpenAction->setEnabled(policy.allowsProjectEditing);
ui_->addNormallyClosedAction->setEnabled(policy.allowsProjectEditing);
@@ -1203,6 +1743,7 @@ void MainWindow::updateModeUi(const QString &message)
ui_->saveProjectAction->setEnabled(policy.allowsProjectEditing);
ui_->saveAsProjectAction->setEnabled(policy.allowsProjectEditing);
ui_->loadProjectAction->setEnabled(policy.allowsProjectEditing);
updateProjectTreeActions();

mode_status_label_->setText(modeText(mode));
if (mode == ApplicationMode::Editing)
@@ -1300,8 +1841,17 @@ void MainWindow::updateSimulationUi(bool report_fault)
if (report_fault)
{
const LogicScanResult &error = runtime_mode_service_.simulationError();
if (!error.logicId.empty()
&& logic_editor_service_.findLogic(error.logicId) != nullptr)
{
current_logic_id_ = error.logicId;
logic_editor_widget_->setLogicId(current_logic_id_);
runtime_monitor_widget_->setProjectObjects(
hmi_navigation_service_->currentPageId(), current_logic_id_);
}
logic_editor_widget_->setRuntimeTrace(
runtime_mode_service_.offlineSimulationService().traceSnapshot(),
runtime_mode_service_.offlineSimulationService()
.traceSnapshot().forLogic(current_logic_id_),
error.nodeId);
runtime_monitor_widget_->setLogicTrace(
runtime_mode_service_.offlineSimulationService().traceSnapshot(),


+ 37
- 0
app/src/ui/main_window.h Datei anzeigen

@@ -18,6 +18,7 @@
#include <memory>

QT_BEGIN_NAMESPACE
class QAction;
class QActionGroup;
class QLabel;
class QTimer;
@@ -31,6 +32,7 @@ class RuntimeModeService;
class ProjectService;
class HmiEditorService;
class HmiRuntimeService;
class HmiNavigationService;
class HmiEditorWidget;
class LogicEditorService;
class LogicEditorWidget;
@@ -63,9 +65,21 @@ public:
HmiRuntimeService &hmi_runtime_service,
RegisterMonitorService &register_monitor_service,
QWidget *parent = nullptr);
MainWindow(
RuntimeModeService &runtime_mode_service,
ProjectService &project_service,
HmiEditorService &hmi_editor_service,
LogicEditorService &logic_editor_service,
HmiRuntimeService &hmi_runtime_service,
HmiNavigationService &hmi_navigation_service,
RegisterMonitorService &register_monitor_service,
QWidget *parent = nullptr);

~MainWindow() override;

const std::string &currentHmiPageId() const;
const std::string &currentLogicId() const;

private:
// 配置菜单和工具栏动作
void configureActions();
@@ -76,11 +90,21 @@ private:
// 创建并连接控制逻辑编辑画布
void configureLogicEditor();
void configureRuntimeMonitor();
void configureProjectTree();
// 创建并连接控件属性编辑表单
void configurePropertyEditor();
void configurePlcConnection();
// 刷新工程树和当前 HMI 页面信息
void refreshProjectUi();
void handleProjectTreeSelection();
void updateProjectTreeActions();
void addPage();
void addLogic();
void renameProjectItem();
void deleteProjectItem();
void moveProjectItem(int offset);
void setSelectedPageAsInitial();
void toggleSelectedLogicEnabled();
// 显示指定 HMI 控件的可编辑属性
void showControlProperties(const std::string &control_id);
// 显示指定控制逻辑节点的可编辑属性
@@ -138,6 +162,7 @@ private:
* @brief 将互斥模式动作同步到服务层当前状态
*/
void restoreCurrentModeAction();
void initializeUi();

std::unique_ptr<Ui::MainWindow> ui_;
/**
@@ -152,6 +177,8 @@ private:
LogicEditorService &logic_editor_service_;
HmiRuntimeService &hmi_runtime_service_;
RegisterMonitorService &register_monitor_service_;
std::unique_ptr<HmiNavigationService> owned_hmi_navigation_service_;
HmiNavigationService *hmi_navigation_service_ = nullptr;
QActionGroup *mode_action_group_ = nullptr;
HmiEditorWidget *hmi_editor_widget_ = nullptr;
LogicEditorWidget *logic_editor_widget_ = nullptr;
@@ -160,9 +187,19 @@ private:
QLabel *mode_status_label_ = nullptr;
QLabel *register_status_label_ = nullptr;
QLabel *executor_status_label_ = nullptr;
QAction *add_page_action_ = nullptr;
QAction *add_logic_action_ = nullptr;
QAction *rename_project_item_action_ = nullptr;
QAction *delete_project_item_action_ = nullptr;
QAction *move_project_item_up_action_ = nullptr;
QAction *move_project_item_down_action_ = nullptr;
QAction *set_initial_page_action_ = nullptr;
QAction *toggle_logic_enabled_action_ = nullptr;
PlcSerialConfiguration plc_configuration_;
std::string selected_control_id_;
std::string selected_logic_node_id_;
std::string current_hmi_page_id_;
std::string current_logic_id_;
bool plc_status_update_pending_ = false;
int last_editing_tab_index_ = 0;
};

+ 31
- 3
app/src/ui/main_window.ui Datei anzeigen

@@ -302,6 +302,8 @@
<addaction name="addIndicatorAction"/>
<addaction name="addNumericDisplayAction"/>
<addaction name="addNumericInputAction"/>
<addaction name="addLabelAction"/>
<addaction name="addPageJumpAction"/>
<addaction name="deleteControlAction"/>
</widget>
<widget class="QToolBar" name="logicToolBar">
@@ -575,14 +577,24 @@
</property>
</widget>
</item>
<item row="8" column="0">
<item row="8" column="0">
<widget class="QLabel" name="targetPageLabel">
<property name="text">
<string>目标页面</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QComboBox" name="targetPageComboBox"/>
</item>
<item row="9" column="0">
<widget class="QLabel" name="buttonOperationLabel">
<property name="text">
<string>按钮操作</string>
</property>
</widget>
</item>
<item row="8" column="1">
<item row="9" column="1">
<widget class="QComboBox" name="buttonOperationComboBox">
<item>
<property name="text">
@@ -606,7 +618,7 @@
</item>
</widget>
</item>
<item row="9" column="0" colspan="2">
<item row="10" column="0" colspan="2">
<widget class="QPushButton" name="applyPropertiesButton">
<property name="text">
<string>应用属性</string>
@@ -874,6 +886,22 @@
<string>添加数值输入控件</string>
</property>
</action>
<action name="addLabelAction">
<property name="text">
<string>文本</string>
</property>
<property name="toolTip">
<string>添加固定文本控件</string>
</property>
</action>
<action name="addPageJumpAction">
<property name="text">
<string>页面跳转</string>
</property>
<property name="toolTip">
<string>添加页面跳转控件</string>
</property>
</action>
<action name="deleteControlAction">
<property name="text">
<string>删除</string>


+ 110
- 8
app/src/ui/runtime_monitor_widget.cpp Datei anzeigen

@@ -3,18 +3,43 @@
#include "free_monitor_widget.h"
#include "hmi_editor_widget.h"
#include "logic_editor_widget.h"
#include "services/hmi_navigation_service.h"
#include "services/project_service.h"
#include "ui_runtime_monitor_widget.h"

#include <QSplitter>
#include <QComboBox>
#include <QSignalBlocker>

#include <algorithm>

namespace {

QString fromUtf8(const std::string &value)
{
return QString::fromUtf8(value.data(), static_cast<int>(value.size()));
}

std::string toUtf8(const QString &value)
{
const QByteArray bytes = value.toUtf8();
return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size()));
}

} // namespace

RuntimeMonitorWidget::RuntimeMonitorWidget(
HmiEditorService &hmi_editor_service,
HmiRuntimeService &hmi_runtime_service,
LogicEditorService &logic_editor_service,
ProjectService &project_service,
HmiNavigationService &hmi_navigation_service,
RegisterMonitorService &register_monitor_service,
QWidget *parent)
: QWidget(parent),
ui_(std::make_unique<Ui::RuntimeMonitorWidget>())
ui_(std::make_unique<Ui::RuntimeMonitorWidget>()),
project_service_(project_service),
hmi_navigation_service_(hmi_navigation_service)
{
ui_->setupUi(this);
hmi_view_ = new HmiEditorWidget(
@@ -35,6 +60,31 @@ RuntimeMonitorWidget::RuntimeMonitorWidget(
ui_->monitorContainerLayout->addWidget(free_monitor_widget_);
ui_->upperSplitter->setSizes({600, 600});
ui_->verticalSplitter->setSizes({440, 260});
connect(hmi_view_, &HmiEditorWidget::pageNavigationRequested,
this,
[this](const QString &target_page_id)
{
const HmiNavigationResult result =
hmi_navigation_service_.navigateTo(toUtf8(target_page_id));
if (!result.succeeded)
{
emit navigationFailed(fromUtf8(result.message));
return;
}
showRuntimePage(result.pageId);
emit runtimePageChanged(fromUtf8(result.pageId));
});
connect(ui_->runtimeLogicComboBox,
QOverload<int>::of(&QComboBox::currentIndexChanged),
this,
[this](int index)
{
if (index >= 0)
{
selectRuntimeLogic(toUtf8(
ui_->runtimeLogicComboBox->itemData(index).toString()));
}
});
}

RuntimeMonitorWidget::~RuntimeMonitorWidget() = default;
@@ -42,12 +92,27 @@ RuntimeMonitorWidget::~RuntimeMonitorWidget() = default;
void RuntimeMonitorWidget::setProjectObjects(
const std::string &page_id, const std::string &logic_id)
{
hmi_view_->setPageId(page_id);
// 运行视图是独立投影,同一页面的控件也可能已在编辑视图中发生变化
hmi_view_->reloadPage();
logic_view_->setLogicId(logic_id);
// 同一逻辑 ID 下的网络结构变化同样需要在首次扫描前立即可见
logic_view_->reloadLogic();
showRuntimePage(page_id);

const QSignalBlocker blocker(ui_->runtimeLogicComboBox);
ui_->runtimeLogicComboBox->clear();
for (const ControlLogic &logic : project_service_.project().controlLogics)
{
if (logic.enabled)
{
ui_->runtimeLogicComboBox->addItem(
fromUtf8(logic.name), fromUtf8(logic.id));
}
}
int selected_index = ui_->runtimeLogicComboBox->findData(fromUtf8(logic_id));
if (selected_index < 0 && ui_->runtimeLogicComboBox->count() > 0)
{
selected_index = 0;
}
ui_->runtimeLogicComboBox->setCurrentIndex(selected_index);
selectRuntimeLogic(selected_index >= 0
? toUtf8(ui_->runtimeLogicComboBox->itemData(selected_index).toString())
: std::string{});
}

void RuntimeMonitorWidget::setMode(
@@ -62,6 +127,9 @@ void RuntimeMonitorWidget::setMode(
if (!offline)
{
logic_view_->clearRuntimeTrace();
latest_trace_.clear();
latest_fault_node_id_.clear();
has_logic_trace_ = false;
}
ui_->modeLabel->setText(
offline ? tr("离线仿真 · 虚拟 M/D")
@@ -84,10 +152,44 @@ void RuntimeMonitorWidget::refreshValues(
void RuntimeMonitorWidget::setLogicTrace(
const LogicTraceSnapshot &trace, const std::string &fault_node_id)
{
logic_view_->setRuntimeTrace(trace, fault_node_id);
latest_trace_ = trace;
latest_fault_node_id_ = fault_node_id;
has_logic_trace_ = true;
logic_view_->setRuntimeTrace(
trace.forLogic(selected_logic_id_), fault_node_id);
}

FreeMonitorWidget *RuntimeMonitorWidget::freeMonitorWidget() const
{
return free_monitor_widget_;
}

std::string RuntimeMonitorWidget::selectedLogicId() const
{
return selected_logic_id_;
}

void RuntimeMonitorWidget::showRuntimePage(const std::string &page_id)
{
hmi_view_->setPageId(page_id);
hmi_view_->reloadPage();
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 RuntimeMonitorWidget::selectRuntimeLogic(const std::string &logic_id)
{
selected_logic_id_ = logic_id;
logic_view_->setLogicId(logic_id);
logic_view_->reloadLogic();
if (has_logic_trace_)
{
logic_view_->setRuntimeTrace(
latest_trace_.forLogic(logic_id), latest_fault_node_id_);
}
}

+ 19
- 1
app/src/ui/runtime_monitor_widget.h Datei anzeigen

@@ -2,6 +2,7 @@

#include "domain/runtime_state.h"
#include "services/plc_communication_gateway.h"
#include "services/software_logic_executor.h"

#include <QWidget>

@@ -16,10 +17,11 @@ class FreeMonitorWidget;
class HmiEditorService;
class HmiEditorWidget;
class HmiRuntimeService;
class HmiNavigationService;
class LogicEditorService;
class LogicEditorWidget;
class RegisterMonitorService;
struct LogicTraceSnapshot;
class ProjectService;

class RuntimeMonitorWidget final : public QWidget
{
@@ -30,6 +32,8 @@ public:
HmiEditorService &hmi_editor_service,
HmiRuntimeService &hmi_runtime_service,
LogicEditorService &logic_editor_service,
ProjectService &project_service,
HmiNavigationService &hmi_navigation_service,
RegisterMonitorService &register_monitor_service,
QWidget *parent = nullptr);
~RuntimeMonitorWidget() override;
@@ -40,10 +44,24 @@ public:
void setHmiWriteEnabled(bool enabled);
void setLogicTrace(const LogicTraceSnapshot &trace, const std::string &fault_node_id = {});
FreeMonitorWidget *freeMonitorWidget() const;
std::string selectedLogicId() const;

signals:
void navigationFailed(const QString &message);
void runtimePageChanged(const QString &page_id);

private:
void showRuntimePage(const std::string &page_id);
void selectRuntimeLogic(const std::string &logic_id);

std::unique_ptr<Ui::RuntimeMonitorWidget> ui_;
ProjectService &project_service_;
HmiNavigationService &hmi_navigation_service_;
HmiEditorWidget *hmi_view_ = nullptr;
LogicEditorWidget *logic_view_ = nullptr;
FreeMonitorWidget *free_monitor_widget_ = nullptr;
std::string selected_logic_id_;
LogicTraceSnapshot latest_trace_;
std::string latest_fault_node_id_;
bool has_logic_trace_ = false;
};

+ 14
- 2
app/src/ui/runtime_monitor_widget.ui Datei anzeigen

@@ -24,7 +24,13 @@
<property name="frameShape"><enum>QFrame::StyledPanel</enum></property>
<layout class="QVBoxLayout" name="hmiLayout">
<property name="leftMargin"><number>4</number></property><property name="topMargin"><number>4</number></property><property name="rightMargin"><number>4</number></property><property name="bottomMargin"><number>4</number></property>
<item><widget class="QLabel" name="hmiLabel"><property name="text"><string>HMI 运行视图</string></property></widget></item>
<item>
<layout class="QHBoxLayout" name="hmiHeaderLayout">
<item><widget class="QLabel" name="hmiLabel"><property name="text"><string>HMI 运行视图</string></property></widget></item>
<item><spacer name="hmiHeaderSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>24</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>
</layout>
</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>
@@ -32,7 +38,13 @@
<property name="frameShape"><enum>QFrame::StyledPanel</enum></property>
<layout class="QVBoxLayout" name="logicLayout">
<property name="leftMargin"><number>4</number></property><property name="topMargin"><number>4</number></property><property name="rightMargin"><number>4</number></property><property name="bottomMargin"><number>4</number></property>
<item><widget class="QLabel" name="logicLabel"><property name="text"><string>梯形图运行状态</string></property></widget></item>
<item>
<layout class="QHBoxLayout" name="logicHeaderLayout">
<item><widget class="QLabel" name="logicLabel"><property name="text"><string>梯形图运行状态</string></property></widget></item>
<item><spacer name="logicHeaderSpacer"><property name="orientation"><enum>Qt::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>24</width><height>20</height></size></property></spacer></item>
<item><widget class="QComboBox" name="runtimeLogicComboBox"><property name="minimumSize"><size><width>150</width><height>0</height></size></property></widget></item>
</layout>
</item>
<item><widget class="QWidget" name="logicViewContainer" native="true"><layout class="QVBoxLayout" name="logicViewLayout"><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>


+ 174
- 0
app/tests/main_window_tests.cpp Datei anzeigen

@@ -3,6 +3,7 @@
#include "domain/register_repository.h"
#include "services/hmi_editor_service.h"
#include "services/hmi_runtime_service.h"
#include "services/hmi_navigation_service.h"
#include "services/logic_editor_service.h"
#include "services/offline_simulation_service.h"
#include "services/project_service.h"
@@ -13,6 +14,7 @@
#include "ui/logic_editor_widget.h"
#include "ui/main_window.h"
#include "ui/plc_connection_dialog.h"
#include "ui/runtime_monitor_widget.h"

#include <QAction>
#include <QApplication>
@@ -31,6 +33,7 @@
#include <QTableWidget>
#include <QTimer>
#include <QTest>
#include <QTreeWidget>

#include <iostream>
#include <stdexcept>
@@ -341,6 +344,56 @@ void testRuntimeButtonMouseInteraction()
"a disabled runtime button must not write its register");
}

void testRuntimePageJumpDoesNotRequireRegisterWritePermission()
{
TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService editor_service(project_service);
const std::string source_page = editor_service.ensureDefaultPage().id;
const std::string target_page = editor_service.addPage("Target").id;
const HmiEditorResult jump_result = editor_service.addControl(
source_page, HmiControlType::PageJump);
HmiControl jump = *editor_service.findControl(source_page, jump_result.id);
jump.pageJump = HmiPageJumpConfig{target_page};
require(editor_service.updateControl(
source_page, jump_result.id, jump).succeeded,
"PageJump fixture setup must succeed");

VirtualRegisterRepository repository;
HmiRuntimeService runtime_service(repository);
HmiEditorWidget view(editor_service, runtime_service);
view.setPageId(source_page);
view.setEditingEnabled(false);
view.setRuntimeActive(true);
view.setRuntimeWriteEnabled(false);
view.resize(900, 560);
view.show();
QApplication::processEvents();

QString requested_page;
QObject::connect(
&view, &HmiEditorWidget::pageNavigationRequested,
[&requested_page](const QString &page_id)
{
requested_page = page_id;
});
QGraphicsItem *jump_item = nullptr;
for (QGraphicsItem *item : view.scene()->items())
{
if (item->zValue() >= 0.0)
{
jump_item = item;
break;
}
}
require(jump_item != nullptr, "PageJump graphics item must be rendered");
const QPoint center = view.mapFromScene(
jump_item->mapToScene(jump_item->boundingRect().center()));
QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, center);
require(requested_page == QString::fromStdString(target_page),
"PageJump must remain active when register writes are disabled");
}

void testModeActionsControlEditingAvailability()
{
// 验证模式动作会同步禁用编辑入口,并在失败时恢复当前选择
@@ -776,6 +829,125 @@ void testOnlineWorkspaceShowsHmiAndFreeMonitorOnly()
"faulted PLC state must not re-enter online running without a fresh read");
}

void testMultiPageAndLogicMainWindowIntegration()
{
TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService editor_service(project_service);
LogicEditorService logic_editor_service(project_service);
const std::string main_page_id = editor_service.ensureDefaultPage().id;
const std::string settings_page_id = editor_service.addPage("Settings").id;
const std::string first_logic_id = logic_editor_service.ensureDefaultLogic().id;
const std::string second_logic_id =
logic_editor_service.addLogic("Safety logic").id;

const HmiEditorResult jump_result = editor_service.addControl(
main_page_id, HmiControlType::PageJump);
HmiControl jump = *editor_service.findControl(main_page_id, jump_result.id);
jump.pageJump = HmiPageJumpConfig{settings_page_id};
require(editor_service.updateControl(
main_page_id, jump_result.id, jump).succeeded,
"the integration fixture must configure PageJump");

VirtualRegisterRepository repository;
HmiRuntimeService runtime_service(repository);
OfflineSimulationService simulation_service(repository);
RuntimeModeService mode_service(project_service, simulation_service);
RegisterMonitorService monitor_service(repository);
HmiNavigationService navigation_service(project_service);
MainWindow window(
mode_service,
project_service,
editor_service,
logic_editor_service,
runtime_service,
navigation_service,
monitor_service);
window.resize(1400, 820);
window.show();
QApplication::processEvents();

QTreeWidget *tree = requiredChild<QTreeWidget>(window, "projectTree");
require(tree->topLevelItemCount() == 2
&& tree->topLevelItem(0)->childCount() == 2
&& tree->topLevelItem(1)->childCount() == 2,
"the project tree must list every page and logic module");

tree->setCurrentItem(tree->topLevelItem(0)->child(1));
QApplication::processEvents();
require(window.currentHmiPageId() == settings_page_id,
"selecting a page tree node must change the current HMI page id");
QAction *add_label = requiredChild<QAction>(window, "addLabelAction");
add_label->trigger();
require(editor_service.findPage(settings_page_id)->controls.size() == 1U
&& editor_service.findPage(main_page_id)->controls.size() == 1U,
"HMI toolbar actions must edit the selected page rather than the first page");
QComboBox *binding_area = requiredChild<QComboBox>(window, "bindingAreaComboBox");
QComboBox *target_page = requiredChild<QComboBox>(window, "targetPageComboBox");
require(!binding_area->isVisible() && !target_page->isVisible(),
"Label properties must hide both register binding and PageJump target fields");

tree->setCurrentItem(tree->topLevelItem(1)->child(1));
QApplication::processEvents();
require(window.currentLogicId() == second_logic_id,
"selecting a logic tree node must change the current logic id");
requiredChild<QAction>(window, "addRungAction")->trigger();
require(logic_editor_service.findLogic(second_logic_id)->rungs.size() == 2U
&& logic_editor_service.findLogic(first_logic_id)->rungs.size() == 1U,
"logic toolbar actions must edit the selected logic module");

tree->setCurrentItem(tree->topLevelItem(0)->child(0));
QApplication::processEvents();
HmiEditorWidget *editor = requiredChild<HmiEditorWidget>(window, "hmiEditorWidget");
editor->selectControl(jump_result.id);
QApplication::processEvents();
require(target_page->isVisible() && !binding_area->isVisible()
&& target_page->currentData().toString()
== QString::fromStdString(settings_page_id),
"PageJump properties must show a target-page selector without register fields");

requiredChild<QAction>(window, "offlineModeAction")->trigger();
QApplication::processEvents();
require(mode_service.mode() == ApplicationMode::OfflineRunning
&& navigation_service.currentPageId() == main_page_id,
"runtime HMI navigation must start on the persisted initial page");
HmiEditorWidget *runtime_hmi = requiredChild<HmiEditorWidget>(
window, "runtimeHmiView");
QGraphicsItem *jump_item = nullptr;
for (QGraphicsItem *item : runtime_hmi->scene()->items())
{
if (item->zValue() >= 0.0)
{
jump_item = item;
break;
}
}
require(jump_item != nullptr, "runtime HMI must render the PageJump control");
const QPoint jump_center = runtime_hmi->mapFromScene(
jump_item->mapToScene(jump_item->boundingRect().center()));
QTest::mouseClick(runtime_hmi->viewport(), Qt::LeftButton,
Qt::NoModifier, jump_center);
QApplication::processEvents();
require(navigation_service.currentPageId() == settings_page_id
&& requiredChild<QLabel>(window, "runtimePageLabel")->text()
== QStringLiteral("Settings"),
"clicking PageJump at runtime must navigate locally to its target page");

QComboBox *logic_selector = requiredChild<QComboBox>(
window, "runtimeLogicComboBox");
require(logic_selector->count() == 2,
"runtime logic selector must list all enabled logic modules");
logic_selector->setCurrentIndex(
logic_selector->findData(QString::fromStdString(second_logic_id)));
QApplication::processEvents();
auto *runtime_monitor = requiredChild<RuntimeMonitorWidget>(
window, "runtimeMonitorWidget");
require(runtime_monitor->selectedLogicId() == second_logic_id,
"runtime logic selection must change only the visible trace projection");

requiredChild<QAction>(window, "editingModeAction")->trigger();
}

} // namespace

int main(int argc, char *argv[])
@@ -787,10 +959,12 @@ int main(int argc, char *argv[])
try
{
testRuntimeButtonMouseInteraction();
testRuntimePageJumpDoesNotRequireRegisterWritePermission();
testModeActionsControlEditingAvailability();
testPlcConfigurationUsesDialog();
testRepeatedPlcStatusNotificationsAreCoalesced();
testOnlineWorkspaceShowsHmiAndFreeMonitorOnly();
testMultiPageAndLogicMainWindowIntegration();
}
catch (const std::exception &error)
{


+ 2
- 0
app/tests/main_window_tests.pro Datei anzeigen

@@ -26,6 +26,7 @@ SOURCES += \
../src/domain/runtime_state.cpp \
../src/services/project_service.cpp \
../src/services/hmi_editor_service.cpp \
../src/services/hmi_navigation_service.cpp \
../src/services/logic_editor_service.cpp \
../src/services/hmi_runtime_service.cpp \
../src/services/software_logic_executor.cpp \
@@ -51,6 +52,7 @@ HEADERS += \
../src/domain/runtime_state.h \
../src/services/project_service.h \
../src/services/hmi_editor_service.h \
../src/services/hmi_navigation_service.h \
../src/services/logic_editor_service.h \
../src/services/hmi_runtime_service.h \
../src/services/software_logic_executor.h \


Laden…
Abbrechen
Speichern