| @@ -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); | ||||
| @@ -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 控件在页面坐标系中的位置和尺寸 | ||||
| * | * | ||||
| @@ -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; | ||||
| @@ -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 | ||||
| { | { | ||||
| @@ -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> | ||||
| @@ -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( | ||||
| @@ -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; | ||||
| @@ -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(); | ||||
| @@ -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(); | ||||
| } | } | ||||
| @@ -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}; | ||||
| @@ -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, | ||||