From 187a4992847e918c0ab08811856659c1010ab46b Mon Sep 17 00:00:00 2001 From: suyu <1643689728@qq.com> Date: Fri, 21 Aug 2026 14:05:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81HMI=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=B0=BA=E5=AF=B8=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/domain/hmi_model.h | 5 +- app/src/domain/project_limits.h | 8 +- .../infrastructure/json_project_storage.cpp | 6 +- app/src/services/hmi_editor_service.cpp | 34 ++++++++ app/src/services/hmi_editor_service.h | 2 + app/src/ui/main_window.cpp | 1 + app/src/ui/main_window.ui | 50 ++++++++++- app/src/ui/property_panel_controller.cpp | 65 +++++++++++++++ app/src/ui/property_panel_controller.h | 2 + app/tests/domain_tests.cpp | 13 ++- app/tests/hmi_editor_service_tests.cpp | 38 +++++++++ app/tests/main_window_tests.cpp | 15 ++++ app/tests/project_management_tests.cpp | 83 +++++++++++++++++++ 13 files changed, 311 insertions(+), 11 deletions(-) diff --git a/app/src/domain/hmi_model.h b/app/src/domain/hmi_model.h index 0469504..2bb91dc 100644 --- a/app/src/domain/hmi_model.h +++ b/app/src/domain/hmi_model.h @@ -8,6 +8,7 @@ #pragma once #include "register_address.h" +#include "project_limits.h" #include #include @@ -125,8 +126,8 @@ struct HmiPage { std::string id; std::string name; - int width = 800; - int height = 480; + int width = ProjectLimits::kDefaultHmiPageWidth; + int height = ProjectLimits::kDefaultHmiPageHeight; std::vector controls; /** diff --git a/app/src/domain/project_limits.h b/app/src/domain/project_limits.h index b9f4d15..69b0d5a 100644 --- a/app/src/domain/project_limits.h +++ b/app/src/domain/project_limits.h @@ -33,8 +33,12 @@ constexpr std::size_t kMaximumPropertyValueBytes = 4096U; constexpr std::size_t kMaximumRegisterCommentBytes = 64U; constexpr std::size_t kMaximumRungCommentBytes = 128U; -constexpr int kMaximumHmiPageWidth = 8192; -constexpr int kMaximumHmiPageHeight = 8192; +constexpr int kMinimumHmiPageWidth = 320; +constexpr int kMaximumHmiPageWidth = 1600; +constexpr int kMinimumHmiPageHeight = 200; +constexpr int kMaximumHmiPageHeight = 800; +constexpr int kDefaultHmiPageWidth = 800; +constexpr int kDefaultHmiPageHeight = 400; constexpr int kMaximumHmiControlWidth = 8192; constexpr int kMaximumHmiControlHeight = 8192; constexpr int kMinimumHmiFontPointSize = 6; diff --git a/app/src/infrastructure/json_project_storage.cpp b/app/src/infrastructure/json_project_storage.cpp index f2f8068..e7bc398 100644 --- a/app/src/infrastructure/json_project_storage.cpp +++ b/app/src/infrastructure/json_project_storage.cpp @@ -876,10 +876,12 @@ bool parseHmiPage( object, "id", context, &page->id, state, ProjectLimits::kMaximumIdBytes) || !readString(object, "name", context, &page->name, state) - || !readInt(object, "width", context, 1, + || !readInt(object, "width", context, + ProjectLimits::kMinimumHmiPageWidth, ProjectLimits::kMaximumHmiPageWidth, &page->width, state) - || !readInt(object, "height", context, 1, + || !readInt(object, "height", context, + ProjectLimits::kMinimumHmiPageHeight, ProjectLimits::kMaximumHmiPageHeight, &page->height, state) || !readArray( diff --git a/app/src/services/hmi_editor_service.cpp b/app/src/services/hmi_editor_service.cpp index 9a5a58c..00c3e3f 100644 --- a/app/src/services/hmi_editor_service.cpp +++ b/app/src/services/hmi_editor_service.cpp @@ -241,6 +241,40 @@ HmiEditorResult HmiEditorService::addPage(const std::string &name) 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(&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) { diff --git a/app/src/services/hmi_editor_service.h b/app/src/services/hmi_editor_service.h index de3b3f6..4e16049 100644 --- a/app/src/services/hmi_editor_service.h +++ b/app/src/services/hmi_editor_service.h @@ -83,6 +83,8 @@ public: */ HmiEditorResult ensureDefaultPage(); HmiEditorResult addPage(const std::string &name); + HmiEditorResult resizePage( + const std::string &page_id, int width, int height); HmiEditorResult renamePage( const std::string &page_id, const std::string &name); HmiEditorResult removePage(const std::string &page_id); diff --git a/app/src/ui/main_window.cpp b/app/src/ui/main_window.cpp index 8ad1460..a920393 100644 --- a/app/src/ui/main_window.cpp +++ b/app/src/ui/main_window.cpp @@ -293,6 +293,7 @@ void MainWindow::initializeUi() clearEditorHistories(); current_hmi_page_id_ = hmi_editor_service_.firstPageId(); current_logic_id_ = logic_editor_service_.firstLogicId(); + showControlProperties({}); refreshProjectUi(); updateModeUi(tr("系统已进入编辑态")); } diff --git a/app/src/ui/main_window.ui b/app/src/ui/main_window.ui index 14b89ad..838776b 100644 --- a/app/src/ui/main_window.ui +++ b/app/src/ui/main_window.ui @@ -132,7 +132,7 @@ - 800 x 480 + 800 x 400 Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter @@ -452,6 +452,54 @@ + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + 页面宽度 + + + + + + + 320 + + + 1600 + + + + + + + 页面高度 + + + + + + + 200 + + + 800 + + + + + + + 应用页面尺寸 + + + + + diff --git a/app/src/ui/property_panel_controller.cpp b/app/src/ui/property_panel_controller.cpp index 18f005b..4fa1fac 100644 --- a/app/src/ui/property_panel_controller.cpp +++ b/app/src/ui/property_panel_controller.cpp @@ -86,6 +86,12 @@ PropertyPanelController::PropertyPanelController( void PropertyPanelController::configure() { + ui_.pageWidthSpinBox->setRange( + ProjectLimits::kMinimumHmiPageWidth, + ProjectLimits::kMaximumHmiPageWidth); + ui_.pageHeightSpinBox->setRange( + ProjectLimits::kMinimumHmiPageHeight, + ProjectLimits::kMaximumHmiPageHeight); ui_.fontSizeSpinBox->setRange( ProjectLimits::kMinimumHmiFontPointSize, ProjectLimits::kMaximumHmiFontPointSize); @@ -106,6 +112,8 @@ void PropertyPanelController::configure() } QObject::connect(ui_.applyPropertiesButton, &QPushButton::clicked, &parent_, [this] { applySelectedControlProperties(); }); + QObject::connect(ui_.applyPagePropertiesButton, &QPushButton::clicked, + &parent_, [this] { applySelectedPageProperties(); }); QObject::connect(ui_.applyLogicPropertiesButton, &QPushButton::clicked, &parent_, [this] { applySelectedLogicNodeProperties(); }); QObject::connect(ui_.textColorButton, &QPushButton::clicked, @@ -156,6 +164,11 @@ void PropertyPanelController::bindEditorWidgets( void PropertyPanelController::showControlProperties(const std::string &control_id) { + if (control_id.empty()) + { + showPageProperties(current_page_id_()); + return; + } ui_.propertyStack->setCurrentWidget(ui_.hmiPropertiesPage); selected_control_id_ = control_id; const HmiControl *control = hmi_editor_service_.findControl( @@ -324,6 +337,33 @@ void PropertyPanelController::showControlProperties(const std::string &control_i && font_italic->second == "true"); } +void PropertyPanelController::showPageProperties(const std::string &page_id) +{ + ui_.propertyStack->setCurrentWidget(ui_.pagePropertiesPage); + selected_control_id_.clear(); + const HmiPage *page = hmi_editor_service_.findPage(page_id); + const bool has_page = page != nullptr; + ui_.pageWidthSpinBox->setRange( + ProjectLimits::kMinimumHmiPageWidth, + ProjectLimits::kMaximumHmiPageWidth); + ui_.pageHeightSpinBox->setRange( + ProjectLimits::kMinimumHmiPageHeight, + ProjectLimits::kMaximumHmiPageHeight); + ui_.selectionValueLabel->setText( + has_page ? fromUtf8(page->name) : QObject::tr("未选择")); + ui_.pageWidthSpinBox->setEnabled(has_page); + ui_.pageHeightSpinBox->setEnabled(has_page); + ui_.applyPagePropertiesButton->setEnabled(has_page); + if (!has_page) + { + ui_.pageWidthSpinBox->setValue(ProjectLimits::kDefaultHmiPageWidth); + ui_.pageHeightSpinBox->setValue(ProjectLimits::kDefaultHmiPageHeight); + return; + } + ui_.pageWidthSpinBox->setValue(page->width); + ui_.pageHeightSpinBox->setValue(page->height); +} + void PropertyPanelController::showLogicNodeProperties(const std::string &node_id) { ui_.propertyStack->setCurrentWidget(ui_.logicPropertiesPage); @@ -621,6 +661,31 @@ void PropertyPanelController::applySelectedControlProperties() status_reporter_(QObject::tr("控件属性已更新"), 3000); } +void PropertyPanelController::applySelectedPageProperties() +{ + const std::string page_id = current_page_id_(); + if (hmi_editor_service_.findPage(page_id) == nullptr) + { + return; + } + const HmiEditorResult result = hmi_editor_service_.resizePage( + page_id, + ui_.pageWidthSpinBox->value(), + ui_.pageHeightSpinBox->value()); + if (!result.succeeded) + { + reportFailure(QObject::tr("应用页面尺寸"), result.message); + return; + } + if (hmi_editor_widget_ != nullptr) + { + hmi_editor_widget_->reloadPage(); + } + showPageProperties(page_id); + refresh_project_ui_(); + status_reporter_(QObject::tr("页面尺寸已更新"), 3000); +} + void PropertyPanelController::chooseTextColor() { QColor initial_color(ui_.textColorEdit->text()); diff --git a/app/src/ui/property_panel_controller.h b/app/src/ui/property_panel_controller.h index 24abb2e..4328c21 100644 --- a/app/src/ui/property_panel_controller.h +++ b/app/src/ui/property_panel_controller.h @@ -48,10 +48,12 @@ public: LogicEditorWidget &logic_editor_widget); void showControlProperties(const std::string &control_id); + void showPageProperties(const std::string &page_id); void showLogicNodeProperties(const std::string &node_id); void addHmiControl(HmiControlType type); void deleteSelectedControl(); void applySelectedControlProperties(); + void applySelectedPageProperties(); void applySelectedLogicNodeProperties(); private: diff --git a/app/tests/domain_tests.cpp b/app/tests/domain_tests.cpp index e665a65..67d13e7 100644 --- a/app/tests/domain_tests.cpp +++ b/app/tests/domain_tests.cpp @@ -352,11 +352,11 @@ void testQuantityBoundaries() "page-" + std::to_string(index), "Page " + std::to_string(index), 800, - 480, + 400, {}}); } require(project.validate(), "an HMI page count of 128 must be accepted"); - project.hmiPages.push_back({"page-over", "Page over", 800, 480, {}}); + project.hmiPages.push_back({"page-over", "Page over", 800, 400, {}}); require(!project.validate(), "an HMI page count of 129 must be rejected"); project = makeValidProject(); @@ -384,9 +384,14 @@ void testQuantityBoundaries() project = makeValidProject(); project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth; project.hmiPages.front().height = ProjectLimits::kMaximumHmiPageHeight; - require(project.validate(), "an HMI page size of 8192 by 8192 must be accepted"); + require(project.validate(), "an HMI page size of 1600 by 800 must be accepted"); project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth + 1; - require(!project.validate(), "an HMI page width of 8193 must be rejected"); + require(!project.validate(), "an HMI page width of 1601 must be rejected"); + project.hmiPages.front().width = ProjectLimits::kMinimumHmiPageWidth - 1; + require(!project.validate(), "an HMI page width of 319 must be rejected"); + project.hmiPages.front().width = ProjectLimits::kMaximumHmiPageWidth; + project.hmiPages.front().height = ProjectLimits::kMinimumHmiPageHeight - 1; + require(!project.validate(), "an HMI page height of 199 must be rejected"); ConditionExpression leaf = ConditionExpression::fromNode({ "depth-node-0", diff --git a/app/tests/hmi_editor_service_tests.cpp b/app/tests/hmi_editor_service_tests.cpp index cf54a30..c19fcfb 100644 --- a/app/tests/hmi_editor_service_tests.cpp +++ b/app/tests/hmi_editor_service_tests.cpp @@ -329,6 +329,43 @@ void testPageLifecycleAndNavigation() "stopping runtime navigation must clear session state"); } +void testPageResizeIsAtomicAndUndoable() +{ + TestProjectStorage storage; + ProjectService project_service(storage); + HmiEditorService service(project_service); + const std::string page_id = service.ensureDefaultPage().id; + const HmiEditorResult label = service.addControl(page_id, HmiControlType::Label); + require(label.succeeded, "page resize fixture must add a control"); + + HmiControl control = *service.findControl(page_id, label.id); + control.bounds = {700, 340, 80, 32}; + require(service.updateControl(page_id, label.id, control).succeeded, + "page resize fixture must place the control near the page edge"); + require(service.resizePage(page_id, 1024, 600).succeeded, + "an HMI page must be resizable to a larger valid rectangle"); + require(service.findPage(page_id)->width == 1024 + && service.findPage(page_id)->height == 600, + "page resize must update both dimensions"); + require(service.resizePage(page_id, 700, 300).error + == HmiEditorError::InvalidPage, + "page resize must reject dimensions that clip an existing control"); + require(service.findPage(page_id)->width == 1024 + && service.findPage(page_id)->height == 600, + "a rejected page resize must leave the original dimensions intact"); + require(service.undo().succeeded + && service.findPage(page_id)->width == 800 + && service.findPage(page_id)->height == 400, + "page resize must participate in HMI undo history"); + require(service.redo().succeeded + && service.findPage(page_id)->width == 1024 + && service.findPage(page_id)->height == 600, + "page resize redo must restore the new dimensions"); + require(service.resizePage(page_id, 319, 600).error + == HmiEditorError::InvalidPage, + "page width below the business minimum must be rejected"); +} + } // namespace int main() @@ -341,6 +378,7 @@ int main() testAppearanceEditing(); testRuntimeUsesRegisterRepository(); testPageLifecycleAndNavigation(); + testPageResizeIsAtomicAndUndoable(); } catch (const std::exception &error) { diff --git a/app/tests/main_window_tests.cpp b/app/tests/main_window_tests.cpp index f921e5a..41f12ab 100644 --- a/app/tests/main_window_tests.cpp +++ b/app/tests/main_window_tests.cpp @@ -2174,6 +2174,21 @@ void testMultiPageAndLogicMainWindowIntegration() QApplication::processEvents(); require(window.currentHmiPageId() == settings_page_id, "selecting a page tree node must change the current HMI page id"); + QSpinBox *page_width = requiredChild(window, "pageWidthSpinBox"); + QSpinBox *page_height = requiredChild(window, "pageHeightSpinBox"); + QPushButton *apply_page_properties = requiredChild( + window, "applyPagePropertiesButton"); + require(page_width->isEnabled() && page_height->isEnabled() + && page_width->value() == 800 && page_height->value() == 400, + "selecting an HMI page must expose its page dimensions"); + page_width->setValue(1024); + page_height->setValue(600); + apply_page_properties->click(); + require(editor_service.findPage(settings_page_id)->width == 1024 + && editor_service.findPage(settings_page_id)->height == 600 + && requiredChild(window, "hmiPageSizeLabel")->text() + == QStringLiteral("1024 x 600"), + "applying page properties must update the model and page summary"); QAction *add_label = requiredChild(window, "addLabelAction"); add_label->trigger(); require(editor_service.findPage(settings_page_id)->controls.size() == 1U diff --git a/app/tests/project_management_tests.cpp b/app/tests/project_management_tests.cpp index e74833f..865391c 100644 --- a/app/tests/project_management_tests.cpp +++ b/app/tests/project_management_tests.cpp @@ -866,6 +866,88 @@ void testQuantityFileLimits() "a condition expression deeper than 20 levels must be rejected while parsing"); } +void testHmiPageJsonBoundaries() +{ + // JSON 入口必须和页面领域校验使用同一组业务边界 + QTemporaryDir directory; + require(directory.isValid(), "temporary directory must be valid"); + + JsonProjectStorage storage; + ProjectService service(storage); + Project boundary_project = makeExampleProject(); + boundary_project.hmiPages.front().controls.clear(); + service.editProject() = std::move(boundary_project); + + const QString fixture_path = directory.filePath("hmi-page-boundary-fixture.json"); + require(service.saveAs(fixture_path.toStdString()).succeeded, + "the HMI boundary fixture must be saved"); + + const QJsonObject original_root = + QJsonDocument::fromJson(readBytes(fixture_path)).object(); + + const auto loadWithSize = [&](int width, int height, const QString &name) + { + QJsonObject root = original_root; + QJsonArray pages = root.value(QStringLiteral("hmiPages")).toArray(); + QJsonObject page = pages.at(0).toObject(); + page.insert(QStringLiteral("width"), width); + page.insert(QStringLiteral("height"), height); + pages.replace(0, page); + root.insert(QStringLiteral("hmiPages"), pages); + + const QString path = directory.filePath(name); + writeText(path, QJsonDocument(root).toJson(QJsonDocument::Compact)); + return storage.load(path.toStdString()); + }; + + require(!loadWithSize( + ProjectLimits::kMinimumHmiPageWidth - 1, + ProjectLimits::kDefaultHmiPageHeight, + QStringLiteral("width-below-minimum.json")) + .succeeded, + "JSON must reject an HMI page width below 320"); + require(!loadWithSize( + ProjectLimits::kMaximumHmiPageWidth + 1, + ProjectLimits::kDefaultHmiPageHeight, + QStringLiteral("width-above-maximum.json")) + .succeeded, + "JSON must reject an HMI page width above 1600"); + require(!loadWithSize( + ProjectLimits::kDefaultHmiPageWidth, + ProjectLimits::kMinimumHmiPageHeight - 1, + QStringLiteral("height-below-minimum.json")) + .succeeded, + "JSON must reject an HMI page height below 200"); + require(!loadWithSize( + ProjectLimits::kDefaultHmiPageWidth, + ProjectLimits::kMaximumHmiPageHeight + 1, + QStringLiteral("height-above-maximum.json")) + .succeeded, + "JSON must reject an HMI page height above 800"); + + const ProjectLoadResult minimum_result = loadWithSize( + ProjectLimits::kMinimumHmiPageWidth, + ProjectLimits::kMinimumHmiPageHeight, + QStringLiteral("minimum-size.json")); + require(minimum_result.succeeded + && minimum_result.project.hmiPages.front().width + == ProjectLimits::kMinimumHmiPageWidth + && minimum_result.project.hmiPages.front().height + == ProjectLimits::kMinimumHmiPageHeight, + "JSON must accept the minimum HMI page size 320x200"); + + const ProjectLoadResult maximum_result = loadWithSize( + ProjectLimits::kMaximumHmiPageWidth, + ProjectLimits::kMaximumHmiPageHeight, + QStringLiteral("maximum-size.json")); + require(maximum_result.succeeded + && maximum_result.project.hmiPages.front().width + == ProjectLimits::kMaximumHmiPageWidth + && maximum_result.project.hmiPages.front().height + == ProjectLimits::kMaximumHmiPageHeight, + "JSON must accept the maximum HMI page size 1600x800"); +} + void testServiceStateAndSaveErrors() { // 保存路径和修改标记只在成功持久化后更新 @@ -954,6 +1036,7 @@ int main() testRegisterCommentService(); testInvalidFiles(); testQuantityFileLimits(); + testHmiPageJsonBoundaries(); testServiceStateAndSaveErrors(); } catch (const std::exception &error)