Parcourir la source

feat: 支持 HMI 字体外观属性

main
suyu il y a 1 mois
Parent
révision
9f4bf2b9b8
11 fichiers modifiés avec 430 ajouts et 13 suppressions
  1. +75
    -0
      app/src/domain/hmi_model.cpp
  2. +10
    -0
      app/src/domain/hmi_model.h
  3. +2
    -0
      app/src/domain/project_limits.h
  4. +63
    -9
      app/src/ui/hmi_editor_widget.cpp
  5. +69
    -1
      app/src/ui/main_window.ui
  6. +98
    -0
      app/src/ui/property_panel_controller.cpp
  7. +1
    -0
      app/src/ui/property_panel_controller.h
  8. +36
    -0
      app/tests/domain_tests.cpp
  9. +42
    -0
      app/tests/hmi_editor_service_tests.cpp
  10. +20
    -3
      app/tests/main_window_tests.cpp
  11. +14
    -0
      app/tests/project_management_tests.cpp

+ 75
- 0
app/src/domain/hmi_model.cpp Voir le fichier

@@ -4,6 +4,8 @@
#include "project_limits.h" #include "project_limits.h"


#include <algorithm> #include <algorithm>
#include <cctype>
#include <charconv>


namespace { namespace {


@@ -16,6 +18,75 @@ void setError(std::string *error, const std::string &message)
} }
} }


bool isHexDigit(char value)
{
return std::isxdigit(static_cast<unsigned char>(value)) != 0;
}

bool isValidTextColor(const std::string &value)
{
return value.size() == 7U
&& value.front() == '#'
&& std::all_of(value.cbegin() + 1, value.cend(), isHexDigit);
}

bool parseInteger(const std::string &value, int *result)
{
if (value.empty() || result == nullptr)
{
return false;
}
const char *begin = value.data();
const char *end = begin + value.size();
const auto parsed = std::from_chars(begin, end, *result);
return parsed.ec == std::errc{} && parsed.ptr == end;
}

bool isBooleanValue(const std::string &value)
{
return value == "true" || value == "false";
}

bool validateAppearanceProperty(
const std::string &key, const std::string &value, std::string *error)
{
if (key == HmiAppearanceProperty::kTextColor)
{
if (!isValidTextColor(value))
{
setError(error, "HMI 字体颜色必须是 #RRGGBB 格式");
return false;
}
}
else if (key == HmiAppearanceProperty::kFontSize)
{
int point_size = 0;
if (!parseInteger(value, &point_size)
|| point_size < ProjectLimits::kMinimumHmiFontPointSize
|| point_size > ProjectLimits::kMaximumHmiFontPointSize)
{
setError(
error,
"HMI 字号必须在 "
+ std::to_string(ProjectLimits::kMinimumHmiFontPointSize)
+ "~"
+ std::to_string(ProjectLimits::kMaximumHmiFontPointSize)
+ " 范围内");
return false;
}
}
else if (key == HmiAppearanceProperty::kFontBold
|| key == HmiAppearanceProperty::kFontItalic)
{
if (!isBooleanValue(value))
{
setError(error, "HMI 粗体和斜体属性必须是 true 或 false");
return false;
}
}
return true;
}

} // namespace } // namespace


bool HmiProgressBarConfig::isValid() const bool HmiProgressBarConfig::isValid() const
@@ -87,6 +158,10 @@ bool HmiControl::validate(std::string *error) const
setError(error, "HMI 控件属性值不能超过 4096 个 UTF-8 字节"); setError(error, "HMI 控件属性值不能超过 4096 个 UTF-8 字节");
return false; return false;
} }
if (!validateAppearanceProperty(property.first, property.second, error))
{
return false;
}
} }
const std::optional<RegisterArea> binding_area = const std::optional<RegisterArea> binding_area =
hmiBindingArea(descriptor->bindingKind); hmiBindingArea(descriptor->bindingKind);


+ 10
- 0
app/src/domain/hmi_model.h Voir le fichier

@@ -15,6 +15,16 @@
#include <string> #include <string>
#include <vector> #include <vector>


namespace HmiAppearanceProperty
{

inline constexpr const char kTextColor[] = "textColor";
inline constexpr const char kFontSize[] = "fontSize";
inline constexpr const char kFontBold[] = "fontBold";
inline constexpr const char kFontItalic[] = "fontItalic";

} // namespace HmiAppearanceProperty

/** /**
* @brief 描述 HMI 控件在页面坐标系中的位置和尺寸 * @brief 描述 HMI 控件在页面坐标系中的位置和尺寸
* *


+ 2
- 0
app/src/domain/project_limits.h Voir le fichier

@@ -32,6 +32,8 @@ constexpr int kMaximumHmiPageWidth = 8192;
constexpr int kMaximumHmiPageHeight = 8192; constexpr int kMaximumHmiPageHeight = 8192;
constexpr int kMaximumHmiControlWidth = 8192; constexpr int kMaximumHmiControlWidth = 8192;
constexpr int kMaximumHmiControlHeight = 8192; constexpr int kMaximumHmiControlHeight = 8192;
constexpr int kMinimumHmiFontPointSize = 6;
constexpr int kMaximumHmiFontPointSize = 72;


constexpr std::size_t kMaximumPollAddresses = 1024U; constexpr std::size_t kMaximumPollAddresses = 1024U;
constexpr std::size_t kMaximumPollBlocks = 64U; constexpr std::size_t kMaximumPollBlocks = 64U;


+ 63
- 9
app/src/ui/hmi_editor_widget.cpp Voir le fichier

@@ -3,8 +3,10 @@
#include "services/hmi_editor_service.h" #include "services/hmi_editor_service.h"
#include "services/hmi_runtime_service.h" #include "services/hmi_runtime_service.h"
#include "services/alarm_service.h" #include "services/alarm_service.h"
#include "domain/project_limits.h"


#include <QDateTime> #include <QDateTime>
#include <QFont>
#include <QGraphicsItem> #include <QGraphicsItem>
#include <QGraphicsRectItem> #include <QGraphicsRectItem>
#include <QGraphicsScene> #include <QGraphicsScene>
@@ -98,6 +100,8 @@ public:
painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1)); painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1));
} }


applyConfiguredFont(painter);

switch (control_.type) switch (control_.type)
{ {
case HmiControlType::Button: case HmiControlType::Button:
@@ -115,7 +119,7 @@ public:
: QColor(QStringLiteral("#5e816b")); : QColor(QStringLiteral("#5e816b"));
const QColor text = disabled const QColor text = disabled
? QColor(QStringLiteral("#7a837e")) ? QColor(QStringLiteral("#7a837e"))
: QColor(QStringLiteral("#205c3b"));
: configuredTextColor(QColor(QStringLiteral("#205c3b")));


// 未按下时保留下沿阴影,按下后将按钮面下移形成明确的回弹感 // 未按下时保留下沿阴影,按下后将按钮面下移形成明确的回弹感
QRectF face = rect.adjusted(0, 0, 0, -2); QRectF face = rect.adjusted(0, 0, 0, -2);
@@ -150,7 +154,7 @@ public:
painter->setBrush(bit_value_ ? QColor(QStringLiteral("#24a148")) painter->setBrush(bit_value_ ? QColor(QStringLiteral("#24a148"))
: QColor(QStringLiteral("#b8c1c8"))); : QColor(QStringLiteral("#b8c1c8")));
painter->drawEllipse(lamp); painter->drawEllipse(lamp);
painter->setPen(QColor(QStringLiteral("#24313b")));
painter->setPen(configuredTextColor(QColor(QStringLiteral("#24313b"))));
painter->drawText( painter->drawText(
QRectF(rect.left(), lamp.bottom() + 1, rect.width(), 16), QRectF(rect.left(), lamp.bottom() + 1, rect.width(), 16),
Qt::AlignCenter, Qt::AlignCenter,
@@ -163,7 +167,7 @@ public:
// 数值显示为只读样式,文本由运行值刷新 // 数值显示为只读样式,文本由运行值刷新
painter->setBrush(QColor(QStringLiteral("#edf2f6"))); painter->setBrush(QColor(QStringLiteral("#edf2f6")));
painter->drawRect(rect); painter->drawRect(rect);
painter->setPen(QColor(QStringLiteral("#24313b")));
painter->setPen(configuredTextColor(QColor(QStringLiteral("#24313b"))));
painter->drawText(rect.adjusted(7, 0, -7, 0), painter->drawText(rect.adjusted(7, 0, -7, 0),
Qt::AlignVCenter | Qt::AlignLeft, Qt::AlignVCenter | Qt::AlignLeft,
textWithValue()); textWithValue());
@@ -174,7 +178,7 @@ public:
// 数值输入以白色编辑框样式呈现,双击后才请求写入 // 数值输入以白色编辑框样式呈现,双击后才请求写入
painter->setBrush(QColor(QStringLiteral("#ffffff"))); painter->setBrush(QColor(QStringLiteral("#ffffff")));
painter->drawRoundedRect(rect, 3, 3); painter->drawRoundedRect(rect, 3, 3);
painter->setPen(QColor(QStringLiteral("#24313b")));
painter->setPen(configuredTextColor(QColor(QStringLiteral("#24313b"))));
painter->drawText(rect.adjusted(7, 0, -7, 0), painter->drawText(rect.adjusted(7, 0, -7, 0),
Qt::AlignVCenter | Qt::AlignLeft, Qt::AlignVCenter | Qt::AlignLeft,
textWithValue()); textWithValue());
@@ -216,7 +220,7 @@ public:
? percentage_text ? percentage_text
: display_text + QStringLiteral(": ") + percentage_text; : display_text + QStringLiteral(": ") + percentage_text;
} }
painter->setPen(QColor(QStringLiteral("#20342a")));
painter->setPen(configuredTextColor(QColor(QStringLiteral("#20342a"))));
painter->drawText(rect.adjusted(6, 0, -6, 0), painter->drawText(rect.adjusted(6, 0, -6, 0),
Qt::AlignCenter, Qt::AlignCenter,
display_text); display_text);
@@ -230,7 +234,7 @@ public:
painter->setBrush(fill); painter->setBrush(fill);
painter->setPen(QPen(QColor(QStringLiteral("#4e789f")), 1)); painter->setPen(QPen(QColor(QStringLiteral("#4e789f")), 1));
painter->drawRoundedRect(rect, 4, 4); painter->drawRoundedRect(rect, 4, 4);
painter->setPen(QColor(QStringLiteral("#244b6b")));
painter->setPen(configuredTextColor(QColor(QStringLiteral("#244b6b"))));
painter->drawText(rect, Qt::AlignCenter, textWithValue()); painter->drawText(rect, Qt::AlignCenter, textWithValue());
break; break;
} }
@@ -257,7 +261,7 @@ public:
maximum_rows, static_cast<int>(alarm_records_.size())); maximum_rows, static_cast<int>(alarm_records_.size()));
if (visible_rows == 0) if (visible_rows == 0)
{ {
painter->setPen(QColor(QStringLiteral("#6f7a82")));
painter->setPen(configuredTextColor(QColor(QStringLiteral("#6f7a82"))));
painter->drawText( painter->drawText(
QRectF( QRectF(
rect.left(), rect.left(),
@@ -282,7 +286,7 @@ public:
QColor(QStringLiteral("#fde8e8"))); QColor(QStringLiteral("#fde8e8")));
painter->setPen(QColor(QStringLiteral("#d3d8dc"))); painter->setPen(QColor(QStringLiteral("#d3d8dc")));
painter->drawLine(row_rect.bottomLeft(), row_rect.bottomRight()); painter->drawLine(row_rect.bottomLeft(), row_rect.bottomRight());
painter->setPen(QColor(QStringLiteral("#8d1f1f")));
painter->setPen(configuredTextColor(QColor(QStringLiteral("#8d1f1f"))));
const QString state = record.acknowledged const QString state = record.acknowledged
? QObject::tr("已确认") : QObject::tr("未确认"); ? QObject::tr("已确认") : QObject::tr("未确认");
painter->drawText( painter->drawText(
@@ -309,7 +313,7 @@ public:
default: default:
{ {
// 标签只显示固定文本,不绑定寄存器运行值 // 标签只显示固定文本,不绑定寄存器运行值
painter->setPen(QColor(QStringLiteral("#24313b")));
painter->setPen(configuredTextColor(QColor(QStringLiteral("#24313b"))));
painter->drawText(rect, Qt::AlignCenter, painter->drawText(rect, Qt::AlignCenter,
QString::fromUtf8(control_.text.data(), QString::fromUtf8(control_.text.data(),
static_cast<int>(control_.text.size()))); static_cast<int>(control_.text.size())));
@@ -577,6 +581,56 @@ private:
: QString{}; : 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);
}
}
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 QString textWithValue() const
{ {


+ 69
- 1
app/src/ui/main_window.ui Voir le fichier

@@ -679,7 +679,75 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="13" column="0" colspan="2">
<item row="13" column="0">
<widget class="QLabel" name="textColorLabel">
<property name="text">
<string>字体颜色</string>
</property>
</widget>
</item>
<item row="13" column="1">
<widget class="QWidget" name="textColorContainer">
<layout class="QHBoxLayout" name="textColorLayout">
<property name="leftMargin"><number>0</number></property>
<property name="topMargin"><number>0</number></property>
<property name="rightMargin"><number>0</number></property>
<property name="bottomMargin"><number>0</number></property>
<item>
<widget class="QLineEdit" name="textColorEdit">
<property name="placeholderText">
<string>默认,例如 #E53935</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="textColorButton">
<property name="text">
<string>选择</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="14" column="0">
<widget class="QLabel" name="fontSizeLabel">
<property name="text">
<string>字号</string>
</property>
</widget>
</item>
<item row="14" column="1">
<widget class="QSpinBox" name="fontSizeSpinBox"/>
</item>
<item row="15" column="0">
<widget class="QLabel" name="fontBoldLabel">
<property name="text">
<string>字体样式</string>
</property>
</widget>
</item>
<item row="15" column="1">
<widget class="QWidget" name="fontStyleContainer">
<layout class="QHBoxLayout" name="fontStyleLayout">
<property name="leftMargin"><number>0</number></property>
<property name="topMargin"><number>0</number></property>
<property name="rightMargin"><number>0</number></property>
<property name="bottomMargin"><number>0</number></property>
<item>
<widget class="QCheckBox" name="fontBoldCheckBox">
<property name="text"><string>粗体</string></property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="fontItalicCheckBox">
<property name="text"><string>斜体</string></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="16" column="0" colspan="2">
<widget class="QPushButton" name="applyPropertiesButton"> <widget class="QPushButton" name="applyPropertiesButton">
<property name="text"> <property name="text">
<string>应用属性</string> <string>应用属性</string>


+ 98
- 0
app/src/ui/property_panel_controller.cpp Voir le fichier

@@ -1,6 +1,7 @@
#include "property_panel_controller.h" #include "property_panel_controller.h"


#include "domain/hmi_control_registry.h" #include "domain/hmi_control_registry.h"
#include "domain/project_limits.h"
#include "hmi_editor_widget.h" #include "hmi_editor_widget.h"
#include "logic_editor_widget.h" #include "logic_editor_widget.h"
#include "logic_instruction_dialog.h" #include "logic_instruction_dialog.h"
@@ -9,7 +10,9 @@
#include "services/project_service.h" #include "services/project_service.h"
#include "ui_main_window.h" #include "ui_main_window.h"


#include <QApplication>
#include <QCheckBox> #include <QCheckBox>
#include <QColorDialog>
#include <QComboBox> #include <QComboBox>
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
@@ -19,6 +22,7 @@
#include <QStackedWidget> #include <QStackedWidget>
#include <QWidget> #include <QWidget>


#include <algorithm>
#include <cstdint> #include <cstdint>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -68,6 +72,9 @@ PropertyPanelController::PropertyPanelController(


void PropertyPanelController::configure() void PropertyPanelController::configure()
{ {
ui_.fontSizeSpinBox->setRange(
ProjectLimits::kMinimumHmiFontPointSize,
ProjectLimits::kMaximumHmiFontPointSize);
ui_.bindingAreaComboBox->setItemData(0, -1); ui_.bindingAreaComboBox->setItemData(0, -1);
ui_.bindingAreaComboBox->setItemData(1, 0); ui_.bindingAreaComboBox->setItemData(1, 0);
ui_.bindingAreaComboBox->setItemData(2, 1); ui_.bindingAreaComboBox->setItemData(2, 1);
@@ -87,6 +94,8 @@ void PropertyPanelController::configure()
&parent_, [this] { applySelectedControlProperties(); }); &parent_, [this] { applySelectedControlProperties(); });
QObject::connect(ui_.applyLogicPropertiesButton, &QPushButton::clicked, QObject::connect(ui_.applyLogicPropertiesButton, &QPushButton::clicked,
&parent_, [this] { applySelectedLogicNodeProperties(); }); &parent_, [this] { applySelectedLogicNodeProperties(); });
QObject::connect(ui_.textColorButton, &QPushButton::clicked,
&parent_, [this] { chooseTextColor(); });
ui_.targetPageLabel->setVisible(false); ui_.targetPageLabel->setVisible(false);
ui_.targetPageComboBox->setVisible(false); ui_.targetPageComboBox->setVisible(false);
showControlProperties({}); showControlProperties({});
@@ -160,6 +169,11 @@ void PropertyPanelController::showControlProperties(const std::string &control_i
static_cast<QWidget *>(ui_.progressMinimumSpinBox), static_cast<QWidget *>(ui_.progressMinimumSpinBox),
static_cast<QWidget *>(ui_.progressMaximumSpinBox), static_cast<QWidget *>(ui_.progressMaximumSpinBox),
static_cast<QWidget *>(ui_.progressShowValueCheckBox), static_cast<QWidget *>(ui_.progressShowValueCheckBox),
static_cast<QWidget *>(ui_.textColorEdit),
static_cast<QWidget *>(ui_.textColorButton),
static_cast<QWidget *>(ui_.fontSizeSpinBox),
static_cast<QWidget *>(ui_.fontBoldCheckBox),
static_cast<QWidget *>(ui_.fontItalicCheckBox),
static_cast<QWidget *>(ui_.applyPropertiesButton)}) static_cast<QWidget *>(ui_.applyPropertiesButton)})
{ {
widget->setEnabled(has_control); widget->setEnabled(has_control);
@@ -181,6 +195,15 @@ void PropertyPanelController::showControlProperties(const std::string &control_i
ui_.progressMaximumSpinBox->setVisible(false); ui_.progressMaximumSpinBox->setVisible(false);
ui_.progressShowValueLabel->setVisible(false); ui_.progressShowValueLabel->setVisible(false);
ui_.progressShowValueCheckBox->setVisible(false); ui_.progressShowValueCheckBox->setVisible(false);
ui_.textColorEdit->clear();
ui_.textColorButton->setStyleSheet(QString{});
ui_.fontSizeSpinBox->setValue(
std::clamp(
QApplication::font().pointSize(),
ProjectLimits::kMinimumHmiFontPointSize,
ProjectLimits::kMaximumHmiFontPointSize));
ui_.fontBoldCheckBox->setChecked(false);
ui_.fontItalicCheckBox->setChecked(false);
return; return;
} }


@@ -242,6 +265,48 @@ void PropertyPanelController::showControlProperties(const std::string &control_i
ui_.progressMinimumSpinBox->setValue(progress_config.minimumValue); ui_.progressMinimumSpinBox->setValue(progress_config.minimumValue);
ui_.progressMaximumSpinBox->setValue(progress_config.maximumValue); ui_.progressMaximumSpinBox->setValue(progress_config.maximumValue);
ui_.progressShowValueCheckBox->setChecked(progress_config.showValue); ui_.progressShowValueCheckBox->setChecked(progress_config.showValue);

const auto text_color = control->properties.find(
HmiAppearanceProperty::kTextColor);
ui_.textColorEdit->setText(text_color == control->properties.cend()
? QString{}
: fromUtf8(text_color->second));
const QColor swatch_color(ui_.textColorEdit->text());
ui_.textColorButton->setStyleSheet(
swatch_color.isValid()
? QStringLiteral("background-color: %1;").arg(
swatch_color.name(QColor::HexRgb))
: QString{});

const int default_font_size = std::clamp(
QApplication::font().pointSize(),
ProjectLimits::kMinimumHmiFontPointSize,
ProjectLimits::kMaximumHmiFontPointSize);
int font_size = default_font_size;
const auto font_size_property = control->properties.find(
HmiAppearanceProperty::kFontSize);
if (font_size_property != control->properties.cend())
{
bool ok = false;
const int parsed = fromUtf8(font_size_property->second).toInt(&ok);
if (ok)
{
font_size = parsed;
}
}
ui_.fontSizeSpinBox->setValue(std::clamp(
font_size,
ProjectLimits::kMinimumHmiFontPointSize,
ProjectLimits::kMaximumHmiFontPointSize));
const auto font_bold = control->properties.find(
HmiAppearanceProperty::kFontBold);
ui_.fontBoldCheckBox->setChecked(
font_bold != control->properties.cend() && font_bold->second == "true");
const auto font_italic = control->properties.find(
HmiAppearanceProperty::kFontItalic);
ui_.fontItalicCheckBox->setChecked(
font_italic != control->properties.cend()
&& font_italic->second == "true");
} }


void PropertyPanelController::showLogicNodeProperties(const std::string &node_id) void PropertyPanelController::showLogicNodeProperties(const std::string &node_id)
@@ -510,6 +575,21 @@ void PropertyPanelController::applySelectedControlProperties()
static_cast<std::int16_t>(ui_.progressMaximumSpinBox->value()), static_cast<std::int16_t>(ui_.progressMaximumSpinBox->value()),
ui_.progressShowValueCheckBox->isChecked()}; ui_.progressShowValueCheckBox->isChecked()};
} }
const std::string text_color = toUtf8(ui_.textColorEdit->text());
if (text_color.empty())
{
control.properties.erase(HmiAppearanceProperty::kTextColor);
}
else
{
control.properties[HmiAppearanceProperty::kTextColor] = text_color;
}
control.properties[HmiAppearanceProperty::kFontSize] = std::to_string(
ui_.fontSizeSpinBox->value());
control.properties[HmiAppearanceProperty::kFontBold] =
ui_.fontBoldCheckBox->isChecked() ? "true" : "false";
control.properties[HmiAppearanceProperty::kFontItalic] =
ui_.fontItalicCheckBox->isChecked() ? "true" : "false";
const HmiEditorResult result = hmi_editor_service_.updateControl( const HmiEditorResult result = hmi_editor_service_.updateControl(
current_page_id_(), selected_control_id_, control); current_page_id_(), selected_control_id_, control);
if (!result.succeeded) if (!result.succeeded)
@@ -526,6 +606,24 @@ void PropertyPanelController::applySelectedControlProperties()
status_reporter_(QObject::tr("控件属性已更新"), 3000); status_reporter_(QObject::tr("控件属性已更新"), 3000);
} }


void PropertyPanelController::chooseTextColor()
{
QColor initial_color(ui_.textColorEdit->text());
if (!initial_color.isValid())
{
initial_color = QColor(QStringLiteral("#24313b"));
}
const QColor color = QColorDialog::getColor(
initial_color, &parent_, QObject::tr("选择字体颜色"));
if (color.isValid())
{
ui_.textColorEdit->setText(color.name(QColor::HexRgb));
ui_.textColorButton->setStyleSheet(
QStringLiteral("background-color: %1;").arg(
color.name(QColor::HexRgb)));
}
}

void PropertyPanelController::applySelectedLogicNodeProperties() void PropertyPanelController::applySelectedLogicNodeProperties()
{ {
const LogicNode *node = logic_editor_service_.findNode( const LogicNode *node = logic_editor_service_.findNode(


+ 1
- 0
app/src/ui/property_panel_controller.h Voir le fichier

@@ -57,6 +57,7 @@ public:
private: private:
void handleHmiEditorError(const QString &message) const; void handleHmiEditorError(const QString &message) const;
void handleLogicEditorError(const QString &message) const; void handleLogicEditorError(const QString &message) const;
void chooseTextColor();
void reportFailure( void reportFailure(
const QString &action, const std::string &message) const; const QString &action, const std::string &message) const;




+ 36
- 0
app/tests/domain_tests.cpp Voir le fichier

@@ -205,6 +205,41 @@ void testProgressBarConfigurationBoundaries()
"a progress bar must reject an M binding"); "a progress bar must reject an M binding");
} }


Project makeValidProject();

void testHmiAppearancePropertyBoundaries()
{
// 外观属性必须在领域层拒绝格式错误,但不能影响未知扩展属性
Project project = makeValidProject();
HmiControl &button = project.hmiPages.front().controls.front();
button.properties[HmiAppearanceProperty::kTextColor] = "#E53935";
button.properties[HmiAppearanceProperty::kFontSize] = "18";
button.properties[HmiAppearanceProperty::kFontBold] = "true";
button.properties[HmiAppearanceProperty::kFontItalic] = "false";
require(project.validate(), "valid HMI appearance properties must pass validation");

button.properties[HmiAppearanceProperty::kTextColor] = "red";
require(!project.validate(), "text colors must use the #RRGGBB format");

project = makeValidProject();
HmiControl &font_control = project.hmiPages.front().controls.front();
font_control.properties[HmiAppearanceProperty::kFontSize] = "5";
require(!project.validate(), "font sizes below the minimum must be rejected");
font_control.properties[HmiAppearanceProperty::kFontSize] = "73";
require(!project.validate(), "font sizes above the maximum must be rejected");
font_control.properties[HmiAppearanceProperty::kFontSize] = "large";
require(!project.validate(), "non-numeric font sizes must be rejected");

project = makeValidProject();
HmiControl &style_control = project.hmiPages.front().controls.front();
style_control.properties[HmiAppearanceProperty::kFontBold] = "yes";
require(!project.validate(), "font style flags must be true or false");

project = makeValidProject();
project.hmiPages.front().controls.front().properties["legacyColor"] = "green";
require(project.validate(), "unknown HMI extension properties must remain supported");
}

Project makeValidProject() Project makeValidProject()
{ {
// 构造包含 HMI 绑定和完整梯形图网络的最小合法工程作为测试基线 // 构造包含 HMI 绑定和完整梯形图网络的最小合法工程作为测试基线
@@ -856,6 +891,7 @@ int main()
testRegisterRepositorySeparatesAreas(); testRegisterRepositorySeparatesAreas();
testHmiControlRegistryCompleteness(); testHmiControlRegistryCompleteness();
testProgressBarConfigurationBoundaries(); testProgressBarConfigurationBoundaries();
testHmiAppearancePropertyBoundaries();
testLogicNodeConfigurationBoundaries(); testLogicNodeConfigurationBoundaries();
testTimerAndCommentBoundaries(); testTimerAndCommentBoundaries();
testTimerReferencesForRunning(); testTimerReferencesForRunning();


+ 42
- 0
app/tests/hmi_editor_service_tests.cpp Voir le fichier

@@ -240,6 +240,47 @@ void testHistoryAndAtomicBatchDelete()
"HMI history must retain exactly the configured 100 most recent steps"); "HMI history must retain exactly the configured 100 most recent steps");
} }


void testAppearanceEditing()
{
TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService service(project_service);
const std::string page_id = service.ensureDefaultPage().id;
const HmiEditorResult label = service.addControl(page_id, HmiControlType::Label);
require(label.succeeded, "a label must be available for appearance editing");

HmiControl appearance = *service.findControl(page_id, label.id);
appearance.properties[HmiAppearanceProperty::kTextColor] = "#E53935";
appearance.properties[HmiAppearanceProperty::kFontSize] = "18";
appearance.properties[HmiAppearanceProperty::kFontBold] = "true";
appearance.properties[HmiAppearanceProperty::kFontItalic] = "false";
require(service.updateControl(page_id, label.id, appearance).succeeded,
"valid appearance properties must be applied atomically");
const HmiControl *updated = service.findControl(page_id, label.id);
require(updated != nullptr
&& updated->properties.at(HmiAppearanceProperty::kTextColor) == "#E53935"
&& updated->properties.at(HmiAppearanceProperty::kFontSize) == "18"
&& updated->properties.at(HmiAppearanceProperty::kFontBold) == "true",
"appearance properties must be stored on the HMI control");

HmiControl invalid = *updated;
invalid.properties[HmiAppearanceProperty::kTextColor] = "invalid";
require(!service.updateControl(page_id, label.id, invalid).succeeded,
"invalid appearance properties must be rejected without a partial update");
require(service.findControl(page_id, label.id)->properties.at(
HmiAppearanceProperty::kTextColor) == "#E53935",
"failed appearance updates must leave the old color intact");

require(service.undo().succeeded,
"appearance updates must participate in HMI undo history");
require(service.findControl(page_id, label.id)->properties.empty(),
"undo must remove the applied appearance properties");
require(service.redo().succeeded
&& service.findControl(page_id, label.id)->properties.at(
HmiAppearanceProperty::kFontSize) == "18",
"redo must restore the applied appearance properties");
}

void testPageLifecycleAndNavigation() void testPageLifecycleAndNavigation()
{ {
TestProjectStorage storage; TestProjectStorage storage;
@@ -316,6 +357,7 @@ int main()
// 编辑和运行场景分别验证服务层两条独立职责 // 编辑和运行场景分别验证服务层两条独立职责
testControlEditing(); testControlEditing();
testHistoryAndAtomicBatchDelete(); testHistoryAndAtomicBatchDelete();
testAppearanceEditing();
testRuntimeUsesRegisterRepository(); testRuntimeUsesRegisterRepository();
testPageLifecycleAndNavigation(); testPageLifecycleAndNavigation();
} }


+ 20
- 3
app/tests/main_window_tests.cpp Voir le fichier

@@ -783,6 +783,7 @@ void testModeActionsControlEditingAvailability()
QDockWidget *properties_dock = requiredChild<QDockWidget>(window, "propertiesDock"); QDockWidget *properties_dock = requiredChild<QDockWidget>(window, "propertiesDock");
QLabel *selection = requiredChild<QLabel>(window, "selectionValueLabel"); QLabel *selection = requiredChild<QLabel>(window, "selectionValueLabel");
QLineEdit *text_edit = requiredChild<QLineEdit>(window, "controlTextEdit"); QLineEdit *text_edit = requiredChild<QLineEdit>(window, "controlTextEdit");
QLineEdit *text_color_edit = requiredChild<QLineEdit>(window, "textColorEdit");
QComboBox *button_operation = requiredChild<QComboBox>( QComboBox *button_operation = requiredChild<QComboBox>(
window, "buttonOperationComboBox"); window, "buttonOperationComboBox");
QComboBox *binding_area = requiredChild<QComboBox>( QComboBox *binding_area = requiredChild<QComboBox>(
@@ -793,6 +794,9 @@ void testModeActionsControlEditingAvailability()
window, "progressMinimumSpinBox"); window, "progressMinimumSpinBox");
QSpinBox *progress_maximum = requiredChild<QSpinBox>( QSpinBox *progress_maximum = requiredChild<QSpinBox>(
window, "progressMaximumSpinBox"); window, "progressMaximumSpinBox");
QSpinBox *font_size = requiredChild<QSpinBox>(window, "fontSizeSpinBox");
QCheckBox *font_bold = requiredChild<QCheckBox>(window, "fontBoldCheckBox");
QCheckBox *font_italic = requiredChild<QCheckBox>(window, "fontItalicCheckBox");
QCheckBox *progress_show_value = requiredChild<QCheckBox>( QCheckBox *progress_show_value = requiredChild<QCheckBox>(
window, "progressShowValueCheckBox"); window, "progressShowValueCheckBox");
QPushButton *apply_properties = requiredChild<QPushButton>( QPushButton *apply_properties = requiredChild<QPushButton>(
@@ -966,10 +970,23 @@ void testModeActionsControlEditingAvailability()
"an unbound HMI control must not reserve an address label area"); "an unbound HMI control must not reserve an address label area");
button_operation->setCurrentIndex( button_operation->setCurrentIndex(
button_operation->findData(static_cast<int>(HmiButtonOperation::Toggle))); button_operation->findData(static_cast<int>(HmiButtonOperation::Toggle)));
text_color_edit->setText(QStringLiteral("#E53935"));
font_size->setValue(18);
font_bold->setChecked(true);
font_italic->setChecked(true);
apply_properties->click(); apply_properties->click();
require(editor_service.findControl(page_id, "button-1")->buttonOperation
== HmiButtonOperation::Toggle,
"the property panel must update the HMI button operation");
const HmiControl *styled_button = editor_service.findControl(page_id, "button-1");
require(styled_button != nullptr
&& styled_button->buttonOperation == HmiButtonOperation::Toggle
&& styled_button->properties.at(HmiAppearanceProperty::kTextColor)
== "#E53935"
&& styled_button->properties.at(HmiAppearanceProperty::kFontSize)
== "18"
&& styled_button->properties.at(HmiAppearanceProperty::kFontBold)
== "true"
&& styled_button->properties.at(HmiAppearanceProperty::kFontItalic)
== "true",
"the property panel must update HMI appearance properties");


HmiControl bound_button = *editor_service.findControl(page_id, "button-1"); HmiControl bound_button = *editor_service.findControl(page_id, "button-1");
bound_button.binding = RegisterAddress{RegisterArea::M, 0}; bound_button.binding = RegisterAddress{RegisterArea::M, 0};


+ 14
- 0
app/tests/project_management_tests.cpp Voir le fichier

@@ -1,4 +1,5 @@
#include "domain/project_storage.h" #include "domain/project_storage.h"
#include "domain/hmi_model.h"
#include "infrastructure/json_project_storage.h" #include "infrastructure/json_project_storage.h"
#include "services/register_comment_service.h" #include "services/register_comment_service.h"
#include "services/project_service.h" #include "services/project_service.h"
@@ -37,6 +38,10 @@ Project makeExampleProject()
start_button.binding = RegisterAddress{RegisterArea::M, 0}; start_button.binding = RegisterAddress{RegisterArea::M, 0};
start_button.buttonOperation = HmiButtonOperation::SetOn; start_button.buttonOperation = HmiButtonOperation::SetOn;
start_button.properties.emplace("color", "green"); start_button.properties.emplace("color", "green");
start_button.properties.emplace(HmiAppearanceProperty::kTextColor, "#E53935");
start_button.properties.emplace(HmiAppearanceProperty::kFontSize, "18");
start_button.properties.emplace(HmiAppearanceProperty::kFontBold, "true");
start_button.properties.emplace(HmiAppearanceProperty::kFontItalic, "false");


HmiControl running_indicator; HmiControl running_indicator;
running_indicator.id = "running-indicator"; running_indicator.id = "running-indicator";
@@ -487,6 +492,15 @@ void testExampleProjectRoundTrip()
"HMI button operation must survive round trip"); "HMI button operation must survive round trip");
require(project.hmiPages.front().controls.front().properties.at("color") == "green", require(project.hmiPages.front().controls.front().properties.at("color") == "green",
"HMI properties must survive round trip"); "HMI properties must survive round trip");
require(project.hmiPages.front().controls.front().properties.at(
HmiAppearanceProperty::kTextColor) == "#E53935"
&& project.hmiPages.front().controls.front().properties.at(
HmiAppearanceProperty::kFontSize) == "18"
&& project.hmiPages.front().controls.front().properties.at(
HmiAppearanceProperty::kFontBold) == "true"
&& project.hmiPages.front().controls.front().properties.at(
HmiAppearanceProperty::kFontItalic) == "false",
"HMI appearance properties must survive round trip");
require(project.hmiPages.front().controls.at(1).type == HmiControlType::Indicator, require(project.hmiPages.front().controls.at(1).type == HmiControlType::Indicator,
"indicator control type must survive round trip"); "indicator control type must survive round trip");
require(project.hmiPages.front().controls.at(2).binding->area() == RegisterArea::D, require(project.hmiPages.front().controls.at(2).binding->area() == RegisterArea::D,


Chargement…
Annuler
Enregistrer