#include "hmi_editor_service.h" #include "domain/hmi_control_registry.h" #include "project_service.h" #include "domain/project_limits.h" #include #include #include #include #include #include #include namespace { // 仅在调用方提供错误字符串时写入失败原因 void setError(std::string *error, const std::string &message) { if (error != nullptr) { *error = message; } } // 创建统一的 HMI 编辑失败结果 HmiEditorResult failure(HmiEditorError error, const std::string &message) { return {false, error, message, {}}; } // 返回控件绑定值连续占用的 D 字数量 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() || std::all_of( value.cbegin(), value.cend(), [](unsigned char character) { return std::isspace(character) != 0; }); } // 从 page-1 开始递增生成工程内唯一的页面 ID std::string makeUniquePageId(const Project &project) { int suffix = 1; while (true) { const std::string candidate = "page-" + std::to_string(suffix); const bool found = std::any_of( project.hmiPages.cbegin(), project.hmiPages.cend(), [&candidate](const HmiPage &page) { return page.id == candidate; }); if (!found) { return candidate; } ++suffix; } } // 统计当前工程全部 HMI 页面中的控件总数 std::size_t totalControlCount(const Project &project) { std::size_t total = 0U; for (const HmiPage &page : project.hmiPages) { total += page.controls.size(); } return total; } } // namespace // 保存工程服务和 HMI 默认配置的引用 HmiEditorService::HmiEditorService( ProjectService &project_service, const HmiDefaultSettings &hmi_defaults) : project_service_(project_service), hmi_defaults_(hmi_defaults) { } // 深拷贝当前页面集合和初始页面 ID 作为历史快照 HmiEditorService::HistoryState HmiEditorService::captureState() const { const Project &project = project_service_.project(); return {project.hmiPages, project.initialHmiPageId}; } // 仅在编辑确实改变工程状态时记录撤销快照 void HmiEditorService::recordHistory(HistoryState before) { const HistoryState after = captureState(); history_.record(std::move(before), after, &HmiEditorService::statesEqual); } // 比较两个完整 HMI 编辑快照是否一致 bool HmiEditorService::statesEqual( const HistoryState &left, const HistoryState &right) { if (left.initial_page_id != right.initial_page_id || left.pages.size() != right.pages.size()) { return false; } for (std::size_t index = 0; index < left.pages.size(); ++index) { if (!pagesEqual(left.pages[index], right.pages[index])) { return false; } } return true; } // 比较两个页面的基础属性和全部控件 bool HmiEditorService::pagesEqual( const HmiPage &left, const HmiPage &right) { if (left.id != right.id || left.name != right.name || left.width != right.width || left.height != right.height || left.controls.size() != right.controls.size()) { return false; } for (std::size_t index = 0; index < left.controls.size(); ++index) { if (!controlsEqual(left.controls[index], right.controls[index])) { return false; } } return true; } // 比较两个控件中所有会保存到工程的属性 bool HmiEditorService::controlsEqual( const HmiControl &left, const HmiControl &right) { const bool page_jump_equal = left.pageJump.has_value() == right.pageJump.has_value() && (!left.pageJump.has_value() || left.pageJump->targetPageId == right.pageJump->targetPageId); return left.id == right.id && left.type == right.type && left.bounds.x == right.bounds.x && left.bounds.y == right.bounds.y && left.bounds.width == right.bounds.width && left.bounds.height == right.bounds.height && left.text == right.text && left.binding == right.binding && left.dataType == right.dataType && left.properties == right.properties && left.buttonOperation == right.buttonOperation && left.buttonEnableCondition == right.buttonEnableCondition && left.statusText == right.statusText && page_jump_equal; } // 创建撤销或重做不可执行时的统一失败结果 HmiEditorResult HmiEditorService::historyFailure(const std::string &message) { return {false, HmiEditorError::InvalidOperation, message, {}}; } // 遍历工程所有 HmiPage,根据页面 ID 查找页面,找不到返回空指针 const HmiPage *HmiEditorService::findPage(const std::string &page_id) const { const Project &project = project_service_.project(); const auto page = std::find_if( project.hmiPages.cbegin(), project.hmiPages.cend(), [&page_id](const HmiPage &candidate) { return candidate.id == page_id; }); return page == project.hmiPages.cend() ? nullptr : &*page; } // 先找到页面 → 在页面内查找指定控件;页面不存在 / 控件不存在都返回 nullptr const HmiControl *HmiEditorService::findControl( const std::string &page_id, const std::string &control_id) const { const HmiPage *page = findPage(page_id); if (page == nullptr) { return nullptr; } const auto control = std::find_if( page->controls.cbegin(), page->controls.cend(), [&control_id](const HmiControl &candidate) { return candidate.id == control_id; }); return control == page->controls.cend() ? nullptr : &*control; } // 获取第一个页面 ID;没有页面返回空字符串 std::string HmiEditorService::firstPageId() const { const Project &project = project_service_.project(); return project.hmiPages.empty() ? std::string{} : project.hmiPages.front().id; } // 保证工程至少存在一个 HMI 页面 HmiEditorResult HmiEditorService::ensureDefaultPage() { if (!project_service_.project().hmiPages.empty()) { if (project_service_.project().initialHmiPageId.empty()) { Project &project = project_service_.editProject(); project.initialHmiPageId = project.hmiPages.front().id; } return {true, HmiEditorError::None, {}, firstPageId()}; } // 仅在首个控件操作前创建默认页面,空工程仍可正常保存 HistoryState before = captureState(); HmiPage page; page.id = "page-1"; page.name = "主操作页面"; page.width = hmi_defaults_.pageWidth; page.height = hmi_defaults_.pageHeight; Project &project = project_service_.editProject(); project.hmiPages.push_back(std::move(page)); project.initialHmiPageId = project.hmiPages.back().id; recordHistory(std::move(before)); return {true, HmiEditorError::None, {}, project.hmiPages.back().id}; } // 新建并校验一个页面,成功后记录单步撤销历史 HmiEditorResult HmiEditorService::addPage(const std::string &name) { if (isBlank(name)) { return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能为空"); } const Project ¤t = project_service_.project(); const ProjectLimitSettings &limits = project_service_.projectLimits(); if (current.hmiPages.size() >= limits.maximumHmiPages) { return failure( HmiEditorError::InvalidPage, "当前配置最多允许 " + std::to_string(limits.maximumHmiPages) + " 个 HMI 页面"); } if (name.size() > ProjectLimits::kMaximumTextBytes) { return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能超过 256 个 UTF-8 字节"); } const bool duplicate = std::any_of( current.hmiPages.cbegin(), current.hmiPages.cend(), [&name](const HmiPage &page) { return page.name == name; }); if (duplicate) { return failure(HmiEditorError::DuplicateName, "HMI 页面名称必须唯一"); } HmiPage page; page.id = makeUniquePageId(current); page.name = name; page.width = hmi_defaults_.pageWidth; page.height = hmi_defaults_.pageHeight; HistoryState before = captureState(); Project &project = project_service_.editProject(); project.hmiPages.push_back(std::move(page)); if (project.initialHmiPageId.empty()) { project.initialHmiPageId = project.hmiPages.back().id; } recordHistory(std::move(before)); return {true, HmiEditorError::None, {}, project.hmiPages.back().id}; } // 校验新尺寸不会让已有控件越界后调整页面大小 HmiEditorResult HmiEditorService::resizePage( const std::string &page_id, int width, int height) { const HmiPage *page = findPage(page_id); if (page == nullptr) { return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面"); } if (page->width == width && page->height == height) { return {true, HmiEditorError::None, {}, page_id}; } // 先在副本上校验新页面边界,失败时不触碰当前工程 HmiPage candidate = *page; candidate.width = width; candidate.height = height; std::string error; if (!candidate.validate(project_service_.projectLimits(), &error)) { return failure(HmiEditorError::InvalidPage, error); } HistoryState before = captureState(); Project &project = project_service_.editProject(); auto editable = std::find_if( project.hmiPages.begin(), project.hmiPages.end(), [&page_id](const HmiPage &item) { return item.id == page_id; }); editable->width = width; editable->height = height; recordHistory(std::move(before)); return {true, HmiEditorError::None, {}, page_id}; } // 校验名称非空且不重复后修改页面名称 HmiEditorResult HmiEditorService::renamePage( const std::string &page_id, const std::string &name) { if (isBlank(name)) { return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能为空"); } if (name.size() > ProjectLimits::kMaximumTextBytes) { return failure(HmiEditorError::InvalidPage, "HMI 页面名称不能超过 256 个 UTF-8 字节"); } const Project ¤t = project_service_.project(); const auto existing = std::find_if( current.hmiPages.cbegin(), current.hmiPages.cend(), [&page_id](const HmiPage &page) { return page.id == page_id; }); if (existing == current.hmiPages.cend()) { return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面"); } const bool duplicate = std::any_of( current.hmiPages.cbegin(), current.hmiPages.cend(), [&page_id, &name](const HmiPage &page) { return page.id != page_id && page.name == name; }); if (duplicate) { return failure(HmiEditorError::DuplicateName, "HMI 页面名称必须唯一"); } if (existing->name == name) { return {true, HmiEditorError::None, {}, page_id}; } HistoryState before = captureState(); Project &project = project_service_.editProject(); auto page = std::find_if( project.hmiPages.begin(), project.hmiPages.end(), [&page_id](const HmiPage &candidate) { return candidate.id == page_id; }); page->name = name; recordHistory(std::move(before)); return {true, HmiEditorError::None, {}, page_id}; } // 确认页面可删除且未被引用后从工程中移除页面 HmiEditorResult HmiEditorService::removePage(const std::string &page_id) { const Project ¤t = project_service_.project(); const auto page = std::find_if( current.hmiPages.cbegin(), current.hmiPages.cend(), [&page_id](const HmiPage &candidate) { return candidate.id == page_id; }); if (page == current.hmiPages.cend()) { return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面"); } if (current.hmiPages.size() <= 1) { return failure(HmiEditorError::LastPageRequired, "工程至少需要保留一个 HMI 页面"); } if (current.initialHmiPageId == page_id) { return failure( HmiEditorError::InitialPageCannotBeRemoved, "请先设置其他初始页面,再删除当前页面"); } for (const HmiPage &source_page : current.hmiPages) { const auto reference = std::find_if( source_page.controls.cbegin(), source_page.controls.cend(), [&page_id](const HmiControl &control) { return control.type == HmiControlType::PageJump && control.pageJump.has_value() && control.pageJump->targetPageId == page_id; }); if (reference != source_page.controls.cend()) { return failure( HmiEditorError::PageReferenced, "页面仍被跳转控件 " + reference->id + " 引用"); } } HistoryState before = captureState(); Project &project = project_service_.editProject(); project.hmiPages.erase( std::remove_if( project.hmiPages.begin(), project.hmiPages.end(), [&page_id](const HmiPage &candidate) { return candidate.id == page_id; }), project.hmiPages.end()); recordHistory(std::move(before)); return {true, HmiEditorError::None, {}, page_id}; } // 将指定页面在页面列表中上移或下移一位 HmiEditorResult HmiEditorService::movePage(const std::string &page_id, int offset) { if (offset != -1 && offset != 1) { return failure(HmiEditorError::InvalidOperation, "页面每次只能上移或下移一位"); } const Project ¤t = project_service_.project(); const auto page = std::find_if( current.hmiPages.cbegin(), current.hmiPages.cend(), [&page_id](const HmiPage &candidate) { return candidate.id == page_id; }); if (page == current.hmiPages.cend()) { return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面"); } const auto index = std::distance(current.hmiPages.cbegin(), page); const std::ptrdiff_t target_index = index + offset; if (target_index < 0 || target_index >= static_cast(current.hmiPages.size())) { return failure(HmiEditorError::InvalidOperation, "HMI 页面已经位于目标边界"); } HistoryState before = captureState(); Project &project = project_service_.editProject(); std::iter_swap( project.hmiPages.begin() + index, project.hmiPages.begin() + target_index); recordHistory(std::move(before)); return {true, HmiEditorError::None, {}, page_id}; } // 将指定页面设置为工程启动时显示的初始页面 HmiEditorResult HmiEditorService::setInitialPage(const std::string &page_id) { if (findPage(page_id) == nullptr) { return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面"); } if (project_service_.project().initialHmiPageId == page_id) { return {true, HmiEditorError::None, {}, page_id}; } HistoryState before = captureState(); project_service_.editProject().initialHmiPageId = page_id; recordHistory(std::move(before)); return {true, HmiEditorError::None, {}, page_id}; } // 根据控件注册描述创建控件并分配默认寄存器地址 HmiEditorResult HmiEditorService::addControl( const std::string &page_id, HmiControlType type) { const HmiPage *page = findPage(page_id); if (page == nullptr) { return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面"); } const ProjectLimitSettings &limits = project_service_.projectLimits(); if (page->controls.size() >= limits.maximumHmiControlsPerPage) { return failure( HmiEditorError::InvalidControl, "当前配置下单个 HMI 页面最多允许 " + std::to_string(limits.maximumHmiControlsPerPage) + " 个控件"); } if (totalControlCount(project_service_.project()) >= ProjectLimits::kMaximumHmiControlsPerProject) { return failure(HmiEditorError::InvalidControl, "单个工程最多包含 2048 个 HMI 控件"); } const HmiControlDescriptor *descriptor = findHmiControlDescriptor(type); if (descriptor == nullptr) { 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( project.hmiPages.begin(), project.hmiPages.end(), [&page_id](const HmiPage &candidate) { return candidate.id == page_id; }); target_page->controls.push_back(std::move(control)); recordHistory(std::move(before)); return {true, HmiEditorError::None, {}, target_page->controls.back().id}; } // 通过批量删除入口删除单个控件 HmiEditorResult HmiEditorService::removeControl( const std::string &page_id, const std::string &control_id) { return removeControls(page_id, {control_id}); } // 完整校验所选控件后一次性删除,避免留下部分修改 HmiEditorResult HmiEditorService::removeControls( const std::string &page_id, const std::vector &control_ids) { if (findPage(page_id) == nullptr) { return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面"); } if (control_ids.empty()) { return failure(HmiEditorError::InvalidOperation, "请先选择要删除的 HMI 控件"); } std::set 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 控件"); } } HistoryState before = captureState(); Project &project = project_service_.editProject(); auto page = std::find_if( project.hmiPages.begin(), project.hmiPages.end(), [&page_id](const HmiPage &candidate) { return candidate.id == page_id; }); page->controls.erase( std::remove_if( page->controls.begin(), page->controls.end(), [&selected_ids](const HmiControl &candidate) { return selected_ids.find(candidate.id) != selected_ids.end(); }), page->controls.end()); recordHistory(std::move(before)); return {true, HmiEditorError::None, {}, control_ids.front()}; } // 校验候选矩形后移动或缩放指定控件 HmiEditorResult HmiEditorService::moveControl( const std::string &page_id, const std::string &control_id, const HmiRect &bounds) { const HmiPage *page = findPage(page_id); const HmiControl *control = findControl(page_id, control_id); if (page == nullptr) { return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面"); } if (control == nullptr) { return failure(HmiEditorError::ControlNotFound, "未找到 HMI 控件"); } // 先校验候选位置,失败时不修改工程模型 HmiControl candidate = *control; candidate.bounds = bounds; // 更新前保留原控件,只有全部编辑规则通过才覆盖原值 // 属性更新是整体替换:类型不变,但绑定、外观和扩展属性必须一起通过校验 std::string error; if (!validateEditableControl(*page, candidate, &error)) { return failure(HmiEditorError::InvalidControl, error); } if (control->bounds.x == bounds.x && control->bounds.y == bounds.y && control->bounds.width == bounds.width && control->bounds.height == bounds.height) { return {true, HmiEditorError::None, {}, control_id}; } HistoryState before = captureState(); Project &project = project_service_.editProject(); auto target = std::find_if( project.hmiPages.begin(), project.hmiPages.end(), [&page_id](const HmiPage &item) { return item.id == page_id; }); auto editable = std::find_if( target->controls.begin(), target->controls.end(), [&control_id](const HmiControl &item) { return item.id == control_id; }); editable->bounds = bounds; recordHistory(std::move(before)); return {true, HmiEditorError::None, {}, control_id}; } // 按所选控件的整体范围批量计算并应用对齐位置 HmiEditorResult HmiEditorService::alignControls( const std::string &page_id, const std::vector &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 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, const std::string &control_id, const HmiControl &control) { const HmiPage *page = findPage(page_id); const HmiControl *existing = findControl(page_id, control_id); if (page == nullptr) { return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面"); } if (existing == nullptr) { return failure(HmiEditorError::ControlNotFound, "未找到 HMI 控件"); } if (control.type != existing->type) { return failure(HmiEditorError::InvalidControl, "不能修改已创建 HMI 控件的类型"); } std::string error; if (!validateEditableControl(*page, control, &error)) { return failure(HmiEditorError::InvalidControl, error); } if (hasDuplicateControlId(*page, control_id, control.id)) { return failure(HmiEditorError::DuplicateId, "同一 HMI 页面内的控件 ID 必须唯一"); } if (controlsEqual(*existing, control)) { return {true, HmiEditorError::None, {}, control.id}; } HistoryState before = captureState(); Project &project = project_service_.editProject(); auto target = std::find_if( project.hmiPages.begin(), project.hmiPages.end(), [&page_id](const HmiPage &item) { return item.id == page_id; }); auto editable = std::find_if( target->controls.begin(), target->controls.end(), [&control_id](const HmiControl &item) { return item.id == control_id; }); *editable = control; recordHistory(std::move(before)); return {true, HmiEditorError::None, {}, control.id}; } // 保留相对布局批量粘贴控件,任一控件失败时整体回滚 HmiEditorResult HmiEditorService::pasteControls( const std::string &page_id, const std::vector &controls, int offset_x, int offset_y) { const HmiPage *existing_page = findPage(page_id); if (existing_page == nullptr) { return failure(HmiEditorError::PageNotFound, "未找到 HMI 页面"); } if (controls.empty()) { return failure(HmiEditorError::InvalidOperation, "请先复制 HMI 控件"); } const ProjectLimitSettings &limits = project_service_.projectLimits(); if (controls.size() > limits.maximumHmiControlsPerPage || existing_page->controls.size() + controls.size() > limits.maximumHmiControlsPerPage) { return failure( HmiEditorError::InvalidControl, "当前配置下单个 HMI 页面最多允许 " + std::to_string(limits.maximumHmiControlsPerPage) + " 个控件"); } if (totalControlCount(project_service_.project()) + controls.size() > ProjectLimits::kMaximumHmiControlsPerProject) { return failure(HmiEditorError::InvalidControl, "单个工程最多包含 2048 个 HMI 控件"); } // 让整组控件尽量保留相对位置,靠近页面边界时自动向页面内收拢 int minimum_x = controls.front().bounds.x; int minimum_y = controls.front().bounds.y; int maximum_x = controls.front().bounds.x + controls.front().bounds.width; int maximum_y = controls.front().bounds.y + controls.front().bounds.height; for (const HmiControl &control : controls) { minimum_x = std::min(minimum_x, control.bounds.x); minimum_y = std::min(minimum_y, control.bounds.y); maximum_x = std::max( maximum_x, control.bounds.x + control.bounds.width); maximum_y = std::max( maximum_y, control.bounds.y + control.bounds.height); } if (maximum_x - minimum_x > existing_page->width || maximum_y - minimum_y > existing_page->height) { return failure(HmiEditorError::InvalidControl, "复制的控件尺寸超出目标 HMI 页面"); } int final_offset_x = offset_x; int final_offset_y = offset_y; if (minimum_x + final_offset_x < 0) { final_offset_x = -minimum_x; } if (minimum_y + final_offset_y < 0) { final_offset_y = -minimum_y; } if (maximum_x + final_offset_x > existing_page->width) { final_offset_x = existing_page->width - maximum_x; } if (maximum_y + final_offset_y > existing_page->height) { final_offset_y = existing_page->height - maximum_y; } const HistoryState before = captureState(); const bool modified_before = project_service_.isModified(); Project &project = project_service_.editProject(); auto target_page = std::find_if( project.hmiPages.begin(), project.hmiPages.end(), [&page_id](const HmiPage &candidate) { return candidate.id == page_id; }); std::vector new_ids; new_ids.reserve(controls.size()); for (const HmiControl &source : controls) { const HmiControlDescriptor *descriptor = findHmiControlDescriptor(source.type); if (descriptor == nullptr) { project.hmiPages = before.pages; project.initialHmiPageId = before.initial_page_id; project_service_.restoreModifiedState(modified_before); return failure(HmiEditorError::InvalidControl, "不支持的 HMI 控件类型"); } HmiControl candidate = source; candidate.id = makeUniqueId(*target_page, descriptor->idPrefix); candidate.bounds.x += final_offset_x; candidate.bounds.y += final_offset_y; std::string error; if (!validateEditableControl(*target_page, candidate, &error)) { project.hmiPages = before.pages; project.initialHmiPageId = before.initial_page_id; project_service_.restoreModifiedState(modified_before); return failure(HmiEditorError::InvalidControl, error); } new_ids.push_back(candidate.id); target_page->controls.push_back(std::move(candidate)); } recordHistory(before); return {true, HmiEditorError::None, {}, new_ids.front()}; } // 判断当前 HMI 编辑会话是否有可撤销操作 bool HmiEditorService::canUndo() const { return history_.canUndo(); } // 判断当前 HMI 编辑会话是否有可重做操作 bool HmiEditorService::canRedo() const { return history_.canRedo(); } // 恢复最近一份编辑前快照 HmiEditorResult HmiEditorService::undo() { const std::optional target = history_.undo(captureState()); if (!target.has_value()) { return historyFailure("没有可撤销的 HMI 编辑操作"); } Project &project = project_service_.editProject(); project.hmiPages = target->pages; project.initialHmiPageId = target->initial_page_id; return {true, HmiEditorError::None, {}, {}}; } // 重新应用最近一份被撤销的快照 HmiEditorResult HmiEditorService::redo() { const std::optional target = history_.redo(captureState()); if (!target.has_value()) { return historyFailure("没有可重做的 HMI 编辑操作"); } Project &project = project_service_.editProject(); project.hmiPages = target->pages; project.initialHmiPageId = target->initial_page_id; return {true, HmiEditorError::None, {}, {}}; } // 清空当前 HMI 编辑会话的撤销和重做历史 void HmiEditorService::clearHistory() { history_.clear(); } // 校验控件自身配置、页面边界和页面跳转目标 bool HmiEditorService::validateEditableControl( const HmiPage &page, const HmiControl &control, std::string *error) const { if (!control.validate(error)) { return false; } 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) { setError(error, "HMI 控件不能超出页面范围"); return false; } if (control.type == HmiControlType::PageJump && !control.pageJump->targetPageId.empty() && findPage(control.pageJump->targetPageId) == nullptr) { setError(error, "页面跳转控件的目标页面不存在"); return false; } return true; } // 检查候选控件 ID 是否与页面内其他控件重复 bool HmiEditorService::hasDuplicateControlId( const HmiPage &page, const std::string &excluded_id, const std::string &candidate_id) { return std::any_of( page.controls.cbegin(), page.controls.cend(), [&excluded_id, &candidate_id](const HmiControl &item) { // 排除正在编辑的原控件,只检查同页的其他控件 return item.id != excluded_id && item.id == candidate_id; }); } // 根据控件类型生成默认样式、文字、大小、初始坐标 HmiControl HmiEditorService::makeControl( const HmiPage &page, const HmiControlDescriptor &descriptor) { HmiControl control; 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{}; } if (descriptor.type == HmiControlType::StatusText) { control.statusText = HmiStatusBitTextConfig{ "设备停止", "设备运行"}; } const int offset = static_cast(page.controls.size()) * 16; control.bounds.x = std::min(20 + offset, page.width - control.bounds.width); control.bounds.y = std::min(20 + offset, page.height - control.bounds.height); return control; } // 生成由类型前缀和递增数字组成的页面内唯一控件 ID std::string HmiEditorService::makeUniqueId( const HmiPage &page, const std::string &prefix) { // 同类控件从 1 递增命名,保证页面内标识稳定且唯一 int suffix = 1; while (true) { const std::string candidate = prefix + '-' + std::to_string(suffix); const bool found = std::any_of( page.controls.cbegin(), page.controls.cend(), [&candidate](const HmiControl &item) { return item.id == candidate; }); if (!found) { return candidate; } ++suffix; } }