Bläddra i källkod

feat: 简化HMI控件寄存器绑定

main
suyu 3 veckor sedan
förälder
incheckning
34bdd3ffe3
7 ändrade filer med 222 tillägg och 49 borttagningar
  1. +72
    -1
      app/src/services/hmi_editor_service.cpp
  2. +1
    -1
      app/src/services/hmi_editor_service.h
  3. +2
    -28
      app/src/ui/main_window.ui
  4. +15
    -18
      app/src/ui/property_panel_controller.cpp
  5. +9
    -0
      app/tests/hmi_editor_service_tests.cpp
  6. +110
    -0
      app/tests/runtime_panel_controller_tests.cpp
  7. +13
    -1
      app/tests/runtime_panel_controller_tests.pro

+ 72
- 1
app/src/services/hmi_editor_service.cpp Visa fil

@@ -8,6 +8,7 @@
#include <cctype>
#include <cstddef>
#include <map>
#include <optional>
#include <set>
#include <utility>

@@ -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<RegisterAddress> nextHmiBindingAddress(
const Project &project,
const HmiControlDescriptor &descriptor,
RegisterDataType data_type)
{
std::optional<RegisterArea> 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<RegisterAddress> 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(


+ 1
- 1
app/src/services/hmi_editor_service.h Visa fil

@@ -301,7 +301,7 @@ private:
* @brief 按控件类型创建带默认属性的新控件
* @param page 新控件所属页面,用于确定初始位置和唯一标识
* @param descriptor 新控件类型对应的注册描述
* @return 未绑定寄存器的默认控件配置
* @return 带默认外观和未分配地址的控件配置
*/
static HmiControl makeControl(
const HmiPage &page, const HmiControlDescriptor &descriptor);


+ 2
- 28
app/src/ui/main_window.ui Visa fil

@@ -626,38 +626,12 @@
</item>
<item row="6" column="0">
<widget class="QLabel" name="bindingAreaLabel">
<property name="text">
<string>绑定区域</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QComboBox" name="bindingAreaComboBox">
<item>
<property name="text">
<string>未绑定</string>
</property>
</item>
<item>
<property name="text">
<string>M</string>
<string>绑定地址</string>
</property>
</item>
<item>
<property name="text">
<string>D</string>
</property>
</item>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="bindingIndexLabel">
<property name="text">
<string>绑定地址</string>
</property>
</widget>
</item>
<item row="7" column="1">
<item row="6" column="1">
<widget class="QSpinBox" name="bindingIndexSpinBox">
<property name="maximum">
<number>4000</number>


+ 15
- 18
app/src/ui/property_panel_controller.cpp Visa fil

@@ -13,7 +13,6 @@
#include "ui_main_window.h"

#include <QApplication>
#include <QCheckBox>
#include <QColorDialog>
#include <QComboBox>
#include <QLabel>
@@ -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<int>(HmiButtonOperation::SetOn));
ui_.buttonOperationComboBox->setItemData(
@@ -205,7 +201,6 @@ void PropertyPanelController::showControlProperties(const std::string &control_i
static_cast<QWidget *>(ui_.controlYSpinBox),
static_cast<QWidget *>(ui_.controlWidthSpinBox),
static_cast<QWidget *>(ui_.controlHeightSpinBox),
static_cast<QWidget *>(ui_.bindingAreaComboBox),
static_cast<QWidget *>(ui_.bindingIndexSpinBox),
static_cast<QWidget *>(ui_.targetPageComboBox),
static_cast<QWidget *>(ui_.buttonOperationComboBox),
@@ -227,8 +222,6 @@ void PropertyPanelController::showControlProperties(const std::string &control_i
ui_.controlTextEdit->setMaxLength(
static_cast<int>(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<RegisterArea> 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<RegisterArea> 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)


+ 9
- 0
app/tests/hmi_editor_service_tests.cpp Visa fil

@@ -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,


+ 110
- 0
app/tests/runtime_panel_controller_tests.cpp Visa fil

@@ -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 <QApplication>
#include <QCoreApplication>
@@ -28,10 +30,12 @@
#include <QKeyEvent>
#include <QLabel>
#include <QLineEdit>
#include <QMainWindow>
#include <QComboBox>
#include <QMouseEvent>
#include <QPainter>
#include <QScrollBar>
#include <QSpinBox>
#include <QWidget>

#include <algorithm>
@@ -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();


+ 13
- 1
app/tests/runtime_panel_controller_tests.pro Visa fil

@@ -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

Laddar…
Avbryt
Spara