From 34bdd3ffe3c259df22f7d1fd5e757e051f93d036 Mon Sep 17 00:00:00 2001 From: suyu <1643689728@qq.com> Date: Thu, 27 Aug 2026 16:53:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=AE=80=E5=8C=96HMI=E6=8E=A7=E4=BB=B6?= =?UTF-8?q?=E5=AF=84=E5=AD=98=E5=99=A8=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/services/hmi_editor_service.cpp | 73 +++++++++++- app/src/services/hmi_editor_service.h | 2 +- app/src/ui/main_window.ui | 30 +---- app/src/ui/property_panel_controller.cpp | 33 +++--- app/tests/hmi_editor_service_tests.cpp | 9 ++ app/tests/runtime_panel_controller_tests.cpp | 110 +++++++++++++++++++ app/tests/runtime_panel_controller_tests.pro | 14 ++- 7 files changed, 222 insertions(+), 49 deletions(-) diff --git a/app/src/services/hmi_editor_service.cpp b/app/src/services/hmi_editor_service.cpp index 67b2a04..07e9678 100644 --- a/app/src/services/hmi_editor_service.cpp +++ b/app/src/services/hmi_editor_service.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -26,6 +27,68 @@ HmiEditorResult failure(HmiEditorError error, const std::string &message) return {false, error, message, {}}; } +int hmiBindingWordCount(const HmiControl &control) +{ + if (control.binding.has_value() && control.binding->area() == RegisterArea::M) + { + return 1; + } + if (control.type == HmiControlType::NumericDisplay + || control.type == HmiControlType::NumericInput + || (control.type == HmiControlType::StatusText + && control.binding.has_value() + && control.binding->area() == RegisterArea::D)) + { + return registerDataTypeWordCount(control.dataType); + } + return 1; +} + +std::optional nextHmiBindingAddress( + const Project &project, + const HmiControlDescriptor &descriptor, + RegisterDataType data_type) +{ + std::optional area = hmiBindingArea(descriptor.bindingKind); + if (descriptor.bindingKind == HmiBindingKind::BitOrWord) + { + area = RegisterArea::M; + } + if (!area.has_value()) + { + return std::nullopt; + } + + int next_index = RegisterAddress::kMinimumIndex; + for (const HmiPage &page : project.hmiPages) + { + for (const HmiControl &control : page.controls) + { + if (!control.binding.has_value() + || control.binding->area() != *area) + { + continue; + } + next_index = std::max( + next_index, + control.binding->index() + hmiBindingWordCount(control)); + } + } + for (int index = next_index; + index <= RegisterAddress::kMaximumIndex; + ++index) + { + const RegisterAddress candidate{*area, index}; + if (*area == RegisterArea::D + && !registerDataTypeAddressIsValid(data_type, candidate)) + { + continue; + } + return candidate; + } + return std::nullopt; +} + bool isBlank(const std::string &value) { return value.empty() @@ -459,8 +522,16 @@ HmiEditorResult HmiEditorService::addControl( return failure(HmiEditorError::InvalidControl, "不支持的 HMI 控件类型"); } - // 新控件初始不绑定寄存器,避免自动分配地址造成误写风险 HmiControl control = makeControl(*page, *descriptor); + const std::optional binding = nextHmiBindingAddress( + project_service_.project(), *descriptor, control.dataType); + if (descriptor->bindingKind != HmiBindingKind::None && !binding.has_value()) + { + return failure( + HmiEditorError::InvalidControl, + "没有可用的 HMI 默认寄存器地址"); + } + control.binding = binding; HistoryState before = captureState(); Project &project = project_service_.editProject(); auto target_page = std::find_if( diff --git a/app/src/services/hmi_editor_service.h b/app/src/services/hmi_editor_service.h index 1b36381..1010701 100644 --- a/app/src/services/hmi_editor_service.h +++ b/app/src/services/hmi_editor_service.h @@ -301,7 +301,7 @@ private: * @brief 按控件类型创建带默认属性的新控件 * @param page 新控件所属页面,用于确定初始位置和唯一标识 * @param descriptor 新控件类型对应的注册描述 - * @return 未绑定寄存器的默认控件配置 + * @return 带默认外观和未分配地址的控件配置 */ static HmiControl makeControl( const HmiPage &page, const HmiControlDescriptor &descriptor); diff --git a/app/src/ui/main_window.ui b/app/src/ui/main_window.ui index 28e4cae..c2f0942 100644 --- a/app/src/ui/main_window.ui +++ b/app/src/ui/main_window.ui @@ -626,38 +626,12 @@ - - 绑定区域 - - - - - - - - 未绑定 - - - - M + 绑定地址 - - - - D - - - - - - 绑定地址 - - - - + 4000 diff --git a/app/src/ui/property_panel_controller.cpp b/app/src/ui/property_panel_controller.cpp index 1e6c3bc..7164390 100644 --- a/app/src/ui/property_panel_controller.cpp +++ b/app/src/ui/property_panel_controller.cpp @@ -13,7 +13,6 @@ #include "ui_main_window.h" #include -#include #include #include #include @@ -99,9 +98,6 @@ void PropertyPanelController::configure() ui_.fontSizeSpinBox->setRange( ProjectLimits::kMinimumHmiFontPointSize, ProjectLimits::kMaximumHmiFontPointSize); - ui_.bindingAreaComboBox->setItemData(0, -1); - ui_.bindingAreaComboBox->setItemData(1, 0); - ui_.bindingAreaComboBox->setItemData(2, 1); ui_.buttonOperationComboBox->setItemData( 0, static_cast(HmiButtonOperation::SetOn)); ui_.buttonOperationComboBox->setItemData( @@ -205,7 +201,6 @@ void PropertyPanelController::showControlProperties(const std::string &control_i static_cast(ui_.controlYSpinBox), static_cast(ui_.controlWidthSpinBox), static_cast(ui_.controlHeightSpinBox), - static_cast(ui_.bindingAreaComboBox), static_cast(ui_.bindingIndexSpinBox), static_cast(ui_.targetPageComboBox), static_cast(ui_.buttonOperationComboBox), @@ -227,8 +222,6 @@ void PropertyPanelController::showControlProperties(const std::string &control_i ui_.controlTextEdit->setMaxLength( static_cast(ProjectLimits::kMaximumHmiControlTextCharacters)); ui_.bindingAreaLabel->setVisible(false); - ui_.bindingAreaComboBox->setVisible(false); - ui_.bindingIndexLabel->setVisible(false); ui_.bindingIndexSpinBox->setVisible(false); ui_.targetPageLabel->setVisible(false); ui_.targetPageComboBox->setVisible(false); @@ -263,17 +256,22 @@ void PropertyPanelController::showControlProperties(const std::string &control_i ui_.controlYSpinBox->setValue(control->bounds.y); ui_.controlWidthSpinBox->setValue(control->bounds.width); ui_.controlHeightSpinBox->setValue(control->bounds.height); - ui_.bindingAreaComboBox->setCurrentIndex(!control->binding.has_value() ? 0 - : control->binding->area() == RegisterArea::M ? 1 : 2); + const std::optional fixed_binding_area = descriptor == nullptr + ? std::nullopt : hmiBindingArea(descriptor->bindingKind); + ui_.bindingAreaLabel->setText( + fixed_binding_area.has_value() + ? QObject::tr("绑定地址(%1)") + .arg(*fixed_binding_area == RegisterArea::M ? QStringLiteral("M") + : QStringLiteral("D")) + : QObject::tr("绑定地址")); ui_.bindingIndexSpinBox->setValue( control->binding.has_value() ? control->binding->index() : 0); const bool has_binding = descriptor != nullptr && descriptor->bindingKind != HmiBindingKind::None && !is_status_text; ui_.bindingAreaLabel->setVisible(has_binding); - ui_.bindingAreaComboBox->setVisible(has_binding); - ui_.bindingIndexLabel->setVisible(has_binding); ui_.bindingIndexSpinBox->setVisible(has_binding); + ui_.bindingIndexSpinBox->setEnabled(has_binding); const bool is_button = control->type == HmiControlType::Button; ui_.buttonOperationLabel->setVisible(is_button); @@ -625,17 +623,16 @@ void PropertyPanelController::applySelectedControlProperties() } else if (control.type != HmiControlType::StatusText) { - const int binding_area = ui_.bindingAreaComboBox->currentData().toInt(); - if (binding_area < 0) + const std::optional fixed_binding_area = descriptor == nullptr + ? std::nullopt : hmiBindingArea(descriptor->bindingKind); + if (fixed_binding_area.has_value()) { - control.binding.reset(); + control.binding = RegisterAddress{ + *fixed_binding_area, ui_.bindingIndexSpinBox->value()}; } else { - const RegisterArea area = binding_area == 0 - ? RegisterArea::M : RegisterArea::D; - control.binding = RegisterAddress{ - area, ui_.bindingIndexSpinBox->value()}; + control.binding.reset(); } } if (control.type == HmiControlType::Button) diff --git a/app/tests/hmi_editor_service_tests.cpp b/app/tests/hmi_editor_service_tests.cpp index f1a7a92..e4a0d07 100644 --- a/app/tests/hmi_editor_service_tests.cpp +++ b/app/tests/hmi_editor_service_tests.cpp @@ -42,6 +42,15 @@ void testControlEditing() const HmiPage *page = service.findPage(page_id); require(page != nullptr && page->controls.size() == 4, "all added controls must be kept in the page model"); + require(service.findControl(page_id, button.id)->binding + == RegisterAddress{RegisterArea::M, 0} + && service.findControl(page_id, indicator.id)->binding + == RegisterAddress{RegisterArea::M, 1} + && service.findControl(page_id, display.id)->binding + == RegisterAddress{RegisterArea::D, 0} + && service.findControl(page_id, input.id)->binding + == RegisterAddress{RegisterArea::D, 1}, + "new basic HMI controls must receive sequential M/D addresses"); require(service.moveControl(page_id, button.id, {120, 80, 120, 40}).succeeded, "a valid control move must succeed"); require(!service.moveControl(page_id, button.id, {790, 460, 120, 40}).succeeded, diff --git a/app/tests/runtime_panel_controller_tests.cpp b/app/tests/runtime_panel_controller_tests.cpp index c78a45d..483e676 100644 --- a/app/tests/runtime_panel_controller_tests.cpp +++ b/app/tests/runtime_panel_controller_tests.cpp @@ -14,8 +14,10 @@ #include "ui/free_monitor_widget.h" #include "ui/hmi_editor_widget.h" #include "ui/logic_editor_widget.h" +#include "ui/property_panel_controller.h" #include "ui/runtime_monitor_widget.h" #include "ui/runtime_panel_controller.h" +#include "ui_main_window.h" #include #include @@ -28,10 +30,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include @@ -204,6 +208,111 @@ void testAlarmListKeepsFixedGeometryWhileRecordsChange() "removing the last alarm row must keep the fixed alarm control visible"); } +void testHmiPropertyPanelInfersFixedBindingAreas() +{ + TestProjectStorage storage; + ProjectService project_service(storage, defaultProjectLimitSettings()); + VirtualRegisterRepository repository; + HmiDefaultSettings hmi_defaults; + HmiEditorService hmi_editor_service(project_service, hmi_defaults); + HmiRuntimeService hmi_runtime_service(repository); + AlarmService alarm_service(project_service, repository); + LogicEditorService logic_editor_service(project_service); + + const HmiEditorResult page = hmi_editor_service.ensureDefaultPage(); + const HmiEditorResult button = hmi_editor_service.addControl( + page.id, HmiControlType::Button); + const HmiEditorResult second_button = hmi_editor_service.addControl( + page.id, HmiControlType::Button); + const HmiEditorResult display = hmi_editor_service.addControl( + page.id, HmiControlType::NumericDisplay); + const HmiEditorResult second_display = hmi_editor_service.addControl( + page.id, HmiControlType::NumericDisplay); + require(page.succeeded && button.succeeded && second_button.succeeded + && display.succeeded && second_display.succeeded, + "property-panel fixture must create its page and controls"); + const HmiControl *default_button = hmi_editor_service.findControl( + page.id, button.id); + const HmiControl *default_second_button = hmi_editor_service.findControl( + page.id, second_button.id); + const HmiControl *default_display = hmi_editor_service.findControl( + page.id, display.id); + const HmiControl *default_second_display = hmi_editor_service.findControl( + page.id, second_display.id); + require(default_button != nullptr && default_second_button != nullptr + && default_display != nullptr && default_second_display != nullptr + && default_button->binding + == RegisterAddress{RegisterArea::M, 0} + && default_second_button->binding + == RegisterAddress{RegisterArea::M, 1} + && default_display->binding + == RegisterAddress{RegisterArea::D, 0} + && default_second_display->binding + == RegisterAddress{RegisterArea::D, 1}, + "new HMI controls must receive sequential default M/D addresses"); + + QMainWindow parent; + Ui::MainWindow ui; + ui.setupUi(&parent); + HmiEditorWidget hmi_editor_widget( + hmi_editor_service, hmi_runtime_service, alarm_service, &parent); + LogicEditorWidget logic_editor_widget(logic_editor_service, &parent); + hmi_editor_widget.setPageId(page.id); + + std::string selected_control_id; + std::string selected_logic_node_id; + PropertyPanelController controller( + parent, + ui, + project_service, + hmi_editor_service, + logic_editor_service, + hmi_defaults, + [&page] { return page.id; }, + [] { return std::string{}; }, + selected_control_id, + selected_logic_node_id, + [] {}, + [](const QString &, const QString &message, bool succeeded) + { + require(succeeded, message.toStdString()); + }, + [](const QString &, int) {}); + controller.configure(); + controller.bindEditorWidgets(hmi_editor_widget, logic_editor_widget); + + controller.showControlProperties(button.id); + require(ui.bindingAreaLabel->text() + == QStringLiteral("绑定地址(M)") + && ui.bindingIndexSpinBox->isEnabled() + && ui.bindingIndexSpinBox->value() == 0, + "button properties must show the fixed M address"); + ui.bindingIndexSpinBox->setValue(17); + controller.applySelectedControlProperties(); + const HmiControl *updated_button = hmi_editor_service.findControl( + page.id, button.id); + require(updated_button != nullptr + && updated_button->binding + == RegisterAddress{RegisterArea::M, 17}, + "button property submission must infer the M area"); + + controller.showControlProperties(display.id); + require(ui.bindingAreaLabel->text() + == QStringLiteral("绑定地址(D)") + && ui.bindingIndexSpinBox->isEnabled() + && ui.bindingIndexSpinBox->value() == 0, + "numeric properties must show the fixed D address"); + ui.bindingIndexSpinBox->setValue(23); + controller.applySelectedControlProperties(); + const HmiControl *updated_display = hmi_editor_service.findControl( + page.id, display.id); + require(updated_display != nullptr + && updated_display->binding + == RegisterAddress{RegisterArea::D, 23}, + "numeric property submission must infer the D area"); + +} + void testQueuedOfflineTraceIsIgnoredAfterReturningToEditing() { TestProjectStorage storage; @@ -1283,6 +1392,7 @@ int main(int argc, char *argv[]) testAlarmConfigurationOffersMOnAndMOff(); testMonitorOffersAllNumericTypes(); testAlarmListKeepsFixedGeometryWhileRecordsChange(); + testHmiPropertyPanelInfersFixedBindingAreas(); testQueuedOfflineTraceIsIgnoredAfterReturningToEditing(); testLogicEditorGridSelectionAndDeletion(); testLadderLayoutAndDragDeletion(); diff --git a/app/tests/runtime_panel_controller_tests.pro b/app/tests/runtime_panel_controller_tests.pro index c9eaec8..d5c6d0a 100644 --- a/app/tests/runtime_panel_controller_tests.pro +++ b/app/tests/runtime_panel_controller_tests.pro @@ -16,11 +16,15 @@ SOURCES += \ $$SERVICE_RUNTIME_SOURCES \ $$SERVICE_MONITOR_SOURCES \ ../src/ui/alarm_configuration_dialog.cpp \ + ../src/ui/button_extension_dialog.cpp \ ../src/ui/hmi_editor_widget.cpp \ + ../src/ui/logic_instruction_dialog.cpp \ ../src/ui/logic_editor_widget.cpp \ + ../src/ui/property_panel_controller.cpp \ ../src/ui/free_monitor_widget.cpp \ ../src/ui/runtime_monitor_window.cpp \ ../src/ui/runtime_monitor_widget.cpp \ + ../src/ui/status_text_dialog.cpp \ ../src/ui/toolbar_icon_factory.cpp \ ../src/ui/runtime_panel_controller.cpp @@ -35,17 +39,25 @@ HEADERS += \ $$SERVICE_MONITOR_HEADERS \ ../src/services/plc_communication_gateway.h \ ../src/ui/alarm_configuration_dialog.h \ + ../src/ui/button_extension_dialog.h \ ../src/ui/hmi_editor_widget.h \ + ../src/ui/logic_instruction_dialog.h \ ../src/ui/logic_editor_widget.h \ + ../src/ui/property_panel_controller.h \ ../src/ui/free_monitor_widget.h \ ../src/ui/runtime_monitor_window.h \ ../src/ui/runtime_monitor_widget.h \ + ../src/ui/status_text_dialog.h \ ../src/ui/toolbar_icon_factory.h \ ../src/ui/runtime_panel_controller.h \ $$TEST_SUPPORT_HEADERS FORMS += \ ../src/ui/alarm_configuration_dialog.ui \ + ../src/ui/button_extension_dialog.ui \ ../src/ui/free_monitor_widget.ui \ + ../src/ui/logic_instruction_dialog.ui \ + ../src/ui/main_window.ui \ ../src/ui/runtime_monitor_window.ui \ - ../src/ui/runtime_monitor_widget.ui + ../src/ui/runtime_monitor_widget.ui \ + ../src/ui/status_text_dialog.ui