#include "hmi_editor_widget.h" #include "services/hmi_editor_service.h" #include "services/hmi_runtime_service.h" #include "services/alarm_service.h" #include "domain/project_limits.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr qreal kAlarmHeaderHeight = 24.0; constexpr qreal kAlarmRowHeight = 22.0; constexpr qreal kAlarmCellLeftPadding = 5.0; constexpr qreal kAlarmCellRightPadding = 5.0; constexpr qreal kAlarmColumnSpacing = 3.0; constexpr qreal kAlarmTimeColumnWidth = 76.0; constexpr qreal kAlarmStateColumnWidth = 52.0; constexpr qreal kAlarmPageButtonSize = 16.0; constexpr qreal kAlarmPageIndicatorWidth = 34.0; QString alarmTimeText(const std::chrono::system_clock::time_point &time) { const auto seconds = std::chrono::duration_cast( time.time_since_epoch()).count(); return QDateTime::fromSecsSinceEpoch(seconds).toString(QStringLiteral("HH:mm:ss")); } // 将领域层 HMI 控件投影为可绘制、可选择和可交互的场景图元 class HmiGraphicsItem final : public QGraphicsItem { public: // 保存控件快照、页面边界和回调,使图元不直接依赖服务层 HmiGraphicsItem( const HmiControl &control, int page_width, int page_height, std::function moved, std::function button_event, std::function numeric_input_activated, std::function page_navigation, std::function alarm_acknowledge) : control_(control), page_width_(page_width), page_height_(page_height), moved_(std::move(moved)), button_event_(std::move(button_event)), numeric_input_activated_(std::move(numeric_input_activated)), page_navigation_(std::move(page_navigation)), alarm_acknowledge_(std::move(alarm_acknowledge)) { // 领域坐标直接作为图元在场景中的初始位置 setPos(control_.bounds.x, control_.bounds.y); // 所有控件均可选中,便于主窗口显示对应属性 setFlag(ItemIsSelectable, true); // 开启可选中 setFlag(ItemSendsGeometryChanges, true); // 画布只处理左键交互,保留其他按键给视图默认行为 setAcceptedMouseButtons(Qt::LeftButton); } // 返回图元自身坐标系中的矩形范围,用于绘制和命中测试 QRectF boundingRect() const override { if (!control_.binding.has_value()) { return visibleControlRect(); } return visibleControlRect().united(addressRect()); } void paint( QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) override { // 留出一个像素边距,避免描边被图元边界裁剪 const QRectF rect = visibleControlRect().adjusted(1, 1, -1, -1); painter->setRenderHint(QPainter::Antialiasing, true); painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1)); if (control_.binding.has_value()) { const QFont original_font = painter->font(); QFont address_font = original_font; address_font.setPixelSize(11); painter->setFont(address_font); painter->setPen(QColor(QStringLiteral("#5f6b73"))); painter->drawText(addressRect(), Qt::AlignCenter, bindingText()); painter->setFont(original_font); painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1)); } applyConfiguredFont(painter); switch (control_.type) { case HmiControlType::Button: { const bool disabled = !editing_enabled_ && runtime_active_ && !runtime_write_enabled_; const QColor fill = disabled ? QColor(QStringLiteral("#e2e6e4")) : button_pressed_ ? QColor(QStringLiteral("#a9cbb6")) : button_hovered_ ? QColor(QStringLiteral("#cce4d5")) : QColor(QStringLiteral("#dcece3")); const QColor border = disabled ? QColor(QStringLiteral("#a5ada9")) : button_pressed_ ? QColor(QStringLiteral("#356248")) : QColor(QStringLiteral("#5e816b")); const QColor text = disabled ? QColor(QStringLiteral("#7a837e")) : configuredTextColor(QColor(QStringLiteral("#205c3b"))); // 未按下时保留下沿阴影,按下后将按钮面下移形成明确的回弹感 QRectF face = rect.adjusted(0, 0, 0, -2); if (!disabled && button_pressed_) { face.translate(0, 2); } else { painter->setPen(Qt::NoPen); painter->setBrush(disabled ? QColor(QStringLiteral("#c5cbc8")) : QColor(QStringLiteral("#789b85"))); painter->drawRoundedRect(face.translated(0, 2), 4, 4); } painter->setPen(QPen(border, button_pressed_ ? 2 : 1)); painter->setBrush(fill); painter->drawRoundedRect(face, 4, 4); painter->setPen(text); painter->drawText(face, 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(configuredTextColor(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(configuredTextColor(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(configuredTextColor(QColor(QStringLiteral("#24313b")))); painter->drawText(rect.adjusted(7, 0, -7, 0), Qt::AlignVCenter | Qt::AlignLeft, textWithValue()); break; } case HmiControlType::ProgressBar: { const HmiProgressBarConfig config = control_.progressBar.value_or( HmiProgressBarConfig{}); const int percentage = has_runtime_value_ ? config.percentageForValue(word_value_) : 0; painter->setPen(Qt::NoPen); painter->setBrush(QColor(QStringLiteral("#e4e9e7"))); painter->drawRoundedRect(rect, 4, 4); const QRectF track = rect.adjusted(2, 2, -2, -2); if (has_runtime_value_ && percentage > 0) { QRectF fill = track; fill.setWidth(track.width() * percentage / 100.0); painter->setBrush(QColor(QStringLiteral("#3c8c62"))); painter->drawRoundedRect(fill, 3, 3); } painter->setBrush(Qt::NoBrush); painter->setPen(QPen(QColor(QStringLiteral("#617069")), 1)); painter->drawRoundedRect(rect, 4, 4); QString display_text = QString::fromUtf8( control_.text.data(), static_cast(control_.text.size())); if (config.showValue) { const QString percentage_text = has_runtime_value_ ? QString::number(percentage) + QLatin1Char('%') : runtime_active_ ? QStringLiteral("--") : QStringLiteral("0%"); display_text = display_text.isEmpty() ? percentage_text : display_text + QStringLiteral(": ") + percentage_text; } painter->setPen(configuredTextColor(QColor(QStringLiteral("#20342a")))); painter->drawText(rect.adjusted(6, 0, -6, 0), Qt::AlignCenter, display_text); break; } case HmiControlType::PageJump: { const QColor fill = runtime_active_ && page_hovered_ ? QColor(QStringLiteral("#d8eafa")) : QColor(QStringLiteral("#e8f1fa")); painter->setBrush(fill); painter->setPen(QPen(QColor(QStringLiteral("#4e789f")), 1)); painter->drawRoundedRect(rect, 4, 4); painter->setPen(configuredTextColor(QColor(QStringLiteral("#244b6b")))); painter->drawText(rect, Qt::AlignCenter, textWithValue()); break; } case HmiControlType::AlarmList: { painter->setPen(QPen(QColor(QStringLiteral("#9b3a3a")), 1)); painter->setBrush(QColor(QStringLiteral("#ffffff"))); painter->drawRect(rect); const QRectF header( rect.left(), rect.top(), rect.width(), kAlarmHeaderHeight); painter->fillRect(header, QColor(QStringLiteral("#a63f3f"))); painter->setPen(Qt::white); QRectF title_rect = header.adjusted(7, 0, -7, 0); const std::size_t page_count = alarmPageCount(); if (runtime_active_ && page_count > 1U) { const QRectF previous_rect = alarmPreviousPageRect(header); const QRectF next_rect = alarmNextPageRect(header); const QRectF indicator_rect = alarmPageIndicatorRect(header); title_rect.setRight(previous_rect.left() - kAlarmColumnSpacing); QStyleOption previous_option; previous_option.rect = previous_rect.toAlignedRect(); previous_option.state = alarm_page_ > 0U ? QStyle::State_Enabled : QStyle::State_None; QApplication::style()->drawPrimitive( QStyle::PE_IndicatorArrowLeft, &previous_option, painter); QStyleOption next_option; next_option.rect = next_rect.toAlignedRect(); next_option.state = alarm_page_ + 1U < page_count ? QStyle::State_Enabled : QStyle::State_None; QApplication::style()->drawPrimitive( QStyle::PE_IndicatorArrowRight, &next_option, painter); painter->setPen(Qt::white); painter->drawText( indicator_rect, Qt::AlignCenter, QStringLiteral("%1/%2") .arg(static_cast(alarm_page_ + 1U)) .arg(static_cast(page_count))); } const QString title = QString::fromUtf8( control_.text.data(), static_cast(control_.text.size())); painter->drawText( title_rect, Qt::AlignVCenter | Qt::AlignLeft, painter->fontMetrics().elidedText( title, Qt::ElideRight, std::max(0, static_cast(title_rect.width())))); const int visible_rows = static_cast(visibleAlarmRecordCount()); if (visible_rows == 0) { painter->setPen(configuredTextColor(QColor(QStringLiteral("#6f7a82")))); painter->drawText( QRectF( rect.left(), header.bottom(), rect.width(), rect.height() - kAlarmHeaderHeight), Qt::AlignCenter, runtime_active_ ? QObject::tr("暂无报警") : QObject::tr("运行时显示当前报警")); break; } const std::size_t first_record = alarmFirstRecordIndex(); for (int row = 0; row < visible_rows; ++row) { const AlarmRecord &record = alarm_records_[ first_record + static_cast(row)]; const QRectF row_rect( rect.left(), header.bottom() + row * kAlarmRowHeight, rect.width(), kAlarmRowHeight); painter->fillRect( row_rect, QColor(QStringLiteral("#fde8e8"))); painter->setPen(QColor(QStringLiteral("#d3d8dc"))); painter->drawLine(row_rect.bottomLeft(), row_rect.bottomRight()); painter->setPen(configuredTextColor(QColor(QStringLiteral("#8d1f1f")))); const QString state = record.acknowledged ? QObject::tr("已确认") : QObject::tr("未确认"); const QRectF time_rect( row_rect.left() + kAlarmCellLeftPadding, row_rect.top(), kAlarmTimeColumnWidth, row_rect.height()); const QRectF state_rect( time_rect.right() + kAlarmColumnSpacing, row_rect.top(), kAlarmStateColumnWidth, row_rect.height()); const qreal message_left = state_rect.right() + kAlarmColumnSpacing; const QRectF message_rect( message_left, row_rect.top(), std::max( 0.0, row_rect.right() - kAlarmCellRightPadding - message_left), row_rect.height()); painter->drawText( time_rect, Qt::AlignVCenter | Qt::AlignLeft, alarmTimeText(record.occurredAt)); painter->drawText( state_rect, Qt::AlignVCenter | Qt::AlignLeft, state); painter->drawText( message_rect, Qt::AlignVCenter | Qt::AlignLeft, painter->fontMetrics().elidedText( QString::fromUtf8( record.message.data(), static_cast(record.message.size())), Qt::ElideRight, std::max(0, static_cast(message_rect.width())))); } break; } case HmiControlType::Label: default: { // 标签只显示固定文本,不绑定寄存器运行值 painter->setPen(configuredTextColor(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(controlRect().adjusted(0, 0, -1, -1)); } } // 向场景和外部控件返回该图元对应的领域控件标识 const std::string &controlId() const { return control_.id; } // 分离编辑选择与运行输入,避免运行态点击继续显示编辑选中框 void setInteractionState( bool editable, bool runtime_active, bool runtime_write_enabled) { if (control_.type == HmiControlType::AlarmList) { prepareGeometryChange(); if (!runtime_active) { alarm_page_ = 0U; } } editing_enabled_ = editable; runtime_active_ = runtime_active; runtime_write_enabled_ = runtime_active && runtime_write_enabled; setFlag(ItemIsMovable, editable); setFlag(ItemIsSelectable, editable); if (!editable) { setSelected(false); } const bool runtime_input = runtime_active_ && (control_.type == HmiControlType::Button || control_.type == HmiControlType::NumericInput || control_.type == HmiControlType::PageJump || control_.type == HmiControlType::AlarmList); const bool runtime_input_enabled = runtime_input && (control_.type == HmiControlType::PageJump || control_.type == HmiControlType::AlarmList || runtime_write_enabled_); setAcceptedMouseButtons( editable || runtime_input_enabled ? Qt::LeftButton : Qt::NoButton); const bool runtime_button_enabled = runtime_active_ && control_.type == HmiControlType::Button && runtime_write_enabled_; const bool runtime_page_jump_enabled = runtime_active_ && control_.type == HmiControlType::PageJump; const bool runtime_alarm_enabled = runtime_active_ && control_.type == HmiControlType::AlarmList; setAcceptHoverEvents(runtime_button_enabled || runtime_page_jump_enabled); if (runtime_button_enabled || runtime_page_jump_enabled || runtime_alarm_enabled) { setCursor(Qt::PointingHandCursor); } else { unsetCursor(); button_hovered_ = false; page_hovered_ = false; button_pressed_ = false; } updateAlarmVisibility(); update(); } // 缓存运行服务读出的值并触发 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(); } void setAlarmRecords(const std::vector &records) { if (control_.type == HmiControlType::AlarmList) { prepareGeometryChange(); } alarm_records_ = records; const std::size_t page_count = alarmPageCount(); alarm_page_ = page_count == 0U ? 0U : std::min(alarm_page_, page_count - 1U); updateAlarmVisibility(); 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 { if (runtime_active_ && control_.type == HmiControlType::AlarmList) { if (event->pos().y() < kAlarmHeaderHeight) { const QRectF content_rect = visibleControlRect().adjusted(1, 1, -1, -1); const QRectF header( content_rect.left(), content_rect.top(), content_rect.width(), kAlarmHeaderHeight); const std::size_t page_count = alarmPageCount(); if (alarmPreviousPageRect(header).contains(event->pos()) && alarm_page_ > 0U) { prepareGeometryChange(); --alarm_page_; update(); } else if (alarmNextPageRect(header).contains(event->pos()) && alarm_page_ + 1U < page_count) { prepareGeometryChange(); ++alarm_page_; update(); } event->accept(); return; } const int row = static_cast( (event->pos().y() - kAlarmHeaderHeight) / kAlarmRowHeight); const std::size_t record_index = alarmFirstRecordIndex() + static_cast(std::max(0, row)); if (row >= 0 && row < static_cast(visibleAlarmRecordCount()) && record_index < alarm_records_.size()) { const AlarmRecord &record = alarm_records_[record_index]; if (!record.acknowledged && alarm_acknowledge_) { alarm_acknowledge_(record.definitionId); } } event->accept(); return; } if (runtime_active_ && control_.type == HmiControlType::PageJump) { page_pressed_ = true; update(); event->accept(); return; } if (runtime_active_ && runtime_write_enabled_ && control_.type == HmiControlType::Button && button_event_) { button_pressed_ = true; update(); button_event_(control_.id, HmiButtonEvent::Pressed); event->accept(); return; } // 编辑模式:不进if分支,执行基类事件——只做选中、拖拽 QGraphicsItem::mousePressEvent(event); } // 运行态双击数值输入时通知外层弹出数值编辑对话框 void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override { if (runtime_active_ && runtime_write_enabled_ && control_.type == HmiControlType::NumericInput && numeric_input_activated_) { // 数值输入使用双击,避免普通选择操作意外写入 D 字 numeric_input_activated_(control_.id); event->accept(); return; } QGraphicsItem::mouseDoubleClickEvent(event); } // 拖拽结束后才提交最终坐标,避免移动过程频繁修改领域模型 void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override { if (runtime_active_ && control_.type == HmiControlType::PageJump && page_pressed_) { page_pressed_ = false; update(); if (page_navigation_ && control_.pageJump.has_value()) { page_navigation_(control_.pageJump->targetPageId); } event->accept(); return; } if (runtime_active_ && runtime_write_enabled_ && control_.type == HmiControlType::Button && button_pressed_) { button_pressed_ = false; update(); button_event_(control_.id, HmiButtonEvent::Released); event->accept(); return; } QGraphicsItem::mouseReleaseEvent(event); // 只有ItemIsMovable打开(编辑态),松开鼠标才提交位置给业务层 if (flags().testFlag(ItemIsMovable) && moved_) { moved_(control_.id, pos()); } } void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override { if (runtime_active_ && runtime_write_enabled_ && control_.type == HmiControlType::Button) { button_hovered_ = true; update(); } if (runtime_active_ && control_.type == HmiControlType::PageJump) { page_hovered_ = true; update(); } QGraphicsItem::hoverEnterEvent(event); } void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override { if (button_hovered_) { button_hovered_ = false; update(); } if (page_hovered_) { page_hovered_ = false; update(); } QGraphicsItem::hoverLeaveEvent(event); } private: std::size_t alarmPageSize() const { const QRectF rect = controlRect().adjusted(1, 1, -1, -1); const int rows_by_height = std::max( 0, static_cast((rect.height() - kAlarmHeaderHeight) / kAlarmRowHeight)); return static_cast(std::min( rows_by_height, ProjectLimits::kMaximumVisibleAlarmRows)); } std::size_t alarmPageCount() const { const std::size_t page_size = alarmPageSize(); return page_size == 0U || alarm_records_.empty() ? 0U : (alarm_records_.size() + page_size - 1U) / page_size; } std::size_t alarmFirstRecordIndex() const { return alarm_page_ * alarmPageSize(); } std::size_t visibleAlarmRecordCount() const { const std::size_t first_record = alarmFirstRecordIndex(); if (first_record >= alarm_records_.size()) { return 0U; } return std::min( alarmPageSize(), alarm_records_.size() - first_record); } QRectF visibleControlRect() const { const QRectF full_rect = controlRect(); if (control_.type != HmiControlType::AlarmList || !runtime_active_) { return full_rect; } const qreal content_height = kAlarmHeaderHeight + static_cast(visibleAlarmRecordCount()) * kAlarmRowHeight + 2.0; return {full_rect.left(), full_rect.top(), full_rect.width(), std::min(full_rect.height(), content_height)}; } QRectF alarmNextPageRect(const QRectF &header) const { return { header.right() - kAlarmCellRightPadding - kAlarmPageButtonSize, header.center().y() - kAlarmPageButtonSize / 2.0, kAlarmPageButtonSize, kAlarmPageButtonSize}; } QRectF alarmPageIndicatorRect(const QRectF &header) const { const QRectF next_rect = alarmNextPageRect(header); return { next_rect.left() - kAlarmPageIndicatorWidth, header.top(), kAlarmPageIndicatorWidth, header.height()}; } QRectF alarmPreviousPageRect(const QRectF &header) const { const QRectF indicator_rect = alarmPageIndicatorRect(header); return { indicator_rect.left() - kAlarmPageButtonSize, header.center().y() - kAlarmPageButtonSize / 2.0, kAlarmPageButtonSize, kAlarmPageButtonSize}; } void updateAlarmVisibility() { if (control_.type != HmiControlType::AlarmList) { return; } const bool has_active_alarm = !alarm_records_.empty(); setVisible(editing_enabled_ || !runtime_active_ || has_active_alarm); setZValue(runtime_active_ && has_active_alarm ? 1000.0 : 0.0); } // 控件本体范围保持领域坐标和尺寸语义不变 QRectF controlRect() const { return {0, 0, static_cast(control_.bounds.width), static_cast(control_.bounds.height)}; } // 地址作为控件的附属信息显示在本体上方 QRectF addressRect() const { constexpr qreal address_height = 16.0; constexpr qreal address_spacing = 2.0; return {0, -address_height - address_spacing, static_cast(control_.bounds.width), address_height}; } QString bindingText() const { return control_.binding.has_value() ? QString::fromStdString(control_.binding->toString()) : QString{}; } QColor configuredTextColor(const QColor &fallback) const { const auto property = control_.properties.find( HmiAppearanceProperty::kTextColor); if (property == control_.properties.cend()) { return fallback; } const QColor color = QColor(QString::fromUtf8( property->second.data(), static_cast(property->second.size()))); return color.isValid() ? color : fallback; } void applyConfiguredFont(QPainter *painter) const { if (painter == nullptr) { return; } QFont font = painter->font(); const auto font_size = control_.properties.find( HmiAppearanceProperty::kFontSize); if (font_size != control_.properties.cend()) { bool ok = false; const int point_size = QString::fromUtf8( font_size->second.data(), static_cast(font_size->second.size())).toInt(&ok); if (ok && point_size >= ProjectLimits::kMinimumHmiFontPointSize && point_size <= ProjectLimits::kMaximumHmiFontPointSize) { font.setPointSize(point_size); } } else if (control_.type == HmiControlType::AlarmList && font.pointSize() > 0) { font.setPointSize(std::max( ProjectLimits::kMinimumHmiFontPointSize, font.pointSize() - ProjectLimits::kAlarmDefaultFontPointReduction)); } const auto font_bold = control_.properties.find( HmiAppearanceProperty::kFontBold); if (font_bold != control_.properties.cend()) { font.setBold(font_bold->second == "true"); } const auto font_italic = control_.properties.find( HmiAppearanceProperty::kFontItalic); if (font_italic != control_.properties.cend()) { font.setItalic(font_italic->second == "true"); } painter->setFont(font); } // 根据控件类型和运行数据组合当前应绘制的文字 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 button_event_; std::function numeric_input_activated_; std::function page_navigation_; std::function alarm_acknowledge_; std::vector alarm_records_; std::size_t alarm_page_ = 0U; bool editing_enabled_ = true; bool runtime_active_ = false; bool runtime_write_enabled_ = false; bool button_hovered_ = false; bool button_pressed_ = false; bool page_hovered_ = false; bool page_pressed_ = false; // 指示灯读取到的 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, AlarmService &alarm_service, QWidget *parent) : QGraphicsView(parent), editor_service_(editor_service), runtime_service_(runtime_service), alarm_service_(alarm_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) { // 切换页面只改变投影上下文,真正的页面数据仍由 ProjectService 持有 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_) { runtime_write_enabled_ = false; for (QGraphicsItem *item : scene_->items()) { HmiGraphicsItem *control_item = asHmiItem(item); if (control_item != nullptr) { control_item->setRuntimeValue(false, 0, false); control_item->setAlarmRecords({}); } } } updateItemInteractions(); } void HmiEditorWidget::setRuntimeWriteEnabled(bool enabled) { runtime_write_enabled_ = runtime_active_ && enabled; updateItemInteractions(); } // 全部重新加载当前页面,把内存模型 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, HmiButtonEvent event) { handleButtonEvent(control_id, event); }, [this](const std::string &control_id) { handleNumericInputActivated(control_id); }, [this](const std::string &target_page_id) { emit pageNavigationRequested( QString::fromStdString(target_page_id)); }, [this](const std::string &definition_id) { alarm_service_.acknowledge(definition_id); refreshRuntimeValues(); }); scene_->addItem(item); item->setInteractionState( editing_enabled_, runtime_active_, runtime_write_enabled_); } fitCurrentPage(); refreshRuntimeValues(); } // 遍历场景所有图元,找到对应 id 的图元,设置选中,视图滚动到把控件显示出来 void HmiEditorWidget::selectControl(const std::string &control_id) { scene_->clearSelection(); 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 std::vector ids = selectedControlIds(); return ids.empty() ? std::string{} : ids.front(); } std::vector HmiEditorWidget::selectedControlIds() const { std::vector> positioned_ids; for (QGraphicsItem *item : scene_->selectedItems()) { const HmiGraphicsItem *control_item = asHmiItem(item); if (control_item != nullptr) { positioned_ids.emplace_back( control_item->scenePos(), control_item->controlId()); } } std::sort( positioned_ids.begin(), positioned_ids.end(), [](const auto &left, const auto &right) { if (!qFuzzyCompare(left.first.y(), right.first.y())) { return left.first.y() < right.first.y(); } return left.first.x() < right.first.x(); }); std::vector ids; ids.reserve(positioned_ids.size()); for (const auto &positioned_id : positioned_ids) { ids.push_back(positioned_id.second); } return ids; } 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; } if (control->type == HmiControlType::AlarmList) { control_item->setAlarmRecords(alarm_service_.records()); 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(-24, -24, 24, 24), Qt::KeepAspectRatio); } void HmiEditorWidget::updateItemInteractions() { // 遍历所有控件图元,统一设置flag for (QGraphicsItem *item : scene_->items()) { HmiGraphicsItem *control_item = asHmiItem(item); if (control_item != nullptr) { control_item->setInteractionState( editing_enabled_, runtime_active_, runtime_write_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::handleButtonEvent( const std::string &control_id, HmiButtonEvent event) { if (!runtime_active_ || !runtime_write_enabled_) { return; } // 运行态按钮事件只通过 HmiRuntimeService 写寄存器,不直接访问仓库 const HmiControl *control = editor_service_.findControl(page_id_, control_id); if (control == nullptr) { return; } if (control->type != HmiControlType::Button) { return; } const HmiRuntimeWriteResult result = runtime_service_.operateButton(*control, event); if (!result.succeeded) { emit editorError(tr("寄存器操作失败")); return; } refreshRuntimeValues(); } void HmiEditorWidget::handleNumericInputActivated(const std::string &control_id) { if (!runtime_active_ || !runtime_write_enabled_) { return; } const HmiControl *control = editor_service_.findControl(page_id_, control_id); if (control == nullptr || control->type != HmiControlType::NumericInput) { return; } 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; } const HmiRuntimeWriteResult result = runtime_service_.writeNumericInput( *control, static_cast(value)); if (!result.succeeded) { emit editorError(tr("寄存器操作失败")); return; } refreshRuntimeValues(); }