Sfoglia il codice sorgente

feat: 支持HMI页面尺寸设置

main
suyu 4 settimane fa
parent
commit
187a499284
13 ha cambiato i file con 311 aggiunte e 11 eliminazioni
  1. +3
    -2
      app/src/domain/hmi_model.h
  2. +6
    -2
      app/src/domain/project_limits.h
  3. +4
    -2
      app/src/infrastructure/json_project_storage.cpp
  4. +34
    -0
      app/src/services/hmi_editor_service.cpp
  5. +2
    -0
      app/src/services/hmi_editor_service.h
  6. +1
    -0
      app/src/ui/main_window.cpp
  7. +49
    -1
      app/src/ui/main_window.ui
  8. +65
    -0
      app/src/ui/property_panel_controller.cpp
  9. +2
    -0
      app/src/ui/property_panel_controller.h
  10. +9
    -4
      app/tests/domain_tests.cpp
  11. +38
    -0
      app/tests/hmi_editor_service_tests.cpp
  12. +15
    -0
      app/tests/main_window_tests.cpp
  13. +83
    -0
      app/tests/project_management_tests.cpp

+ 3
- 2
app/src/domain/hmi_model.h Vedi File

@@ -8,6 +8,7 @@
#pragma once

#include "register_address.h"
#include "project_limits.h"

#include <cstdint>
#include <map>
@@ -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<HmiControl> controls;

/**


+ 6
- 2
app/src/domain/project_limits.h Vedi File

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


+ 4
- 2
app/src/infrastructure/json_project_storage.cpp Vedi File

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


+ 34
- 0
app/src/services/hmi_editor_service.cpp Vedi File

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


+ 2
- 0
app/src/services/hmi_editor_service.h Vedi File

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


+ 1
- 0
app/src/ui/main_window.cpp Vedi File

@@ -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("系统已进入编辑态"));
}


+ 49
- 1
app/src/ui/main_window.ui Vedi File

@@ -132,7 +132,7 @@
<item>
<widget class="QLabel" name="hmiPageSizeLabel">
<property name="text">
<string>800 x 480</string>
<string>800 x 400</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
@@ -452,6 +452,54 @@
</item>
<item>
<widget class="QStackedWidget" name="propertyStack">
<widget class="QWidget" name="pagePropertiesPage">
<layout class="QFormLayout" name="pagePropertiesForm">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="pageWidthLabel">
<property name="text">
<string>页面宽度</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="pageWidthSpinBox">
<property name="minimum">
<number>320</number>
</property>
<property name="maximum">
<number>1600</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="pageHeightLabel">
<property name="text">
<string>页面高度</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="pageHeightSpinBox">
<property name="minimum">
<number>200</number>
</property>
<property name="maximum">
<number>800</number>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QPushButton" name="applyPagePropertiesButton">
<property name="text">
<string>应用页面尺寸</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="hmiPropertiesPage">
<layout class="QFormLayout" name="hmiPropertiesForm">
<property name="fieldGrowthPolicy">


+ 65
- 0
app/src/ui/property_panel_controller.cpp Vedi File

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


+ 2
- 0
app/src/ui/property_panel_controller.h Vedi File

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


+ 9
- 4
app/tests/domain_tests.cpp Vedi File

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


+ 38
- 0
app/tests/hmi_editor_service_tests.cpp Vedi File

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


+ 15
- 0
app/tests/main_window_tests.cpp Vedi File

@@ -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<QSpinBox>(window, "pageWidthSpinBox");
QSpinBox *page_height = requiredChild<QSpinBox>(window, "pageHeightSpinBox");
QPushButton *apply_page_properties = requiredChild<QPushButton>(
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<QLabel>(window, "hmiPageSizeLabel")->text()
== QStringLiteral("1024 x 600"),
"applying page properties must update the model and page summary");
QAction *add_label = requiredChild<QAction>(window, "addLabelAction");
add_label->trigger();
require(editor_service.findPage(settings_page_id)->controls.size() == 1U


+ 83
- 0
app/tests/project_management_tests.cpp Vedi File

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


Caricamento…
Annulla
Salva