综合平台编程器项目的远程存储
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

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