Procházet zdrojové kódy

feat: 增加 HMI 控件基础布局对齐

main
suyu před 3 týdny
rodič
revize
9be85587fc
11 změnil soubory, kde provedl 465 přidání a 14 odebrání
  1. +18
    -0
      app/src/domain/register_address.cpp
  2. +18
    -12
      app/src/domain/register_address.h
  3. +144
    -0
      app/src/services/hmi_editor_service.cpp
  4. +26
    -0
      app/src/services/hmi_editor_service.h
  5. +27
    -0
      app/src/ui/hmi_editor_widget.cpp
  6. +2
    -0
      app/src/ui/hmi_editor_widget.h
  7. +83
    -2
      app/src/ui/main_window.cpp
  8. +12
    -0
      app/src/ui/main_window.h
  9. +9
    -0
      app/src/ui/toolbar_icon_factory.cpp
  10. +1
    -0
      app/src/ui/toolbar_icon_factory.h
  11. +125
    -0
      app/tests/hmi_editor_service_tests.cpp

+ 18
- 0
app/src/domain/register_address.cpp Zobrazit soubor

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

namespace {

// 判断寄存器区域是否为项目支持的 M 区或 D 区
bool isSupportedArea(RegisterArea area)
{
// 地址值对象只承认项目定义的 M 区和 D 区
@@ -14,8 +15,10 @@ bool isSupportedArea(RegisterArea area)

} // namespace

// 解析文本中的 M/D 区域和数字下标,并返回具体失败原因
RegisterAddressParseResult parseRegisterAddress(const std::string &text)
{
// 从两端找到第一个非空白字符,后续解析只处理去掉首尾空白后的内容
const auto first = std::find_if_not(
text.begin(), text.end(), [](unsigned char value) { return std::isspace(value); });
const auto last = std::find_if_not(
@@ -23,9 +26,11 @@ RegisterAddressParseResult parseRegisterAddress(const std::string &text)
.base();
if (first >= last)
{
// 首尾空白之外没有任何内容,说明用户没有输入地址
return {false, {RegisterArea::M, 0}, RegisterAddressParseError::Empty};
}

// 将首字符统一转成大写,使 m10 和 M10 的解析结果保持一致
const char area_character = static_cast<char>(
std::toupper(static_cast<unsigned char>(*first)));
RegisterArea area = RegisterArea::M;
@@ -39,57 +44,68 @@ RegisterAddressParseResult parseRegisterAddress(const std::string &text)
}
else
{
// 首字符不是 M 或 D,返回“不支持的区域”错误并保留默认地址
return {false, {RegisterArea::M, 0}, RegisterAddressParseError::UnsupportedArea};
}

if (first + 1 == last)
{
// 只有区域字母而没有数字下标,例如单独输入 M 或 D
return {false, {area, 0}, RegisterAddressParseError::InvalidFormat};
}
int index = 0;
// 逐字符读取下标,避免直接调用字符串转数字函数带来格式不透明的问题
for (auto current = first + 1; current != last; ++current)
{
const unsigned char character = static_cast<unsigned char>(*current);
if (!std::isdigit(character))
{
// 下标部分只能由十进制数字组成
return {false, {area, 0}, RegisterAddressParseError::InvalidFormat};
}
const int digit = *current - '0';
if (index > (std::numeric_limits<int>::max() - digit) / 10)
{
// 在执行 index * 10 + digit 前先检查,防止整数溢出
return {false, {area, 0}, RegisterAddressParseError::OutOfRange};
}
index = index * 10 + digit;
}

RegisterAddress address{area, index};
// 解析完成后再检查项目边界,区分格式正确但下标超范围的地址
return address.isValid()
? RegisterAddressParseResult{true, address, RegisterAddressParseError::None}
: RegisterAddressParseResult{false, address, RegisterAddressParseError::OutOfRange};
}

// 使用指定区域和下标创建寄存器地址对象
RegisterAddress::RegisterAddress(RegisterArea area, int index)
: area_(area),
index_(index)
{
}

// 返回地址所属的寄存器区域
RegisterArea RegisterAddress::area() const
{
return area_;
}

// 返回地址的数字下标
int RegisterAddress::index() const
{
return index_;
}

// 检查区域和下标是否符合项目支持的地址范围
bool RegisterAddress::isValid() const
{
return isSupportedArea(area_)
&& index_ >= kMinimumIndex && index_ <= kMaximumIndex;
}

// 将地址转换为 M10 或 D4000 这样的显示文本
std::string RegisterAddress::toString() const
{
const char *areaName = nullptr;
@@ -109,11 +125,13 @@ std::string RegisterAddress::toString() const
return std::string(areaName) + std::to_string(index_);
}

// 判断当前地址与另一个地址是否完全相同
bool RegisterAddress::operator==(const RegisterAddress &other) const
{
return area_ == other.area_ && index_ == other.index_;
}

// 判断当前地址与另一个地址是否不同
bool RegisterAddress::operator!=(const RegisterAddress &other) const
{
return !(*this == other);


+ 18
- 12
app/src/domain/register_address.h Zobrazit soubor

@@ -5,18 +5,18 @@
// 解析用户输入的 M/D 地址时可能出现的失败原因
enum class RegisterAddressParseError
{
None,
Empty,
UnsupportedArea,
InvalidFormat,
OutOfRange
None, // 地址文本解析成功
Empty, // 输入文本为空或只包含空白
UnsupportedArea, // 地址区域不是项目支持的 M 或 D
InvalidFormat, // 地址文本格式不符合要求
OutOfRange // 地址下标超出 0~4000 范围
};

// PLC 中支持的寄存器区域:M 是位,D 是 16 位有符号字
enum class RegisterArea
{
M,
D
M, // M 位寄存器区域
D // D 16 位数据寄存器区域
};

// 表示一个已经拆分为“区域 + 下标”的 M/D 地址
@@ -24,33 +24,39 @@ enum class RegisterArea
class RegisterAddress
{
public:
static constexpr int kMinimumIndex = 0;
static constexpr int kMaximumIndex = 4000;
static constexpr int kMinimumIndex = 0; // 地址允许使用的最小下标
static constexpr int kMaximumIndex = 4000; // 地址允许使用的最大下标

// 构造地址对象;非法下标不会在构造时抛异常,应通过 isValid() 检查
RegisterAddress(RegisterArea area, int index);

// 返回地址区域和数字下标
// 返回地址所属的寄存器区域
RegisterArea area() const;
// 返回地址的数字下标
int index() const;
// 判断区域和下标是否满足项目边界
bool isValid() const;
// 转成界面和日志中使用的形式,例如 M10 或 D4000
std::string toString() const;

// 判断两个地址的区域和下标是否都相同
bool operator==(const RegisterAddress &other) const;
// 判断两个地址是否存在区域或下标差异
bool operator!=(const RegisterAddress &other) const;

private:
RegisterArea area_;
int index_;
RegisterArea area_; // 保存地址所属的寄存器区域
int index_; // 保存地址的数字下标
};

// 文本地址解析结果;失败时 address 保持默认值,具体原因见 error
struct RegisterAddressParseResult
{
// 表示文本是否成功解析为有效地址
bool succeeded = false;
// 保存解析出的地址或失败时的默认地址
RegisterAddress address{RegisterArea::M, 0};
// 保存成功状态或具体失败原因
RegisterAddressParseError error = RegisterAddressParseError::InvalidFormat;
};



+ 144
- 0
app/src/services/hmi_editor_service.cpp Zobrazit soubor

@@ -578,6 +578,150 @@ HmiEditorResult HmiEditorService::moveControl(
return {true, HmiEditorError::None, {}, control_id};
}

HmiEditorResult HmiEditorService::alignControls(
const std::string &page_id,
const std::vector<std::string> &control_ids,
HmiAlignment alignment)
{
const HmiPage *page = findPage(page_id);
if (page == nullptr)
{
return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面");
}
if (control_ids.size() < 2U)
{
return failure(
HmiEditorError::InvalidOperation,
"请至少选择两个 HMI 控件");
}

std::set<std::string> selected_ids;
for (const std::string &control_id : control_ids)
{
if (!selected_ids.insert(control_id).second)
{
return failure(
HmiEditorError::InvalidOperation,
"对齐列表中存在重复控件");
}
if (findControl(page_id, control_id) == nullptr)
{
return failure(HmiEditorError::ControlNotFound, "未找到 HMI 控件");
}
}

const HmiControl *first = findControl(page_id, control_ids.front());
int left = first->bounds.x;
int top = first->bounds.y;
int right = first->bounds.x + first->bounds.width;
int bottom = first->bounds.y + first->bounds.height;
for (std::size_t index = 1U; index < control_ids.size(); ++index)
{
const HmiControl *control = findControl(page_id, control_ids[index]);
left = std::min(left, control->bounds.x);
top = std::min(top, control->bounds.y);
right = std::max(
right, control->bounds.x + control->bounds.width);
bottom = std::max(
bottom, control->bounds.y + control->bounds.height);
}

bool supported = true;
switch (alignment)
{
case HmiAlignment::Left:
case HmiAlignment::HorizontalCenter:
case HmiAlignment::Right:
case HmiAlignment::Top:
case HmiAlignment::VerticalCenter:
case HmiAlignment::Bottom:
break;
default:
supported = false;
break;
}
if (!supported)
{
return failure(HmiEditorError::InvalidOperation, "不支持的 HMI 对齐方式");
}

HmiPage candidate_page = *page;
const int horizontal_center = (left + right) / 2;
const int vertical_center = (top + bottom) / 2;
bool changed = false;
for (HmiControl &control : candidate_page.controls)
{
if (selected_ids.find(control.id) == selected_ids.end())
{
continue;
}

const HmiRect original = control.bounds;
switch (alignment)
{
case HmiAlignment::Left:
control.bounds.x = left;
break;
case HmiAlignment::HorizontalCenter:
control.bounds.x = horizontal_center - control.bounds.width / 2;
break;
case HmiAlignment::Right:
control.bounds.x = right - control.bounds.width;
break;
case HmiAlignment::Top:
control.bounds.y = top;
break;
case HmiAlignment::VerticalCenter:
control.bounds.y = vertical_center - control.bounds.height / 2;
break;
case HmiAlignment::Bottom:
control.bounds.y = bottom - control.bounds.height;
break;
default:
break;
}
changed = changed || original.x != control.bounds.x
|| original.y != control.bounds.y;
}

if (!changed)
{
return {
true,
HmiEditorError::None,
{},
control_ids.front()};
}

std::string error;
if (!candidate_page.validate(project_service_.projectLimits(), &error))
{
return failure(HmiEditorError::InvalidControl, error);
}

const HistoryState before = captureState();
Project &project = project_service_.editProject();
auto target_page = std::find_if(
project.hmiPages.begin(), project.hmiPages.end(),
[&page_id](const HmiPage &item) { return item.id == page_id; });
for (const HmiControl &candidate : candidate_page.controls)
{
if (selected_ids.find(candidate.id) == selected_ids.end())
{
continue;
}
auto editable = std::find_if(
target_page->controls.begin(), target_page->controls.end(),
[&candidate](const HmiControl &item)
{
return item.id == candidate.id;
});
editable->bounds = candidate.bounds;
}
recordHistory(before);
return {true, HmiEditorError::None, {}, control_ids.front()};
}

// 更新控件全部属性
HmiEditorResult HmiEditorService::updateControl(
const std::string &page_id,


+ 26
- 0
app/src/services/hmi_editor_service.h Zobrazit soubor

@@ -47,6 +47,19 @@ struct HmiEditorResult
std::string id; // 成功时返回新建或更新后的页面/控件 ID
};

/**
* @brief HMI 控件批量对齐方向
*/
enum class HmiAlignment
{
Left,
HorizontalCenter,
Right,
Top,
VerticalCenter,
Bottom
};

/**
* @brief 编排 HMI 页面和控件的编辑操作,不依赖 Qt 视图
*
@@ -174,6 +187,19 @@ public:
const std::string &page_id,
const std::string &control_id,
const HmiRect &bounds);
/**
* @brief 批量对齐同一页面中的 HMI 控件
* @param page_id 控件所属页面唯一标识
* @param control_ids 待对齐控件 ID 列表,至少包含两个不重复 ID
* @param alignment 对齐方向,按所选控件整体外接矩形计算
* @return 页面、控件、选择列表或对齐结果无效时返回失败结果
*
* 对齐只修改控件位置,整批操作只产生一条撤销记录
*/
HmiEditorResult alignControls(
const std::string &page_id,
const std::vector<std::string> &control_ids,
HmiAlignment alignment);
/**
* @brief 更新控件可编辑属性
* @param page_id 所属页面唯一标识


+ 27
- 0
app/src/ui/hmi_editor_widget.cpp Zobrazit soubor

@@ -1046,6 +1046,33 @@ void HmiEditorWidget::selectControl(const std::string &control_id)
}
}

void HmiEditorWidget::selectControls(
const std::vector<std::string> &control_ids)
{
scene_->clearSelection();
HmiGraphicsItem *first_selected = nullptr;
for (QGraphicsItem *item : scene_->items())
{
HmiGraphicsItem *control_item = asHmiItem(item);
if (control_item == nullptr
|| std::find(
control_ids.cbegin(), control_ids.cend(),
control_item->controlId()) == control_ids.cend())
{
continue;
}
control_item->setSelected(true);
if (first_selected == nullptr)
{
first_selected = control_item;
}
}
if (first_selected != nullptr)
{
ensureVisible(first_selected);
}
}

std::string HmiEditorWidget::selectedControlId() const
{
const std::vector<std::string> ids = selectedControlIds();


+ 2
- 0
app/src/ui/hmi_editor_widget.h Zobrazit soubor

@@ -70,6 +70,8 @@ public:
* @param control_id 待选中控件标识,不存在时不改变当前选择
*/
void selectControl(const std::string &control_id);
/** 选中指定的多个控件并滚动到第一个可见控件 */
void selectControls(const std::vector<std::string> &control_ids);
/** 返回当前选中的单个控件标识 */
std::string selectedControlId() const;
/** 返回当前选中的多个控件标识 */


+ 83
- 2
app/src/ui/main_window.cpp Zobrazit soubor

@@ -655,6 +655,42 @@ void MainWindow::configureActions()
[this] { addHmiControl(HmiControlType::AlarmList); });
connect(ui_->deleteControlAction, &QAction::triggered,
this, &MainWindow::deleteSelectedControl);
align_left_action_ = new QAction(tr("左对齐"), this);
align_horizontal_center_action_ = new QAction(tr("水平居中"), this);
align_right_action_ = new QAction(tr("右对齐"), this);
align_top_action_ = new QAction(tr("顶部对齐"), this);
align_vertical_center_action_ = new QAction(tr("垂直居中"), this);
align_bottom_action_ = new QAction(tr("底部对齐"), this);
hmi_layout_button_ = addToolbarMenu(
ui_->hmiToolBar,
tr("布局"),
QStringLiteral("hmiLayoutMenuButton"),
makeUiIcon(UiIcon::Layout),
QList<QAction *>{
align_left_action_,
align_horizontal_center_action_,
align_right_action_,
align_top_action_,
align_vertical_center_action_,
align_bottom_action_});
connect(align_left_action_, &QAction::triggered,
this, [this] { alignSelectedHmiControls(HmiAlignment::Left); });
connect(align_horizontal_center_action_, &QAction::triggered,
this, [this]
{
alignSelectedHmiControls(HmiAlignment::HorizontalCenter);
});
connect(align_right_action_, &QAction::triggered,
this, [this] { alignSelectedHmiControls(HmiAlignment::Right); });
connect(align_top_action_, &QAction::triggered,
this, [this] { alignSelectedHmiControls(HmiAlignment::Top); });
connect(align_vertical_center_action_, &QAction::triggered,
this, [this]
{
alignSelectedHmiControls(HmiAlignment::VerticalCenter);
});
connect(align_bottom_action_, &QAction::triggered,
this, [this] { alignSelectedHmiControls(HmiAlignment::Bottom); });
addToolbarMenu(
ui_->hmiToolBar,
tr("更多控件"),
@@ -1494,8 +1530,10 @@ void MainWindow::updateEditActions()
ui_->redoAction->setEnabled(
editable && ((hmi_active && hmi_editor_service_.canRedo())
|| (logic_active && logic_editor_service_.canRedo())));
const bool hmi_selection = hmi_active
&& !hmi_editor_widget_->selectedControlIds().empty();
const std::vector<std::string> hmi_selected_ids = hmi_editor_widget_ == nullptr
? std::vector<std::string>{}
: hmi_editor_widget_->selectedControlIds();
const bool hmi_selection = hmi_active && !hmi_selected_ids.empty();
const bool logic_selection = logic_active
&& logic_editor_widget_->hasCopyableSelection();
ui_->copyAction->setEnabled(
@@ -1507,6 +1545,25 @@ void MainWindow::updateEditActions()
|| (logic_active && logic_clipboard)));
ui_->deleteSelectionAction->setEnabled(editable && (hmi_active || logic_active));
ui_->clearSelectionAction->setEnabled(editable && (hmi_active || logic_active));
const bool hmi_alignment_enabled =
editable && hmi_active && hmi_selected_ids.size() >= 2U;
for (QAction *action : {
align_left_action_,
align_horizontal_center_action_,
align_right_action_,
align_top_action_,
align_vertical_center_action_,
align_bottom_action_})
{
if (action != nullptr)
{
action->setEnabled(hmi_alignment_enabled);
}
}
if (hmi_layout_button_ != nullptr)
{
hmi_layout_button_->setEnabled(hmi_alignment_enabled);
}
ui_->insertHorizontalWireAction->setEnabled(editable && logic_active);
ui_->insertVerticalWireAction->setEnabled(editable && logic_active);
ui_->insertRungAboveAction->setEnabled(editable && logic_active);
@@ -1660,6 +1717,30 @@ void MainWindow::deleteSelectedControl()
property_panel_controller_->deleteSelectedControl();
}

void MainWindow::alignSelectedHmiControls(HmiAlignment alignment)
{
if (!runtime_mode_service_.policy().allowsProjectEditing
|| hmi_editor_widget_ == nullptr)
{
return;
}
const std::vector<std::string> control_ids =
hmi_editor_widget_->selectedControlIds();
const HmiEditorResult result = hmi_editor_service_.alignControls(
current_hmi_page_id_, control_ids, alignment);
if (!result.succeeded)
{
showProjectResult(tr("对齐 HMI 控件"), fromUtf8(result.message), false);
return;
}

hmi_editor_widget_->reloadPage();
hmi_editor_widget_->selectControls(control_ids);
refreshProjectUi();
statusBar()->showMessage(
tr("已对齐 %1 个 HMI 控件").arg(control_ids.size()), 3000);
}

void MainWindow::addLogicCondition(const LogicNodeConfig &config)
{
const LogicEditorResult result = logic_editor_widget_->addCondition(config);


+ 12
- 0
app/src/ui/main_window.h Zobrazit soubor

@@ -29,6 +29,7 @@ class QCloseEvent;
class QEvent;
class QLabel;
class QTimer;
class QToolButton;

namespace Ui {
class MainWindow;
@@ -38,6 +39,7 @@ QT_END_NAMESPACE
class RuntimeModeService;
class ProjectService;
class HmiEditorService;
enum class HmiAlignment;
class HmiRuntimeService;
class AlarmEditorService;
class AlarmService;
@@ -172,6 +174,8 @@ private:
void addHmiControl(HmiControlType type);
/** 删除当前选中的 HMI 控件 */
void deleteSelectedControl();
/** 对当前选中的 HMI 控件执行批量对齐 */
void alignSelectedHmiControls(HmiAlignment alignment);
/** 在当前光标节点后添加串联条件,没有选择时追加到网络末尾 */
void addLogicCondition(const LogicNodeConfig &config);
/** 为当前选中的连续逻辑范围建立并联支路 */
@@ -304,6 +308,14 @@ private:
std::unique_ptr<RuntimePanelController> runtime_panel_controller_;
/** 运行模式互斥动作组 */
QActionGroup *mode_action_group_ = nullptr;
/** HMI 控件布局菜单按钮和对齐动作 */
QToolButton *hmi_layout_button_ = nullptr;
QAction *align_left_action_ = nullptr;
QAction *align_horizontal_center_action_ = nullptr;
QAction *align_right_action_ = nullptr;
QAction *align_top_action_ = nullptr;
QAction *align_vertical_center_action_ = nullptr;
QAction *align_bottom_action_ = nullptr;
/** 主界面的 HMI 编辑器 */
HmiEditorWidget *hmi_editor_widget_ = nullptr;
/** 主界面的梯形图编辑器 */


+ 9
- 0
app/src/ui/toolbar_icon_factory.cpp Zobrazit soubor

@@ -475,6 +475,15 @@ QPixmap renderIcon(UiIcon icon, int size)
painter.drawLine(QPointF(14, 19), QPointF(21, 12));
break;
}
case UiIcon::Layout:
{
painter.drawRect(QRectF(3, 5, 7, 7));
painter.drawRect(QRectF(14, 5, 7, 7));
painter.drawRect(QRectF(3, 15, 7, 5));
painter.drawLine(QPointF(11, 8), QPointF(13, 8));
painter.drawLine(QPointF(11, 17), QPointF(13, 17));
break;
}
case UiIcon::More:
{
painter.setPen(Qt::NoPen);


+ 1
- 0
app/src/ui/toolbar_icon_factory.h Zobrazit soubor

@@ -54,6 +54,7 @@ enum class UiIcon
Subtract, // SUB 指令
Compare, // 比较指令
Comment, // 注释
Layout, // HMI 控件布局
SyntaxCheck, // 梯形图语法检查与规整
More, // 更多操作
HmiPage, // HMI 页面


+ 125
- 0
app/tests/hmi_editor_service_tests.cpp Zobrazit soubor

@@ -68,6 +68,130 @@ void testControlEditing()
"deleted controls must not remain in the model");
}

void testBatchControlAlignment()
{
TestProjectStorage storage;
ProjectService project_service(storage, defaultProjectLimitSettings());
HmiEditorService service(project_service, HmiDefaultSettings{});
const std::string page_id = service.ensureDefaultPage().id;
const HmiEditorResult first = service.addControl(page_id, HmiControlType::Label);
const HmiEditorResult second = service.addControl(page_id, HmiControlType::Label);
const HmiEditorResult third = service.addControl(page_id, HmiControlType::Label);
require(first.succeeded && second.succeeded && third.succeeded,
"controls for alignment testing must be created");

const std::vector<std::string> ids{first.id, second.id, third.id};
const std::vector<HmiRect> original_bounds{
{100, 80, 80, 30},
{240, 120, 100, 40},
{380, 160, 60, 20}};
for (std::size_t index = 0U; index < ids.size(); ++index)
{
HmiControl control = *service.findControl(page_id, ids[index]);
control.bounds = original_bounds[index];
require(service.updateControl(page_id, ids[index], control).succeeded,
"alignment fixtures must be positioned successfully");
}

service.clearHistory();
require(service.alignControls(page_id, ids, HmiAlignment::Left).succeeded,
"left alignment must succeed");
require(service.findControl(page_id, first.id)->bounds.x == 100
&& service.findControl(page_id, second.id)->bounds.x == 100
&& service.findControl(page_id, third.id)->bounds.x == 100,
"left alignment must use the selected group left edge");
require(service.findControl(page_id, second.id)->bounds.y == 120,
"alignment must preserve the non-aligned coordinate");
require(service.undo().succeeded
&& service.findControl(page_id, second.id)->bounds
.x == original_bounds[1].x,
"alignment must be restored by one undo step");
require(service.redo().succeeded
&& service.findControl(page_id, third.id)->bounds.x == 100,
"alignment redo must restore the whole batch");

const auto resetBounds = [&service, &page_id, &ids, &original_bounds]
{
for (std::size_t index = 0U; index < ids.size(); ++index)
{
HmiControl control = *service.findControl(page_id, ids[index]);
control.bounds = original_bounds[index];
require(service.updateControl(page_id, ids[index], control).succeeded,
"alignment fixtures must be reset successfully");
}
};
const auto assertBounds = [&service, &page_id, &ids, &resetBounds](
HmiAlignment alignment, const std::vector<HmiRect> &expected,
const char *message)
{
resetBounds();
service.clearHistory();
require(service.alignControls(page_id, ids, alignment).succeeded, message);
for (std::size_t index = 0U; index < ids.size(); ++index)
{
const HmiControl *control = service.findControl(page_id, ids[index]);
require(control != nullptr
&& control->bounds.x == expected[index].x
&& control->bounds.y == expected[index].y,
"alignment must calculate deterministic target coordinates");
}
};

assertBounds(
HmiAlignment::HorizontalCenter,
{{230, 80, 0, 0}, {220, 120, 0, 0}, {240, 160, 0, 0}},
"horizontal center alignment must succeed");
assertBounds(
HmiAlignment::Right,
{{360, 80, 0, 0}, {340, 120, 0, 0}, {380, 160, 0, 0}},
"right alignment must succeed");
assertBounds(
HmiAlignment::Top,
{{100, 80, 0, 0}, {240, 80, 0, 0}, {380, 80, 0, 0}},
"top alignment must succeed");
assertBounds(
HmiAlignment::VerticalCenter,
{{100, 115, 0, 0}, {240, 110, 0, 0}, {380, 120, 0, 0}},
"vertical center alignment must succeed");
assertBounds(
HmiAlignment::Bottom,
{{100, 150, 0, 0}, {240, 140, 0, 0}, {380, 160, 0, 0}},
"bottom alignment must succeed");

service.clearHistory();
const std::vector<HmiRect> before_invalid{
service.findControl(page_id, first.id)->bounds,
service.findControl(page_id, second.id)->bounds,
service.findControl(page_id, third.id)->bounds};
require(!service.alignControls(
page_id, {first.id, "missing-control"}, HmiAlignment::Left)
.succeeded,
"alignment must reject an unknown control before mutation");
require(!service.canUndo()
&& service.findControl(page_id, first.id)->bounds.x
== before_invalid[0].x
&& service.findControl(page_id, second.id)->bounds.y
== before_invalid[1].y,
"failed alignment must be atomic and leave history unchanged");
require(!service.alignControls(page_id, {first.id}, HmiAlignment::Left).succeeded,
"alignment must require at least two controls");

HmiControl no_op = *service.findControl(page_id, first.id);
no_op.bounds.x = 100;
HmiControl no_op_second = *service.findControl(page_id, second.id);
no_op_second.bounds.x = 100;
HmiControl no_op_third = *service.findControl(page_id, third.id);
no_op_third.bounds.x = 100;
require(service.updateControl(page_id, first.id, no_op).succeeded
&& service.updateControl(page_id, second.id, no_op_second).succeeded
&& service.updateControl(page_id, third.id, no_op_third).succeeded,
"alignment no-op fixtures must be positioned successfully");
service.clearHistory();
require(service.alignControls(page_id, ids, HmiAlignment::Left).succeeded
&& !service.canUndo(),
"a no-op alignment must not create an undo step");
}

void testRuntimeUsesRegisterRepository()
{
// 运行服务只能经由仓库接口读写 M/D,不依赖具体离线实现
@@ -588,6 +712,7 @@ int main()
{
// 编辑和运行场景分别验证服务层两条独立职责
testControlEditing();
testBatchControlAlignment();
testHistoryAndAtomicBatchDelete();
testBatchPasteControls();
testRuntimeValidationAndPasteBoundaries();


Načítá se…
Zrušit
Uložit