综合平台编程器项目的远程存储
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

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