#include "main_window.h" #include "hmi_editor_widget.h" #include "services/hmi_editor_service.h" #include "services/hmi_runtime_service.h" #include "services/project_service.h" #include "services/runtime_mode_service.h" #include "ui_main_window.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { 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::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 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: default: { return MainWindow::tr("文本"); } } } bool usesMAddress(HmiControlType type) { return type == HmiControlType::Button || type == HmiControlType::Indicator; } } // namespace MainWindow::MainWindow( RuntimeModeService &runtime_mode_service, ProjectService &project_service, HmiEditorService &hmi_editor_service, HmiRuntimeService &hmi_runtime_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), hmi_runtime_service_(hmi_runtime_service) { ui_->setupUi(this); configureAppearance(); configureActions(); configurePropertyEditor(); configureHmiEditor(); hmi_editor_service_.ensureDefaultPage(); refreshProjectUi(); updateModeUi(tr("系统已进入编辑态")); } MainWindow::~MainWindow() = default; 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); new_project_action_ = new QAction(tr("新建工程"), this); save_project_action_ = new QAction(tr("保存工程"), this); save_as_project_action_ = new QAction(tr("工程另存为"), this); load_project_action_ = new QAction(tr("加载工程"), this); new_project_action_->setObjectName(QStringLiteral("newProjectAction")); save_project_action_->setObjectName(QStringLiteral("saveProjectAction")); save_as_project_action_->setObjectName(QStringLiteral("saveAsProjectAction")); load_project_action_->setObjectName(QStringLiteral("loadProjectAction")); new_project_action_->setIcon(style()->standardIcon(QStyle::SP_FileIcon)); save_project_action_->setIcon(style()->standardIcon(QStyle::SP_DialogSaveButton)); load_project_action_->setIcon(style()->standardIcon(QStyle::SP_DialogOpenButton)); ui_->fileMenu->insertAction(ui_->exitAction, new_project_action_); ui_->fileMenu->insertAction(ui_->exitAction, save_project_action_); ui_->fileMenu->insertAction(ui_->exitAction, save_as_project_action_); ui_->fileMenu->insertAction(ui_->exitAction, load_project_action_); ui_->fileMenu->insertSeparator(ui_->exitAction); connect(new_project_action_, &QAction::triggered, this, &MainWindow::createNewProject); connect(save_project_action_, &QAction::triggered, this, &MainWindow::saveProject); connect(save_as_project_action_, &QAction::triggered, this, &MainWindow::saveProjectAs); connect(load_project_action_, &QAction::triggered, this, &MainWindow::loadProject); hmi_tool_bar_ = addToolBar(tr("HMI 控件")); hmi_tool_bar_->setObjectName(QStringLiteral("hmiToolBar")); hmi_tool_bar_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); add_button_action_ = hmi_tool_bar_->addAction( style()->standardIcon(QStyle::SP_DialogApplyButton), tr("按钮")); add_indicator_action_ = hmi_tool_bar_->addAction( style()->standardIcon(QStyle::SP_DialogYesButton), tr("指示灯")); add_numeric_display_action_ = hmi_tool_bar_->addAction( style()->standardIcon(QStyle::SP_FileDialogInfoView), tr("数值显示")); add_numeric_input_action_ = hmi_tool_bar_->addAction( style()->standardIcon(QStyle::SP_FileDialogContentsView), tr("数值输入")); delete_control_action_ = hmi_tool_bar_->addAction( style()->standardIcon(QStyle::SP_TrashIcon), tr("删除")); add_button_action_->setObjectName(QStringLiteral("addButtonAction")); add_indicator_action_->setObjectName(QStringLiteral("addIndicatorAction")); add_numeric_display_action_->setObjectName(QStringLiteral("addNumericDisplayAction")); add_numeric_input_action_->setObjectName(QStringLiteral("addNumericInputAction")); delete_control_action_->setObjectName(QStringLiteral("deleteControlAction")); add_button_action_->setToolTip(tr("添加按钮控件")); add_indicator_action_->setToolTip(tr("添加指示灯控件")); add_numeric_display_action_->setToolTip(tr("添加数值显示控件")); add_numeric_input_action_->setToolTip(tr("添加数值输入控件")); delete_control_action_->setToolTip(tr("删除当前控件")); connect(add_button_action_, &QAction::triggered, this, [this] { addHmiControl(HmiControlType::Button); }); connect(add_indicator_action_, &QAction::triggered, this, [this] { addHmiControl(HmiControlType::Indicator); }); connect(add_numeric_display_action_, &QAction::triggered, this, [this] { addHmiControl(HmiControlType::NumericDisplay); }); connect(add_numeric_input_action_, &QAction::triggered, this, [this] { addHmiControl(HmiControlType::NumericInput); }); connect(delete_control_action_, &QAction::triggered, this, &MainWindow::deleteSelectedControl); ui_->viewMenu->addAction(ui_->projectDock->toggleViewAction()); ui_->viewMenu->addAction(ui_->propertiesDock->toggleViewAction()); ui_->viewMenu->addAction(ui_->outputDock->toggleViewAction()); ui_->viewMenu->addAction(hmi_tool_bar_->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_->editorTabWidget->setTabIcon(0, style()->standardIcon(QStyle::SP_DesktopIcon)); ui_->editorTabWidget->setTabIcon(1, style()->standardIcon(QStyle::SP_FileDialogListView)); 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_->setMinimumWidth(132); executor_status_label_ = new QLabel(this); 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_, 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) { hmi_editor_widget_->refreshRuntimeValues(); } }); runtime_refresh_timer_->start(); } void MainWindow::configurePropertyEditor() { control_id_edit_ = new QLineEdit(ui_->propertiesPage); control_id_edit_->setObjectName(QStringLiteral("controlIdEdit")); control_text_edit_ = new QLineEdit(ui_->propertiesPage); control_text_edit_->setObjectName(QStringLiteral("controlTextEdit")); control_x_spin_box_ = new QSpinBox(ui_->propertiesPage); control_y_spin_box_ = new QSpinBox(ui_->propertiesPage); control_width_spin_box_ = new QSpinBox(ui_->propertiesPage); control_height_spin_box_ = new QSpinBox(ui_->propertiesPage); control_x_spin_box_->setObjectName(QStringLiteral("controlXSpinBox")); control_y_spin_box_->setObjectName(QStringLiteral("controlYSpinBox")); control_width_spin_box_->setObjectName(QStringLiteral("controlWidthSpinBox")); control_height_spin_box_->setObjectName(QStringLiteral("controlHeightSpinBox")); for (QSpinBox *box : {control_x_spin_box_, control_y_spin_box_}) { box->setRange(0, 4000); } for (QSpinBox *box : {control_width_spin_box_, control_height_spin_box_}) { box->setRange(1, 4000); } binding_area_combo_box_ = new QComboBox(ui_->propertiesPage); binding_area_combo_box_->setObjectName(QStringLiteral("bindingAreaComboBox")); binding_area_combo_box_->addItem(QStringLiteral("M"), QVariant::fromValue(0)); binding_area_combo_box_->addItem(QStringLiteral("D"), QVariant::fromValue(1)); binding_index_spin_box_ = new QSpinBox(ui_->propertiesPage); binding_index_spin_box_->setObjectName(QStringLiteral("bindingIndexSpinBox")); binding_index_spin_box_->setRange(0, RegisterAddress::kMaximumIndex); apply_properties_button_ = new QPushButton(tr("应用属性"), ui_->propertiesPage); apply_properties_button_->setObjectName(QStringLiteral("applyPropertiesButton")); QFormLayout *form = ui_->propertiesForm; form->addRow(tr("控件 ID"), control_id_edit_); form->addRow(tr("显示文本"), control_text_edit_); form->addRow(tr("X"), control_x_spin_box_); form->addRow(tr("Y"), control_y_spin_box_); form->addRow(tr("宽度"), control_width_spin_box_); form->addRow(tr("高度"), control_height_spin_box_); form->addRow(tr("绑定区域"), binding_area_combo_box_); form->addRow(tr("绑定地址"), binding_index_spin_box_); form->addRow(apply_properties_button_); connect(apply_properties_button_, &QPushButton::clicked, this, &MainWindow::applySelectedControlProperties); showControlProperties({}); } void MainWindow::refreshProjectUi() { const std::string current_page_id = hmi_editor_service_.firstPageId(); if (hmi_editor_widget_ != nullptr) { hmi_editor_widget_->setPageId(current_page_id); } const HmiPage *page = hmi_editor_service_.findPage(current_page_id); ui_->projectTree->clear(); auto *hmi_root = new QTreeWidgetItem(ui_->projectTree, {tr("HMI 页面")}); if (page != nullptr) { hmi_root->addChild(new QTreeWidgetItem(hmi_root, {fromUtf8(page->name)})); ui_->hmiPageTitleLabel->setText(fromUtf8(page->name)); ui_->hmiPageSizeLabel->setText( tr("%1 x %2").arg(page->width).arg(page->height)); } else { ui_->hmiPageTitleLabel->setText(tr("尚未创建 HMI 页面")); ui_->hmiPageSizeLabel->clear(); } new QTreeWidgetItem(ui_->projectTree, {tr("控制逻辑")}); ui_->projectTree->expandAll(); } void MainWindow::showControlProperties(const std::string &control_id) { selected_control_id_ = control_id; const HmiControl *control = hmi_editor_service_.findControl( hmi_editor_service_.firstPageId(), control_id); const bool has_control = control != nullptr; ui_->selectionValueLabel->setText( has_control ? fromUtf8(control->id) : tr("未选择")); for (QWidget *widget : {static_cast(control_id_edit_), static_cast(control_text_edit_), static_cast(control_x_spin_box_), static_cast(control_y_spin_box_), static_cast(control_width_spin_box_), static_cast(control_height_spin_box_), static_cast(binding_area_combo_box_), static_cast(binding_index_spin_box_), static_cast(apply_properties_button_)}) { widget->setEnabled(has_control); } if (!has_control) { return; } control_id_edit_->setText(fromUtf8(control->id)); control_text_edit_->setText(fromUtf8(control->text)); control_x_spin_box_->setValue(control->bounds.x); control_y_spin_box_->setValue(control->bounds.y); control_width_spin_box_->setValue(control->bounds.width); control_height_spin_box_->setValue(control->bounds.height); const RegisterArea default_area = usesMAddress(control->type) ? RegisterArea::M : RegisterArea::D; const RegisterArea area = control->binding.has_value() ? control->binding->area() : default_area; binding_area_combo_box_->setCurrentIndex(area == RegisterArea::M ? 0 : 1); binding_index_spin_box_->setValue( control->binding.has_value() ? control->binding->index() : 0); } void MainWindow::addHmiControl(HmiControlType type) { const std::string page_id = hmi_editor_service_.firstPageId(); const HmiEditorResult result = hmi_editor_service_.addControl(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( hmi_editor_service_.firstPageId(), 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( hmi_editor_service_.firstPageId(), selected_control_id_); if (old_control == nullptr) { return; } HmiControl control = *old_control; control.id = toUtf8(control_id_edit_->text()); control.text = toUtf8(control_text_edit_->text()); control.bounds.x = control_x_spin_box_->value(); control.bounds.y = control_y_spin_box_->value(); control.bounds.width = control_width_spin_box_->value(); control.bounds.height = control_height_spin_box_->value(); const RegisterArea area = binding_area_combo_box_->currentIndex() == 0 ? RegisterArea::M : RegisterArea::D; control.binding = RegisterAddress{area, binding_index_spin_box_->value()}; const HmiEditorResult result = hmi_editor_service_.updateControl( hmi_editor_service_.firstPageId(), 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::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(); selected_control_id_.clear(); refreshProjectUi(); hmi_editor_widget_->reloadPage(); 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(); refreshProjectUi(); hmi_editor_widget_->reloadPage(); 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) { 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(); statusBar()->showMessage(transitionErrorText(result.error), 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(); ui_->projectDock->setEnabled(policy.allowsProjectEditing); ui_->propertiesDock->setEnabled(policy.allowsProjectEditing); hmi_editor_widget_->setEditingEnabled(policy.allowsProjectEditing); hmi_editor_widget_->setRuntimeActive(policy.usesVirtualRegisters); hmi_tool_bar_->setEnabled(policy.allowsProjectEditing); add_button_action_->setEnabled(policy.allowsProjectEditing); add_indicator_action_->setEnabled(policy.allowsProjectEditing); add_numeric_display_action_->setEnabled(policy.allowsProjectEditing); add_numeric_input_action_->setEnabled(policy.allowsProjectEditing); delete_control_action_->setEnabled(policy.allowsProjectEditing); new_project_action_->setEnabled(policy.allowsProjectEditing); save_project_action_->setEnabled(policy.allowsProjectEditing); save_as_project_action_->setEnabled(policy.allowsProjectEditing); load_project_action_->setEnabled(policy.allowsProjectEditing); 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("数据源:未启用")); executor_status_label_->setText( policy.runsLogicExecutor ? tr("逻辑执行器:运行") : tr("逻辑执行器:停止")); statusBar()->showMessage(message, 4000); ui_->outputList->addItem(message); ui_->outputList->scrollToBottom(); } 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); }