综合平台编程器项目的远程存储
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 

570 linhas
19 KiB

  1. #include "hmi_editor_widget.h"
  2. #include "services/hmi_editor_service.h"
  3. #include "services/hmi_runtime_service.h"
  4. #include <QGraphicsItem>
  5. #include <QGraphicsRectItem>
  6. #include <QGraphicsScene>
  7. #include <QGraphicsSceneMouseEvent>
  8. #include <QInputDialog>
  9. #include <QPainter>
  10. #include <QResizeEvent>
  11. #include <QStyleOptionGraphicsItem>
  12. #include <algorithm>
  13. #include <cmath>
  14. #include <cstdint>
  15. #include <functional>
  16. #include <limits>
  17. namespace {
  18. // 将领域层 HMI 控件投影为可绘制、可选择和可交互的场景图元
  19. class HmiGraphicsItem final : public QGraphicsItem
  20. {
  21. public:
  22. // 保存控件快照、页面边界和回调,使图元不直接依赖服务层
  23. HmiGraphicsItem(
  24. const HmiControl &control,
  25. int page_width,
  26. int page_height,
  27. std::function<void(const std::string &, const QPointF &)> moved,
  28. std::function<void(const std::string &)> activated)
  29. : control_(control),
  30. page_width_(page_width),
  31. page_height_(page_height),
  32. moved_(std::move(moved)),
  33. activated_(std::move(activated))
  34. {
  35. // 领域坐标直接作为图元在场景中的初始位置
  36. setPos(control_.bounds.x, control_.bounds.y);
  37. // 所有控件均可选中,便于主窗口显示对应属性
  38. setFlag(ItemIsSelectable, true);
  39. // 开启可选中
  40. setFlag(ItemSendsGeometryChanges, true);
  41. // 画布只处理左键交互,保留其他按键给视图默认行为
  42. setAcceptedMouseButtons(Qt::LeftButton);
  43. }
  44. // 返回图元自身坐标系中的矩形范围,用于绘制和命中测试
  45. QRectF boundingRect() const override
  46. {
  47. if (!control_.binding.has_value())
  48. {
  49. return controlRect();
  50. }
  51. return controlRect().united(addressRect());
  52. }
  53. void paint(
  54. QPainter *painter,
  55. const QStyleOptionGraphicsItem *option,
  56. QWidget *) override
  57. {
  58. // 留出一个像素边距,避免描边被图元边界裁剪
  59. const QRectF rect = controlRect().adjusted(1, 1, -1, -1);
  60. painter->setRenderHint(QPainter::Antialiasing, true);
  61. painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1));
  62. if (control_.binding.has_value())
  63. {
  64. const QFont original_font = painter->font();
  65. QFont address_font = original_font;
  66. address_font.setPixelSize(11);
  67. painter->setFont(address_font);
  68. painter->setPen(QColor(QStringLiteral("#5f6b73")));
  69. painter->drawText(addressRect(), Qt::AlignCenter, bindingText());
  70. painter->setFont(original_font);
  71. painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1));
  72. }
  73. switch (control_.type)
  74. {
  75. case HmiControlType::Button:
  76. {
  77. // 按钮显示文字,运行态点击行为由鼠标事件处理
  78. painter->setBrush(QColor(QStringLiteral("#dcece3")));
  79. painter->drawRoundedRect(rect, 4, 4);
  80. painter->setPen(QColor(QStringLiteral("#205c3b")));
  81. painter->drawText(rect, Qt::AlignCenter, textWithValue());
  82. break;
  83. }
  84. case HmiControlType::Indicator:
  85. {
  86. // 指示灯颜色由最近一次读取到的 M 位值决定
  87. const qreal diameter = std::min(rect.width(), rect.height() - 18.0);
  88. const QRectF lamp(
  89. rect.center().x() - diameter / 2.0,
  90. rect.top() + 2,
  91. diameter,
  92. diameter);
  93. painter->setBrush(bit_value_ ? QColor(QStringLiteral("#24a148"))
  94. : QColor(QStringLiteral("#b8c1c8")));
  95. painter->drawEllipse(lamp);
  96. painter->setPen(QColor(QStringLiteral("#24313b")));
  97. painter->drawText(
  98. QRectF(rect.left(), lamp.bottom() + 1, rect.width(), 16),
  99. Qt::AlignCenter,
  100. QString::fromUtf8(control_.text.data(),
  101. static_cast<int>(control_.text.size())));
  102. break;
  103. }
  104. case HmiControlType::NumericDisplay:
  105. {
  106. // 数值显示为只读样式,文本由运行值刷新
  107. painter->setBrush(QColor(QStringLiteral("#edf2f6")));
  108. painter->drawRect(rect);
  109. painter->setPen(QColor(QStringLiteral("#24313b")));
  110. painter->drawText(rect.adjusted(7, 0, -7, 0),
  111. Qt::AlignVCenter | Qt::AlignLeft,
  112. textWithValue());
  113. break;
  114. }
  115. case HmiControlType::NumericInput:
  116. {
  117. // 数值输入以白色编辑框样式呈现,双击后才请求写入
  118. painter->setBrush(QColor(QStringLiteral("#ffffff")));
  119. painter->drawRoundedRect(rect, 3, 3);
  120. painter->setPen(QColor(QStringLiteral("#24313b")));
  121. painter->drawText(rect.adjusted(7, 0, -7, 0),
  122. Qt::AlignVCenter | Qt::AlignLeft,
  123. textWithValue());
  124. break;
  125. }
  126. case HmiControlType::Label:
  127. default:
  128. {
  129. // 标签只显示固定文本,不绑定寄存器运行值
  130. painter->setPen(QColor(QStringLiteral("#24313b")));
  131. painter->drawText(rect, Qt::AlignCenter,
  132. QString::fromUtf8(control_.text.data(),
  133. static_cast<int>(control_.text.size())));
  134. break;
  135. }
  136. }
  137. if ((option->state & QStyle::State_Selected) != 0)
  138. {
  139. // 选中框独立于控件类型,提示当前可编辑对象
  140. painter->setBrush(Qt::NoBrush);
  141. painter->setPen(QPen(QColor(QStringLiteral("#1677a8")), 2));
  142. painter->drawRect(controlRect().adjusted(0, 0, -1, -1));
  143. }
  144. }
  145. // 向场景和外部控件返回该图元对应的领域控件标识
  146. const std::string &controlId() const
  147. {
  148. return control_.id;
  149. }
  150. // 编辑态允许拖动,运行态保留点击或双击交互
  151. void setInteractionEnabled(bool editable)
  152. {
  153. setFlag(ItemIsMovable, editable);
  154. }
  155. // 缓存运行服务读出的值并触发 Qt 重绘
  156. void setRuntimeValue(bool bit_value, std::int16_t word_value, bool available)
  157. {
  158. bit_value_ = bit_value;
  159. word_value_ = word_value;
  160. has_runtime_value_ = available;
  161. update();
  162. }
  163. protected:
  164. // 拖拽过程中将新位置限制在页面可见边界内
  165. QVariant itemChange(GraphicsItemChange change, const QVariant &value) override
  166. {
  167. // 只有开启 ItemIsMovable 拖拽时,才做坐标钳位
  168. if (change == ItemPositionChange && flags().testFlag(ItemIsMovable))
  169. {
  170. // 在图元层预先截断拖拽坐标,避免控件视觉上越出页面
  171. QPointF position = value.toPointF();
  172. const qreal maximum_x = std::max(
  173. 0.0, static_cast<double>(page_width_ - control_.bounds.width));
  174. const qreal maximum_y = std::max(
  175. 0.0, static_cast<double>(page_height_ - control_.bounds.height));
  176. position.setX(std::clamp(position.x(), 0.0, maximum_x));
  177. position.setY(std::clamp(position.y(), 0.0, maximum_y));
  178. return position;
  179. }
  180. return QGraphicsItem::itemChange(change, value);
  181. }
  182. // 运行态点击按钮时通知外层执行 M 位切换
  183. void mousePressEvent(QGraphicsSceneMouseEvent *event) override
  184. {
  185. // 条件:!ItemIsMovable 【也就是运行模式】 + 是按钮 + 有回调
  186. if (!flags().testFlag(ItemIsMovable)
  187. && control_.type == HmiControlType::Button && activated_)
  188. {
  189. // 运行态单击按钮才触发 M 位切换,编辑态只用于选择和拖动
  190. setSelected(true);
  191. activated_(control_.id);
  192. event->accept();
  193. return;
  194. }
  195. // 编辑模式:不进if分支,执行基类事件——只做选中、拖拽
  196. QGraphicsItem::mousePressEvent(event);
  197. }
  198. // 运行态双击数值输入时通知外层弹出数值编辑对话框
  199. void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override
  200. {
  201. if (!flags().testFlag(ItemIsMovable)
  202. && control_.type == HmiControlType::NumericInput && activated_)
  203. {
  204. // 数值输入使用双击,避免普通选择操作意外写入 D 字
  205. setSelected(true);
  206. activated_(control_.id);
  207. event->accept();
  208. return;
  209. }
  210. QGraphicsItem::mouseDoubleClickEvent(event);
  211. }
  212. // 拖拽结束后才提交最终坐标,避免移动过程频繁修改领域模型
  213. void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override
  214. {
  215. QGraphicsItem::mouseReleaseEvent(event);
  216. // 只有ItemIsMovable打开(编辑态),松开鼠标才提交位置给业务层
  217. if (flags().testFlag(ItemIsMovable) && moved_)
  218. {
  219. moved_(control_.id, pos());
  220. }
  221. }
  222. private:
  223. // 控件本体范围保持领域坐标和尺寸语义不变
  224. QRectF controlRect() const
  225. {
  226. return {0,
  227. 0,
  228. static_cast<qreal>(control_.bounds.width),
  229. static_cast<qreal>(control_.bounds.height)};
  230. }
  231. // 地址作为控件的附属信息显示在本体上方
  232. QRectF addressRect() const
  233. {
  234. constexpr qreal address_height = 16.0;
  235. constexpr qreal address_spacing = 2.0;
  236. return {0,
  237. -address_height - address_spacing,
  238. static_cast<qreal>(control_.bounds.width),
  239. address_height};
  240. }
  241. QString bindingText() const
  242. {
  243. return control_.binding.has_value()
  244. ? QString::fromStdString(control_.binding->toString())
  245. : QString{};
  246. }
  247. // 根据控件类型和运行数据组合当前应绘制的文字
  248. QString textWithValue() const
  249. {
  250. const QString text = QString::fromUtf8(
  251. control_.text.data(), static_cast<int>(control_.text.size()));
  252. if (!has_runtime_value_ || control_.type == HmiControlType::Button)
  253. {
  254. return text;
  255. }
  256. if (control_.type == HmiControlType::NumericDisplay
  257. || control_.type == HmiControlType::NumericInput)
  258. {
  259. return text + QStringLiteral(": ") + QString::number(word_value_);
  260. }
  261. return text;
  262. }
  263. // 图元创建时的领域控件快照,提供类型、尺寸、文本和标识
  264. HmiControl control_;
  265. // 当前页面宽度,用于限制图元横向拖拽范围
  266. int page_width_ = 0;
  267. // 当前页面高度,用于限制图元纵向拖拽范围
  268. int page_height_ = 0;
  269. // 拖拽完成后回调 HmiEditorWidget 提交控件新位置
  270. std::function<void(const std::string &, const QPointF &)> moved_;
  271. // 运行模式点击按钮 / 双击输入框回调,通知外层做寄存器读写
  272. std::function<void(const std::string &)> activated_;
  273. // 指示灯读取到的 M 位值
  274. bool bit_value_ = false;
  275. // 数值控件读取到的 D 字值
  276. std::int16_t word_value_ = 0;
  277. // 标记当前缓存值是否来自一次成功的运行时读取
  278. bool has_runtime_value_ = false;
  279. };
  280. // 将场景通用图元安全转换为本文件定义的 HMI 控件图元
  281. HmiGraphicsItem *asHmiItem(QGraphicsItem *item)
  282. {
  283. return dynamic_cast<HmiGraphicsItem *>(item);
  284. }
  285. } // namespace
  286. HmiEditorWidget::HmiEditorWidget(
  287. HmiEditorService &editor_service,
  288. HmiRuntimeService &runtime_service,
  289. QWidget *parent)
  290. : QGraphicsView(parent),
  291. editor_service_(editor_service),
  292. runtime_service_(runtime_service),
  293. scene_(new QGraphicsScene(this))
  294. {
  295. setObjectName(QStringLiteral("hmiEditorWidget"));
  296. setScene(scene_);
  297. setRenderHint(QPainter::Antialiasing, true);
  298. setDragMode(QGraphicsView::RubberBandDrag);
  299. setBackgroundBrush(QColor(QStringLiteral("#dfe5e9")));
  300. connect(scene_, &QGraphicsScene::selectionChanged,
  301. this, &HmiEditorWidget::handleSelectionChanged);
  302. }
  303. // 切换显示页面
  304. void HmiEditorWidget::setPageId(const std::string &page_id)
  305. {
  306. if (page_id_ == page_id)
  307. {
  308. return;
  309. }
  310. page_id_ = page_id;
  311. reloadPage();
  312. }
  313. void HmiEditorWidget::setEditingEnabled(bool enabled)
  314. {
  315. editing_enabled_ = enabled;
  316. setDragMode(enabled ? QGraphicsView::RubberBandDrag : QGraphicsView::NoDrag);
  317. updateItemInteractions();
  318. }
  319. void HmiEditorWidget::setRuntimeActive(bool active)
  320. {
  321. runtime_active_ = active;
  322. if (!runtime_active_)
  323. {
  324. runtime_write_enabled_ = false;
  325. for (QGraphicsItem *item : scene_->items())
  326. {
  327. HmiGraphicsItem *control_item = asHmiItem(item);
  328. if (control_item != nullptr)
  329. {
  330. control_item->setRuntimeValue(false, 0, false);
  331. }
  332. }
  333. }
  334. }
  335. void HmiEditorWidget::setRuntimeWriteEnabled(bool enabled)
  336. {
  337. runtime_write_enabled_ = runtime_active_ && enabled;
  338. }
  339. // 全部重新加载当前页面,把内存模型 HmiPage 渲染成画面上图形
  340. void HmiEditorWidget::reloadPage()
  341. {
  342. // 画布始终从当前领域页面重建,避免保留已删除控件的图元
  343. scene_->clear();
  344. const HmiPage *page = editor_service_.findPage(page_id_);
  345. if (page == nullptr)
  346. {
  347. scene_->setSceneRect({});
  348. emit controlSelected({});
  349. return;
  350. }
  351. scene_->setSceneRect(0, 0, page->width, page->height);
  352. QGraphicsRectItem *page_border = scene_->addRect(
  353. scene_->sceneRect(), QPen(QColor(QStringLiteral("#8a98a3")), 1), Qt::white);
  354. page_border->setZValue(-1);
  355. page_border->setAcceptedMouseButtons(Qt::NoButton);
  356. for (const HmiControl &control : page->controls)
  357. {
  358. auto *item = new HmiGraphicsItem(
  359. control,
  360. page->width,
  361. page->height,
  362. [this](const std::string &control_id, const QPointF &position)
  363. {
  364. handleControlMoved(control_id, position);
  365. },
  366. [this](const std::string &control_id)
  367. {
  368. handleControlActivated(control_id);
  369. });
  370. scene_->addItem(item);
  371. item->setInteractionEnabled(editing_enabled_);
  372. }
  373. fitCurrentPage();
  374. refreshRuntimeValues();
  375. }
  376. // 遍历场景所有图元,找到对应 id 的图元,设置选中,视图滚动到把控件显示出来
  377. void HmiEditorWidget::selectControl(const std::string &control_id)
  378. {
  379. for (QGraphicsItem *item : scene_->items())
  380. {
  381. HmiGraphicsItem *control_item = asHmiItem(item);
  382. if (control_item != nullptr && control_item->controlId() == control_id)
  383. {
  384. control_item->setSelected(true);
  385. ensureVisible(control_item);
  386. return;
  387. }
  388. }
  389. }
  390. std::string HmiEditorWidget::selectedControlId() const
  391. {
  392. const QList<QGraphicsItem *> selected = scene_->selectedItems();
  393. for (QGraphicsItem *item : selected)
  394. {
  395. const HmiGraphicsItem *control_item = asHmiItem(item);
  396. if (control_item != nullptr)
  397. {
  398. return control_item->controlId();
  399. }
  400. }
  401. return {};
  402. }
  403. void HmiEditorWidget::refreshRuntimeValues()
  404. {
  405. if (!runtime_active_)
  406. {
  407. return;
  408. }
  409. for (QGraphicsItem *item : scene_->items())
  410. {
  411. HmiGraphicsItem *control_item = asHmiItem(item);
  412. if (control_item == nullptr)
  413. {
  414. continue;
  415. }
  416. const HmiControl *control = editor_service_.findControl(
  417. page_id_, control_item->controlId());
  418. if (control == nullptr)
  419. {
  420. continue;
  421. }
  422. // 运行值通过服务读取,图元不直接接触寄存器仓库
  423. const HmiRuntimeReadResult value = runtime_service_.readControl(*control);
  424. control_item->setRuntimeValue(value.bit_value, value.word_value, value.succeeded);
  425. }
  426. }
  427. void HmiEditorWidget::resizeEvent(QResizeEvent *event)
  428. {
  429. QGraphicsView::resizeEvent(event);
  430. fitCurrentPage();
  431. }
  432. void HmiEditorWidget::fitCurrentPage()
  433. {
  434. if (scene_->sceneRect().isEmpty())
  435. {
  436. return;
  437. }
  438. fitInView(
  439. scene_->sceneRect().adjusted(-24, -24, 24, 24),
  440. Qt::KeepAspectRatio);
  441. }
  442. void HmiEditorWidget::updateItemInteractions()
  443. {
  444. // 遍历所有控件图元,统一设置flag
  445. for (QGraphicsItem *item : scene_->items())
  446. {
  447. HmiGraphicsItem *control_item = asHmiItem(item);
  448. if (control_item != nullptr)
  449. {
  450. control_item->setInteractionEnabled(editing_enabled_);
  451. }
  452. }
  453. }
  454. void HmiEditorWidget::handleSelectionChanged()
  455. {
  456. emit controlSelected(QString::fromStdString(selectedControlId()));
  457. }
  458. void HmiEditorWidget::handleControlMoved(
  459. const std::string &control_id, const QPointF &position)
  460. {
  461. const HmiControl *control = editor_service_.findControl(page_id_, control_id);
  462. if (control == nullptr)
  463. {
  464. return;
  465. }
  466. // 鼠标坐标取整后再交给服务校验并写回模型
  467. HmiRect bounds = control->bounds;
  468. bounds.x = static_cast<int>(std::lround(position.x()));
  469. bounds.y = static_cast<int>(std::lround(position.y()));
  470. const HmiEditorResult result = editor_service_.moveControl(
  471. page_id_, control_id, bounds);
  472. if (!result.succeeded)
  473. {
  474. emit editorError(QString::fromStdString(result.message));
  475. reloadPage();
  476. return;
  477. }
  478. emit controlChanged(QString::fromStdString(control_id));
  479. }
  480. void HmiEditorWidget::handleControlActivated(const std::string &control_id)
  481. {
  482. if (!runtime_active_ || !runtime_write_enabled_)
  483. {
  484. return;
  485. }
  486. const HmiControl *control = editor_service_.findControl(page_id_, control_id);
  487. if (control == nullptr)
  488. {
  489. return;
  490. }
  491. HmiRuntimeWriteResult result;
  492. // 只有可写控件允许激活,其余控件只展示最新运行值
  493. if (control->type == HmiControlType::Button)
  494. {
  495. result = runtime_service_.toggleButton(*control);
  496. }
  497. else if (control->type == HmiControlType::NumericInput)
  498. {
  499. bool accepted = false;
  500. const HmiRuntimeReadResult current = runtime_service_.readControl(*control);
  501. const int initial = current.succeeded ? current.word_value : 0;
  502. const int value = QInputDialog::getInt(
  503. this,
  504. tr("输入数值"),
  505. QString::fromUtf8(control->text.data(), static_cast<int>(control->text.size())),
  506. initial,
  507. std::numeric_limits<std::int16_t>::min(),
  508. std::numeric_limits<std::int16_t>::max(),
  509. 1,
  510. &accepted);
  511. if (!accepted)
  512. {
  513. return;
  514. }
  515. result = runtime_service_.writeNumericInput(
  516. *control, static_cast<std::int16_t>(value));
  517. }
  518. else
  519. {
  520. return;
  521. }
  522. if (!result.succeeded)
  523. {
  524. emit editorError(tr("寄存器操作失败"));
  525. return;
  526. }
  527. refreshRuntimeValues();
  528. }