Преглед изворни кода

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 <QGraphicsRectItem>
#include <QGraphicsScene>
#include <QGraphicsSceneHoverEvent>
#include <QGraphicsSceneMouseEvent>
#include <QInputDialog>
#include <QPainter>
@@ -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<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_;
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_);
}
}
}


+ 86
- 0
app/tests/main_window_tests.cpp Прегледај датотеку

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

#include <iostream>
#include <stdexcept>
@@ -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();


+ 1
- 1
app/tests/main_window_tests.pro Прегледај датотеку

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

TEMPLATE = app
TARGET = main_window_tests


Loading…
Откажи
Сачувај