|
- #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 <QApplication>
- #include <QDateTime>
- #include <QFont>
- #include <QGraphicsItem>
- #include <QGraphicsRectItem>
- #include <QGraphicsScene>
- #include <QGraphicsSceneHoverEvent>
- #include <QGraphicsSceneMouseEvent>
- #include <QInputDialog>
- #include <QPainter>
- #include <QResizeEvent>
- #include <QStyle>
- #include <QStyleOption>
- #include <QStyleOptionGraphicsItem>
-
- #include <algorithm>
- #include <cmath>
- #include <cstdint>
- #include <functional>
- #include <limits>
-
- 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<std::chrono::seconds>(
- 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<void(const std::string &, const QPointF &)> moved,
- std::function<void(const std::string &, HmiButtonEvent)> button_event,
- std::function<void(const std::string &)> numeric_input_activated,
- std::function<void(const std::string &)> page_navigation,
- std::function<void(const std::string &)> 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<int>(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<int>(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<qulonglong>(alarm_page_ + 1U))
- .arg(static_cast<qulonglong>(page_count)));
- }
- const QString title = QString::fromUtf8(
- control_.text.data(), static_cast<int>(control_.text.size()));
- painter->drawText(
- title_rect,
- Qt::AlignVCenter | Qt::AlignLeft,
- painter->fontMetrics().elidedText(
- title,
- Qt::ElideRight,
- std::max(0, static_cast<int>(title_rect.width()))));
-
- const int visible_rows = static_cast<int>(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<std::size_t>(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<int>(record.message.size())),
- Qt::ElideRight,
- std::max(0, static_cast<int>(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<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(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<AlarmRecord> &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<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
- {
- 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<int>(
- (event->pos().y() - kAlarmHeaderHeight) / kAlarmRowHeight);
- const std::size_t record_index = alarmFirstRecordIndex()
- + static_cast<std::size_t>(std::max(0, row));
- if (row >= 0
- && row < static_cast<int>(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<int>((rect.height() - kAlarmHeaderHeight)
- / kAlarmRowHeight));
- return static_cast<std::size_t>(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<qreal>(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<qreal>(control_.bounds.width),
- static_cast<qreal>(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<qreal>(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<int>(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<int>(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<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 &, HmiButtonEvent)> button_event_;
- std::function<void(const std::string &)> numeric_input_activated_;
- std::function<void(const std::string &)> page_navigation_;
- std::function<void(const std::string &)> alarm_acknowledge_;
- std::vector<AlarmRecord> 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<HmiGraphicsItem *>(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<std::string> ids = selectedControlIds();
- return ids.empty() ? std::string{} : ids.front();
- }
-
- std::vector<std::string> HmiEditorWidget::selectedControlIds() const
- {
- std::vector<std::pair<QPointF, std::string>> 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<std::string> 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<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::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<int>(control->text.size())),
- initial,
- std::numeric_limits<std::int16_t>::min(),
- std::numeric_limits<std::int16_t>::max(),
- 1,
- &accepted);
- if (!accepted)
- {
- return;
- }
- const HmiRuntimeWriteResult result = runtime_service_.writeNumericInput(
- *control, static_cast<std::int16_t>(value));
- if (!result.succeeded)
- {
- emit editorError(tr("寄存器操作失败"));
- return;
- }
- refreshRuntimeValues();
- }
|