diff --git a/app/integrated_platform.pro b/app/integrated_platform.pro index 819eec2..a4949a2 100644 --- a/app/integrated_platform.pro +++ b/app/integrated_platform.pro @@ -32,6 +32,7 @@ SOURCES += \ src/domain/register_monitor_model.cpp \ src/domain/alarm_model.cpp \ src/domain/hmi_model.cpp \ + src/domain/hmi_control_registry.cpp \ src/domain/control_logic_model.cpp \ src/domain/project_model.cpp \ src/domain/runtime_state.cpp \ @@ -71,6 +72,7 @@ HEADERS += \ src/domain/register_monitor_model.h \ src/domain/alarm_model.h \ src/domain/hmi_model.h \ + src/domain/hmi_control_registry.h \ src/domain/control_logic_model.h \ src/domain/project_model.h \ src/domain/runtime_state.h \ diff --git a/app/src/domain/hmi_control_registry.cpp b/app/src/domain/hmi_control_registry.cpp new file mode 100644 index 0000000..963ca3b --- /dev/null +++ b/app/src/domain/hmi_control_registry.cpp @@ -0,0 +1,125 @@ +#include "hmi_control_registry.h" + +#include +#include +#include + +namespace { + +constexpr std::array kControlDescriptors = { + HmiControlDescriptor{HmiControlType::Button, + "button", + "按钮", + "button", + "按钮", + {0, 0, 120, 40}, + HmiBindingKind::Bit, + HmiRuntimeValueKind::Bit, + true}, + HmiControlDescriptor{HmiControlType::Indicator, + "indicator", + "指示灯", + "indicator", + "指示灯", + {0, 0, 64, 64}, + HmiBindingKind::Bit, + HmiRuntimeValueKind::Bit, + true}, + HmiControlDescriptor{HmiControlType::NumericDisplay, + "numericDisplay", + "数值显示", + "numeric-display", + "数值显示", + {0, 0, 120, 40}, + HmiBindingKind::Word, + HmiRuntimeValueKind::Word, + true}, + HmiControlDescriptor{HmiControlType::NumericInput, + "numericInput", + "数值输入", + "numeric-input", + "数值输入", + {0, 0, 120, 40}, + HmiBindingKind::Word, + HmiRuntimeValueKind::Word, + true}, + HmiControlDescriptor{HmiControlType::Label, + "label", + "文本", + "label", + "文本", + {0, 0, 120, 40}, + HmiBindingKind::None, + HmiRuntimeValueKind::None, + false}, + HmiControlDescriptor{HmiControlType::PageJump, + "pageJump", + "页面跳转", + "page-jump", + "页面跳转", + {0, 0, 120, 40}, + HmiBindingKind::None, + HmiRuntimeValueKind::None, + false}, + HmiControlDescriptor{HmiControlType::AlarmList, + "alarmList", + "报警列表", + "alarm-list", + "报警信息", + {0, 0, 360, 180}, + HmiBindingKind::None, + HmiRuntimeValueKind::None, + false} +}; + +static_assert( + kControlDescriptors.size() + == static_cast(HmiControlType::Count), + "每个 HMI 控件类型都必须注册描述"); + +} // namespace + +const HmiControlDescriptor *findHmiControlDescriptor(HmiControlType type) +{ + const auto descriptor = std::find_if( + kControlDescriptors.cbegin(), + kControlDescriptors.cend(), + [type](const HmiControlDescriptor &candidate) + { + return candidate.type == type; + }); + return descriptor == kControlDescriptors.cend() ? nullptr : &*descriptor; +} + +const HmiControlDescriptor *findHmiControlDescriptor( + std::string_view storage_name) +{ + const auto descriptor = std::find_if( + kControlDescriptors.cbegin(), + kControlDescriptors.cend(), + [storage_name](const HmiControlDescriptor &candidate) + { + return storage_name == candidate.storageName; + }); + return descriptor == kControlDescriptors.cend() ? nullptr : &*descriptor; +} + +std::optional hmiBindingArea(HmiBindingKind binding_kind) +{ + switch (binding_kind) + { + case HmiBindingKind::Bit: + { + return RegisterArea::M; + } + case HmiBindingKind::Word: + { + return RegisterArea::D; + } + case HmiBindingKind::None: + default: + { + return std::nullopt; + } + } +} diff --git a/app/src/domain/hmi_control_registry.h b/app/src/domain/hmi_control_registry.h new file mode 100644 index 0000000..5b5bfad --- /dev/null +++ b/app/src/domain/hmi_control_registry.h @@ -0,0 +1,68 @@ +/** + * @file hmi_control_registry.h + * @brief 定义 HMI 控件公共描述及查询接口 + * @author suyu + * @date 2026-08-17 + */ + +#pragma once + +#include "hmi_model.h" + +#include +#include + +/** + * @brief 控件允许绑定的寄存器数据类型 + */ +enum class HmiBindingKind +{ + None, + Bit, + Word +}; + +/** + * @brief 控件运行态读取的值类型 + */ +enum class HmiRuntimeValueKind +{ + None, + Bit, + Word +}; + +/** + * @brief 保存一种 HMI 控件的稳定名称、默认值和寄存器能力 + */ +struct HmiControlDescriptor +{ + HmiControlType type; + const char *storageName; + const char *displayName; + const char *idPrefix; + const char *defaultText; + HmiRect defaultBounds; + HmiBindingKind bindingKind; + HmiRuntimeValueKind runtimeValueKind; + bool requiresBindingForRunning; +}; + +/** + * @brief 按领域类型查找控件描述 + * @return 找到时返回描述指针,不支持的类型返回 nullptr + */ +const HmiControlDescriptor *findHmiControlDescriptor(HmiControlType type); + +/** + * @brief 按工程文件中的稳定类型名查找控件描述 + * @return 找到时返回描述指针,未知名称返回 nullptr + */ +const HmiControlDescriptor *findHmiControlDescriptor( + std::string_view storage_name); + +/** + * @brief 返回绑定类型要求的寄存器区域 + * @return 不支持寄存器绑定时返回空值 + */ +std::optional hmiBindingArea(HmiBindingKind binding_kind); diff --git a/app/src/domain/hmi_model.cpp b/app/src/domain/hmi_model.cpp index 202f5a2..ed30dee 100644 --- a/app/src/domain/hmi_model.cpp +++ b/app/src/domain/hmi_model.cpp @@ -1,5 +1,7 @@ #include "hmi_model.h" +#include "hmi_control_registry.h" + #include namespace { @@ -13,44 +15,12 @@ void setError(std::string *error, const std::string &message) } } -// 判断控件是否必须绑定 M 区位地址 -bool requiresBitBinding(HmiControlType type) -{ - return type == HmiControlType::Button || type == HmiControlType::Indicator; -} - -// 判断控件是否必须绑定 D 区字地址 -bool requiresWordBinding(HmiControlType type) -{ - return type == HmiControlType::NumericDisplay || type == HmiControlType::NumericInput; -} - -bool supportsRegisterBinding(HmiControlType type) -{ - return requiresBitBinding(type) || requiresWordBinding(type); -} - -bool isSupportedControlType(HmiControlType type) -{ - switch (type) - { - case HmiControlType::Button: - case HmiControlType::Indicator: - case HmiControlType::NumericDisplay: - case HmiControlType::NumericInput: - case HmiControlType::Label: - case HmiControlType::PageJump: - case HmiControlType::AlarmList: - return true; - } - return false; -} - } // namespace bool HmiControl::validate(std::string *error) const { - if (!isSupportedControlType(type)) + const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type); + if (descriptor == nullptr) { setError(error, "不支持的 HMI 控件类型"); return false; @@ -73,32 +43,31 @@ bool HmiControl::validate(std::string *error) const return false; } } - if (requiresBitBinding(type)) + const std::optional binding_area = + hmiBindingArea(descriptor->bindingKind); + if (binding.has_value() && !binding_area.has_value()) { - if (binding.has_value() && binding->area() != RegisterArea::M) + setError(error, "文本、页面跳转和报警列表控件不能绑定寄存器"); + return false; + } + if (binding.has_value() && binding_area.has_value() + && binding->area() != *binding_area) + { + if (descriptor->bindingKind == HmiBindingKind::Bit) { setError(error, "按钮和指示灯必须绑定 M 区地址"); - return false; } - } - if (requiresWordBinding(type)) - { - if (binding.has_value() && binding->area() != RegisterArea::D) + else { setError(error, "数值控件必须绑定 D 区地址"); - return false; } + return false; } if (binding.has_value() && !binding->isValid()) { setError(error, "HMI 控件绑定了无效地址"); return false; } - if (!supportsRegisterBinding(type) && binding.has_value()) - { - setError(error, "文本、页面跳转和报警列表控件不能绑定寄存器"); - return false; - } if (type == HmiControlType::PageJump) { if (!pageJump.has_value()) @@ -117,7 +86,13 @@ bool HmiControl::validate(std::string *error) const bool HmiControl::isConfigured() const { - if (type == HmiControlType::Label || type == HmiControlType::AlarmList) + const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type); + if (descriptor == nullptr) + { + return false; + } + if (!descriptor->requiresBindingForRunning + && type != HmiControlType::PageJump) { return true; } @@ -125,15 +100,15 @@ bool HmiControl::isConfigured() const { return pageJump.has_value() && !pageJump->targetPageId.empty(); } - if (!binding.has_value() || !binding->isValid()) + if (!descriptor->requiresBindingForRunning + || !binding.has_value() + || !binding->isValid()) { return false; } - if (requiresBitBinding(type)) - { - return binding->area() == RegisterArea::M; - } - return !requiresWordBinding(type) || binding->area() == RegisterArea::D; + const std::optional binding_area = + hmiBindingArea(descriptor->bindingKind); + return binding_area.has_value() && binding->area() == *binding_area; } bool HmiPage::validate(std::string *error) const diff --git a/app/src/domain/hmi_model.h b/app/src/domain/hmi_model.h index 395aa3f..69e48da 100644 --- a/app/src/domain/hmi_model.h +++ b/app/src/domain/hmi_model.h @@ -43,7 +43,8 @@ enum class HmiControlType NumericInput, // 数值输入 Label, // 标签 PageJump, // 页面跳转 - AlarmList // 报警列表 + AlarmList, // 报警列表 + Count // 已注册控件类型数量,不作为实际控件使用 }; /** diff --git a/app/src/infrastructure/json_project_storage.cpp b/app/src/infrastructure/json_project_storage.cpp index 2d57cfc..49079ac 100644 --- a/app/src/infrastructure/json_project_storage.cpp +++ b/app/src/infrastructure/json_project_storage.cpp @@ -1,5 +1,7 @@ #include "json_project_storage.h" +#include "domain/hmi_control_registry.h" + #include #include #include @@ -392,81 +394,24 @@ bool parseWordOperand( // 将 HMI 控件类型枚举转换为工程文件中的稳定字符串 QString hmiControlTypeName(HmiControlType type) { - switch (type) - { - case HmiControlType::Button: - { - return QStringLiteral("button"); - } - case HmiControlType::Indicator: - { - return QStringLiteral("indicator"); - } - case HmiControlType::NumericDisplay: - { - return QStringLiteral("numericDisplay"); - } - case HmiControlType::NumericInput: - { - return QStringLiteral("numericInput"); - } - case HmiControlType::Label: - { - return QStringLiteral("label"); - } - case HmiControlType::PageJump: - { - return QStringLiteral("pageJump"); - } - case HmiControlType::AlarmList: - { - return QStringLiteral("alarmList"); - } - default: - { - return {}; - } - } + const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type); + return descriptor == nullptr + ? QString{} + : QString::fromLatin1(descriptor->storageName); } // 将工程文件中的控件类型字符串转换为 HMI 控件类型枚举 bool parseHmiControlType( const std::string &value, HmiControlType *type, ParseState *state) { - if (value == "button") - { - *type = HmiControlType::Button; - } - else if (value == "indicator") - { - *type = HmiControlType::Indicator; - } - else if (value == "numericDisplay") - { - *type = HmiControlType::NumericDisplay; - } - else if (value == "numericInput") - { - *type = HmiControlType::NumericInput; - } - else if (value == "label") - { - *type = HmiControlType::Label; - } - else if (value == "pageJump") - { - *type = HmiControlType::PageJump; - } - else if (value == "alarmList") - { - *type = HmiControlType::AlarmList; - } - else + const HmiControlDescriptor *descriptor = findHmiControlDescriptor(value); + if (descriptor == nullptr) { return state->fail( ProjectStorageError::InvalidField, "不支持的 HMI 控件类型:" + value); } + *type = descriptor->type; return true; } diff --git a/app/src/services/hmi_editor_service.cpp b/app/src/services/hmi_editor_service.cpp index 0ab8c6f..6100056 100644 --- a/app/src/services/hmi_editor_service.cpp +++ b/app/src/services/hmi_editor_service.cpp @@ -1,5 +1,6 @@ #include "hmi_editor_service.h" +#include "domain/hmi_control_registry.h" #include "project_service.h" #include @@ -17,84 +18,6 @@ void setError(std::string *error, const std::string &message) } } -std::string controlPrefix(HmiControlType type) -{ - switch (type) - { - case HmiControlType::Button: - { - return "button"; - } - case HmiControlType::Indicator: - { - return "indicator"; - } - case HmiControlType::NumericDisplay: - { - return "numeric-display"; - } - case HmiControlType::NumericInput: - { - return "numeric-input"; - } - case HmiControlType::Label: - { - return "label"; - } - case HmiControlType::PageJump: - { - return "page-jump"; - } - case HmiControlType::AlarmList: - { - return "alarm-list"; - } - default: - { - return "control"; - } - } -} - -std::string defaultText(HmiControlType type) -{ - switch (type) - { - case HmiControlType::Button: - { - return "按钮"; - } - case HmiControlType::Indicator: - { - return "指示灯"; - } - case HmiControlType::NumericDisplay: - { - return "数值显示"; - } - case HmiControlType::NumericInput: - { - return "数值输入"; - } - case HmiControlType::Label: - { - return "文本"; - } - case HmiControlType::PageJump: - { - return "页面跳转"; - } - case HmiControlType::AlarmList: - { - return "报警信息"; - } - default: - { - return "控件"; - } - } -} - HmiEditorResult failure(HmiEditorError error, const std::string &message) { return {false, error, message, {}}; @@ -351,9 +274,14 @@ HmiEditorResult HmiEditorService::addControl( { return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面"); } + const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type); + if (descriptor == nullptr) + { + return failure(HmiEditorError::InvalidControl, "不支持的 HMI 控件类型"); + } // 新控件初始不绑定寄存器,避免自动分配地址造成误写风险 - HmiControl control = makeControl(*page, type); + HmiControl control = makeControl(*page, *descriptor); Project &project = project_service_.editProject(); auto target_page = std::find_if( project.hmiPages.begin(), @@ -484,27 +412,11 @@ HmiEditorResult HmiEditorService::updateControl( bool HmiEditorService::validateEditableControl( const HmiPage &page, const HmiControl &control, std::string *error) const { - switch (control.type) - { - case HmiControlType::Button: - case HmiControlType::Indicator: - case HmiControlType::NumericDisplay: - case HmiControlType::NumericInput: - case HmiControlType::Label: - case HmiControlType::PageJump: - case HmiControlType::AlarmList: - break; - default: - setError(error, "不支持的 HMI 控件类型"); - return false; - } - if (control.id.empty()) + if (!control.validate(error)) { - setError(error, "HMI 控件 ID 不能为空"); return false; } - if (control.bounds.width <= 0 || control.bounds.height <= 0 - || control.bounds.x < 0 || control.bounds.y < 0 + if (control.bounds.x < 0 || control.bounds.y < 0 || control.bounds.width > page.width || control.bounds.height > page.height || control.bounds.x > page.width - control.bounds.width || control.bounds.y > page.height - control.bounds.height) @@ -512,60 +424,11 @@ bool HmiEditorService::validateEditableControl( setError(error, "HMI 控件不能超出页面范围"); return false; } - for (const auto &property : control.properties) - { - if (property.first.empty()) - { - setError(error, "HMI 控件属性名称不能为空"); - return false; - } - } - if (control.binding.has_value() && !control.binding->isValid()) - { - setError(error, "HMI 控件绑定了无效地址"); - return false; - } - if (control.binding.has_value() - && (control.type == HmiControlType::Button - || control.type == HmiControlType::Indicator) - && control.binding->area() != RegisterArea::M) - { - setError(error, "按钮和指示灯必须绑定 M 区地址"); - return false; - } - if (control.binding.has_value() - && (control.type == HmiControlType::NumericDisplay - || control.type == HmiControlType::NumericInput) - && control.binding->area() != RegisterArea::D) - { - setError(error, "数值控件必须绑定 D 区地址"); - return false; - } - if ((control.type == HmiControlType::Label - || control.type == HmiControlType::PageJump - || control.type == HmiControlType::AlarmList) - && control.binding.has_value()) - { - setError(error, "文本、页面跳转和报警列表控件不能绑定寄存器"); - return false; - } - if (control.type == HmiControlType::PageJump) - { - if (!control.pageJump.has_value()) - { - setError(error, "页面跳转控件缺少跳转配置"); - return false; - } - if (!control.pageJump->targetPageId.empty() - && findPage(control.pageJump->targetPageId) == nullptr) - { - setError(error, "页面跳转控件的目标页面不存在"); - return false; - } - } - else if (control.pageJump.has_value()) + if (control.type == HmiControlType::PageJump + && !control.pageJump->targetPageId.empty() + && findPage(control.pageJump->targetPageId) == nullptr) { - setError(error, "非页面跳转控件不能包含跳转配置"); + setError(error, "页面跳转控件的目标页面不存在"); return false; } return true; @@ -586,23 +449,15 @@ bool HmiEditorService::hasDuplicateControlId( // 根据控件类型生成默认样式、文字、大小、初始坐标 HmiControl HmiEditorService::makeControl( - const HmiPage &page, HmiControlType type) + const HmiPage &page, const HmiControlDescriptor &descriptor) { HmiControl control; - control.id = makeUniqueId(page, controlPrefix(type)); - control.type = type; - control.text = defaultText(type); - if (type == HmiControlType::AlarmList) - { - control.bounds.width = std::min(360, page.width); - control.bounds.height = std::min(180, page.height); - } - else - { - control.bounds.width = type == HmiControlType::Indicator ? 64 : 120; - control.bounds.height = type == HmiControlType::Indicator ? 64 : 40; - } - if (type == HmiControlType::PageJump) + control.id = makeUniqueId(page, descriptor.idPrefix); + control.type = descriptor.type; + control.text = descriptor.defaultText; + control.bounds.width = std::min(descriptor.defaultBounds.width, page.width); + control.bounds.height = std::min(descriptor.defaultBounds.height, page.height); + if (descriptor.type == HmiControlType::PageJump) { control.pageJump = HmiPageJumpConfig{}; } diff --git a/app/src/services/hmi_editor_service.h b/app/src/services/hmi_editor_service.h index ba739ec..b831aeb 100644 --- a/app/src/services/hmi_editor_service.h +++ b/app/src/services/hmi_editor_service.h @@ -12,6 +12,7 @@ #include class ProjectService; +struct HmiControlDescriptor; /** * @brief HMI 编辑操作的失败分类 @@ -158,7 +159,7 @@ private: * @return 未绑定寄存器的默认控件配置 */ static HmiControl makeControl( - const HmiPage &page, HmiControlType type); + const HmiPage &page, const HmiControlDescriptor &descriptor); /** * @brief 在页面内生成带指定前缀的唯一控件标识 * @param page 待检查页面 diff --git a/app/src/services/hmi_runtime_service.cpp b/app/src/services/hmi_runtime_service.cpp index 291b3ab..198193f 100644 --- a/app/src/services/hmi_runtime_service.cpp +++ b/app/src/services/hmi_runtime_service.cpp @@ -1,5 +1,7 @@ #include "hmi_runtime_service.h" +#include "domain/hmi_control_registry.h" + namespace { HmiRuntimeReadResult readFailure(HmiRuntimeError error) @@ -12,17 +14,6 @@ HmiRuntimeWriteResult writeFailure(HmiRuntimeError error) return {false, error}; } -bool isBitControl(HmiControlType type) -{ - return type == HmiControlType::Button || type == HmiControlType::Indicator; -} - -bool isWordControl(HmiControlType type) -{ - return type == HmiControlType::NumericDisplay - || type == HmiControlType::NumericInput; -} - } // namespace HmiRuntimeService::HmiRuntimeService(RegisterRepository &repository) @@ -32,8 +23,10 @@ HmiRuntimeService::HmiRuntimeService(RegisterRepository &repository) HmiRuntimeReadResult HmiRuntimeService::readControl(const HmiControl &control) const { - // 标签等静态控件不参与寄存器读写 - if (!isBitControl(control.type) && !isWordControl(control.type)) + const HmiControlDescriptor *descriptor = + findHmiControlDescriptor(control.type); + if (descriptor == nullptr + || descriptor->runtimeValueKind == HmiRuntimeValueKind::None) { return readFailure(HmiRuntimeError::UnsupportedControl); } @@ -45,7 +38,16 @@ HmiRuntimeReadResult HmiRuntimeService::readControl(const HmiControl &control) c { return readFailure(HmiRuntimeError::InvalidBinding); } - if (isBitControl(control.type)) + const std::optional binding_area = + hmiBindingArea(descriptor->bindingKind); + if (!binding_area.has_value() || control.binding->area() != *binding_area) + { + return readFailure(HmiRuntimeError::InvalidBinding); + } + + switch (descriptor->runtimeValueKind) + { + case HmiRuntimeValueKind::Bit: { // 按控件类型选择仓库的 M 位读取契约 const BitReadResult result = repository_.readBit(*control.binding); @@ -55,7 +57,7 @@ HmiRuntimeReadResult HmiRuntimeService::readControl(const HmiControl &control) c } return {true, HmiRuntimeError::None, result.value, 0}; } - if (isWordControl(control.type)) + case HmiRuntimeValueKind::Word: { const WordReadResult result = repository_.readWord(*control.binding); if (!result.succeeded) @@ -64,7 +66,12 @@ HmiRuntimeReadResult HmiRuntimeService::readControl(const HmiControl &control) c } return {true, HmiRuntimeError::None, false, result.value}; } - return readFailure(HmiRuntimeError::UnsupportedControl); + case HmiRuntimeValueKind::None: + default: + { + return readFailure(HmiRuntimeError::UnsupportedControl); + } + } } HmiRuntimeWriteResult HmiRuntimeService::operateButton( diff --git a/app/src/ui/property_panel_controller.cpp b/app/src/ui/property_panel_controller.cpp index 2d5ebb8..cf4d472 100644 --- a/app/src/ui/property_panel_controller.cpp +++ b/app/src/ui/property_panel_controller.cpp @@ -1,5 +1,6 @@ #include "property_panel_controller.h" +#include "domain/hmi_control_registry.h" #include "hmi_editor_widget.h" #include "logic_editor_widget.h" #include "logic_instruction_dialog.h" @@ -33,45 +34,6 @@ std::string toUtf8(const QString &value) return std::string(bytes.constData(), static_cast(bytes.size())); } -QString controlTypeText(HmiControlType type) -{ - switch (type) - { - case HmiControlType::Button: - { - return QObject::tr("按钮"); - } - case HmiControlType::Indicator: - { - return QObject::tr("指示灯"); - } - case HmiControlType::NumericDisplay: - { - return QObject::tr("数值显示"); - } - case HmiControlType::NumericInput: - { - return QObject::tr("数值输入"); - } - case HmiControlType::Label: - { - return QObject::tr("文本"); - } - case HmiControlType::PageJump: - { - return QObject::tr("页面跳转"); - } - case HmiControlType::AlarmList: - { - return QObject::tr("报警列表"); - } - default: - { - return QObject::tr("未知控件"); - } - } -} - } // namespace PropertyPanelController::PropertyPanelController( @@ -174,6 +136,8 @@ void PropertyPanelController::showControlProperties(const std::string &control_i const HmiControl *control = hmi_editor_service_.findControl( current_page_id_(), control_id); const bool has_control = control != nullptr; + const HmiControlDescriptor *descriptor = has_control + ? findHmiControlDescriptor(control->type) : nullptr; ui_.selectionValueLabel->setText( has_control ? fromUtf8(control->id) + (control->isConfigured() ? QString{} @@ -219,9 +183,8 @@ void PropertyPanelController::showControlProperties(const std::string &control_i : control->binding->area() == RegisterArea::M ? 1 : 2); ui_.bindingIndexSpinBox->setValue( control->binding.has_value() ? control->binding->index() : 0); - const bool has_binding = control->type != HmiControlType::Label - && control->type != HmiControlType::PageJump - && control->type != HmiControlType::AlarmList; + const bool has_binding = descriptor != nullptr + && descriptor->bindingKind != HmiBindingKind::None; ui_.bindingAreaLabel->setVisible(has_binding); ui_.bindingAreaComboBox->setVisible(has_binding); ui_.bindingIndexLabel->setVisible(has_binding); @@ -434,7 +397,11 @@ void PropertyPanelController::addHmiControl(HmiControlType type) hmi_editor_widget_->selectControl(result.id); showControlProperties(result.id); refresh_project_ui_(); - status_reporter_(QObject::tr("已添加%1").arg(controlTypeText(type)), 3000); + const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type); + const QString display_name = descriptor == nullptr + ? QObject::tr("未知控件") + : QString::fromUtf8(descriptor->displayName); + status_reporter_(QObject::tr("已添加%1").arg(display_name), 3000); } void PropertyPanelController::deleteSelectedControl() @@ -471,9 +438,10 @@ void PropertyPanelController::applySelectedControlProperties() control.bounds.y = ui_.controlYSpinBox->value(); control.bounds.width = ui_.controlWidthSpinBox->value(); control.bounds.height = ui_.controlHeightSpinBox->value(); - if (control.type == HmiControlType::Label - || control.type == HmiControlType::PageJump - || control.type == HmiControlType::AlarmList) + const HmiControlDescriptor *descriptor = + findHmiControlDescriptor(control.type); + if (descriptor == nullptr + || descriptor->bindingKind == HmiBindingKind::None) { control.binding.reset(); } diff --git a/app/tests/alarm_service_tests.pro b/app/tests/alarm_service_tests.pro index 88e78e7..21f2bf0 100644 --- a/app/tests/alarm_service_tests.pro +++ b/app/tests/alarm_service_tests.pro @@ -12,6 +12,7 @@ SOURCES += \ ../src/domain/register_repository.cpp \ ../src/domain/alarm_model.cpp \ ../src/domain/hmi_model.cpp \ + ../src/domain/hmi_control_registry.cpp \ ../src/domain/control_logic_model.cpp \ ../src/domain/project_model.cpp \ ../src/services/project_service.cpp \ @@ -23,6 +24,7 @@ HEADERS += \ ../src/domain/register_repository.h \ ../src/domain/alarm_model.h \ ../src/domain/hmi_model.h \ + ../src/domain/hmi_control_registry.h \ ../src/domain/control_logic_model.h \ ../src/domain/project_model.h \ ../src/domain/project_storage.h \ diff --git a/app/tests/domain_tests.cpp b/app/tests/domain_tests.cpp index 554e942..1b42271 100644 --- a/app/tests/domain_tests.cpp +++ b/app/tests/domain_tests.cpp @@ -1,4 +1,5 @@ #include "domain/control_logic_model.h" +#include "domain/hmi_control_registry.h" #include "domain/hmi_model.h" #include "domain/project_model.h" #include "domain/register_address.h" @@ -8,6 +9,7 @@ #include #include #include +#include #include #include @@ -76,6 +78,92 @@ void testRegisterRepositorySeparatesAreas() "M address must not be read as a word"); } +void testHmiControlRegistryCompleteness() +{ + std::set descriptors; + std::set storage_names; + std::set id_prefixes; + const std::size_t control_type_count = + static_cast(HmiControlType::Count); + for (std::size_t index = 0; index < control_type_count; ++index) + { + const HmiControlType type = static_cast(index); + const HmiControlDescriptor *descriptor = + findHmiControlDescriptor(type); + require(descriptor != nullptr, + "every HMI control type must have one descriptor"); + require(descriptor->type == type, + "HMI type lookup must return the requested descriptor"); + require(descriptors.emplace(descriptor).second, + "each HMI control type must resolve to a different descriptor"); + require(descriptor->storageName != nullptr + && descriptor->storageName[0] != '\0', + "HMI storage names must not be empty"); + require(descriptor->displayName != nullptr + && descriptor->displayName[0] != '\0', + "HMI display names must not be empty"); + require(descriptor->idPrefix != nullptr + && descriptor->idPrefix[0] != '\0', + "HMI id prefixes must not be empty"); + require(descriptor->defaultText != nullptr, + "HMI default text must not be null"); + require(descriptor->defaultBounds.width > 0 + && descriptor->defaultBounds.height > 0, + "HMI default bounds must have a positive size"); + require(findHmiControlDescriptor(descriptor->storageName) == descriptor, + "HMI storage name lookup must return its registered descriptor"); + require(storage_names.emplace(descriptor->storageName).second, + "HMI storage names must be unique"); + require(id_prefixes.emplace(descriptor->idPrefix).second, + "HMI id prefixes must be unique"); + + const std::optional binding_area = + hmiBindingArea(descriptor->bindingKind); + switch (descriptor->runtimeValueKind) + { + case HmiRuntimeValueKind::Bit: + { + require(descriptor->bindingKind == HmiBindingKind::Bit + && binding_area == RegisterArea::M, + "bit runtime controls must bind to the M area"); + break; + } + case HmiRuntimeValueKind::Word: + { + require(descriptor->bindingKind == HmiBindingKind::Word + && binding_area == RegisterArea::D, + "word runtime controls must bind to the D area"); + break; + } + case HmiRuntimeValueKind::None: + { + require(descriptor->bindingKind == HmiBindingKind::None + && !binding_area.has_value(), + "static controls must not declare a register binding"); + break; + } + } + + require(descriptor->requiresBindingForRunning + == (descriptor->runtimeValueKind + != HmiRuntimeValueKind::None), + "runtime value controls must require a configured binding"); + } + + require(findHmiControlDescriptor(HmiControlType::Count) == nullptr, + "the HMI control count marker must not be registered"); + require(findHmiControlDescriptor(static_cast(99)) == nullptr, + "unknown HMI control types must not resolve"); + require(findHmiControlDescriptor("unknown") == nullptr, + "unknown HMI storage names must not resolve"); + require(hmiBindingArea(HmiBindingKind::Bit) == RegisterArea::M, + "bit bindings must use the M area"); + require(hmiBindingArea(HmiBindingKind::Word) == RegisterArea::D, + "word bindings must use the D area"); + require(!hmiBindingArea(HmiBindingKind::None).has_value(), + "controls without bindings must not resolve a register area"); +} + Project makeValidProject() { // 构造包含 HMI 绑定和完整梯形图网络的最小合法工程作为测试基线 @@ -584,6 +672,7 @@ int main() testRegisterAddressBoundaries(); testRegisterAddressParsing(); testRegisterRepositorySeparatesAreas(); + testHmiControlRegistryCompleteness(); testLogicNodeConfigurationBoundaries(); testTimerAndCommentBoundaries(); testTimerReferencesForRunning(); diff --git a/app/tests/domain_tests.pro b/app/tests/domain_tests.pro index c0e4421..92b73d9 100644 --- a/app/tests/domain_tests.pro +++ b/app/tests/domain_tests.pro @@ -12,6 +12,7 @@ SOURCES += \ ../src/domain/register_repository.cpp \ ../src/domain/alarm_model.cpp \ ../src/domain/hmi_model.cpp \ + ../src/domain/hmi_control_registry.cpp \ ../src/domain/control_logic_model.cpp \ ../src/domain/project_model.cpp \ ../src/domain/runtime_state.cpp @@ -21,6 +22,7 @@ HEADERS += \ ../src/domain/register_repository.h \ ../src/domain/alarm_model.h \ ../src/domain/hmi_model.h \ + ../src/domain/hmi_control_registry.h \ ../src/domain/control_logic_model.h \ ../src/domain/project_model.h \ ../src/domain/runtime_state.h diff --git a/app/tests/hmi_editor_service_tests.pro b/app/tests/hmi_editor_service_tests.pro index 02b8730..a4b74ab 100644 --- a/app/tests/hmi_editor_service_tests.pro +++ b/app/tests/hmi_editor_service_tests.pro @@ -12,6 +12,7 @@ SOURCES += \ ../src/domain/register_repository.cpp \ ../src/domain/alarm_model.cpp \ ../src/domain/hmi_model.cpp \ + ../src/domain/hmi_control_registry.cpp \ ../src/domain/control_logic_model.cpp \ ../src/domain/project_model.cpp \ ../src/services/project_service.cpp \ @@ -24,6 +25,7 @@ HEADERS += \ ../src/domain/register_repository.h \ ../src/domain/alarm_model.h \ ../src/domain/hmi_model.h \ + ../src/domain/hmi_control_registry.h \ ../src/domain/control_logic_model.h \ ../src/domain/project_model.h \ ../src/domain/project_storage.h \ diff --git a/app/tests/logic_editor_service_tests.pro b/app/tests/logic_editor_service_tests.pro index 21cdeb2..b566480 100644 --- a/app/tests/logic_editor_service_tests.pro +++ b/app/tests/logic_editor_service_tests.pro @@ -12,6 +12,7 @@ SOURCES += \ ../src/domain/control_logic_model.cpp \ ../src/domain/project_model.cpp \ ../src/domain/hmi_model.cpp \ + ../src/domain/hmi_control_registry.cpp \ ../src/domain/register_repository.cpp \ ../src/domain/alarm_model.cpp \ ../src/services/project_service.cpp \ @@ -22,6 +23,7 @@ HEADERS += \ ../src/domain/control_logic_model.h \ ../src/domain/project_model.h \ ../src/domain/hmi_model.h \ + ../src/domain/hmi_control_registry.h \ ../src/domain/register_repository.h \ ../src/domain/alarm_model.h \ ../src/domain/project_storage.h \ diff --git a/app/tests/main_window_tests.pro b/app/tests/main_window_tests.pro index a0d6c22..cd3444f 100644 --- a/app/tests/main_window_tests.pro +++ b/app/tests/main_window_tests.pro @@ -28,6 +28,7 @@ SOURCES += \ ../src/domain/register_monitor_model.cpp \ ../src/domain/alarm_model.cpp \ ../src/domain/hmi_model.cpp \ + ../src/domain/hmi_control_registry.cpp \ ../src/domain/control_logic_model.cpp \ ../src/domain/project_model.cpp \ ../src/domain/runtime_state.cpp \ @@ -63,6 +64,7 @@ HEADERS += \ ../src/domain/register_monitor_model.h \ ../src/domain/alarm_model.h \ ../src/domain/hmi_model.h \ + ../src/domain/hmi_control_registry.h \ ../src/domain/control_logic_model.h \ ../src/domain/project_model.h \ ../src/domain/project_storage.h \ diff --git a/app/tests/offline_simulation_service_tests.pro b/app/tests/offline_simulation_service_tests.pro index 5c5aca7..7fb4967 100644 --- a/app/tests/offline_simulation_service_tests.pro +++ b/app/tests/offline_simulation_service_tests.pro @@ -14,6 +14,7 @@ SOURCES += \ ../src/domain/register_address.cpp \ ../src/domain/register_repository.cpp \ ../src/domain/hmi_model.cpp \ + ../src/domain/hmi_control_registry.cpp \ ../src/domain/control_logic_model.cpp \ ../src/services/hmi_runtime_service.cpp \ ../src/services/software_logic_executor.cpp \ @@ -23,6 +24,7 @@ HEADERS += \ ../src/domain/register_address.h \ ../src/domain/register_repository.h \ ../src/domain/hmi_model.h \ + ../src/domain/hmi_control_registry.h \ ../src/domain/control_logic_model.h \ ../src/services/hmi_runtime_service.h \ ../src/services/software_logic_executor.h \ diff --git a/app/tests/plc_runtime_tests.pro b/app/tests/plc_runtime_tests.pro index 9617525..6bf19cf 100644 --- a/app/tests/plc_runtime_tests.pro +++ b/app/tests/plc_runtime_tests.pro @@ -16,6 +16,7 @@ SOURCES += \ ../src/domain/register_monitor_model.cpp \ ../src/domain/alarm_model.cpp \ ../src/domain/hmi_model.cpp \ + ../src/domain/hmi_control_registry.cpp \ ../src/domain/control_logic_model.cpp \ ../src/domain/project_model.cpp \ ../src/domain/runtime_state.cpp \ @@ -33,6 +34,7 @@ HEADERS += \ ../src/domain/register_monitor_model.h \ ../src/domain/alarm_model.h \ ../src/domain/hmi_model.h \ + ../src/domain/hmi_control_registry.h \ ../src/domain/control_logic_model.h \ ../src/domain/project_model.h \ ../src/domain/project_storage.h \ diff --git a/app/tests/project_management_tests.pro b/app/tests/project_management_tests.pro index 532a1f4..f7b1811 100644 --- a/app/tests/project_management_tests.pro +++ b/app/tests/project_management_tests.pro @@ -14,6 +14,7 @@ SOURCES += \ ../src/domain/register_repository.cpp \ ../src/domain/alarm_model.cpp \ ../src/domain/hmi_model.cpp \ + ../src/domain/hmi_control_registry.cpp \ ../src/domain/control_logic_model.cpp \ ../src/domain/project_model.cpp \ ../src/domain/runtime_state.cpp \ @@ -26,6 +27,7 @@ HEADERS += \ ../src/domain/register_repository.h \ ../src/domain/alarm_model.h \ ../src/domain/hmi_model.h \ + ../src/domain/hmi_control_registry.h \ ../src/domain/control_logic_model.h \ ../src/domain/project_model.h \ ../src/domain/runtime_state.h \ diff --git a/app/tests/runtime_mode_service_tests.pro b/app/tests/runtime_mode_service_tests.pro index 6ce7603..fc26a51 100644 --- a/app/tests/runtime_mode_service_tests.pro +++ b/app/tests/runtime_mode_service_tests.pro @@ -16,6 +16,7 @@ SOURCES += \ ../src/domain/register_monitor_model.cpp \ ../src/domain/alarm_model.cpp \ ../src/domain/hmi_model.cpp \ + ../src/domain/hmi_control_registry.cpp \ ../src/domain/control_logic_model.cpp \ ../src/domain/project_model.cpp \ ../src/domain/runtime_state.cpp \ @@ -31,6 +32,7 @@ HEADERS += \ ../src/domain/register_monitor_model.h \ ../src/domain/alarm_model.h \ ../src/domain/hmi_model.h \ + ../src/domain/hmi_control_registry.h \ ../src/domain/control_logic_model.h \ ../src/domain/project_model.h \ ../src/domain/project_storage.h \