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

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