综合平台编程器项目的远程存储
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.
 
 
 
 

1208 lines
43 KiB

  1. #include "hmi_editor_widget.h"
  2. #include "services/hmi_editor_service.h"
  3. #include "services/hmi_runtime_service.h"
  4. #include "services/alarm_service.h"
  5. #include "domain/project_limits.h"
  6. #include <QApplication>
  7. #include <QDateTime>
  8. #include <QFont>
  9. #include <QGraphicsItem>
  10. #include <QGraphicsRectItem>
  11. #include <QGraphicsScene>
  12. #include <QGraphicsSceneHoverEvent>
  13. #include <QGraphicsSceneMouseEvent>
  14. #include <QInputDialog>
  15. #include <QPainter>
  16. #include <QResizeEvent>
  17. #include <QStyle>
  18. #include <QStyleOption>
  19. #include <QStyleOptionGraphicsItem>
  20. #include <algorithm>
  21. #include <cmath>
  22. #include <cstdint>
  23. #include <functional>
  24. #include <limits>
  25. namespace {
  26. constexpr qreal kAlarmHeaderHeight = 24.0;
  27. constexpr qreal kAlarmRowHeight = 22.0;
  28. constexpr qreal kAlarmCellLeftPadding = 5.0;
  29. constexpr qreal kAlarmCellRightPadding = 5.0;
  30. constexpr qreal kAlarmColumnSpacing = 3.0;
  31. constexpr qreal kAlarmTimeColumnWidth = 76.0;
  32. constexpr qreal kAlarmStateColumnWidth = 52.0;
  33. constexpr qreal kAlarmPageButtonSize = 16.0;
  34. constexpr qreal kAlarmPageIndicatorWidth = 34.0;
  35. QString alarmTimeText(const std::chrono::system_clock::time_point &time)
  36. {
  37. const auto seconds = std::chrono::duration_cast<std::chrono::seconds>(
  38. time.time_since_epoch()).count();
  39. return QDateTime::fromSecsSinceEpoch(seconds).toString(QStringLiteral("HH:mm:ss"));
  40. }
  41. // 将领域层 HMI 控件投影为可绘制、可选择和可交互的场景图元
  42. class HmiGraphicsItem final : public QGraphicsItem
  43. {
  44. public:
  45. // 保存控件快照、页面边界和回调,使图元不直接依赖服务层
  46. HmiGraphicsItem(
  47. const HmiControl &control,
  48. int page_width,
  49. int page_height,
  50. std::function<void(const std::string &, const QPointF &)> moved,
  51. std::function<void(const std::string &, HmiButtonEvent)> button_event,
  52. std::function<void(const std::string &)> numeric_input_activated,
  53. std::function<void(const std::string &)> page_navigation,
  54. std::function<void(const std::string &)> alarm_acknowledge)
  55. : control_(control),
  56. page_width_(page_width),
  57. page_height_(page_height),
  58. moved_(std::move(moved)),
  59. button_event_(std::move(button_event)),
  60. numeric_input_activated_(std::move(numeric_input_activated)),
  61. page_navigation_(std::move(page_navigation)),
  62. alarm_acknowledge_(std::move(alarm_acknowledge))
  63. {
  64. // 领域坐标直接作为图元在场景中的初始位置
  65. setPos(control_.bounds.x, control_.bounds.y);
  66. // 所有控件均可选中,便于主窗口显示对应属性
  67. setFlag(ItemIsSelectable, true);
  68. // 开启可选中
  69. setFlag(ItemSendsGeometryChanges, true);
  70. // 画布只处理左键交互,保留其他按键给视图默认行为
  71. setAcceptedMouseButtons(Qt::LeftButton);
  72. }
  73. // 返回图元自身坐标系中的矩形范围,用于绘制和命中测试
  74. QRectF boundingRect() const override
  75. {
  76. if (!control_.binding.has_value())
  77. {
  78. return visibleControlRect();
  79. }
  80. return visibleControlRect().united(addressRect());
  81. }
  82. void paint(
  83. QPainter *painter,
  84. const QStyleOptionGraphicsItem *option,
  85. QWidget *) override
  86. {
  87. // 留出一个像素边距,避免描边被图元边界裁剪
  88. const QRectF rect = visibleControlRect().adjusted(1, 1, -1, -1);
  89. painter->setRenderHint(QPainter::Antialiasing, true);
  90. painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1));
  91. if (control_.binding.has_value())
  92. {
  93. const QFont original_font = painter->font();
  94. QFont address_font = original_font;
  95. address_font.setPixelSize(11);
  96. painter->setFont(address_font);
  97. painter->setPen(QColor(QStringLiteral("#5f6b73")));
  98. painter->drawText(addressRect(), Qt::AlignCenter, bindingText());
  99. painter->setFont(original_font);
  100. painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1));
  101. }
  102. applyConfiguredFont(painter);
  103. switch (control_.type)
  104. {
  105. case HmiControlType::Button:
  106. {
  107. const bool disabled = !editing_enabled_ && runtime_active_
  108. && !runtime_write_enabled_;
  109. const QColor fill = disabled
  110. ? QColor(QStringLiteral("#e2e6e4"))
  111. : button_pressed_ ? QColor(QStringLiteral("#a9cbb6"))
  112. : button_hovered_ ? QColor(QStringLiteral("#cce4d5"))
  113. : QColor(QStringLiteral("#dcece3"));
  114. const QColor border = disabled
  115. ? QColor(QStringLiteral("#a5ada9"))
  116. : button_pressed_ ? QColor(QStringLiteral("#356248"))
  117. : QColor(QStringLiteral("#5e816b"));
  118. const QColor text = disabled
  119. ? QColor(QStringLiteral("#7a837e"))
  120. : configuredTextColor(QColor(QStringLiteral("#205c3b")));
  121. // 未按下时保留下沿阴影,按下后将按钮面下移形成明确的回弹感
  122. QRectF face = rect.adjusted(0, 0, 0, -2);
  123. if (!disabled && button_pressed_)
  124. {
  125. face.translate(0, 2);
  126. }
  127. else
  128. {
  129. painter->setPen(Qt::NoPen);
  130. painter->setBrush(disabled
  131. ? QColor(QStringLiteral("#c5cbc8"))
  132. : QColor(QStringLiteral("#789b85")));
  133. painter->drawRoundedRect(face.translated(0, 2), 4, 4);
  134. }
  135. painter->setPen(QPen(border, button_pressed_ ? 2 : 1));
  136. painter->setBrush(fill);
  137. painter->drawRoundedRect(face, 4, 4);
  138. painter->setPen(text);
  139. painter->drawText(face, Qt::AlignCenter, textWithValue());
  140. break;
  141. }
  142. case HmiControlType::Indicator:
  143. {
  144. // 指示灯颜色由最近一次读取到的 M 位值决定
  145. const qreal diameter = std::min(rect.width(), rect.height() - 18.0);
  146. const QRectF lamp(
  147. rect.center().x() - diameter / 2.0,
  148. rect.top() + 2,
  149. diameter,
  150. diameter);
  151. painter->setBrush(bit_value_ ? QColor(QStringLiteral("#24a148"))
  152. : QColor(QStringLiteral("#b8c1c8")));
  153. painter->drawEllipse(lamp);
  154. painter->setPen(configuredTextColor(QColor(QStringLiteral("#24313b"))));
  155. painter->drawText(
  156. QRectF(rect.left(), lamp.bottom() + 1, rect.width(), 16),
  157. Qt::AlignCenter,
  158. QString::fromUtf8(control_.text.data(),
  159. static_cast<int>(control_.text.size())));
  160. break;
  161. }
  162. case HmiControlType::NumericDisplay:
  163. {
  164. // 数值显示为只读样式,文本由运行值刷新
  165. painter->setBrush(QColor(QStringLiteral("#edf2f6")));
  166. painter->drawRect(rect);
  167. painter->setPen(configuredTextColor(QColor(QStringLiteral("#24313b"))));
  168. painter->drawText(rect.adjusted(7, 0, -7, 0),
  169. Qt::AlignVCenter | Qt::AlignLeft,
  170. textWithValue());
  171. break;
  172. }
  173. case HmiControlType::NumericInput:
  174. {
  175. // 数值输入以白色编辑框样式呈现,双击后才请求写入
  176. painter->setBrush(QColor(QStringLiteral("#ffffff")));
  177. painter->drawRoundedRect(rect, 3, 3);
  178. painter->setPen(configuredTextColor(QColor(QStringLiteral("#24313b"))));
  179. painter->drawText(rect.adjusted(7, 0, -7, 0),
  180. Qt::AlignVCenter | Qt::AlignLeft,
  181. textWithValue());
  182. break;
  183. }
  184. case HmiControlType::ProgressBar:
  185. {
  186. const HmiProgressBarConfig config = control_.progressBar.value_or(
  187. HmiProgressBarConfig{});
  188. const int percentage = has_runtime_value_
  189. ? config.percentageForValue(word_value_) : 0;
  190. painter->setPen(Qt::NoPen);
  191. painter->setBrush(QColor(QStringLiteral("#e4e9e7")));
  192. painter->drawRoundedRect(rect, 4, 4);
  193. const QRectF track = rect.adjusted(2, 2, -2, -2);
  194. if (has_runtime_value_ && percentage > 0)
  195. {
  196. QRectF fill = track;
  197. fill.setWidth(track.width() * percentage / 100.0);
  198. painter->setBrush(QColor(QStringLiteral("#3c8c62")));
  199. painter->drawRoundedRect(fill, 3, 3);
  200. }
  201. painter->setBrush(Qt::NoBrush);
  202. painter->setPen(QPen(QColor(QStringLiteral("#617069")), 1));
  203. painter->drawRoundedRect(rect, 4, 4);
  204. QString display_text = QString::fromUtf8(
  205. control_.text.data(), static_cast<int>(control_.text.size()));
  206. if (config.showValue)
  207. {
  208. const QString percentage_text = has_runtime_value_
  209. ? QString::number(percentage) + QLatin1Char('%')
  210. : runtime_active_ ? QStringLiteral("--")
  211. : QStringLiteral("0%");
  212. display_text = display_text.isEmpty()
  213. ? percentage_text
  214. : display_text + QStringLiteral(": ") + percentage_text;
  215. }
  216. painter->setPen(configuredTextColor(QColor(QStringLiteral("#20342a"))));
  217. painter->drawText(rect.adjusted(6, 0, -6, 0),
  218. Qt::AlignCenter,
  219. display_text);
  220. break;
  221. }
  222. case HmiControlType::PageJump:
  223. {
  224. const QColor fill = runtime_active_ && page_hovered_
  225. ? QColor(QStringLiteral("#d8eafa"))
  226. : QColor(QStringLiteral("#e8f1fa"));
  227. painter->setBrush(fill);
  228. painter->setPen(QPen(QColor(QStringLiteral("#4e789f")), 1));
  229. painter->drawRoundedRect(rect, 4, 4);
  230. painter->setPen(configuredTextColor(QColor(QStringLiteral("#244b6b"))));
  231. painter->drawText(rect, Qt::AlignCenter, textWithValue());
  232. break;
  233. }
  234. case HmiControlType::AlarmList:
  235. {
  236. painter->setPen(QPen(QColor(QStringLiteral("#9b3a3a")), 1));
  237. painter->setBrush(QColor(QStringLiteral("#ffffff")));
  238. painter->drawRect(rect);
  239. const QRectF header(
  240. rect.left(), rect.top(), rect.width(), kAlarmHeaderHeight);
  241. painter->fillRect(header, QColor(QStringLiteral("#a63f3f")));
  242. painter->setPen(Qt::white);
  243. QRectF title_rect = header.adjusted(7, 0, -7, 0);
  244. const std::size_t page_count = alarmPageCount();
  245. if (runtime_active_ && page_count > 1U)
  246. {
  247. const QRectF previous_rect = alarmPreviousPageRect(header);
  248. const QRectF next_rect = alarmNextPageRect(header);
  249. const QRectF indicator_rect = alarmPageIndicatorRect(header);
  250. title_rect.setRight(previous_rect.left() - kAlarmColumnSpacing);
  251. QStyleOption previous_option;
  252. previous_option.rect = previous_rect.toAlignedRect();
  253. previous_option.state = alarm_page_ > 0U
  254. ? QStyle::State_Enabled : QStyle::State_None;
  255. QApplication::style()->drawPrimitive(
  256. QStyle::PE_IndicatorArrowLeft,
  257. &previous_option,
  258. painter);
  259. QStyleOption next_option;
  260. next_option.rect = next_rect.toAlignedRect();
  261. next_option.state = alarm_page_ + 1U < page_count
  262. ? QStyle::State_Enabled : QStyle::State_None;
  263. QApplication::style()->drawPrimitive(
  264. QStyle::PE_IndicatorArrowRight,
  265. &next_option,
  266. painter);
  267. painter->setPen(Qt::white);
  268. painter->drawText(
  269. indicator_rect,
  270. Qt::AlignCenter,
  271. QStringLiteral("%1/%2")
  272. .arg(static_cast<qulonglong>(alarm_page_ + 1U))
  273. .arg(static_cast<qulonglong>(page_count)));
  274. }
  275. const QString title = QString::fromUtf8(
  276. control_.text.data(), static_cast<int>(control_.text.size()));
  277. painter->drawText(
  278. title_rect,
  279. Qt::AlignVCenter | Qt::AlignLeft,
  280. painter->fontMetrics().elidedText(
  281. title,
  282. Qt::ElideRight,
  283. std::max(0, static_cast<int>(title_rect.width()))));
  284. const int visible_rows = static_cast<int>(visibleAlarmRecordCount());
  285. if (visible_rows == 0)
  286. {
  287. painter->setPen(configuredTextColor(QColor(QStringLiteral("#6f7a82"))));
  288. painter->drawText(
  289. QRectF(
  290. rect.left(),
  291. header.bottom(),
  292. rect.width(),
  293. rect.height() - kAlarmHeaderHeight),
  294. Qt::AlignCenter,
  295. runtime_active_ ? QObject::tr("暂无报警")
  296. : QObject::tr("运行时显示当前报警"));
  297. break;
  298. }
  299. const std::size_t first_record = alarmFirstRecordIndex();
  300. for (int row = 0; row < visible_rows; ++row)
  301. {
  302. const AlarmRecord &record = alarm_records_[
  303. first_record + static_cast<std::size_t>(row)];
  304. const QRectF row_rect(
  305. rect.left(),
  306. header.bottom() + row * kAlarmRowHeight,
  307. rect.width(),
  308. kAlarmRowHeight);
  309. painter->fillRect(
  310. row_rect,
  311. QColor(QStringLiteral("#fde8e8")));
  312. painter->setPen(QColor(QStringLiteral("#d3d8dc")));
  313. painter->drawLine(row_rect.bottomLeft(), row_rect.bottomRight());
  314. painter->setPen(configuredTextColor(QColor(QStringLiteral("#8d1f1f"))));
  315. const QString state = record.acknowledged
  316. ? QObject::tr("已确认") : QObject::tr("未确认");
  317. const QRectF time_rect(
  318. row_rect.left() + kAlarmCellLeftPadding,
  319. row_rect.top(),
  320. kAlarmTimeColumnWidth,
  321. row_rect.height());
  322. const QRectF state_rect(
  323. time_rect.right() + kAlarmColumnSpacing,
  324. row_rect.top(),
  325. kAlarmStateColumnWidth,
  326. row_rect.height());
  327. const qreal message_left =
  328. state_rect.right() + kAlarmColumnSpacing;
  329. const QRectF message_rect(
  330. message_left,
  331. row_rect.top(),
  332. std::max(
  333. 0.0,
  334. row_rect.right() - kAlarmCellRightPadding
  335. - message_left),
  336. row_rect.height());
  337. painter->drawText(
  338. time_rect,
  339. Qt::AlignVCenter | Qt::AlignLeft,
  340. alarmTimeText(record.occurredAt));
  341. painter->drawText(
  342. state_rect,
  343. Qt::AlignVCenter | Qt::AlignLeft,
  344. state);
  345. painter->drawText(
  346. message_rect,
  347. Qt::AlignVCenter | Qt::AlignLeft,
  348. painter->fontMetrics().elidedText(
  349. QString::fromUtf8(
  350. record.message.data(),
  351. static_cast<int>(record.message.size())),
  352. Qt::ElideRight,
  353. std::max(0, static_cast<int>(message_rect.width()))));
  354. }
  355. break;
  356. }
  357. case HmiControlType::Label:
  358. default:
  359. {
  360. // 标签只显示固定文本,不绑定寄存器运行值
  361. painter->setPen(configuredTextColor(QColor(QStringLiteral("#24313b"))));
  362. painter->drawText(rect, Qt::AlignCenter,
  363. QString::fromUtf8(control_.text.data(),
  364. static_cast<int>(control_.text.size())));
  365. break;
  366. }
  367. }
  368. if ((option->state & QStyle::State_Selected) != 0)
  369. {
  370. // 选中框独立于控件类型,提示当前可编辑对象
  371. painter->setBrush(Qt::NoBrush);
  372. painter->setPen(QPen(QColor(QStringLiteral("#1677a8")), 2));
  373. painter->drawRect(controlRect().adjusted(0, 0, -1, -1));
  374. }
  375. }
  376. // 向场景和外部控件返回该图元对应的领域控件标识
  377. const std::string &controlId() const
  378. {
  379. return control_.id;
  380. }
  381. // 分离编辑选择与运行输入,避免运行态点击继续显示编辑选中框
  382. void setInteractionState(
  383. bool editable, bool runtime_active, bool runtime_write_enabled)
  384. {
  385. if (control_.type == HmiControlType::AlarmList)
  386. {
  387. prepareGeometryChange();
  388. if (!runtime_active)
  389. {
  390. alarm_page_ = 0U;
  391. }
  392. }
  393. editing_enabled_ = editable;
  394. runtime_active_ = runtime_active;
  395. runtime_write_enabled_ = runtime_active && runtime_write_enabled;
  396. setFlag(ItemIsMovable, editable);
  397. setFlag(ItemIsSelectable, editable);
  398. if (!editable)
  399. {
  400. setSelected(false);
  401. }
  402. const bool runtime_input = runtime_active_
  403. && (control_.type == HmiControlType::Button
  404. || control_.type == HmiControlType::NumericInput
  405. || control_.type == HmiControlType::PageJump
  406. || control_.type == HmiControlType::AlarmList);
  407. const bool runtime_input_enabled = runtime_input
  408. && (control_.type == HmiControlType::PageJump
  409. || control_.type == HmiControlType::AlarmList
  410. || runtime_write_enabled_);
  411. setAcceptedMouseButtons(
  412. editable || runtime_input_enabled ? Qt::LeftButton : Qt::NoButton);
  413. const bool runtime_button_enabled = runtime_active_
  414. && control_.type == HmiControlType::Button && runtime_write_enabled_;
  415. const bool runtime_page_jump_enabled = runtime_active_
  416. && control_.type == HmiControlType::PageJump;
  417. const bool runtime_alarm_enabled = runtime_active_
  418. && control_.type == HmiControlType::AlarmList;
  419. setAcceptHoverEvents(runtime_button_enabled || runtime_page_jump_enabled);
  420. if (runtime_button_enabled || runtime_page_jump_enabled
  421. || runtime_alarm_enabled)
  422. {
  423. setCursor(Qt::PointingHandCursor);
  424. }
  425. else
  426. {
  427. unsetCursor();
  428. button_hovered_ = false;
  429. page_hovered_ = false;
  430. button_pressed_ = false;
  431. }
  432. updateAlarmVisibility();
  433. update();
  434. }
  435. // 缓存运行服务读出的值并触发 Qt 重绘
  436. void setRuntimeValue(bool bit_value, std::int16_t word_value, bool available)
  437. {
  438. bit_value_ = bit_value;
  439. word_value_ = word_value;
  440. has_runtime_value_ = available;
  441. update();
  442. }
  443. void setAlarmRecords(const std::vector<AlarmRecord> &records)
  444. {
  445. if (control_.type == HmiControlType::AlarmList)
  446. {
  447. prepareGeometryChange();
  448. }
  449. alarm_records_ = records;
  450. const std::size_t page_count = alarmPageCount();
  451. alarm_page_ = page_count == 0U
  452. ? 0U : std::min(alarm_page_, page_count - 1U);
  453. updateAlarmVisibility();
  454. update();
  455. }
  456. protected:
  457. // 拖拽过程中将新位置限制在页面可见边界内
  458. QVariant itemChange(GraphicsItemChange change, const QVariant &value) override
  459. {
  460. // 只有开启 ItemIsMovable 拖拽时,才做坐标钳位
  461. if (change == ItemPositionChange && flags().testFlag(ItemIsMovable))
  462. {
  463. // 在图元层预先截断拖拽坐标,避免控件视觉上越出页面
  464. QPointF position = value.toPointF();
  465. const qreal maximum_x = std::max(
  466. 0.0, static_cast<double>(page_width_ - control_.bounds.width));
  467. const qreal maximum_y = std::max(
  468. 0.0, static_cast<double>(page_height_ - control_.bounds.height));
  469. position.setX(std::clamp(position.x(), 0.0, maximum_x));
  470. position.setY(std::clamp(position.y(), 0.0, maximum_y));
  471. return position;
  472. }
  473. return QGraphicsItem::itemChange(change, value);
  474. }
  475. // 运行态按钮按下时通知外层执行配置的 M 位操作
  476. void mousePressEvent(QGraphicsSceneMouseEvent *event) override
  477. {
  478. if (runtime_active_ && control_.type == HmiControlType::AlarmList)
  479. {
  480. if (event->pos().y() < kAlarmHeaderHeight)
  481. {
  482. const QRectF content_rect =
  483. visibleControlRect().adjusted(1, 1, -1, -1);
  484. const QRectF header(
  485. content_rect.left(),
  486. content_rect.top(),
  487. content_rect.width(),
  488. kAlarmHeaderHeight);
  489. const std::size_t page_count = alarmPageCount();
  490. if (alarmPreviousPageRect(header).contains(event->pos())
  491. && alarm_page_ > 0U)
  492. {
  493. prepareGeometryChange();
  494. --alarm_page_;
  495. update();
  496. }
  497. else if (alarmNextPageRect(header).contains(event->pos())
  498. && alarm_page_ + 1U < page_count)
  499. {
  500. prepareGeometryChange();
  501. ++alarm_page_;
  502. update();
  503. }
  504. event->accept();
  505. return;
  506. }
  507. const int row = static_cast<int>(
  508. (event->pos().y() - kAlarmHeaderHeight) / kAlarmRowHeight);
  509. const std::size_t record_index = alarmFirstRecordIndex()
  510. + static_cast<std::size_t>(std::max(0, row));
  511. if (row >= 0
  512. && row < static_cast<int>(visibleAlarmRecordCount())
  513. && record_index < alarm_records_.size())
  514. {
  515. const AlarmRecord &record = alarm_records_[record_index];
  516. if (!record.acknowledged && alarm_acknowledge_)
  517. {
  518. alarm_acknowledge_(record.definitionId);
  519. }
  520. }
  521. event->accept();
  522. return;
  523. }
  524. if (runtime_active_ && control_.type == HmiControlType::PageJump)
  525. {
  526. page_pressed_ = true;
  527. update();
  528. event->accept();
  529. return;
  530. }
  531. if (runtime_active_ && runtime_write_enabled_
  532. && control_.type == HmiControlType::Button && button_event_)
  533. {
  534. button_pressed_ = true;
  535. update();
  536. button_event_(control_.id, HmiButtonEvent::Pressed);
  537. event->accept();
  538. return;
  539. }
  540. // 编辑模式:不进if分支,执行基类事件——只做选中、拖拽
  541. QGraphicsItem::mousePressEvent(event);
  542. }
  543. // 运行态双击数值输入时通知外层弹出数值编辑对话框
  544. void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override
  545. {
  546. if (runtime_active_ && runtime_write_enabled_
  547. && control_.type == HmiControlType::NumericInput
  548. && numeric_input_activated_)
  549. {
  550. // 数值输入使用双击,避免普通选择操作意外写入 D 字
  551. numeric_input_activated_(control_.id);
  552. event->accept();
  553. return;
  554. }
  555. QGraphicsItem::mouseDoubleClickEvent(event);
  556. }
  557. // 拖拽结束后才提交最终坐标,避免移动过程频繁修改领域模型
  558. void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override
  559. {
  560. if (runtime_active_ && control_.type == HmiControlType::PageJump
  561. && page_pressed_)
  562. {
  563. page_pressed_ = false;
  564. update();
  565. if (page_navigation_ && control_.pageJump.has_value())
  566. {
  567. page_navigation_(control_.pageJump->targetPageId);
  568. }
  569. event->accept();
  570. return;
  571. }
  572. if (runtime_active_ && runtime_write_enabled_
  573. && control_.type == HmiControlType::Button && button_pressed_)
  574. {
  575. button_pressed_ = false;
  576. update();
  577. button_event_(control_.id, HmiButtonEvent::Released);
  578. event->accept();
  579. return;
  580. }
  581. QGraphicsItem::mouseReleaseEvent(event);
  582. // 只有ItemIsMovable打开(编辑态),松开鼠标才提交位置给业务层
  583. if (flags().testFlag(ItemIsMovable) && moved_)
  584. {
  585. moved_(control_.id, pos());
  586. }
  587. }
  588. void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override
  589. {
  590. if (runtime_active_ && runtime_write_enabled_
  591. && control_.type == HmiControlType::Button)
  592. {
  593. button_hovered_ = true;
  594. update();
  595. }
  596. if (runtime_active_ && control_.type == HmiControlType::PageJump)
  597. {
  598. page_hovered_ = true;
  599. update();
  600. }
  601. QGraphicsItem::hoverEnterEvent(event);
  602. }
  603. void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override
  604. {
  605. if (button_hovered_)
  606. {
  607. button_hovered_ = false;
  608. update();
  609. }
  610. if (page_hovered_)
  611. {
  612. page_hovered_ = false;
  613. update();
  614. }
  615. QGraphicsItem::hoverLeaveEvent(event);
  616. }
  617. private:
  618. std::size_t alarmPageSize() const
  619. {
  620. const QRectF rect = controlRect().adjusted(1, 1, -1, -1);
  621. const int rows_by_height = std::max(
  622. 0,
  623. static_cast<int>((rect.height() - kAlarmHeaderHeight)
  624. / kAlarmRowHeight));
  625. return static_cast<std::size_t>(std::min(
  626. rows_by_height, ProjectLimits::kMaximumVisibleAlarmRows));
  627. }
  628. std::size_t alarmPageCount() const
  629. {
  630. const std::size_t page_size = alarmPageSize();
  631. return page_size == 0U || alarm_records_.empty()
  632. ? 0U
  633. : (alarm_records_.size() + page_size - 1U) / page_size;
  634. }
  635. std::size_t alarmFirstRecordIndex() const
  636. {
  637. return alarm_page_ * alarmPageSize();
  638. }
  639. std::size_t visibleAlarmRecordCount() const
  640. {
  641. const std::size_t first_record = alarmFirstRecordIndex();
  642. if (first_record >= alarm_records_.size())
  643. {
  644. return 0U;
  645. }
  646. return std::min(
  647. alarmPageSize(), alarm_records_.size() - first_record);
  648. }
  649. QRectF visibleControlRect() const
  650. {
  651. const QRectF full_rect = controlRect();
  652. if (control_.type != HmiControlType::AlarmList || !runtime_active_)
  653. {
  654. return full_rect;
  655. }
  656. const qreal content_height = kAlarmHeaderHeight
  657. + static_cast<qreal>(visibleAlarmRecordCount()) * kAlarmRowHeight
  658. + 2.0;
  659. return {full_rect.left(),
  660. full_rect.top(),
  661. full_rect.width(),
  662. std::min(full_rect.height(), content_height)};
  663. }
  664. QRectF alarmNextPageRect(const QRectF &header) const
  665. {
  666. return {
  667. header.right() - kAlarmCellRightPadding - kAlarmPageButtonSize,
  668. header.center().y() - kAlarmPageButtonSize / 2.0,
  669. kAlarmPageButtonSize,
  670. kAlarmPageButtonSize};
  671. }
  672. QRectF alarmPageIndicatorRect(const QRectF &header) const
  673. {
  674. const QRectF next_rect = alarmNextPageRect(header);
  675. return {
  676. next_rect.left() - kAlarmPageIndicatorWidth,
  677. header.top(),
  678. kAlarmPageIndicatorWidth,
  679. header.height()};
  680. }
  681. QRectF alarmPreviousPageRect(const QRectF &header) const
  682. {
  683. const QRectF indicator_rect = alarmPageIndicatorRect(header);
  684. return {
  685. indicator_rect.left() - kAlarmPageButtonSize,
  686. header.center().y() - kAlarmPageButtonSize / 2.0,
  687. kAlarmPageButtonSize,
  688. kAlarmPageButtonSize};
  689. }
  690. void updateAlarmVisibility()
  691. {
  692. if (control_.type != HmiControlType::AlarmList)
  693. {
  694. return;
  695. }
  696. const bool has_active_alarm = !alarm_records_.empty();
  697. setVisible(editing_enabled_ || !runtime_active_ || has_active_alarm);
  698. setZValue(runtime_active_ && has_active_alarm ? 1000.0 : 0.0);
  699. }
  700. // 控件本体范围保持领域坐标和尺寸语义不变
  701. QRectF controlRect() const
  702. {
  703. return {0,
  704. 0,
  705. static_cast<qreal>(control_.bounds.width),
  706. static_cast<qreal>(control_.bounds.height)};
  707. }
  708. // 地址作为控件的附属信息显示在本体上方
  709. QRectF addressRect() const
  710. {
  711. constexpr qreal address_height = 16.0;
  712. constexpr qreal address_spacing = 2.0;
  713. return {0,
  714. -address_height - address_spacing,
  715. static_cast<qreal>(control_.bounds.width),
  716. address_height};
  717. }
  718. QString bindingText() const
  719. {
  720. return control_.binding.has_value()
  721. ? QString::fromStdString(control_.binding->toString())
  722. : QString{};
  723. }
  724. QColor configuredTextColor(const QColor &fallback) const
  725. {
  726. const auto property = control_.properties.find(
  727. HmiAppearanceProperty::kTextColor);
  728. if (property == control_.properties.cend())
  729. {
  730. return fallback;
  731. }
  732. const QColor color = QColor(QString::fromUtf8(
  733. property->second.data(), static_cast<int>(property->second.size())));
  734. return color.isValid() ? color : fallback;
  735. }
  736. void applyConfiguredFont(QPainter *painter) const
  737. {
  738. if (painter == nullptr)
  739. {
  740. return;
  741. }
  742. QFont font = painter->font();
  743. const auto font_size = control_.properties.find(
  744. HmiAppearanceProperty::kFontSize);
  745. if (font_size != control_.properties.cend())
  746. {
  747. bool ok = false;
  748. const int point_size = QString::fromUtf8(
  749. font_size->second.data(),
  750. static_cast<int>(font_size->second.size())).toInt(&ok);
  751. if (ok
  752. && point_size >= ProjectLimits::kMinimumHmiFontPointSize
  753. && point_size <= ProjectLimits::kMaximumHmiFontPointSize)
  754. {
  755. font.setPointSize(point_size);
  756. }
  757. }
  758. else if (control_.type == HmiControlType::AlarmList
  759. && font.pointSize() > 0)
  760. {
  761. font.setPointSize(std::max(
  762. ProjectLimits::kMinimumHmiFontPointSize,
  763. font.pointSize()
  764. - ProjectLimits::kAlarmDefaultFontPointReduction));
  765. }
  766. const auto font_bold = control_.properties.find(
  767. HmiAppearanceProperty::kFontBold);
  768. if (font_bold != control_.properties.cend())
  769. {
  770. font.setBold(font_bold->second == "true");
  771. }
  772. const auto font_italic = control_.properties.find(
  773. HmiAppearanceProperty::kFontItalic);
  774. if (font_italic != control_.properties.cend())
  775. {
  776. font.setItalic(font_italic->second == "true");
  777. }
  778. painter->setFont(font);
  779. }
  780. // 根据控件类型和运行数据组合当前应绘制的文字
  781. QString textWithValue() const
  782. {
  783. const QString text = QString::fromUtf8(
  784. control_.text.data(), static_cast<int>(control_.text.size()));
  785. if (!has_runtime_value_ || control_.type == HmiControlType::Button)
  786. {
  787. return text;
  788. }
  789. if (control_.type == HmiControlType::NumericDisplay
  790. || control_.type == HmiControlType::NumericInput)
  791. {
  792. return text + QStringLiteral(": ") + QString::number(word_value_);
  793. }
  794. return text;
  795. }
  796. // 图元创建时的领域控件快照,提供类型、尺寸、文本和标识
  797. HmiControl control_;
  798. // 当前页面宽度,用于限制图元横向拖拽范围
  799. int page_width_ = 0;
  800. // 当前页面高度,用于限制图元纵向拖拽范围
  801. int page_height_ = 0;
  802. // 拖拽完成后回调 HmiEditorWidget 提交控件新位置
  803. std::function<void(const std::string &, const QPointF &)> moved_;
  804. std::function<void(const std::string &, HmiButtonEvent)> button_event_;
  805. std::function<void(const std::string &)> numeric_input_activated_;
  806. std::function<void(const std::string &)> page_navigation_;
  807. std::function<void(const std::string &)> alarm_acknowledge_;
  808. std::vector<AlarmRecord> alarm_records_;
  809. std::size_t alarm_page_ = 0U;
  810. bool editing_enabled_ = true;
  811. bool runtime_active_ = false;
  812. bool runtime_write_enabled_ = false;
  813. bool button_hovered_ = false;
  814. bool button_pressed_ = false;
  815. bool page_hovered_ = false;
  816. bool page_pressed_ = false;
  817. // 指示灯读取到的 M 位值
  818. bool bit_value_ = false;
  819. // 数值控件读取到的 D 字值
  820. std::int16_t word_value_ = 0;
  821. // 标记当前缓存值是否来自一次成功的运行时读取
  822. bool has_runtime_value_ = false;
  823. };
  824. // 将场景通用图元安全转换为本文件定义的 HMI 控件图元
  825. HmiGraphicsItem *asHmiItem(QGraphicsItem *item)
  826. {
  827. return dynamic_cast<HmiGraphicsItem *>(item);
  828. }
  829. } // namespace
  830. HmiEditorWidget::HmiEditorWidget(
  831. HmiEditorService &editor_service,
  832. HmiRuntimeService &runtime_service,
  833. AlarmService &alarm_service,
  834. QWidget *parent)
  835. : QGraphicsView(parent),
  836. editor_service_(editor_service),
  837. runtime_service_(runtime_service),
  838. alarm_service_(alarm_service),
  839. scene_(new QGraphicsScene(this))
  840. {
  841. setObjectName(QStringLiteral("hmiEditorWidget"));
  842. setScene(scene_);
  843. setRenderHint(QPainter::Antialiasing, true);
  844. setDragMode(QGraphicsView::RubberBandDrag);
  845. setBackgroundBrush(QColor(QStringLiteral("#dfe5e9")));
  846. connect(scene_, &QGraphicsScene::selectionChanged,
  847. this, &HmiEditorWidget::handleSelectionChanged);
  848. }
  849. // 切换显示页面
  850. void HmiEditorWidget::setPageId(const std::string &page_id)
  851. {
  852. // 切换页面只改变投影上下文,真正的页面数据仍由 ProjectService 持有
  853. if (page_id_ == page_id)
  854. {
  855. return;
  856. }
  857. page_id_ = page_id;
  858. reloadPage();
  859. }
  860. void HmiEditorWidget::setEditingEnabled(bool enabled)
  861. {
  862. editing_enabled_ = enabled;
  863. setDragMode(enabled ? QGraphicsView::RubberBandDrag : QGraphicsView::NoDrag);
  864. updateItemInteractions();
  865. }
  866. void HmiEditorWidget::setRuntimeActive(bool active)
  867. {
  868. runtime_active_ = active;
  869. if (!runtime_active_)
  870. {
  871. runtime_write_enabled_ = false;
  872. for (QGraphicsItem *item : scene_->items())
  873. {
  874. HmiGraphicsItem *control_item = asHmiItem(item);
  875. if (control_item != nullptr)
  876. {
  877. control_item->setRuntimeValue(false, 0, false);
  878. control_item->setAlarmRecords({});
  879. }
  880. }
  881. }
  882. updateItemInteractions();
  883. }
  884. void HmiEditorWidget::setRuntimeWriteEnabled(bool enabled)
  885. {
  886. runtime_write_enabled_ = runtime_active_ && enabled;
  887. updateItemInteractions();
  888. }
  889. // 全部重新加载当前页面,把内存模型 HmiPage 渲染成画面上图形
  890. void HmiEditorWidget::reloadPage()
  891. {
  892. // 模型发生变化后完全重建图元,避免增量刷新遗漏属性或选择状态
  893. // 画布始终从当前领域页面重建,避免保留已删除控件的图元
  894. scene_->clear();
  895. const HmiPage *page = editor_service_.findPage(page_id_);
  896. if (page == nullptr)
  897. {
  898. scene_->setSceneRect({});
  899. emit controlSelected({});
  900. return;
  901. }
  902. scene_->setSceneRect(0, 0, page->width, page->height);
  903. QGraphicsRectItem *page_border = scene_->addRect(
  904. scene_->sceneRect(), QPen(QColor(QStringLiteral("#8a98a3")), 1), Qt::white);
  905. page_border->setZValue(-1);
  906. page_border->setAcceptedMouseButtons(Qt::NoButton);
  907. for (const HmiControl &control : page->controls)
  908. {
  909. auto *item = new HmiGraphicsItem(
  910. control,
  911. page->width,
  912. page->height,
  913. [this](const std::string &control_id, const QPointF &position)
  914. {
  915. handleControlMoved(control_id, position);
  916. },
  917. [this](const std::string &control_id, HmiButtonEvent event)
  918. {
  919. handleButtonEvent(control_id, event);
  920. },
  921. [this](const std::string &control_id)
  922. {
  923. handleNumericInputActivated(control_id);
  924. },
  925. [this](const std::string &target_page_id)
  926. {
  927. emit pageNavigationRequested(
  928. QString::fromStdString(target_page_id));
  929. },
  930. [this](const std::string &definition_id)
  931. {
  932. alarm_service_.acknowledge(definition_id);
  933. refreshRuntimeValues();
  934. });
  935. scene_->addItem(item);
  936. item->setInteractionState(
  937. editing_enabled_, runtime_active_, runtime_write_enabled_);
  938. }
  939. fitCurrentPage();
  940. refreshRuntimeValues();
  941. }
  942. // 遍历场景所有图元,找到对应 id 的图元,设置选中,视图滚动到把控件显示出来
  943. void HmiEditorWidget::selectControl(const std::string &control_id)
  944. {
  945. scene_->clearSelection();
  946. for (QGraphicsItem *item : scene_->items())
  947. {
  948. HmiGraphicsItem *control_item = asHmiItem(item);
  949. if (control_item != nullptr && control_item->controlId() == control_id)
  950. {
  951. control_item->setSelected(true);
  952. ensureVisible(control_item);
  953. return;
  954. }
  955. }
  956. }
  957. std::string HmiEditorWidget::selectedControlId() const
  958. {
  959. const std::vector<std::string> ids = selectedControlIds();
  960. return ids.empty() ? std::string{} : ids.front();
  961. }
  962. std::vector<std::string> HmiEditorWidget::selectedControlIds() const
  963. {
  964. std::vector<std::pair<QPointF, std::string>> positioned_ids;
  965. for (QGraphicsItem *item : scene_->selectedItems())
  966. {
  967. const HmiGraphicsItem *control_item = asHmiItem(item);
  968. if (control_item != nullptr)
  969. {
  970. positioned_ids.emplace_back(
  971. control_item->scenePos(), control_item->controlId());
  972. }
  973. }
  974. std::sort(
  975. positioned_ids.begin(), positioned_ids.end(),
  976. [](const auto &left, const auto &right)
  977. {
  978. if (!qFuzzyCompare(left.first.y(), right.first.y()))
  979. {
  980. return left.first.y() < right.first.y();
  981. }
  982. return left.first.x() < right.first.x();
  983. });
  984. std::vector<std::string> ids;
  985. ids.reserve(positioned_ids.size());
  986. for (const auto &positioned_id : positioned_ids)
  987. {
  988. ids.push_back(positioned_id.second);
  989. }
  990. return ids;
  991. }
  992. void HmiEditorWidget::refreshRuntimeValues()
  993. {
  994. // 运行态才读取寄存器;编辑态画布只显示工程配置,不显示缓存值
  995. if (!runtime_active_)
  996. {
  997. return;
  998. }
  999. for (QGraphicsItem *item : scene_->items())
  1000. {
  1001. HmiGraphicsItem *control_item = asHmiItem(item);
  1002. if (control_item == nullptr)
  1003. {
  1004. continue;
  1005. }
  1006. const HmiControl *control = editor_service_.findControl(
  1007. page_id_, control_item->controlId());
  1008. if (control == nullptr)
  1009. {
  1010. continue;
  1011. }
  1012. if (control->type == HmiControlType::AlarmList)
  1013. {
  1014. control_item->setAlarmRecords(alarm_service_.records());
  1015. continue;
  1016. }
  1017. // 运行值通过服务读取,图元不直接接触寄存器仓库
  1018. const HmiRuntimeReadResult value = runtime_service_.readControl(*control);
  1019. control_item->setRuntimeValue(value.bit_value, value.word_value, value.succeeded);
  1020. }
  1021. }
  1022. void HmiEditorWidget::resizeEvent(QResizeEvent *event)
  1023. {
  1024. QGraphicsView::resizeEvent(event);
  1025. fitCurrentPage();
  1026. }
  1027. void HmiEditorWidget::fitCurrentPage()
  1028. {
  1029. if (scene_->sceneRect().isEmpty())
  1030. {
  1031. return;
  1032. }
  1033. fitInView(
  1034. scene_->sceneRect().adjusted(-24, -24, 24, 24),
  1035. Qt::KeepAspectRatio);
  1036. }
  1037. void HmiEditorWidget::updateItemInteractions()
  1038. {
  1039. // 遍历所有控件图元,统一设置flag
  1040. for (QGraphicsItem *item : scene_->items())
  1041. {
  1042. HmiGraphicsItem *control_item = asHmiItem(item);
  1043. if (control_item != nullptr)
  1044. {
  1045. control_item->setInteractionState(
  1046. editing_enabled_, runtime_active_, runtime_write_enabled_);
  1047. }
  1048. }
  1049. }
  1050. void HmiEditorWidget::handleSelectionChanged()
  1051. {
  1052. emit controlSelected(QString::fromStdString(selectedControlId()));
  1053. }
  1054. void HmiEditorWidget::handleControlMoved(
  1055. const std::string &control_id, const QPointF &position)
  1056. {
  1057. // 拖动结束后把坐标交给服务层,由服务层负责页面边界校验和历史记录
  1058. const HmiControl *control = editor_service_.findControl(page_id_, control_id);
  1059. if (control == nullptr)
  1060. {
  1061. return;
  1062. }
  1063. // 鼠标坐标取整后再交给服务校验并写回模型
  1064. HmiRect bounds = control->bounds;
  1065. bounds.x = static_cast<int>(std::lround(position.x()));
  1066. bounds.y = static_cast<int>(std::lround(position.y()));
  1067. const HmiEditorResult result = editor_service_.moveControl(
  1068. page_id_, control_id, bounds);
  1069. if (!result.succeeded)
  1070. {
  1071. emit editorError(QString::fromStdString(result.message));
  1072. reloadPage();
  1073. return;
  1074. }
  1075. emit controlChanged(QString::fromStdString(control_id));
  1076. }
  1077. void HmiEditorWidget::handleButtonEvent(
  1078. const std::string &control_id, HmiButtonEvent event)
  1079. {
  1080. if (!runtime_active_ || !runtime_write_enabled_)
  1081. {
  1082. return;
  1083. }
  1084. // 运行态按钮事件只通过 HmiRuntimeService 写寄存器,不直接访问仓库
  1085. const HmiControl *control = editor_service_.findControl(page_id_, control_id);
  1086. if (control == nullptr)
  1087. {
  1088. return;
  1089. }
  1090. if (control->type != HmiControlType::Button)
  1091. {
  1092. return;
  1093. }
  1094. const HmiRuntimeWriteResult result = runtime_service_.operateButton(*control, event);
  1095. if (!result.succeeded)
  1096. {
  1097. emit editorError(tr("寄存器操作失败"));
  1098. return;
  1099. }
  1100. refreshRuntimeValues();
  1101. }
  1102. void HmiEditorWidget::handleNumericInputActivated(const std::string &control_id)
  1103. {
  1104. if (!runtime_active_ || !runtime_write_enabled_)
  1105. {
  1106. return;
  1107. }
  1108. const HmiControl *control = editor_service_.findControl(page_id_, control_id);
  1109. if (control == nullptr || control->type != HmiControlType::NumericInput)
  1110. {
  1111. return;
  1112. }
  1113. bool accepted = false;
  1114. const HmiRuntimeReadResult current = runtime_service_.readControl(*control);
  1115. const int initial = current.succeeded ? current.word_value : 0;
  1116. const int value = QInputDialog::getInt(
  1117. this,
  1118. tr("输入数值"),
  1119. QString::fromUtf8(control->text.data(), static_cast<int>(control->text.size())),
  1120. initial,
  1121. std::numeric_limits<std::int16_t>::min(),
  1122. std::numeric_limits<std::int16_t>::max(),
  1123. 1,
  1124. &accepted);
  1125. if (!accepted)
  1126. {
  1127. return;
  1128. }
  1129. const HmiRuntimeWriteResult result = runtime_service_.writeNumericInput(
  1130. *control, static_cast<std::int16_t>(value));
  1131. if (!result.succeeded)
  1132. {
  1133. emit editorError(tr("寄存器操作失败"));
  1134. return;
  1135. }
  1136. refreshRuntimeValues();
  1137. }