#include "main_window.h" #include "hmi_editor_widget.h" #include "alarm_configuration_dialog.h" #include "register_comment_dialog.h" #include "logic_editor_widget.h" #include "logic_instruction_dialog.h" #include "plc_connection_dialog.h" #include "free_monitor_widget.h" #include "runtime_monitor_widget.h" #include "toolbar_icon_factory.h" #include "project_workspace_controller.h" #include "property_panel_controller.h" #include "runtime_panel_controller.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 "services/register_comment_service.h" #include "ui_main_window.h" #include "domain/project_limits.h" #include "infrastructure/runtime_project_bundle.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr int kSyntaxLogicIdRole = Qt::UserRole + 1; constexpr int kSyntaxRungIdRole = Qt::UserRole + 2; constexpr int kSyntaxColumnRole = Qt::UserRole + 3; bool isTextEditingObject(QObject *object) { QWidget *widget = qobject_cast(object); while (widget != nullptr) { if (qobject_cast(widget) != nullptr || qobject_cast(widget) != nullptr || qobject_cast(widget) != nullptr || qobject_cast(widget) != nullptr) { return true; } widget = widget->parentWidget(); } return false; } QString suggestedProjectFileName(QString project_name) { project_name = project_name.trimmed(); const QString invalid_characters = QStringLiteral("<>:\"/\\|?*"); for (QChar &character : project_name) { if (character.unicode() < 0x20U || invalid_characters.contains(character)) { character = QLatin1Char('_'); } } constexpr int kMaximumSuggestedBaseNameLength = 240; if (project_name.size() > kMaximumSuggestedBaseNameLength) { project_name.truncate(kMaximumSuggestedBaseNameLength); if (!project_name.isEmpty() && project_name.at(project_name.size() - 1).isHighSurrogate()) { project_name.chop(1); } } while (project_name.endsWith(QLatin1Char(' ')) || project_name.endsWith(QLatin1Char('.'))) { project_name.chop(1); } if (project_name.isEmpty()) { project_name = QStringLiteral("未命名工程"); } const QString device_name = project_name.section(QLatin1Char('.'), 0, 0).toUpper(); const bool is_reserved_device_name = device_name == QStringLiteral("CON") || device_name == QStringLiteral("PRN") || device_name == QStringLiteral("AUX") || device_name == QStringLiteral("NUL") || (device_name.size() == 4 && (device_name.startsWith(QStringLiteral("COM")) || device_name.startsWith(QStringLiteral("LPT"))) && device_name.back() >= QLatin1Char('1') && device_name.back() <= QLatin1Char('9')); if (is_reserved_device_name) { project_name.prepend(QLatin1Char('_')); } return project_name + QStringLiteral(".json"); } bool isEditorShortcut(const QKeyEvent &event) { return event.matches(QKeySequence::Undo) || event.matches(QKeySequence::Redo) || event.matches(QKeySequence::Copy) || event.matches(QKeySequence::Paste) || (event.modifiers() == Qt::NoModifier && (event.key() == Qt::Key_Delete || event.key() == Qt::Key_Escape)); } 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 已断开"); } } } QToolButton *addToolbarMenu( QToolBar *toolbar, const QString &text, const QString &object_name, const QIcon &icon, const QList &actions) { auto *button = new QToolButton(toolbar); button->setObjectName(object_name); button->setText(text); button->setIcon(icon); button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); button->setPopupMode(QToolButton::InstantPopup); auto *menu = new QMenu(button); menu->addActions(actions); button->setMenu(menu); toolbar->addWidget(button); return button; } bool copyDirectoryContents( const QString &source_path, const QString &destination_path, QString *error) { QDir source_directory(source_path); if (!source_directory.exists()) { if (error != nullptr) { *error = MainWindow::tr("未找到打包目录:%1").arg(source_path); } return false; } QDir destination_directory(destination_path); if (!destination_directory.exists() && !QDir().mkpath(destination_path)) { if (error != nullptr) { *error = MainWindow::tr("无法创建导出目录:%1").arg(destination_path); } return false; } const QFileInfoList entries = source_directory.entryInfoList( QDir::NoDotAndDotDot | QDir::AllEntries, QDir::DirsFirst | QDir::Name); for (const QFileInfo &entry : entries) { const QString destination = destination_directory.filePath(entry.fileName()); if (entry.isDir()) { if (!copyDirectoryContents(entry.absoluteFilePath(), destination, error)) { return false; } continue; } if (QFileInfo::exists(destination) || !QFile::copy(entry.absoluteFilePath(), destination)) { if (error != nullptr) { *error = MainWindow::tr("复制打包文件失败:%1").arg(destination); } return false; } } return true; } } // 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, RegisterCommentService ®ister_comment_service, RegisterMonitorService ®ister_monitor_service, PlcDiscoveryGateway &plc_discovery_gateway, const ApplicationSettingsLoadResult &application_settings_result, bool user_runtime_mode, 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_comment_service_(register_comment_service), register_monitor_service_(register_monitor_service), plc_discovery_gateway_(plc_discovery_gateway), application_settings_result_(application_settings_result), user_runtime_mode_(user_runtime_mode), owned_hmi_navigation_service_( std::make_unique(project_service)), hmi_navigation_service_(owned_hmi_navigation_service_.get()), plc_configuration_(application_settings_result.settings.plcDefaults) { 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, RegisterCommentService ®ister_comment_service, RegisterMonitorService ®ister_monitor_service, PlcDiscoveryGateway &plc_discovery_gateway, const ApplicationSettingsLoadResult &application_settings_result, bool user_runtime_mode, 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_comment_service_(register_comment_service), register_monitor_service_(register_monitor_service), plc_discovery_gateway_(plc_discovery_gateway), application_settings_result_(application_settings_result), user_runtime_mode_(user_runtime_mode), hmi_navigation_service_(&hmi_navigation_service), plc_configuration_(application_settings_result.settings.plcDefaults) { initializeUi(); } void MainWindow::initializeUi() { ui_->setupUi(this); qApp->installEventFilter(this); configureAppearance(); property_panel_controller_ = std::make_unique( *this, *ui_, project_service_, hmi_editor_service_, logic_editor_service_, application_settings_result_.settings.hmiDefaults, [this] { return current_hmi_page_id_; }, [this] { return current_logic_id_; }, selected_control_id_, selected_logic_node_id_, [this] { refreshProjectUi(); }, [this](const QString &action, const QString &message, bool succeeded) { showProjectResult(action, message, succeeded); }, [this](const QString &message, int timeout_ms) { statusBar()->showMessage(message, timeout_ms); }); configureActions(); configurePropertyEditor(); configurePlcConnection(); configureAlarms(); configureRegisterComments(); configureHmiEditor(); configureLogicEditor(); configureRuntimeMonitor(); configureDataMonitor(); configureProjectTree(); runtime_mode_service_.setPlcStatusChangedCallback( [this] { schedulePlcStatusUpdate(); }); hmi_editor_service_.ensureDefaultPage(); logic_editor_service_.ensureDefaultLogic(); clearEditorHistories(); current_hmi_page_id_ = hmi_editor_service_.firstPageId(); current_logic_id_ = logic_editor_service_.firstLogicId(); showControlProperties({}); refreshProjectUi(); updateModeUi(tr("系统已进入编辑态")); for (const std::string &message : application_settings_result_.messages) { appendOutputMessage(fromUtf8(message)); } if (application_settings_result_.warningRequired) { const QString warning = fromUtf8( application_settings_result_.warningMessage); QTimer::singleShot( 0, this, [this, warning] { QMessageBox::warning(this, tr("应用配置"), warning); }); } if (user_runtime_mode_) { initializeUserRuntime(); } } void MainWindow::initializeUserRuntime() { setWindowTitle(fromUtf8(project_service_.project().metadata.name)); runtime_panel_controller_->configureUserRuntimeMode( true, [this](int mode) { if (mode == static_cast(ApplicationMode::OfflineRunning)) { requestUserRuntimeMode(ApplicationMode::OfflineRunning); } else if (mode == static_cast(ApplicationMode::OnlineRunning)) { requestUserRuntimeMode(ApplicationMode::OnlineRunning); } }, [this] { connectPlc(); }, [this] { disconnectPlc(); }); menuBar()->setVisible(false); ui_->hmiToolBar->setVisible(false); ui_->logicToolBar->setVisible(false); ui_->outputDock->setVisible(false); ui_->projectDock->setVisible(false); ui_->propertiesDock->setVisible(false); QTimer::singleShot( 0, this, [this] { if (requestMode(ApplicationMode::OfflineRunning)) { hide(); } else { qApp->quit(); } }); } void MainWindow::requestUserRuntimeMode(ApplicationMode requested_mode) { if (runtime_mode_service_.mode() == requested_mode) { return; } if (requested_mode == ApplicationMode::OnlineRunning && !runtime_mode_service_.initialPlcReadCompleted()) { // 用户运行版没有可见的编辑器主窗口,失败前不能先隐藏运行监控窗口 if (runtime_monitor_widget_ != nullptr) { runtime_monitor_widget_->setMode( runtime_mode_service_.mode(), runtime_mode_service_.plcConnectionState()); const QString message = transitionErrorText( ModeTransitionError::InitialPlcReadRequired) + tr("\n当前状态:%1").arg( plcStatusText(runtime_mode_service_)) + tr("\n请先点击“PLC 配置”连接并完成首次读取。"); QMessageBox::warning( runtime_monitor_widget_, tr("无法切换到真机运行"), message); } return; } if (runtime_mode_service_.mode() != ApplicationMode::Editing) { const ModeTransitionResult editing = runtime_mode_service_.enterEditing(); if (!editing.succeeded) { return; } updateModeUi(tr("正在切换运行模式")); QTimer::singleShot( 0, this, [this, requested_mode] { requestMode(requested_mode); }); return; } requestMode(requested_mode); } MainWindow::~MainWindow() { qApp->removeEventFilter(this); register_monitor_service_.setAddressesChangedCallback({}); runtime_mode_service_.setPlcStatusChangedCallback({}); } bool MainWindow::eventFilter(QObject *watched, QEvent *event) { if (event->type() == QEvent::ShortcutOverride && isTextEditingObject(watched)) { auto *key_event = static_cast(event); if (isEditorShortcut(*key_event)) { event->accept(); return true; } } return QMainWindow::eventFilter(watched, event); } void MainWindow::closeEvent(QCloseEvent *event) { if (!confirmSaveBeforeDestructiveAction()) { event->ignore(); return; } if (runtime_panel_controller_ != nullptr) { runtime_panel_controller_->closeForApplicationExit(); } QMainWindow::closeEvent(event); } 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, this, [this] { close(); }); connect(ui_->newProjectAction, &QAction::triggered, this, &MainWindow::createNewProject); connect(ui_->saveProjectAction, &QAction::triggered, this, &MainWindow::saveProject); connect(ui_->saveAsProjectAction, &QAction::triggered, this, &MainWindow::saveProjectAs); connect(ui_->loadProjectAction, &QAction::triggered, this, &MainWindow::loadProject); connect(ui_->exportRuntimeAction, &QAction::triggered, this, &MainWindow::exportRuntimeProgram); connect(ui_->undoAction, &QAction::triggered, this, &MainWindow::undoActiveEditor); connect(ui_->redoAction, &QAction::triggered, this, &MainWindow::redoActiveEditor); connect(ui_->copyAction, &QAction::triggered, this, &MainWindow::copyActiveSelection); connect(ui_->pasteAction, &QAction::triggered, this, &MainWindow::pasteActiveSelection); connect(ui_->deleteSelectionAction, &QAction::triggered, this, &MainWindow::deleteActiveSelection); connect(ui_->clearSelectionAction, &QAction::triggered, this, &MainWindow::clearActiveSelection); connect(ui_->syntaxCheckAction, &QAction::triggered, this, &MainWindow::runLogicSyntaxCheck); connect(ui_->doubleCoilCheckAction, &QAction::triggered, this, &MainWindow::runDoubleCoilCheck); connect(ui_->outputList, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *item) { if (item == nullptr) { return; } const std::string logic_id = toUtf8( item->data(kSyntaxLogicIdRole).toString()); const std::string rung_id = toUtf8( item->data(kSyntaxRungIdRole).toString()); if (logic_id.empty() || rung_id.empty()) { return; } focusLogicSyntaxLocation({ logic_id, rung_id, 0, 0, item->data(kSyntaxColumnRole).toInt()}); }); 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); addToolbarMenu( ui_->hmiToolBar, tr("更多控件"), QStringLiteral("hmiMoreControlsButton"), makeUiIcon(UiIcon::More), QList{ ui_->addPageJumpAction, ui_->addAlarmListAction, ui_->configureAlarmsAction}); connect(ui_->addRungAction, &QAction::triggered, this, &MainWindow::addLogicRung); connect(ui_->insertRungAboveAction, &QAction::triggered, this, &MainWindow::insertLogicRungAbove); connect(ui_->insertRungBelowAction, &QAction::triggered, this, &MainWindow::insertLogicRungBelow); connect(ui_->deleteRungAction, &QAction::triggered, this, &MainWindow::deleteLogicRung); connect(ui_->mouseDrawWireAction, &QAction::triggered, this, [this](bool checked) { if (checked) { const QSignalBlocker blocker(ui_->mouseEraseWireAction); ui_->mouseEraseWireAction->setChecked(false); } logic_editor_widget_->setMouseWireMode( checked ? LogicEditorWidget::MouseWireMode::Draw : ui_->mouseEraseWireAction->isChecked() ? LogicEditorWidget::MouseWireMode::Erase : LogicEditorWidget::MouseWireMode::Select); }); connect(ui_->mouseEraseWireAction, &QAction::triggered, this, [this](bool checked) { if (checked) { const QSignalBlocker blocker(ui_->mouseDrawWireAction); ui_->mouseDrawWireAction->setChecked(false); } logic_editor_widget_->setMouseWireMode( checked ? LogicEditorWidget::MouseWireMode::Erase : ui_->mouseDrawWireAction->isChecked() ? LogicEditorWidget::MouseWireMode::Draw : LogicEditorWidget::MouseWireMode::Select); }); connect(ui_->insertHorizontalWireAction, &QAction::triggered, this, &MainWindow::addLogicHorizontalWire); connect(ui_->insertVerticalWireAction, &QAction::triggered, this, &MainWindow::addLogicVerticalWire); connect(ui_->deleteHorizontalWireAction, &QAction::triggered, this, &MainWindow::deleteLogicHorizontalWire); connect(ui_->deleteVerticalWireAction, &QAction::triggered, this, &MainWindow::deleteLogicVerticalWire); 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_rising = parallel_menu->addAction(tr("并联上升沿触点")); QAction *parallel_falling = 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_rising, &QAction::triggered, this, [this] { addLogicParallelBranch(EdgeContactNodeConfig{ RegisterAddress{RegisterArea::M, 0}, EdgeMode::Rising}); }); connect(parallel_falling, &QAction::triggered, this, [this] { addLogicParallelBranch(EdgeContactNodeConfig{ RegisterAddress{RegisterArea::M, 0}, EdgeMode::Falling}); }); 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_->addRisingEdgeAction, &QAction::triggered, this, [this] { addLogicCondition(EdgeContactNodeConfig{ RegisterAddress{RegisterArea::M, 0}, EdgeMode::Rising}); }); connect(ui_->addFallingEdgeAction, &QAction::triggered, this, [this] { addLogicCondition(EdgeContactNodeConfig{ RegisterAddress{RegisterArea::M, 0}, EdgeMode::Falling}); }); 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_->addMoveAction, &QAction::triggered, this, [this] { configureAndSetLogicOutput(MoveNodeConfig{ WordOperand{ WordOperandKind::Constant, RegisterAddress{RegisterArea::D, 0}, 0}, RegisterAddress{RegisterArea::D, 0}}); }); connect(ui_->addAddAction, &QAction::triggered, this, [this] { configureAndSetLogicOutput(ArithmeticNodeConfig{ ArithmeticOperation::Add, WordOperand{ WordOperandKind::Register, RegisterAddress{RegisterArea::D, 0}, 0}, WordOperand{ WordOperandKind::Constant, RegisterAddress{RegisterArea::D, 0}, 1}, RegisterAddress{RegisterArea::D, 0}}); }); connect(ui_->addSubAction, &QAction::triggered, this, [this] { configureAndSetLogicOutput(ArithmeticNodeConfig{ ArithmeticOperation::Subtract, WordOperand{ WordOperandKind::Register, RegisterAddress{RegisterArea::D, 0}, 0}, WordOperand{ WordOperandKind::Constant, RegisterAddress{RegisterArea::D, 0}, 1}, RegisterAddress{RegisterArea::D, 0}}); }); 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_->editNetworkCommentAction, &QAction::triggered, this, &MainWindow::editSelectedNetworkComment); addToolbarMenu( ui_->logicToolBar, tr("更多触点"), QStringLiteral("logicContactMenuButton"), makeUiIcon(UiIcon::NormallyOpenContact), QList{ ui_->addRisingEdgeAction, ui_->addFallingEdgeAction}); addToolbarMenu( ui_->logicToolBar, tr("更多输出"), QStringLiteral("logicOutputMenuButton"), makeUiIcon(UiIcon::Coil), QList{ui_->addSetCoilAction, ui_->addResetCoilAction}); addToolbarMenu( ui_->logicToolBar, tr("数据运算"), QStringLiteral("logicDataMenuButton"), makeUiIcon(UiIcon::Move), QList{ ui_->addMoveAction, ui_->addAddAction, ui_->addSubAction, ui_->addCompareAction}); 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_); } else { // 数据监控页不对应 HMI 或梯形图对象 showControlProperties({}); } if (index == 0 || index == 1) { 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(makeUiIcon(UiIcon::Edit)); ui_->offlineModeAction->setIcon(makeUiIcon(UiIcon::RunOffline)); ui_->onlineModeAction->setIcon(makeUiIcon(UiIcon::RunOnline)); ui_->newProjectAction->setIcon(makeUiIcon(UiIcon::NewDocument)); ui_->saveProjectAction->setIcon(makeUiIcon(UiIcon::Save)); ui_->saveAsProjectAction->setIcon(makeUiIcon(UiIcon::SaveAs)); ui_->loadProjectAction->setIcon(makeUiIcon(UiIcon::Open)); ui_->undoAction->setIcon(makeUiIcon(UiIcon::Undo)); ui_->redoAction->setIcon(makeUiIcon(UiIcon::Redo)); ui_->deleteSelectionAction->setIcon(makeUiIcon(UiIcon::Delete)); ui_->clearSelectionAction->setIcon(makeUiIcon(UiIcon::ClearList)); ui_->exitAction->setIcon(makeUiIcon(UiIcon::Exit)); ui_->configurePlcAction->setIcon(makeUiIcon(UiIcon::PlcSettings)); ui_->disconnectPlcAction->setIcon(makeUiIcon(UiIcon::Disconnect)); ui_->configureRegisterCommentsAction->setIcon( makeUiIcon(UiIcon::RegisterComments)); ui_->addButtonAction->setIcon(makeUiIcon(UiIcon::HmiButton)); ui_->addIndicatorAction->setIcon(makeUiIcon(UiIcon::Indicator)); ui_->addNumericDisplayAction->setIcon(makeUiIcon(UiIcon::NumericDisplay)); ui_->addNumericInputAction->setIcon(makeUiIcon(UiIcon::NumericInput)); ui_->addLabelAction->setIcon(makeUiIcon(UiIcon::Text)); ui_->addPageJumpAction->setIcon(makeUiIcon(UiIcon::PageJump)); ui_->addAlarmListAction->setIcon(makeUiIcon(UiIcon::AlarmList)); ui_->configureAlarmsAction->setIcon(makeUiIcon(UiIcon::AlarmSettings)); ui_->deleteControlAction->setIcon(makeUiIcon(UiIcon::Delete)); ui_->addRungAction->setIcon(makeUiIcon(UiIcon::AddRung)); ui_->insertRungAboveAction->setIcon(makeUiIcon(UiIcon::AddRung)); ui_->insertRungBelowAction->setIcon(makeUiIcon(UiIcon::AddRung)); ui_->deleteRungAction->setIcon(makeUiIcon(UiIcon::Delete)); ui_->mouseDrawWireAction->setIcon(makeUiIcon(UiIcon::MouseDrawWire)); ui_->mouseEraseWireAction->setIcon(makeUiIcon(UiIcon::MouseEraseWire)); ui_->parallelInsertAction->setIcon(makeUiIcon(UiIcon::ParallelBranch)); ui_->insertHorizontalWireAction->setIcon(makeUiIcon(UiIcon::HorizontalWire)); ui_->insertVerticalWireAction->setIcon(makeUiIcon(UiIcon::VerticalWire)); ui_->deleteHorizontalWireAction->setIcon(makeUiIcon(UiIcon::Delete)); ui_->deleteVerticalWireAction->setIcon(makeUiIcon(UiIcon::Delete)); ui_->addNormallyOpenAction->setIcon(makeUiIcon(UiIcon::NormallyOpenContact)); ui_->addNormallyClosedAction->setIcon(makeUiIcon(UiIcon::NormallyClosedContact)); ui_->addRisingEdgeAction->setIcon(makeUiIcon(UiIcon::RisingEdgeContact)); ui_->addFallingEdgeAction->setIcon(makeUiIcon(UiIcon::FallingEdgeContact)); ui_->addNormalCoilAction->setIcon(makeUiIcon(UiIcon::Coil)); ui_->addSetCoilAction->setIcon(makeUiIcon(UiIcon::SetCoil)); ui_->addResetCoilAction->setIcon(makeUiIcon(UiIcon::ResetCoil)); ui_->addMoveAction->setIcon(makeUiIcon(UiIcon::Move)); ui_->addAddAction->setIcon(makeUiIcon(UiIcon::Add)); ui_->addSubAction->setIcon(makeUiIcon(UiIcon::Subtract)); ui_->addCompareAction->setIcon(makeUiIcon(UiIcon::Compare)); ui_->editNetworkCommentAction->setIcon(makeUiIcon(UiIcon::Comment)); ui_->syntaxCheckAction->setIcon(makeUiIcon(UiIcon::SyntaxCheck)); ui_->doubleCoilCheckAction->setIcon(makeUiIcon(UiIcon::Coil)); ui_->deleteLogicAction->setIcon(makeUiIcon(UiIcon::Delete)); ui_->editorTabWidget->setTabIcon(0, makeUiIcon(UiIcon::HmiPage)); ui_->editorTabWidget->setTabIcon(1, makeUiIcon(UiIcon::Logic)); 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_); } 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_); property_panel_controller_->bindEditorWidgets( *hmi_editor_widget_, *logic_editor_widget_); connect( hmi_editor_widget_, &HmiEditorWidget::controlSelected, this, [this](const QString &) { updateEditActions(); }); connect( logic_editor_widget_, &LogicEditorWidget::nodeSelected, this, [this](const QString &) { updateEditActions(); }); } void MainWindow::configureRuntimeMonitor() { runtime_panel_controller_ = std::make_unique( *this, runtime_mode_service_, project_service_, hmi_editor_service_, hmi_runtime_service_, logic_editor_service_, *hmi_navigation_service_, alarm_service_, register_monitor_service_, *hmi_editor_widget_, *logic_editor_widget_, *executor_status_label_, [this] { return current_logic_id_; }, [this](const std::string &logic_id) { current_logic_id_ = logic_id; }, [this](const std::string &node_id) { showLogicNodeProperties(node_id); }, [this](const QString &message, int timeout_ms) { statusBar()->showMessage(message, timeout_ms); }, [this](const QString &message) { appendOutputMessage(message); }, [this] { if (user_runtime_mode_) { qApp->quit(); } else { requestMode(ApplicationMode::Editing); } }); runtime_panel_controller_->configure(); runtime_monitor_widget_ = runtime_panel_controller_->runtimeMonitorWidget(); } void MainWindow::configureDataMonitor() { data_monitor_widget_ = new FreeMonitorWidget( register_monitor_service_, ui_->dataMonitorTab); data_monitor_widget_->setObjectName(QStringLiteral("dataMonitorWidget")); ui_->dataMonitorLayout->addWidget(data_monitor_widget_); connect(data_monitor_widget_, &FreeMonitorWidget::monitorAddressesChanged, this, [this] { runtime_mode_service_.setMonitorAddresses( register_monitor_service_.pollAddresses(), register_monitor_service_.multiWordRanges()); refreshDataMonitorUi(); }); connect(data_monitor_widget_, &FreeMonitorWidget::operationMessage, this, [this](const QString &message) { statusBar()->showMessage(message, 4000); appendOutputMessage(message); }); register_monitor_service_.setAddressesChangedCallback( [this] { if (data_monitor_widget_ != nullptr) { data_monitor_widget_->reloadAddresses(); } if (runtime_monitor_widget_ != nullptr && runtime_monitor_widget_->freeMonitorWidget() != nullptr) { runtime_monitor_widget_->freeMonitorWidget()->reloadAddresses(); } runtime_mode_service_.setMonitorAddresses( register_monitor_service_.pollAddresses(), register_monitor_service_.multiWordRanges()); }); register_monitor_service_.setPollConfigurationValidator( [this](const std::vector &addresses, const std::vector &ranges) { const PlcCommunicationResult result = runtime_mode_service_.setMonitorAddresses(addresses, ranges); return result.succeeded ? std::string{} : result.message; }); data_monitor_refresh_timer_ = new QTimer(this); data_monitor_refresh_timer_->setInterval(150); connect(data_monitor_refresh_timer_, &QTimer::timeout, this, &MainWindow::refreshDataMonitorUi); data_monitor_refresh_timer_->start(); refreshDataMonitorUi(); } void MainWindow::refreshDataMonitorUi() { if (data_monitor_widget_ == nullptr) { return; } const ApplicationMode mode = runtime_mode_service_.mode(); const bool online_connected = mode == ApplicationMode::OnlineRunning && runtime_mode_service_.plcConnectionState() == PlcConnectionState::Connected; data_monitor_widget_->setWriteEnabled( mode == ApplicationMode::Editing || mode == ApplicationMode::OfflineRunning || online_connected); data_monitor_widget_->refreshValues( mode, runtime_mode_service_.plcConnectionState()); } void MainWindow::configureProjectTree() { project_workspace_controller_ = std::make_unique( *this, *ui_, project_service_, hmi_editor_service_, logic_editor_service_, runtime_mode_service_, *hmi_navigation_service_, *hmi_editor_widget_, *logic_editor_widget_, *runtime_monitor_widget_, current_hmi_page_id_, current_logic_id_, [this](const std::string &control_id) { showControlProperties(control_id); }, [this](const std::string &node_id) { showLogicNodeProperties(node_id); }, [this](const QString &action, const QString &message, bool succeeded) { showProjectResult(action, message, succeeded); }, [this](const QString &message, int timeout_ms) { statusBar()->showMessage(message, timeout_ms); }, [this] { updateEditActions(); }); project_workspace_controller_->configure(); } void MainWindow::configurePropertyEditor() { property_panel_controller_->configure(); } 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::configureRegisterComments() { connect( ui_->configureRegisterCommentsAction, &QAction::triggered, this, [this] { RegisterCommentDialog dialog(register_comment_service_, this); dialog.exec(); logic_editor_widget_->reloadLogic(); refreshProjectUi(); }); } void MainWindow::undoActiveEditor() { if (!runtime_mode_service_.policy().allowsProjectEditing) { return; } if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab) { if (!hmi_editor_service_.undo().succeeded) { return; } selected_control_id_.clear(); showControlProperties({}); refreshProjectUi(); hmi_editor_widget_->reloadPage(); statusBar()->showMessage(tr("已撤销 HMI 编辑操作"), 3000); } else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab) { if (!logic_editor_service_.undo().succeeded) { return; } selected_logic_node_id_.clear(); showLogicNodeProperties({}); refreshProjectUi(); logic_editor_widget_->reloadLogic(); statusBar()->showMessage(tr("已撤销梯形图编辑操作"), 3000); } updateEditActions(); } void MainWindow::redoActiveEditor() { if (!runtime_mode_service_.policy().allowsProjectEditing) { return; } if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab) { if (!hmi_editor_service_.redo().succeeded) { return; } selected_control_id_.clear(); showControlProperties({}); refreshProjectUi(); hmi_editor_widget_->reloadPage(); statusBar()->showMessage(tr("已重做 HMI 编辑操作"), 3000); } else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab) { if (!logic_editor_service_.redo().succeeded) { return; } selected_logic_node_id_.clear(); showLogicNodeProperties({}); refreshProjectUi(); logic_editor_widget_->reloadLogic(); statusBar()->showMessage(tr("已重做梯形图编辑操作"), 3000); } updateEditActions(); } void MainWindow::copyActiveSelection() { if (!runtime_mode_service_.policy().allowsProjectEditing || isTextEditingObject(qApp->focusWidget())) { return; } if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab) { const std::vector ids = hmi_editor_widget_->selectedControlIds(); std::vector controls; controls.reserve(ids.size()); for (const std::string &id : ids) { const HmiControl *control = hmi_editor_service_.findControl( current_hmi_page_id_, id); if (control != nullptr) { controls.push_back(*control); } } if (controls.empty()) { return; } editor_clipboard_ = HmiClipboardData{std::move(controls), 0}; statusBar()->showMessage(tr("已复制 %1 个 HMI 控件").arg(ids.size()), 3000); } else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab) { LogicClipboardCopyResult result = logic_editor_widget_->copySelection(); if (!result.copy.succeeded) { statusBar()->showMessage(fromUtf8(result.copy.message), 5000); return; } const bool whole_rows = result.fragment.mode == LogicClipboardMode::WholeRows; const std::size_t object_count = result.fragment.cells.size() + result.fragment.outputs.size() + result.fragment.verticalConnections.size(); editor_clipboard_ = LogicClipboardData{std::move(result.fragment)}; if (whole_rows) { const auto *data = std::get_if(&editor_clipboard_); statusBar()->showMessage( tr("已复制 %1 行梯形图").arg(data->fragment.rows.size()), 3000); } else { statusBar()->showMessage( tr("已复制 %1 个梯形图对象").arg(object_count), 3000); } } updateEditActions(); } void MainWindow::pasteActiveSelection() { if (!runtime_mode_service_.policy().allowsProjectEditing || isTextEditingObject(qApp->focusWidget())) { return; } if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab) { auto *data = std::get_if(&editor_clipboard_); if (data == nullptr) { return; } const int offset = 20 * std::min(data->pasteCount + 1, 10); const HmiEditorResult result = hmi_editor_service_.pasteControls( current_hmi_page_id_, data->controls, offset, offset); if (!result.succeeded) { showProjectResult(tr("粘贴 HMI 控件"), fromUtf8(result.message), false); return; } ++data->pasteCount; refreshProjectUi(); hmi_editor_widget_->reloadPage(); hmi_editor_widget_->selectControl(result.id); showControlProperties(result.id); statusBar()->showMessage(tr("已粘贴 HMI 控件"), 3000); } else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab) { const auto *data = std::get_if(&editor_clipboard_); if (data == nullptr) { return; } const LogicClipboardPasteResult result = logic_editor_widget_->pasteClipboard(data->fragment); if (!result.edit.succeeded) { if (!result.edit.message.empty()) { showProjectResult( tr("粘贴梯形图"), fromUtf8(result.edit.message), false); } return; } selected_logic_node_id_ = logic_editor_widget_->selectedNodeId(); showLogicNodeProperties(selected_logic_node_id_); refreshProjectUi(); statusBar()->showMessage(tr("已粘贴梯形图对象"), 3000); } updateEditActions(); } void MainWindow::deleteActiveSelection() { if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab) { deleteSelectedControl(); } else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab) { deleteSelectedLogicObject(); } } void MainWindow::clearActiveSelection() { if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab) { hmi_editor_widget_->scene()->clearSelection(); selected_control_id_.clear(); showControlProperties({}); } else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab) { logic_editor_widget_->clearSelection(); selected_logic_node_id_.clear(); showLogicNodeProperties({}); if (logic_editor_widget_->mouseWireMode() != LogicEditorWidget::MouseWireMode::Select) { const QSignalBlocker draw_blocker(ui_->mouseDrawWireAction); const QSignalBlocker erase_blocker(ui_->mouseEraseWireAction); ui_->mouseDrawWireAction->setChecked(false); ui_->mouseEraseWireAction->setChecked(false); logic_editor_widget_->setMouseWireMode( LogicEditorWidget::MouseWireMode::Select); } } } void MainWindow::updateEditActions() { const bool editable = runtime_mode_service_.policy().allowsProjectEditing; const bool hmi_active = ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab; const bool logic_active = ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab; ui_->undoAction->setEnabled( editable && ((hmi_active && hmi_editor_service_.canUndo()) || (logic_active && logic_editor_service_.canUndo()))); ui_->redoAction->setEnabled( editable && ((hmi_active && hmi_editor_service_.canRedo()) || (logic_active && logic_editor_service_.canRedo()))); const bool hmi_selection = hmi_active && !hmi_editor_widget_->selectedControlIds().empty(); const bool logic_selection = logic_active && logic_editor_widget_->hasCopyableSelection(); ui_->copyAction->setEnabled( editable && (hmi_selection || logic_selection)); const bool hmi_clipboard = std::holds_alternative(editor_clipboard_); const bool logic_clipboard = std::holds_alternative(editor_clipboard_); ui_->pasteAction->setEnabled(editable && ((hmi_active && hmi_clipboard) || (logic_active && logic_clipboard))); ui_->deleteSelectionAction->setEnabled(editable && (hmi_active || logic_active)); ui_->clearSelectionAction->setEnabled(editable && (hmi_active || logic_active)); ui_->insertHorizontalWireAction->setEnabled(editable && logic_active); ui_->insertVerticalWireAction->setEnabled(editable && logic_active); ui_->insertRungAboveAction->setEnabled(editable && logic_active); ui_->insertRungBelowAction->setEnabled(editable && logic_active); ui_->deleteRungAction->setEnabled(editable && logic_active); ui_->deleteHorizontalWireAction->setEnabled(editable && logic_active); ui_->deleteVerticalWireAction->setEnabled(editable && logic_active); ui_->mouseDrawWireAction->setEnabled(editable && logic_active); ui_->mouseEraseWireAction->setEnabled(editable && logic_active); ui_->syntaxCheckAction->setEnabled( editable && !current_logic_id_.empty()); ui_->doubleCoilCheckAction->setEnabled( editable && !current_logic_id_.empty()); } void MainWindow::clearEditorHistories() { hmi_editor_service_.clearHistory(); logic_editor_service_.clearHistory(); if (ui_->undoAction != nullptr) { updateEditActions(); } } void MainWindow::refreshProjectUi() { project_workspace_controller_->refresh(); updateEditActions(); updateWindowTitle(); } void MainWindow::updateWindowTitle() { QString title = tr("综合平台编程器"); if (project_service_.isModified()) { title += QStringLiteral("*"); } setWindowTitle(title); } bool MainWindow::confirmSaveBeforeDestructiveAction() { if (!project_service_.isModified()) { return true; } const QString project_name = fromUtf8(project_service_.project().metadata.name); const QMessageBox::StandardButton choice = QMessageBox::warning( this, tr("工程尚未保存"), tr("工程“%1”有未保存的修改,是否先保存?").arg(project_name), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Save); if (choice == QMessageBox::Cancel) { return false; } if (choice == QMessageBox::Discard) { return true; } saveProject(); return !project_service_.isModified(); } void MainWindow::updateProjectTreeActions() { project_workspace_controller_->updateActions(); } void MainWindow::connectPlc() { PlcConnectionDialog dialog( plc_configuration_, plc_discovery_gateway_, [this] { runtime_mode_service_.disconnectPlc(); }, 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) { property_panel_controller_->showControlProperties(control_id); } void MainWindow::showLogicNodeProperties(const std::string &node_id) { property_panel_controller_->showLogicNodeProperties(node_id); } void MainWindow::addHmiControl(HmiControlType type) { property_panel_controller_->addHmiControl(type); } void MainWindow::deleteSelectedControl() { property_panel_controller_->deleteSelectedControl(); } 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::addLogicHorizontalWire() { const LogicEditorResult result = logic_editor_widget_->addHorizontalWire(); if (!result.succeeded) { showProjectResult(tr("插入横线"), fromUtf8(result.message), false); return; } selected_logic_node_id_.clear(); showLogicNodeProperties({}); refreshProjectUi(); statusBar()->showMessage(tr("横线已插入,可直接用触点替换"), 3000); } void MainWindow::addLogicVerticalWire() { const LogicEditorResult result = logic_editor_widget_->addVerticalWire(); if (!result.succeeded) { showProjectResult(tr("插入竖线"), fromUtf8(result.message), false); return; } selected_logic_node_id_.clear(); showLogicNodeProperties({}); refreshProjectUi(); statusBar()->showMessage( result.message.empty() ? tr("竖线连接已建立") : fromUtf8(result.message), 3000); } void MainWindow::deleteLogicHorizontalWire() { const LogicEditorResult result = logic_editor_widget_->deleteHorizontalWire(); if (!result.succeeded) { showProjectResult(tr("删除横线"), fromUtf8(result.message), false); return; } selected_logic_node_id_.clear(); showLogicNodeProperties({}); refreshProjectUi(); statusBar()->showMessage(tr("横线已删除"), 3000); } void MainWindow::deleteLogicVerticalWire() { const LogicEditorResult result = logic_editor_widget_->deleteVerticalWire(); if (!result.succeeded) { showProjectResult(tr("删除竖线"), fromUtf8(result.message), false); return; } selected_logic_node_id_.clear(); showLogicNodeProperties({}); 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::configureAndSetLogicOutput(const LogicNodeConfig &config) { LogicInstructionDialog dialog(config, this); if (dialog.exec() != QDialog::Accepted) { return; } const LogicNodeConfig configured = dialog.config(); LogicEditorResult result = logic_editor_widget_->setOutput(configured, true); if (!result.succeeded) { showProjectResult(tr("设置逻辑输出"), fromUtf8(result.message), false); return; } const std::string node_id = result.id; selected_logic_node_id_ = node_id; showLogicNodeProperties(node_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::insertLogicRungAbove() { const LogicEditorResult result = logic_editor_widget_->insertRung(false); if (!result.succeeded) { showProjectResult(tr("上方插入行"), fromUtf8(result.message), false); return; } selected_logic_node_id_.clear(); showLogicNodeProperties({}); refreshProjectUi(); statusBar()->showMessage(tr("已在当前行上方插入空白行"), 3000); } void MainWindow::insertLogicRungBelow() { const LogicEditorResult result = logic_editor_widget_->insertRung(true); if (!result.succeeded) { showProjectResult(tr("下方插入行"), fromUtf8(result.message), false); return; } selected_logic_node_id_.clear(); showLogicNodeProperties({}); refreshProjectUi(); statusBar()->showMessage(tr("已在当前行下方插入空白行"), 3000); } void MainWindow::deleteLogicRung() { const LogicEditorResult result = logic_editor_widget_->deleteRung(); if (!result.succeeded) { showProjectResult(tr("删除行"), fromUtf8(result.message), false); return; } selected_logic_node_id_.clear(); showLogicNodeProperties({}); refreshProjectUi(); statusBar()->showMessage(tr("当前行已删除,竖线连接已重新整理"), 3000); } void MainWindow::editSelectedNetworkComment() { const std::string rung_id = logic_editor_widget_->selectedRungId(); if (rung_id.empty()) { showProjectResult(tr("网络注释"), tr("请先选择一个梯形图网络"), false); return; } const LadderRung *head = logic_editor_service_.findNetworkHeadRung( current_logic_id_, rung_id); if (head == nullptr) { return; } bool accepted = false; const QString comment = QInputDialog::getText( this, tr("网络注释"), tr("说明"), QLineEdit::Normal, fromUtf8(head->comment), &accepted); if (!accepted) { return; } const LogicEditorResult result = logic_editor_service_.updateNetworkComment( current_logic_id_, rung_id, toUtf8(comment)); if (!result.succeeded) { showProjectResult(tr("网络注释"), fromUtf8(result.message), false); return; } logic_editor_widget_->reloadLogic(); refreshProjectUi(); statusBar()->showMessage(tr("网络注释已更新"), 3000); } void MainWindow::runLogicSyntaxCheck() { const LogicSyntaxCheckResult result = logic_editor_service_.checkSyntax( current_logic_id_); reportLogicSyntaxCheck(result, tr("语法检查")); } void MainWindow::runDoubleCoilCheck() { const LogicSyntaxCheckResult result = logic_editor_service_.checkDoubleCoils(current_logic_id_); reportLogicSyntaxCheck(result, tr("双线圈检查")); } void MainWindow::reportLogicSyntaxCheck( const LogicSyntaxCheckResult &result, const QString &action) { if (result.changed) { logic_editor_widget_->reloadLogic(); refreshProjectUi(); } QString message = action + QStringLiteral(": ") + fromUtf8(result.message); if (result.removedWireCells > 0U || result.removedVerticalConnections > 0U) { message += tr(";已规整 %1 格横线、%2 段竖线") .arg(result.removedWireCells) .arg(result.removedVerticalConnections); } appendOutputMessage(message, result.location); ui_->outputDock->show(); ui_->outputDock->raise(); statusBar()->showMessage(message, 6000); if (!result.valid && result.location.has_value()) { focusLogicSyntaxLocation(*result.location); } updateEditActions(); } void MainWindow::focusLogicSyntaxLocation( const LogicSyntaxLocation &location) { if (logic_editor_service_.findRung( location.logicId, location.rungId) == nullptr) { return; } current_logic_id_ = location.logicId; ui_->editorTabWidget->setCurrentWidget(ui_->logicEditorTab); refreshProjectUi(); logic_editor_widget_->focusSyntaxLocation( location.rungId, location.column); } void MainWindow::deleteSelectedLogicObject() { const LogicEditorResult result = logic_editor_widget_->deleteSelected(); if (!result.succeeded) { return; } selected_logic_node_id_.clear(); showLogicNodeProperties({}); refreshProjectUi(); } void MainWindow::createNewProject() { if (!confirmSaveBeforeDestructiveAction()) { return; } QInputDialog name_dialog(this); name_dialog.setWindowTitle(tr("新建工程")); name_dialog.setLabelText(tr("工程名称")); name_dialog.setInputMode(QInputDialog::TextInput); QDialogButtonBox *button_box = name_dialog.findChild(); QPushButton *ok_button = button_box != nullptr ? button_box->button(QDialogButtonBox::Ok) : nullptr; if (ok_button != nullptr) { ok_button->setEnabled(false); connect(&name_dialog, &QInputDialog::textValueChanged, &name_dialog, [ok_button](const QString &text) { ok_button->setEnabled(!text.trimmed().isEmpty()); }); } if (name_dialog.exec() != QDialog::Accepted) { return; } const QString name = name_dialog.textValue().trimmed(); 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(); clearEditorHistories(); selected_control_id_.clear(); selected_logic_node_id_.clear(); editor_clipboard_ = std::monostate{}; 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() { if (!project_service_.hasCurrentFile()) { saveProjectAs(); return; } const ProjectOperationResult result = project_service_.save(); if (!result.succeeded) { showProjectResult(tr("保存工程"), fromUtf8(result.message), false); return; } updateWindowTitle(); showProjectResult(tr("保存工程"), tr("工程已保存"), true); } void MainWindow::saveProjectAs() { const QString suggested_file_name = suggestedProjectFileName( fromUtf8(project_service_.project().metadata.name)); const QString path = QFileDialog::getSaveFileName( this, tr("工程另存为"), suggested_file_name, tr("工程文件 (*.json)")); if (path.isEmpty()) { return; } const ProjectOperationResult result = project_service_.saveAs(toUtf8(path)); if (result.succeeded) { updateWindowTitle(); } showProjectResult( tr("保存工程"), result.succeeded ? tr("工程已保存") : fromUtf8(result.message), result.succeeded); } void MainWindow::loadProject() { if (!confirmSaveBeforeDestructiveAction()) { return; } 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; } clearEditorHistories(); selected_control_id_.clear(); selected_logic_node_id_.clear(); editor_clipboard_ = std::monostate{}; 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::exportRuntimeProgram() { const LogicSyntaxCheckResult syntax = logic_editor_service_.checkEnabledSyntax(); if (syntax.changed || !syntax.completed || !syntax.valid) { reportLogicSyntaxCheck(syntax, tr("导出前语法检查")); } if (!syntax.completed || !syntax.valid) { return; } std::string validation_error; if (!project_service_.project().validateForRunning( project_service_.projectLimits(), &validation_error)) { showProjectResult( tr("导出用户运行程序"), tr("工程还不能运行:%1").arg(fromUtf8(validation_error)), false); return; } QString suggested_name = suggestedProjectFileName( fromUtf8(project_service_.project().metadata.name)); suggested_name.chop(QStringLiteral(".json").size()); const QString parent_directory = QFileDialog::getExistingDirectory( this, tr("选择用户运行程序的目标父目录"), QDir::homePath(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); if (parent_directory.isEmpty()) { return; } bool name_accepted = false; const QString requested_name = QInputDialog::getText( this, tr("导出用户运行程序"), tr("请输入导出文件夹名称"), QLineEdit::Normal, suggested_name, &name_accepted).trimmed(); if (!name_accepted || requested_name.isEmpty()) { return; } const QString output_name = suggestedProjectFileName(requested_name).chopped( QStringLiteral(".json").size()); const QString destination_directory = QDir(parent_directory).filePath(output_name); if (QFileInfo::exists(destination_directory)) { if (!QFileInfo(destination_directory).isDir()) { showProjectResult( tr("导出用户运行程序"), tr("导出目标已经存在且不是文件夹:%1").arg(destination_directory), false); return; } const QMessageBox::StandardButton answer = QMessageBox::question( this, tr("覆盖已有导出目录"), tr("文件夹“%1”已经存在,是否覆盖?").arg(destination_directory), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (answer != QMessageBox::Yes) { return; } if (!QDir(destination_directory).removeRecursively()) { showProjectResult( tr("导出用户运行程序"), tr("无法清理已有导出目录:%1").arg(destination_directory), false); return; } } const QString temp_project = QDir::tempPath() + QStringLiteral("/qtproxinje-runtime-") + QString::number(QCoreApplication::applicationPid()) + QStringLiteral(".json"); const ProjectOperationResult save_result = project_service_.exportAs( temp_project.toUtf8().toStdString()); if (!save_result.succeeded) { showProjectResult(tr("导出用户运行程序"), fromUtf8(save_result.message), false); return; } QProgressDialog progress( tr("正在准备导出用户运行程序…"), QString(), 0, 100, this); progress.setWindowTitle(tr("导出用户运行程序")); progress.setWindowModality(Qt::WindowModal); progress.setAutoClose(false); progress.setAutoReset(false); progress.setMinimumDuration(0); progress.setValue(5); progress.show(); QApplication::processEvents(); ui_->exportRuntimeAction->setEnabled(false); const QString destination_executable = QDir(destination_directory).filePath( output_name + QStringLiteral(".exe")); const QString template_executable = QCoreApplication::applicationFilePath(); const auto failExport = [this, &progress, &temp_project, &destination_directory]( const QString &message) { QFile::remove(temp_project); QDir(destination_directory).removeRecursively(); progress.setValue(0); progress.close(); ui_->exportRuntimeAction->setEnabled( runtime_mode_service_.policy().allowsProjectEditing); showProjectResult(tr("导出用户运行程序"), message, false); }; if (!QDir().mkpath(destination_directory)) { failExport(tr("无法创建导出目录:%1").arg(destination_directory)); return; } progress.setLabelText(tr("正在封装工程数据…")); progress.setValue(35); QApplication::processEvents(); const RuntimeProjectBundleWriteResult bundle_result = RuntimeProjectBundleService::write( template_executable, temp_project, destination_executable); if (!bundle_result.succeeded) { failExport(bundle_result.message); return; } progress.setLabelText(tr("正在复制 Qt 运行库和平台插件…")); progress.setValue(65); QApplication::processEvents(); const QString application_directory = QFileInfo(template_executable).absolutePath(); const QStringList runtime_files = { QStringLiteral("Qt5Core.dll"), QStringLiteral("Qt5Gui.dll"), QStringLiteral("Qt5Network.dll"), QStringLiteral("Qt5SerialBus.dll"), QStringLiteral("Qt5SerialPort.dll"), QStringLiteral("Qt5Widgets.dll"), QStringLiteral("libgcc_s_seh-1.dll"), QStringLiteral("libstdc++-6.dll"), QStringLiteral("libwinpthread-1.dll")}; for (const QString &runtime_file : runtime_files) { const QString source = QDir(application_directory).filePath(runtime_file); const QString destination = QDir(destination_directory).filePath(runtime_file); if (!QFileInfo::exists(source) || !QFile::copy(source, destination)) { failExport(tr("复制运行库失败:%1").arg(source)); return; } } QString copy_error; if (!copyDirectoryContents( QDir(application_directory).filePath(QStringLiteral("platforms")), QDir(destination_directory).filePath(QStringLiteral("platforms")), ©_error) || !copyDirectoryContents( QDir(application_directory).filePath(QStringLiteral("styles")), QDir(destination_directory).filePath(QStringLiteral("styles")), ©_error)) { failExport(copy_error); return; } const RuntimeProjectBundleLoadResult verification = RuntimeProjectBundleService::load(destination_executable); if (verification.status != RuntimeProjectBundleStatus::Loaded) { failExport(verification.message.isEmpty() ? tr("导出结果校验失败") : verification.message); return; } QFile::remove(temp_project); progress.setLabelText(tr("导出完成")); progress.setValue(100); QApplication::processEvents(); progress.close(); ui_->exportRuntimeAction->setEnabled( runtime_mode_service_.policy().allowsProjectEditing); showProjectResult( tr("导出用户运行程序"), tr("已导出:%1").arg(destination_executable), true); } void MainWindow::showProjectResult( const QString &action, const QString &message, bool succeeded) { const QString output = action + QStringLiteral(": ") + message; statusBar()->showMessage(output, 5000); appendOutputMessage(output); if (!succeeded) { QMessageBox::warning(this, action, message); } } void MainWindow::appendOutputMessage( const QString &message, const std::optional &location) { const int maximum = application_settings_result_ .settings.projectLimits.maximumOutputMessages; while (ui_->outputList->count() >= maximum) { delete ui_->outputList->takeItem(0); } auto *item = new QListWidgetItem(message, ui_->outputList); if (location.has_value()) { item->setData( kSyntaxLogicIdRole, fromUtf8(location->logicId)); item->setData( kSyntaxRungIdRole, fromUtf8(location->rungId)); item->setData(kSyntaxColumnRole, location->column); item->setToolTip(tr("双击定位到网络 %1,第 %2 行第 %3 列") .arg(location->network) .arg(location->row) .arg(location->column)); } ui_->outputList->scrollToBottom(); } bool MainWindow::requestMode(ApplicationMode requested_mode) { // UI 动作只提出目标模式,所有前置条件和仓库切换由 RuntimeModeService 决定 // 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 false; } } const bool runtime_request = requested_mode == ApplicationMode::OfflineRunning || requested_mode == ApplicationMode::OnlineRunning; if (runtime_request && (result.succeeded || result.error == ModeTransitionError::ProjectNotReady)) { const LogicSyntaxCheckResult &syntax = runtime_mode_service_.lastSyntaxCheck(); if (syntax.completed && (syntax.changed || !syntax.valid)) { reportLogicSyntaxCheck(syntax, tr("运行前语法检查")); } } if (!result.succeeded) { restoreCurrentModeAction(); QString message = transitionErrorText(result.error); if (result.error == ModeTransitionError::InitialPlcReadRequired) { message += tr(";当前状态:%1").arg(plcStatusText(runtime_mode_service_)); } if (!result.detail.empty()) { message += QStringLiteral(": ") + fromUtf8(result.detail); } if (result.error == ModeTransitionError::SimulationStartFailed) { const LogicScanResult &error = requested_mode == ApplicationMode::OnlineRunning ? runtime_mode_service_.onlineLogicMonitorService().lastError() : runtime_mode_service_.simulationError(); if (!error.message.empty()) { message += QStringLiteral(": ") + fromUtf8(error.message); } } statusBar()->showMessage(message, 5000); return false; } updateModeUi(tr("已进入%1").arg(modeText(runtime_mode_service_.mode()))); return true; } void MainWindow::updateModeUi(const QString &message) { // 按服务层策略统一启用/禁用编辑入口,避免单个按钮遗漏状态同步 const ApplicationMode mode = runtime_mode_service_.mode(); const ModePolicy policy = runtime_mode_service_.policy(); register_monitor_service_.setOfflineInitialCaptureEnabled( mode == ApplicationMode::Editing); restoreCurrentModeAction(); const bool running = mode != ApplicationMode::Editing; 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_panel_controller_->enterRuntime( hmi_navigation_service_->currentPageId(), current_logic_id_, mode, runtime_mode_service_.plcConnectionState()); } else { hmi_navigation_service_->stop(); runtime_panel_controller_->leaveRuntime( mode, runtime_mode_service_.plcConnectionState()); } // 将同一份模式策略同步到所有可编辑入口,避免只禁用部分操作 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 (!policy.allowsProjectEditing) { const QSignalBlocker draw_blocker(ui_->mouseDrawWireAction); const QSignalBlocker erase_blocker(ui_->mouseEraseWireAction); ui_->mouseDrawWireAction->setChecked(false); ui_->mouseEraseWireAction->setChecked(false); } 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_->configureRegisterCommentsAction->setEnabled(policy.allowsProjectEditing); ui_->deleteControlAction->setEnabled(policy.allowsProjectEditing); ui_->addNormallyOpenAction->setEnabled(policy.allowsProjectEditing); ui_->addNormallyClosedAction->setEnabled(policy.allowsProjectEditing); ui_->addRisingEdgeAction->setEnabled(policy.allowsProjectEditing); ui_->addFallingEdgeAction->setEnabled(policy.allowsProjectEditing); ui_->addNormalCoilAction->setEnabled(policy.allowsProjectEditing); ui_->addSetCoilAction->setEnabled(policy.allowsProjectEditing); ui_->addResetCoilAction->setEnabled(policy.allowsProjectEditing); ui_->addMoveAction->setEnabled(policy.allowsProjectEditing); ui_->addAddAction->setEnabled(policy.allowsProjectEditing); ui_->addSubAction->setEnabled(policy.allowsProjectEditing); ui_->addCompareAction->setEnabled(policy.allowsProjectEditing); ui_->editNetworkCommentAction->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); ui_->exportRuntimeAction->setEnabled(policy.allowsProjectEditing); updateProjectTreeActions(); updateEditActions(); 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); refreshDataMonitorUi(); statusBar()->showMessage(message, 4000); const bool duplicate = ui_->outputList->count() > 0 && ui_->outputList->item(ui_->outputList->count() - 1)->text() == message; if (!duplicate) { appendOutputMessage(message); } } void MainWindow::updateSimulationUi(bool report_fault) { runtime_panel_controller_->updateSimulationUi(report_fault); } // 根据当前真实运行模式,同步更新模式工具栏单选按钮选中状态 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); }