浏览代码

feat: 优化HMI运行按钮交互

main
suyu 1 个月前
父节点
当前提交
5d842042c5
共有 3 个文件被更改,包括 191 次插入15 次删除
  1. +104
    -14
      app/src/ui/hmi_editor_widget.cpp
  2. +86
    -0
      app/tests/main_window_tests.cpp
  3. +1
    -1
      app/tests/main_window_tests.pro

+ 104
- 14
app/src/ui/hmi_editor_widget.cpp 查看文件

@@ -6,6 +6,7 @@
#include <QGraphicsItem> #include <QGraphicsItem>
#include <QGraphicsRectItem> #include <QGraphicsRectItem>
#include <QGraphicsScene> #include <QGraphicsScene>
#include <QGraphicsSceneHoverEvent>
#include <QGraphicsSceneMouseEvent> #include <QGraphicsSceneMouseEvent>
#include <QInputDialog> #include <QInputDialog>
#include <QPainter> #include <QPainter>
@@ -85,11 +86,40 @@ public:
{ {
case HmiControlType::Button: 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; break;
} }
case HmiControlType::Indicator: case HmiControlType::Indicator:
@@ -161,10 +191,41 @@ public:
return control_.id; 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(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 重绘 // 缓存运行服务读出的值并触发 Qt 重绘
@@ -199,11 +260,11 @@ protected:
// 运行态按钮按下时通知外层执行配置的 M 位操作 // 运行态按钮按下时通知外层执行配置的 M 位操作
void mousePressEvent(QGraphicsSceneMouseEvent *event) override void mousePressEvent(QGraphicsSceneMouseEvent *event) override
{ {
if (!flags().testFlag(ItemIsMovable)
if (runtime_active_ && runtime_write_enabled_
&& control_.type == HmiControlType::Button && button_event_) && control_.type == HmiControlType::Button && button_event_)
{ {
setSelected(true);
button_pressed_ = true; button_pressed_ = true;
update();
button_event_(control_.id, HmiButtonEvent::Pressed); button_event_(control_.id, HmiButtonEvent::Pressed);
event->accept(); event->accept();
return; return;
@@ -215,12 +276,11 @@ protected:
// 运行态双击数值输入时通知外层弹出数值编辑对话框 // 运行态双击数值输入时通知外层弹出数值编辑对话框
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override
{ {
if (!flags().testFlag(ItemIsMovable)
if (runtime_active_ && runtime_write_enabled_
&& control_.type == HmiControlType::NumericInput && control_.type == HmiControlType::NumericInput
&& numeric_input_activated_) && numeric_input_activated_)
{ {
// 数值输入使用双击,避免普通选择操作意外写入 D 字 // 数值输入使用双击,避免普通选择操作意外写入 D 字
setSelected(true);
numeric_input_activated_(control_.id); numeric_input_activated_(control_.id);
event->accept(); event->accept();
return; return;
@@ -231,10 +291,11 @@ protected:
// 拖拽结束后才提交最终坐标,避免移动过程频繁修改领域模型 // 拖拽结束后才提交最终坐标,避免移动过程频繁修改领域模型
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override
{ {
if (!flags().testFlag(ItemIsMovable)
if (runtime_active_ && runtime_write_enabled_
&& control_.type == HmiControlType::Button && button_pressed_) && control_.type == HmiControlType::Button && button_pressed_)
{ {
button_pressed_ = false; button_pressed_ = false;
update();
button_event_(control_.id, HmiButtonEvent::Released); button_event_(control_.id, HmiButtonEvent::Released);
event->accept(); event->accept();
return; 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: private:
// 控件本体范围保持领域坐标和尺寸语义不变 // 控件本体范围保持领域坐标和尺寸语义不变
QRectF controlRect() const QRectF controlRect() const
@@ -302,6 +384,10 @@ private:
std::function<void(const std::string &, const QPointF &)> moved_; std::function<void(const std::string &, const QPointF &)> moved_;
std::function<void(const std::string &, HmiButtonEvent)> button_event_; std::function<void(const std::string &, HmiButtonEvent)> button_event_;
std::function<void(const std::string &)> numeric_input_activated_; std::function<void(const std::string &)> numeric_input_activated_;
bool editing_enabled_ = true;
bool runtime_active_ = false;
bool runtime_write_enabled_ = false;
bool button_hovered_ = false;
bool button_pressed_ = false; bool button_pressed_ = false;
// 指示灯读取到的 M 位值 // 指示灯读取到的 M 位值
bool bit_value_ = false; bool bit_value_ = false;
@@ -370,11 +456,13 @@ void HmiEditorWidget::setRuntimeActive(bool active)
} }
} }
} }
updateItemInteractions();
} }


void HmiEditorWidget::setRuntimeWriteEnabled(bool enabled) void HmiEditorWidget::setRuntimeWriteEnabled(bool enabled)
{ {
runtime_write_enabled_ = runtime_active_ && enabled; runtime_write_enabled_ = runtime_active_ && enabled;
updateItemInteractions();
} }


// 全部重新加载当前页面,把内存模型 HmiPage 渲染成画面上图形 // 全部重新加载当前页面,把内存模型 HmiPage 渲染成画面上图形
@@ -415,7 +503,8 @@ void HmiEditorWidget::reloadPage()
handleNumericInputActivated(control_id); handleNumericInputActivated(control_id);
}); });
scene_->addItem(item); scene_->addItem(item);
item->setInteractionEnabled(editing_enabled_);
item->setInteractionState(
editing_enabled_, runtime_active_, runtime_write_enabled_);
} }
fitCurrentPage(); fitCurrentPage();
refreshRuntimeValues(); refreshRuntimeValues();
@@ -500,7 +589,8 @@ void HmiEditorWidget::updateItemInteractions()
HmiGraphicsItem *control_item = asHmiItem(item); HmiGraphicsItem *control_item = asHmiItem(item);
if (control_item != nullptr) if (control_item != nullptr)
{ {
control_item->setInteractionEnabled(editing_enabled_);
control_item->setInteractionState(
editing_enabled_, runtime_active_, runtime_write_enabled_);
} }
} }
} }


+ 86
- 0
app/tests/main_window_tests.cpp 查看文件

@@ -21,13 +21,16 @@
#include <QDockWidget> #include <QDockWidget>
#include <QGraphicsItem> #include <QGraphicsItem>
#include <QGraphicsScene> #include <QGraphicsScene>
#include <QImage>
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QListWidget> #include <QListWidget>
#include <QPainter>
#include <QSpinBox> #include <QSpinBox>
#include <QPushButton> #include <QPushButton>
#include <QTableWidget> #include <QTableWidget>
#include <QTimer> #include <QTimer>
#include <QTest>


#include <iostream> #include <iostream>
#include <stdexcept> #include <stdexcept>
@@ -179,6 +182,88 @@ ObjectType *requiredChild(MainWindow &window, const char *name)
return child; 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() void testModeActionsControlEditingAvailability()
{ {
// 验证模式动作会同步禁用编辑入口,并在失败时恢复当前选择 // 验证模式动作会同步禁用编辑入口,并在失败时恢复当前选择
@@ -571,6 +656,7 @@ int main(int argc, char *argv[])


try try
{ {
testRuntimeButtonMouseInteraction();
testModeActionsControlEditingAvailability(); testModeActionsControlEditingAvailability();
testPlcConfigurationUsesDialog(); testPlcConfigurationUsesDialog();
testRepeatedPlcStatusNotificationsAreCoalesced(); testRepeatedPlcStatusNotificationsAreCoalesced();


+ 1
- 1
app/tests/main_window_tests.pro 查看文件

@@ -1,4 +1,4 @@
QT += widgets serialport
QT += widgets serialport testlib


TEMPLATE = app TEMPLATE = app
TARGET = main_window_tests TARGET = main_window_tests


正在加载...
取消
保存