|
- #include "hmi_editor_widget.h"
-
- #include "services/hmi_editor_service.h"
- #include "services/hmi_runtime_service.h"
-
- #include <QGraphicsItem>
- #include <QGraphicsRectItem>
- #include <QGraphicsScene>
- #include <QGraphicsSceneMouseEvent>
- #include <QInputDialog>
- #include <QPainter>
- #include <QResizeEvent>
- #include <QStyleOptionGraphicsItem>
-
- #include <algorithm>
- #include <cmath>
- #include <cstdint>
- #include <functional>
- #include <limits>
-
- namespace {
-
- // 将领域层 HMI 控件投影为可绘制、可选择和可交互的场景图元
- class HmiGraphicsItem final : public QGraphicsItem
- {
- public:
- // 保存控件快照、页面边界和回调,使图元不直接依赖服务层
- HmiGraphicsItem(
- const HmiControl &control,
- int page_width,
- int page_height,
- std::function<void(const std::string &, const QPointF &)> moved,
- std::function<void(const std::string &)> activated)
- : control_(control),
- page_width_(page_width),
- page_height_(page_height),
- moved_(std::move(moved)),
- activated_(std::move(activated))
- {
- // 领域坐标直接作为图元在场景中的初始位置
- setPos(control_.bounds.x, control_.bounds.y);
- // 所有控件均可选中,便于主窗口显示对应属性
- setFlag(ItemIsSelectable, true);
- // 开启可选中
- setFlag(ItemSendsGeometryChanges, true);
- // 画布只处理左键交互,保留其他按键给视图默认行为
- setAcceptedMouseButtons(Qt::LeftButton);
- }
-
- // 返回图元自身坐标系中的矩形范围,用于绘制和命中测试
- QRectF boundingRect() const override
- {
- return {0,
- 0,
- static_cast<qreal>(control_.bounds.width),
- static_cast<qreal>(control_.bounds.height)};
- }
-
- void paint(
- QPainter *painter,
- const QStyleOptionGraphicsItem *option,
- QWidget *) override
- {
- // 留出一个像素边距,避免描边被图元边界裁剪
- const QRectF rect = boundingRect().adjusted(1, 1, -1, -1);
- painter->setRenderHint(QPainter::Antialiasing, true);
- painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1));
-
- switch (control_.type)
- {
- case HmiControlType::Button:
- {
- // 按钮显示文字,运行态点击行为由鼠标事件处理
- painter->setBrush(QColor(QStringLiteral("#dcece3")));
- painter->drawRoundedRect(rect, 4, 4);
- painter->setPen(QColor(QStringLiteral("#205c3b")));
- painter->drawText(rect, Qt::AlignCenter, textWithValue());
- break;
- }
- case HmiControlType::Indicator:
- {
- // 指示灯颜色由最近一次读取到的 M 位值决定
- const qreal diameter = std::min(rect.width(), rect.height() - 18.0);
- const QRectF lamp(
- rect.center().x() - diameter / 2.0,
- rect.top() + 2,
- diameter,
- diameter);
- painter->setBrush(bit_value_ ? QColor(QStringLiteral("#24a148"))
- : QColor(QStringLiteral("#b8c1c8")));
- painter->drawEllipse(lamp);
- painter->setPen(QColor(QStringLiteral("#24313b")));
- painter->drawText(
- QRectF(rect.left(), lamp.bottom() + 1, rect.width(), 16),
- Qt::AlignCenter,
- QString::fromUtf8(control_.text.data(),
- static_cast<int>(control_.text.size())));
- break;
- }
- case HmiControlType::NumericDisplay:
- {
- // 数值显示为只读样式,文本由运行值刷新
- painter->setBrush(QColor(QStringLiteral("#edf2f6")));
- painter->drawRect(rect);
- painter->setPen(QColor(QStringLiteral("#24313b")));
- painter->drawText(rect.adjusted(7, 0, -7, 0),
- Qt::AlignVCenter | Qt::AlignLeft,
- textWithValue());
- break;
- }
- case HmiControlType::NumericInput:
- {
- // 数值输入以白色编辑框样式呈现,双击后才请求写入
- painter->setBrush(QColor(QStringLiteral("#ffffff")));
- painter->drawRoundedRect(rect, 3, 3);
- painter->setPen(QColor(QStringLiteral("#24313b")));
- painter->drawText(rect.adjusted(7, 0, -7, 0),
- Qt::AlignVCenter | Qt::AlignLeft,
- textWithValue());
- break;
- }
- case HmiControlType::Label:
- default:
- {
- // 标签只显示固定文本,不绑定寄存器运行值
- painter->setPen(QColor(QStringLiteral("#24313b")));
- painter->drawText(rect, Qt::AlignCenter,
- QString::fromUtf8(control_.text.data(),
- static_cast<int>(control_.text.size())));
- break;
- }
- }
-
- if ((option->state & QStyle::State_Selected) != 0)
- {
- // 选中框独立于控件类型,提示当前可编辑对象
- painter->setBrush(Qt::NoBrush);
- painter->setPen(QPen(QColor(QStringLiteral("#1677a8")), 2));
- painter->drawRect(boundingRect().adjusted(0, 0, -1, -1));
- }
- }
-
- // 向场景和外部控件返回该图元对应的领域控件标识
- const std::string &controlId() const
- {
- return control_.id;
- }
-
- // 编辑态允许拖动,运行态保留点击或双击交互
- void setInteractionEnabled(bool editable)
- {
- setFlag(ItemIsMovable, editable);
- }
-
- // 缓存运行服务读出的值并触发 Qt 重绘
- void setRuntimeValue(bool bit_value, std::int16_t word_value, bool available)
- {
- bit_value_ = bit_value;
- word_value_ = word_value;
- has_runtime_value_ = available;
- update();
- }
-
- protected:
- // 拖拽过程中将新位置限制在页面可见边界内
- QVariant itemChange(GraphicsItemChange change, const QVariant &value) override
- {
- // 只有开启 ItemIsMovable 拖拽时,才做坐标钳位
- if (change == ItemPositionChange && flags().testFlag(ItemIsMovable))
- {
- // 在图元层预先截断拖拽坐标,避免控件视觉上越出页面
- QPointF position = value.toPointF();
- const qreal maximum_x = std::max(
- 0.0, static_cast<double>(page_width_ - control_.bounds.width));
- const qreal maximum_y = std::max(
- 0.0, static_cast<double>(page_height_ - control_.bounds.height));
- position.setX(std::clamp(position.x(), 0.0, maximum_x));
- position.setY(std::clamp(position.y(), 0.0, maximum_y));
- return position;
- }
- return QGraphicsItem::itemChange(change, value);
- }
-
- // 运行态点击按钮时通知外层执行 M 位切换
- void mousePressEvent(QGraphicsSceneMouseEvent *event) override
- {
- // 条件:!ItemIsMovable 【也就是运行模式】 + 是按钮 + 有回调
- if (!flags().testFlag(ItemIsMovable)
- && control_.type == HmiControlType::Button && activated_)
- {
- // 运行态单击按钮才触发 M 位切换,编辑态只用于选择和拖动
- setSelected(true);
- activated_(control_.id);
- event->accept();
- return;
- }
- // 编辑模式:不进if分支,执行基类事件——只做选中、拖拽
- QGraphicsItem::mousePressEvent(event);
- }
-
- // 运行态双击数值输入时通知外层弹出数值编辑对话框
- void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override
- {
- if (!flags().testFlag(ItemIsMovable)
- && control_.type == HmiControlType::NumericInput && activated_)
- {
- // 数值输入使用双击,避免普通选择操作意外写入 D 字
- setSelected(true);
- activated_(control_.id);
- event->accept();
- return;
- }
- QGraphicsItem::mouseDoubleClickEvent(event);
- }
-
- // 拖拽结束后才提交最终坐标,避免移动过程频繁修改领域模型
- void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override
- {
- QGraphicsItem::mouseReleaseEvent(event);
- // 只有ItemIsMovable打开(编辑态),松开鼠标才提交位置给业务层
- if (flags().testFlag(ItemIsMovable) && moved_)
- {
- moved_(control_.id, pos());
- }
- }
-
- private:
- // 根据控件类型和运行数据组合当前应绘制的文字
- QString textWithValue() const
- {
- const QString text = QString::fromUtf8(
- control_.text.data(), static_cast<int>(control_.text.size()));
- if (!has_runtime_value_ || control_.type == HmiControlType::Button)
- {
- return text;
- }
- if (control_.type == HmiControlType::NumericDisplay
- || control_.type == HmiControlType::NumericInput)
- {
- return text + QStringLiteral(": ") + QString::number(word_value_);
- }
- return text;
- }
-
- // 图元创建时的领域控件快照,提供类型、尺寸、文本和标识
- HmiControl control_;
- // 当前页面宽度,用于限制图元横向拖拽范围
- int page_width_ = 0;
- // 当前页面高度,用于限制图元纵向拖拽范围
- int page_height_ = 0;
- // 拖拽完成后回调 HmiEditorWidget 提交控件新位置
- std::function<void(const std::string &, const QPointF &)> moved_;
- // 运行模式点击按钮 / 双击输入框回调,通知外层做寄存器读写
- std::function<void(const std::string &)> activated_;
- // 指示灯读取到的 M 位值
- bool bit_value_ = false;
- // 数值控件读取到的 D 字值
- std::int16_t word_value_ = 0;
- // 标记当前缓存值是否来自一次成功的运行时读取
- bool has_runtime_value_ = false;
- };
-
- // 将场景通用图元安全转换为本文件定义的 HMI 控件图元
- HmiGraphicsItem *asHmiItem(QGraphicsItem *item)
- {
- return dynamic_cast<HmiGraphicsItem *>(item);
- }
-
- } // namespace
-
- HmiEditorWidget::HmiEditorWidget(
- HmiEditorService &editor_service,
- HmiRuntimeService &runtime_service,
- QWidget *parent)
- : QGraphicsView(parent),
- editor_service_(editor_service),
- runtime_service_(runtime_service),
- scene_(new QGraphicsScene(this))
- {
- setObjectName(QStringLiteral("hmiEditorWidget"));
- setScene(scene_);
- setRenderHint(QPainter::Antialiasing, true);
- setDragMode(QGraphicsView::RubberBandDrag);
- setBackgroundBrush(QColor(QStringLiteral("#dfe5e9")));
- connect(scene_, &QGraphicsScene::selectionChanged,
- this, &HmiEditorWidget::handleSelectionChanged);
- }
-
- // 切换显示页面
- void HmiEditorWidget::setPageId(const std::string &page_id)
- {
- if (page_id_ == page_id)
- {
- return;
- }
- page_id_ = page_id;
- reloadPage();
- }
-
- void HmiEditorWidget::setEditingEnabled(bool enabled)
- {
- editing_enabled_ = enabled;
- setDragMode(enabled ? QGraphicsView::RubberBandDrag : QGraphicsView::NoDrag);
- updateItemInteractions();
- }
-
- void HmiEditorWidget::setRuntimeActive(bool active)
- {
- runtime_active_ = active;
- if (!runtime_active_)
- {
- for (QGraphicsItem *item : scene_->items())
- {
- HmiGraphicsItem *control_item = asHmiItem(item);
- if (control_item != nullptr)
- {
- control_item->setRuntimeValue(false, 0, false);
- }
- }
- }
- }
-
- // 全部重新加载当前页面,把内存模型 HmiPage 渲染成画面上图形
- void HmiEditorWidget::reloadPage()
- {
- // 画布始终从当前领域页面重建,避免保留已删除控件的图元
- scene_->clear();
- const HmiPage *page = editor_service_.findPage(page_id_);
- if (page == nullptr)
- {
- scene_->setSceneRect({});
- emit controlSelected({});
- return;
- }
-
- scene_->setSceneRect(0, 0, page->width, page->height);
- QGraphicsRectItem *page_border = scene_->addRect(
- scene_->sceneRect(), QPen(QColor(QStringLiteral("#8a98a3")), 1), Qt::white);
- page_border->setZValue(-1);
- page_border->setAcceptedMouseButtons(Qt::NoButton);
-
- for (const HmiControl &control : page->controls)
- {
- auto *item = new HmiGraphicsItem(
- control,
- page->width,
- page->height,
- [this](const std::string &control_id, const QPointF &position)
- {
- handleControlMoved(control_id, position);
- },
- [this](const std::string &control_id)
- {
- handleControlActivated(control_id);
- });
- scene_->addItem(item);
- item->setInteractionEnabled(editing_enabled_);
- }
- fitCurrentPage();
- refreshRuntimeValues();
- }
-
- // 遍历场景所有图元,找到对应 id 的图元,设置选中,视图滚动到把控件显示出来
- void HmiEditorWidget::selectControl(const std::string &control_id)
- {
- for (QGraphicsItem *item : scene_->items())
- {
- HmiGraphicsItem *control_item = asHmiItem(item);
- if (control_item != nullptr && control_item->controlId() == control_id)
- {
- control_item->setSelected(true);
- ensureVisible(control_item);
- return;
- }
- }
- }
-
- std::string HmiEditorWidget::selectedControlId() const
- {
- const QList<QGraphicsItem *> selected = scene_->selectedItems();
- for (QGraphicsItem *item : selected)
- {
- const HmiGraphicsItem *control_item = asHmiItem(item);
- if (control_item != nullptr)
- {
- return control_item->controlId();
- }
- }
- return {};
- }
-
- void HmiEditorWidget::refreshRuntimeValues()
- {
- if (!runtime_active_)
- {
- return;
- }
- for (QGraphicsItem *item : scene_->items())
- {
- HmiGraphicsItem *control_item = asHmiItem(item);
- if (control_item == nullptr)
- {
- continue;
- }
- const HmiControl *control = editor_service_.findControl(
- page_id_, control_item->controlId());
- if (control == nullptr)
- {
- continue;
- }
- // 运行值通过服务读取,图元不直接接触寄存器仓库
- const HmiRuntimeReadResult value = runtime_service_.readControl(*control);
- control_item->setRuntimeValue(value.bit_value, value.word_value, value.succeeded);
- }
- }
-
- void HmiEditorWidget::resizeEvent(QResizeEvent *event)
- {
- QGraphicsView::resizeEvent(event);
- fitCurrentPage();
- }
-
- void HmiEditorWidget::fitCurrentPage()
- {
- if (scene_->sceneRect().isEmpty())
- {
- return;
- }
- fitInView(
- scene_->sceneRect().adjusted(-16, -16, 16, 16),
- Qt::KeepAspectRatio);
- }
-
- void HmiEditorWidget::updateItemInteractions()
- {
- // 遍历所有控件图元,统一设置flag
- for (QGraphicsItem *item : scene_->items())
- {
- HmiGraphicsItem *control_item = asHmiItem(item);
- if (control_item != nullptr)
- {
- control_item->setInteractionEnabled(editing_enabled_);
- }
- }
- }
-
- void HmiEditorWidget::handleSelectionChanged()
- {
- emit controlSelected(QString::fromStdString(selectedControlId()));
- }
-
- void HmiEditorWidget::handleControlMoved(
- const std::string &control_id, const QPointF &position)
- {
- const HmiControl *control = editor_service_.findControl(page_id_, control_id);
- if (control == nullptr)
- {
- return;
- }
- // 鼠标坐标取整后再交给服务校验并写回模型
- HmiRect bounds = control->bounds;
- bounds.x = static_cast<int>(std::lround(position.x()));
- bounds.y = static_cast<int>(std::lround(position.y()));
- const HmiEditorResult result = editor_service_.moveControl(
- page_id_, control_id, bounds);
- if (!result.succeeded)
- {
- emit editorError(QString::fromStdString(result.message));
- reloadPage();
- return;
- }
- emit controlChanged(QString::fromStdString(control_id));
- }
-
- void HmiEditorWidget::handleControlActivated(const std::string &control_id)
- {
- if (!runtime_active_)
- {
- return;
- }
- const HmiControl *control = editor_service_.findControl(page_id_, control_id);
- if (control == nullptr)
- {
- return;
- }
- HmiRuntimeWriteResult result;
- // 只有可写控件允许激活,其余控件只展示最新运行值
- if (control->type == HmiControlType::Button)
- {
- result = runtime_service_.toggleButton(*control);
- }
- else if (control->type == HmiControlType::NumericInput)
- {
- bool accepted = false;
- const HmiRuntimeReadResult current = runtime_service_.readControl(*control);
- const int initial = current.succeeded ? current.word_value : 0;
- const int value = QInputDialog::getInt(
- this,
- tr("输入数值"),
- QString::fromUtf8(control->text.data(), static_cast<int>(control->text.size())),
- initial,
- std::numeric_limits<std::int16_t>::min(),
- std::numeric_limits<std::int16_t>::max(),
- 1,
- &accepted);
- if (!accepted)
- {
- return;
- }
- result = runtime_service_.writeNumericInput(
- *control, static_cast<std::int16_t>(value));
- }
- else
- {
- return;
- }
- if (!result.succeeded)
- {
- emit editorError(tr("寄存器操作失败"));
- return;
- }
- refreshRuntimeValues();
- }
|