From 5d842042c5e0f77413e0e26a449546331baaea0f Mon Sep 17 00:00:00 2001 From: suyu <1643689728@qq.com> Date: Fri, 14 Aug 2026 09:25:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96HMI=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/ui/hmi_editor_widget.cpp | 118 +++++++++++++++++++++++++++---- app/tests/main_window_tests.cpp | 86 ++++++++++++++++++++++ app/tests/main_window_tests.pro | 2 +- 3 files changed, 191 insertions(+), 15 deletions(-) diff --git a/app/src/ui/hmi_editor_widget.cpp b/app/src/ui/hmi_editor_widget.cpp index 3011dbb..87ffb54 100644 --- a/app/src/ui/hmi_editor_widget.cpp +++ b/app/src/ui/hmi_editor_widget.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -85,11 +86,40 @@ public: { case HmiControlType::Button: { - // 按钮显示文字,运行态点击行为由鼠标事件处理 - painter->setBrush(QColor(QStringLiteral("#dcece3"))); - painter->drawRoundedRect(rect, 4, 4); - painter->setPen(QColor(QStringLiteral("#205c3b"))); - painter->drawText(rect, Qt::AlignCenter, textWithValue()); + 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")) + : 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: @@ -161,10 +191,41 @@ public: return control_.id; } - // 编辑态允许拖动,运行态保留点击或双击交互 - void setInteractionEnabled(bool editable) + // 分离编辑选择与运行输入,避免运行态点击继续显示编辑选中框 + void setInteractionState( + bool editable, bool runtime_active, bool runtime_write_enabled) { + 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); + const bool runtime_input_enabled = runtime_input && 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_; + setAcceptHoverEvents(runtime_button_enabled); + if (runtime_button_enabled) + { + setCursor(Qt::PointingHandCursor); + } + else + { + unsetCursor(); + button_hovered_ = false; + button_pressed_ = false; + } + update(); } // 缓存运行服务读出的值并触发 Qt 重绘 @@ -199,11 +260,11 @@ protected: // 运行态按钮按下时通知外层执行配置的 M 位操作 void mousePressEvent(QGraphicsSceneMouseEvent *event) override { - if (!flags().testFlag(ItemIsMovable) + if (runtime_active_ && runtime_write_enabled_ && control_.type == HmiControlType::Button && button_event_) { - setSelected(true); button_pressed_ = true; + update(); button_event_(control_.id, HmiButtonEvent::Pressed); event->accept(); return; @@ -215,12 +276,11 @@ protected: // 运行态双击数值输入时通知外层弹出数值编辑对话框 void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override { - if (!flags().testFlag(ItemIsMovable) + if (runtime_active_ && runtime_write_enabled_ && control_.type == HmiControlType::NumericInput && numeric_input_activated_) { // 数值输入使用双击,避免普通选择操作意外写入 D 字 - setSelected(true); numeric_input_activated_(control_.id); event->accept(); return; @@ -231,10 +291,11 @@ protected: // 拖拽结束后才提交最终坐标,避免移动过程频繁修改领域模型 void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override { - if (!flags().testFlag(ItemIsMovable) + 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; @@ -247,6 +308,27 @@ protected: } } + void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override + { + if (runtime_active_ && runtime_write_enabled_ + && control_.type == HmiControlType::Button) + { + button_hovered_ = true; + update(); + } + QGraphicsItem::hoverEnterEvent(event); + } + + void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override + { + if (button_hovered_) + { + button_hovered_ = false; + update(); + } + QGraphicsItem::hoverLeaveEvent(event); + } + private: // 控件本体范围保持领域坐标和尺寸语义不变 QRectF controlRect() const @@ -302,6 +384,10 @@ private: std::function moved_; std::function button_event_; std::function numeric_input_activated_; + bool editing_enabled_ = true; + bool runtime_active_ = false; + bool runtime_write_enabled_ = false; + bool button_hovered_ = false; bool button_pressed_ = false; // 指示灯读取到的 M 位值 bool bit_value_ = false; @@ -370,11 +456,13 @@ void HmiEditorWidget::setRuntimeActive(bool active) } } } + updateItemInteractions(); } void HmiEditorWidget::setRuntimeWriteEnabled(bool enabled) { runtime_write_enabled_ = runtime_active_ && enabled; + updateItemInteractions(); } // 全部重新加载当前页面,把内存模型 HmiPage 渲染成画面上图形 @@ -415,7 +503,8 @@ void HmiEditorWidget::reloadPage() handleNumericInputActivated(control_id); }); scene_->addItem(item); - item->setInteractionEnabled(editing_enabled_); + item->setInteractionState( + editing_enabled_, runtime_active_, runtime_write_enabled_); } fitCurrentPage(); refreshRuntimeValues(); @@ -500,7 +589,8 @@ void HmiEditorWidget::updateItemInteractions() HmiGraphicsItem *control_item = asHmiItem(item); if (control_item != nullptr) { - control_item->setInteractionEnabled(editing_enabled_); + control_item->setInteractionState( + editing_enabled_, runtime_active_, runtime_write_enabled_); } } } diff --git a/app/tests/main_window_tests.cpp b/app/tests/main_window_tests.cpp index c72c389..c33eae7 100644 --- a/app/tests/main_window_tests.cpp +++ b/app/tests/main_window_tests.cpp @@ -21,13 +21,16 @@ #include #include #include +#include #include #include #include +#include #include #include #include #include +#include #include #include @@ -179,6 +182,88 @@ ObjectType *requiredChild(MainWindow &window, const char *name) return child; } +QColor renderedColorAt(HmiEditorWidget &view, const QPoint &viewport_position) +{ + QImage image(view.viewport()->size(), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::transparent); + QPainter painter(&image); + view.viewport()->render(&painter); + return image.pixelColor(viewport_position); +} + +void testRuntimeButtonMouseInteraction() +{ + TestProjectStorage storage; + ProjectService project_service(storage); + HmiEditorService editor_service(project_service); + VirtualRegisterRepository repository; + HmiRuntimeService runtime_service(repository); + const HmiEditorResult page_result = editor_service.ensureDefaultPage(); + require(page_result.succeeded, "runtime button test must create an HMI page"); + const HmiEditorResult button_result = editor_service.addControl( + page_result.id, HmiControlType::Button); + require(button_result.succeeded, "runtime button test must create a button"); + + HmiControl button = *editor_service.findControl(page_result.id, button_result.id); + button.binding = RegisterAddress{RegisterArea::M, 0}; + button.buttonOperation = HmiButtonOperation::MomentaryOn; + require(editor_service.updateControl(page_result.id, button.id, button).succeeded, + "runtime button test must bind the button to M0"); + + HmiEditorWidget view(editor_service, runtime_service); + view.resize(900, 600); + view.setPageId(page_result.id); + view.setEditingEnabled(false); + view.setRuntimeActive(true); + view.setRuntimeWriteEnabled(true); + view.show(); + QApplication::processEvents(); + + const QPoint center = view.mapFromScene(QPointF( + button.bounds.x + button.bounds.width / 2.0, + button.bounds.y + button.bounds.height / 2.0)); + const QPoint color_sample = view.mapFromScene(QPointF( + button.bounds.x + 8.0, button.bounds.y + 8.0)); + const QColor normal_color = renderedColorAt(view, color_sample); + + QTest::mouseMove(view.viewport(), center); + QApplication::processEvents(); + QGraphicsItem *button_item = view.itemAt(center); + require(button_item != nullptr, + "runtime button must be hit-testable while writes are enabled"); + require(button_item->cursor().shape() == Qt::PointingHandCursor, + "runtime button must use a pointing-hand cursor"); + const QColor hovered_color = renderedColorAt(view, color_sample); + require(hovered_color != normal_color, + "hovering a runtime button must change its visual state"); + + QTest::mousePress(view.viewport(), Qt::LeftButton, Qt::NoModifier, center); + QApplication::processEvents(); + require(repository.readBit({RegisterArea::M, 0}).value, + "pressing a momentary runtime button must write M0 ON"); + require(view.scene()->selectedItems().isEmpty(), + "pressing a runtime button must not show an editing selection"); + const QColor pressed_color = renderedColorAt(view, color_sample); + require(pressed_color != hovered_color, + "pressing a runtime button must change its visual state"); + + QTest::mouseRelease(view.viewport(), Qt::LeftButton, Qt::NoModifier, center); + QApplication::processEvents(); + require(!repository.readBit({RegisterArea::M, 0}).value, + "releasing a momentary runtime button must write M0 OFF"); + require(renderedColorAt(view, color_sample) == hovered_color, + "releasing a runtime button must restore its hovered visual state"); + + view.setRuntimeWriteEnabled(false); + QApplication::processEvents(); + const QColor disabled_color = renderedColorAt(view, color_sample); + require(disabled_color != hovered_color, + "a non-writable runtime button must use its disabled visual state"); + QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, center); + require(!repository.readBit({RegisterArea::M, 0}).value, + "a disabled runtime button must not write its register"); +} + void testModeActionsControlEditingAvailability() { // 验证模式动作会同步禁用编辑入口,并在失败时恢复当前选择 @@ -571,6 +656,7 @@ int main(int argc, char *argv[]) try { + testRuntimeButtonMouseInteraction(); testModeActionsControlEditingAvailability(); testPlcConfigurationUsesDialog(); testRepeatedPlcStatusNotificationsAreCoalesced(); diff --git a/app/tests/main_window_tests.pro b/app/tests/main_window_tests.pro index 5225e18..4da01b9 100644 --- a/app/tests/main_window_tests.pro +++ b/app/tests/main_window_tests.pro @@ -1,4 +1,4 @@ -QT += widgets serialport +QT += widgets serialport testlib TEMPLATE = app TARGET = main_window_tests