#include "hmi_editor_widget.h" #include "services/hmi_editor_service.h" #include "services/hmi_runtime_service.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { // 将领域层 HMI 控件投影为可绘制、可选择和可交互的场景图元 class HmiGraphicsItem final : public QGraphicsItem { public: // 保存控件快照、页面边界和回调,使图元不直接依赖服务层 HmiGraphicsItem( const HmiControl &control, int page_width, int page_height, std::function moved, std::function 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(control_.bounds.width), static_cast(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(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(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(page_width_ - control_.bounds.width)); const qreal maximum_y = std::max( 0.0, static_cast(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(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 moved_; // 运行模式点击按钮 / 双击输入框回调,通知外层做寄存器读写 std::function 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(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 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(std::lround(position.x())); bounds.y = static_cast(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(control->text.size())), initial, std::numeric_limits::min(), std::numeric_limits::max(), 1, &accepted); if (!accepted) { return; } result = runtime_service_.writeNumericInput( *control, static_cast(value)); } else { return; } if (!result.succeeded) { emit editorError(tr("寄存器操作失败")); return; } refreshRuntimeValues(); }