|
|
@@ -47,8 +47,10 @@ constexpr qreal kAlarmPageIndicatorWidth = 34.0; |
|
|
// 将报警时间转换为时分秒显示文字 |
|
|
// 将报警时间转换为时分秒显示文字 |
|
|
QString alarmTimeText(const std::chrono::system_clock::time_point &time) |
|
|
QString alarmTimeText(const std::chrono::system_clock::time_point &time) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// duration_cast 丢弃不足一秒的部分,报警列表不显示毫秒 |
|
|
const auto seconds = std::chrono::duration_cast<std::chrono::seconds>( |
|
|
const auto seconds = std::chrono::duration_cast<std::chrono::seconds>( |
|
|
time.time_since_epoch()).count(); |
|
|
time.time_since_epoch()).count(); |
|
|
|
|
|
// fromSecsSinceEpoch 把 Unix 时间戳交给 Qt,再按本机时区格式化 |
|
|
return QDateTime::fromSecsSinceEpoch(seconds).toString(QStringLiteral("HH:mm:ss")); |
|
|
return QDateTime::fromSecsSinceEpoch(seconds).toString(QStringLiteral("HH:mm:ss")); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -59,9 +61,12 @@ QString numericValueText(const RegisterNumericValue &value) |
|
|
return std::visit( |
|
|
return std::visit( |
|
|
[](const auto &typed_value) |
|
|
[](const auto &typed_value) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// decay_t 去掉 const 和引用,得到 variant 当前值的实际基础类型 |
|
|
using Value = std::decay_t<decltype(typed_value)>; |
|
|
using Value = std::decay_t<decltype(typed_value)>; |
|
|
|
|
|
// if constexpr 只编译命中的类型分支,不会产生运行时类型判断 |
|
|
if constexpr (std::is_same_v<Value, float>) |
|
|
if constexpr (std::is_same_v<Value, float>) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// max_digits10 保证浮点数转成文字后还能无损读回原值 |
|
|
return QString::number( |
|
|
return QString::number( |
|
|
static_cast<double>(typed_value), 'g', |
|
|
static_cast<double>(typed_value), 'g', |
|
|
std::numeric_limits<float>::max_digits10); |
|
|
std::numeric_limits<float>::max_digits10); |
|
|
@@ -73,6 +78,7 @@ QString numericValueText(const RegisterNumericValue &value) |
|
|
} |
|
|
} |
|
|
else |
|
|
else |
|
|
{ |
|
|
{ |
|
|
|
|
|
// 整数先扩成 qlonglong,统一调用 QString 的有符号整数重载 |
|
|
return QString::number(static_cast<qlonglong>(typed_value)); |
|
|
return QString::number(static_cast<qlonglong>(typed_value)); |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
@@ -83,6 +89,7 @@ QString numericValueText(const RegisterNumericValue &value) |
|
|
double numericValueAsDouble(const RegisterNumericValue &value) |
|
|
double numericValueAsDouble(const RegisterNumericValue &value) |
|
|
{ |
|
|
{ |
|
|
// std::visit 统一处理整数、浮点数等不同寄存器数值类型 |
|
|
// std::visit 统一处理整数、浮点数等不同寄存器数值类型 |
|
|
|
|
|
// 返回 double 只用于对话框初值,真正写入时仍按控件数据类型重新编码校验 |
|
|
return std::visit( |
|
|
return std::visit( |
|
|
[](const auto &typed_value) |
|
|
[](const auto &typed_value) |
|
|
{ |
|
|
{ |
|
|
@@ -106,7 +113,9 @@ std::optional<double> requestFloatingPointInput( |
|
|
? std::numeric_limits<float>::max_digits10 |
|
|
? std::numeric_limits<float>::max_digits10 |
|
|
: std::numeric_limits<double>::max_digits10; |
|
|
: std::numeric_limits<double>::max_digits10; |
|
|
|
|
|
|
|
|
|
|
|
// 栈上模态对话框在函数返回时自动销毁,parent 只负责窗口层级和居中 |
|
|
QInputDialog dialog(parent); |
|
|
QInputDialog dialog(parent); |
|
|
|
|
|
// 使用文本输入模式才能安装支持科学计数法的 QDoubleValidator |
|
|
dialog.setInputMode(QInputDialog::TextInput); |
|
|
dialog.setInputMode(QInputDialog::TextInput); |
|
|
dialog.setWindowTitle(QObject::tr("输入 %1").arg( |
|
|
dialog.setWindowTitle(QObject::tr("输入 %1").arg( |
|
|
QString::fromLatin1(registerDataTypeDescriptor(control.dataType).displayName))); |
|
|
QString::fromLatin1(registerDataTypeDescriptor(control.dataType).displayName))); |
|
|
@@ -116,15 +125,20 @@ std::optional<double> requestFloatingPointInput( |
|
|
has_current_value ? numericValueAsDouble(current_value) : 0.0, |
|
|
has_current_value ? numericValueAsDouble(current_value) : 0.0, |
|
|
'g', digits)); |
|
|
'g', digits)); |
|
|
// QInputDialog 没有直接暴露验证器,通过内部输入框安装范围验证 |
|
|
// QInputDialog 没有直接暴露验证器,通过内部输入框安装范围验证 |
|
|
|
|
|
// findChild 从对话框的 QObject 子树里取得内部输入框 |
|
|
QLineEdit *editor = dialog.findChild<QLineEdit *>(); |
|
|
QLineEdit *editor = dialog.findChild<QLineEdit *>(); |
|
|
if (editor != nullptr) |
|
|
if (editor != nullptr) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// validator 以输入框为父对象,输入框销毁时会自动释放 |
|
|
auto *validator = new QDoubleValidator(-maximum, maximum, digits, editor); |
|
|
auto *validator = new QDoubleValidator(-maximum, maximum, digits, editor); |
|
|
|
|
|
// ScientificNotation 同时接受普通小数和 1.2e3 形式 |
|
|
validator->setNotation(QDoubleValidator::ScientificNotation); |
|
|
validator->setNotation(QDoubleValidator::ScientificNotation); |
|
|
|
|
|
// C locale 固定使用点号作为小数点,避免不同电脑输入格式不一致 |
|
|
validator->setLocale(QLocale::c()); |
|
|
validator->setLocale(QLocale::c()); |
|
|
editor->setValidator(validator); |
|
|
editor->setValidator(validator); |
|
|
editor->selectAll(); |
|
|
editor->selectAll(); |
|
|
} |
|
|
} |
|
|
|
|
|
// exec 启动局部模态事件循环,只有点击确认才返回 Accepted |
|
|
if (dialog.exec() != QDialog::Accepted) |
|
|
if (dialog.exec() != QDialog::Accepted) |
|
|
{ |
|
|
{ |
|
|
return std::nullopt; |
|
|
return std::nullopt; |
|
|
@@ -132,6 +146,7 @@ std::optional<double> requestFloatingPointInput( |
|
|
bool converted = false; |
|
|
bool converted = false; |
|
|
// 使用固定 C locale,确保输入格式不受系统区域设置影响 |
|
|
// 使用固定 C locale,确保输入格式不受系统区域设置影响 |
|
|
const double value = QLocale::c().toDouble(dialog.textValue(), &converted); |
|
|
const double value = QLocale::c().toDouble(dialog.textValue(), &converted); |
|
|
|
|
|
// 再调用统一编码器检查该 double 能否由目标寄存器类型准确接受 |
|
|
return converted && encodeRegisterNumericValue(control.dataType, value).has_value() |
|
|
return converted && encodeRegisterNumericValue(control.dataType, value).has_value() |
|
|
? std::optional<double>{value} : std::nullopt; |
|
|
? std::optional<double>{value} : std::nullopt; |
|
|
} |
|
|
} |
|
|
@@ -153,6 +168,7 @@ public: |
|
|
: control_(control), |
|
|
: control_(control), |
|
|
page_width_(page_width), |
|
|
page_width_(page_width), |
|
|
page_height_(page_height), |
|
|
page_height_(page_height), |
|
|
|
|
|
// std::move 把回调所有权转入图元,避免复制较重的 std::function |
|
|
moved_(std::move(moved)), |
|
|
moved_(std::move(moved)), |
|
|
button_event_(std::move(button_event)), |
|
|
button_event_(std::move(button_event)), |
|
|
numeric_input_activated_(std::move(numeric_input_activated)), |
|
|
numeric_input_activated_(std::move(numeric_input_activated)), |
|
|
@@ -160,8 +176,10 @@ public: |
|
|
alarm_acknowledge_(std::move(alarm_acknowledge)) |
|
|
alarm_acknowledge_(std::move(alarm_acknowledge)) |
|
|
{ |
|
|
{ |
|
|
// 领域坐标直接作为图元在场景中的初始位置 |
|
|
// 领域坐标直接作为图元在场景中的初始位置 |
|
|
|
|
|
// setPos 设置的是图元原点在场景中的位置,不会改动 boundingRect |
|
|
setPos(control_.bounds.x, control_.bounds.y); |
|
|
setPos(control_.bounds.x, control_.bounds.y); |
|
|
// 所有控件均可选中,便于主窗口显示对应属性 |
|
|
// 所有控件均可选中,便于主窗口显示对应属性 |
|
|
|
|
|
// QGraphicsItem 标志决定场景默认事件处理能否选择和拖动图元 |
|
|
setFlag(ItemIsSelectable, true); |
|
|
setFlag(ItemIsSelectable, true); |
|
|
// 开启位置变化通知,让 itemChange 可以限制拖动坐标 |
|
|
// 开启位置变化通知,让 itemChange 可以限制拖动坐标 |
|
|
setFlag(ItemSendsGeometryChanges, true); |
|
|
setFlag(ItemSendsGeometryChanges, true); |
|
|
@@ -172,10 +190,12 @@ public: |
|
|
// 返回图元自身坐标系中的矩形范围,用于绘制和命中测试 |
|
|
// 返回图元自身坐标系中的矩形范围,用于绘制和命中测试 |
|
|
QRectF boundingRect() const override |
|
|
QRectF boundingRect() const override |
|
|
{ |
|
|
{ |
|
|
|
|
|
// Qt 用 boundingRect 做裁剪、碰撞和重绘判断,必须包住 paint 的全部像素 |
|
|
if (!control_.binding.has_value()) |
|
|
if (!control_.binding.has_value()) |
|
|
{ |
|
|
{ |
|
|
return controlRect(); |
|
|
return controlRect(); |
|
|
} |
|
|
} |
|
|
|
|
|
// united 返回同时覆盖控件本体和上方地址标签的最小矩形 |
|
|
return controlRect().united(addressRect()); |
|
|
return controlRect().united(addressRect()); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -185,13 +205,17 @@ public: |
|
|
const QStyleOptionGraphicsItem *option, |
|
|
const QStyleOptionGraphicsItem *option, |
|
|
QWidget *) override |
|
|
QWidget *) override |
|
|
{ |
|
|
{ |
|
|
|
|
|
// painter 此时使用图元本地坐标,图元的 scenePos 由场景变换自动叠加 |
|
|
// 留出一个像素边距,避免描边被图元边界裁剪 |
|
|
// 留出一个像素边距,避免描边被图元边界裁剪 |
|
|
const QRectF rect = controlRect().adjusted(1, 1, -1, -1); |
|
|
const QRectF rect = controlRect().adjusted(1, 1, -1, -1); |
|
|
|
|
|
// 抗锯齿让圆角、椭圆和斜线边缘更平滑 |
|
|
painter->setRenderHint(QPainter::Antialiasing, true); |
|
|
painter->setRenderHint(QPainter::Antialiasing, true); |
|
|
|
|
|
// pen 负责轮廓和文字,brush 负责封闭图形内部填充 |
|
|
painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1)); |
|
|
painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1)); |
|
|
|
|
|
|
|
|
if (control_.binding.has_value()) |
|
|
if (control_.binding.has_value()) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// 先保存原字体,画完小号地址后恢复,避免影响控件正文 |
|
|
const QFont original_font = painter->font(); |
|
|
const QFont original_font = painter->font(); |
|
|
QFont address_font = original_font; |
|
|
QFont address_font = original_font; |
|
|
address_font.setPixelSize(11); |
|
|
address_font.setPixelSize(11); |
|
|
@@ -204,10 +228,12 @@ public: |
|
|
|
|
|
|
|
|
applyConfiguredFont(painter); |
|
|
applyConfiguredFont(painter); |
|
|
|
|
|
|
|
|
|
|
|
// 每种领域控件复用同一个图元类,只在绘制阶段按类型选择外观 |
|
|
switch (control_.type) |
|
|
switch (control_.type) |
|
|
{ |
|
|
{ |
|
|
case HmiControlType::Button: |
|
|
case HmiControlType::Button: |
|
|
{ |
|
|
{ |
|
|
|
|
|
// 编辑态不按运行写权限置灰,运行态则同时检查通信写权限和启用条件 |
|
|
const bool disabled = !editing_enabled_ && runtime_active_ |
|
|
const bool disabled = !editing_enabled_ && runtime_active_ |
|
|
&& (!runtime_write_enabled_ || !button_condition_enabled_); |
|
|
&& (!runtime_write_enabled_ || !button_condition_enabled_); |
|
|
const QColor fill = disabled |
|
|
const QColor fill = disabled |
|
|
@@ -247,6 +273,7 @@ public: |
|
|
case HmiControlType::Indicator: |
|
|
case HmiControlType::Indicator: |
|
|
{ |
|
|
{ |
|
|
// 指示灯颜色由最近一次读取到的 M 位值决定 |
|
|
// 指示灯颜色由最近一次读取到的 M 位值决定 |
|
|
|
|
|
// 取宽高较小值保证灯始终是圆形,并给下方说明文字预留高度 |
|
|
const qreal diameter = std::min(rect.width(), rect.height() - 18.0); |
|
|
const qreal diameter = std::min(rect.width(), rect.height() - 18.0); |
|
|
const QRectF lamp( |
|
|
const QRectF lamp( |
|
|
rect.center().x() - diameter / 2.0, |
|
|
rect.center().x() - diameter / 2.0, |
|
|
@@ -288,6 +315,7 @@ public: |
|
|
} |
|
|
} |
|
|
case HmiControlType::StatusText: |
|
|
case HmiControlType::StatusText: |
|
|
{ |
|
|
{ |
|
|
|
|
|
// TextWordWrap 允许状态文字在控件固定宽度内自动换行 |
|
|
painter->setPen(configuredTextColor(QColor(QStringLiteral("#24313b")))); |
|
|
painter->setPen(configuredTextColor(QColor(QStringLiteral("#24313b")))); |
|
|
painter->drawText( |
|
|
painter->drawText( |
|
|
rect.adjusted(4, 0, -4, 0), |
|
|
rect.adjusted(4, 0, -4, 0), |
|
|
@@ -297,6 +325,7 @@ public: |
|
|
} |
|
|
} |
|
|
case HmiControlType::PageJump: |
|
|
case HmiControlType::PageJump: |
|
|
{ |
|
|
{ |
|
|
|
|
|
// 页面跳转只在运行态显示悬停反馈,编辑态点击仍用于选中和拖动 |
|
|
const QColor fill = runtime_active_ && page_hovered_ |
|
|
const QColor fill = runtime_active_ && page_hovered_ |
|
|
? QColor(QStringLiteral("#d8eafa")) |
|
|
? QColor(QStringLiteral("#d8eafa")) |
|
|
: QColor(QStringLiteral("#e8f1fa")); |
|
|
: QColor(QStringLiteral("#e8f1fa")); |
|
|
@@ -309,6 +338,7 @@ public: |
|
|
} |
|
|
} |
|
|
case HmiControlType::AlarmList: |
|
|
case HmiControlType::AlarmList: |
|
|
{ |
|
|
{ |
|
|
|
|
|
// 报警列表完全由 QPainter 绘制,没有为每一行创建额外 QWidget |
|
|
painter->setPen(QPen(QColor(QStringLiteral("#9b3a3a")), 1)); |
|
|
painter->setPen(QPen(QColor(QStringLiteral("#9b3a3a")), 1)); |
|
|
painter->setBrush(QColor(QStringLiteral("#ffffff"))); |
|
|
painter->setBrush(QColor(QStringLiteral("#ffffff"))); |
|
|
painter->drawRect(rect); |
|
|
painter->drawRect(rect); |
|
|
@@ -317,6 +347,7 @@ public: |
|
|
painter->fillRect(header, QColor(QStringLiteral("#a63f3f"))); |
|
|
painter->fillRect(header, QColor(QStringLiteral("#a63f3f"))); |
|
|
painter->setPen(Qt::white); |
|
|
painter->setPen(Qt::white); |
|
|
QRectF title_rect = header.adjusted(7, 0, -7, 0); |
|
|
QRectF title_rect = header.adjusted(7, 0, -7, 0); |
|
|
|
|
|
// 页数由控件当前高度和报警记录数量实时计算 |
|
|
const std::size_t page_count = alarmPageCount(); |
|
|
const std::size_t page_count = alarmPageCount(); |
|
|
if (runtime_active_ && page_count > 1U) |
|
|
if (runtime_active_ && page_count > 1U) |
|
|
{ |
|
|
{ |
|
|
@@ -325,10 +356,13 @@ public: |
|
|
const QRectF indicator_rect = alarmPageIndicatorRect(header); |
|
|
const QRectF indicator_rect = alarmPageIndicatorRect(header); |
|
|
title_rect.setRight(previous_rect.left() - kAlarmColumnSpacing); |
|
|
title_rect.setRight(previous_rect.left() - kAlarmColumnSpacing); |
|
|
|
|
|
|
|
|
|
|
|
// QStyleOption 把区域和启用状态交给当前系统主题绘制标准箭头 |
|
|
QStyleOption previous_option; |
|
|
QStyleOption previous_option; |
|
|
|
|
|
// QStyle 接受整数像素矩形,因此将 QRectF 对齐到设备像素 |
|
|
previous_option.rect = previous_rect.toAlignedRect(); |
|
|
previous_option.rect = previous_rect.toAlignedRect(); |
|
|
previous_option.state = alarm_page_ > 0U |
|
|
previous_option.state = alarm_page_ > 0U |
|
|
? QStyle::State_Enabled : QStyle::State_None; |
|
|
? QStyle::State_Enabled : QStyle::State_None; |
|
|
|
|
|
// QApplication::style 返回整个应用当前使用的 Qt 样式对象 |
|
|
QApplication::style()->drawPrimitive( |
|
|
QApplication::style()->drawPrimitive( |
|
|
QStyle::PE_IndicatorArrowLeft, |
|
|
QStyle::PE_IndicatorArrowLeft, |
|
|
&previous_option, |
|
|
&previous_option, |
|
|
@@ -355,6 +389,7 @@ public: |
|
|
painter->drawText( |
|
|
painter->drawText( |
|
|
title_rect, |
|
|
title_rect, |
|
|
Qt::AlignVCenter | Qt::AlignLeft, |
|
|
Qt::AlignVCenter | Qt::AlignLeft, |
|
|
|
|
|
// elidedText 在空间不足时用省略号截断,避免标题覆盖分页按钮 |
|
|
painter->fontMetrics().elidedText( |
|
|
painter->fontMetrics().elidedText( |
|
|
title, |
|
|
title, |
|
|
Qt::ElideRight, |
|
|
Qt::ElideRight, |
|
|
@@ -375,9 +410,11 @@ public: |
|
|
: QObject::tr("运行时显示当前报警")); |
|
|
: QObject::tr("运行时显示当前报警")); |
|
|
break; |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
// first_record 把当前页内行号换算成完整报警数组下标 |
|
|
const std::size_t first_record = alarmFirstRecordIndex(); |
|
|
const std::size_t first_record = alarmFirstRecordIndex(); |
|
|
for (int row = 0; row < visible_rows; ++row) |
|
|
for (int row = 0; row < visible_rows; ++row) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// 这里保存引用而不是复制,绘制一行时不会复制整条报警记录 |
|
|
const AlarmRecord &record = alarm_records_[ |
|
|
const AlarmRecord &record = alarm_records_[ |
|
|
first_record + static_cast<std::size_t>(row)]; |
|
|
first_record + static_cast<std::size_t>(row)]; |
|
|
const QRectF row_rect( |
|
|
const QRectF row_rect( |
|
|
@@ -445,6 +482,7 @@ public: |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// option->state 是位标志集合,按位与检查当前图元是否被场景选中 |
|
|
if ((option->state & QStyle::State_Selected) != 0) |
|
|
if ((option->state & QStyle::State_Selected) != 0) |
|
|
{ |
|
|
{ |
|
|
// 选中框独立于控件类型,提示当前可编辑对象 |
|
|
// 选中框独立于控件类型,提示当前可编辑对象 |
|
|
@@ -464,6 +502,7 @@ public: |
|
|
void setInteractionState( |
|
|
void setInteractionState( |
|
|
bool editable, bool runtime_active, bool runtime_write_enabled) |
|
|
bool editable, bool runtime_active, bool runtime_write_enabled) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// 先记住旧状态,用于识别刚刚进入运行态这一条状态边界 |
|
|
const bool runtime_was_active = runtime_active_; |
|
|
const bool runtime_was_active = runtime_active_; |
|
|
if (control_.type == HmiControlType::AlarmList && !runtime_active) |
|
|
if (control_.type == HmiControlType::AlarmList && !runtime_active) |
|
|
{ |
|
|
{ |
|
|
@@ -480,10 +519,12 @@ public: |
|
|
{ |
|
|
{ |
|
|
button_condition_enabled_ = false; |
|
|
button_condition_enabled_ = false; |
|
|
} |
|
|
} |
|
|
|
|
|
// 修改 ItemIsMovable 后,QGraphicsScene 的默认鼠标处理会自动允许或禁止拖动 |
|
|
setFlag(ItemIsMovable, editable); |
|
|
setFlag(ItemIsMovable, editable); |
|
|
setFlag(ItemIsSelectable, editable); |
|
|
setFlag(ItemIsSelectable, editable); |
|
|
if (!editable) |
|
|
if (!editable) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// 进入运行态时清除编辑选框,避免看起来仍能编辑控件 |
|
|
setSelected(false); |
|
|
setSelected(false); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -500,6 +541,7 @@ public: |
|
|
&& (control_.type == HmiControlType::PageJump |
|
|
&& (control_.type == HmiControlType::PageJump |
|
|
|| control_.type == HmiControlType::AlarmList |
|
|
|| control_.type == HmiControlType::AlarmList |
|
|
|| runtime_write_input_enabled); |
|
|
|| runtime_write_input_enabled); |
|
|
|
|
|
// NoButton 会让鼠标事件穿过当前图元,不会进入下面的事件重载 |
|
|
setAcceptedMouseButtons( |
|
|
setAcceptedMouseButtons( |
|
|
editable || runtime_input_enabled ? Qt::LeftButton : Qt::NoButton); |
|
|
editable || runtime_input_enabled ? Qt::LeftButton : Qt::NoButton); |
|
|
|
|
|
|
|
|
@@ -510,10 +552,12 @@ public: |
|
|
&& control_.type == HmiControlType::PageJump; |
|
|
&& control_.type == HmiControlType::PageJump; |
|
|
const bool runtime_alarm_enabled = runtime_active_ |
|
|
const bool runtime_alarm_enabled = runtime_active_ |
|
|
&& control_.type == HmiControlType::AlarmList; |
|
|
&& control_.type == HmiControlType::AlarmList; |
|
|
|
|
|
// 只有显式开启悬停事件后,Qt 才会调用 hoverEnterEvent 和 hoverLeaveEvent |
|
|
setAcceptHoverEvents(runtime_button_enabled || runtime_page_jump_enabled); |
|
|
setAcceptHoverEvents(runtime_button_enabled || runtime_page_jump_enabled); |
|
|
if (runtime_button_enabled || runtime_page_jump_enabled |
|
|
if (runtime_button_enabled || runtime_page_jump_enabled |
|
|
|| runtime_alarm_enabled) |
|
|
|| runtime_alarm_enabled) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// setCursor 只改变鼠标外观,不代表业务写入一定成功 |
|
|
setCursor(Qt::PointingHandCursor); |
|
|
setCursor(Qt::PointingHandCursor); |
|
|
} |
|
|
} |
|
|
else |
|
|
else |
|
|
@@ -526,6 +570,7 @@ public: |
|
|
button_pressed_ = false; |
|
|
button_pressed_ = false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
// update 只把图元标记为待重绘,真正 paint 在下一轮事件循环执行 |
|
|
update(); |
|
|
update(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -536,6 +581,7 @@ public: |
|
|
bit_value_ = bit_value; |
|
|
bit_value_ = bit_value; |
|
|
numeric_value_ = numeric_value; |
|
|
numeric_value_ = numeric_value; |
|
|
has_runtime_value_ = available; |
|
|
has_runtime_value_ = available; |
|
|
|
|
|
// 缓存改变后请求局部重绘,不需要重建整个 QGraphicsScene |
|
|
update(); |
|
|
update(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -559,6 +605,7 @@ public: |
|
|
// 更新报警记录并将当前页限制在有效分页范围内 |
|
|
// 更新报警记录并将当前页限制在有效分页范围内 |
|
|
void setAlarmRecords(const std::vector<AlarmRecord> &records) |
|
|
void setAlarmRecords(const std::vector<AlarmRecord> &records) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// 值复制让图元持有稳定快照,不依赖 AlarmService 内部容器地址 |
|
|
alarm_records_ = records; |
|
|
alarm_records_ = records; |
|
|
const std::size_t page_count = alarmPageCount(); |
|
|
const std::size_t page_count = alarmPageCount(); |
|
|
alarm_page_ = page_count == 0U |
|
|
alarm_page_ = page_count == 0U |
|
|
@@ -570,25 +617,30 @@ protected: |
|
|
// 拖拽过程中将新位置限制在页面可见边界内 |
|
|
// 拖拽过程中将新位置限制在页面可见边界内 |
|
|
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override |
|
|
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override |
|
|
{ |
|
|
{ |
|
|
|
|
|
// ItemPositionChange 发生在位置真正写入前,返回值可以替换即将采用的位置 |
|
|
// 只有开启 ItemIsMovable 拖拽时,才做坐标钳位 |
|
|
// 只有开启 ItemIsMovable 拖拽时,才做坐标钳位 |
|
|
if (change == ItemPositionChange && flags().testFlag(ItemIsMovable)) |
|
|
if (change == ItemPositionChange && flags().testFlag(ItemIsMovable)) |
|
|
{ |
|
|
{ |
|
|
// 在图元层预先截断拖拽坐标,避免控件视觉上越出页面 |
|
|
// 在图元层预先截断拖拽坐标,避免控件视觉上越出页面 |
|
|
|
|
|
// QVariant 是 Qt 通用值容器,这里按该通知契约取回 QPointF |
|
|
QPointF position = value.toPointF(); |
|
|
QPointF position = value.toPointF(); |
|
|
const qreal maximum_x = std::max( |
|
|
const qreal maximum_x = std::max( |
|
|
0.0, static_cast<double>(page_width_ - control_.bounds.width)); |
|
|
0.0, static_cast<double>(page_width_ - control_.bounds.width)); |
|
|
const qreal maximum_y = std::max( |
|
|
const qreal maximum_y = std::max( |
|
|
0.0, static_cast<double>(page_height_ - control_.bounds.height)); |
|
|
0.0, static_cast<double>(page_height_ - control_.bounds.height)); |
|
|
|
|
|
// clamp 把拖动坐标压进闭区间,控件右下角不会越过页面边缘 |
|
|
position.setX(std::clamp(position.x(), 0.0, maximum_x)); |
|
|
position.setX(std::clamp(position.x(), 0.0, maximum_x)); |
|
|
position.setY(std::clamp(position.y(), 0.0, maximum_y)); |
|
|
position.setY(std::clamp(position.y(), 0.0, maximum_y)); |
|
|
return position; |
|
|
return position; |
|
|
} |
|
|
} |
|
|
|
|
|
// 未处理的变化必须交回基类,保留 Qt 默认的选择和可见性处理 |
|
|
return QGraphicsItem::itemChange(change, value); |
|
|
return QGraphicsItem::itemChange(change, value); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 运行态按钮按下时通知外层执行配置的 M 位操作 |
|
|
// 运行态按钮按下时通知外层执行配置的 M 位操作 |
|
|
void mousePressEvent(QGraphicsSceneMouseEvent *event) override |
|
|
void mousePressEvent(QGraphicsSceneMouseEvent *event) override |
|
|
{ |
|
|
{ |
|
|
|
|
|
// event->pos 是图元本地坐标,可直接和 controlRect 内的子区域比较 |
|
|
if (runtime_active_ && control_.type == HmiControlType::AlarmList) |
|
|
if (runtime_active_ && control_.type == HmiControlType::AlarmList) |
|
|
{ |
|
|
{ |
|
|
if (event->pos().y() < kAlarmHeaderHeight) |
|
|
if (event->pos().y() < kAlarmHeaderHeight) |
|
|
@@ -604,6 +656,7 @@ protected: |
|
|
if (alarmPreviousPageRect(header).contains(event->pos()) |
|
|
if (alarmPreviousPageRect(header).contains(event->pos()) |
|
|
&& alarm_page_ > 0U) |
|
|
&& alarm_page_ > 0U) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// prepareGeometryChange 让场景先刷新该图元的几何索引缓存 |
|
|
prepareGeometryChange(); |
|
|
prepareGeometryChange(); |
|
|
--alarm_page_; |
|
|
--alarm_page_; |
|
|
update(); |
|
|
update(); |
|
|
@@ -615,6 +668,7 @@ protected: |
|
|
++alarm_page_; |
|
|
++alarm_page_; |
|
|
update(); |
|
|
update(); |
|
|
} |
|
|
} |
|
|
|
|
|
// accept 表示本次点击已处理,不再交给场景选择或底层图元 |
|
|
event->accept(); |
|
|
event->accept(); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
@@ -653,6 +707,7 @@ protected: |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
// 编辑模式:不进if分支,执行基类事件——只做选中、拖拽 |
|
|
// 编辑模式:不进if分支,执行基类事件——只做选中、拖拽 |
|
|
|
|
|
// 编辑态交给基类完成选中、Ctrl 多选和拖动起点记录 |
|
|
QGraphicsItem::mousePressEvent(event); |
|
|
QGraphicsItem::mousePressEvent(event); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -668,6 +723,7 @@ protected: |
|
|
event->accept(); |
|
|
event->accept(); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
// 非运行输入场景继续使用 QGraphicsItem 默认双击分派 |
|
|
QGraphicsItem::mouseDoubleClickEvent(event); |
|
|
QGraphicsItem::mouseDoubleClickEvent(event); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -695,10 +751,12 @@ protected: |
|
|
event->accept(); |
|
|
event->accept(); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
// 基类先结束本次拖动并更新图元最终 pos |
|
|
QGraphicsItem::mouseReleaseEvent(event); |
|
|
QGraphicsItem::mouseReleaseEvent(event); |
|
|
// 只有ItemIsMovable打开(编辑态),松开鼠标才提交位置给业务层 |
|
|
// 只有ItemIsMovable打开(编辑态),松开鼠标才提交位置给业务层 |
|
|
if (flags().testFlag(ItemIsMovable) && moved_) |
|
|
if (flags().testFlag(ItemIsMovable) && moved_) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// pos 返回图元在父项坐标系的位置,本项目顶层图元的父坐标系就是场景 |
|
|
moved_(control_.id, pos()); |
|
|
moved_(control_.id, pos()); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
@@ -717,6 +775,7 @@ protected: |
|
|
page_hovered_ = true; |
|
|
page_hovered_ = true; |
|
|
update(); |
|
|
update(); |
|
|
} |
|
|
} |
|
|
|
|
|
// 继续交给基类,保留 Qt 对悬停状态的标准处理 |
|
|
QGraphicsItem::hoverEnterEvent(event); |
|
|
QGraphicsItem::hoverEnterEvent(event); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -745,6 +804,7 @@ private: |
|
|
0, |
|
|
0, |
|
|
static_cast<int>((rect.height() - kAlarmHeaderHeight) |
|
|
static_cast<int>((rect.height() - kAlarmHeaderHeight) |
|
|
/ kAlarmRowHeight)); |
|
|
/ kAlarmRowHeight)); |
|
|
|
|
|
// 同时受控件实际高度和项目统一可见行上限约束 |
|
|
return static_cast<std::size_t>(std::min( |
|
|
return static_cast<std::size_t>(std::min( |
|
|
rows_by_height, ProjectLimits::kMaximumVisibleAlarmRows)); |
|
|
rows_by_height, ProjectLimits::kMaximumVisibleAlarmRows)); |
|
|
} |
|
|
} |
|
|
@@ -755,6 +815,7 @@ private: |
|
|
const std::size_t page_size = alarmPageSize(); |
|
|
const std::size_t page_size = alarmPageSize(); |
|
|
return page_size == 0U || alarm_records_.empty() |
|
|
return page_size == 0U || alarm_records_.empty() |
|
|
? 0U |
|
|
? 0U |
|
|
|
|
|
// 加 page_size - 1 是整数除法向上取整的常见写法 |
|
|
: (alarm_records_.size() + page_size - 1U) / page_size; |
|
|
: (alarm_records_.size() + page_size - 1U) / page_size; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -845,6 +906,7 @@ private: |
|
|
{ |
|
|
{ |
|
|
return fallback; |
|
|
return fallback; |
|
|
} |
|
|
} |
|
|
|
|
|
// QColor 能解析 #RRGGBB 等 Qt 颜色文本,isValid 负责拒绝非法配置 |
|
|
const QColor color = QColor(QString::fromUtf8( |
|
|
const QColor color = QColor(QString::fromUtf8( |
|
|
property->second.data(), static_cast<int>(property->second.size()))); |
|
|
property->second.data(), static_cast<int>(property->second.size()))); |
|
|
return color.isValid() ? color : fallback; |
|
|
return color.isValid() ? color : fallback; |
|
|
@@ -857,12 +919,14 @@ private: |
|
|
{ |
|
|
{ |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
// 复制当前字体后修改局部副本,最后一次性写回 painter |
|
|
QFont font = painter->font(); |
|
|
QFont font = painter->font(); |
|
|
const auto font_size = control_.properties.find( |
|
|
const auto font_size = control_.properties.find( |
|
|
HmiAppearanceProperty::kFontSize); |
|
|
HmiAppearanceProperty::kFontSize); |
|
|
if (font_size != control_.properties.cend()) |
|
|
if (font_size != control_.properties.cend()) |
|
|
{ |
|
|
{ |
|
|
bool ok = false; |
|
|
bool ok = false; |
|
|
|
|
|
// toInt 通过 ok 返回转换结果,避免非法文字被当成字号零 |
|
|
const int point_size = QString::fromUtf8( |
|
|
const int point_size = QString::fromUtf8( |
|
|
font_size->second.data(), |
|
|
font_size->second.data(), |
|
|
static_cast<int>(font_size->second.size())).toInt(&ok); |
|
|
static_cast<int>(font_size->second.size())).toInt(&ok); |
|
|
@@ -893,6 +957,7 @@ private: |
|
|
{ |
|
|
{ |
|
|
font.setItalic(font_italic->second == "true"); |
|
|
font.setItalic(font_italic->second == "true"); |
|
|
} |
|
|
} |
|
|
|
|
|
// 后续所有 drawText 都使用这份已经合并配置的字体 |
|
|
painter->setFont(font); |
|
|
painter->setFont(font); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -971,6 +1036,7 @@ private: |
|
|
// 将场景通用图元安全转换为本文件定义的 HMI 控件图元 |
|
|
// 将场景通用图元安全转换为本文件定义的 HMI 控件图元 |
|
|
HmiGraphicsItem *asHmiItem(QGraphicsItem *item) |
|
|
HmiGraphicsItem *asHmiItem(QGraphicsItem *item) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// dynamic_cast 失败返回 nullptr,页面边框等其他图元会被安全跳过 |
|
|
return dynamic_cast<HmiGraphicsItem *>(item); |
|
|
return dynamic_cast<HmiGraphicsItem *>(item); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -986,13 +1052,18 @@ HmiEditorWidget::HmiEditorWidget( |
|
|
editor_service_(editor_service), |
|
|
editor_service_(editor_service), |
|
|
runtime_service_(runtime_service), |
|
|
runtime_service_(runtime_service), |
|
|
alarm_service_(alarm_service), |
|
|
alarm_service_(alarm_service), |
|
|
|
|
|
// scene 以视图为 QObject 父对象,视图析构时会自动释放场景 |
|
|
scene_(new QGraphicsScene(this)) |
|
|
scene_(new QGraphicsScene(this)) |
|
|
{ |
|
|
{ |
|
|
setObjectName(QStringLiteral("hmiEditorWidget")); |
|
|
setObjectName(QStringLiteral("hmiEditorWidget")); |
|
|
|
|
|
// setScene 只把视图连接到场景,场景所有权仍由上面的 parent 关系决定 |
|
|
setScene(scene_); |
|
|
setScene(scene_); |
|
|
|
|
|
// 视图级抗锯齿会传递给场景中每个图元的 QPainter |
|
|
setRenderHint(QPainter::Antialiasing, true); |
|
|
setRenderHint(QPainter::Antialiasing, true); |
|
|
|
|
|
// RubberBandDrag 开启鼠标拖框多选,运行态会切换为 NoDrag |
|
|
setDragMode(QGraphicsView::RubberBandDrag); |
|
|
setDragMode(QGraphicsView::RubberBandDrag); |
|
|
setBackgroundBrush(QColor(QStringLiteral("#dfe5e9"))); |
|
|
setBackgroundBrush(QColor(QStringLiteral("#dfe5e9"))); |
|
|
|
|
|
// 场景内任一图元选择变化都会统一转换成业务控件 ID 信号 |
|
|
connect(scene_, &QGraphicsScene::selectionChanged, |
|
|
connect(scene_, &QGraphicsScene::selectionChanged, |
|
|
this, &HmiEditorWidget::handleSelectionChanged); |
|
|
this, &HmiEditorWidget::handleSelectionChanged); |
|
|
} |
|
|
} |
|
|
@@ -1013,6 +1084,7 @@ void HmiEditorWidget::setPageId(const std::string &page_id) |
|
|
void HmiEditorWidget::setEditingEnabled(bool enabled) |
|
|
void HmiEditorWidget::setEditingEnabled(bool enabled) |
|
|
{ |
|
|
{ |
|
|
editing_enabled_ = enabled; |
|
|
editing_enabled_ = enabled; |
|
|
|
|
|
// NoDrag 只关闭视图的框选拖动,具体图元权限还要在下一行同步 |
|
|
setDragMode(enabled ? QGraphicsView::RubberBandDrag : QGraphicsView::NoDrag); |
|
|
setDragMode(enabled ? QGraphicsView::RubberBandDrag : QGraphicsView::NoDrag); |
|
|
updateItemInteractions(); |
|
|
updateItemInteractions(); |
|
|
} |
|
|
} |
|
|
@@ -1024,6 +1096,7 @@ void HmiEditorWidget::setRuntimeActive(bool active) |
|
|
if (!runtime_active_) |
|
|
if (!runtime_active_) |
|
|
{ |
|
|
{ |
|
|
runtime_write_enabled_ = false; |
|
|
runtime_write_enabled_ = false; |
|
|
|
|
|
// scene->items 返回场景当前全部图元,包含页面边框和 HMI 控件 |
|
|
for (QGraphicsItem *item : scene_->items()) |
|
|
for (QGraphicsItem *item : scene_->items()) |
|
|
{ |
|
|
{ |
|
|
HmiGraphicsItem *control_item = asHmiItem(item); |
|
|
HmiGraphicsItem *control_item = asHmiItem(item); |
|
|
@@ -1050,19 +1123,25 @@ void HmiEditorWidget::reloadPage() |
|
|
{ |
|
|
{ |
|
|
// 模型发生变化后完全重建图元,避免增量刷新遗漏属性或选择状态 |
|
|
// 模型发生变化后完全重建图元,避免增量刷新遗漏属性或选择状态 |
|
|
// 画布始终从当前领域页面重建,避免保留已删除控件的图元 |
|
|
// 画布始终从当前领域页面重建,避免保留已删除控件的图元 |
|
|
|
|
|
// clear 会从场景移除并 delete 所有图元,因此旧图元不会泄漏 |
|
|
scene_->clear(); |
|
|
scene_->clear(); |
|
|
const HmiPage *page = editor_service_.findPage(page_id_); |
|
|
const HmiPage *page = editor_service_.findPage(page_id_); |
|
|
if (page == nullptr) |
|
|
if (page == nullptr) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// 空场景矩形让 fitCurrentPage 直接跳过缩放 |
|
|
scene_->setSceneRect({}); |
|
|
scene_->setSceneRect({}); |
|
|
emit controlSelected({}); |
|
|
emit controlSelected({}); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// sceneRect 定义页面的逻辑坐标范围,与视口像素大小不是同一个概念 |
|
|
scene_->setSceneRect(0, 0, page->width, page->height); |
|
|
scene_->setSceneRect(0, 0, page->width, page->height); |
|
|
|
|
|
// addRect 创建的边框图元由 QGraphicsScene 接管所有权 |
|
|
QGraphicsRectItem *page_border = scene_->addRect( |
|
|
QGraphicsRectItem *page_border = scene_->addRect( |
|
|
scene_->sceneRect(), QPen(QColor(QStringLiteral("#8a98a3")), 1), Qt::white); |
|
|
scene_->sceneRect(), QPen(QColor(QStringLiteral("#8a98a3")), 1), Qt::white); |
|
|
|
|
|
// 较小 Z 值把白色页面底板放到所有业务控件后面 |
|
|
page_border->setZValue(-1); |
|
|
page_border->setZValue(-1); |
|
|
|
|
|
// 页面底板不接收鼠标,否则空白处点击会命中边框图元 |
|
|
page_border->setAcceptedMouseButtons(Qt::NoButton); |
|
|
page_border->setAcceptedMouseButtons(Qt::NoButton); |
|
|
|
|
|
|
|
|
for (const HmiControl &control : page->controls) |
|
|
for (const HmiControl &control : page->controls) |
|
|
@@ -1093,6 +1172,7 @@ void HmiEditorWidget::reloadPage() |
|
|
alarm_service_.acknowledge(definition_id); |
|
|
alarm_service_.acknowledge(definition_id); |
|
|
refreshRuntimeValues(); |
|
|
refreshRuntimeValues(); |
|
|
}); |
|
|
}); |
|
|
|
|
|
// addItem 后场景接管图元,后续 scene->clear 会统一释放 |
|
|
scene_->addItem(item); |
|
|
scene_->addItem(item); |
|
|
item->setInteractionState( |
|
|
item->setInteractionState( |
|
|
editing_enabled_, runtime_active_, runtime_write_enabled_); |
|
|
editing_enabled_, runtime_active_, runtime_write_enabled_); |
|
|
@@ -1104,6 +1184,7 @@ void HmiEditorWidget::reloadPage() |
|
|
// 遍历场景所有图元,找到对应 id 的图元,设置选中,视图滚动到把控件显示出来 |
|
|
// 遍历场景所有图元,找到对应 id 的图元,设置选中,视图滚动到把控件显示出来 |
|
|
void HmiEditorWidget::selectControl(const std::string &control_id) |
|
|
void HmiEditorWidget::selectControl(const std::string &control_id) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// 先清空旧选择,保证这个单选入口最多留下一个选中控件 |
|
|
scene_->clearSelection(); |
|
|
scene_->clearSelection(); |
|
|
for (QGraphicsItem *item : scene_->items()) |
|
|
for (QGraphicsItem *item : scene_->items()) |
|
|
{ |
|
|
{ |
|
|
@@ -1111,6 +1192,7 @@ void HmiEditorWidget::selectControl(const std::string &control_id) |
|
|
if (control_item != nullptr && control_item->controlId() == control_id) |
|
|
if (control_item != nullptr && control_item->controlId() == control_id) |
|
|
{ |
|
|
{ |
|
|
control_item->setSelected(true); |
|
|
control_item->setSelected(true); |
|
|
|
|
|
// ensureVisible 自动滚动视图,使目标图元进入当前视口 |
|
|
ensureVisible(control_item); |
|
|
ensureVisible(control_item); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
@@ -1122,11 +1204,13 @@ void HmiEditorWidget::selectControls( |
|
|
const std::vector<std::string> &control_ids) |
|
|
const std::vector<std::string> &control_ids) |
|
|
{ |
|
|
{ |
|
|
scene_->clearSelection(); |
|
|
scene_->clearSelection(); |
|
|
|
|
|
// 记录第一个命中图元,遍历完成后只滚动一次视口 |
|
|
HmiGraphicsItem *first_selected = nullptr; |
|
|
HmiGraphicsItem *first_selected = nullptr; |
|
|
for (QGraphicsItem *item : scene_->items()) |
|
|
for (QGraphicsItem *item : scene_->items()) |
|
|
{ |
|
|
{ |
|
|
HmiGraphicsItem *control_item = asHmiItem(item); |
|
|
HmiGraphicsItem *control_item = asHmiItem(item); |
|
|
if (control_item == nullptr |
|
|
if (control_item == nullptr |
|
|
|
|
|
// std::find 在业务 ID 集合中确认当前场景图元是否属于目标选择 |
|
|
|| std::find( |
|
|
|| std::find( |
|
|
control_ids.cbegin(), control_ids.cend(), |
|
|
control_ids.cbegin(), control_ids.cend(), |
|
|
control_item->controlId()) == control_ids.cend()) |
|
|
control_item->controlId()) == control_ids.cend()) |
|
|
@@ -1155,6 +1239,7 @@ std::string HmiEditorWidget::selectedControlId() const |
|
|
// 按从上到下、从左到右的顺序返回选中控件标识 |
|
|
// 按从上到下、从左到右的顺序返回选中控件标识 |
|
|
std::vector<std::string> HmiEditorWidget::selectedControlIds() const |
|
|
std::vector<std::string> HmiEditorWidget::selectedControlIds() const |
|
|
{ |
|
|
{ |
|
|
|
|
|
// selectedItems 的返回顺序不是视觉顺序,因此先保存位置再自行排序 |
|
|
std::vector<std::pair<QPointF, std::string>> positioned_ids; |
|
|
std::vector<std::pair<QPointF, std::string>> positioned_ids; |
|
|
for (QGraphicsItem *item : scene_->selectedItems()) |
|
|
for (QGraphicsItem *item : scene_->selectedItems()) |
|
|
{ |
|
|
{ |
|
|
@@ -1162,6 +1247,7 @@ std::vector<std::string> HmiEditorWidget::selectedControlIds() const |
|
|
if (control_item != nullptr) |
|
|
if (control_item != nullptr) |
|
|
{ |
|
|
{ |
|
|
positioned_ids.emplace_back( |
|
|
positioned_ids.emplace_back( |
|
|
|
|
|
// scenePos 是经过所有父图元变换后的最终场景坐标 |
|
|
control_item->scenePos(), control_item->controlId()); |
|
|
control_item->scenePos(), control_item->controlId()); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
@@ -1169,6 +1255,7 @@ std::vector<std::string> HmiEditorWidget::selectedControlIds() const |
|
|
positioned_ids.begin(), positioned_ids.end(), |
|
|
positioned_ids.begin(), positioned_ids.end(), |
|
|
[](const auto &left, const auto &right) |
|
|
[](const auto &left, const auto &right) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// qFuzzyCompare 避免浮点坐标极小误差把同一视觉行错误拆开 |
|
|
if (!qFuzzyCompare(left.first.y(), right.first.y())) |
|
|
if (!qFuzzyCompare(left.first.y(), right.first.y())) |
|
|
{ |
|
|
{ |
|
|
return left.first.y() < right.first.y(); |
|
|
return left.first.y() < right.first.y(); |
|
|
@@ -1176,6 +1263,7 @@ std::vector<std::string> HmiEditorWidget::selectedControlIds() const |
|
|
return left.first.x() < right.first.x(); |
|
|
return left.first.x() < right.first.x(); |
|
|
}); |
|
|
}); |
|
|
std::vector<std::string> ids; |
|
|
std::vector<std::string> ids; |
|
|
|
|
|
// reserve 只预留容量,随后 push_back 时不会频繁重新分配内存 |
|
|
ids.reserve(positioned_ids.size()); |
|
|
ids.reserve(positioned_ids.size()); |
|
|
for (const auto &positioned_id : positioned_ids) |
|
|
for (const auto &positioned_id : positioned_ids) |
|
|
{ |
|
|
{ |
|
|
@@ -1192,6 +1280,7 @@ void HmiEditorWidget::refreshRuntimeValues() |
|
|
{ |
|
|
{ |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
// 每轮从服务层重新读取并更新现有图元缓存,不重建页面和选择状态 |
|
|
for (QGraphicsItem *item : scene_->items()) |
|
|
for (QGraphicsItem *item : scene_->items()) |
|
|
{ |
|
|
{ |
|
|
HmiGraphicsItem *control_item = asHmiItem(item); |
|
|
HmiGraphicsItem *control_item = asHmiItem(item); |
|
|
@@ -1199,6 +1288,7 @@ void HmiEditorWidget::refreshRuntimeValues() |
|
|
{ |
|
|
{ |
|
|
continue; |
|
|
continue; |
|
|
} |
|
|
} |
|
|
|
|
|
// 图元只存创建时快照,运行读取前仍按 ID 获取模型中的最新控件 |
|
|
const HmiControl *control = editor_service_.findControl( |
|
|
const HmiControl *control = editor_service_.findControl( |
|
|
page_id_, control_item->controlId()); |
|
|
page_id_, control_item->controlId()); |
|
|
if (control == nullptr) |
|
|
if (control == nullptr) |
|
|
@@ -1207,11 +1297,13 @@ void HmiEditorWidget::refreshRuntimeValues() |
|
|
} |
|
|
} |
|
|
if (control->type == HmiControlType::AlarmList) |
|
|
if (control->type == HmiControlType::AlarmList) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// records 返回当前报警快照,图元内部再复制一份用于稳定绘制 |
|
|
control_item->setAlarmRecords(alarm_service_.records()); |
|
|
control_item->setAlarmRecords(alarm_service_.records()); |
|
|
continue; |
|
|
continue; |
|
|
} |
|
|
} |
|
|
if (control->type == HmiControlType::Button) |
|
|
if (control->type == HmiControlType::Button) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// 每轮刷新都重新计算按钮条件,相关 M/D 值变化后交互状态立即跟随 |
|
|
const HmiButtonEnabledResult enabled = |
|
|
const HmiButtonEnabledResult enabled = |
|
|
runtime_service_.evaluateButtonEnabled(*control); |
|
|
runtime_service_.evaluateButtonEnabled(*control); |
|
|
control_item->setButtonConditionEnabled( |
|
|
control_item->setButtonConditionEnabled( |
|
|
@@ -1234,6 +1326,7 @@ void HmiEditorWidget::refreshRuntimeValues() |
|
|
// 视图尺寸变化后保持完整页面可见 |
|
|
// 视图尺寸变化后保持完整页面可见 |
|
|
void HmiEditorWidget::resizeEvent(QResizeEvent *event) |
|
|
void HmiEditorWidget::resizeEvent(QResizeEvent *event) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// 先让基类更新视口和滚动条尺寸,再根据新尺寸重新计算缩放 |
|
|
QGraphicsView::resizeEvent(event); |
|
|
QGraphicsView::resizeEvent(event); |
|
|
fitCurrentPage(); |
|
|
fitCurrentPage(); |
|
|
} |
|
|
} |
|
|
@@ -1246,6 +1339,7 @@ void HmiEditorWidget::fitCurrentPage() |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
// KeepAspectRatio 防止页面和控件被非等比拉伸 |
|
|
// KeepAspectRatio 防止页面和控件被非等比拉伸 |
|
|
|
|
|
// fitInView 会重设视图变换矩阵,使给定场景矩形完整落入 viewport |
|
|
fitInView( |
|
|
fitInView( |
|
|
scene_->sceneRect().adjusted(-24, -24, 24, 24), |
|
|
scene_->sceneRect().adjusted(-24, -24, 24, 24), |
|
|
Qt::KeepAspectRatio); |
|
|
Qt::KeepAspectRatio); |
|
|
@@ -1269,6 +1363,7 @@ void HmiEditorWidget::updateItemInteractions() |
|
|
// 将场景选择变化转换为控件标识信号 |
|
|
// 将场景选择变化转换为控件标识信号 |
|
|
void HmiEditorWidget::handleSelectionChanged() |
|
|
void HmiEditorWidget::handleSelectionChanged() |
|
|
{ |
|
|
{ |
|
|
|
|
|
// emit 发出 Qt 信号,属性面板等接收者会根据连接类型立即或排队处理 |
|
|
emit controlSelected(QString::fromStdString(selectedControlId())); |
|
|
emit controlSelected(QString::fromStdString(selectedControlId())); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -1284,16 +1379,19 @@ void HmiEditorWidget::handleControlMoved( |
|
|
} |
|
|
} |
|
|
// 鼠标坐标取整后再交给服务校验并写回模型 |
|
|
// 鼠标坐标取整后再交给服务校验并写回模型 |
|
|
HmiRect bounds = control->bounds; |
|
|
HmiRect bounds = control->bounds; |
|
|
|
|
|
// lround 按四舍五入转整数,比直接 static_cast 截断更符合拖动观感 |
|
|
bounds.x = static_cast<int>(std::lround(position.x())); |
|
|
bounds.x = static_cast<int>(std::lround(position.x())); |
|
|
bounds.y = static_cast<int>(std::lround(position.y())); |
|
|
bounds.y = static_cast<int>(std::lround(position.y())); |
|
|
const HmiEditorResult result = editor_service_.moveControl( |
|
|
const HmiEditorResult result = editor_service_.moveControl( |
|
|
page_id_, control_id, bounds); |
|
|
page_id_, control_id, bounds); |
|
|
if (!result.succeeded) |
|
|
if (!result.succeeded) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// 服务拒绝后发出错误并重载页面,把视觉位置恢复成模型中的合法值 |
|
|
emit editorError(QString::fromStdString(result.message)); |
|
|
emit editorError(QString::fromStdString(result.message)); |
|
|
reloadPage(); |
|
|
reloadPage(); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
// 只在服务提交成功后通知外部刷新工程树、属性和修改状态 |
|
|
emit controlChanged(QString::fromStdString(control_id)); |
|
|
emit controlChanged(QString::fromStdString(control_id)); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -1315,12 +1413,14 @@ void HmiEditorWidget::handleButtonEvent( |
|
|
{ |
|
|
{ |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
// Pressed 和 Released 都进入服务,点动按钮依赖这两个事件成对出现 |
|
|
const HmiRuntimeWriteResult result = runtime_service_.operateButton(*control, event); |
|
|
const HmiRuntimeWriteResult result = runtime_service_.operateButton(*control, event); |
|
|
if (!result.succeeded) |
|
|
if (!result.succeeded) |
|
|
{ |
|
|
{ |
|
|
if (result.error == HmiRuntimeError::ConditionNotMet |
|
|
if (result.error == HmiRuntimeError::ConditionNotMet |
|
|
|| result.error == HmiRuntimeError::ConditionUnavailable) |
|
|
|| result.error == HmiRuntimeError::ConditionUnavailable) |
|
|
{ |
|
|
{ |
|
|
|
|
|
// 条件刚失效时静默刷新按钮置灰状态,不把正常拒绝当通信错误 |
|
|
refreshRuntimeValues(); |
|
|
refreshRuntimeValues(); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
@@ -1342,8 +1442,10 @@ void HmiEditorWidget::handleNumericInputActivated(const std::string &control_id) |
|
|
{ |
|
|
{ |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
// 先读当前值作为输入框初值,读取失败时仍允许从零开始输入 |
|
|
const HmiRuntimeReadResult current = runtime_service_.readControl(*control); |
|
|
const HmiRuntimeReadResult current = runtime_service_.readControl(*control); |
|
|
std::optional<double> value; |
|
|
std::optional<double> value; |
|
|
|
|
|
// 描述符统一告诉 UI 该类型使用浮点输入还是整数输入 |
|
|
if (registerDataTypeDescriptor(control->dataType).floatingPoint) |
|
|
if (registerDataTypeDescriptor(control->dataType).floatingPoint) |
|
|
{ |
|
|
{ |
|
|
value = requestFloatingPointInput( |
|
|
value = requestFloatingPointInput( |
|
|
@@ -1358,6 +1460,7 @@ void HmiEditorWidget::handleNumericInputActivated(const std::string &control_id) |
|
|
const int maximum = control->dataType == RegisterDataType::Int16 |
|
|
const int maximum = control->dataType == RegisterDataType::Int16 |
|
|
? std::numeric_limits<std::int16_t>::max() |
|
|
? std::numeric_limits<std::int16_t>::max() |
|
|
: std::numeric_limits<std::int32_t>::max(); |
|
|
: std::numeric_limits<std::int32_t>::max(); |
|
|
|
|
|
// getInt 内置整数范围和步长校验,取消时 accepted 为 false |
|
|
const int input = QInputDialog::getInt( |
|
|
const int input = QInputDialog::getInt( |
|
|
this, |
|
|
this, |
|
|
tr("输入 %1").arg(QString::fromLatin1( |
|
|
tr("输入 %1").arg(QString::fromLatin1( |
|
|
@@ -1379,6 +1482,7 @@ void HmiEditorWidget::handleNumericInputActivated(const std::string &control_id) |
|
|
{ |
|
|
{ |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
// 服务把 double 按 Int16、Int32、Float32 或 Float64 编码为连续 D 字 |
|
|
const HmiRuntimeWriteResult result = runtime_service_.writeNumericInput( |
|
|
const HmiRuntimeWriteResult result = runtime_service_.writeNumericInput( |
|
|
*control, *value); |
|
|
*control, *value); |
|
|
if (!result.succeeded) |
|
|
if (!result.succeeded) |
|
|
|