综合平台编程器项目的远程存储
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 

2541 rinda
89 KiB

  1. #include "main_window.h"
  2. #include "hmi_editor_widget.h"
  3. #include "alarm_configuration_dialog.h"
  4. #include "register_comment_dialog.h"
  5. #include "logic_editor_widget.h"
  6. #include "logic_instruction_dialog.h"
  7. #include "plc_connection_dialog.h"
  8. #include "free_monitor_widget.h"
  9. #include "runtime_monitor_widget.h"
  10. #include "toolbar_icon_factory.h"
  11. #include "project_workspace_controller.h"
  12. #include "property_panel_controller.h"
  13. #include "runtime_panel_controller.h"
  14. #include "services/hmi_editor_service.h"
  15. #include "services/hmi_navigation_service.h"
  16. #include "services/hmi_runtime_service.h"
  17. #include "services/alarm_editor_service.h"
  18. #include "services/alarm_service.h"
  19. #include "services/logic_editor_service.h"
  20. #include "services/project_service.h"
  21. #include "services/runtime_mode_service.h"
  22. #include "services/register_monitor_service.h"
  23. #include "services/register_comment_service.h"
  24. #include "ui_main_window.h"
  25. #include "domain/project_limits.h"
  26. #include "infrastructure/runtime_project_bundle.h"
  27. #include <QActionGroup>
  28. #include <QAbstractSpinBox>
  29. #include <QApplication>
  30. #include <QCloseEvent>
  31. #include <QDialogButtonBox>
  32. #include <QDockWidget>
  33. #include <QEvent>
  34. #include <QFileDialog>
  35. #include <QGraphicsScene>
  36. #include <QInputDialog>
  37. #include <QIcon>
  38. #include <QLabel>
  39. #include <QLineEdit>
  40. #include <QListWidget>
  41. #include <QListWidgetItem>
  42. #include <QKeyEvent>
  43. #include <QKeySequence>
  44. #include <QMessageBox>
  45. #include <QMenu>
  46. #include <QPlainTextEdit>
  47. #include <QPushButton>
  48. #include <QStatusBar>
  49. #include <QTabWidget>
  50. #include <QToolBar>
  51. #include <QToolButton>
  52. #include <QTextEdit>
  53. #include <QTimer>
  54. #include <QDir>
  55. #include <QFileInfo>
  56. #include <QProgressDialog>
  57. #include <QSignalBlocker>
  58. #include <algorithm>
  59. namespace {
  60. constexpr int kSyntaxLogicIdRole = Qt::UserRole + 1;
  61. constexpr int kSyntaxRungIdRole = Qt::UserRole + 2;
  62. constexpr int kSyntaxColumnRole = Qt::UserRole + 3;
  63. bool isTextEditingObject(QObject *object)
  64. {
  65. QWidget *widget = qobject_cast<QWidget *>(object);
  66. while (widget != nullptr)
  67. {
  68. if (qobject_cast<QLineEdit *>(widget) != nullptr
  69. || qobject_cast<QTextEdit *>(widget) != nullptr
  70. || qobject_cast<QPlainTextEdit *>(widget) != nullptr
  71. || qobject_cast<QAbstractSpinBox *>(widget) != nullptr)
  72. {
  73. return true;
  74. }
  75. widget = widget->parentWidget();
  76. }
  77. return false;
  78. }
  79. QString suggestedProjectFileName(QString project_name)
  80. {
  81. project_name = project_name.trimmed();
  82. const QString invalid_characters = QStringLiteral("<>:\"/\\|?*");
  83. for (QChar &character : project_name)
  84. {
  85. if (character.unicode() < 0x20U || invalid_characters.contains(character))
  86. {
  87. character = QLatin1Char('_');
  88. }
  89. }
  90. constexpr int kMaximumSuggestedBaseNameLength = 240;
  91. if (project_name.size() > kMaximumSuggestedBaseNameLength)
  92. {
  93. project_name.truncate(kMaximumSuggestedBaseNameLength);
  94. if (!project_name.isEmpty()
  95. && project_name.at(project_name.size() - 1).isHighSurrogate())
  96. {
  97. project_name.chop(1);
  98. }
  99. }
  100. while (project_name.endsWith(QLatin1Char(' '))
  101. || project_name.endsWith(QLatin1Char('.')))
  102. {
  103. project_name.chop(1);
  104. }
  105. if (project_name.isEmpty())
  106. {
  107. project_name = QStringLiteral("未命名工程");
  108. }
  109. const QString device_name = project_name.section(QLatin1Char('.'), 0, 0).toUpper();
  110. const bool is_reserved_device_name = device_name == QStringLiteral("CON")
  111. || device_name == QStringLiteral("PRN")
  112. || device_name == QStringLiteral("AUX")
  113. || device_name == QStringLiteral("NUL")
  114. || (device_name.size() == 4
  115. && (device_name.startsWith(QStringLiteral("COM"))
  116. || device_name.startsWith(QStringLiteral("LPT")))
  117. && device_name.back() >= QLatin1Char('1')
  118. && device_name.back() <= QLatin1Char('9'));
  119. if (is_reserved_device_name)
  120. {
  121. project_name.prepend(QLatin1Char('_'));
  122. }
  123. return project_name + QStringLiteral(".json");
  124. }
  125. bool isEditorShortcut(const QKeyEvent &event)
  126. {
  127. return event.matches(QKeySequence::Undo)
  128. || event.matches(QKeySequence::Redo)
  129. || event.matches(QKeySequence::Copy)
  130. || event.matches(QKeySequence::Paste)
  131. || (event.modifiers() == Qt::NoModifier
  132. && (event.key() == Qt::Key_Delete || event.key() == Qt::Key_Escape));
  133. }
  134. QString modeText(ApplicationMode mode)
  135. {
  136. switch (mode)
  137. {
  138. case ApplicationMode::Editing:
  139. {
  140. return MainWindow::tr("编辑态");
  141. }
  142. case ApplicationMode::OfflineRunning:
  143. {
  144. return MainWindow::tr("离线运行态");
  145. }
  146. case ApplicationMode::OnlineRunning:
  147. {
  148. return MainWindow::tr("真机运行态");
  149. }
  150. default:
  151. {
  152. return MainWindow::tr("未知状态");
  153. }
  154. }
  155. }
  156. QString transitionErrorText(ModeTransitionError error)
  157. {
  158. switch (error)
  159. {
  160. case ModeTransitionError::MustReturnToEditing:
  161. {
  162. return MainWindow::tr("请先返回编辑态,再切换运行模式");
  163. }
  164. case ModeTransitionError::InitialPlcReadRequired:
  165. {
  166. return MainWindow::tr("真机运行前必须连接 PLC 并完成首次读取");
  167. }
  168. case ModeTransitionError::AlreadyInRequestedMode:
  169. {
  170. return MainWindow::tr("当前已处于所选模式");
  171. }
  172. case ModeTransitionError::ProjectNotReady:
  173. {
  174. return MainWindow::tr("工程存在未配置或未完成的 HMI/梯形图节点,暂不能进入运行态");
  175. }
  176. case ModeTransitionError::SimulationStartFailed:
  177. {
  178. return MainWindow::tr("本地逻辑执行器预检或启动失败");
  179. }
  180. case ModeTransitionError::None:
  181. default:
  182. {
  183. return MainWindow::tr("模式切换失败");
  184. }
  185. }
  186. }
  187. std::string toUtf8(const QString &value)
  188. {
  189. const QByteArray bytes = value.toUtf8();
  190. return std::string(bytes.constData(), static_cast<std::size_t>(bytes.size()));
  191. }
  192. QString fromUtf8(const std::string &value)
  193. {
  194. return QString::fromUtf8(value.data(), static_cast<int>(value.size()));
  195. }
  196. QString plcStatusText(const RuntimeModeService &service)
  197. {
  198. switch (service.plcConnectionState())
  199. {
  200. case PlcConnectionState::Connecting:
  201. return MainWindow::tr("PLC 正在连接");
  202. case PlcConnectionState::Connected:
  203. return service.initialPlcReadCompleted()
  204. ? MainWindow::tr("PLC 已连接,首次读取完成")
  205. : MainWindow::tr("PLC 已连接,正在读取工程使用的 M/D 地址");
  206. case PlcConnectionState::Recovering:
  207. return MainWindow::tr("PLC 通信已恢复,正在重新读取工程使用的 M/D 地址");
  208. case PlcConnectionState::Faulted:
  209. {
  210. const QString error = fromUtf8(service.plcError());
  211. return error.isEmpty()
  212. ? MainWindow::tr("PLC 通信故障")
  213. : MainWindow::tr("PLC 通信故障:%1").arg(error);
  214. }
  215. case PlcConnectionState::Disconnected:
  216. {
  217. const QString error = fromUtf8(service.plcError());
  218. return error.isEmpty()
  219. ? MainWindow::tr("PLC 已断开")
  220. : MainWindow::tr("PLC 已断开:%1").arg(error);
  221. }
  222. default:
  223. {
  224. return MainWindow::tr("PLC 已断开");
  225. }
  226. }
  227. }
  228. QToolButton *addToolbarMenu(
  229. QToolBar *toolbar,
  230. const QString &text,
  231. const QString &object_name,
  232. const QIcon &icon,
  233. const QList<QAction *> &actions)
  234. {
  235. auto *button = new QToolButton(toolbar);
  236. button->setObjectName(object_name);
  237. button->setText(text);
  238. button->setIcon(icon);
  239. button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
  240. button->setPopupMode(QToolButton::InstantPopup);
  241. auto *menu = new QMenu(button);
  242. menu->addActions(actions);
  243. button->setMenu(menu);
  244. toolbar->addWidget(button);
  245. return button;
  246. }
  247. bool copyDirectoryContents(
  248. const QString &source_path, const QString &destination_path, QString *error)
  249. {
  250. QDir source_directory(source_path);
  251. if (!source_directory.exists())
  252. {
  253. if (error != nullptr)
  254. {
  255. *error = MainWindow::tr("未找到打包目录:%1").arg(source_path);
  256. }
  257. return false;
  258. }
  259. QDir destination_directory(destination_path);
  260. if (!destination_directory.exists()
  261. && !QDir().mkpath(destination_path))
  262. {
  263. if (error != nullptr)
  264. {
  265. *error = MainWindow::tr("无法创建导出目录:%1").arg(destination_path);
  266. }
  267. return false;
  268. }
  269. const QFileInfoList entries = source_directory.entryInfoList(
  270. QDir::NoDotAndDotDot | QDir::AllEntries,
  271. QDir::DirsFirst | QDir::Name);
  272. for (const QFileInfo &entry : entries)
  273. {
  274. const QString destination = destination_directory.filePath(entry.fileName());
  275. if (entry.isDir())
  276. {
  277. if (!copyDirectoryContents(entry.absoluteFilePath(), destination, error))
  278. {
  279. return false;
  280. }
  281. continue;
  282. }
  283. if (QFileInfo::exists(destination)
  284. || !QFile::copy(entry.absoluteFilePath(), destination))
  285. {
  286. if (error != nullptr)
  287. {
  288. *error = MainWindow::tr("复制打包文件失败:%1").arg(destination);
  289. }
  290. return false;
  291. }
  292. }
  293. return true;
  294. }
  295. } // namespace
  296. MainWindow::MainWindow(
  297. RuntimeModeService &runtime_mode_service,
  298. ProjectService &project_service,
  299. HmiEditorService &hmi_editor_service,
  300. LogicEditorService &logic_editor_service,
  301. HmiRuntimeService &hmi_runtime_service,
  302. AlarmEditorService &alarm_editor_service,
  303. AlarmService &alarm_service,
  304. RegisterCommentService &register_comment_service,
  305. RegisterMonitorService &register_monitor_service,
  306. PlcDiscoveryGateway &plc_discovery_gateway,
  307. const ApplicationSettingsLoadResult &application_settings_result,
  308. bool user_runtime_mode,
  309. QWidget *parent)
  310. : QMainWindow(parent),
  311. ui_(std::make_unique<Ui::MainWindow>()),
  312. runtime_mode_service_(runtime_mode_service),
  313. project_service_(project_service),
  314. hmi_editor_service_(hmi_editor_service),
  315. logic_editor_service_(logic_editor_service),
  316. hmi_runtime_service_(hmi_runtime_service),
  317. alarm_editor_service_(alarm_editor_service),
  318. alarm_service_(alarm_service),
  319. register_comment_service_(register_comment_service),
  320. register_monitor_service_(register_monitor_service),
  321. plc_discovery_gateway_(plc_discovery_gateway),
  322. application_settings_result_(application_settings_result),
  323. user_runtime_mode_(user_runtime_mode),
  324. owned_hmi_navigation_service_(
  325. std::make_unique<HmiNavigationService>(project_service)),
  326. hmi_navigation_service_(owned_hmi_navigation_service_.get()),
  327. plc_configuration_(application_settings_result.settings.plcDefaults)
  328. {
  329. initializeUi();
  330. }
  331. MainWindow::MainWindow(
  332. RuntimeModeService &runtime_mode_service,
  333. ProjectService &project_service,
  334. HmiEditorService &hmi_editor_service,
  335. LogicEditorService &logic_editor_service,
  336. HmiRuntimeService &hmi_runtime_service,
  337. AlarmEditorService &alarm_editor_service,
  338. AlarmService &alarm_service,
  339. HmiNavigationService &hmi_navigation_service,
  340. RegisterCommentService &register_comment_service,
  341. RegisterMonitorService &register_monitor_service,
  342. PlcDiscoveryGateway &plc_discovery_gateway,
  343. const ApplicationSettingsLoadResult &application_settings_result,
  344. bool user_runtime_mode,
  345. QWidget *parent)
  346. : QMainWindow(parent),
  347. ui_(std::make_unique<Ui::MainWindow>()),
  348. runtime_mode_service_(runtime_mode_service),
  349. project_service_(project_service),
  350. hmi_editor_service_(hmi_editor_service),
  351. logic_editor_service_(logic_editor_service),
  352. hmi_runtime_service_(hmi_runtime_service),
  353. alarm_editor_service_(alarm_editor_service),
  354. alarm_service_(alarm_service),
  355. register_comment_service_(register_comment_service),
  356. register_monitor_service_(register_monitor_service),
  357. plc_discovery_gateway_(plc_discovery_gateway),
  358. application_settings_result_(application_settings_result),
  359. user_runtime_mode_(user_runtime_mode),
  360. hmi_navigation_service_(&hmi_navigation_service),
  361. plc_configuration_(application_settings_result.settings.plcDefaults)
  362. {
  363. initializeUi();
  364. }
  365. void MainWindow::initializeUi()
  366. {
  367. ui_->setupUi(this);
  368. qApp->installEventFilter(this);
  369. configureAppearance();
  370. property_panel_controller_ = std::make_unique<PropertyPanelController>(
  371. *this,
  372. *ui_,
  373. project_service_,
  374. hmi_editor_service_,
  375. logic_editor_service_,
  376. application_settings_result_.settings.hmiDefaults,
  377. [this] { return current_hmi_page_id_; },
  378. [this] { return current_logic_id_; },
  379. selected_control_id_,
  380. selected_logic_node_id_,
  381. [this] { refreshProjectUi(); },
  382. [this](const QString &action, const QString &message, bool succeeded)
  383. {
  384. showProjectResult(action, message, succeeded);
  385. },
  386. [this](const QString &message, int timeout_ms)
  387. {
  388. statusBar()->showMessage(message, timeout_ms);
  389. });
  390. configureActions();
  391. configurePropertyEditor();
  392. configurePlcConnection();
  393. configureAlarms();
  394. configureRegisterComments();
  395. configureHmiEditor();
  396. configureLogicEditor();
  397. configureRuntimeMonitor();
  398. configureDataMonitor();
  399. configureProjectTree();
  400. runtime_mode_service_.setPlcStatusChangedCallback(
  401. [this] { schedulePlcStatusUpdate(); });
  402. hmi_editor_service_.ensureDefaultPage();
  403. logic_editor_service_.ensureDefaultLogic();
  404. clearEditorHistories();
  405. current_hmi_page_id_ = hmi_editor_service_.firstPageId();
  406. current_logic_id_ = logic_editor_service_.firstLogicId();
  407. showControlProperties({});
  408. refreshProjectUi();
  409. updateModeUi(tr("系统已进入编辑态"));
  410. for (const std::string &message : application_settings_result_.messages)
  411. {
  412. appendOutputMessage(fromUtf8(message));
  413. }
  414. if (application_settings_result_.warningRequired)
  415. {
  416. const QString warning = fromUtf8(
  417. application_settings_result_.warningMessage);
  418. QTimer::singleShot(
  419. 0,
  420. this,
  421. [this, warning]
  422. {
  423. QMessageBox::warning(this, tr("应用配置"), warning);
  424. });
  425. }
  426. if (user_runtime_mode_)
  427. {
  428. initializeUserRuntime();
  429. }
  430. }
  431. void MainWindow::initializeUserRuntime()
  432. {
  433. setWindowTitle(fromUtf8(project_service_.project().metadata.name));
  434. runtime_panel_controller_->configureUserRuntimeMode(
  435. true,
  436. [this](int mode)
  437. {
  438. if (mode == static_cast<int>(ApplicationMode::OfflineRunning))
  439. {
  440. requestUserRuntimeMode(ApplicationMode::OfflineRunning);
  441. }
  442. else if (mode == static_cast<int>(ApplicationMode::OnlineRunning))
  443. {
  444. requestUserRuntimeMode(ApplicationMode::OnlineRunning);
  445. }
  446. },
  447. [this] { connectPlc(); },
  448. [this] { disconnectPlc(); });
  449. menuBar()->setVisible(false);
  450. ui_->hmiToolBar->setVisible(false);
  451. ui_->logicToolBar->setVisible(false);
  452. ui_->outputDock->setVisible(false);
  453. ui_->projectDock->setVisible(false);
  454. ui_->propertiesDock->setVisible(false);
  455. QTimer::singleShot(
  456. 0,
  457. this,
  458. [this]
  459. {
  460. if (requestMode(ApplicationMode::OfflineRunning))
  461. {
  462. hide();
  463. }
  464. else
  465. {
  466. qApp->quit();
  467. }
  468. });
  469. }
  470. void MainWindow::requestUserRuntimeMode(ApplicationMode requested_mode)
  471. {
  472. if (runtime_mode_service_.mode() == requested_mode)
  473. {
  474. return;
  475. }
  476. if (requested_mode == ApplicationMode::OnlineRunning
  477. && !runtime_mode_service_.initialPlcReadCompleted())
  478. {
  479. // 用户运行版没有可见的编辑器主窗口,失败前不能先隐藏运行监控窗口
  480. if (runtime_monitor_widget_ != nullptr)
  481. {
  482. runtime_monitor_widget_->setMode(
  483. runtime_mode_service_.mode(),
  484. runtime_mode_service_.plcConnectionState());
  485. const QString message = transitionErrorText(
  486. ModeTransitionError::InitialPlcReadRequired)
  487. + tr("\n当前状态:%1").arg(
  488. plcStatusText(runtime_mode_service_))
  489. + tr("\n请先点击“PLC 配置”连接并完成首次读取。");
  490. QMessageBox::warning(
  491. runtime_monitor_widget_,
  492. tr("无法切换到真机运行"),
  493. message);
  494. }
  495. return;
  496. }
  497. if (runtime_mode_service_.mode() != ApplicationMode::Editing)
  498. {
  499. const ModeTransitionResult editing = runtime_mode_service_.enterEditing();
  500. if (!editing.succeeded)
  501. {
  502. return;
  503. }
  504. updateModeUi(tr("正在切换运行模式"));
  505. QTimer::singleShot(
  506. 0,
  507. this,
  508. [this, requested_mode]
  509. {
  510. requestMode(requested_mode);
  511. });
  512. return;
  513. }
  514. requestMode(requested_mode);
  515. }
  516. MainWindow::~MainWindow()
  517. {
  518. qApp->removeEventFilter(this);
  519. register_monitor_service_.setAddressesChangedCallback({});
  520. runtime_mode_service_.setPlcStatusChangedCallback({});
  521. }
  522. bool MainWindow::eventFilter(QObject *watched, QEvent *event)
  523. {
  524. if (event->type() == QEvent::ShortcutOverride
  525. && isTextEditingObject(watched))
  526. {
  527. auto *key_event = static_cast<QKeyEvent *>(event);
  528. if (isEditorShortcut(*key_event))
  529. {
  530. event->accept();
  531. return true;
  532. }
  533. }
  534. return QMainWindow::eventFilter(watched, event);
  535. }
  536. void MainWindow::closeEvent(QCloseEvent *event)
  537. {
  538. if (!confirmSaveBeforeDestructiveAction())
  539. {
  540. event->ignore();
  541. return;
  542. }
  543. if (runtime_panel_controller_ != nullptr)
  544. {
  545. runtime_panel_controller_->closeForApplicationExit();
  546. }
  547. QMainWindow::closeEvent(event);
  548. }
  549. void MainWindow::configureActions()
  550. {
  551. mode_action_group_ = new QActionGroup(this);
  552. mode_action_group_->setExclusive(true);
  553. mode_action_group_->addAction(ui_->editingModeAction);
  554. mode_action_group_->addAction(ui_->offlineModeAction);
  555. mode_action_group_->addAction(ui_->onlineModeAction);
  556. connect(ui_->editingModeAction, &QAction::triggered, this,
  557. [this] { requestMode(ApplicationMode::Editing); });
  558. connect(ui_->offlineModeAction, &QAction::triggered, this,
  559. [this] { requestMode(ApplicationMode::OfflineRunning); });
  560. connect(ui_->onlineModeAction, &QAction::triggered, this,
  561. [this] { requestMode(ApplicationMode::OnlineRunning); });
  562. connect(ui_->exitAction, &QAction::triggered, this, [this] { close(); });
  563. connect(ui_->newProjectAction, &QAction::triggered, this, &MainWindow::createNewProject);
  564. connect(ui_->saveProjectAction, &QAction::triggered, this, &MainWindow::saveProject);
  565. connect(ui_->saveAsProjectAction, &QAction::triggered, this, &MainWindow::saveProjectAs);
  566. connect(ui_->loadProjectAction, &QAction::triggered, this, &MainWindow::loadProject);
  567. connect(ui_->exportRuntimeAction, &QAction::triggered,
  568. this, &MainWindow::exportRuntimeProgram);
  569. connect(ui_->undoAction, &QAction::triggered,
  570. this, &MainWindow::undoActiveEditor);
  571. connect(ui_->redoAction, &QAction::triggered,
  572. this, &MainWindow::redoActiveEditor);
  573. connect(ui_->copyAction, &QAction::triggered,
  574. this, &MainWindow::copyActiveSelection);
  575. connect(ui_->pasteAction, &QAction::triggered,
  576. this, &MainWindow::pasteActiveSelection);
  577. connect(ui_->deleteSelectionAction, &QAction::triggered,
  578. this, &MainWindow::deleteActiveSelection);
  579. connect(ui_->clearSelectionAction, &QAction::triggered,
  580. this, &MainWindow::clearActiveSelection);
  581. connect(ui_->syntaxCheckAction, &QAction::triggered,
  582. this, &MainWindow::runLogicSyntaxCheck);
  583. connect(ui_->doubleCoilCheckAction, &QAction::triggered,
  584. this, &MainWindow::runDoubleCoilCheck);
  585. connect(ui_->outputList, &QListWidget::itemDoubleClicked,
  586. this,
  587. [this](QListWidgetItem *item)
  588. {
  589. if (item == nullptr)
  590. {
  591. return;
  592. }
  593. const std::string logic_id = toUtf8(
  594. item->data(kSyntaxLogicIdRole).toString());
  595. const std::string rung_id = toUtf8(
  596. item->data(kSyntaxRungIdRole).toString());
  597. if (logic_id.empty() || rung_id.empty())
  598. {
  599. return;
  600. }
  601. focusLogicSyntaxLocation({
  602. logic_id,
  603. rung_id,
  604. 0,
  605. 0,
  606. item->data(kSyntaxColumnRole).toInt()});
  607. });
  608. connect(ui_->addButtonAction, &QAction::triggered, this,
  609. [this] { addHmiControl(HmiControlType::Button); });
  610. connect(ui_->addIndicatorAction, &QAction::triggered, this,
  611. [this] { addHmiControl(HmiControlType::Indicator); });
  612. connect(ui_->addNumericDisplayAction, &QAction::triggered, this,
  613. [this] { addHmiControl(HmiControlType::NumericDisplay); });
  614. connect(ui_->addNumericInputAction, &QAction::triggered, this,
  615. [this] { addHmiControl(HmiControlType::NumericInput); });
  616. connect(ui_->addLabelAction, &QAction::triggered, this,
  617. [this] { addHmiControl(HmiControlType::Label); });
  618. connect(ui_->addPageJumpAction, &QAction::triggered, this,
  619. [this] { addHmiControl(HmiControlType::PageJump); });
  620. connect(ui_->addAlarmListAction, &QAction::triggered, this,
  621. [this] { addHmiControl(HmiControlType::AlarmList); });
  622. connect(ui_->deleteControlAction, &QAction::triggered,
  623. this, &MainWindow::deleteSelectedControl);
  624. addToolbarMenu(
  625. ui_->hmiToolBar,
  626. tr("更多控件"),
  627. QStringLiteral("hmiMoreControlsButton"),
  628. makeUiIcon(UiIcon::More),
  629. QList<QAction *>{
  630. ui_->addPageJumpAction,
  631. ui_->addAlarmListAction,
  632. ui_->configureAlarmsAction});
  633. connect(ui_->addRungAction, &QAction::triggered,
  634. this, &MainWindow::addLogicRung);
  635. connect(ui_->insertRungAboveAction, &QAction::triggered,
  636. this, &MainWindow::insertLogicRungAbove);
  637. connect(ui_->insertRungBelowAction, &QAction::triggered,
  638. this, &MainWindow::insertLogicRungBelow);
  639. connect(ui_->deleteRungAction, &QAction::triggered,
  640. this, &MainWindow::deleteLogicRung);
  641. connect(ui_->mouseDrawWireAction, &QAction::triggered,
  642. this,
  643. [this](bool checked)
  644. {
  645. if (checked)
  646. {
  647. const QSignalBlocker blocker(ui_->mouseEraseWireAction);
  648. ui_->mouseEraseWireAction->setChecked(false);
  649. }
  650. logic_editor_widget_->setMouseWireMode(
  651. checked
  652. ? LogicEditorWidget::MouseWireMode::Draw
  653. : ui_->mouseEraseWireAction->isChecked()
  654. ? LogicEditorWidget::MouseWireMode::Erase
  655. : LogicEditorWidget::MouseWireMode::Select);
  656. });
  657. connect(ui_->mouseEraseWireAction, &QAction::triggered,
  658. this,
  659. [this](bool checked)
  660. {
  661. if (checked)
  662. {
  663. const QSignalBlocker blocker(ui_->mouseDrawWireAction);
  664. ui_->mouseDrawWireAction->setChecked(false);
  665. }
  666. logic_editor_widget_->setMouseWireMode(
  667. checked
  668. ? LogicEditorWidget::MouseWireMode::Erase
  669. : ui_->mouseDrawWireAction->isChecked()
  670. ? LogicEditorWidget::MouseWireMode::Draw
  671. : LogicEditorWidget::MouseWireMode::Select);
  672. });
  673. connect(ui_->insertHorizontalWireAction, &QAction::triggered,
  674. this, &MainWindow::addLogicHorizontalWire);
  675. connect(ui_->insertVerticalWireAction, &QAction::triggered,
  676. this, &MainWindow::addLogicVerticalWire);
  677. connect(ui_->deleteHorizontalWireAction, &QAction::triggered,
  678. this, &MainWindow::deleteLogicHorizontalWire);
  679. connect(ui_->deleteVerticalWireAction, &QAction::triggered,
  680. this, &MainWindow::deleteLogicVerticalWire);
  681. connect(ui_->parallelInsertAction, &QAction::triggered,
  682. this,
  683. [this]
  684. {
  685. addLogicParallelBranch(ContactNodeConfig{
  686. RegisterAddress{RegisterArea::M, 0},
  687. ContactMode::NormallyOpen});
  688. });
  689. QMenu *parallel_menu = new QMenu(this);
  690. QAction *parallel_open = parallel_menu->addAction(tr("并联常开触点"));
  691. QAction *parallel_closed = parallel_menu->addAction(tr("并联常闭触点"));
  692. QAction *parallel_rising = parallel_menu->addAction(tr("并联上升沿触点"));
  693. QAction *parallel_falling = parallel_menu->addAction(tr("并联下降沿触点"));
  694. QAction *parallel_compare = parallel_menu->addAction(tr("并联比较条件"));
  695. connect(parallel_open, &QAction::triggered,
  696. this,
  697. [this]
  698. {
  699. addLogicParallelBranch(ContactNodeConfig{
  700. RegisterAddress{RegisterArea::M, 0},
  701. ContactMode::NormallyOpen});
  702. });
  703. connect(parallel_closed, &QAction::triggered,
  704. this,
  705. [this]
  706. {
  707. addLogicParallelBranch(ContactNodeConfig{
  708. RegisterAddress{RegisterArea::M, 0},
  709. ContactMode::NormallyClosed});
  710. });
  711. connect(parallel_rising, &QAction::triggered,
  712. this,
  713. [this]
  714. {
  715. addLogicParallelBranch(EdgeContactNodeConfig{
  716. RegisterAddress{RegisterArea::M, 0}, EdgeMode::Rising});
  717. });
  718. connect(parallel_falling, &QAction::triggered,
  719. this,
  720. [this]
  721. {
  722. addLogicParallelBranch(EdgeContactNodeConfig{
  723. RegisterAddress{RegisterArea::M, 0}, EdgeMode::Falling});
  724. });
  725. connect(parallel_compare, &QAction::triggered,
  726. this,
  727. [this]
  728. {
  729. addLogicParallelBranch(CompareNodeConfig{
  730. RegisterAddress{RegisterArea::D, 0},
  731. ComparisonOperator::Equal,
  732. 0});
  733. });
  734. ui_->parallelInsertAction->setMenu(parallel_menu);
  735. if (QToolButton *parallel_button = qobject_cast<QToolButton *>(
  736. ui_->logicToolBar->widgetForAction(ui_->parallelInsertAction)))
  737. {
  738. parallel_button->setPopupMode(QToolButton::MenuButtonPopup);
  739. }
  740. connect(ui_->addNormallyOpenAction, &QAction::triggered,
  741. this,
  742. [this]
  743. {
  744. addLogicCondition(ContactNodeConfig{
  745. RegisterAddress{RegisterArea::M, 0},
  746. ContactMode::NormallyOpen});
  747. });
  748. connect(ui_->addNormallyClosedAction, &QAction::triggered,
  749. this,
  750. [this]
  751. {
  752. addLogicCondition(ContactNodeConfig{
  753. RegisterAddress{RegisterArea::M, 0},
  754. ContactMode::NormallyClosed});
  755. });
  756. connect(ui_->addRisingEdgeAction, &QAction::triggered,
  757. this,
  758. [this]
  759. {
  760. addLogicCondition(EdgeContactNodeConfig{
  761. RegisterAddress{RegisterArea::M, 0}, EdgeMode::Rising});
  762. });
  763. connect(ui_->addFallingEdgeAction, &QAction::triggered,
  764. this,
  765. [this]
  766. {
  767. addLogicCondition(EdgeContactNodeConfig{
  768. RegisterAddress{RegisterArea::M, 0}, EdgeMode::Falling});
  769. });
  770. connect(ui_->addNormalCoilAction, &QAction::triggered,
  771. this,
  772. [this]
  773. {
  774. setLogicOutput(CoilNodeConfig{
  775. RegisterAddress{RegisterArea::M, 0},
  776. CoilMode::Normal});
  777. });
  778. connect(ui_->addSetCoilAction, &QAction::triggered,
  779. this,
  780. [this]
  781. {
  782. setLogicOutput(CoilNodeConfig{
  783. RegisterAddress{RegisterArea::M, 0},
  784. CoilMode::Set});
  785. });
  786. connect(ui_->addResetCoilAction, &QAction::triggered,
  787. this,
  788. [this]
  789. {
  790. setLogicOutput(CoilNodeConfig{
  791. RegisterAddress{RegisterArea::M, 0},
  792. CoilMode::Reset});
  793. });
  794. connect(ui_->addMoveAction, &QAction::triggered,
  795. this,
  796. [this]
  797. {
  798. configureAndSetLogicOutput(MoveNodeConfig{
  799. WordOperand{
  800. WordOperandKind::Constant,
  801. RegisterAddress{RegisterArea::D, 0},
  802. 0},
  803. RegisterAddress{RegisterArea::D, 0}});
  804. });
  805. connect(ui_->addAddAction, &QAction::triggered,
  806. this,
  807. [this]
  808. {
  809. configureAndSetLogicOutput(ArithmeticNodeConfig{
  810. ArithmeticOperation::Add,
  811. WordOperand{
  812. WordOperandKind::Register,
  813. RegisterAddress{RegisterArea::D, 0},
  814. 0},
  815. WordOperand{
  816. WordOperandKind::Constant,
  817. RegisterAddress{RegisterArea::D, 0},
  818. 1},
  819. RegisterAddress{RegisterArea::D, 0}});
  820. });
  821. connect(ui_->addSubAction, &QAction::triggered,
  822. this,
  823. [this]
  824. {
  825. configureAndSetLogicOutput(ArithmeticNodeConfig{
  826. ArithmeticOperation::Subtract,
  827. WordOperand{
  828. WordOperandKind::Register,
  829. RegisterAddress{RegisterArea::D, 0},
  830. 0},
  831. WordOperand{
  832. WordOperandKind::Constant,
  833. RegisterAddress{RegisterArea::D, 0},
  834. 1},
  835. RegisterAddress{RegisterArea::D, 0}});
  836. });
  837. connect(ui_->addCompareAction, &QAction::triggered,
  838. this,
  839. [this]
  840. {
  841. addLogicCondition(CompareNodeConfig{
  842. RegisterAddress{RegisterArea::D, 0},
  843. ComparisonOperator::Equal,
  844. 0});
  845. });
  846. connect(ui_->deleteLogicAction, &QAction::triggered,
  847. this, &MainWindow::deleteSelectedLogicObject);
  848. connect(ui_->editNetworkCommentAction, &QAction::triggered,
  849. this, &MainWindow::editSelectedNetworkComment);
  850. addToolbarMenu(
  851. ui_->logicToolBar,
  852. tr("更多触点"),
  853. QStringLiteral("logicContactMenuButton"),
  854. makeUiIcon(UiIcon::NormallyOpenContact),
  855. QList<QAction *>{
  856. ui_->addRisingEdgeAction,
  857. ui_->addFallingEdgeAction});
  858. addToolbarMenu(
  859. ui_->logicToolBar,
  860. tr("更多输出"),
  861. QStringLiteral("logicOutputMenuButton"),
  862. makeUiIcon(UiIcon::Coil),
  863. QList<QAction *>{ui_->addSetCoilAction, ui_->addResetCoilAction});
  864. addToolbarMenu(
  865. ui_->logicToolBar,
  866. tr("数据运算"),
  867. QStringLiteral("logicDataMenuButton"),
  868. makeUiIcon(UiIcon::Move),
  869. QList<QAction *>{
  870. ui_->addMoveAction,
  871. ui_->addAddAction,
  872. ui_->addSubAction,
  873. ui_->addCompareAction});
  874. connect(ui_->editorTabWidget, &QTabWidget::currentChanged,
  875. this,
  876. [this](int index)
  877. {
  878. ui_->hmiToolBar->setVisible(index == 0);
  879. ui_->logicToolBar->setVisible(index == 1);
  880. if (index == 0)
  881. {
  882. showControlProperties(selected_control_id_);
  883. }
  884. else if (index == 1)
  885. {
  886. showLogicNodeProperties(selected_logic_node_id_);
  887. }
  888. else
  889. {
  890. // 数据监控页不对应 HMI 或梯形图对象
  891. showControlProperties({});
  892. }
  893. if (index == 0 || index == 1)
  894. {
  895. refreshProjectUi();
  896. }
  897. });
  898. ui_->hmiToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 0);
  899. ui_->logicToolBar->setVisible(ui_->editorTabWidget->currentIndex() == 1);
  900. ui_->viewMenu->addAction(ui_->projectDock->toggleViewAction());
  901. ui_->viewMenu->addAction(ui_->propertiesDock->toggleViewAction());
  902. ui_->viewMenu->addAction(ui_->outputDock->toggleViewAction());
  903. ui_->viewMenu->addAction(ui_->hmiToolBar->toggleViewAction());
  904. ui_->viewMenu->addAction(ui_->logicToolBar->toggleViewAction());
  905. ui_->viewMenu->addSeparator();
  906. ui_->viewMenu->addAction(ui_->modeToolBar->toggleViewAction());
  907. }
  908. void MainWindow::configureAppearance()
  909. {
  910. setDockNestingEnabled(true);
  911. setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea);
  912. setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea);
  913. ui_->editingModeAction->setIcon(makeUiIcon(UiIcon::Edit));
  914. ui_->offlineModeAction->setIcon(makeUiIcon(UiIcon::RunOffline));
  915. ui_->onlineModeAction->setIcon(makeUiIcon(UiIcon::RunOnline));
  916. ui_->newProjectAction->setIcon(makeUiIcon(UiIcon::NewDocument));
  917. ui_->saveProjectAction->setIcon(makeUiIcon(UiIcon::Save));
  918. ui_->saveAsProjectAction->setIcon(makeUiIcon(UiIcon::SaveAs));
  919. ui_->loadProjectAction->setIcon(makeUiIcon(UiIcon::Open));
  920. ui_->undoAction->setIcon(makeUiIcon(UiIcon::Undo));
  921. ui_->redoAction->setIcon(makeUiIcon(UiIcon::Redo));
  922. ui_->deleteSelectionAction->setIcon(makeUiIcon(UiIcon::Delete));
  923. ui_->clearSelectionAction->setIcon(makeUiIcon(UiIcon::ClearList));
  924. ui_->exitAction->setIcon(makeUiIcon(UiIcon::Exit));
  925. ui_->configurePlcAction->setIcon(makeUiIcon(UiIcon::PlcSettings));
  926. ui_->disconnectPlcAction->setIcon(makeUiIcon(UiIcon::Disconnect));
  927. ui_->configureRegisterCommentsAction->setIcon(
  928. makeUiIcon(UiIcon::RegisterComments));
  929. ui_->addButtonAction->setIcon(makeUiIcon(UiIcon::HmiButton));
  930. ui_->addIndicatorAction->setIcon(makeUiIcon(UiIcon::Indicator));
  931. ui_->addNumericDisplayAction->setIcon(makeUiIcon(UiIcon::NumericDisplay));
  932. ui_->addNumericInputAction->setIcon(makeUiIcon(UiIcon::NumericInput));
  933. ui_->addLabelAction->setIcon(makeUiIcon(UiIcon::Text));
  934. ui_->addPageJumpAction->setIcon(makeUiIcon(UiIcon::PageJump));
  935. ui_->addAlarmListAction->setIcon(makeUiIcon(UiIcon::AlarmList));
  936. ui_->configureAlarmsAction->setIcon(makeUiIcon(UiIcon::AlarmSettings));
  937. ui_->deleteControlAction->setIcon(makeUiIcon(UiIcon::Delete));
  938. ui_->addRungAction->setIcon(makeUiIcon(UiIcon::AddRung));
  939. ui_->insertRungAboveAction->setIcon(makeUiIcon(UiIcon::AddRung));
  940. ui_->insertRungBelowAction->setIcon(makeUiIcon(UiIcon::AddRung));
  941. ui_->deleteRungAction->setIcon(makeUiIcon(UiIcon::Delete));
  942. ui_->mouseDrawWireAction->setIcon(makeUiIcon(UiIcon::MouseDrawWire));
  943. ui_->mouseEraseWireAction->setIcon(makeUiIcon(UiIcon::MouseEraseWire));
  944. ui_->parallelInsertAction->setIcon(makeUiIcon(UiIcon::ParallelBranch));
  945. ui_->insertHorizontalWireAction->setIcon(makeUiIcon(UiIcon::HorizontalWire));
  946. ui_->insertVerticalWireAction->setIcon(makeUiIcon(UiIcon::VerticalWire));
  947. ui_->deleteHorizontalWireAction->setIcon(makeUiIcon(UiIcon::Delete));
  948. ui_->deleteVerticalWireAction->setIcon(makeUiIcon(UiIcon::Delete));
  949. ui_->addNormallyOpenAction->setIcon(makeUiIcon(UiIcon::NormallyOpenContact));
  950. ui_->addNormallyClosedAction->setIcon(makeUiIcon(UiIcon::NormallyClosedContact));
  951. ui_->addRisingEdgeAction->setIcon(makeUiIcon(UiIcon::RisingEdgeContact));
  952. ui_->addFallingEdgeAction->setIcon(makeUiIcon(UiIcon::FallingEdgeContact));
  953. ui_->addNormalCoilAction->setIcon(makeUiIcon(UiIcon::Coil));
  954. ui_->addSetCoilAction->setIcon(makeUiIcon(UiIcon::SetCoil));
  955. ui_->addResetCoilAction->setIcon(makeUiIcon(UiIcon::ResetCoil));
  956. ui_->addMoveAction->setIcon(makeUiIcon(UiIcon::Move));
  957. ui_->addAddAction->setIcon(makeUiIcon(UiIcon::Add));
  958. ui_->addSubAction->setIcon(makeUiIcon(UiIcon::Subtract));
  959. ui_->addCompareAction->setIcon(makeUiIcon(UiIcon::Compare));
  960. ui_->editNetworkCommentAction->setIcon(makeUiIcon(UiIcon::Comment));
  961. ui_->syntaxCheckAction->setIcon(makeUiIcon(UiIcon::SyntaxCheck));
  962. ui_->doubleCoilCheckAction->setIcon(makeUiIcon(UiIcon::Coil));
  963. ui_->deleteLogicAction->setIcon(makeUiIcon(UiIcon::Delete));
  964. ui_->editorTabWidget->setTabIcon(0, makeUiIcon(UiIcon::HmiPage));
  965. ui_->editorTabWidget->setTabIcon(1, makeUiIcon(UiIcon::Logic));
  966. ui_->outputDock->setMaximumHeight(220);
  967. mode_status_label_ = new QLabel(this);
  968. mode_status_label_->setObjectName(QStringLiteral("modeStatusLabel"));
  969. mode_status_label_->setMinimumWidth(88);
  970. mode_status_label_->setAlignment(Qt::AlignCenter);
  971. register_status_label_ = new QLabel(this);
  972. register_status_label_->setObjectName(QStringLiteral("registerStatusLabel"));
  973. register_status_label_->setMinimumWidth(132);
  974. executor_status_label_ = new QLabel(this);
  975. executor_status_label_->setObjectName(QStringLiteral("executorStatusLabel"));
  976. executor_status_label_->setMinimumWidth(132);
  977. statusBar()->addPermanentWidget(mode_status_label_);
  978. statusBar()->addPermanentWidget(register_status_label_);
  979. statusBar()->addPermanentWidget(executor_status_label_);
  980. setStyleSheet(QStringLiteral(
  981. "QMainWindow { background: #f3f5f7; }"
  982. "QToolBar { background: #ffffff; border: 0; border-bottom: 1px solid #cbd2d9;"
  983. " padding: 4px; spacing: 3px; }"
  984. "QToolButton { min-height: 26px; padding: 3px 9px; border: 1px solid transparent; }"
  985. "QToolButton:hover { background: #edf2f6; border-color: #c5ced6; }"
  986. "QToolButton:checked { background: #dcece3; border-color: #75a58a; }"
  987. "QDockWidget { color: #24313b; font-weight: 600; }"
  988. "QDockWidget::title { background: #e9edf0; padding: 6px;"
  989. " border-bottom: 1px solid #cbd2d9; }"
  990. "QTreeWidget, QListWidget, QScrollArea { background: #ffffff; border: 1px solid #d5dbe0; }"
  991. "QTabWidget::pane { border: 0; background: #f3f5f7; }"
  992. "QTabBar::tab { background: #e5e9ec; padding: 7px 14px; border-right: 1px solid #cbd2d9; }"
  993. "QTabBar::tab:selected { background: #ffffff; color: #15232d; }"
  994. "QFrame#hmiCanvasPlaceholder, QFrame#logicCanvasPlaceholder { background: #ffffff;"
  995. " border: 1px solid #cbd2d9; }"
  996. "QLabel#hmiEmptyLabel, QLabel#logicEmptyLabel { color: #75838d; }"
  997. "QLabel#hmiPageTitleLabel { color: #1d2a33; font-weight: 600; }"
  998. "QLabel#hmiPageSizeLabel { color: #66747e; }"));
  999. }
  1000. void MainWindow::configureHmiEditor()
  1001. {
  1002. QLayout *layout = ui_->hmiCanvasPlaceholder->layout();
  1003. delete ui_->hmiEmptyLabel;
  1004. hmi_editor_widget_ = new HmiEditorWidget(
  1005. hmi_editor_service_,
  1006. hmi_runtime_service_,
  1007. alarm_service_,
  1008. ui_->hmiCanvasPlaceholder);
  1009. hmi_editor_widget_->setMinimumHeight(320);
  1010. layout->addWidget(hmi_editor_widget_);
  1011. }
  1012. void MainWindow::configureLogicEditor()
  1013. {
  1014. QLayout *layout = ui_->logicCanvasPlaceholder->layout();
  1015. delete ui_->logicEmptyLabel;
  1016. logic_editor_widget_ = new LogicEditorWidget(
  1017. logic_editor_service_, ui_->logicCanvasPlaceholder);
  1018. logic_editor_widget_->setObjectName(QStringLiteral("logicEditorWidget"));
  1019. logic_editor_widget_->setMinimumHeight(320);
  1020. layout->addWidget(logic_editor_widget_);
  1021. property_panel_controller_->bindEditorWidgets(
  1022. *hmi_editor_widget_, *logic_editor_widget_);
  1023. connect(
  1024. hmi_editor_widget_, &HmiEditorWidget::controlSelected,
  1025. this, [this](const QString &) { updateEditActions(); });
  1026. connect(
  1027. logic_editor_widget_, &LogicEditorWidget::nodeSelected,
  1028. this, [this](const QString &) { updateEditActions(); });
  1029. }
  1030. void MainWindow::configureRuntimeMonitor()
  1031. {
  1032. runtime_panel_controller_ = std::make_unique<RuntimePanelController>(
  1033. *this,
  1034. runtime_mode_service_,
  1035. project_service_,
  1036. hmi_editor_service_,
  1037. hmi_runtime_service_,
  1038. logic_editor_service_,
  1039. *hmi_navigation_service_,
  1040. alarm_service_,
  1041. register_monitor_service_,
  1042. *hmi_editor_widget_,
  1043. *logic_editor_widget_,
  1044. *executor_status_label_,
  1045. [this] { return current_logic_id_; },
  1046. [this](const std::string &logic_id) { current_logic_id_ = logic_id; },
  1047. [this](const std::string &node_id)
  1048. {
  1049. showLogicNodeProperties(node_id);
  1050. },
  1051. [this](const QString &message, int timeout_ms)
  1052. {
  1053. statusBar()->showMessage(message, timeout_ms);
  1054. },
  1055. [this](const QString &message)
  1056. {
  1057. appendOutputMessage(message);
  1058. },
  1059. [this]
  1060. {
  1061. if (user_runtime_mode_)
  1062. {
  1063. qApp->quit();
  1064. }
  1065. else
  1066. {
  1067. requestMode(ApplicationMode::Editing);
  1068. }
  1069. });
  1070. runtime_panel_controller_->configure();
  1071. runtime_monitor_widget_ = runtime_panel_controller_->runtimeMonitorWidget();
  1072. }
  1073. void MainWindow::configureDataMonitor()
  1074. {
  1075. data_monitor_widget_ = new FreeMonitorWidget(
  1076. register_monitor_service_, ui_->dataMonitorTab);
  1077. data_monitor_widget_->setObjectName(QStringLiteral("dataMonitorWidget"));
  1078. ui_->dataMonitorLayout->addWidget(data_monitor_widget_);
  1079. connect(data_monitor_widget_, &FreeMonitorWidget::monitorAddressesChanged,
  1080. this,
  1081. [this]
  1082. {
  1083. runtime_mode_service_.setMonitorAddresses(
  1084. register_monitor_service_.pollAddresses(),
  1085. register_monitor_service_.multiWordRanges());
  1086. refreshDataMonitorUi();
  1087. });
  1088. connect(data_monitor_widget_, &FreeMonitorWidget::operationMessage,
  1089. this,
  1090. [this](const QString &message)
  1091. {
  1092. statusBar()->showMessage(message, 4000);
  1093. appendOutputMessage(message);
  1094. });
  1095. register_monitor_service_.setAddressesChangedCallback(
  1096. [this]
  1097. {
  1098. if (data_monitor_widget_ != nullptr)
  1099. {
  1100. data_monitor_widget_->reloadAddresses();
  1101. }
  1102. if (runtime_monitor_widget_ != nullptr
  1103. && runtime_monitor_widget_->freeMonitorWidget() != nullptr)
  1104. {
  1105. runtime_monitor_widget_->freeMonitorWidget()->reloadAddresses();
  1106. }
  1107. runtime_mode_service_.setMonitorAddresses(
  1108. register_monitor_service_.pollAddresses(),
  1109. register_monitor_service_.multiWordRanges());
  1110. });
  1111. register_monitor_service_.setPollConfigurationValidator(
  1112. [this](const std::vector<RegisterAddress> &addresses,
  1113. const std::vector<RegisterWordRange> &ranges)
  1114. {
  1115. const PlcCommunicationResult result =
  1116. runtime_mode_service_.setMonitorAddresses(addresses, ranges);
  1117. return result.succeeded ? std::string{} : result.message;
  1118. });
  1119. data_monitor_refresh_timer_ = new QTimer(this);
  1120. data_monitor_refresh_timer_->setInterval(150);
  1121. connect(data_monitor_refresh_timer_, &QTimer::timeout,
  1122. this, &MainWindow::refreshDataMonitorUi);
  1123. data_monitor_refresh_timer_->start();
  1124. refreshDataMonitorUi();
  1125. }
  1126. void MainWindow::refreshDataMonitorUi()
  1127. {
  1128. if (data_monitor_widget_ == nullptr)
  1129. {
  1130. return;
  1131. }
  1132. const ApplicationMode mode = runtime_mode_service_.mode();
  1133. const bool online_connected = mode == ApplicationMode::OnlineRunning
  1134. && runtime_mode_service_.plcConnectionState()
  1135. == PlcConnectionState::Connected;
  1136. data_monitor_widget_->setWriteEnabled(
  1137. mode == ApplicationMode::Editing
  1138. || mode == ApplicationMode::OfflineRunning
  1139. || online_connected);
  1140. data_monitor_widget_->refreshValues(
  1141. mode, runtime_mode_service_.plcConnectionState());
  1142. }
  1143. void MainWindow::configureProjectTree()
  1144. {
  1145. project_workspace_controller_ = std::make_unique<ProjectWorkspaceController>(
  1146. *this,
  1147. *ui_,
  1148. project_service_,
  1149. hmi_editor_service_,
  1150. logic_editor_service_,
  1151. runtime_mode_service_,
  1152. *hmi_navigation_service_,
  1153. *hmi_editor_widget_,
  1154. *logic_editor_widget_,
  1155. *runtime_monitor_widget_,
  1156. current_hmi_page_id_,
  1157. current_logic_id_,
  1158. [this](const std::string &control_id)
  1159. {
  1160. showControlProperties(control_id);
  1161. },
  1162. [this](const std::string &node_id)
  1163. {
  1164. showLogicNodeProperties(node_id);
  1165. },
  1166. [this](const QString &action, const QString &message, bool succeeded)
  1167. {
  1168. showProjectResult(action, message, succeeded);
  1169. },
  1170. [this](const QString &message, int timeout_ms)
  1171. {
  1172. statusBar()->showMessage(message, timeout_ms);
  1173. },
  1174. [this]
  1175. {
  1176. updateEditActions();
  1177. });
  1178. project_workspace_controller_->configure();
  1179. }
  1180. void MainWindow::configurePropertyEditor()
  1181. {
  1182. property_panel_controller_->configure();
  1183. }
  1184. void MainWindow::configurePlcConnection()
  1185. {
  1186. connect(ui_->configurePlcAction, &QAction::triggered, this, &MainWindow::connectPlc);
  1187. connect(ui_->disconnectPlcAction, &QAction::triggered, this, &MainWindow::disconnectPlc);
  1188. }
  1189. void MainWindow::configureAlarms()
  1190. {
  1191. connect(ui_->configureAlarmsAction, &QAction::triggered,
  1192. this,
  1193. [this]
  1194. {
  1195. AlarmConfigurationDialog dialog(alarm_editor_service_, this);
  1196. dialog.exec();
  1197. refreshProjectUi();
  1198. });
  1199. }
  1200. void MainWindow::configureRegisterComments()
  1201. {
  1202. connect(
  1203. ui_->configureRegisterCommentsAction,
  1204. &QAction::triggered,
  1205. this,
  1206. [this]
  1207. {
  1208. RegisterCommentDialog dialog(register_comment_service_, this);
  1209. dialog.exec();
  1210. logic_editor_widget_->reloadLogic();
  1211. refreshProjectUi();
  1212. });
  1213. }
  1214. void MainWindow::undoActiveEditor()
  1215. {
  1216. if (!runtime_mode_service_.policy().allowsProjectEditing)
  1217. {
  1218. return;
  1219. }
  1220. if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab)
  1221. {
  1222. if (!hmi_editor_service_.undo().succeeded)
  1223. {
  1224. return;
  1225. }
  1226. selected_control_id_.clear();
  1227. showControlProperties({});
  1228. refreshProjectUi();
  1229. hmi_editor_widget_->reloadPage();
  1230. statusBar()->showMessage(tr("已撤销 HMI 编辑操作"), 3000);
  1231. }
  1232. else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab)
  1233. {
  1234. if (!logic_editor_service_.undo().succeeded)
  1235. {
  1236. return;
  1237. }
  1238. selected_logic_node_id_.clear();
  1239. showLogicNodeProperties({});
  1240. refreshProjectUi();
  1241. logic_editor_widget_->reloadLogic();
  1242. statusBar()->showMessage(tr("已撤销梯形图编辑操作"), 3000);
  1243. }
  1244. updateEditActions();
  1245. }
  1246. void MainWindow::redoActiveEditor()
  1247. {
  1248. if (!runtime_mode_service_.policy().allowsProjectEditing)
  1249. {
  1250. return;
  1251. }
  1252. if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab)
  1253. {
  1254. if (!hmi_editor_service_.redo().succeeded)
  1255. {
  1256. return;
  1257. }
  1258. selected_control_id_.clear();
  1259. showControlProperties({});
  1260. refreshProjectUi();
  1261. hmi_editor_widget_->reloadPage();
  1262. statusBar()->showMessage(tr("已重做 HMI 编辑操作"), 3000);
  1263. }
  1264. else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab)
  1265. {
  1266. if (!logic_editor_service_.redo().succeeded)
  1267. {
  1268. return;
  1269. }
  1270. selected_logic_node_id_.clear();
  1271. showLogicNodeProperties({});
  1272. refreshProjectUi();
  1273. logic_editor_widget_->reloadLogic();
  1274. statusBar()->showMessage(tr("已重做梯形图编辑操作"), 3000);
  1275. }
  1276. updateEditActions();
  1277. }
  1278. void MainWindow::copyActiveSelection()
  1279. {
  1280. if (!runtime_mode_service_.policy().allowsProjectEditing
  1281. || isTextEditingObject(qApp->focusWidget()))
  1282. {
  1283. return;
  1284. }
  1285. if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab)
  1286. {
  1287. const std::vector<std::string> ids = hmi_editor_widget_->selectedControlIds();
  1288. std::vector<HmiControl> controls;
  1289. controls.reserve(ids.size());
  1290. for (const std::string &id : ids)
  1291. {
  1292. const HmiControl *control = hmi_editor_service_.findControl(
  1293. current_hmi_page_id_, id);
  1294. if (control != nullptr)
  1295. {
  1296. controls.push_back(*control);
  1297. }
  1298. }
  1299. if (controls.empty())
  1300. {
  1301. return;
  1302. }
  1303. editor_clipboard_ = HmiClipboardData{std::move(controls), 0};
  1304. statusBar()->showMessage(tr("已复制 %1 个 HMI 控件").arg(ids.size()), 3000);
  1305. }
  1306. else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab)
  1307. {
  1308. LogicClipboardCopyResult result = logic_editor_widget_->copySelection();
  1309. if (!result.copy.succeeded)
  1310. {
  1311. statusBar()->showMessage(fromUtf8(result.copy.message), 5000);
  1312. return;
  1313. }
  1314. const bool whole_rows =
  1315. result.fragment.mode == LogicClipboardMode::WholeRows;
  1316. const std::size_t object_count = result.fragment.cells.size()
  1317. + result.fragment.outputs.size()
  1318. + result.fragment.verticalConnections.size();
  1319. editor_clipboard_ = LogicClipboardData{std::move(result.fragment)};
  1320. if (whole_rows)
  1321. {
  1322. const auto *data = std::get_if<LogicClipboardData>(&editor_clipboard_);
  1323. statusBar()->showMessage(
  1324. tr("已复制 %1 行梯形图").arg(data->fragment.rows.size()),
  1325. 3000);
  1326. }
  1327. else
  1328. {
  1329. statusBar()->showMessage(
  1330. tr("已复制 %1 个梯形图对象").arg(object_count), 3000);
  1331. }
  1332. }
  1333. updateEditActions();
  1334. }
  1335. void MainWindow::pasteActiveSelection()
  1336. {
  1337. if (!runtime_mode_service_.policy().allowsProjectEditing
  1338. || isTextEditingObject(qApp->focusWidget()))
  1339. {
  1340. return;
  1341. }
  1342. if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab)
  1343. {
  1344. auto *data = std::get_if<HmiClipboardData>(&editor_clipboard_);
  1345. if (data == nullptr)
  1346. {
  1347. return;
  1348. }
  1349. const int offset = 20 * std::min(data->pasteCount + 1, 10);
  1350. const HmiEditorResult result = hmi_editor_service_.pasteControls(
  1351. current_hmi_page_id_, data->controls, offset, offset);
  1352. if (!result.succeeded)
  1353. {
  1354. showProjectResult(tr("粘贴 HMI 控件"), fromUtf8(result.message), false);
  1355. return;
  1356. }
  1357. ++data->pasteCount;
  1358. refreshProjectUi();
  1359. hmi_editor_widget_->reloadPage();
  1360. hmi_editor_widget_->selectControl(result.id);
  1361. showControlProperties(result.id);
  1362. statusBar()->showMessage(tr("已粘贴 HMI 控件"), 3000);
  1363. }
  1364. else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab)
  1365. {
  1366. const auto *data = std::get_if<LogicClipboardData>(&editor_clipboard_);
  1367. if (data == nullptr)
  1368. {
  1369. return;
  1370. }
  1371. const LogicClipboardPasteResult result =
  1372. logic_editor_widget_->pasteClipboard(data->fragment);
  1373. if (!result.edit.succeeded)
  1374. {
  1375. if (!result.edit.message.empty())
  1376. {
  1377. showProjectResult(
  1378. tr("粘贴梯形图"), fromUtf8(result.edit.message), false);
  1379. }
  1380. return;
  1381. }
  1382. selected_logic_node_id_ = logic_editor_widget_->selectedNodeId();
  1383. showLogicNodeProperties(selected_logic_node_id_);
  1384. refreshProjectUi();
  1385. statusBar()->showMessage(tr("已粘贴梯形图对象"), 3000);
  1386. }
  1387. updateEditActions();
  1388. }
  1389. void MainWindow::deleteActiveSelection()
  1390. {
  1391. if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab)
  1392. {
  1393. deleteSelectedControl();
  1394. }
  1395. else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab)
  1396. {
  1397. deleteSelectedLogicObject();
  1398. }
  1399. }
  1400. void MainWindow::clearActiveSelection()
  1401. {
  1402. if (ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab)
  1403. {
  1404. hmi_editor_widget_->scene()->clearSelection();
  1405. selected_control_id_.clear();
  1406. showControlProperties({});
  1407. }
  1408. else if (ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab)
  1409. {
  1410. logic_editor_widget_->clearSelection();
  1411. selected_logic_node_id_.clear();
  1412. showLogicNodeProperties({});
  1413. if (logic_editor_widget_->mouseWireMode()
  1414. != LogicEditorWidget::MouseWireMode::Select)
  1415. {
  1416. const QSignalBlocker draw_blocker(ui_->mouseDrawWireAction);
  1417. const QSignalBlocker erase_blocker(ui_->mouseEraseWireAction);
  1418. ui_->mouseDrawWireAction->setChecked(false);
  1419. ui_->mouseEraseWireAction->setChecked(false);
  1420. logic_editor_widget_->setMouseWireMode(
  1421. LogicEditorWidget::MouseWireMode::Select);
  1422. }
  1423. }
  1424. }
  1425. void MainWindow::updateEditActions()
  1426. {
  1427. const bool editable = runtime_mode_service_.policy().allowsProjectEditing;
  1428. const bool hmi_active = ui_->editorTabWidget->currentWidget() == ui_->hmiEditorTab;
  1429. const bool logic_active =
  1430. ui_->editorTabWidget->currentWidget() == ui_->logicEditorTab;
  1431. ui_->undoAction->setEnabled(
  1432. editable && ((hmi_active && hmi_editor_service_.canUndo())
  1433. || (logic_active && logic_editor_service_.canUndo())));
  1434. ui_->redoAction->setEnabled(
  1435. editable && ((hmi_active && hmi_editor_service_.canRedo())
  1436. || (logic_active && logic_editor_service_.canRedo())));
  1437. const bool hmi_selection = hmi_active
  1438. && !hmi_editor_widget_->selectedControlIds().empty();
  1439. const bool logic_selection = logic_active
  1440. && logic_editor_widget_->hasCopyableSelection();
  1441. ui_->copyAction->setEnabled(
  1442. editable && (hmi_selection || logic_selection));
  1443. const bool hmi_clipboard = std::holds_alternative<HmiClipboardData>(editor_clipboard_);
  1444. const bool logic_clipboard =
  1445. std::holds_alternative<LogicClipboardData>(editor_clipboard_);
  1446. ui_->pasteAction->setEnabled(editable && ((hmi_active && hmi_clipboard)
  1447. || (logic_active && logic_clipboard)));
  1448. ui_->deleteSelectionAction->setEnabled(editable && (hmi_active || logic_active));
  1449. ui_->clearSelectionAction->setEnabled(editable && (hmi_active || logic_active));
  1450. ui_->insertHorizontalWireAction->setEnabled(editable && logic_active);
  1451. ui_->insertVerticalWireAction->setEnabled(editable && logic_active);
  1452. ui_->insertRungAboveAction->setEnabled(editable && logic_active);
  1453. ui_->insertRungBelowAction->setEnabled(editable && logic_active);
  1454. ui_->deleteRungAction->setEnabled(editable && logic_active);
  1455. ui_->deleteHorizontalWireAction->setEnabled(editable && logic_active);
  1456. ui_->deleteVerticalWireAction->setEnabled(editable && logic_active);
  1457. ui_->mouseDrawWireAction->setEnabled(editable && logic_active);
  1458. ui_->mouseEraseWireAction->setEnabled(editable && logic_active);
  1459. ui_->syntaxCheckAction->setEnabled(
  1460. editable && !current_logic_id_.empty());
  1461. ui_->doubleCoilCheckAction->setEnabled(
  1462. editable && !current_logic_id_.empty());
  1463. }
  1464. void MainWindow::clearEditorHistories()
  1465. {
  1466. hmi_editor_service_.clearHistory();
  1467. logic_editor_service_.clearHistory();
  1468. if (ui_->undoAction != nullptr)
  1469. {
  1470. updateEditActions();
  1471. }
  1472. }
  1473. void MainWindow::refreshProjectUi()
  1474. {
  1475. project_workspace_controller_->refresh();
  1476. updateEditActions();
  1477. updateWindowTitle();
  1478. }
  1479. void MainWindow::updateWindowTitle()
  1480. {
  1481. QString title = tr("综合平台编程器");
  1482. if (project_service_.isModified())
  1483. {
  1484. title += QStringLiteral("*");
  1485. }
  1486. setWindowTitle(title);
  1487. }
  1488. bool MainWindow::confirmSaveBeforeDestructiveAction()
  1489. {
  1490. if (!project_service_.isModified())
  1491. {
  1492. return true;
  1493. }
  1494. const QString project_name = fromUtf8(project_service_.project().metadata.name);
  1495. const QMessageBox::StandardButton choice = QMessageBox::warning(
  1496. this,
  1497. tr("工程尚未保存"),
  1498. tr("工程“%1”有未保存的修改,是否先保存?").arg(project_name),
  1499. QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
  1500. QMessageBox::Save);
  1501. if (choice == QMessageBox::Cancel)
  1502. {
  1503. return false;
  1504. }
  1505. if (choice == QMessageBox::Discard)
  1506. {
  1507. return true;
  1508. }
  1509. saveProject();
  1510. return !project_service_.isModified();
  1511. }
  1512. void MainWindow::updateProjectTreeActions()
  1513. {
  1514. project_workspace_controller_->updateActions();
  1515. }
  1516. void MainWindow::connectPlc()
  1517. {
  1518. PlcConnectionDialog dialog(
  1519. plc_configuration_,
  1520. plc_discovery_gateway_,
  1521. [this] { runtime_mode_service_.disconnectPlc(); },
  1522. this);
  1523. if (dialog.exec() != QDialog::Accepted)
  1524. {
  1525. return;
  1526. }
  1527. plc_configuration_ = dialog.configuration();
  1528. if (plc_configuration_.portName.empty())
  1529. {
  1530. showProjectResult(tr("连接 PLC"), tr("请选择或输入串口端口"), false);
  1531. return;
  1532. }
  1533. const PlcCommunicationResult result = runtime_mode_service_.connectPlc(
  1534. plc_configuration_);
  1535. if (!result.succeeded)
  1536. {
  1537. const QString message = fromUtf8(result.message);
  1538. if (result.message == runtime_mode_service_.plcError())
  1539. {
  1540. statusBar()->showMessage(message, 5000);
  1541. QMessageBox::warning(this, tr("连接 PLC"), message);
  1542. }
  1543. else
  1544. {
  1545. showProjectResult(tr("连接 PLC"), message, false);
  1546. }
  1547. return;
  1548. }
  1549. statusBar()->showMessage(tr("PLC 正在连接,等待首次 M/D 读取"), 5000);
  1550. }
  1551. void MainWindow::disconnectPlc()
  1552. {
  1553. runtime_mode_service_.disconnectPlc();
  1554. }
  1555. void MainWindow::schedulePlcStatusUpdate()
  1556. {
  1557. if (plc_status_update_pending_)
  1558. {
  1559. return;
  1560. }
  1561. plc_status_update_pending_ = true;
  1562. QMetaObject::invokeMethod(
  1563. this,
  1564. [this]
  1565. {
  1566. plc_status_update_pending_ = false;
  1567. updateModeUi(plcStatusText(runtime_mode_service_));
  1568. },
  1569. Qt::QueuedConnection);
  1570. }
  1571. // 根据控件ID加载控件属性到右侧属性面板
  1572. void MainWindow::showControlProperties(const std::string &control_id)
  1573. {
  1574. property_panel_controller_->showControlProperties(control_id);
  1575. }
  1576. void MainWindow::showLogicNodeProperties(const std::string &node_id)
  1577. {
  1578. property_panel_controller_->showLogicNodeProperties(node_id);
  1579. }
  1580. void MainWindow::addHmiControl(HmiControlType type)
  1581. {
  1582. property_panel_controller_->addHmiControl(type);
  1583. }
  1584. void MainWindow::deleteSelectedControl()
  1585. {
  1586. property_panel_controller_->deleteSelectedControl();
  1587. }
  1588. void MainWindow::addLogicCondition(const LogicNodeConfig &config)
  1589. {
  1590. const LogicEditorResult result = logic_editor_widget_->addCondition(config);
  1591. if (!result.succeeded)
  1592. {
  1593. showProjectResult(tr("添加逻辑条件"), fromUtf8(result.message), false);
  1594. return;
  1595. }
  1596. selected_logic_node_id_ = result.id;
  1597. showLogicNodeProperties(result.id);
  1598. refreshProjectUi();
  1599. statusBar()->showMessage(tr("已添加逻辑条件"), 3000);
  1600. }
  1601. void MainWindow::addLogicParallelBranch(const LogicNodeConfig &config)
  1602. {
  1603. const LogicEditorResult result = logic_editor_widget_->addParallelBranch(config);
  1604. if (!result.succeeded)
  1605. {
  1606. showProjectResult(tr("建立并联支路"), fromUtf8(result.message), false);
  1607. return;
  1608. }
  1609. selected_logic_node_id_ = result.id;
  1610. showLogicNodeProperties(result.id);
  1611. refreshProjectUi();
  1612. statusBar()->showMessage(tr("已建立并联支路,请配置新触点"), 3000);
  1613. }
  1614. void MainWindow::addLogicHorizontalWire()
  1615. {
  1616. const LogicEditorResult result = logic_editor_widget_->addHorizontalWire();
  1617. if (!result.succeeded)
  1618. {
  1619. showProjectResult(tr("插入横线"), fromUtf8(result.message), false);
  1620. return;
  1621. }
  1622. selected_logic_node_id_.clear();
  1623. showLogicNodeProperties({});
  1624. refreshProjectUi();
  1625. statusBar()->showMessage(tr("横线已插入,可直接用触点替换"), 3000);
  1626. }
  1627. void MainWindow::addLogicVerticalWire()
  1628. {
  1629. const LogicEditorResult result = logic_editor_widget_->addVerticalWire();
  1630. if (!result.succeeded)
  1631. {
  1632. showProjectResult(tr("插入竖线"), fromUtf8(result.message), false);
  1633. return;
  1634. }
  1635. selected_logic_node_id_.clear();
  1636. showLogicNodeProperties({});
  1637. refreshProjectUi();
  1638. statusBar()->showMessage(
  1639. result.message.empty()
  1640. ? tr("竖线连接已建立") : fromUtf8(result.message),
  1641. 3000);
  1642. }
  1643. void MainWindow::deleteLogicHorizontalWire()
  1644. {
  1645. const LogicEditorResult result = logic_editor_widget_->deleteHorizontalWire();
  1646. if (!result.succeeded)
  1647. {
  1648. showProjectResult(tr("删除横线"), fromUtf8(result.message), false);
  1649. return;
  1650. }
  1651. selected_logic_node_id_.clear();
  1652. showLogicNodeProperties({});
  1653. refreshProjectUi();
  1654. statusBar()->showMessage(tr("横线已删除"), 3000);
  1655. }
  1656. void MainWindow::deleteLogicVerticalWire()
  1657. {
  1658. const LogicEditorResult result = logic_editor_widget_->deleteVerticalWire();
  1659. if (!result.succeeded)
  1660. {
  1661. showProjectResult(tr("删除竖线"), fromUtf8(result.message), false);
  1662. return;
  1663. }
  1664. selected_logic_node_id_.clear();
  1665. showLogicNodeProperties({});
  1666. refreshProjectUi();
  1667. statusBar()->showMessage(tr("竖线已删除,网络已拆分"), 3000);
  1668. }
  1669. void MainWindow::setLogicOutput(const LogicNodeConfig &config)
  1670. {
  1671. const LogicEditorResult result = logic_editor_widget_->setOutput(config);
  1672. if (!result.succeeded)
  1673. {
  1674. showProjectResult(tr("设置逻辑输出"), fromUtf8(result.message), false);
  1675. return;
  1676. }
  1677. selected_logic_node_id_ = result.id;
  1678. showLogicNodeProperties(result.id);
  1679. refreshProjectUi();
  1680. statusBar()->showMessage(tr("逻辑输出已设置"), 3000);
  1681. }
  1682. void MainWindow::configureAndSetLogicOutput(const LogicNodeConfig &config)
  1683. {
  1684. LogicInstructionDialog dialog(config, this);
  1685. if (dialog.exec() != QDialog::Accepted)
  1686. {
  1687. return;
  1688. }
  1689. const LogicNodeConfig configured = dialog.config();
  1690. LogicEditorResult result = logic_editor_widget_->setOutput(configured, true);
  1691. if (!result.succeeded)
  1692. {
  1693. showProjectResult(tr("设置逻辑输出"), fromUtf8(result.message), false);
  1694. return;
  1695. }
  1696. const std::string node_id = result.id;
  1697. selected_logic_node_id_ = node_id;
  1698. showLogicNodeProperties(node_id);
  1699. refreshProjectUi();
  1700. statusBar()->showMessage(tr("逻辑输出已配置"), 3000);
  1701. }
  1702. void MainWindow::addLogicRung()
  1703. {
  1704. const LogicEditorResult result = logic_editor_widget_->addRung();
  1705. if (!result.succeeded)
  1706. {
  1707. showProjectResult(tr("新建网络"), fromUtf8(result.message), false);
  1708. return;
  1709. }
  1710. selected_logic_node_id_.clear();
  1711. showLogicNodeProperties({});
  1712. refreshProjectUi();
  1713. statusBar()->showMessage(tr("已新建网络"), 3000);
  1714. }
  1715. void MainWindow::insertLogicRungAbove()
  1716. {
  1717. const LogicEditorResult result = logic_editor_widget_->insertRung(false);
  1718. if (!result.succeeded)
  1719. {
  1720. showProjectResult(tr("上方插入行"), fromUtf8(result.message), false);
  1721. return;
  1722. }
  1723. selected_logic_node_id_.clear();
  1724. showLogicNodeProperties({});
  1725. refreshProjectUi();
  1726. statusBar()->showMessage(tr("已在当前行上方插入空白行"), 3000);
  1727. }
  1728. void MainWindow::insertLogicRungBelow()
  1729. {
  1730. const LogicEditorResult result = logic_editor_widget_->insertRung(true);
  1731. if (!result.succeeded)
  1732. {
  1733. showProjectResult(tr("下方插入行"), fromUtf8(result.message), false);
  1734. return;
  1735. }
  1736. selected_logic_node_id_.clear();
  1737. showLogicNodeProperties({});
  1738. refreshProjectUi();
  1739. statusBar()->showMessage(tr("已在当前行下方插入空白行"), 3000);
  1740. }
  1741. void MainWindow::deleteLogicRung()
  1742. {
  1743. const LogicEditorResult result = logic_editor_widget_->deleteRung();
  1744. if (!result.succeeded)
  1745. {
  1746. showProjectResult(tr("删除行"), fromUtf8(result.message), false);
  1747. return;
  1748. }
  1749. selected_logic_node_id_.clear();
  1750. showLogicNodeProperties({});
  1751. refreshProjectUi();
  1752. statusBar()->showMessage(tr("当前行已删除,竖线连接已重新整理"), 3000);
  1753. }
  1754. void MainWindow::editSelectedNetworkComment()
  1755. {
  1756. const std::string rung_id = logic_editor_widget_->selectedRungId();
  1757. if (rung_id.empty())
  1758. {
  1759. showProjectResult(tr("网络注释"), tr("请先选择一个梯形图网络"), false);
  1760. return;
  1761. }
  1762. const LadderRung *head = logic_editor_service_.findNetworkHeadRung(
  1763. current_logic_id_, rung_id);
  1764. if (head == nullptr)
  1765. {
  1766. return;
  1767. }
  1768. bool accepted = false;
  1769. const QString comment = QInputDialog::getText(
  1770. this,
  1771. tr("网络注释"),
  1772. tr("说明"),
  1773. QLineEdit::Normal,
  1774. fromUtf8(head->comment),
  1775. &accepted);
  1776. if (!accepted)
  1777. {
  1778. return;
  1779. }
  1780. const LogicEditorResult result = logic_editor_service_.updateNetworkComment(
  1781. current_logic_id_, rung_id, toUtf8(comment));
  1782. if (!result.succeeded)
  1783. {
  1784. showProjectResult(tr("网络注释"), fromUtf8(result.message), false);
  1785. return;
  1786. }
  1787. logic_editor_widget_->reloadLogic();
  1788. refreshProjectUi();
  1789. statusBar()->showMessage(tr("网络注释已更新"), 3000);
  1790. }
  1791. void MainWindow::runLogicSyntaxCheck()
  1792. {
  1793. const LogicSyntaxCheckResult result = logic_editor_service_.checkSyntax(
  1794. current_logic_id_);
  1795. reportLogicSyntaxCheck(result, tr("语法检查"));
  1796. }
  1797. void MainWindow::runDoubleCoilCheck()
  1798. {
  1799. const LogicSyntaxCheckResult result =
  1800. logic_editor_service_.checkDoubleCoils(current_logic_id_);
  1801. reportLogicSyntaxCheck(result, tr("双线圈检查"));
  1802. }
  1803. void MainWindow::reportLogicSyntaxCheck(
  1804. const LogicSyntaxCheckResult &result, const QString &action)
  1805. {
  1806. if (result.changed)
  1807. {
  1808. logic_editor_widget_->reloadLogic();
  1809. refreshProjectUi();
  1810. }
  1811. QString message = action + QStringLiteral(": ") + fromUtf8(result.message);
  1812. if (result.removedWireCells > 0U
  1813. || result.removedVerticalConnections > 0U)
  1814. {
  1815. message += tr(";已规整 %1 格横线、%2 段竖线")
  1816. .arg(result.removedWireCells)
  1817. .arg(result.removedVerticalConnections);
  1818. }
  1819. appendOutputMessage(message, result.location);
  1820. ui_->outputDock->show();
  1821. ui_->outputDock->raise();
  1822. statusBar()->showMessage(message, 6000);
  1823. if (!result.valid && result.location.has_value())
  1824. {
  1825. focusLogicSyntaxLocation(*result.location);
  1826. }
  1827. updateEditActions();
  1828. }
  1829. void MainWindow::focusLogicSyntaxLocation(
  1830. const LogicSyntaxLocation &location)
  1831. {
  1832. if (logic_editor_service_.findRung(
  1833. location.logicId, location.rungId) == nullptr)
  1834. {
  1835. return;
  1836. }
  1837. current_logic_id_ = location.logicId;
  1838. ui_->editorTabWidget->setCurrentWidget(ui_->logicEditorTab);
  1839. refreshProjectUi();
  1840. logic_editor_widget_->focusSyntaxLocation(
  1841. location.rungId, location.column);
  1842. }
  1843. void MainWindow::deleteSelectedLogicObject()
  1844. {
  1845. const LogicEditorResult result = logic_editor_widget_->deleteSelected();
  1846. if (!result.succeeded)
  1847. {
  1848. return;
  1849. }
  1850. selected_logic_node_id_.clear();
  1851. showLogicNodeProperties({});
  1852. refreshProjectUi();
  1853. }
  1854. void MainWindow::createNewProject()
  1855. {
  1856. if (!confirmSaveBeforeDestructiveAction())
  1857. {
  1858. return;
  1859. }
  1860. QInputDialog name_dialog(this);
  1861. name_dialog.setWindowTitle(tr("新建工程"));
  1862. name_dialog.setLabelText(tr("工程名称"));
  1863. name_dialog.setInputMode(QInputDialog::TextInput);
  1864. QDialogButtonBox *button_box = name_dialog.findChild<QDialogButtonBox *>();
  1865. QPushButton *ok_button = button_box != nullptr
  1866. ? button_box->button(QDialogButtonBox::Ok)
  1867. : nullptr;
  1868. if (ok_button != nullptr)
  1869. {
  1870. ok_button->setEnabled(false);
  1871. connect(&name_dialog,
  1872. &QInputDialog::textValueChanged,
  1873. &name_dialog,
  1874. [ok_button](const QString &text) {
  1875. ok_button->setEnabled(!text.trimmed().isEmpty());
  1876. });
  1877. }
  1878. if (name_dialog.exec() != QDialog::Accepted)
  1879. {
  1880. return;
  1881. }
  1882. const QString name = name_dialog.textValue().trimmed();
  1883. const ProjectOperationResult result = project_service_.createNewProject(toUtf8(name));
  1884. if (!result.succeeded)
  1885. {
  1886. showProjectResult(tr("新建工程"), fromUtf8(result.message), false);
  1887. return;
  1888. }
  1889. hmi_editor_service_.ensureDefaultPage();
  1890. logic_editor_service_.ensureDefaultLogic();
  1891. clearEditorHistories();
  1892. selected_control_id_.clear();
  1893. selected_logic_node_id_.clear();
  1894. editor_clipboard_ = std::monostate{};
  1895. current_hmi_page_id_ = project_service_.project().initialHmiPageId;
  1896. current_logic_id_ = logic_editor_service_.firstLogicId();
  1897. refreshProjectUi();
  1898. hmi_editor_widget_->reloadPage();
  1899. logic_editor_widget_->reloadLogic();
  1900. showControlProperties({});
  1901. statusBar()->showMessage(tr("已创建新工程"), 3000);
  1902. }
  1903. void MainWindow::saveProject()
  1904. {
  1905. if (!project_service_.hasCurrentFile())
  1906. {
  1907. saveProjectAs();
  1908. return;
  1909. }
  1910. const ProjectOperationResult result = project_service_.save();
  1911. if (!result.succeeded)
  1912. {
  1913. showProjectResult(tr("保存工程"), fromUtf8(result.message), false);
  1914. return;
  1915. }
  1916. updateWindowTitle();
  1917. showProjectResult(tr("保存工程"), tr("工程已保存"), true);
  1918. }
  1919. void MainWindow::saveProjectAs()
  1920. {
  1921. const QString suggested_file_name = suggestedProjectFileName(
  1922. fromUtf8(project_service_.project().metadata.name));
  1923. const QString path = QFileDialog::getSaveFileName(
  1924. this,
  1925. tr("工程另存为"),
  1926. suggested_file_name,
  1927. tr("工程文件 (*.json)"));
  1928. if (path.isEmpty())
  1929. {
  1930. return;
  1931. }
  1932. const ProjectOperationResult result = project_service_.saveAs(toUtf8(path));
  1933. if (result.succeeded)
  1934. {
  1935. updateWindowTitle();
  1936. }
  1937. showProjectResult(
  1938. tr("保存工程"),
  1939. result.succeeded ? tr("工程已保存") : fromUtf8(result.message),
  1940. result.succeeded);
  1941. }
  1942. void MainWindow::loadProject()
  1943. {
  1944. if (!confirmSaveBeforeDestructiveAction())
  1945. {
  1946. return;
  1947. }
  1948. const QString path = QFileDialog::getOpenFileName(
  1949. this, tr("加载工程"), {}, tr("工程文件 (*.json)"));
  1950. if (path.isEmpty())
  1951. {
  1952. return;
  1953. }
  1954. const ProjectOperationResult result = project_service_.load(toUtf8(path));
  1955. if (!result.succeeded)
  1956. {
  1957. showProjectResult(tr("加载工程"), fromUtf8(result.message), false);
  1958. return;
  1959. }
  1960. clearEditorHistories();
  1961. selected_control_id_.clear();
  1962. selected_logic_node_id_.clear();
  1963. editor_clipboard_ = std::monostate{};
  1964. current_hmi_page_id_ = project_service_.project().initialHmiPageId;
  1965. current_logic_id_ = logic_editor_service_.firstLogicId();
  1966. refreshProjectUi();
  1967. hmi_editor_widget_->reloadPage();
  1968. logic_editor_widget_->reloadLogic();
  1969. showControlProperties({});
  1970. showProjectResult(tr("加载工程"), tr("工程已加载"), true);
  1971. }
  1972. void MainWindow::exportRuntimeProgram()
  1973. {
  1974. const LogicSyntaxCheckResult syntax =
  1975. logic_editor_service_.checkEnabledSyntax();
  1976. if (syntax.changed || !syntax.completed || !syntax.valid)
  1977. {
  1978. reportLogicSyntaxCheck(syntax, tr("导出前语法检查"));
  1979. }
  1980. if (!syntax.completed || !syntax.valid)
  1981. {
  1982. return;
  1983. }
  1984. std::string validation_error;
  1985. if (!project_service_.project().validateForRunning(
  1986. project_service_.projectLimits(), &validation_error))
  1987. {
  1988. showProjectResult(
  1989. tr("导出用户运行程序"),
  1990. tr("工程还不能运行:%1").arg(fromUtf8(validation_error)),
  1991. false);
  1992. return;
  1993. }
  1994. QString suggested_name = suggestedProjectFileName(
  1995. fromUtf8(project_service_.project().metadata.name));
  1996. suggested_name.chop(QStringLiteral(".json").size());
  1997. const QString parent_directory = QFileDialog::getExistingDirectory(
  1998. this,
  1999. tr("选择用户运行程序的目标父目录"),
  2000. QDir::homePath(),
  2001. QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
  2002. if (parent_directory.isEmpty())
  2003. {
  2004. return;
  2005. }
  2006. bool name_accepted = false;
  2007. const QString requested_name = QInputDialog::getText(
  2008. this,
  2009. tr("导出用户运行程序"),
  2010. tr("请输入导出文件夹名称"),
  2011. QLineEdit::Normal,
  2012. suggested_name,
  2013. &name_accepted).trimmed();
  2014. if (!name_accepted || requested_name.isEmpty())
  2015. {
  2016. return;
  2017. }
  2018. const QString output_name = suggestedProjectFileName(requested_name).chopped(
  2019. QStringLiteral(".json").size());
  2020. const QString destination_directory = QDir(parent_directory).filePath(output_name);
  2021. if (QFileInfo::exists(destination_directory))
  2022. {
  2023. if (!QFileInfo(destination_directory).isDir())
  2024. {
  2025. showProjectResult(
  2026. tr("导出用户运行程序"),
  2027. tr("导出目标已经存在且不是文件夹:%1").arg(destination_directory),
  2028. false);
  2029. return;
  2030. }
  2031. const QMessageBox::StandardButton answer = QMessageBox::question(
  2032. this,
  2033. tr("覆盖已有导出目录"),
  2034. tr("文件夹“%1”已经存在,是否覆盖?").arg(destination_directory),
  2035. QMessageBox::Yes | QMessageBox::No,
  2036. QMessageBox::No);
  2037. if (answer != QMessageBox::Yes)
  2038. {
  2039. return;
  2040. }
  2041. if (!QDir(destination_directory).removeRecursively())
  2042. {
  2043. showProjectResult(
  2044. tr("导出用户运行程序"),
  2045. tr("无法清理已有导出目录:%1").arg(destination_directory),
  2046. false);
  2047. return;
  2048. }
  2049. }
  2050. const QString temp_project = QDir::tempPath()
  2051. + QStringLiteral("/qtproxinje-runtime-")
  2052. + QString::number(QCoreApplication::applicationPid())
  2053. + QStringLiteral(".json");
  2054. const ProjectOperationResult save_result = project_service_.exportAs(
  2055. temp_project.toUtf8().toStdString());
  2056. if (!save_result.succeeded)
  2057. {
  2058. showProjectResult(tr("导出用户运行程序"), fromUtf8(save_result.message), false);
  2059. return;
  2060. }
  2061. QProgressDialog progress(
  2062. tr("正在准备导出用户运行程序…"),
  2063. QString(),
  2064. 0,
  2065. 100,
  2066. this);
  2067. progress.setWindowTitle(tr("导出用户运行程序"));
  2068. progress.setWindowModality(Qt::WindowModal);
  2069. progress.setAutoClose(false);
  2070. progress.setAutoReset(false);
  2071. progress.setMinimumDuration(0);
  2072. progress.setValue(5);
  2073. progress.show();
  2074. QApplication::processEvents();
  2075. ui_->exportRuntimeAction->setEnabled(false);
  2076. const QString destination_executable = QDir(destination_directory).filePath(
  2077. output_name + QStringLiteral(".exe"));
  2078. const QString template_executable = QCoreApplication::applicationFilePath();
  2079. const auto failExport = [this, &progress, &temp_project, &destination_directory](
  2080. const QString &message)
  2081. {
  2082. QFile::remove(temp_project);
  2083. QDir(destination_directory).removeRecursively();
  2084. progress.setValue(0);
  2085. progress.close();
  2086. ui_->exportRuntimeAction->setEnabled(
  2087. runtime_mode_service_.policy().allowsProjectEditing);
  2088. showProjectResult(tr("导出用户运行程序"), message, false);
  2089. };
  2090. if (!QDir().mkpath(destination_directory))
  2091. {
  2092. failExport(tr("无法创建导出目录:%1").arg(destination_directory));
  2093. return;
  2094. }
  2095. progress.setLabelText(tr("正在封装工程数据…"));
  2096. progress.setValue(35);
  2097. QApplication::processEvents();
  2098. const RuntimeProjectBundleWriteResult bundle_result =
  2099. RuntimeProjectBundleService::write(
  2100. template_executable,
  2101. temp_project,
  2102. destination_executable);
  2103. if (!bundle_result.succeeded)
  2104. {
  2105. failExport(bundle_result.message);
  2106. return;
  2107. }
  2108. progress.setLabelText(tr("正在复制 Qt 运行库和平台插件…"));
  2109. progress.setValue(65);
  2110. QApplication::processEvents();
  2111. const QString application_directory = QFileInfo(template_executable).absolutePath();
  2112. const QStringList runtime_files = {
  2113. QStringLiteral("Qt5Core.dll"),
  2114. QStringLiteral("Qt5Gui.dll"),
  2115. QStringLiteral("Qt5Network.dll"),
  2116. QStringLiteral("Qt5SerialBus.dll"),
  2117. QStringLiteral("Qt5SerialPort.dll"),
  2118. QStringLiteral("Qt5Widgets.dll"),
  2119. QStringLiteral("libgcc_s_seh-1.dll"),
  2120. QStringLiteral("libstdc++-6.dll"),
  2121. QStringLiteral("libwinpthread-1.dll")};
  2122. for (const QString &runtime_file : runtime_files)
  2123. {
  2124. const QString source = QDir(application_directory).filePath(runtime_file);
  2125. const QString destination = QDir(destination_directory).filePath(runtime_file);
  2126. if (!QFileInfo::exists(source) || !QFile::copy(source, destination))
  2127. {
  2128. failExport(tr("复制运行库失败:%1").arg(source));
  2129. return;
  2130. }
  2131. }
  2132. QString copy_error;
  2133. if (!copyDirectoryContents(
  2134. QDir(application_directory).filePath(QStringLiteral("platforms")),
  2135. QDir(destination_directory).filePath(QStringLiteral("platforms")),
  2136. &copy_error)
  2137. || !copyDirectoryContents(
  2138. QDir(application_directory).filePath(QStringLiteral("styles")),
  2139. QDir(destination_directory).filePath(QStringLiteral("styles")),
  2140. &copy_error))
  2141. {
  2142. failExport(copy_error);
  2143. return;
  2144. }
  2145. const RuntimeProjectBundleLoadResult verification =
  2146. RuntimeProjectBundleService::load(destination_executable);
  2147. if (verification.status != RuntimeProjectBundleStatus::Loaded)
  2148. {
  2149. failExport(verification.message.isEmpty()
  2150. ? tr("导出结果校验失败")
  2151. : verification.message);
  2152. return;
  2153. }
  2154. QFile::remove(temp_project);
  2155. progress.setLabelText(tr("导出完成"));
  2156. progress.setValue(100);
  2157. QApplication::processEvents();
  2158. progress.close();
  2159. ui_->exportRuntimeAction->setEnabled(
  2160. runtime_mode_service_.policy().allowsProjectEditing);
  2161. showProjectResult(
  2162. tr("导出用户运行程序"),
  2163. tr("已导出:%1").arg(destination_executable),
  2164. true);
  2165. }
  2166. void MainWindow::showProjectResult(
  2167. const QString &action, const QString &message, bool succeeded)
  2168. {
  2169. const QString output = action + QStringLiteral(": ") + message;
  2170. statusBar()->showMessage(output, 5000);
  2171. appendOutputMessage(output);
  2172. if (!succeeded)
  2173. {
  2174. QMessageBox::warning(this, action, message);
  2175. }
  2176. }
  2177. void MainWindow::appendOutputMessage(
  2178. const QString &message,
  2179. const std::optional<LogicSyntaxLocation> &location)
  2180. {
  2181. const int maximum = application_settings_result_
  2182. .settings.projectLimits.maximumOutputMessages;
  2183. while (ui_->outputList->count() >= maximum)
  2184. {
  2185. delete ui_->outputList->takeItem(0);
  2186. }
  2187. auto *item = new QListWidgetItem(message, ui_->outputList);
  2188. if (location.has_value())
  2189. {
  2190. item->setData(
  2191. kSyntaxLogicIdRole, fromUtf8(location->logicId));
  2192. item->setData(
  2193. kSyntaxRungIdRole, fromUtf8(location->rungId));
  2194. item->setData(kSyntaxColumnRole, location->column);
  2195. item->setToolTip(tr("双击定位到网络 %1,第 %2 行第 %3 列")
  2196. .arg(location->network)
  2197. .arg(location->row)
  2198. .arg(location->column));
  2199. }
  2200. ui_->outputList->scrollToBottom();
  2201. }
  2202. bool MainWindow::requestMode(ApplicationMode requested_mode)
  2203. {
  2204. // UI 动作只提出目标模式,所有前置条件和仓库切换由 RuntimeModeService 决定
  2205. // UI 仅转发模式意图,合法性由服务层和领域状态机决定
  2206. ModeTransitionResult result;
  2207. switch (requested_mode)
  2208. {
  2209. case ApplicationMode::Editing:
  2210. {
  2211. result = runtime_mode_service_.enterEditing();
  2212. break;
  2213. }
  2214. case ApplicationMode::OfflineRunning:
  2215. {
  2216. result = runtime_mode_service_.enterOfflineRunning();
  2217. break;
  2218. }
  2219. case ApplicationMode::OnlineRunning:
  2220. {
  2221. result = runtime_mode_service_.enterOnlineRunning();
  2222. break;
  2223. }
  2224. default:
  2225. {
  2226. restoreCurrentModeAction();
  2227. statusBar()->showMessage(tr("不支持的运行模式"), 5000);
  2228. return false;
  2229. }
  2230. }
  2231. const bool runtime_request = requested_mode == ApplicationMode::OfflineRunning
  2232. || requested_mode == ApplicationMode::OnlineRunning;
  2233. if (runtime_request
  2234. && (result.succeeded
  2235. || result.error == ModeTransitionError::ProjectNotReady))
  2236. {
  2237. const LogicSyntaxCheckResult &syntax =
  2238. runtime_mode_service_.lastSyntaxCheck();
  2239. if (syntax.completed && (syntax.changed || !syntax.valid))
  2240. {
  2241. reportLogicSyntaxCheck(syntax, tr("运行前语法检查"));
  2242. }
  2243. }
  2244. if (!result.succeeded)
  2245. {
  2246. restoreCurrentModeAction();
  2247. QString message = transitionErrorText(result.error);
  2248. if (result.error == ModeTransitionError::InitialPlcReadRequired)
  2249. {
  2250. message += tr(";当前状态:%1").arg(plcStatusText(runtime_mode_service_));
  2251. }
  2252. if (!result.detail.empty())
  2253. {
  2254. message += QStringLiteral(": ") + fromUtf8(result.detail);
  2255. }
  2256. if (result.error == ModeTransitionError::SimulationStartFailed)
  2257. {
  2258. const LogicScanResult &error = requested_mode
  2259. == ApplicationMode::OnlineRunning
  2260. ? runtime_mode_service_.onlineLogicMonitorService().lastError()
  2261. : runtime_mode_service_.simulationError();
  2262. if (!error.message.empty())
  2263. {
  2264. message += QStringLiteral(": ") + fromUtf8(error.message);
  2265. }
  2266. }
  2267. statusBar()->showMessage(message, 5000);
  2268. return false;
  2269. }
  2270. updateModeUi(tr("已进入%1").arg(modeText(runtime_mode_service_.mode())));
  2271. return true;
  2272. }
  2273. void MainWindow::updateModeUi(const QString &message)
  2274. {
  2275. // 按服务层策略统一启用/禁用编辑入口,避免单个按钮遗漏状态同步
  2276. const ApplicationMode mode = runtime_mode_service_.mode();
  2277. const ModePolicy policy = runtime_mode_service_.policy();
  2278. register_monitor_service_.setOfflineInitialCaptureEnabled(
  2279. mode == ApplicationMode::Editing);
  2280. restoreCurrentModeAction();
  2281. const bool running = mode != ApplicationMode::Editing;
  2282. if (running)
  2283. {
  2284. if (hmi_navigation_service_->currentPageId().empty())
  2285. {
  2286. const HmiNavigationResult navigation = hmi_navigation_service_->start();
  2287. if (!navigation.succeeded)
  2288. {
  2289. statusBar()->showMessage(fromUtf8(navigation.message), 5000);
  2290. }
  2291. }
  2292. runtime_panel_controller_->enterRuntime(
  2293. hmi_navigation_service_->currentPageId(),
  2294. current_logic_id_,
  2295. mode,
  2296. runtime_mode_service_.plcConnectionState());
  2297. }
  2298. else
  2299. {
  2300. hmi_navigation_service_->stop();
  2301. runtime_panel_controller_->leaveRuntime(
  2302. mode, runtime_mode_service_.plcConnectionState());
  2303. }
  2304. // 将同一份模式策略同步到所有可编辑入口,避免只禁用部分操作
  2305. ui_->projectDock->setEnabled(policy.allowsProjectEditing);
  2306. ui_->propertiesDock->setEnabled(policy.allowsProjectEditing);
  2307. hmi_editor_widget_->setEditingEnabled(policy.allowsProjectEditing);
  2308. hmi_editor_widget_->setRuntimeActive(
  2309. policy.usesVirtualRegisters || policy.usesPlcRegisters);
  2310. logic_editor_widget_->setEditingEnabled(policy.allowsProjectEditing);
  2311. if (!policy.allowsProjectEditing)
  2312. {
  2313. const QSignalBlocker draw_blocker(ui_->mouseDrawWireAction);
  2314. const QSignalBlocker erase_blocker(ui_->mouseEraseWireAction);
  2315. ui_->mouseDrawWireAction->setChecked(false);
  2316. ui_->mouseEraseWireAction->setChecked(false);
  2317. }
  2318. if (mode == ApplicationMode::Editing)
  2319. {
  2320. alarm_service_.reset();
  2321. logic_editor_widget_->clearRuntimeTrace();
  2322. }
  2323. ui_->hmiToolBar->setEnabled(policy.allowsProjectEditing);
  2324. ui_->logicToolBar->setEnabled(policy.allowsProjectEditing);
  2325. ui_->addButtonAction->setEnabled(policy.allowsProjectEditing);
  2326. ui_->addIndicatorAction->setEnabled(policy.allowsProjectEditing);
  2327. ui_->addNumericDisplayAction->setEnabled(policy.allowsProjectEditing);
  2328. ui_->addNumericInputAction->setEnabled(policy.allowsProjectEditing);
  2329. ui_->addLabelAction->setEnabled(policy.allowsProjectEditing);
  2330. ui_->addPageJumpAction->setEnabled(policy.allowsProjectEditing);
  2331. ui_->addAlarmListAction->setEnabled(policy.allowsProjectEditing);
  2332. ui_->configureAlarmsAction->setEnabled(policy.allowsProjectEditing);
  2333. ui_->configureRegisterCommentsAction->setEnabled(policy.allowsProjectEditing);
  2334. ui_->deleteControlAction->setEnabled(policy.allowsProjectEditing);
  2335. ui_->addNormallyOpenAction->setEnabled(policy.allowsProjectEditing);
  2336. ui_->addNormallyClosedAction->setEnabled(policy.allowsProjectEditing);
  2337. ui_->addRisingEdgeAction->setEnabled(policy.allowsProjectEditing);
  2338. ui_->addFallingEdgeAction->setEnabled(policy.allowsProjectEditing);
  2339. ui_->addNormalCoilAction->setEnabled(policy.allowsProjectEditing);
  2340. ui_->addSetCoilAction->setEnabled(policy.allowsProjectEditing);
  2341. ui_->addResetCoilAction->setEnabled(policy.allowsProjectEditing);
  2342. ui_->addMoveAction->setEnabled(policy.allowsProjectEditing);
  2343. ui_->addAddAction->setEnabled(policy.allowsProjectEditing);
  2344. ui_->addSubAction->setEnabled(policy.allowsProjectEditing);
  2345. ui_->addCompareAction->setEnabled(policy.allowsProjectEditing);
  2346. ui_->editNetworkCommentAction->setEnabled(policy.allowsProjectEditing);
  2347. ui_->deleteLogicAction->setEnabled(policy.allowsProjectEditing);
  2348. ui_->newProjectAction->setEnabled(policy.allowsProjectEditing);
  2349. ui_->saveProjectAction->setEnabled(policy.allowsProjectEditing);
  2350. ui_->saveAsProjectAction->setEnabled(policy.allowsProjectEditing);
  2351. ui_->loadProjectAction->setEnabled(policy.allowsProjectEditing);
  2352. ui_->exportRuntimeAction->setEnabled(policy.allowsProjectEditing);
  2353. updateProjectTreeActions();
  2354. updateEditActions();
  2355. mode_status_label_->setText(modeText(mode));
  2356. if (mode == ApplicationMode::Editing)
  2357. {
  2358. mode_status_label_->setStyleSheet(QStringLiteral(
  2359. "border: 1px solid #75a58a; background: #e3f0e8; color: #205c3b; padding: 2px 8px;"));
  2360. }
  2361. else if (mode == ApplicationMode::OfflineRunning)
  2362. {
  2363. mode_status_label_->setStyleSheet(QStringLiteral(
  2364. "border: 1px solid #bd8739; background: #fff0d2; color: #744b0e; padding: 2px 8px;"));
  2365. }
  2366. else
  2367. {
  2368. mode_status_label_->setStyleSheet(QStringLiteral(
  2369. "border: 1px solid #4b88a8; background: #deeff7; color: #174f6c; padding: 2px 8px;"));
  2370. }
  2371. register_status_label_->setText(
  2372. policy.usesVirtualRegisters ? tr("数据源:虚拟 M/D")
  2373. : policy.usesPlcRegisters ? tr("数据源:PLC 缓存") : tr("数据源:未启用"));
  2374. const PlcConnectionState plc_state = runtime_mode_service_.plcConnectionState();
  2375. const bool plc_configuration_available = plc_state == PlcConnectionState::Disconnected
  2376. || plc_state == PlcConnectionState::Faulted;
  2377. ui_->configurePlcAction->setEnabled(
  2378. policy.allowsProjectEditing && plc_configuration_available);
  2379. ui_->configurePlcAction->setText(
  2380. plc_state == PlcConnectionState::Faulted ? tr("PLC 重新配置") : tr("PLC 配置"));
  2381. ui_->configurePlcAction->setToolTip(
  2382. plc_state == PlcConnectionState::Faulted
  2383. ? tr("清理故障会话后重新配置并连接真实 PLC")
  2384. : tr("配置参数并连接真实 PLC"));
  2385. ui_->disconnectPlcAction->setEnabled(plc_state != PlcConnectionState::Disconnected);
  2386. if (plc_state == PlcConnectionState::Connecting)
  2387. {
  2388. register_status_label_->setText(tr("PLC:正在连接"));
  2389. }
  2390. else if (plc_state == PlcConnectionState::Connected)
  2391. {
  2392. register_status_label_->setText(
  2393. runtime_mode_service_.initialPlcReadCompleted()
  2394. ? tr("PLC:已连接,首读完成") : tr("PLC:已连接,正在首读"));
  2395. }
  2396. else if (plc_state == PlcConnectionState::Recovering)
  2397. {
  2398. register_status_label_->setText(tr("PLC:通信已恢复,正在重新首读"));
  2399. }
  2400. else if (plc_state == PlcConnectionState::Faulted)
  2401. {
  2402. register_status_label_->setText(tr("PLC:通信故障"));
  2403. }
  2404. updateSimulationUi(false);
  2405. refreshDataMonitorUi();
  2406. statusBar()->showMessage(message, 4000);
  2407. const bool duplicate = ui_->outputList->count() > 0
  2408. && ui_->outputList->item(ui_->outputList->count() - 1)->text() == message;
  2409. if (!duplicate)
  2410. {
  2411. appendOutputMessage(message);
  2412. }
  2413. }
  2414. void MainWindow::updateSimulationUi(bool report_fault)
  2415. {
  2416. runtime_panel_controller_->updateSimulationUi(report_fault);
  2417. }
  2418. // 根据当前真实运行模式,同步更新模式工具栏单选按钮选中状态
  2419. void MainWindow::restoreCurrentModeAction()
  2420. {
  2421. ui_->editingModeAction->setChecked(runtime_mode_service_.mode()
  2422. == ApplicationMode::Editing);
  2423. ui_->offlineModeAction->setChecked(runtime_mode_service_.mode()
  2424. == ApplicationMode::OfflineRunning);
  2425. ui_->onlineModeAction->setChecked(runtime_mode_service_.mode()
  2426. == ApplicationMode::OnlineRunning);
  2427. }