综合平台编程器项目的远程存储
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1946 line
73 KiB

  1. #include "main_window.h"
  2. #include "hmi_editor_widget.h"
  3. #include "alarm_configuration_dialog.h"
  4. #include "logic_editor_widget.h"
  5. #include "plc_connection_dialog.h"
  6. #include "runtime_monitor_widget.h"
  7. #include "free_monitor_widget.h"
  8. #include "services/hmi_editor_service.h"
  9. #include "services/hmi_navigation_service.h"
  10. #include "services/hmi_runtime_service.h"
  11. #include "services/alarm_editor_service.h"
  12. #include "services/alarm_service.h"
  13. #include "services/logic_editor_service.h"
  14. #include "services/project_service.h"
  15. #include "services/runtime_mode_service.h"
  16. #include "services/register_monitor_service.h"
  17. #include "ui_main_window.h"
  18. #include <QActionGroup>
  19. #include <QApplication>
  20. #include <QComboBox>
  21. #include <QFileDialog>
  22. #include <QInputDialog>
  23. #include <QLabel>
  24. #include <QLineEdit>
  25. #include <QMessageBox>
  26. #include <QMenu>
  27. #include <QPushButton>
  28. #include <QSignalBlocker>
  29. #include <QSpinBox>
  30. #include <QStatusBar>
  31. #include <QStyle>
  32. #include <QTimer>
  33. #include <QTabWidget>
  34. #include <QToolButton>
  35. #include <QTreeWidget>
  36. #include <QTreeWidgetItem>
  37. #include <cstdint>
  38. #include <type_traits>
  39. namespace {
  40. enum class ProjectTreeItemKind
  41. {
  42. Root = 0,
  43. HmiPage = 1,
  44. ControlLogic = 2
  45. };
  46. constexpr int kProjectTreeKindRole = Qt::UserRole;
  47. constexpr int kProjectTreeIdRole = Qt::UserRole + 1;
  48. QString modeText(ApplicationMode mode)
  49. {
  50. switch (mode)
  51. {
  52. case ApplicationMode::Editing:
  53. {
  54. return MainWindow::tr("编辑态");
  55. }
  56. case ApplicationMode::OfflineRunning:
  57. {
  58. return MainWindow::tr("离线运行态");
  59. }
  60. case ApplicationMode::OnlineRunning:
  61. {
  62. return MainWindow::tr("真机运行态");
  63. }
  64. default:
  65. {
  66. return MainWindow::tr("未知状态");
  67. }
  68. }
  69. }
  70. QString transitionErrorText(ModeTransitionError error)
  71. {
  72. switch (error)
  73. {
  74. case ModeTransitionError::MustReturnToEditing:
  75. {
  76. return MainWindow::tr("请先返回编辑态,再切换运行模式");
  77. }
  78. case ModeTransitionError::InitialPlcReadRequired:
  79. {
  80. return MainWindow::tr("真机运行前必须连接 PLC 并完成首次读取");
  81. }
  82. case ModeTransitionError::AlreadyInRequestedMode:
  83. {
  84. return MainWindow::tr("当前已处于所选模式");
  85. }
  86. case ModeTransitionError::ProjectNotReady:
  87. {
  88. return MainWindow::tr("工程存在未配置或未完成的 HMI/梯形图节点,暂不能进入离线运行态");
  89. }
  90. case ModeTransitionError::SimulationStartFailed:
  91. {
  92. return MainWindow::tr("离线逻辑执行器预检或启动失败");
  93. }
  94. case ModeTransitionError::None:
  95. default:
  96. {
  97. return MainWindow::tr("模式切换失败");
  98. }
  99. }
  100. }
  101. std::string toUtf8(const QString &value)
  102. {
  103. const QByteArray bytes = value.toUtf8();
  104. return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size()));
  105. }
  106. QString fromUtf8(const std::string &value)
  107. {
  108. return QString::fromUtf8(value.data(), static_cast<int>(value.size()));
  109. }
  110. QString plcStatusText(const RuntimeModeService &service)
  111. {
  112. switch (service.plcConnectionState())
  113. {
  114. case PlcConnectionState::Connecting:
  115. return MainWindow::tr("PLC 正在连接");
  116. case PlcConnectionState::Connected:
  117. return service.initialPlcReadCompleted()
  118. ? MainWindow::tr("PLC 已连接,首次读取完成")
  119. : MainWindow::tr("PLC 已连接,正在读取工程使用的 M/D 地址");
  120. case PlcConnectionState::Recovering:
  121. return MainWindow::tr("PLC 通信已恢复,正在重新读取工程使用的 M/D 地址");
  122. case PlcConnectionState::Faulted:
  123. {
  124. const QString error = fromUtf8(service.plcError());
  125. return error.isEmpty()
  126. ? MainWindow::tr("PLC 通信故障")
  127. : MainWindow::tr("PLC 通信故障:%1").arg(error);
  128. }
  129. case PlcConnectionState::Disconnected:
  130. {
  131. const QString error = fromUtf8(service.plcError());
  132. return error.isEmpty()
  133. ? MainWindow::tr("PLC 已断开")
  134. : MainWindow::tr("PLC 已断开:%1").arg(error);
  135. }
  136. default:
  137. {
  138. return MainWindow::tr("PLC 已断开");
  139. }
  140. }
  141. }
  142. QString controlTypeText(HmiControlType type)
  143. {
  144. switch (type)
  145. {
  146. case HmiControlType::Button:
  147. {
  148. return MainWindow::tr("按钮");
  149. }
  150. case HmiControlType::Indicator:
  151. {
  152. return MainWindow::tr("指示灯");
  153. }
  154. case HmiControlType::NumericDisplay:
  155. {
  156. return MainWindow::tr("数值显示");
  157. }
  158. case HmiControlType::NumericInput:
  159. {
  160. return MainWindow::tr("数值输入");
  161. }
  162. case HmiControlType::Label:
  163. {
  164. return MainWindow::tr("文本");
  165. }
  166. case HmiControlType::PageJump:
  167. {
  168. return MainWindow::tr("页面跳转");
  169. }
  170. case HmiControlType::AlarmList:
  171. {
  172. return MainWindow::tr("报警列表");
  173. }
  174. default:
  175. {
  176. return MainWindow::tr("未知控件");
  177. }
  178. }
  179. }
  180. } // namespace
  181. MainWindow::MainWindow(
  182. RuntimeModeService &runtime_mode_service,
  183. ProjectService &project_service,
  184. HmiEditorService &hmi_editor_service,
  185. LogicEditorService &logic_editor_service,
  186. HmiRuntimeService &hmi_runtime_service,
  187. AlarmEditorService &alarm_editor_service,
  188. AlarmService &alarm_service,
  189. RegisterMonitorService &register_monitor_service,
  190. QWidget *parent)
  191. : QMainWindow(parent),
  192. ui_(std::make_unique<Ui::MainWindow>()),
  193. runtime_mode_service_(runtime_mode_service),
  194. project_service_(project_service),
  195. hmi_editor_service_(hmi_editor_service),
  196. logic_editor_service_(logic_editor_service),
  197. hmi_runtime_service_(hmi_runtime_service),
  198. alarm_editor_service_(alarm_editor_service),
  199. alarm_service_(alarm_service),
  200. register_monitor_service_(register_monitor_service),
  201. owned_hmi_navigation_service_(
  202. std::make_unique<HmiNavigationService>(project_service)),
  203. hmi_navigation_service_(owned_hmi_navigation_service_.get())
  204. {
  205. initializeUi();
  206. }
  207. MainWindow::MainWindow(
  208. RuntimeModeService &runtime_mode_service,
  209. ProjectService &project_service,
  210. HmiEditorService &hmi_editor_service,
  211. LogicEditorService &logic_editor_service,
  212. HmiRuntimeService &hmi_runtime_service,
  213. AlarmEditorService &alarm_editor_service,
  214. AlarmService &alarm_service,
  215. HmiNavigationService &hmi_navigation_service,
  216. RegisterMonitorService &register_monitor_service,
  217. QWidget *parent)
  218. : QMainWindow(parent),
  219. ui_(std::make_unique<Ui::MainWindow>()),
  220. runtime_mode_service_(runtime_mode_service),
  221. project_service_(project_service),
  222. hmi_editor_service_(hmi_editor_service),
  223. logic_editor_service_(logic_editor_service),
  224. hmi_runtime_service_(hmi_runtime_service),
  225. alarm_editor_service_(alarm_editor_service),
  226. alarm_service_(alarm_service),
  227. register_monitor_service_(register_monitor_service),
  228. hmi_navigation_service_(&hmi_navigation_service)
  229. {
  230. initializeUi();
  231. }
  232. void MainWindow::initializeUi()
  233. {
  234. ui_->setupUi(this);
  235. configureAppearance();
  236. configureActions();
  237. configurePropertyEditor();
  238. configurePlcConnection();
  239. configureAlarms();
  240. configureHmiEditor();
  241. configureLogicEditor();
  242. configureRuntimeMonitor();
  243. configureProjectTree();
  244. runtime_mode_service_.setPlcStatusChangedCallback(
  245. [this] { schedulePlcStatusUpdate(); });
  246. hmi_editor_service_.ensureDefaultPage();
  247. logic_editor_service_.ensureDefaultLogic();
  248. current_hmi_page_id_ = hmi_editor_service_.firstPageId();
  249. current_logic_id_ = logic_editor_service_.firstLogicId();
  250. refreshProjectUi();
  251. updateModeUi(tr("系统已进入编辑态"));
  252. }
  253. MainWindow::~MainWindow()
  254. {
  255. runtime_mode_service_.setPlcStatusChangedCallback({});
  256. }
  257. const std::string &MainWindow::currentHmiPageId() const
  258. {
  259. return current_hmi_page_id_;
  260. }
  261. const std::string &MainWindow::currentLogicId() const
  262. {
  263. return current_logic_id_;
  264. }
  265. void MainWindow::configureActions()
  266. {
  267. mode_action_group_ = new QActionGroup(this);
  268. mode_action_group_->setExclusive(true);
  269. mode_action_group_->addAction(ui_->editingModeAction);
  270. mode_action_group_->addAction(ui_->offlineModeAction);
  271. mode_action_group_->addAction(ui_->onlineModeAction);
  272. connect(ui_->editingModeAction, &QAction::triggered, this,
  273. [this] { requestMode(ApplicationMode::Editing); });
  274. connect(ui_->offlineModeAction, &QAction::triggered, this,
  275. [this] { requestMode(ApplicationMode::OfflineRunning); });
  276. connect(ui_->onlineModeAction, &QAction::triggered, this,
  277. [this] { requestMode(ApplicationMode::OnlineRunning); });
  278. connect(ui_->exitAction, &QAction::triggered, qApp, &QApplication::closeAllWindows);
  279. connect(ui_->newProjectAction, &QAction::triggered, this, &MainWindow::createNewProject);
  280. connect(ui_->saveProjectAction, &QAction::triggered, this, &MainWindow::saveProject);
  281. connect(ui_->saveAsProjectAction, &QAction::triggered, this, &MainWindow::saveProjectAs);
  282. connect(ui_->loadProjectAction, &QAction::triggered, this, &MainWindow::loadProject);
  283. connect(ui_->addButtonAction, &QAction::triggered, this,
  284. [this] { addHmiControl(HmiControlType::Button); });
  285. connect(ui_->addIndicatorAction, &QAction::triggered, this,
  286. [this] { addHmiControl(HmiControlType::Indicator); });
  287. connect(ui_->addNumericDisplayAction, &QAction::triggered, this,
  288. [this] { addHmiControl(HmiControlType::NumericDisplay); });
  289. connect(ui_->addNumericInputAction, &QAction::triggered, this,
  290. [this] { addHmiControl(HmiControlType::NumericInput); });
  291. connect(ui_->addLabelAction, &QAction::triggered, this,
  292. [this] { addHmiControl(HmiControlType::Label); });
  293. connect(ui_->addPageJumpAction, &QAction::triggered, this,
  294. [this] { addHmiControl(HmiControlType::PageJump); });
  295. connect(ui_->addAlarmListAction, &QAction::triggered, this,
  296. [this] { addHmiControl(HmiControlType::AlarmList); });
  297. connect(ui_->deleteControlAction, &QAction::triggered,
  298. this, &MainWindow::deleteSelectedControl);
  299. connect(ui_->addRungAction, &QAction::triggered,
  300. this, &MainWindow::addLogicRung);
  301. connect(ui_->parallelInsertAction, &QAction::triggered,
  302. this,
  303. [this]
  304. {
  305. addLogicParallelBranch(ContactNodeConfig{
  306. RegisterAddress{RegisterArea::M, 0},
  307. ContactMode::NormallyOpen});
  308. });
  309. QMenu *parallel_menu = new QMenu(this);
  310. QAction *parallel_open = parallel_menu->addAction(tr("并联常开触点"));
  311. QAction *parallel_closed = parallel_menu->addAction(tr("并联常闭触点"));
  312. QAction *parallel_compare = parallel_menu->addAction(tr("并联比较条件"));
  313. connect(parallel_open, &QAction::triggered,
  314. this,
  315. [this]
  316. {
  317. addLogicParallelBranch(ContactNodeConfig{
  318. RegisterAddress{RegisterArea::M, 0},
  319. ContactMode::NormallyOpen});
  320. });
  321. connect(parallel_closed, &QAction::triggered,
  322. this,
  323. [this]
  324. {
  325. addLogicParallelBranch(ContactNodeConfig{
  326. RegisterAddress{RegisterArea::M, 0},
  327. ContactMode::NormallyClosed});
  328. });
  329. connect(parallel_compare, &QAction::triggered,
  330. this,
  331. [this]
  332. {
  333. addLogicParallelBranch(CompareNodeConfig{
  334. RegisterAddress{RegisterArea::D, 0},
  335. ComparisonOperator::Equal,
  336. 0});
  337. });
  338. ui_->parallelInsertAction->setMenu(parallel_menu);
  339. if (QToolButton *parallel_button = qobject_cast<QToolButton *>(
  340. ui_->logicToolBar->widgetForAction(ui_->parallelInsertAction)))
  341. {
  342. parallel_button->setPopupMode(QToolButton::MenuButtonPopup);
  343. }
  344. connect(ui_->addNormallyOpenAction, &QAction::triggered,
  345. this,
  346. [this]
  347. {
  348. addLogicCondition(ContactNodeConfig{
  349. RegisterAddress{RegisterArea::M, 0},
  350. ContactMode::NormallyOpen});
  351. });
  352. connect(ui_->addNormallyClosedAction, &QAction::triggered,
  353. this,
  354. [this]
  355. {
  356. addLogicCondition(ContactNodeConfig{
  357. RegisterAddress{RegisterArea::M, 0},
  358. ContactMode::NormallyClosed});
  359. });
  360. connect(ui_->addNormalCoilAction, &QAction::triggered,
  361. this,
  362. [this]
  363. {
  364. setLogicOutput(CoilNodeConfig{
  365. RegisterAddress{RegisterArea::M, 0},
  366. CoilMode::Normal});
  367. });
  368. connect(ui_->addSetCoilAction, &QAction::triggered,
  369. this,
  370. [this]
  371. {
  372. setLogicOutput(CoilNodeConfig{
  373. RegisterAddress{RegisterArea::M, 0},
  374. CoilMode::Set});
  375. });
  376. connect(ui_->addResetCoilAction, &QAction::triggered,
  377. this,
  378. [this]
  379. {
  380. setLogicOutput(CoilNodeConfig{
  381. RegisterAddress{RegisterArea::M, 0},
  382. CoilMode::Reset});
  383. });
  384. connect(ui_->addCompareAction, &QAction::triggered,
  385. this,
  386. [this]
  387. {
  388. addLogicCondition(CompareNodeConfig{
  389. RegisterAddress{RegisterArea::D, 0},
  390. ComparisonOperator::Equal,
  391. 0});
  392. });
  393. connect(ui_->deleteLogicAction, &QAction::triggered,
  394. this, &MainWindow::deleteSelectedLogicObject);
  395. connect(ui_->editorTabWidget, &QTabWidget::currentChanged,
  396. this,
  397. [this](int index)
  398. {
  399. ui_->hmiToolBar->setVisible(index == 0);
  400. ui_->logicToolBar->setVisible(index == 1);
  401. if (index == 0)
  402. {
  403. showControlProperties(selected_control_id_);
  404. }
  405. else if (index == 1)
  406. {
  407. showLogicNodeProperties(selected_logic_node_id_);
  408. }
  409. if (index == 0 || index == 1)
  410. {
  411. last_editing_tab_index_ = index;
  412. refreshProjectUi();
  413. }
  414. });
  415. ui_->hmiToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 0);
  416. ui_->logicToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 1);
  417. ui_->viewMenu->addAction(ui_->projectDock->toggleViewAction());
  418. ui_->viewMenu->addAction(ui_->propertiesDock->toggleViewAction());
  419. ui_->viewMenu->addAction(ui_->outputDock->toggleViewAction());
  420. ui_->viewMenu->addAction(ui_->hmiToolBar->toggleViewAction());
  421. ui_->viewMenu->addAction(ui_->logicToolBar->toggleViewAction());
  422. ui_->viewMenu->addSeparator();
  423. ui_->viewMenu->addAction(ui_->modeToolBar->toggleViewAction());
  424. }
  425. void MainWindow::configureAppearance()
  426. {
  427. setDockNestingEnabled(true);
  428. setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea);
  429. setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea);
  430. ui_->editingModeAction->setIcon(style()->standardIcon(QStyle::SP_FileDialogDetailedView));
  431. ui_->offlineModeAction->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
  432. ui_->onlineModeAction->setIcon(style()->standardIcon(QStyle::SP_DriveNetIcon));
  433. ui_->newProjectAction->setIcon(style()->standardIcon(QStyle::SP_FileIcon));
  434. ui_->saveProjectAction->setIcon(style()->standardIcon(QStyle::SP_DialogSaveButton));
  435. ui_->loadProjectAction->setIcon(style()->standardIcon(QStyle::SP_DialogOpenButton));
  436. ui_->configurePlcAction->setIcon(style()->standardIcon(QStyle::SP_ComputerIcon));
  437. ui_->disconnectPlcAction->setIcon(style()->standardIcon(QStyle::SP_DialogCloseButton));
  438. ui_->addButtonAction->setIcon(style()->standardIcon(QStyle::SP_DialogApplyButton));
  439. ui_->addIndicatorAction->setIcon(style()->standardIcon(QStyle::SP_DialogYesButton));
  440. ui_->addNumericDisplayAction->setIcon(
  441. style()->standardIcon(QStyle::SP_FileDialogInfoView));
  442. ui_->addNumericInputAction->setIcon(
  443. style()->standardIcon(QStyle::SP_FileDialogContentsView));
  444. ui_->addLabelAction->setIcon(
  445. style()->standardIcon(QStyle::SP_FileDialogInfoView));
  446. ui_->addPageJumpAction->setIcon(
  447. style()->standardIcon(QStyle::SP_ArrowForward));
  448. ui_->addAlarmListAction->setIcon(
  449. style()->standardIcon(QStyle::SP_MessageBoxWarning));
  450. ui_->configureAlarmsAction->setIcon(
  451. style()->standardIcon(QStyle::SP_FileDialogDetailedView));
  452. ui_->deleteControlAction->setIcon(style()->standardIcon(QStyle::SP_TrashIcon));
  453. ui_->addRungAction->setIcon(style()->standardIcon(QStyle::SP_FileIcon));
  454. ui_->parallelInsertAction->setIcon(style()->standardIcon(QStyle::SP_ArrowDown));
  455. ui_->addNormallyOpenAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight));
  456. ui_->addNormallyClosedAction->setIcon(style()->standardIcon(QStyle::SP_ArrowRight));
  457. ui_->addNormalCoilAction->setIcon(style()->standardIcon(QStyle::SP_DialogApplyButton));
  458. ui_->addSetCoilAction->setIcon(style()->standardIcon(QStyle::SP_DialogYesButton));
  459. ui_->addResetCoilAction->setIcon(style()->standardIcon(QStyle::SP_DialogNoButton));
  460. ui_->addCompareAction->setIcon(style()->standardIcon(QStyle::SP_FileDialogInfoView));
  461. ui_->deleteLogicAction->setIcon(style()->standardIcon(QStyle::SP_TrashIcon));
  462. ui_->editorTabWidget->setTabIcon(0, style()->standardIcon(QStyle::SP_DesktopIcon));
  463. ui_->editorTabWidget->setTabIcon(1, style()->standardIcon(QStyle::SP_FileDialogListView));
  464. ui_->editorTabWidget->setTabIcon(2, style()->standardIcon(QStyle::SP_ComputerIcon));
  465. ui_->outputDock->setMaximumHeight(220);
  466. mode_status_label_ = new QLabel(this);
  467. mode_status_label_->setObjectName(QStringLiteral("modeStatusLabel"));
  468. mode_status_label_->setMinimumWidth(88);
  469. mode_status_label_->setAlignment(Qt::AlignCenter);
  470. register_status_label_ = new QLabel(this);
  471. register_status_label_->setObjectName(QStringLiteral("registerStatusLabel"));
  472. register_status_label_->setMinimumWidth(132);
  473. executor_status_label_ = new QLabel(this);
  474. executor_status_label_->setObjectName(QStringLiteral("executorStatusLabel"));
  475. executor_status_label_->setMinimumWidth(132);
  476. statusBar()->addPermanentWidget(mode_status_label_);
  477. statusBar()->addPermanentWidget(register_status_label_);
  478. statusBar()->addPermanentWidget(executor_status_label_);
  479. setStyleSheet(QStringLiteral(
  480. "QMainWindow { background: #f3f5f7; }"
  481. "QToolBar { background: #ffffff; border: 0; border-bottom: 1px solid #cbd2d9;"
  482. " padding: 4px; spacing: 3px; }"
  483. "QToolButton { min-height: 26px; padding: 3px 9px; border: 1px solid transparent; }"
  484. "QToolButton:hover { background: #edf2f6; border-color: #c5ced6; }"
  485. "QToolButton:checked { background: #dcece3; border-color: #75a58a; }"
  486. "QDockWidget { color: #24313b; font-weight: 600; }"
  487. "QDockWidget::title { background: #e9edf0; padding: 6px;"
  488. " border-bottom: 1px solid #cbd2d9; }"
  489. "QTreeWidget, QListWidget, QScrollArea { background: #ffffff; border: 1px solid #d5dbe0; }"
  490. "QTabWidget::pane { border: 0; background: #f3f5f7; }"
  491. "QTabBar::tab { background: #e5e9ec; padding: 7px 14px; border-right: 1px solid #cbd2d9; }"
  492. "QTabBar::tab:selected { background: #ffffff; color: #15232d; }"
  493. "QFrame#hmiCanvasPlaceholder, QFrame#logicCanvasPlaceholder { background: #ffffff;"
  494. " border: 1px solid #cbd2d9; }"
  495. "QLabel#hmiEmptyLabel, QLabel#logicEmptyLabel { color: #75838d; }"
  496. "QLabel#hmiPageTitleLabel { color: #1d2a33; font-weight: 600; }"
  497. "QLabel#hmiPageSizeLabel { color: #66747e; }"));
  498. }
  499. void MainWindow::configureHmiEditor()
  500. {
  501. QLayout *layout = ui_->hmiCanvasPlaceholder->layout();
  502. delete ui_->hmiEmptyLabel;
  503. hmi_editor_widget_ = new HmiEditorWidget(
  504. hmi_editor_service_,
  505. hmi_runtime_service_,
  506. alarm_service_,
  507. ui_->hmiCanvasPlaceholder);
  508. hmi_editor_widget_->setMinimumHeight(320);
  509. layout->addWidget(hmi_editor_widget_);
  510. connect(hmi_editor_widget_, &HmiEditorWidget::controlSelected,
  511. this,
  512. [this](const QString &id) { showControlProperties(toUtf8(id)); });
  513. connect(hmi_editor_widget_, &HmiEditorWidget::controlChanged,
  514. this,
  515. [this](const QString &id)
  516. {
  517. showControlProperties(toUtf8(id));
  518. refreshProjectUi();
  519. });
  520. connect(hmi_editor_widget_, &HmiEditorWidget::editorError,
  521. this,
  522. [this](const QString &message) { statusBar()->showMessage(message, 5000); });
  523. // 运行态定时投影仓库最新值,编辑态不触发寄存器读取
  524. runtime_refresh_timer_ = new QTimer(this);
  525. runtime_refresh_timer_->setInterval(150);
  526. connect(runtime_refresh_timer_, &QTimer::timeout,
  527. this,
  528. [this]
  529. {
  530. // 离线 / 联机运行模式
  531. if (runtime_mode_service_.mode() != ApplicationMode::Editing)
  532. {
  533. alarm_service_.refresh();
  534. hmi_editor_widget_->refreshRuntimeValues();
  535. runtime_monitor_widget_->refreshValues(
  536. runtime_mode_service_.mode(),
  537. runtime_mode_service_.plcConnectionState());
  538. updateSimulationUi(false);
  539. }
  540. });
  541. runtime_refresh_timer_->start();
  542. connect(&runtime_mode_service_.offlineSimulationService(),
  543. &OfflineSimulationService::stateChanged,
  544. this,
  545. [this] { updateSimulationUi(true); },
  546. Qt::QueuedConnection);
  547. connect(&runtime_mode_service_.offlineSimulationService(),
  548. &OfflineSimulationService::scanCompleted,
  549. this,
  550. [this]
  551. {
  552. logic_editor_widget_->setRuntimeTrace(
  553. runtime_mode_service_.offlineSimulationService()
  554. .traceSnapshot().forLogic(current_logic_id_));
  555. runtime_monitor_widget_->setLogicTrace(
  556. runtime_mode_service_.offlineSimulationService().traceSnapshot());
  557. },
  558. Qt::QueuedConnection);
  559. }
  560. void MainWindow::configureLogicEditor()
  561. {
  562. QLayout *layout = ui_->logicCanvasPlaceholder->layout();
  563. delete ui_->logicEmptyLabel;
  564. logic_editor_widget_ = new LogicEditorWidget(
  565. logic_editor_service_, ui_->logicCanvasPlaceholder);
  566. logic_editor_widget_->setObjectName(QStringLiteral("logicEditorWidget"));
  567. logic_editor_widget_->setMinimumHeight(320);
  568. layout->addWidget(logic_editor_widget_);
  569. connect(logic_editor_widget_, &LogicEditorWidget::nodeSelected,
  570. this,
  571. [this](const QString &id)
  572. {
  573. showLogicNodeProperties(toUtf8(id));
  574. });
  575. connect(logic_editor_widget_, &LogicEditorWidget::graphChanged,
  576. this, &MainWindow::refreshProjectUi);
  577. connect(logic_editor_widget_, &LogicEditorWidget::editorError,
  578. this,
  579. [this](const QString &message)
  580. {
  581. statusBar()->showMessage(message, 5000);
  582. });
  583. }
  584. void MainWindow::configureRuntimeMonitor()
  585. {
  586. runtime_monitor_widget_ = new RuntimeMonitorWidget(
  587. hmi_editor_service_,
  588. hmi_runtime_service_,
  589. logic_editor_service_,
  590. project_service_,
  591. *hmi_navigation_service_,
  592. alarm_service_,
  593. register_monitor_service_,
  594. ui_->runtimeMonitorTab);
  595. runtime_monitor_widget_->setObjectName(QStringLiteral("runtimeMonitorWidget"));
  596. ui_->runtimeMonitorLayout->addWidget(runtime_monitor_widget_);
  597. connect(runtime_monitor_widget_->freeMonitorWidget(),
  598. &FreeMonitorWidget::monitorAddressesChanged,
  599. this,
  600. [this]
  601. {
  602. runtime_mode_service_.setMonitorAddresses(
  603. register_monitor_service_.addresses());
  604. runtime_monitor_widget_->refreshValues(
  605. runtime_mode_service_.mode(),
  606. runtime_mode_service_.plcConnectionState());
  607. });
  608. connect(runtime_monitor_widget_->freeMonitorWidget(),
  609. &FreeMonitorWidget::operationMessage,
  610. this,
  611. [this](const QString &message)
  612. {
  613. statusBar()->showMessage(message, 4000);
  614. });
  615. connect(runtime_monitor_widget_, &RuntimeMonitorWidget::navigationFailed,
  616. this,
  617. [this](const QString &message)
  618. {
  619. statusBar()->showMessage(message, 5000);
  620. ui_->outputList->addItem(tr("页面导航失败:%1").arg(message));
  621. });
  622. ui_->editorTabWidget->setTabVisible(2, false);
  623. }
  624. void MainWindow::configureProjectTree()
  625. {
  626. ui_->projectTree->setContextMenuPolicy(Qt::ActionsContextMenu);
  627. add_page_action_ = new QAction(tr("新建页面"), this);
  628. add_page_action_->setObjectName(QStringLiteral("addPageAction"));
  629. add_logic_action_ = new QAction(tr("新建控制逻辑"), this);
  630. add_logic_action_->setObjectName(QStringLiteral("addLogicAction"));
  631. rename_project_item_action_ = new QAction(tr("重命名"), this);
  632. rename_project_item_action_->setObjectName(
  633. QStringLiteral("renameProjectItemAction"));
  634. delete_project_item_action_ = new QAction(tr("删除"), this);
  635. delete_project_item_action_->setObjectName(
  636. QStringLiteral("deleteProjectItemAction"));
  637. move_project_item_up_action_ = new QAction(tr("上移"), this);
  638. move_project_item_up_action_->setObjectName(
  639. QStringLiteral("moveProjectItemUpAction"));
  640. move_project_item_down_action_ = new QAction(tr("下移"), this);
  641. move_project_item_down_action_->setObjectName(
  642. QStringLiteral("moveProjectItemDownAction"));
  643. set_initial_page_action_ = new QAction(tr("设为初始页面"), this);
  644. set_initial_page_action_->setObjectName(
  645. QStringLiteral("setInitialPageAction"));
  646. toggle_logic_enabled_action_ = new QAction(tr("禁用控制逻辑"), this);
  647. toggle_logic_enabled_action_->setObjectName(
  648. QStringLiteral("toggleLogicEnabledAction"));
  649. ui_->projectTree->addActions({
  650. add_page_action_,
  651. add_logic_action_,
  652. rename_project_item_action_,
  653. delete_project_item_action_,
  654. move_project_item_up_action_,
  655. move_project_item_down_action_,
  656. set_initial_page_action_,
  657. toggle_logic_enabled_action_});
  658. connect(add_page_action_, &QAction::triggered, this, &MainWindow::addPage);
  659. connect(add_logic_action_, &QAction::triggered, this, &MainWindow::addLogic);
  660. connect(rename_project_item_action_, &QAction::triggered,
  661. this, &MainWindow::renameProjectItem);
  662. connect(delete_project_item_action_, &QAction::triggered,
  663. this, &MainWindow::deleteProjectItem);
  664. connect(move_project_item_up_action_, &QAction::triggered,
  665. this, [this] { moveProjectItem(-1); });
  666. connect(move_project_item_down_action_, &QAction::triggered,
  667. this, [this] { moveProjectItem(1); });
  668. connect(set_initial_page_action_, &QAction::triggered,
  669. this, &MainWindow::setSelectedPageAsInitial);
  670. connect(toggle_logic_enabled_action_, &QAction::triggered,
  671. this, &MainWindow::toggleSelectedLogicEnabled);
  672. connect(ui_->projectTree, &QTreeWidget::currentItemChanged,
  673. this,
  674. [this](QTreeWidgetItem *, QTreeWidgetItem *)
  675. {
  676. handleProjectTreeSelection();
  677. });
  678. updateProjectTreeActions();
  679. }
  680. void MainWindow::configurePropertyEditor()
  681. {
  682. ui_->bindingAreaComboBox->setItemData(0, -1);
  683. ui_->bindingAreaComboBox->setItemData(1, 0);
  684. ui_->bindingAreaComboBox->setItemData(2, 1);
  685. ui_->buttonOperationComboBox->setItemData(
  686. 0, static_cast<int>(HmiButtonOperation::SetOn));
  687. ui_->buttonOperationComboBox->setItemData(
  688. 1, static_cast<int>(HmiButtonOperation::SetOff));
  689. ui_->buttonOperationComboBox->setItemData(
  690. 2, static_cast<int>(HmiButtonOperation::Toggle));
  691. ui_->buttonOperationComboBox->setItemData(
  692. 3, static_cast<int>(HmiButtonOperation::MomentaryOn));
  693. for (int index = 0; index < ui_->logicComparisonComboBox->count(); ++index)
  694. {
  695. ui_->logicComparisonComboBox->setItemData(index, index);
  696. }
  697. connect(ui_->applyPropertiesButton, &QPushButton::clicked,
  698. this, &MainWindow::applySelectedControlProperties);
  699. connect(ui_->applyLogicPropertiesButton, &QPushButton::clicked,
  700. this, &MainWindow::applySelectedLogicNodeProperties);
  701. ui_->targetPageLabel->setVisible(false);
  702. ui_->targetPageComboBox->setVisible(false);
  703. showControlProperties({});
  704. }
  705. void MainWindow::configurePlcConnection()
  706. {
  707. connect(ui_->configurePlcAction, &QAction::triggered, this, &MainWindow::connectPlc);
  708. connect(ui_->disconnectPlcAction, &QAction::triggered, this, &MainWindow::disconnectPlc);
  709. }
  710. void MainWindow::configureAlarms()
  711. {
  712. connect(ui_->configureAlarmsAction, &QAction::triggered,
  713. this,
  714. [this]
  715. {
  716. AlarmConfigurationDialog dialog(alarm_editor_service_, this);
  717. dialog.exec();
  718. refreshProjectUi();
  719. });
  720. }
  721. void MainWindow::refreshProjectUi()
  722. {
  723. // 仅在当前对象消失时回退,普通刷新必须保留用户的会话选择
  724. if (hmi_editor_service_.findPage(current_hmi_page_id_) == nullptr)
  725. {
  726. const std::string &initial_id = project_service_.project().initialHmiPageId;
  727. current_hmi_page_id_ = hmi_editor_service_.findPage(initial_id) != nullptr
  728. ? initial_id : hmi_editor_service_.firstPageId();
  729. selected_control_id_.clear();
  730. }
  731. if (logic_editor_service_.findLogic(current_logic_id_) == nullptr)
  732. {
  733. current_logic_id_ = logic_editor_service_.firstLogicId();
  734. selected_logic_node_id_.clear();
  735. }
  736. if (hmi_editor_widget_ != nullptr)
  737. {
  738. hmi_editor_widget_->setPageId(current_hmi_page_id_);
  739. }
  740. const HmiPage *page = hmi_editor_service_.findPage(current_hmi_page_id_);
  741. if (logic_editor_widget_ != nullptr)
  742. {
  743. logic_editor_widget_->setLogicId(current_logic_id_);
  744. }
  745. if (runtime_monitor_widget_ != nullptr
  746. && runtime_mode_service_.mode() != ApplicationMode::Editing)
  747. {
  748. runtime_monitor_widget_->setProjectObjects(
  749. hmi_navigation_service_->currentPageId(), current_logic_id_);
  750. }
  751. runtime_mode_service_.refreshPlcPollAddresses();
  752. const QSignalBlocker blocker(ui_->projectTree);
  753. ui_->projectTree->clear();
  754. auto *hmi_root = new QTreeWidgetItem(ui_->projectTree, {tr("HMI 页面")});
  755. hmi_root->setData(0, kProjectTreeKindRole,
  756. static_cast<int>(ProjectTreeItemKind::Root));
  757. QTreeWidgetItem *selected_tree_item = nullptr;
  758. for (const HmiPage &candidate : project_service_.project().hmiPages)
  759. {
  760. const QString initial_suffix = candidate.id
  761. == project_service_.project().initialHmiPageId
  762. ? tr("(初始)") : QString{};
  763. auto *item = new QTreeWidgetItem(
  764. hmi_root, {fromUtf8(candidate.name) + initial_suffix});
  765. item->setData(0, kProjectTreeKindRole,
  766. static_cast<int>(ProjectTreeItemKind::HmiPage));
  767. item->setData(0, kProjectTreeIdRole, fromUtf8(candidate.id));
  768. if (candidate.id == current_hmi_page_id_
  769. && ui_->editorTabWidget->currentIndex() == 0)
  770. {
  771. selected_tree_item = item;
  772. }
  773. }
  774. if (page != nullptr)
  775. {
  776. ui_->hmiPageTitleLabel->setText(fromUtf8(page->name));
  777. ui_->hmiPageSizeLabel->setText(
  778. tr("%1 x %2").arg(page->width).arg(page->height));
  779. }
  780. else
  781. {
  782. ui_->hmiPageTitleLabel->setText(tr("尚未创建 HMI 页面"));
  783. ui_->hmiPageSizeLabel->clear();
  784. }
  785. auto *logic_root = new QTreeWidgetItem(ui_->projectTree, {tr("控制逻辑")});
  786. logic_root->setData(0, kProjectTreeKindRole,
  787. static_cast<int>(ProjectTreeItemKind::Root));
  788. for (const ControlLogic &logic : project_service_.project().controlLogics)
  789. {
  790. const QString suffix = logic.enabled ? QString{} : tr("(已禁用)");
  791. auto *item = new QTreeWidgetItem(
  792. logic_root, {fromUtf8(logic.name) + suffix});
  793. item->setData(0, kProjectTreeKindRole,
  794. static_cast<int>(ProjectTreeItemKind::ControlLogic));
  795. item->setData(0, kProjectTreeIdRole, fromUtf8(logic.id));
  796. if (logic.id == current_logic_id_
  797. && ui_->editorTabWidget->currentIndex() == 1)
  798. {
  799. selected_tree_item = item;
  800. }
  801. }
  802. ui_->projectTree->expandAll();
  803. if (selected_tree_item != nullptr)
  804. {
  805. ui_->projectTree->setCurrentItem(selected_tree_item);
  806. }
  807. updateProjectTreeActions();
  808. }
  809. void MainWindow::handleProjectTreeSelection()
  810. {
  811. QTreeWidgetItem *item = ui_->projectTree->currentItem();
  812. if (item == nullptr)
  813. {
  814. updateProjectTreeActions();
  815. return;
  816. }
  817. const auto kind = static_cast<ProjectTreeItemKind>(
  818. item->data(0, kProjectTreeKindRole).toInt());
  819. const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString());
  820. if (kind == ProjectTreeItemKind::HmiPage
  821. && hmi_editor_service_.findPage(id) != nullptr)
  822. {
  823. current_hmi_page_id_ = id;
  824. selected_control_id_.clear();
  825. hmi_editor_widget_->setPageId(id);
  826. hmi_editor_widget_->reloadPage();
  827. ui_->editorTabWidget->setCurrentWidget(ui_->hmiEditorTab);
  828. showControlProperties({});
  829. const HmiPage *page = hmi_editor_service_.findPage(id);
  830. ui_->hmiPageTitleLabel->setText(fromUtf8(page->name));
  831. ui_->hmiPageSizeLabel->setText(
  832. tr("%1 x %2").arg(page->width).arg(page->height));
  833. }
  834. else if (kind == ProjectTreeItemKind::ControlLogic
  835. && logic_editor_service_.findLogic(id) != nullptr)
  836. {
  837. current_logic_id_ = id;
  838. selected_logic_node_id_.clear();
  839. logic_editor_widget_->setLogicId(id);
  840. logic_editor_widget_->reloadLogic();
  841. ui_->editorTabWidget->setCurrentWidget(ui_->logicEditorTab);
  842. showLogicNodeProperties({});
  843. }
  844. updateProjectTreeActions();
  845. }
  846. void MainWindow::updateProjectTreeActions()
  847. {
  848. if (add_page_action_ == nullptr)
  849. {
  850. return;
  851. }
  852. QTreeWidgetItem *item = ui_->projectTree->currentItem();
  853. const auto kind = item == nullptr ? ProjectTreeItemKind::Root
  854. : static_cast<ProjectTreeItemKind>(
  855. item->data(0, kProjectTreeKindRole).toInt());
  856. const bool page_selected = kind == ProjectTreeItemKind::HmiPage;
  857. const bool logic_selected = kind == ProjectTreeItemKind::ControlLogic;
  858. const bool editable = runtime_mode_service_.policy().allowsProjectEditing;
  859. add_page_action_->setEnabled(editable);
  860. add_logic_action_->setEnabled(editable);
  861. rename_project_item_action_->setEnabled(
  862. editable && (page_selected || logic_selected));
  863. delete_project_item_action_->setEnabled(
  864. editable && (page_selected || logic_selected));
  865. move_project_item_up_action_->setEnabled(
  866. editable && (page_selected || logic_selected));
  867. move_project_item_down_action_->setEnabled(
  868. editable && (page_selected || logic_selected));
  869. set_initial_page_action_->setEnabled(editable && page_selected);
  870. toggle_logic_enabled_action_->setEnabled(editable && logic_selected);
  871. if (logic_selected)
  872. {
  873. const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString());
  874. const ControlLogic *logic = logic_editor_service_.findLogic(id);
  875. toggle_logic_enabled_action_->setText(
  876. logic != nullptr && logic->enabled
  877. ? tr("禁用控制逻辑") : tr("启用控制逻辑"));
  878. }
  879. }
  880. void MainWindow::addPage()
  881. {
  882. const QString name = QInputDialog::getText(
  883. this, tr("新建页面"), tr("页面名称"));
  884. if (name.isEmpty())
  885. {
  886. return;
  887. }
  888. const HmiEditorResult result = hmi_editor_service_.addPage(toUtf8(name));
  889. if (!result.succeeded)
  890. {
  891. showProjectResult(tr("新建页面"), fromUtf8(result.message), false);
  892. return;
  893. }
  894. current_hmi_page_id_ = result.id;
  895. selected_control_id_.clear();
  896. ui_->editorTabWidget->setCurrentWidget(ui_->hmiEditorTab);
  897. refreshProjectUi();
  898. hmi_editor_widget_->reloadPage();
  899. }
  900. void MainWindow::addLogic()
  901. {
  902. const QString name = QInputDialog::getText(
  903. this, tr("新建控制逻辑"), tr("逻辑名称"));
  904. if (name.isEmpty())
  905. {
  906. return;
  907. }
  908. const LogicEditorResult result = logic_editor_service_.addLogic(toUtf8(name));
  909. if (!result.succeeded)
  910. {
  911. showProjectResult(tr("新建控制逻辑"), fromUtf8(result.message), false);
  912. return;
  913. }
  914. current_logic_id_ = result.id;
  915. selected_logic_node_id_.clear();
  916. ui_->editorTabWidget->setCurrentWidget(ui_->logicEditorTab);
  917. refreshProjectUi();
  918. logic_editor_widget_->reloadLogic();
  919. }
  920. void MainWindow::renameProjectItem()
  921. {
  922. QTreeWidgetItem *item = ui_->projectTree->currentItem();
  923. if (item == nullptr)
  924. {
  925. return;
  926. }
  927. const auto kind = static_cast<ProjectTreeItemKind>(
  928. item->data(0, kProjectTreeKindRole).toInt());
  929. const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString());
  930. QString current_name;
  931. if (kind == ProjectTreeItemKind::HmiPage)
  932. {
  933. const HmiPage *page = hmi_editor_service_.findPage(id);
  934. current_name = page == nullptr ? QString{} : fromUtf8(page->name);
  935. }
  936. else if (kind == ProjectTreeItemKind::ControlLogic)
  937. {
  938. const ControlLogic *logic = logic_editor_service_.findLogic(id);
  939. current_name = logic == nullptr ? QString{} : fromUtf8(logic->name);
  940. }
  941. else
  942. {
  943. return;
  944. }
  945. bool accepted = false;
  946. const QString name = QInputDialog::getText(
  947. this, tr("重命名"), tr("新名称"), QLineEdit::Normal,
  948. current_name, &accepted);
  949. if (!accepted)
  950. {
  951. return;
  952. }
  953. if (kind == ProjectTreeItemKind::HmiPage)
  954. {
  955. const HmiEditorResult result = hmi_editor_service_.renamePage(id, toUtf8(name));
  956. if (!result.succeeded)
  957. {
  958. showProjectResult(tr("重命名页面"), fromUtf8(result.message), false);
  959. return;
  960. }
  961. }
  962. else
  963. {
  964. const LogicEditorResult result = logic_editor_service_.renameLogic(id, toUtf8(name));
  965. if (!result.succeeded)
  966. {
  967. showProjectResult(tr("重命名控制逻辑"), fromUtf8(result.message), false);
  968. return;
  969. }
  970. }
  971. refreshProjectUi();
  972. }
  973. void MainWindow::deleteProjectItem()
  974. {
  975. QTreeWidgetItem *item = ui_->projectTree->currentItem();
  976. if (item == nullptr)
  977. {
  978. return;
  979. }
  980. const auto kind = static_cast<ProjectTreeItemKind>(
  981. item->data(0, kProjectTreeKindRole).toInt());
  982. const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString());
  983. if (kind == ProjectTreeItemKind::HmiPage)
  984. {
  985. const HmiEditorResult result = hmi_editor_service_.removePage(id);
  986. if (!result.succeeded)
  987. {
  988. showProjectResult(tr("删除页面"), fromUtf8(result.message), false);
  989. return;
  990. }
  991. }
  992. else if (kind == ProjectTreeItemKind::ControlLogic)
  993. {
  994. const LogicEditorResult result = logic_editor_service_.removeLogic(id);
  995. if (!result.succeeded)
  996. {
  997. showProjectResult(tr("删除控制逻辑"), fromUtf8(result.message), false);
  998. return;
  999. }
  1000. }
  1001. else
  1002. {
  1003. return;
  1004. }
  1005. refreshProjectUi();
  1006. showControlProperties({});
  1007. showLogicNodeProperties({});
  1008. }
  1009. void MainWindow::moveProjectItem(int offset)
  1010. {
  1011. QTreeWidgetItem *item = ui_->projectTree->currentItem();
  1012. if (item == nullptr)
  1013. {
  1014. return;
  1015. }
  1016. const auto kind = static_cast<ProjectTreeItemKind>(
  1017. item->data(0, kProjectTreeKindRole).toInt());
  1018. const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString());
  1019. if (kind == ProjectTreeItemKind::HmiPage)
  1020. {
  1021. const HmiEditorResult result = hmi_editor_service_.movePage(id, offset);
  1022. if (!result.succeeded)
  1023. {
  1024. statusBar()->showMessage(fromUtf8(result.message), 4000);
  1025. return;
  1026. }
  1027. }
  1028. else if (kind == ProjectTreeItemKind::ControlLogic)
  1029. {
  1030. const LogicEditorResult result = logic_editor_service_.moveLogic(id, offset);
  1031. if (!result.succeeded)
  1032. {
  1033. statusBar()->showMessage(fromUtf8(result.message), 4000);
  1034. return;
  1035. }
  1036. }
  1037. refreshProjectUi();
  1038. }
  1039. void MainWindow::setSelectedPageAsInitial()
  1040. {
  1041. QTreeWidgetItem *item = ui_->projectTree->currentItem();
  1042. if (item == nullptr
  1043. || static_cast<ProjectTreeItemKind>(
  1044. item->data(0, kProjectTreeKindRole).toInt())
  1045. != ProjectTreeItemKind::HmiPage)
  1046. {
  1047. return;
  1048. }
  1049. const HmiEditorResult result = hmi_editor_service_.setInitialPage(
  1050. toUtf8(item->data(0, kProjectTreeIdRole).toString()));
  1051. if (!result.succeeded)
  1052. {
  1053. showProjectResult(tr("设置初始页面"), fromUtf8(result.message), false);
  1054. return;
  1055. }
  1056. refreshProjectUi();
  1057. }
  1058. void MainWindow::toggleSelectedLogicEnabled()
  1059. {
  1060. QTreeWidgetItem *item = ui_->projectTree->currentItem();
  1061. if (item == nullptr
  1062. || static_cast<ProjectTreeItemKind>(
  1063. item->data(0, kProjectTreeKindRole).toInt())
  1064. != ProjectTreeItemKind::ControlLogic)
  1065. {
  1066. return;
  1067. }
  1068. const std::string id = toUtf8(item->data(0, kProjectTreeIdRole).toString());
  1069. const ControlLogic *logic = logic_editor_service_.findLogic(id);
  1070. if (logic == nullptr)
  1071. {
  1072. return;
  1073. }
  1074. const LogicEditorResult result = logic_editor_service_.setLogicEnabled(
  1075. id, !logic->enabled);
  1076. if (!result.succeeded)
  1077. {
  1078. showProjectResult(tr("启停控制逻辑"), fromUtf8(result.message), false);
  1079. return;
  1080. }
  1081. refreshProjectUi();
  1082. }
  1083. void MainWindow::connectPlc()
  1084. {
  1085. PlcConnectionDialog dialog(plc_configuration_, this);
  1086. if (dialog.exec() != QDialog::Accepted)
  1087. {
  1088. return;
  1089. }
  1090. plc_configuration_ = dialog.configuration();
  1091. if (plc_configuration_.portName.empty())
  1092. {
  1093. showProjectResult(tr("连接 PLC"), tr("请选择或输入串口端口"), false);
  1094. return;
  1095. }
  1096. const PlcCommunicationResult result = runtime_mode_service_.connectPlc(
  1097. plc_configuration_);
  1098. if (!result.succeeded)
  1099. {
  1100. const QString message = fromUtf8(result.message);
  1101. if (result.message == runtime_mode_service_.plcError())
  1102. {
  1103. statusBar()->showMessage(message, 5000);
  1104. QMessageBox::warning(this, tr("连接 PLC"), message);
  1105. }
  1106. else
  1107. {
  1108. showProjectResult(tr("连接 PLC"), message, false);
  1109. }
  1110. return;
  1111. }
  1112. statusBar()->showMessage(tr("PLC 正在连接,等待首次 M/D 读取"), 5000);
  1113. }
  1114. void MainWindow::disconnectPlc()
  1115. {
  1116. runtime_mode_service_.disconnectPlc();
  1117. }
  1118. void MainWindow::schedulePlcStatusUpdate()
  1119. {
  1120. if (plc_status_update_pending_)
  1121. {
  1122. return;
  1123. }
  1124. plc_status_update_pending_ = true;
  1125. QMetaObject::invokeMethod(
  1126. this,
  1127. [this]
  1128. {
  1129. plc_status_update_pending_ = false;
  1130. updateModeUi(plcStatusText(runtime_mode_service_));
  1131. },
  1132. Qt::QueuedConnection);
  1133. }
  1134. // 根据控件ID加载控件属性到右侧属性面板
  1135. void MainWindow::showControlProperties(const std::string &control_id)
  1136. {
  1137. ui_->propertyStack->setCurrentWidget(ui_->hmiPropertiesPage);
  1138. // 更新窗口全局状态:记录当前选中控件ID
  1139. selected_control_id_ = control_id;
  1140. // 在第一页画面中,根据ID查找目标控件,获取只读模型指针
  1141. const HmiControl *control = hmi_editor_service_.findControl(
  1142. current_hmi_page_id_, control_id);
  1143. // 判断是否查询到有效控件
  1144. const bool has_control = control != nullptr;
  1145. // 更新属性面板顶部状态标签,显示选中控件ID或提示未选择
  1146. ui_->selectionValueLabel->setText(
  1147. has_control ? fromUtf8(control->id)
  1148. + (control->isConfigured() ? QString{}
  1149. : control->type == HmiControlType::PageJump
  1150. ? tr("(未配置)") : tr("(未绑定)"))
  1151. : tr("未选择"));
  1152. // 批量控制所有属性编辑组件可用性:无选中控件时整体置灰,禁止编辑
  1153. for (QWidget *widget : {static_cast<QWidget *>(ui_->controlIdEdit),
  1154. static_cast<QWidget *>(ui_->controlTextEdit),
  1155. static_cast<QWidget *>(ui_->controlXSpinBox),
  1156. static_cast<QWidget *>(ui_->controlYSpinBox),
  1157. static_cast<QWidget *>(ui_->controlWidthSpinBox),
  1158. static_cast<QWidget *>(ui_->controlHeightSpinBox),
  1159. static_cast<QWidget *>(ui_->bindingAreaComboBox),
  1160. static_cast<QWidget *>(ui_->bindingIndexSpinBox),
  1161. static_cast<QWidget *>(ui_->targetPageComboBox),
  1162. static_cast<QWidget *>(ui_->buttonOperationComboBox),
  1163. static_cast<QWidget *>(ui_->applyPropertiesButton)})
  1164. {
  1165. widget->setEnabled(has_control);
  1166. }
  1167. // 未选中有效控件,无需填充属性,直接退出
  1168. if (!has_control)
  1169. {
  1170. ui_->bindingAreaLabel->setVisible(false);
  1171. ui_->bindingAreaComboBox->setVisible(false);
  1172. ui_->bindingIndexLabel->setVisible(false);
  1173. ui_->bindingIndexSpinBox->setVisible(false);
  1174. ui_->targetPageLabel->setVisible(false);
  1175. ui_->targetPageComboBox->setVisible(false);
  1176. ui_->buttonOperationLabel->setVisible(false);
  1177. ui_->buttonOperationComboBox->setVisible(false);
  1178. return;
  1179. }
  1180. // 将控件基础信息填充至UI输入框
  1181. ui_->controlIdEdit->setText(fromUtf8(control->id));
  1182. ui_->controlTextEdit->setText(fromUtf8(control->text));
  1183. // 控件位置坐标
  1184. ui_->controlXSpinBox->setValue(control->bounds.x);
  1185. ui_->controlYSpinBox->setValue(control->bounds.y);
  1186. // 控件宽高尺寸
  1187. ui_->controlWidthSpinBox->setValue(control->bounds.width);
  1188. ui_->controlHeightSpinBox->setValue(control->bounds.height);
  1189. // 根据控件类型获取默认寄存器区域:按钮/指示灯默认M区;数值类控件默认D区
  1190. // 未绑定必须保留明确状态,避免属性编辑把显示默认值误提交为 M0/D0
  1191. ui_->bindingAreaComboBox->setCurrentIndex(!control->binding.has_value() ? 0
  1192. : control->binding->area() == RegisterArea::M ? 1 : 2);
  1193. ui_->bindingIndexSpinBox->setValue(
  1194. control->binding.has_value() ? control->binding->index() : 0);
  1195. const bool has_binding = control->type != HmiControlType::Label
  1196. && control->type != HmiControlType::PageJump
  1197. && control->type != HmiControlType::AlarmList;
  1198. ui_->bindingAreaLabel->setVisible(has_binding);
  1199. ui_->bindingAreaComboBox->setVisible(has_binding);
  1200. ui_->bindingIndexLabel->setVisible(has_binding);
  1201. ui_->bindingIndexSpinBox->setVisible(has_binding);
  1202. const bool is_button = control->type == HmiControlType::Button;
  1203. ui_->buttonOperationLabel->setVisible(is_button);
  1204. ui_->buttonOperationComboBox->setVisible(is_button);
  1205. ui_->buttonOperationComboBox->setEnabled(is_button);
  1206. ui_->buttonOperationComboBox->setCurrentIndex(
  1207. ui_->buttonOperationComboBox->findData(
  1208. static_cast<int>(control->buttonOperation)));
  1209. const bool is_page_jump = control->type == HmiControlType::PageJump;
  1210. ui_->targetPageLabel->setVisible(is_page_jump);
  1211. ui_->targetPageComboBox->setVisible(is_page_jump);
  1212. ui_->targetPageComboBox->setEnabled(is_page_jump);
  1213. ui_->targetPageComboBox->clear();
  1214. if (is_page_jump)
  1215. {
  1216. for (const HmiPage &page : project_service_.project().hmiPages)
  1217. {
  1218. ui_->targetPageComboBox->addItem(
  1219. fromUtf8(page.name), fromUtf8(page.id));
  1220. }
  1221. const std::string target_id = control->pageJump.has_value()
  1222. ? control->pageJump->targetPageId : std::string{};
  1223. ui_->targetPageComboBox->setCurrentIndex(
  1224. ui_->targetPageComboBox->findData(fromUtf8(target_id)));
  1225. }
  1226. }
  1227. void MainWindow::showLogicNodeProperties(const std::string &node_id)
  1228. {
  1229. ui_->propertyStack->setCurrentWidget(ui_->logicPropertiesPage);
  1230. selected_logic_node_id_ = node_id;
  1231. const LogicNode *node = logic_editor_service_.findNode(
  1232. current_logic_id_, node_id);
  1233. const bool has_node = node != nullptr;
  1234. ui_->selectionValueLabel->setText(
  1235. has_node ? fromUtf8(node->id) : tr("未选择"));
  1236. for (QWidget *widget : {static_cast<QWidget *>(ui_->logicNodeIdEdit),
  1237. static_cast<QWidget *>(ui_->logicAddressSpinBox),
  1238. static_cast<QWidget *>(ui_->logicModeComboBox),
  1239. static_cast<QWidget *>(ui_->logicComparisonComboBox),
  1240. static_cast<QWidget *>(ui_->logicValueSpinBox),
  1241. static_cast<QWidget *>(ui_->applyLogicPropertiesButton)})
  1242. {
  1243. widget->setEnabled(has_node);
  1244. }
  1245. ui_->logicNodeTypeLabel->setEnabled(has_node);
  1246. ui_->logicAddressAreaLabel->setEnabled(has_node);
  1247. if (!has_node)
  1248. {
  1249. ui_->logicNodeIdEdit->clear();
  1250. ui_->logicNodeTypeLabel->clear();
  1251. ui_->logicAddressAreaLabel->clear();
  1252. ui_->logicModeComboBox->clear();
  1253. return;
  1254. }
  1255. ui_->logicNodeIdEdit->setText(fromUtf8(node->id));
  1256. ui_->logicModeComboBox->clear();
  1257. if (const auto *contact = std::get_if<ContactNodeConfig>(&node->config))
  1258. {
  1259. ui_->logicNodeTypeLabel->setText(tr("触点"));
  1260. ui_->logicAddressAreaLabel->setText(QStringLiteral("M"));
  1261. ui_->logicAddressSpinBox->setValue(contact->address.index());
  1262. ui_->logicModeComboBox->addItem(
  1263. tr("常开"), static_cast<int>(ContactMode::NormallyOpen));
  1264. ui_->logicModeComboBox->addItem(
  1265. tr("常闭"), static_cast<int>(ContactMode::NormallyClosed));
  1266. ui_->logicModeComboBox->setCurrentIndex(
  1267. ui_->logicModeComboBox->findData(static_cast<int>(contact->mode)));
  1268. ui_->logicModeComboBox->setEnabled(true);
  1269. ui_->logicComparisonComboBox->setEnabled(false);
  1270. ui_->logicValueSpinBox->setEnabled(false);
  1271. }
  1272. else if (const auto *coil = std::get_if<CoilNodeConfig>(&node->config))
  1273. {
  1274. ui_->logicNodeTypeLabel->setText(tr("线圈"));
  1275. ui_->logicAddressAreaLabel->setText(QStringLiteral("M"));
  1276. ui_->logicAddressSpinBox->setValue(coil->address.index());
  1277. ui_->logicModeComboBox->addItem(
  1278. tr("普通"), static_cast<int>(CoilMode::Normal));
  1279. ui_->logicModeComboBox->addItem(
  1280. tr("置位"), static_cast<int>(CoilMode::Set));
  1281. ui_->logicModeComboBox->addItem(
  1282. tr("复位"), static_cast<int>(CoilMode::Reset));
  1283. ui_->logicModeComboBox->setCurrentIndex(
  1284. ui_->logicModeComboBox->findData(static_cast<int>(coil->mode)));
  1285. ui_->logicModeComboBox->setEnabled(true);
  1286. ui_->logicComparisonComboBox->setEnabled(false);
  1287. ui_->logicValueSpinBox->setEnabled(false);
  1288. }
  1289. else
  1290. {
  1291. const auto &compare = std::get<CompareNodeConfig>(node->config);
  1292. ui_->logicNodeTypeLabel->setText(tr("D 值比较"));
  1293. ui_->logicAddressAreaLabel->setText(QStringLiteral("D"));
  1294. ui_->logicAddressSpinBox->setValue(compare.address.index());
  1295. ui_->logicModeComboBox->addItem(tr("不适用"));
  1296. ui_->logicModeComboBox->setEnabled(false);
  1297. ui_->logicComparisonComboBox->setEnabled(true);
  1298. ui_->logicComparisonComboBox->setCurrentIndex(
  1299. ui_->logicComparisonComboBox->findData(
  1300. static_cast<int>(compare.comparison)));
  1301. ui_->logicValueSpinBox->setEnabled(true);
  1302. ui_->logicValueSpinBox->setValue(compare.value);
  1303. }
  1304. if (!node->configured)
  1305. {
  1306. ui_->logicNodeTypeLabel->setText(tr("待配置:%1").arg(
  1307. ui_->logicNodeTypeLabel->text()));
  1308. }
  1309. }
  1310. void MainWindow::addHmiControl(HmiControlType type)
  1311. {
  1312. const HmiEditorResult result = hmi_editor_service_.addControl(
  1313. current_hmi_page_id_, type);
  1314. if (!result.succeeded)
  1315. {
  1316. showProjectResult(tr("添加控件"), fromUtf8(result.message), false);
  1317. return;
  1318. }
  1319. hmi_editor_widget_->reloadPage();
  1320. hmi_editor_widget_->selectControl(result.id);
  1321. showControlProperties(result.id);
  1322. refreshProjectUi();
  1323. statusBar()->showMessage(tr("已添加%1").arg(controlTypeText(type)), 3000);
  1324. }
  1325. void MainWindow::deleteSelectedControl()
  1326. {
  1327. if (selected_control_id_.empty())
  1328. {
  1329. return;
  1330. }
  1331. const HmiEditorResult result = hmi_editor_service_.removeControl(
  1332. current_hmi_page_id_, selected_control_id_);
  1333. if (!result.succeeded)
  1334. {
  1335. showProjectResult(tr("删除控件"), fromUtf8(result.message), false);
  1336. return;
  1337. }
  1338. selected_control_id_.clear();
  1339. hmi_editor_widget_->reloadPage();
  1340. showControlProperties({});
  1341. refreshProjectUi();
  1342. }
  1343. void MainWindow::applySelectedControlProperties()
  1344. {
  1345. const HmiControl *old_control = hmi_editor_service_.findControl(
  1346. current_hmi_page_id_, selected_control_id_);
  1347. if (old_control == nullptr)
  1348. {
  1349. return;
  1350. }
  1351. // 从现有控件复制不可见属性,避免属性面板覆盖扩展配置
  1352. HmiControl control = *old_control;
  1353. control.id = toUtf8(ui_->controlIdEdit->text());
  1354. control.text = toUtf8(ui_->controlTextEdit->text());
  1355. control.bounds.x = ui_->controlXSpinBox->value();
  1356. control.bounds.y = ui_->controlYSpinBox->value();
  1357. control.bounds.width = ui_->controlWidthSpinBox->value();
  1358. control.bounds.height = ui_->controlHeightSpinBox->value();
  1359. if (control.type == HmiControlType::Label
  1360. || control.type == HmiControlType::PageJump
  1361. || control.type == HmiControlType::AlarmList)
  1362. {
  1363. control.binding.reset();
  1364. }
  1365. else
  1366. {
  1367. const int binding_area = ui_->bindingAreaComboBox->currentData().toInt();
  1368. if (binding_area < 0)
  1369. {
  1370. control.binding.reset();
  1371. }
  1372. else
  1373. {
  1374. const RegisterArea area = binding_area == 0
  1375. ? RegisterArea::M : RegisterArea::D;
  1376. control.binding = RegisterAddress{
  1377. area, ui_->bindingIndexSpinBox->value()};
  1378. }
  1379. }
  1380. if (control.type == HmiControlType::Button)
  1381. {
  1382. control.buttonOperation = static_cast<HmiButtonOperation>(
  1383. ui_->buttonOperationComboBox->currentData().toInt());
  1384. }
  1385. if (control.type == HmiControlType::PageJump)
  1386. {
  1387. control.pageJump = HmiPageJumpConfig{
  1388. toUtf8(ui_->targetPageComboBox->currentData().toString())};
  1389. }
  1390. const HmiEditorResult result = hmi_editor_service_.updateControl(
  1391. current_hmi_page_id_, selected_control_id_, control);
  1392. if (!result.succeeded)
  1393. {
  1394. showProjectResult(tr("应用属性"), fromUtf8(result.message), false);
  1395. return;
  1396. }
  1397. selected_control_id_ = result.id;
  1398. hmi_editor_widget_->reloadPage();
  1399. hmi_editor_widget_->selectControl(selected_control_id_);
  1400. refreshProjectUi();
  1401. statusBar()->showMessage(tr("控件属性已更新"), 3000);
  1402. }
  1403. void MainWindow::addLogicCondition(const LogicNodeConfig &config)
  1404. {
  1405. const LogicEditorResult result = logic_editor_widget_->addCondition(config);
  1406. if (!result.succeeded)
  1407. {
  1408. showProjectResult(tr("添加逻辑条件"), fromUtf8(result.message), false);
  1409. return;
  1410. }
  1411. selected_logic_node_id_ = result.id;
  1412. showLogicNodeProperties(result.id);
  1413. refreshProjectUi();
  1414. statusBar()->showMessage(tr("已添加逻辑条件"), 3000);
  1415. }
  1416. void MainWindow::addLogicParallelBranch(const LogicNodeConfig &config)
  1417. {
  1418. const LogicEditorResult result = logic_editor_widget_->addParallelBranch(config);
  1419. if (!result.succeeded)
  1420. {
  1421. showProjectResult(tr("建立并联支路"), fromUtf8(result.message), false);
  1422. return;
  1423. }
  1424. selected_logic_node_id_ = result.id;
  1425. showLogicNodeProperties(result.id);
  1426. refreshProjectUi();
  1427. statusBar()->showMessage(tr("已建立并联支路,请配置新触点"), 3000);
  1428. }
  1429. void MainWindow::setLogicOutput(const LogicNodeConfig &config)
  1430. {
  1431. const LogicEditorResult result = logic_editor_widget_->setOutput(config);
  1432. if (!result.succeeded)
  1433. {
  1434. showProjectResult(tr("设置输出线圈"), fromUtf8(result.message), false);
  1435. return;
  1436. }
  1437. selected_logic_node_id_ = result.id;
  1438. showLogicNodeProperties(result.id);
  1439. refreshProjectUi();
  1440. statusBar()->showMessage(tr("输出线圈已设置"), 3000);
  1441. }
  1442. void MainWindow::addLogicRung()
  1443. {
  1444. const LogicEditorResult result = logic_editor_widget_->addRung();
  1445. if (!result.succeeded)
  1446. {
  1447. showProjectResult(tr("新建网络"), fromUtf8(result.message), false);
  1448. return;
  1449. }
  1450. selected_logic_node_id_.clear();
  1451. showLogicNodeProperties({});
  1452. refreshProjectUi();
  1453. statusBar()->showMessage(tr("已新建网络"), 3000);
  1454. }
  1455. void MainWindow::deleteSelectedLogicObject()
  1456. {
  1457. const LogicEditorResult result = logic_editor_widget_->deleteSelected();
  1458. if (!result.succeeded)
  1459. {
  1460. return;
  1461. }
  1462. selected_logic_node_id_.clear();
  1463. showLogicNodeProperties({});
  1464. refreshProjectUi();
  1465. }
  1466. void MainWindow::applySelectedLogicNodeProperties()
  1467. {
  1468. const LogicNode *node = logic_editor_service_.findNode(
  1469. current_logic_id_, selected_logic_node_id_);
  1470. if (node == nullptr)
  1471. {
  1472. return;
  1473. }
  1474. LogicNodeConfig config = node->config;
  1475. if (std::holds_alternative<ContactNodeConfig>(config))
  1476. {
  1477. config = ContactNodeConfig{
  1478. RegisterAddress{RegisterArea::M, ui_->logicAddressSpinBox->value()},
  1479. static_cast<ContactMode>(ui_->logicModeComboBox->currentData().toInt())};
  1480. }
  1481. else if (std::holds_alternative<CoilNodeConfig>(config))
  1482. {
  1483. config = CoilNodeConfig{
  1484. RegisterAddress{RegisterArea::M, ui_->logicAddressSpinBox->value()},
  1485. static_cast<CoilMode>(ui_->logicModeComboBox->currentData().toInt())};
  1486. }
  1487. else
  1488. {
  1489. config = CompareNodeConfig{
  1490. RegisterAddress{RegisterArea::D, ui_->logicAddressSpinBox->value()},
  1491. static_cast<ComparisonOperator>(
  1492. ui_->logicComparisonComboBox->currentData().toInt()),
  1493. static_cast<std::int16_t>(ui_->logicValueSpinBox->value())};
  1494. }
  1495. const LogicEditorResult result = logic_editor_service_.updateNodeConfig(
  1496. current_logic_id_, selected_logic_node_id_, config);
  1497. if (!result.succeeded)
  1498. {
  1499. showProjectResult(tr("应用逻辑属性"), fromUtf8(result.message), false);
  1500. return;
  1501. }
  1502. logic_editor_widget_->reloadLogic();
  1503. logic_editor_widget_->selectNode(selected_logic_node_id_);
  1504. showLogicNodeProperties(selected_logic_node_id_);
  1505. refreshProjectUi();
  1506. statusBar()->showMessage(tr("逻辑节点属性已更新,节点已配置"), 3000);
  1507. }
  1508. void MainWindow::createNewProject()
  1509. {
  1510. const QString name = QInputDialog::getText(
  1511. this, tr("新建工程"), tr("工程名称"));
  1512. if (name.isEmpty())
  1513. {
  1514. return;
  1515. }
  1516. const ProjectOperationResult result = project_service_.createNewProject(toUtf8(name));
  1517. if (!result.succeeded)
  1518. {
  1519. showProjectResult(tr("新建工程"), fromUtf8(result.message), false);
  1520. return;
  1521. }
  1522. hmi_editor_service_.ensureDefaultPage();
  1523. logic_editor_service_.ensureDefaultLogic();
  1524. selected_control_id_.clear();
  1525. selected_logic_node_id_.clear();
  1526. current_hmi_page_id_ = project_service_.project().initialHmiPageId;
  1527. current_logic_id_ = logic_editor_service_.firstLogicId();
  1528. refreshProjectUi();
  1529. hmi_editor_widget_->reloadPage();
  1530. logic_editor_widget_->reloadLogic();
  1531. showControlProperties({});
  1532. statusBar()->showMessage(tr("已创建新工程"), 3000);
  1533. }
  1534. void MainWindow::saveProject()
  1535. {
  1536. const ProjectOperationResult result = project_service_.save();
  1537. if (!result.succeeded)
  1538. {
  1539. saveProjectAs();
  1540. return;
  1541. }
  1542. showProjectResult(tr("保存工程"), tr("工程已保存"), true);
  1543. }
  1544. void MainWindow::saveProjectAs()
  1545. {
  1546. const QString path = QFileDialog::getSaveFileName(
  1547. this, tr("工程另存为"), {}, tr("工程文件 (*.json)"));
  1548. if (path.isEmpty())
  1549. {
  1550. return;
  1551. }
  1552. const ProjectOperationResult result = project_service_.saveAs(toUtf8(path));
  1553. showProjectResult(
  1554. tr("保存工程"),
  1555. result.succeeded ? tr("工程已保存") : fromUtf8(result.message),
  1556. result.succeeded);
  1557. }
  1558. void MainWindow::loadProject()
  1559. {
  1560. const QString path = QFileDialog::getOpenFileName(
  1561. this, tr("加载工程"), {}, tr("工程文件 (*.json)"));
  1562. if (path.isEmpty())
  1563. {
  1564. return;
  1565. }
  1566. const ProjectOperationResult result = project_service_.load(toUtf8(path));
  1567. if (!result.succeeded)
  1568. {
  1569. showProjectResult(tr("加载工程"), fromUtf8(result.message), false);
  1570. return;
  1571. }
  1572. selected_control_id_.clear();
  1573. selected_logic_node_id_.clear();
  1574. current_hmi_page_id_ = project_service_.project().initialHmiPageId;
  1575. current_logic_id_ = logic_editor_service_.firstLogicId();
  1576. refreshProjectUi();
  1577. hmi_editor_widget_->reloadPage();
  1578. logic_editor_widget_->reloadLogic();
  1579. showControlProperties({});
  1580. showProjectResult(tr("加载工程"), tr("工程已加载"), true);
  1581. }
  1582. void MainWindow::showProjectResult(
  1583. const QString &action, const QString &message, bool succeeded)
  1584. {
  1585. const QString output = action + QStringLiteral(": ") + message;
  1586. statusBar()->showMessage(output, 5000);
  1587. ui_->outputList->addItem(output);
  1588. ui_->outputList->scrollToBottom();
  1589. if (!succeeded)
  1590. {
  1591. QMessageBox::warning(this, action, message);
  1592. }
  1593. }
  1594. void MainWindow::requestMode(ApplicationMode requested_mode)
  1595. {
  1596. // UI 仅转发模式意图,合法性由服务层和领域状态机决定
  1597. ModeTransitionResult result;
  1598. switch (requested_mode)
  1599. {
  1600. case ApplicationMode::Editing:
  1601. {
  1602. result = runtime_mode_service_.enterEditing();
  1603. break;
  1604. }
  1605. case ApplicationMode::OfflineRunning:
  1606. {
  1607. result = runtime_mode_service_.enterOfflineRunning();
  1608. break;
  1609. }
  1610. case ApplicationMode::OnlineRunning:
  1611. {
  1612. result = runtime_mode_service_.enterOnlineRunning();
  1613. break;
  1614. }
  1615. default:
  1616. {
  1617. restoreCurrentModeAction();
  1618. statusBar()->showMessage(tr("不支持的运行模式"), 5000);
  1619. return;
  1620. }
  1621. }
  1622. if (!result.succeeded)
  1623. {
  1624. restoreCurrentModeAction();
  1625. QString message = transitionErrorText(result.error);
  1626. if (result.error == ModeTransitionError::InitialPlcReadRequired)
  1627. {
  1628. message += tr(";当前状态:%1").arg(plcStatusText(runtime_mode_service_));
  1629. }
  1630. if (result.error == ModeTransitionError::SimulationStartFailed)
  1631. {
  1632. const LogicScanResult &error = runtime_mode_service_.simulationError();
  1633. if (!error.message.empty())
  1634. {
  1635. message += QStringLiteral(": ") + fromUtf8(error.message);
  1636. }
  1637. }
  1638. statusBar()->showMessage(message, 5000);
  1639. return;
  1640. }
  1641. updateModeUi(tr("已进入%1").arg(modeText(runtime_mode_service_.mode())));
  1642. }
  1643. void MainWindow::updateModeUi(const QString &message)
  1644. {
  1645. const ApplicationMode mode = runtime_mode_service_.mode();
  1646. const ModePolicy policy = runtime_mode_service_.policy();
  1647. restoreCurrentModeAction();
  1648. const bool running = mode != ApplicationMode::Editing;
  1649. ui_->editorTabWidget->setTabVisible(2, running);
  1650. if (running)
  1651. {
  1652. if (hmi_navigation_service_->currentPageId().empty())
  1653. {
  1654. const HmiNavigationResult navigation = hmi_navigation_service_->start();
  1655. if (!navigation.succeeded)
  1656. {
  1657. statusBar()->showMessage(fromUtf8(navigation.message), 5000);
  1658. }
  1659. }
  1660. runtime_monitor_widget_->setProjectObjects(
  1661. hmi_navigation_service_->currentPageId(), current_logic_id_);
  1662. runtime_monitor_widget_->setMode(mode, runtime_mode_service_.plcConnectionState());
  1663. ui_->editorTabWidget->setCurrentWidget(ui_->runtimeMonitorTab);
  1664. }
  1665. else
  1666. {
  1667. hmi_navigation_service_->stop();
  1668. runtime_monitor_widget_->setMode(
  1669. mode, runtime_mode_service_.plcConnectionState());
  1670. if (ui_->editorTabWidget->currentWidget() == ui_->runtimeMonitorTab)
  1671. {
  1672. ui_->editorTabWidget->setCurrentIndex(last_editing_tab_index_);
  1673. }
  1674. }
  1675. // 将同一份模式策略同步到所有可编辑入口,避免只禁用部分操作
  1676. ui_->projectDock->setEnabled(policy.allowsProjectEditing);
  1677. ui_->propertiesDock->setEnabled(policy.allowsProjectEditing);
  1678. hmi_editor_widget_->setEditingEnabled(policy.allowsProjectEditing);
  1679. hmi_editor_widget_->setRuntimeActive(
  1680. policy.usesVirtualRegisters || policy.usesPlcRegisters);
  1681. logic_editor_widget_->setEditingEnabled(policy.allowsProjectEditing);
  1682. if (mode == ApplicationMode::Editing)
  1683. {
  1684. alarm_service_.reset();
  1685. logic_editor_widget_->clearRuntimeTrace();
  1686. }
  1687. ui_->hmiToolBar->setEnabled(policy.allowsProjectEditing);
  1688. ui_->logicToolBar->setEnabled(policy.allowsProjectEditing);
  1689. ui_->addButtonAction->setEnabled(policy.allowsProjectEditing);
  1690. ui_->addIndicatorAction->setEnabled(policy.allowsProjectEditing);
  1691. ui_->addNumericDisplayAction->setEnabled(policy.allowsProjectEditing);
  1692. ui_->addNumericInputAction->setEnabled(policy.allowsProjectEditing);
  1693. ui_->addLabelAction->setEnabled(policy.allowsProjectEditing);
  1694. ui_->addPageJumpAction->setEnabled(policy.allowsProjectEditing);
  1695. ui_->addAlarmListAction->setEnabled(policy.allowsProjectEditing);
  1696. ui_->configureAlarmsAction->setEnabled(policy.allowsProjectEditing);
  1697. ui_->deleteControlAction->setEnabled(policy.allowsProjectEditing);
  1698. ui_->addNormallyOpenAction->setEnabled(policy.allowsProjectEditing);
  1699. ui_->addNormallyClosedAction->setEnabled(policy.allowsProjectEditing);
  1700. ui_->addNormalCoilAction->setEnabled(policy.allowsProjectEditing);
  1701. ui_->addSetCoilAction->setEnabled(policy.allowsProjectEditing);
  1702. ui_->addResetCoilAction->setEnabled(policy.allowsProjectEditing);
  1703. ui_->addCompareAction->setEnabled(policy.allowsProjectEditing);
  1704. ui_->deleteLogicAction->setEnabled(policy.allowsProjectEditing);
  1705. ui_->newProjectAction->setEnabled(policy.allowsProjectEditing);
  1706. ui_->saveProjectAction->setEnabled(policy.allowsProjectEditing);
  1707. ui_->saveAsProjectAction->setEnabled(policy.allowsProjectEditing);
  1708. ui_->loadProjectAction->setEnabled(policy.allowsProjectEditing);
  1709. updateProjectTreeActions();
  1710. mode_status_label_->setText(modeText(mode));
  1711. if (mode == ApplicationMode::Editing)
  1712. {
  1713. mode_status_label_->setStyleSheet(QStringLiteral(
  1714. "border: 1px solid #75a58a; background: #e3f0e8; color: #205c3b; padding: 2px 8px;"));
  1715. }
  1716. else if (mode == ApplicationMode::OfflineRunning)
  1717. {
  1718. mode_status_label_->setStyleSheet(QStringLiteral(
  1719. "border: 1px solid #bd8739; background: #fff0d2; color: #744b0e; padding: 2px 8px;"));
  1720. }
  1721. else
  1722. {
  1723. mode_status_label_->setStyleSheet(QStringLiteral(
  1724. "border: 1px solid #4b88a8; background: #deeff7; color: #174f6c; padding: 2px 8px;"));
  1725. }
  1726. register_status_label_->setText(
  1727. policy.usesVirtualRegisters ? tr("数据源:虚拟 M/D")
  1728. : policy.usesPlcRegisters ? tr("数据源:PLC 缓存") : tr("数据源:未启用"));
  1729. const PlcConnectionState plc_state = runtime_mode_service_.plcConnectionState();
  1730. const bool plc_configuration_available = plc_state == PlcConnectionState::Disconnected
  1731. || plc_state == PlcConnectionState::Faulted;
  1732. ui_->configurePlcAction->setEnabled(
  1733. policy.allowsProjectEditing && plc_configuration_available);
  1734. ui_->configurePlcAction->setText(
  1735. plc_state == PlcConnectionState::Faulted ? tr("PLC 重新配置") : tr("PLC 配置"));
  1736. ui_->configurePlcAction->setToolTip(
  1737. plc_state == PlcConnectionState::Faulted
  1738. ? tr("清理故障会话后重新配置并连接真实 PLC")
  1739. : tr("配置参数并连接真实 PLC"));
  1740. ui_->disconnectPlcAction->setEnabled(plc_state != PlcConnectionState::Disconnected);
  1741. if (plc_state == PlcConnectionState::Connecting)
  1742. {
  1743. register_status_label_->setText(tr("PLC:正在连接"));
  1744. }
  1745. else if (plc_state == PlcConnectionState::Connected)
  1746. {
  1747. register_status_label_->setText(
  1748. runtime_mode_service_.initialPlcReadCompleted()
  1749. ? tr("PLC:已连接,首读完成") : tr("PLC:已连接,正在首读"));
  1750. }
  1751. else if (plc_state == PlcConnectionState::Recovering)
  1752. {
  1753. register_status_label_->setText(tr("PLC:通信已恢复,正在重新首读"));
  1754. }
  1755. else if (plc_state == PlcConnectionState::Faulted)
  1756. {
  1757. register_status_label_->setText(tr("PLC:通信故障"));
  1758. }
  1759. updateSimulationUi(false);
  1760. statusBar()->showMessage(message, 4000);
  1761. const bool duplicate = ui_->outputList->count() > 0
  1762. && ui_->outputList->item(ui_->outputList->count() - 1)->text() == message;
  1763. if (!duplicate)
  1764. {
  1765. ui_->outputList->addItem(message);
  1766. ui_->outputList->scrollToBottom();
  1767. }
  1768. }
  1769. void MainWindow::updateSimulationUi(bool report_fault)
  1770. {
  1771. const bool offline = runtime_mode_service_.mode()
  1772. == ApplicationMode::OfflineRunning;
  1773. const bool online = runtime_mode_service_.mode()
  1774. == ApplicationMode::OnlineRunning;
  1775. const bool plc_connected = runtime_mode_service_.plcConnectionState()
  1776. == PlcConnectionState::Connected;
  1777. const SimulationState state = runtime_mode_service_.simulationState();
  1778. hmi_editor_widget_->setRuntimeWriteEnabled(
  1779. (online && plc_connected)
  1780. || (offline && state == SimulationState::Running));
  1781. if (runtime_monitor_widget_ != nullptr)
  1782. {
  1783. runtime_monitor_widget_->setHmiWriteEnabled(
  1784. (online && plc_connected)
  1785. || (offline && state == SimulationState::Running));
  1786. runtime_monitor_widget_->refreshValues(
  1787. runtime_mode_service_.mode(), runtime_mode_service_.plcConnectionState());
  1788. }
  1789. switch (state)
  1790. {
  1791. case SimulationState::Running:
  1792. {
  1793. executor_status_label_->setText(
  1794. tr("逻辑执行器:运行(%1 次)")
  1795. .arg(runtime_mode_service_.successfulScanCount()));
  1796. break;
  1797. }
  1798. case SimulationState::Faulted:
  1799. {
  1800. executor_status_label_->setText(tr("逻辑执行器:故障"));
  1801. if (report_fault)
  1802. {
  1803. const LogicScanResult &error = runtime_mode_service_.simulationError();
  1804. if (!error.logicId.empty()
  1805. && logic_editor_service_.findLogic(error.logicId) != nullptr)
  1806. {
  1807. current_logic_id_ = error.logicId;
  1808. logic_editor_widget_->setLogicId(current_logic_id_);
  1809. runtime_monitor_widget_->setProjectObjects(
  1810. hmi_navigation_service_->currentPageId(), current_logic_id_);
  1811. }
  1812. logic_editor_widget_->setRuntimeTrace(
  1813. runtime_mode_service_.offlineSimulationService()
  1814. .traceSnapshot().forLogic(current_logic_id_),
  1815. error.nodeId);
  1816. runtime_monitor_widget_->setLogicTrace(
  1817. runtime_mode_service_.offlineSimulationService().traceSnapshot(),
  1818. error.nodeId);
  1819. if (!error.nodeId.empty())
  1820. {
  1821. logic_editor_widget_->selectNode(error.nodeId);
  1822. showLogicNodeProperties(error.nodeId);
  1823. }
  1824. QString message = tr("离线仿真异常:%1").arg(fromUtf8(error.message));
  1825. if (!error.logicId.empty())
  1826. {
  1827. message += tr(",逻辑 %1").arg(fromUtf8(error.logicId));
  1828. }
  1829. if (!error.rungId.empty())
  1830. {
  1831. message += tr(",网络 %1").arg(fromUtf8(error.rungId));
  1832. }
  1833. if (!error.nodeId.empty())
  1834. {
  1835. message += tr(",节点 %1").arg(fromUtf8(error.nodeId));
  1836. }
  1837. statusBar()->showMessage(message);
  1838. ui_->outputList->addItem(message);
  1839. ui_->outputList->scrollToBottom();
  1840. }
  1841. break;
  1842. }
  1843. case SimulationState::Stopped:
  1844. default:
  1845. {
  1846. executor_status_label_->setText(tr("逻辑执行器:停止"));
  1847. break;
  1848. }
  1849. }
  1850. }
  1851. // 根据当前真实运行模式,同步更新模式工具栏单选按钮选中状态
  1852. void MainWindow::restoreCurrentModeAction()
  1853. {
  1854. ui_->editingModeAction->setChecked(runtime_mode_service_.mode()
  1855. == ApplicationMode::Editing);
  1856. ui_->offlineModeAction->setChecked(runtime_mode_service_.mode()
  1857. == ApplicationMode::OfflineRunning);
  1858. ui_->onlineModeAction->setChecked(runtime_mode_service_.mode()
  1859. == ApplicationMode::OnlineRunning);
  1860. }