Quellcode durchsuchen

feat: 增加HMI进度条控件

main
suyu vor 1 Monat
Ursprung
Commit
f2f3fa7358
13 geänderte Dateien mit 487 neuen und 16 gelöschten Zeilen
  1. +10
    -1
      app/src/domain/hmi_control_registry.cpp
  2. +38
    -3
      app/src/domain/hmi_model.cpp
  3. +17
    -1
      app/src/domain/hmi_model.h
  4. +38
    -0
      app/src/infrastructure/json_project_storage.cpp
  5. +4
    -0
      app/src/services/hmi_editor_service.cpp
  6. +42
    -0
      app/src/ui/hmi_editor_widget.cpp
  7. +5
    -0
      app/src/ui/main_window.cpp
  8. +60
    -3
      app/src/ui/main_window.ui
  9. +37
    -2
      app/src/ui/property_panel_controller.cpp
  10. +40
    -0
      app/tests/domain_tests.cpp
  11. +40
    -3
      app/tests/hmi_editor_service_tests.cpp
  12. +113
    -0
      app/tests/main_window_tests.cpp
  13. +43
    -3
      app/tests/project_management_tests.cpp

+ 10
- 1
app/src/domain/hmi_control_registry.cpp Datei anzeigen

@@ -69,7 +69,16 @@ constexpr std::array kControlDescriptors = {
{0, 0, 360, 180}, {0, 0, 360, 180},
HmiBindingKind::None, HmiBindingKind::None,
HmiRuntimeValueKind::None, HmiRuntimeValueKind::None,
false}
false},
HmiControlDescriptor{HmiControlType::ProgressBar,
"progressBar",
"进度条",
"progress-bar",
"进度",
{0, 0, 240, 36},
HmiBindingKind::Word,
HmiRuntimeValueKind::Word,
true}
}; };


static_assert( static_assert(


+ 38
- 3
app/src/domain/hmi_model.cpp Datei anzeigen

@@ -17,6 +17,23 @@ void setError(std::string *error, const std::string &message)


} // namespace } // namespace


bool HmiProgressBarConfig::isValid() const
{
return minimumValue < maximumValue;
}

int HmiProgressBarConfig::percentageForValue(std::int16_t value) const
{
if (!isValid())
{
return 0;
}
const int minimum = minimumValue;
const int maximum = maximumValue;
const int bounded_value = std::clamp(static_cast<int>(value), minimum, maximum);
return (bounded_value - minimum) * 100 / (maximum - minimum);
}

bool HmiControl::validate(std::string *error) const bool HmiControl::validate(std::string *error) const
{ {
const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type); const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type);
@@ -47,7 +64,7 @@ bool HmiControl::validate(std::string *error) const
hmiBindingArea(descriptor->bindingKind); hmiBindingArea(descriptor->bindingKind);
if (binding.has_value() && !binding_area.has_value()) if (binding.has_value() && !binding_area.has_value())
{ {
setError(error, "文本、页面跳转和报警列表控件不能绑定寄存器");
setError(error, "该 HMI 控件不能绑定寄存器");
return false; return false;
} }
if (binding.has_value() && binding_area.has_value() if (binding.has_value() && binding_area.has_value()
@@ -55,11 +72,11 @@ bool HmiControl::validate(std::string *error) const
{ {
if (descriptor->bindingKind == HmiBindingKind::Bit) if (descriptor->bindingKind == HmiBindingKind::Bit)
{ {
setError(error, "按钮和指示灯必须绑定 M 区地址");
setError(error, "HMI 位控件必须绑定 M 区地址");
} }
else else
{ {
setError(error, "数值控件必须绑定 D 区地址");
setError(error, "HMI 字控件必须绑定 D 区地址");
} }
return false; return false;
} }
@@ -81,6 +98,19 @@ bool HmiControl::validate(std::string *error) const
setError(error, "非页面跳转控件不能包含跳转配置"); setError(error, "非页面跳转控件不能包含跳转配置");
return false; return false;
} }
if (type == HmiControlType::ProgressBar)
{
if (!progressBar.has_value() || !progressBar->isValid())
{
setError(error, "进度条最小值必须小于最大值");
return false;
}
}
else if (progressBar.has_value())
{
setError(error, "非进度条控件不能包含进度条配置");
return false;
}
return true; return true;
} }


@@ -91,6 +121,11 @@ bool HmiControl::isConfigured() const
{ {
return false; return false;
} }
if (type == HmiControlType::ProgressBar
&& (!progressBar.has_value() || !progressBar->isValid()))
{
return false;
}
if (!descriptor->requiresBindingForRunning if (!descriptor->requiresBindingForRunning
&& type != HmiControlType::PageJump) && type != HmiControlType::PageJump)
{ {


+ 17
- 1
app/src/domain/hmi_model.h Datei anzeigen

@@ -9,6 +9,7 @@


#include "register_address.h" #include "register_address.h"


#include <cstdint>
#include <map> #include <map>
#include <optional> #include <optional>
#include <string> #include <string>
@@ -44,6 +45,7 @@ enum class HmiControlType
Label, // 标签 Label, // 标签
PageJump, // 页面跳转 PageJump, // 页面跳转
AlarmList, // 报警列表 AlarmList, // 报警列表
ProgressBar, // 进度条
Count // 已注册控件类型数量,不作为实际控件使用 Count // 已注册控件类型数量,不作为实际控件使用
}; };


@@ -63,6 +65,19 @@ struct HmiPageJumpConfig
std::string targetPageId; std::string targetPageId;
}; };


/**
* @brief 进度条的有效值范围和文字显示方式
*/
struct HmiProgressBarConfig
{
std::int16_t minimumValue = 0;
std::int16_t maximumValue = 100;
bool showValue = true;

bool isValid() const;
int percentageForValue(std::int16_t value) const;
};

/** /**
* @brief 描述一个可保存的 HMI 控件及其显示和寄存器配置 * @brief 描述一个可保存的 HMI 控件及其显示和寄存器配置
* *
@@ -78,13 +93,14 @@ struct HmiControl
std::map<std::string, std::string> properties; std::map<std::string, std::string> properties;
HmiButtonOperation buttonOperation = HmiButtonOperation::MomentaryOn; HmiButtonOperation buttonOperation = HmiButtonOperation::MomentaryOn;
std::optional<HmiPageJumpConfig> pageJump; std::optional<HmiPageJumpConfig> pageJump;
std::optional<HmiProgressBarConfig> progressBar;


/** /**
* @brief 校验控件的标识、尺寸、扩展属性和寄存器绑定 * @brief 校验控件的标识、尺寸、扩展属性和寄存器绑定
* @param error 可选错误输出,失败时写入首个校验原因 * @param error 可选错误输出,失败时写入首个校验原因
* @return 配置完整且满足控件类型绑定规则时返回 true * @return 配置完整且满足控件类型绑定规则时返回 true
* *
* 按钮和指示灯必须绑定有效 M 地址,数值显示和数值输入必须绑定有效 D 地址
* 位控件必须绑定有效 M 地址,字控件必须绑定有效 D 地址
*/ */
bool validate(std::string *error = nullptr) const; bool validate(std::string *error = nullptr) const;
bool isConfigured() const; bool isConfigured() const;


+ 38
- 0
app/src/infrastructure/json_project_storage.cpp Datei anzeigen

@@ -696,6 +696,14 @@ QJsonObject serializeHmiControl(const HmiControl &control)
? control.pageJump->targetPageId ? control.pageJump->targetPageId
: std::string{})); : std::string{}));
} }
if (control.type == HmiControlType::ProgressBar)
{
const HmiProgressBarConfig config = control.progressBar.value_or(
HmiProgressBarConfig{});
object.insert(QStringLiteral("minimumValue"), config.minimumValue);
object.insert(QStringLiteral("maximumValue"), config.maximumValue);
object.insert(QStringLiteral("showValue"), config.showValue);
}
return object; return object;
} }


@@ -749,6 +757,36 @@ bool parseHmiControl(
} }
control->pageJump = HmiPageJumpConfig{std::move(target_page_id)}; control->pageJump = HmiPageJumpConfig{std::move(target_page_id)};
} }
if (control->type == HmiControlType::ProgressBar)
{
int minimum_value = 0;
int maximum_value = 0;
bool show_value = true;
if (!readInt(
object,
"minimumValue",
context,
std::numeric_limits<std::int16_t>::min(),
std::numeric_limits<std::int16_t>::max(),
&minimum_value,
state)
|| !readInt(
object,
"maximumValue",
context,
std::numeric_limits<std::int16_t>::min(),
std::numeric_limits<std::int16_t>::max(),
&maximum_value,
state)
|| !readBool(object, "showValue", context, &show_value, state))
{
return false;
}
control->progressBar = HmiProgressBarConfig{
static_cast<std::int16_t>(minimum_value),
static_cast<std::int16_t>(maximum_value),
show_value};
}


// binding 允许为 null,其余非空值必须是合法的寄存器地址对象 // binding 允许为 null,其余非空值必须是合法的寄存器地址对象
if (binding.isNull()) if (binding.isNull())


+ 4
- 0
app/src/services/hmi_editor_service.cpp Datei anzeigen

@@ -461,6 +461,10 @@ HmiControl HmiEditorService::makeControl(
{ {
control.pageJump = HmiPageJumpConfig{}; control.pageJump = HmiPageJumpConfig{};
} }
if (descriptor.type == HmiControlType::ProgressBar)
{
control.progressBar = HmiProgressBarConfig{};
}
const int offset = static_cast<int>(page.controls.size()) * 16; const int offset = static_cast<int>(page.controls.size()) * 16;
control.bounds.x = std::min(20 + offset, page.width - control.bounds.width); control.bounds.x = std::min(20 + offset, page.width - control.bounds.width);
control.bounds.y = std::min(20 + offset, page.height - control.bounds.height); control.bounds.y = std::min(20 + offset, page.height - control.bounds.height);


+ 42
- 0
app/src/ui/hmi_editor_widget.cpp Datei anzeigen

@@ -180,6 +180,48 @@ public:
textWithValue()); textWithValue());
break; break;
} }
case HmiControlType::ProgressBar:
{
const HmiProgressBarConfig config = control_.progressBar.value_or(
HmiProgressBarConfig{});
const int percentage = has_runtime_value_
? config.percentageForValue(word_value_) : 0;

painter->setPen(Qt::NoPen);
painter->setBrush(QColor(QStringLiteral("#e4e9e7")));
painter->drawRoundedRect(rect, 4, 4);

const QRectF track = rect.adjusted(2, 2, -2, -2);
if (has_runtime_value_ && percentage > 0)
{
QRectF fill = track;
fill.setWidth(track.width() * percentage / 100.0);
painter->setBrush(QColor(QStringLiteral("#3c8c62")));
painter->drawRoundedRect(fill, 3, 3);
}

painter->setBrush(Qt::NoBrush);
painter->setPen(QPen(QColor(QStringLiteral("#617069")), 1));
painter->drawRoundedRect(rect, 4, 4);

QString display_text = QString::fromUtf8(
control_.text.data(), static_cast<int>(control_.text.size()));
if (config.showValue)
{
const QString percentage_text = has_runtime_value_
? QString::number(percentage) + QLatin1Char('%')
: runtime_active_ ? QStringLiteral("--")
: QStringLiteral("0%");
display_text = display_text.isEmpty()
? percentage_text
: display_text + QStringLiteral(": ") + percentage_text;
}
painter->setPen(QColor(QStringLiteral("#20342a")));
painter->drawText(rect.adjusted(6, 0, -6, 0),
Qt::AlignCenter,
display_text);
break;
}
case HmiControlType::PageJump: case HmiControlType::PageJump:
{ {
const QColor fill = runtime_active_ && page_hovered_ const QColor fill = runtime_active_ && page_hovered_


+ 5
- 0
app/src/ui/main_window.cpp Datei anzeigen

@@ -279,6 +279,8 @@ void MainWindow::configureActions()
[this] { addHmiControl(HmiControlType::NumericDisplay); }); [this] { addHmiControl(HmiControlType::NumericDisplay); });
connect(ui_->addNumericInputAction, &QAction::triggered, this, connect(ui_->addNumericInputAction, &QAction::triggered, this,
[this] { addHmiControl(HmiControlType::NumericInput); }); [this] { addHmiControl(HmiControlType::NumericInput); });
connect(ui_->addProgressBarAction, &QAction::triggered, this,
[this] { addHmiControl(HmiControlType::ProgressBar); });
connect(ui_->addLabelAction, &QAction::triggered, this, connect(ui_->addLabelAction, &QAction::triggered, this,
[this] { addHmiControl(HmiControlType::Label); }); [this] { addHmiControl(HmiControlType::Label); });
connect(ui_->addPageJumpAction, &QAction::triggered, this, connect(ui_->addPageJumpAction, &QAction::triggered, this,
@@ -576,6 +578,8 @@ void MainWindow::configureAppearance()
style()->standardIcon(QStyle::SP_FileDialogInfoView)); style()->standardIcon(QStyle::SP_FileDialogInfoView));
ui_->addNumericInputAction->setIcon( ui_->addNumericInputAction->setIcon(
style()->standardIcon(QStyle::SP_FileDialogContentsView)); style()->standardIcon(QStyle::SP_FileDialogContentsView));
ui_->addProgressBarAction->setIcon(
style()->standardIcon(QStyle::SP_MediaSeekForward));
ui_->addLabelAction->setIcon( ui_->addLabelAction->setIcon(
style()->standardIcon(QStyle::SP_FileDialogInfoView)); style()->standardIcon(QStyle::SP_FileDialogInfoView));
ui_->addPageJumpAction->setIcon( ui_->addPageJumpAction->setIcon(
@@ -1189,6 +1193,7 @@ void MainWindow::updateModeUi(const QString &message)
ui_->addIndicatorAction->setEnabled(policy.allowsProjectEditing); ui_->addIndicatorAction->setEnabled(policy.allowsProjectEditing);
ui_->addNumericDisplayAction->setEnabled(policy.allowsProjectEditing); ui_->addNumericDisplayAction->setEnabled(policy.allowsProjectEditing);
ui_->addNumericInputAction->setEnabled(policy.allowsProjectEditing); ui_->addNumericInputAction->setEnabled(policy.allowsProjectEditing);
ui_->addProgressBarAction->setEnabled(policy.allowsProjectEditing);
ui_->addLabelAction->setEnabled(policy.allowsProjectEditing); ui_->addLabelAction->setEnabled(policy.allowsProjectEditing);
ui_->addPageJumpAction->setEnabled(policy.allowsProjectEditing); ui_->addPageJumpAction->setEnabled(policy.allowsProjectEditing);
ui_->addAlarmListAction->setEnabled(policy.allowsProjectEditing); ui_->addAlarmListAction->setEnabled(policy.allowsProjectEditing);


+ 60
- 3
app/src/ui/main_window.ui Datei anzeigen

@@ -303,6 +303,7 @@
<addaction name="addIndicatorAction"/> <addaction name="addIndicatorAction"/>
<addaction name="addNumericDisplayAction"/> <addaction name="addNumericDisplayAction"/>
<addaction name="addNumericInputAction"/> <addaction name="addNumericInputAction"/>
<addaction name="addProgressBarAction"/>
<addaction name="addLabelAction"/> <addaction name="addLabelAction"/>
<addaction name="addPageJumpAction"/> <addaction name="addPageJumpAction"/>
<addaction name="addAlarmListAction"/> <addaction name="addAlarmListAction"/>
@@ -324,6 +325,7 @@
</attribute> </attribute>
<addaction name="addRungAction"/> <addaction name="addRungAction"/>
<addaction name="parallelInsertAction"/> <addaction name="parallelInsertAction"/>
<addaction name="deleteLogicAction"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="addNormallyOpenAction"/> <addaction name="addNormallyOpenAction"/>
<addaction name="addNormallyClosedAction"/> <addaction name="addNormallyClosedAction"/>
@@ -342,7 +344,6 @@
<addaction name="addSubAction"/> <addaction name="addSubAction"/>
<addaction name="addCompareAction"/> <addaction name="addCompareAction"/>
<addaction name="editRungCommentAction"/> <addaction name="editRungCommentAction"/>
<addaction name="deleteLogicAction"/>
</widget> </widget>
<widget class="QDockWidget" name="projectDock"> <widget class="QDockWidget" name="projectDock">
<property name="minimumSize"> <property name="minimumSize">
@@ -632,7 +633,55 @@
</item> </item>
</widget> </widget>
</item> </item>
<item row="10" column="0" colspan="2">
<item row="10" column="0">
<widget class="QLabel" name="progressMinimumLabel">
<property name="text">
<string>最小值</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QSpinBox" name="progressMinimumSpinBox">
<property name="minimum">
<number>-32768</number>
</property>
<property name="maximum">
<number>32767</number>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="progressMaximumLabel">
<property name="text">
<string>最大值</string>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QSpinBox" name="progressMaximumSpinBox">
<property name="minimum">
<number>-32768</number>
</property>
<property name="maximum">
<number>32767</number>
</property>
</widget>
</item>
<item row="12" column="0">
<widget class="QLabel" name="progressShowValueLabel">
<property name="text">
<string>数值显示</string>
</property>
</widget>
</item>
<item row="12" column="1">
<widget class="QCheckBox" name="progressShowValueCheckBox">
<property name="text">
<string>显示百分比</string>
</property>
</widget>
</item>
<item row="13" column="0" colspan="2">
<widget class="QPushButton" name="applyPropertiesButton"> <widget class="QPushButton" name="applyPropertiesButton">
<property name="text"> <property name="text">
<string>应用属性</string> <string>应用属性</string>
@@ -928,6 +977,14 @@
<string>添加数值输入控件</string> <string>添加数值输入控件</string>
</property> </property>
</action> </action>
<action name="addProgressBarAction">
<property name="text">
<string>进度条</string>
</property>
<property name="toolTip">
<string>添加进度条控件</string>
</property>
</action>
<action name="addLabelAction"> <action name="addLabelAction">
<property name="text"> <property name="text">
<string>文本</string> <string>文本</string>
@@ -1098,7 +1155,7 @@
</action> </action>
<action name="deleteLogicAction"> <action name="deleteLogicAction">
<property name="text"> <property name="text">
<string>删除</string>
<string>删除节点/网络</string>
</property> </property>
<property name="toolTip"> <property name="toolTip">
<string>删除选中的逻辑节点或网络</string> <string>删除选中的逻辑节点或网络</string>


+ 37
- 2
app/src/ui/property_panel_controller.cpp Datei anzeigen

@@ -9,6 +9,7 @@
#include "services/project_service.h" #include "services/project_service.h"
#include "ui_main_window.h" #include "ui_main_window.h"


#include <QCheckBox>
#include <QComboBox> #include <QComboBox>
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
@@ -155,6 +156,9 @@ void PropertyPanelController::showControlProperties(const std::string &control_i
static_cast<QWidget *>(ui_.bindingIndexSpinBox), static_cast<QWidget *>(ui_.bindingIndexSpinBox),
static_cast<QWidget *>(ui_.targetPageComboBox), static_cast<QWidget *>(ui_.targetPageComboBox),
static_cast<QWidget *>(ui_.buttonOperationComboBox), static_cast<QWidget *>(ui_.buttonOperationComboBox),
static_cast<QWidget *>(ui_.progressMinimumSpinBox),
static_cast<QWidget *>(ui_.progressMaximumSpinBox),
static_cast<QWidget *>(ui_.progressShowValueCheckBox),
static_cast<QWidget *>(ui_.applyPropertiesButton)}) static_cast<QWidget *>(ui_.applyPropertiesButton)})
{ {
widget->setEnabled(has_control); widget->setEnabled(has_control);
@@ -170,6 +174,12 @@ void PropertyPanelController::showControlProperties(const std::string &control_i
ui_.targetPageComboBox->setVisible(false); ui_.targetPageComboBox->setVisible(false);
ui_.buttonOperationLabel->setVisible(false); ui_.buttonOperationLabel->setVisible(false);
ui_.buttonOperationComboBox->setVisible(false); ui_.buttonOperationComboBox->setVisible(false);
ui_.progressMinimumLabel->setVisible(false);
ui_.progressMinimumSpinBox->setVisible(false);
ui_.progressMaximumLabel->setVisible(false);
ui_.progressMaximumSpinBox->setVisible(false);
ui_.progressShowValueLabel->setVisible(false);
ui_.progressShowValueCheckBox->setVisible(false);
return; return;
} }


@@ -215,6 +225,22 @@ void PropertyPanelController::showControlProperties(const std::string &control_i
ui_.targetPageComboBox->setCurrentIndex( ui_.targetPageComboBox->setCurrentIndex(
ui_.targetPageComboBox->findData(fromUtf8(target_id))); ui_.targetPageComboBox->findData(fromUtf8(target_id)));
} }

const bool is_progress_bar = control->type == HmiControlType::ProgressBar;
ui_.progressMinimumLabel->setVisible(is_progress_bar);
ui_.progressMinimumSpinBox->setVisible(is_progress_bar);
ui_.progressMaximumLabel->setVisible(is_progress_bar);
ui_.progressMaximumSpinBox->setVisible(is_progress_bar);
ui_.progressShowValueLabel->setVisible(is_progress_bar);
ui_.progressShowValueCheckBox->setVisible(is_progress_bar);
ui_.progressMinimumSpinBox->setEnabled(is_progress_bar);
ui_.progressMaximumSpinBox->setEnabled(is_progress_bar);
ui_.progressShowValueCheckBox->setEnabled(is_progress_bar);
const HmiProgressBarConfig progress_config = control->progressBar.value_or(
HmiProgressBarConfig{});
ui_.progressMinimumSpinBox->setValue(progress_config.minimumValue);
ui_.progressMaximumSpinBox->setValue(progress_config.maximumValue);
ui_.progressShowValueCheckBox->setChecked(progress_config.showValue);
} }


void PropertyPanelController::showLogicNodeProperties(const std::string &node_id) void PropertyPanelController::showLogicNodeProperties(const std::string &node_id)
@@ -470,6 +496,13 @@ void PropertyPanelController::applySelectedControlProperties()
control.pageJump = HmiPageJumpConfig{ control.pageJump = HmiPageJumpConfig{
toUtf8(ui_.targetPageComboBox->currentData().toString())}; toUtf8(ui_.targetPageComboBox->currentData().toString())};
} }
if (control.type == HmiControlType::ProgressBar)
{
control.progressBar = HmiProgressBarConfig{
static_cast<std::int16_t>(ui_.progressMinimumSpinBox->value()),
static_cast<std::int16_t>(ui_.progressMaximumSpinBox->value()),
ui_.progressShowValueCheckBox->isChecked()};
}
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)
@@ -477,9 +510,11 @@ void PropertyPanelController::applySelectedControlProperties()
reportFailure(QObject::tr("应用属性"), result.message); reportFailure(QObject::tr("应用属性"), result.message);
return; return;
} }
selected_control_id_ = result.id;
const std::string updated_control_id = result.id;
hmi_editor_widget_->reloadPage(); hmi_editor_widget_->reloadPage();
hmi_editor_widget_->selectControl(selected_control_id_);
selected_control_id_ = updated_control_id;
hmi_editor_widget_->selectControl(updated_control_id);
showControlProperties(updated_control_id);
refresh_project_ui_(); refresh_project_ui_();
status_reporter_(QObject::tr("控件属性已更新"), 3000); status_reporter_(QObject::tr("控件属性已更新"), 3000);
} }


+ 40
- 0
app/tests/domain_tests.cpp Datei anzeigen

@@ -164,6 +164,45 @@ void testHmiControlRegistryCompleteness()
"controls without bindings must not resolve a register area"); "controls without bindings must not resolve a register area");
} }


void testProgressBarConfigurationBoundaries()
{
const HmiProgressBarConfig config{-20, 80, true};
require(config.isValid(), "a progress bar range must accept increasing bounds");
require(config.percentageForValue(-20) == 0,
"progress bar minimum must map to zero percent");
require(config.percentageForValue(30) == 50,
"progress bar midpoint must map to fifty percent");
require(config.percentageForValue(80) == 100,
"progress bar maximum must map to one hundred percent");
require(config.percentageForValue(-100) == 0
&& config.percentageForValue(100) == 100,
"progress bar percentages must clamp values outside the range");

HmiProgressBarConfig invalid_range{10, 10, true};
require(!invalid_range.isValid()
&& invalid_range.percentageForValue(10) == 0,
"progress bar must reject an empty range defensively");

HmiControl progress;
progress.id = "progress";
progress.type = HmiControlType::ProgressBar;
progress.progressBar = config;
require(progress.validate(),
"an unbound progress bar with valid configuration must remain a valid draft");
require(!progress.isConfigured(),
"an unbound progress bar must not be ready for running");
progress.binding = RegisterAddress{RegisterArea::D, 0};
require(progress.validate() && progress.isConfigured(),
"a progress bar with a D binding must be ready for running");
progress.progressBar->maximumValue = progress.progressBar->minimumValue;
require(!progress.validate(),
"a progress bar with an invalid range must be rejected");
progress.progressBar = config;
progress.binding = RegisterAddress{RegisterArea::M, 0};
require(!progress.validate(),
"a progress bar must reject an M binding");
}

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


+ 40
- 3
app/tests/hmi_editor_service_tests.cpp Datei anzeigen

@@ -50,12 +50,26 @@ void testControlEditing()
const HmiEditorResult display = service.addControl( const HmiEditorResult display = service.addControl(
page_id, HmiControlType::NumericDisplay); page_id, HmiControlType::NumericDisplay);
const HmiEditorResult input = service.addControl(page_id, HmiControlType::NumericInput); const HmiEditorResult input = service.addControl(page_id, HmiControlType::NumericInput);
require(button.succeeded && indicator.succeeded && display.succeeded && input.succeeded,
"four basic HMI controls must be added");
const HmiEditorResult progress = service.addControl(
page_id, HmiControlType::ProgressBar);
require(button.succeeded && indicator.succeeded && display.succeeded
&& input.succeeded && progress.succeeded,
"basic HMI controls must be added");


const HmiPage *page = service.findPage(page_id); const HmiPage *page = service.findPage(page_id);
require(page != nullptr && page->controls.size() == 4,
require(page != nullptr && page->controls.size() == 5,
"all added controls must be kept in the page model"); "all added controls must be kept in the page model");
const HmiControl *progress_control = service.findControl(page_id, progress.id);
require(progress_control != nullptr
&& progress_control->id == "progress-bar-1"
&& progress_control->text == "进度"
&& progress_control->bounds.width == 240
&& progress_control->bounds.height == 36
&& progress_control->progressBar.has_value()
&& progress_control->progressBar->minimumValue == 0
&& progress_control->progressBar->maximumValue == 100
&& progress_control->progressBar->showValue,
"new progress bars must use the registered defaults");
require(service.moveControl(page_id, button.id, {120, 80, 120, 40}).succeeded, require(service.moveControl(page_id, button.id, {120, 80, 120, 40}).succeeded,
"a valid control move must succeed"); "a valid control move must succeed");
require(!service.moveControl(page_id, button.id, {790, 460, 120, 40}).succeeded, require(!service.moveControl(page_id, button.id, {790, 460, 120, 40}).succeeded,
@@ -80,6 +94,20 @@ void testControlEditing()
"deleting a selected control must succeed"); "deleting a selected control must succeed");
require(service.findControl(page_id, indicator.id) == nullptr, require(service.findControl(page_id, indicator.id) == nullptr,
"deleted controls must not remain in the model"); "deleted controls must not remain in the model");

HmiControl configured_progress = *service.findControl(page_id, progress.id);
configured_progress.binding = RegisterAddress{RegisterArea::D, 8};
configured_progress.progressBar->minimumValue = -10;
configured_progress.progressBar->maximumValue = 90;
require(service.updateControl(page_id, progress.id, configured_progress).succeeded,
"a progress bar must accept a valid D binding and value range");
configured_progress.progressBar->maximumValue = -10;
require(!service.updateControl(page_id, progress.id, configured_progress).succeeded,
"a progress bar must reject a non-increasing value range");
configured_progress.progressBar->maximumValue = 90;
configured_progress.binding = RegisterAddress{RegisterArea::M, 8};
require(!service.updateControl(page_id, progress.id, configured_progress).succeeded,
"a progress bar must reject an M binding");
} }


void testRuntimeUsesRegisterRepository() void testRuntimeUsesRegisterRepository()
@@ -147,6 +175,15 @@ void testRuntimeUsesRegisterRepository()
const HmiRuntimeReadResult numeric_value = runtime_service.readControl(numeric_display); const HmiRuntimeReadResult numeric_value = runtime_service.readControl(numeric_display);
require(numeric_value.succeeded && numeric_value.word_value == -18, require(numeric_value.succeeded && numeric_value.word_value == -18,
"numeric display must read D values through the repository"); "numeric display must read D values through the repository");

HmiControl progress;
progress.id = "progress";
progress.type = HmiControlType::ProgressBar;
progress.binding = RegisterAddress{RegisterArea::D, 9};
progress.progressBar = HmiProgressBarConfig{-20, 80, true};
const HmiRuntimeReadResult progress_value = runtime_service.readControl(progress);
require(progress_value.succeeded && progress_value.word_value == -18,
"progress bars must read D values through the repository");
} }


void testPageLifecycleAndNavigation() void testPageLifecycleAndNavigation()


+ 113
- 0
app/tests/main_window_tests.cpp Datei anzeigen

@@ -21,6 +21,7 @@


#include <QAction> #include <QAction>
#include <QApplication> #include <QApplication>
#include <QCheckBox>
#include <QComboBox> #include <QComboBox>
#include <QDialog> #include <QDialog>
#include <QDockWidget> #include <QDockWidget>
@@ -350,6 +351,70 @@ void testRuntimeButtonMouseInteraction()
"a disabled runtime button must not write its register"); "a disabled runtime button must not write its register");
} }


void testRuntimeProgressBarRendering()
{
TestProjectStorage storage;
ProjectService project_service(storage);
HmiEditorService editor_service(project_service);
VirtualRegisterRepository repository;
HmiRuntimeService runtime_service(repository);
AlarmService alarm_service(project_service, repository);
const std::string page_id = editor_service.ensureDefaultPage().id;
const HmiEditorResult progress_result = editor_service.addControl(
page_id, HmiControlType::ProgressBar);
require(progress_result.succeeded,
"runtime progress test must create a ProgressBar control");

HmiControl progress = *editor_service.findControl(
page_id, progress_result.id);
progress.binding = RegisterAddress{RegisterArea::D, 0};
progress.progressBar = HmiProgressBarConfig{0, 100, true};
require(editor_service.updateControl(
page_id, progress.id, progress).succeeded,
"runtime progress test must configure a D value range");
require(repository.writeWord(
{RegisterArea::D, 0}, static_cast<std::int16_t>(25)).succeeded,
"runtime progress test must seed D0");

HmiEditorWidget view(editor_service, runtime_service, alarm_service);
view.resize(900, 560);
view.setPageId(page_id);
view.setEditingEnabled(false);
view.setRuntimeActive(true);
view.refreshRuntimeValues();
view.show();
QApplication::processEvents();

const QPoint sample = view.mapFromScene(QPointF(
progress.bounds.x + progress.bounds.width * 0.6,
progress.bounds.y + 6.0));
const QColor background_color = renderedColorAt(view, sample);
QGraphicsItem *progress_item = view.itemAt(sample);
require(progress_item != nullptr
&& progress_item->acceptedMouseButtons() == Qt::NoButton,
"runtime ProgressBar must be visible and read-only");

require(repository.writeWord(
{RegisterArea::D, 0}, static_cast<std::int16_t>(75)).succeeded,
"runtime progress test must update D0");
view.refreshRuntimeValues();
QApplication::processEvents();
const QColor fill_color = renderedColorAt(view, sample);
require(fill_color != background_color,
"ProgressBar fill must expand when the D value increases");

require(repository.writeWord(
{RegisterArea::D, 0}, static_cast<std::int16_t>(200)).succeeded,
"runtime progress test must accept an out-of-range source value");
view.refreshRuntimeValues();
QApplication::processEvents();
const QPoint near_end = view.mapFromScene(QPointF(
progress.bounds.x + progress.bounds.width * 0.95,
progress.bounds.y + 6.0));
require(renderedColorAt(view, near_end) == fill_color,
"ProgressBar rendering must clamp values above its maximum");
}

void testRuntimePageJumpDoesNotRequireRegisterWritePermission() void testRuntimePageJumpDoesNotRequireRegisterWritePermission()
{ {
TestProjectStorage storage; TestProjectStorage storage;
@@ -533,6 +598,8 @@ void testModeActionsControlEditingAvailability()
QAction *online_action = requiredChild<QAction>(window, "onlineModeAction"); QAction *online_action = requiredChild<QAction>(window, "onlineModeAction");
QAction *add_button_action = requiredChild<QAction>(window, "addButtonAction"); QAction *add_button_action = requiredChild<QAction>(window, "addButtonAction");
QAction *add_indicator_action = requiredChild<QAction>(window, "addIndicatorAction"); QAction *add_indicator_action = requiredChild<QAction>(window, "addIndicatorAction");
QAction *add_progress_bar_action = requiredChild<QAction>(
window, "addProgressBarAction");
QAction *delete_control_action = requiredChild<QAction>(window, "deleteControlAction"); QAction *delete_control_action = requiredChild<QAction>(window, "deleteControlAction");
QAction *add_normally_open_action = requiredChild<QAction>( QAction *add_normally_open_action = requiredChild<QAction>(
window, "addNormallyOpenAction"); window, "addNormallyOpenAction");
@@ -546,6 +613,16 @@ void testModeActionsControlEditingAvailability()
QLineEdit *text_edit = requiredChild<QLineEdit>(window, "controlTextEdit"); QLineEdit *text_edit = requiredChild<QLineEdit>(window, "controlTextEdit");
QComboBox *button_operation = requiredChild<QComboBox>( QComboBox *button_operation = requiredChild<QComboBox>(
window, "buttonOperationComboBox"); window, "buttonOperationComboBox");
QComboBox *binding_area = requiredChild<QComboBox>(
window, "bindingAreaComboBox");
QSpinBox *binding_index = requiredChild<QSpinBox>(
window, "bindingIndexSpinBox");
QSpinBox *progress_minimum = requiredChild<QSpinBox>(
window, "progressMinimumSpinBox");
QSpinBox *progress_maximum = requiredChild<QSpinBox>(
window, "progressMaximumSpinBox");
QCheckBox *progress_show_value = requiredChild<QCheckBox>(
window, "progressShowValueCheckBox");
QPushButton *apply_properties = requiredChild<QPushButton>( QPushButton *apply_properties = requiredChild<QPushButton>(
window, "applyPropertiesButton"); window, "applyPropertiesButton");
HmiEditorWidget *hmi_editor = requiredChild<HmiEditorWidget>( HmiEditorWidget *hmi_editor = requiredChild<HmiEditorWidget>(
@@ -603,6 +680,37 @@ void testModeActionsControlEditingAvailability()
require(editor_service.findPage(page_id)->controls.empty(), require(editor_service.findPage(page_id)->controls.empty(),
"deleting a selected control must update the HMI page model"); "deleting a selected control must update the HMI page model");


add_progress_bar_action->trigger();
const HmiControl *default_progress = editor_service.findControl(
page_id, "progress-bar-1");
require(default_progress != nullptr
&& default_progress->progressBar.has_value()
&& progress_minimum->isVisible()
&& progress_maximum->isVisible()
&& progress_show_value->isVisible()
&& progress_minimum->value() == 0
&& progress_maximum->value() == 100
&& progress_show_value->isChecked(),
"ProgressBar properties must expose the registered defaults");
progress_minimum->setValue(-20);
progress_maximum->setValue(80);
progress_show_value->setChecked(false);
binding_area->setCurrentIndex(binding_area->findData(1));
binding_index->setValue(4);
apply_properties->click();
const HmiControl *configured_progress = editor_service.findControl(
page_id, "progress-bar-1");
require(configured_progress != nullptr
&& configured_progress->binding
== RegisterAddress{RegisterArea::D, 4}
&& configured_progress->progressBar->minimumValue == -20
&& configured_progress->progressBar->maximumValue == 80
&& !configured_progress->progressBar->showValue,
"the property panel must update ProgressBar binding and range settings");
delete_control_action->trigger();
require(editor_service.findPage(page_id)->controls.empty(),
"deleting a ProgressBar must remove it from the HMI page");

add_indicator_action->trigger(); add_indicator_action->trigger();
HmiControl runtime_indicator = *editor_service.findControl(page_id, "indicator-1"); HmiControl runtime_indicator = *editor_service.findControl(page_id, "indicator-1");
runtime_indicator.binding = RegisterAddress{RegisterArea::M, 3}; runtime_indicator.binding = RegisterAddress{RegisterArea::M, 3};
@@ -663,6 +771,8 @@ void testModeActionsControlEditingAvailability()
"properties dock must be disabled while running"); "properties dock must be disabled while running");
require(!add_button_action->isEnabled(), require(!add_button_action->isEnabled(),
"HMI add controls must be disabled while running"); "HMI add controls must be disabled while running");
require(!add_progress_bar_action->isEnabled(),
"ProgressBar creation must be disabled while running");
require(!add_normally_open_action->isEnabled(), require(!add_normally_open_action->isEnabled(),
"logic add nodes must be disabled while running"); "logic add nodes must be disabled while running");
require(runtime_tab->isVisible() && runtime_hmi->isVisible() require(runtime_tab->isVisible() && runtime_hmi->isVisible()
@@ -704,6 +814,8 @@ void testModeActionsControlEditingAvailability()
"properties dock must be restored after returning to editing"); "properties dock must be restored after returning to editing");
require(add_button_action->isEnabled(), require(add_button_action->isEnabled(),
"HMI add controls must be restored after returning to editing"); "HMI add controls must be restored after returning to editing");
require(add_progress_bar_action->isEnabled(),
"ProgressBar creation must be restored after returning to editing");
require(add_normally_open_action->isEnabled(), require(add_normally_open_action->isEnabled(),
"logic add nodes must be restored after returning to editing"); "logic add nodes must be restored after returning to editing");
require(!runtime_tab->isVisible(), require(!runtime_tab->isVisible(),
@@ -1187,6 +1299,7 @@ int main(int argc, char *argv[])
try try
{ {
testRuntimeButtonMouseInteraction(); testRuntimeButtonMouseInteraction();
testRuntimeProgressBarRendering();
testRuntimePageJumpDoesNotRequireRegisterWritePermission(); testRuntimePageJumpDoesNotRequireRegisterWritePermission();
testRuntimeAlarmListInteraction(); testRuntimeAlarmListInteraction();
testModeActionsControlEditingAvailability(); testModeActionsControlEditingAvailability();


+ 43
- 3
app/tests/project_management_tests.cpp Datei anzeigen

@@ -80,6 +80,14 @@ Project makeExampleProject()
alarm_list.bounds = {10, 200, 360, 180}; alarm_list.bounds = {10, 200, 360, 180};
alarm_list.text = "Alarms"; alarm_list.text = "Alarms";


HmiControl progress_bar;
progress_bar.id = "progress-bar";
progress_bar.type = HmiControlType::ProgressBar;
progress_bar.bounds = {400, 20, 240, 36};
progress_bar.text = "Completion";
progress_bar.binding = RegisterAddress{RegisterArea::D, 4};
progress_bar.progressBar = HmiProgressBarConfig{-20, 80, true};

HmiPage page; HmiPage page;
page.id = "main-page"; page.id = "main-page";
page.name = "Main"; page.name = "Main";
@@ -90,6 +98,7 @@ Project makeExampleProject()
page.controls.push_back(title_label); page.controls.push_back(title_label);
page.controls.push_back(settings_jump); page.controls.push_back(settings_jump);
page.controls.push_back(alarm_list); page.controls.push_back(alarm_list);
page.controls.push_back(progress_bar);


HmiPage settings_page; HmiPage settings_page;
settings_page.id = "settings-page"; settings_page.id = "settings-page";
@@ -379,6 +388,8 @@ void testExampleProjectRoundTrip()
const QString invalid_operation_path = directory.filePath("invalid-operation.json"); const QString invalid_operation_path = directory.filePath("invalid-operation.json");
const QString missing_initial_path = directory.filePath("missing-initial.json"); const QString missing_initial_path = directory.filePath("missing-initial.json");
const QString missing_target_path = directory.filePath("missing-target.json"); const QString missing_target_path = directory.filePath("missing-target.json");
const QString missing_progress_range_path = directory.filePath(
"missing-progress-range.json");
const QString missing_alarms_path = directory.filePath("missing-alarms.json"); const QString missing_alarms_path = directory.filePath("missing-alarms.json");
const QString missing_register_comments_path = directory.filePath( const QString missing_register_comments_path = directory.filePath(
"missing-register-comments.json"); "missing-register-comments.json");
@@ -411,7 +422,11 @@ void testExampleProjectRoundTrip()
&& saved_json.contains("\"operation\": \"add\"") && saved_json.contains("\"operation\": \"add\"")
&& saved_json.contains("\"operation\": \"subtract\"") && saved_json.contains("\"operation\": \"subtract\"")
&& saved_json.contains("\"comment\": \"启动条件与温度检查\"") && saved_json.contains("\"comment\": \"启动条件与温度检查\"")
&& saved_json.contains("\"type\": \"alarmList\""),
&& saved_json.contains("\"type\": \"alarmList\"")
&& saved_json.contains("\"type\": \"progressBar\"")
&& saved_json.contains("\"minimumValue\": -20")
&& saved_json.contains("\"maximumValue\": 80")
&& saved_json.contains("\"showValue\": true"),
"version 1.0 projects must persist pages and alarm definitions"); "version 1.0 projects must persist pages and alarm definitions");
require(!saved_json.contains("\"stages\"") require(!saved_json.contains("\"stages\"")
&& !saved_json.contains("\"branches\""), && !saved_json.contains("\"branches\""),
@@ -427,8 +442,8 @@ void testExampleProjectRoundTrip()
require(project.hmiPages.size() == 2, "HMI page count must survive round trip"); require(project.hmiPages.size() == 2, "HMI page count must survive round trip");
require(project.initialHmiPageId == "main-page", require(project.initialHmiPageId == "main-page",
"the initial HMI page id must survive round trip"); "the initial HMI page id must survive round trip");
require(project.hmiPages.front().controls.size() == 7,
"register, navigation and AlarmList controls must survive round trip");
require(project.hmiPages.front().controls.size() == 8,
"register, navigation, AlarmList and ProgressBar controls must survive round trip");
require(project.hmiPages.front().controls.front().binding->area() require(project.hmiPages.front().controls.front().binding->area()
== RegisterArea::M, == RegisterArea::M,
"HMI M binding must survive round trip"); "HMI M binding must survive round trip");
@@ -450,6 +465,13 @@ void testExampleProjectRoundTrip()
require(project.hmiPages.front().controls.at(6).type require(project.hmiPages.front().controls.at(6).type
== HmiControlType::AlarmList, == HmiControlType::AlarmList,
"AlarmList control type must survive round trip"); "AlarmList control type must survive round trip");
require(project.hmiPages.front().controls.at(7).type
== HmiControlType::ProgressBar
&& project.hmiPages.front().controls.at(7).progressBar.has_value()
&& project.hmiPages.front().controls.at(7).progressBar->minimumValue == -20
&& project.hmiPages.front().controls.at(7).progressBar->maximumValue == 80
&& project.hmiPages.front().controls.at(7).progressBar->showValue,
"ProgressBar configuration must survive round trip");
require(project.alarmDefinitions.size() == 2 require(project.alarmDefinitions.size() == 2
&& project.alarmDefinitions.front().condition && project.alarmDefinitions.front().condition
== AlarmCondition::MOn == AlarmCondition::MOn
@@ -614,6 +636,24 @@ void testExampleProjectRoundTrip()
require(!missing_result.succeeded require(!missing_result.succeeded
&& missing_result.storageError == ProjectStorageError::MissingField, && missing_result.storageError == ProjectStorageError::MissingField,
"the 1.0 schema must require PageJump targetPageId"); "the 1.0 schema must require PageJump targetPageId");

QJsonObject missing_progress_range = QJsonDocument::fromJson(saved_json).object();
pages = missing_progress_range.value(QStringLiteral("hmiPages")).toArray();
main_page = pages.at(0).toObject();
controls = main_page.value(QStringLiteral("controls")).toArray();
QJsonObject progress = controls.at(7).toObject();
progress.remove(QStringLiteral("maximumValue"));
controls.replace(7, progress);
main_page.insert(QStringLiteral("controls"), controls);
pages.replace(0, main_page);
missing_progress_range.insert(QStringLiteral("hmiPages"), pages);
writeText(
missing_progress_range_path,
QJsonDocument(missing_progress_range).toJson(QJsonDocument::Compact));
missing_result = service.load(missing_progress_range_path.toStdString());
require(!missing_result.succeeded
&& missing_result.storageError == ProjectStorageError::MissingField,
"the 1.0 schema must require the ProgressBar value range");
} }


void testInvalidFiles() void testInvalidFiles()


Laden…
Abbrechen
Speichern