#include "main_window.h" #include "hmi_editor_widget.h" #include "alarm_configuration_dialog.h" #include "logic_editor_widget.h" #include "plc_connection_dialog.h" #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/alarm_editor_service.h" #include "services/alarm_service.h" #include "services/logic_editor_service.h" #include "services/project_service.h" #include "services/runtime_mode_service.h" #include "services/register_monitor_service.h" #include "ui_main_window.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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) { case ApplicationMode::Editing: { return MainWindow::tr("编辑态"); } case ApplicationMode::OfflineRunning: { return MainWindow::tr("离线运行态"); } case ApplicationMode::OnlineRunning: { return MainWindow::tr("真机运行态"); } default: { return MainWindow::tr("未知状态"); } } } QString transitionErrorText(ModeTransitionError error) { switch (error) { case ModeTransitionError::MustReturnToEditing: { return MainWindow::tr("请先返回编辑态,再切换运行模式"); } case ModeTransitionError::InitialPlcReadRequired: { return MainWindow::tr("真机运行前必须连接 PLC 并完成首次读取"); } case ModeTransitionError::AlreadyInRequestedMode: { return MainWindow::tr("当前已处于所选模式"); } case ModeTransitionError::ProjectNotReady: { return MainWindow::tr("工程存在未配置或未完成的 HMI/梯形图节点,暂不能进入离线运行态"); } case ModeTransitionError::SimulationStartFailed: { return MainWindow::tr("离线逻辑执行器预检或启动失败"); } case ModeTransitionError::None: default: { return MainWindow::tr("模式切换失败"); } } } std::string toUtf8(const QString &value) { const QByteArray bytes = value.toUtf8(); return std::string(bytes.constData(), static_cast(bytes.size())); } QString fromUtf8(const std::string &value) { return QString::fromUtf8(value.data(), static_cast(value.size())); } QString plcStatusText(const RuntimeModeService &service) { switch (service.plcConnectionState()) { case PlcConnectionState::Connecting: return MainWindow::tr("PLC 正在连接"); case PlcConnectionState::Connected: return service.initialPlcReadCompleted() ? MainWindow::tr("PLC 已连接,首次读取完成") : MainWindow::tr("PLC 已连接,正在读取工程使用的 M/D 地址"); case PlcConnectionState::Recovering: return MainWindow::tr("PLC 通信已恢复,正在重新读取工程使用的 M/D 地址"); case PlcConnectionState::Faulted: { const QString error = fromUtf8(service.plcError()); return error.isEmpty() ? MainWindow::tr("PLC 通信故障") : MainWindow::tr("PLC 通信故障:%1").arg(error); } case PlcConnectionState::Disconnected: { const QString error = fromUtf8(service.plcError()); return error.isEmpty() ? MainWindow::tr("PLC 已断开") : MainWindow::tr("PLC 已断开:%1").arg(error); } default: { return MainWindow::tr("PLC 已断开"); } } } QString controlTypeText(HmiControlType type) { switch (type) { case HmiControlType::Button: { return MainWindow::tr("按钮"); } case HmiControlType::Indicator: { return MainWindow::tr("指示灯"); } case HmiControlType::NumericDisplay: { return MainWindow::tr("数值显示"); } case HmiControlType::NumericInput: { return MainWindow::tr("数值输入"); } case HmiControlType::Label: { return MainWindow::tr("文本"); } case HmiControlType::PageJump: { return MainWindow::tr("页面跳转"); } case HmiControlType::AlarmList: { return MainWindow::tr("报警列表"); } default: { return MainWindow::tr("未知控件"); } } } } // namespace MainWindow::MainWindow( RuntimeModeService &runtime_mode_service, ProjectService &project_service, HmiEditorService &hmi_editor_service, LogicEditorService &logic_editor_service, HmiRuntimeService &hmi_runtime_service, AlarmEditorService &alarm_editor_service, AlarmService &alarm_service, RegisterMonitorService ®ister_monitor_service, QWidget *parent) : QMainWindow(parent), ui_(std::make_unique()), 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), alarm_editor_service_(alarm_editor_service), alarm_service_(alarm_service), register_monitor_service_(register_monitor_service), owned_hmi_navigation_service_( std::make_unique(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, AlarmEditorService &alarm_editor_service, AlarmService &alarm_service, HmiNavigationService &hmi_navigation_service, RegisterMonitorService ®ister_monitor_service, QWidget *parent) : QMainWindow(parent), ui_(std::make_unique()), 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), alarm_editor_service_(alarm_editor_service), alarm_service_(alarm_service), register_monitor_service_(register_monitor_service), hmi_navigation_service_(&hmi_navigation_service) { initializeUi(); } void MainWindow::initializeUi() { ui_->setupUi(this); configureAppearance(); configureActions(); configurePropertyEditor(); configurePlcConnection(); configureAlarms(); 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("系统已进入编辑态")); } 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); mode_action_group_->setExclusive(true); mode_action_group_->addAction(ui_->editingModeAction); mode_action_group_->addAction(ui_->offlineModeAction); mode_action_group_->addAction(ui_->onlineModeAction); connect(ui_->editingModeAction, &QAction::triggered, this, [this] { requestMode(ApplicationMode::Editing); }); connect(ui_->offlineModeAction, &QAction::triggered, this, [this] { requestMode(ApplicationMode::OfflineRunning); }); connect(ui_->onlineModeAction, &QAction::triggered, this, [this] { requestMode(ApplicationMode::OnlineRunning); }); connect(ui_->exitAction, &QAction::triggered, qApp, &QApplication::closeAllWindows); connect(ui_->newProjectAction, &QAction::triggered, this, &MainWindow::createNewProject); connect(ui_->saveProjectAction, &QAction::triggered, this, &MainWindow::saveProject); connect(ui_->saveAsProjectAction, &QAction::triggered, this, &MainWindow::saveProjectAs); connect(ui_->loadProjectAction, &QAction::triggered, this, &MainWindow::loadProject); connect(ui_->addButtonAction, &QAction::triggered, this, [this] { addHmiControl(HmiControlType::Button); }); connect(ui_->addIndicatorAction, &QAction::triggered, this, [this] { addHmiControl(HmiControlType::Indicator); }); connect(ui_->addNumericDisplayAction, &QAction::triggered, this, [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_->addAlarmListAction, &QAction::triggered, this, [this] { addHmiControl(HmiControlType::AlarmList); }); connect(ui_->deleteControlAction, &QAction::triggered, this, &MainWindow::deleteSelectedControl); connect(ui_->addRungAction, &QAction::triggered, this, &MainWindow::addLogicRung); connect(ui_->parallelInsertAction, &QAction::triggered, this, [this] { addLogicParallelBranch(ContactNodeConfig{ RegisterAddress{RegisterArea::M, 0}, ContactMode::NormallyOpen}); }); QMenu *parallel_menu = new QMenu(this); QAction *parallel_open = parallel_menu->addAction(tr("并联常开触点")); QAction *parallel_closed = parallel_menu->addAction(tr("并联常闭触点")); QAction *parallel_compare = parallel_menu->addAction(tr("并联比较条件")); connect(parallel_open, &QAction::triggered, this, [this] { addLogicParallelBranch(ContactNodeConfig{ RegisterAddress{RegisterArea::M, 0}, ContactMode::NormallyOpen}); }); connect(parallel_closed, &QAction::triggered, this, [this] { addLogicParallelBranch(ContactNodeConfig{ RegisterAddress{RegisterArea::M, 0}, ContactMode::NormallyClosed}); }); connect(parallel_compare, &QAction::triggered, this, [this] { addLogicParallelBranch(CompareNodeConfig{ RegisterAddress{RegisterArea::D, 0}, ComparisonOperator::Equal, 0}); }); ui_->parallelInsertAction->setMenu(parallel_menu); if (QToolButton *parallel_button = qobject_cast( ui_->logicToolBar->widgetForAction(ui_->parallelInsertAction))) { parallel_button->setPopupMode(QToolButton::MenuButtonPopup); } connect(ui_->addNormallyOpenAction, &QAction::triggered, this, [this] { addLogicCondition(ContactNodeConfig{ RegisterAddress{RegisterArea::M, 0}, ContactMode::NormallyOpen}); }); connect(ui_->addNormallyClosedAction, &QAction::triggered, this, [this] { addLogicCondition(ContactNodeConfig{ RegisterAddress{RegisterArea::M, 0}, ContactMode::NormallyClosed}); }); connect(ui_->addNormalCoilAction, &QAction::triggered, this, [this] { setLogicOutput(CoilNodeConfig{ RegisterAddress{RegisterArea::M, 0}, CoilMode::Normal}); }); connect(ui_->addSetCoilAction, &QAction::triggered, this, [this] { setLogicOutput(CoilNodeConfig{ RegisterAddress{RegisterArea::M, 0}, CoilMode::Set}); }); connect(ui_->addResetCoilAction, &QAction::triggered, this, [this] { setLogicOutput(CoilNodeConfig{ RegisterAddress{RegisterArea::M, 0}, CoilMode::Reset}); }); connect(ui_->addCompareAction, &QAction::triggered, this, [this] { addLogicCondition(CompareNodeConfig{ RegisterAddress{RegisterArea::D, 0}, ComparisonOperator::Equal, 0}); }); connect(ui_->deleteLogicAction, &QAction::triggered, this, &MainWindow::deleteSelectedLogicObject); connect(ui_->editorTabWidget, &QTabWidget::currentChanged, this, [this](int index) { ui_->hmiToolBar->setVisible(index == 0); ui_->logicToolBar->setVisible(index == 1); if (index == 0) { showControlProperties(selected_control_id_); } else if (index == 1) { showLogicNodeProperties(selected_logic_node_id_); } if (index == 0 || index == 1) { last_editing_tab_index_ = index; refreshProjectUi(); } }); ui_->hmiToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 0); ui_->logicToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 1); ui_->viewMenu->addAction(ui_->projectDock->toggleViewAction()); ui_->viewMenu->addAction(ui_->propertiesDock->toggleViewAction()); ui_->viewMenu->addAction(ui_->outputDock->toggleViewAction()); ui_->viewMenu->addAction(ui_->hmiToolBar->toggleViewAction()); ui_->viewMenu->addAction(ui_->logicToolBar->toggleViewAction()); ui_->viewMenu->addSeparator(); ui_->viewMenu->addAction(ui_->modeToolBar->toggleViewAction()); } void MainWindow::configureAppearance() { setDockNestingEnabled(true); setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); ui_->editingModeAction->setIcon(style()->standardIcon(QStyle::SP_FileDialogDetailedView)); ui_->offlineModeAction->setIcon(style()->standardIcon(QStyle::SP_MediaPlay)); ui_->onlineModeAction->setIcon(style()->standardIcon(QStyle::SP_DriveNetIcon)); ui_->newProjectAction->setIcon(style()->standardIcon(QStyle::SP_FileIcon)); ui_->saveProjectAction->setIcon(style()->standardIcon(QStyle::SP_DialogSaveButton)); ui_->loadProjectAction->setIcon(style()->standardIcon(QStyle::SP_DialogOpenButton)); ui_->configurePlcAction->setIcon(style()->standardIcon(QStyle::SP_ComputerIcon)); ui_->disconnectPlcAction->setIcon(style()->standardIcon(QStyle::SP_DialogCloseButton)); ui_->addButtonAction->setIcon(style()->standardIcon(QStyle::SP_DialogApplyButton)); ui_->addIndicatorAction->setIcon(style()->standardIcon(QStyle::SP_DialogYesButton)); ui_->addNumericDisplayAction->setIcon( 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_->addAlarmListAction->setIcon( style()->standardIcon(QStyle::SP_MessageBoxWarning)); ui_->configureAlarmsAction->setIcon( style()->standardIcon(QStyle::SP_FileDialogDetailedView)); ui_->deleteControlAction->setIcon(style()->standardIcon(QStyle::SP_TrashIcon)); ui_->addRungAction->setIcon(style()->standardIcon(QStyle::SP_FileIcon)); ui_->parallelInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowDown)); ui_->addNormallyOpenAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight)); ui_->addNormallyClosedAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight)); ui_->addNormalCoilAction->setIcon(style()->standardIcon(QStyle::SP_DialogApplyButton)); ui_->addSetCoilAction->setIcon(style()->standardIcon(QStyle::SP_DialogYesButton)); ui_->addResetCoilAction->setIcon(style()->standardIcon(QStyle::SP_DialogNoButton)); ui_->addCompareAction->setIcon(style()->standardIcon(QStyle::SP_FileDialogInfoView)); ui_->deleteLogicAction->setIcon(style()->standardIcon(QStyle::SP_TrashIcon)); ui_->editorTabWidget->setTabIcon(0, style()->standardIcon(QStyle::SP_DesktopIcon)); ui_->editorTabWidget->setTabIcon(1, style()->standardIcon(QStyle::SP_FileDialogListView)); ui_->editorTabWidget->setTabIcon(2, style()->standardIcon(QStyle::SP_ComputerIcon)); ui_->outputDock->setMaximumHeight(220); mode_status_label_ = new QLabel(this); mode_status_label_->setObjectName(QStringLiteral("modeStatusLabel")); mode_status_label_->setMinimumWidth(88); mode_status_label_->setAlignment(Qt::AlignCenter); register_status_label_ = new QLabel(this); register_status_label_->setObjectName(QStringLiteral("registerStatusLabel")); register_status_label_->setMinimumWidth(132); executor_status_label_ = new QLabel(this); executor_status_label_->setObjectName(QStringLiteral("executorStatusLabel")); executor_status_label_->setMinimumWidth(132); statusBar()->addPermanentWidget(mode_status_label_); statusBar()->addPermanentWidget(register_status_label_); statusBar()->addPermanentWidget(executor_status_label_); setStyleSheet(QStringLiteral( "QMainWindow { background: #f3f5f7; }" "QToolBar { background: #ffffff; border: 0; border-bottom: 1px solid #cbd2d9;" " padding: 4px; spacing: 3px; }" "QToolButton { min-height: 26px; padding: 3px 9px; border: 1px solid transparent; }" "QToolButton:hover { background: #edf2f6; border-color: #c5ced6; }" "QToolButton:checked { background: #dcece3; border-color: #75a58a; }" "QDockWidget { color: #24313b; font-weight: 600; }" "QDockWidget::title { background: #e9edf0; padding: 6px;" " border-bottom: 1px solid #cbd2d9; }" "QTreeWidget, QListWidget, QScrollArea { background: #ffffff; border: 1px solid #d5dbe0; }" "QTabWidget::pane { border: 0; background: #f3f5f7; }" "QTabBar::tab { background: #e5e9ec; padding: 7px 14px; border-right: 1px solid #cbd2d9; }" "QTabBar::tab:selected { background: #ffffff; color: #15232d; }" "QFrame#hmiCanvasPlaceholder, QFrame#logicCanvasPlaceholder { background: #ffffff;" " border: 1px solid #cbd2d9; }" "QLabel#hmiEmptyLabel, QLabel#logicEmptyLabel { color: #75838d; }" "QLabel#hmiPageTitleLabel { color: #1d2a33; font-weight: 600; }" "QLabel#hmiPageSizeLabel { color: #66747e; }")); } void MainWindow::configureHmiEditor() { QLayout *layout = ui_->hmiCanvasPlaceholder->layout(); delete ui_->hmiEmptyLabel; hmi_editor_widget_ = new HmiEditorWidget( hmi_editor_service_, hmi_runtime_service_, alarm_service_, ui_->hmiCanvasPlaceholder); hmi_editor_widget_->setMinimumHeight(320); layout->addWidget(hmi_editor_widget_); connect(hmi_editor_widget_, &HmiEditorWidget::controlSelected, this, [this](const QString &id) { showControlProperties(toUtf8(id)); }); connect(hmi_editor_widget_, &HmiEditorWidget::controlChanged, this, [this](const QString &id) { showControlProperties(toUtf8(id)); refreshProjectUi(); }); connect(hmi_editor_widget_, &HmiEditorWidget::editorError, this, [this](const QString &message) { statusBar()->showMessage(message, 5000); }); // 运行态定时投影仓库最新值,编辑态不触发寄存器读取 runtime_refresh_timer_ = new QTimer(this); runtime_refresh_timer_->setInterval(150); connect(runtime_refresh_timer_, &QTimer::timeout, this, [this] { // 离线 / 联机运行模式 if (runtime_mode_service_.mode() != ApplicationMode::Editing) { alarm_service_.refresh(); hmi_editor_widget_->refreshRuntimeValues(); runtime_monitor_widget_->refreshValues( runtime_mode_service_.mode(), runtime_mode_service_.plcConnectionState()); updateSimulationUi(false); } }); runtime_refresh_timer_->start(); connect(&runtime_mode_service_.offlineSimulationService(), &OfflineSimulationService::stateChanged, this, [this] { updateSimulationUi(true); }, Qt::QueuedConnection); connect(&runtime_mode_service_.offlineSimulationService(), &OfflineSimulationService::scanCompleted, this, [this] { logic_editor_widget_->setRuntimeTrace( runtime_mode_service_.offlineSimulationService() .traceSnapshot().forLogic(current_logic_id_)); runtime_monitor_widget_->setLogicTrace( runtime_mode_service_.offlineSimulationService().traceSnapshot()); }, Qt::QueuedConnection); } void MainWindow::configureLogicEditor() { QLayout *layout = ui_->logicCanvasPlaceholder->layout(); delete ui_->logicEmptyLabel; logic_editor_widget_ = new LogicEditorWidget( logic_editor_service_, ui_->logicCanvasPlaceholder); logic_editor_widget_->setObjectName(QStringLiteral("logicEditorWidget")); logic_editor_widget_->setMinimumHeight(320); layout->addWidget(logic_editor_widget_); connect(logic_editor_widget_, &LogicEditorWidget::nodeSelected, this, [this](const QString &id) { showLogicNodeProperties(toUtf8(id)); }); connect(logic_editor_widget_, &LogicEditorWidget::graphChanged, this, &MainWindow::refreshProjectUi); connect(logic_editor_widget_, &LogicEditorWidget::editorError, this, [this](const QString &message) { statusBar()->showMessage(message, 5000); }); } void MainWindow::configureRuntimeMonitor() { runtime_monitor_widget_ = new RuntimeMonitorWidget( hmi_editor_service_, hmi_runtime_service_, logic_editor_service_, project_service_, *hmi_navigation_service_, alarm_service_, register_monitor_service_, ui_->runtimeMonitorTab); runtime_monitor_widget_->setObjectName(QStringLiteral("runtimeMonitorWidget")); ui_->runtimeMonitorLayout->addWidget(runtime_monitor_widget_); connect(runtime_monitor_widget_->freeMonitorWidget(), &FreeMonitorWidget::monitorAddressesChanged, this, [this] { runtime_mode_service_.setMonitorAddresses( register_monitor_service_.addresses()); runtime_monitor_widget_->refreshValues( runtime_mode_service_.mode(), runtime_mode_service_.plcConnectionState()); }); connect(runtime_monitor_widget_->freeMonitorWidget(), &FreeMonitorWidget::operationMessage, this, [this](const QString &message) { 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); ui_->bindingAreaComboBox->setItemData(1, 0); ui_->bindingAreaComboBox->setItemData(2, 1); ui_->buttonOperationComboBox->setItemData( 0, static_cast(HmiButtonOperation::SetOn)); ui_->buttonOperationComboBox->setItemData( 1, static_cast(HmiButtonOperation::SetOff)); ui_->buttonOperationComboBox->setItemData( 2, static_cast(HmiButtonOperation::Toggle)); ui_->buttonOperationComboBox->setItemData( 3, static_cast(HmiButtonOperation::MomentaryOn)); for (int index = 0; index < ui_->logicComparisonComboBox->count(); ++index) { ui_->logicComparisonComboBox->setItemData(index, index); } connect(ui_->applyPropertiesButton, &QPushButton::clicked, this, &MainWindow::applySelectedControlProperties); connect(ui_->applyLogicPropertiesButton, &QPushButton::clicked, this, &MainWindow::applySelectedLogicNodeProperties); ui_->targetPageLabel->setVisible(false); ui_->targetPageComboBox->setVisible(false); showControlProperties({}); } void MainWindow::configurePlcConnection() { connect(ui_->configurePlcAction, &QAction::triggered, this, &MainWindow::connectPlc); connect(ui_->disconnectPlcAction, &QAction::triggered, this, &MainWindow::disconnectPlc); } void MainWindow::configureAlarms() { connect(ui_->configureAlarmsAction, &QAction::triggered, this, [this] { AlarmConfigurationDialog dialog(alarm_editor_service_, this); dialog.exec(); refreshProjectUi(); }); } void MainWindow::refreshProjectUi() { // 仅在当前对象消失时回退,普通刷新必须保留用户的会话选择 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_hmi_page_id_); } const HmiPage *page = hmi_editor_service_.findPage(current_hmi_page_id_); if (logic_editor_widget_ != nullptr) { logic_editor_widget_->setLogicId(current_logic_id_); } if (runtime_monitor_widget_ != nullptr && runtime_mode_service_.mode() != ApplicationMode::Editing) { 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(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(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) { ui_->hmiPageTitleLabel->setText(fromUtf8(page->name)); ui_->hmiPageSizeLabel->setText( tr("%1 x %2").arg(page->width).arg(page->height)); } else { ui_->hmiPageTitleLabel->setText(tr("尚未创建 HMI 页面")); ui_->hmiPageSizeLabel->clear(); } auto *logic_root = new QTreeWidgetItem(ui_->projectTree, {tr("控制逻辑")}); logic_root->setData(0, kProjectTreeKindRole, static_cast(ProjectTreeItemKind::Root)); for (const ControlLogic &logic : project_service_.project().controlLogics) { const QString suffix = logic.enabled ? QString{} : tr("(已禁用)"); auto *item = new QTreeWidgetItem( logic_root, {fromUtf8(logic.name) + suffix}); item->setData(0, kProjectTreeKindRole, static_cast(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( 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( 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( 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( 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( 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( 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( 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() { PlcConnectionDialog dialog(plc_configuration_, this); if (dialog.exec() != QDialog::Accepted) { return; } plc_configuration_ = dialog.configuration(); if (plc_configuration_.portName.empty()) { showProjectResult(tr("连接 PLC"), tr("请选择或输入串口端口"), false); return; } const PlcCommunicationResult result = runtime_mode_service_.connectPlc( plc_configuration_); if (!result.succeeded) { const QString message = fromUtf8(result.message); if (result.message == runtime_mode_service_.plcError()) { statusBar()->showMessage(message, 5000); QMessageBox::warning(this, tr("连接 PLC"), message); } else { showProjectResult(tr("连接 PLC"), message, false); } return; } statusBar()->showMessage(tr("PLC 正在连接,等待首次 M/D 读取"), 5000); } void MainWindow::disconnectPlc() { runtime_mode_service_.disconnectPlc(); } void MainWindow::schedulePlcStatusUpdate() { if (plc_status_update_pending_) { return; } plc_status_update_pending_ = true; QMetaObject::invokeMethod( this, [this] { plc_status_update_pending_ = false; updateModeUi(plcStatusText(runtime_mode_service_)); }, Qt::QueuedConnection); } // 根据控件ID加载控件属性到右侧属性面板 void MainWindow::showControlProperties(const std::string &control_id) { ui_->propertyStack->setCurrentWidget(ui_->hmiPropertiesPage); // 更新窗口全局状态:记录当前选中控件ID selected_control_id_ = control_id; // 在第一页画面中,根据ID查找目标控件,获取只读模型指针 const HmiControl *control = hmi_editor_service_.findControl( current_hmi_page_id_, control_id); // 判断是否查询到有效控件 const bool has_control = control != nullptr; // 更新属性面板顶部状态标签,显示选中控件ID或提示未选择 ui_->selectionValueLabel->setText( has_control ? fromUtf8(control->id) + (control->isConfigured() ? QString{} : control->type == HmiControlType::PageJump ? tr("(未配置)") : tr("(未绑定)")) : tr("未选择")); // 批量控制所有属性编辑组件可用性:无选中控件时整体置灰,禁止编辑 for (QWidget *widget : {static_cast(ui_->controlIdEdit), static_cast(ui_->controlTextEdit), static_cast(ui_->controlXSpinBox), static_cast(ui_->controlYSpinBox), static_cast(ui_->controlWidthSpinBox), static_cast(ui_->controlHeightSpinBox), static_cast(ui_->bindingAreaComboBox), static_cast(ui_->bindingIndexSpinBox), static_cast(ui_->targetPageComboBox), static_cast(ui_->buttonOperationComboBox), static_cast(ui_->applyPropertiesButton)}) { widget->setEnabled(has_control); } // 未选中有效控件,无需填充属性,直接退出 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; } // 将控件基础信息填充至UI输入框 ui_->controlIdEdit->setText(fromUtf8(control->id)); ui_->controlTextEdit->setText(fromUtf8(control->text)); // 控件位置坐标 ui_->controlXSpinBox->setValue(control->bounds.x); ui_->controlYSpinBox->setValue(control->bounds.y); // 控件宽高尺寸 ui_->controlWidthSpinBox->setValue(control->bounds.width); ui_->controlHeightSpinBox->setValue(control->bounds.height); // 根据控件类型获取默认寄存器区域:按钮/指示灯默认M区;数值类控件默认D区 // 未绑定必须保留明确状态,避免属性编辑把显示默认值误提交为 M0/D0 ui_->bindingAreaComboBox->setCurrentIndex(!control->binding.has_value() ? 0 : 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 && control->type != HmiControlType::AlarmList; 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); ui_->buttonOperationComboBox->setEnabled(is_button); ui_->buttonOperationComboBox->setCurrentIndex( ui_->buttonOperationComboBox->findData( static_cast(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) { ui_->propertyStack->setCurrentWidget(ui_->logicPropertiesPage); selected_logic_node_id_ = node_id; const LogicNode *node = logic_editor_service_.findNode( current_logic_id_, node_id); const bool has_node = node != nullptr; ui_->selectionValueLabel->setText( has_node ? fromUtf8(node->id) : tr("未选择")); for (QWidget *widget : {static_cast(ui_->logicNodeIdEdit), static_cast(ui_->logicAddressSpinBox), static_cast(ui_->logicModeComboBox), static_cast(ui_->logicComparisonComboBox), static_cast(ui_->logicValueSpinBox), static_cast(ui_->applyLogicPropertiesButton)}) { widget->setEnabled(has_node); } ui_->logicNodeTypeLabel->setEnabled(has_node); ui_->logicAddressAreaLabel->setEnabled(has_node); if (!has_node) { ui_->logicNodeIdEdit->clear(); ui_->logicNodeTypeLabel->clear(); ui_->logicAddressAreaLabel->clear(); ui_->logicModeComboBox->clear(); return; } ui_->logicNodeIdEdit->setText(fromUtf8(node->id)); ui_->logicModeComboBox->clear(); if (const auto *contact = std::get_if(&node->config)) { ui_->logicNodeTypeLabel->setText(tr("触点")); ui_->logicAddressAreaLabel->setText(QStringLiteral("M")); ui_->logicAddressSpinBox->setValue(contact->address.index()); ui_->logicModeComboBox->addItem( tr("常开"), static_cast(ContactMode::NormallyOpen)); ui_->logicModeComboBox->addItem( tr("常闭"), static_cast(ContactMode::NormallyClosed)); ui_->logicModeComboBox->setCurrentIndex( ui_->logicModeComboBox->findData(static_cast(contact->mode))); ui_->logicModeComboBox->setEnabled(true); ui_->logicComparisonComboBox->setEnabled(false); ui_->logicValueSpinBox->setEnabled(false); } else if (const auto *coil = std::get_if(&node->config)) { ui_->logicNodeTypeLabel->setText(tr("线圈")); ui_->logicAddressAreaLabel->setText(QStringLiteral("M")); ui_->logicAddressSpinBox->setValue(coil->address.index()); ui_->logicModeComboBox->addItem( tr("普通"), static_cast(CoilMode::Normal)); ui_->logicModeComboBox->addItem( tr("置位"), static_cast(CoilMode::Set)); ui_->logicModeComboBox->addItem( tr("复位"), static_cast(CoilMode::Reset)); ui_->logicModeComboBox->setCurrentIndex( ui_->logicModeComboBox->findData(static_cast(coil->mode))); ui_->logicModeComboBox->setEnabled(true); ui_->logicComparisonComboBox->setEnabled(false); ui_->logicValueSpinBox->setEnabled(false); } else { const auto &compare = std::get(node->config); ui_->logicNodeTypeLabel->setText(tr("D 值比较")); ui_->logicAddressAreaLabel->setText(QStringLiteral("D")); ui_->logicAddressSpinBox->setValue(compare.address.index()); ui_->logicModeComboBox->addItem(tr("不适用")); ui_->logicModeComboBox->setEnabled(false); ui_->logicComparisonComboBox->setEnabled(true); ui_->logicComparisonComboBox->setCurrentIndex( ui_->logicComparisonComboBox->findData( static_cast(compare.comparison))); ui_->logicValueSpinBox->setEnabled(true); ui_->logicValueSpinBox->setValue(compare.value); } if (!node->configured) { ui_->logicNodeTypeLabel->setText(tr("待配置:%1").arg( ui_->logicNodeTypeLabel->text())); } } void MainWindow::addHmiControl(HmiControlType type) { const HmiEditorResult result = hmi_editor_service_.addControl( current_hmi_page_id_, type); if (!result.succeeded) { showProjectResult(tr("添加控件"), fromUtf8(result.message), false); return; } hmi_editor_widget_->reloadPage(); hmi_editor_widget_->selectControl(result.id); showControlProperties(result.id); refreshProjectUi(); statusBar()->showMessage(tr("已添加%1").arg(controlTypeText(type)), 3000); } void MainWindow::deleteSelectedControl() { if (selected_control_id_.empty()) { return; } const HmiEditorResult result = hmi_editor_service_.removeControl( current_hmi_page_id_, selected_control_id_); if (!result.succeeded) { showProjectResult(tr("删除控件"), fromUtf8(result.message), false); return; } selected_control_id_.clear(); hmi_editor_widget_->reloadPage(); showControlProperties({}); refreshProjectUi(); } void MainWindow::applySelectedControlProperties() { const HmiControl *old_control = hmi_editor_service_.findControl( current_hmi_page_id_, selected_control_id_); if (old_control == nullptr) { return; } // 从现有控件复制不可见属性,避免属性面板覆盖扩展配置 HmiControl control = *old_control; control.id = toUtf8(ui_->controlIdEdit->text()); control.text = toUtf8(ui_->controlTextEdit->text()); control.bounds.x = ui_->controlXSpinBox->value(); control.bounds.y = ui_->controlYSpinBox->value(); control.bounds.width = ui_->controlWidthSpinBox->value(); control.bounds.height = ui_->controlHeightSpinBox->value(); if (control.type == HmiControlType::Label || control.type == HmiControlType::PageJump || control.type == HmiControlType::AlarmList) { control.binding.reset(); } else { 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( ui_->buttonOperationComboBox->currentData().toInt()); } if (control.type == HmiControlType::PageJump) { control.pageJump = HmiPageJumpConfig{ toUtf8(ui_->targetPageComboBox->currentData().toString())}; } const HmiEditorResult result = hmi_editor_service_.updateControl( current_hmi_page_id_, selected_control_id_, control); if (!result.succeeded) { showProjectResult(tr("应用属性"), fromUtf8(result.message), false); return; } selected_control_id_ = result.id; hmi_editor_widget_->reloadPage(); hmi_editor_widget_->selectControl(selected_control_id_); refreshProjectUi(); statusBar()->showMessage(tr("控件属性已更新"), 3000); } void MainWindow::addLogicCondition(const LogicNodeConfig &config) { const LogicEditorResult result = logic_editor_widget_->addCondition(config); if (!result.succeeded) { showProjectResult(tr("添加逻辑条件"), fromUtf8(result.message), false); return; } selected_logic_node_id_ = result.id; showLogicNodeProperties(result.id); refreshProjectUi(); statusBar()->showMessage(tr("已添加逻辑条件"), 3000); } void MainWindow::addLogicParallelBranch(const LogicNodeConfig &config) { const LogicEditorResult result = logic_editor_widget_->addParallelBranch(config); if (!result.succeeded) { showProjectResult(tr("建立并联支路"), fromUtf8(result.message), false); return; } selected_logic_node_id_ = result.id; showLogicNodeProperties(result.id); refreshProjectUi(); statusBar()->showMessage(tr("已建立并联支路,请配置新触点"), 3000); } void MainWindow::setLogicOutput(const LogicNodeConfig &config) { const LogicEditorResult result = logic_editor_widget_->setOutput(config); if (!result.succeeded) { showProjectResult(tr("设置输出线圈"), fromUtf8(result.message), false); return; } selected_logic_node_id_ = result.id; showLogicNodeProperties(result.id); refreshProjectUi(); statusBar()->showMessage(tr("输出线圈已设置"), 3000); } void MainWindow::addLogicRung() { const LogicEditorResult result = logic_editor_widget_->addRung(); if (!result.succeeded) { showProjectResult(tr("新建网络"), fromUtf8(result.message), false); return; } selected_logic_node_id_.clear(); showLogicNodeProperties({}); refreshProjectUi(); statusBar()->showMessage(tr("已新建网络"), 3000); } void MainWindow::deleteSelectedLogicObject() { const LogicEditorResult result = logic_editor_widget_->deleteSelected(); if (!result.succeeded) { return; } selected_logic_node_id_.clear(); showLogicNodeProperties({}); refreshProjectUi(); } void MainWindow::applySelectedLogicNodeProperties() { const LogicNode *node = logic_editor_service_.findNode( current_logic_id_, selected_logic_node_id_); if (node == nullptr) { return; } LogicNodeConfig config = node->config; if (std::holds_alternative(config)) { config = ContactNodeConfig{ RegisterAddress{RegisterArea::M, ui_->logicAddressSpinBox->value()}, static_cast(ui_->logicModeComboBox->currentData().toInt())}; } else if (std::holds_alternative(config)) { config = CoilNodeConfig{ RegisterAddress{RegisterArea::M, ui_->logicAddressSpinBox->value()}, static_cast(ui_->logicModeComboBox->currentData().toInt())}; } else { config = CompareNodeConfig{ RegisterAddress{RegisterArea::D, ui_->logicAddressSpinBox->value()}, static_cast( ui_->logicComparisonComboBox->currentData().toInt()), static_cast(ui_->logicValueSpinBox->value())}; } const LogicEditorResult result = logic_editor_service_.updateNodeConfig( current_logic_id_, selected_logic_node_id_, config); if (!result.succeeded) { showProjectResult(tr("应用逻辑属性"), fromUtf8(result.message), false); return; } logic_editor_widget_->reloadLogic(); logic_editor_widget_->selectNode(selected_logic_node_id_); showLogicNodeProperties(selected_logic_node_id_); refreshProjectUi(); statusBar()->showMessage(tr("逻辑节点属性已更新,节点已配置"), 3000); } void MainWindow::createNewProject() { const QString name = QInputDialog::getText( this, tr("新建工程"), tr("工程名称")); if (name.isEmpty()) { return; } const ProjectOperationResult result = project_service_.createNewProject(toUtf8(name)); if (!result.succeeded) { showProjectResult(tr("新建工程"), fromUtf8(result.message), false); return; } hmi_editor_service_.ensureDefaultPage(); 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(); showControlProperties({}); statusBar()->showMessage(tr("已创建新工程"), 3000); } void MainWindow::saveProject() { const ProjectOperationResult result = project_service_.save(); if (!result.succeeded) { saveProjectAs(); return; } showProjectResult(tr("保存工程"), tr("工程已保存"), true); } void MainWindow::saveProjectAs() { const QString path = QFileDialog::getSaveFileName( this, tr("工程另存为"), {}, tr("工程文件 (*.json)")); if (path.isEmpty()) { return; } const ProjectOperationResult result = project_service_.saveAs(toUtf8(path)); showProjectResult( tr("保存工程"), result.succeeded ? tr("工程已保存") : fromUtf8(result.message), result.succeeded); } void MainWindow::loadProject() { const QString path = QFileDialog::getOpenFileName( this, tr("加载工程"), {}, tr("工程文件 (*.json)")); if (path.isEmpty()) { return; } const ProjectOperationResult result = project_service_.load(toUtf8(path)); if (!result.succeeded) { showProjectResult(tr("加载工程"), fromUtf8(result.message), false); return; } 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(); showControlProperties({}); showProjectResult(tr("加载工程"), tr("工程已加载"), true); } void MainWindow::showProjectResult( const QString &action, const QString &message, bool succeeded) { const QString output = action + QStringLiteral(": ") + message; statusBar()->showMessage(output, 5000); ui_->outputList->addItem(output); ui_->outputList->scrollToBottom(); if (!succeeded) { QMessageBox::warning(this, action, message); } } void MainWindow::requestMode(ApplicationMode requested_mode) { // UI 仅转发模式意图,合法性由服务层和领域状态机决定 ModeTransitionResult result; switch (requested_mode) { case ApplicationMode::Editing: { result = runtime_mode_service_.enterEditing(); break; } case ApplicationMode::OfflineRunning: { result = runtime_mode_service_.enterOfflineRunning(); break; } case ApplicationMode::OnlineRunning: { result = runtime_mode_service_.enterOnlineRunning(); break; } default: { restoreCurrentModeAction(); statusBar()->showMessage(tr("不支持的运行模式"), 5000); return; } } if (!result.succeeded) { restoreCurrentModeAction(); QString message = transitionErrorText(result.error); if (result.error == ModeTransitionError::InitialPlcReadRequired) { message += tr(";当前状态:%1").arg(plcStatusText(runtime_mode_service_)); } if (result.error == ModeTransitionError::SimulationStartFailed) { const LogicScanResult &error = runtime_mode_service_.simulationError(); if (!error.message.empty()) { message += QStringLiteral(": ") + fromUtf8(error.message); } } statusBar()->showMessage(message, 5000); return; } updateModeUi(tr("已进入%1").arg(modeText(runtime_mode_service_.mode()))); } void MainWindow::updateModeUi(const QString &message) { const ApplicationMode mode = runtime_mode_service_.mode(); 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()) { const HmiNavigationResult navigation = hmi_navigation_service_->start(); if (!navigation.succeeded) { statusBar()->showMessage(fromUtf8(navigation.message), 5000); } } runtime_monitor_widget_->setProjectObjects( hmi_navigation_service_->currentPageId(), current_logic_id_); runtime_monitor_widget_->setMode(mode, runtime_mode_service_.plcConnectionState()); ui_->editorTabWidget->setCurrentWidget(ui_->runtimeMonitorTab); } else { 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); ui_->propertiesDock->setEnabled(policy.allowsProjectEditing); hmi_editor_widget_->setEditingEnabled(policy.allowsProjectEditing); hmi_editor_widget_->setRuntimeActive( policy.usesVirtualRegisters || policy.usesPlcRegisters); logic_editor_widget_->setEditingEnabled(policy.allowsProjectEditing); if (mode == ApplicationMode::Editing) { alarm_service_.reset(); logic_editor_widget_->clearRuntimeTrace(); } ui_->hmiToolBar->setEnabled(policy.allowsProjectEditing); ui_->logicToolBar->setEnabled(policy.allowsProjectEditing); ui_->addButtonAction->setEnabled(policy.allowsProjectEditing); 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_->addAlarmListAction->setEnabled(policy.allowsProjectEditing); ui_->configureAlarmsAction->setEnabled(policy.allowsProjectEditing); ui_->deleteControlAction->setEnabled(policy.allowsProjectEditing); ui_->addNormallyOpenAction->setEnabled(policy.allowsProjectEditing); ui_->addNormallyClosedAction->setEnabled(policy.allowsProjectEditing); ui_->addNormalCoilAction->setEnabled(policy.allowsProjectEditing); ui_->addSetCoilAction->setEnabled(policy.allowsProjectEditing); ui_->addResetCoilAction->setEnabled(policy.allowsProjectEditing); ui_->addCompareAction->setEnabled(policy.allowsProjectEditing); ui_->deleteLogicAction->setEnabled(policy.allowsProjectEditing); ui_->newProjectAction->setEnabled(policy.allowsProjectEditing); 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) { mode_status_label_->setStyleSheet(QStringLiteral( "border: 1px solid #75a58a; background: #e3f0e8; color: #205c3b; padding: 2px 8px;")); } else if (mode == ApplicationMode::OfflineRunning) { mode_status_label_->setStyleSheet(QStringLiteral( "border: 1px solid #bd8739; background: #fff0d2; color: #744b0e; padding: 2px 8px;")); } else { mode_status_label_->setStyleSheet(QStringLiteral( "border: 1px solid #4b88a8; background: #deeff7; color: #174f6c; padding: 2px 8px;")); } register_status_label_->setText( policy.usesVirtualRegisters ? tr("数据源:虚拟 M/D") : policy.usesPlcRegisters ? tr("数据源:PLC 缓存") : tr("数据源:未启用")); const PlcConnectionState plc_state = runtime_mode_service_.plcConnectionState(); const bool plc_configuration_available = plc_state == PlcConnectionState::Disconnected || plc_state == PlcConnectionState::Faulted; ui_->configurePlcAction->setEnabled( policy.allowsProjectEditing && plc_configuration_available); ui_->configurePlcAction->setText( plc_state == PlcConnectionState::Faulted ? tr("PLC 重新配置") : tr("PLC 配置")); ui_->configurePlcAction->setToolTip( plc_state == PlcConnectionState::Faulted ? tr("清理故障会话后重新配置并连接真实 PLC") : tr("配置参数并连接真实 PLC")); ui_->disconnectPlcAction->setEnabled(plc_state != PlcConnectionState::Disconnected); if (plc_state == PlcConnectionState::Connecting) { register_status_label_->setText(tr("PLC:正在连接")); } else if (plc_state == PlcConnectionState::Connected) { register_status_label_->setText( runtime_mode_service_.initialPlcReadCompleted() ? tr("PLC:已连接,首读完成") : tr("PLC:已连接,正在首读")); } else if (plc_state == PlcConnectionState::Recovering) { register_status_label_->setText(tr("PLC:通信已恢复,正在重新首读")); } else if (plc_state == PlcConnectionState::Faulted) { register_status_label_->setText(tr("PLC:通信故障")); } updateSimulationUi(false); statusBar()->showMessage(message, 4000); const bool duplicate = ui_->outputList->count() > 0 && ui_->outputList->item(ui_->outputList->count() - 1)->text() == message; if (!duplicate) { ui_->outputList->addItem(message); ui_->outputList->scrollToBottom(); } } void MainWindow::updateSimulationUi(bool report_fault) { const bool offline = runtime_mode_service_.mode() == ApplicationMode::OfflineRunning; const bool online = runtime_mode_service_.mode() == ApplicationMode::OnlineRunning; const bool plc_connected = runtime_mode_service_.plcConnectionState() == PlcConnectionState::Connected; const SimulationState state = runtime_mode_service_.simulationState(); hmi_editor_widget_->setRuntimeWriteEnabled( (online && plc_connected) || (offline && state == SimulationState::Running)); if (runtime_monitor_widget_ != nullptr) { runtime_monitor_widget_->setHmiWriteEnabled( (online && plc_connected) || (offline && state == SimulationState::Running)); runtime_monitor_widget_->refreshValues( runtime_mode_service_.mode(), runtime_mode_service_.plcConnectionState()); } switch (state) { case SimulationState::Running: { executor_status_label_->setText( tr("逻辑执行器:运行(%1 次)") .arg(runtime_mode_service_.successfulScanCount())); break; } case SimulationState::Faulted: { executor_status_label_->setText(tr("逻辑执行器:故障")); 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().forLogic(current_logic_id_), error.nodeId); runtime_monitor_widget_->setLogicTrace( runtime_mode_service_.offlineSimulationService().traceSnapshot(), error.nodeId); if (!error.nodeId.empty()) { logic_editor_widget_->selectNode(error.nodeId); showLogicNodeProperties(error.nodeId); } QString message = tr("离线仿真异常:%1").arg(fromUtf8(error.message)); if (!error.logicId.empty()) { message += tr(",逻辑 %1").arg(fromUtf8(error.logicId)); } if (!error.rungId.empty()) { message += tr(",网络 %1").arg(fromUtf8(error.rungId)); } if (!error.nodeId.empty()) { message += tr(",节点 %1").arg(fromUtf8(error.nodeId)); } statusBar()->showMessage(message); ui_->outputList->addItem(message); ui_->outputList->scrollToBottom(); } break; } case SimulationState::Stopped: default: { executor_status_label_->setText(tr("逻辑执行器:停止")); break; } } } // 根据当前真实运行模式,同步更新模式工具栏单选按钮选中状态 void MainWindow::restoreCurrentModeAction() { ui_->editingModeAction->setChecked(runtime_mode_service_.mode() == ApplicationMode::Editing); ui_->offlineModeAction->setChecked(runtime_mode_service_.mode() == ApplicationMode::OfflineRunning); ui_->onlineModeAction->setChecked(runtime_mode_service_.mode() == ApplicationMode::OnlineRunning); }