Sfoglia il codice sorgente

feat: 完善 HMI 编辑器交互与运行模式控制

main
suyu 1 mese fa
parent
commit
074c32b3bb
23 ha cambiato i file con 259 aggiunte e 27 eliminazioni
  1. +3
    -1
      .gitignore
  2. +5
    -5
      app/src/domain/hmi_model.h
  3. +2
    -6
      app/src/domain/project_model.cpp
  4. +2
    -0
      app/src/domain/register_address.cpp
  5. +2
    -0
      app/src/domain/runtime_state.cpp
  6. +24
    -3
      app/src/domain/runtime_state.h
  7. +2
    -0
      app/src/main.cpp
  8. +14
    -1
      app/src/services/hmi_editor_service.cpp
  9. +29
    -0
      app/src/services/hmi_editor_service.h
  10. +4
    -0
      app/src/services/hmi_runtime_service.cpp
  11. +5
    -5
      app/src/services/hmi_runtime_service.h
  12. +7
    -1
      app/src/services/project_service.cpp
  13. +2
    -0
      app/src/services/runtime_mode_service.cpp
  14. +30
    -1
      app/src/services/runtime_mode_service.h
  15. +47
    -0
      app/src/ui/hmi_editor_widget.cpp
  16. +11
    -0
      app/src/ui/hmi_editor_widget.h
  17. +32
    -4
      app/src/ui/main_window.cpp
  18. +14
    -0
      app/src/ui/main_window.h
  19. +7
    -0
      app/tests/domain_tests.cpp
  20. +4
    -0
      app/tests/hmi_editor_service_tests.cpp
  21. +4
    -0
      app/tests/main_window_tests.cpp
  22. +7
    -0
      app/tests/project_management_tests.cpp
  23. +2
    -0
      app/tests/runtime_mode_service_tests.cpp

+ 3
- 1
.gitignore Vedi File

@@ -58,4 +58,6 @@ Desktop.ini
/scripts /scripts
/docs/images /docs/images
/docs/类图Mermaid代码.md /docs/类图Mermaid代码.md
/other
/other
设计方案书.doc
/学习

+ 5
- 5
app/src/domain/hmi_model.h Vedi File

@@ -37,11 +37,11 @@ struct HmiRect
*/ */
enum class HmiControlType enum class HmiControlType
{ {
Button,
Indicator,
NumericDisplay,
NumericInput,
Label
Button, // 按钮
Indicator, // 指示灯
NumericDisplay, // 数值显示
NumericInput, // 数值输入
Label // 标签
}; };


/** /**


+ 2
- 6
app/src/domain/project_model.cpp Vedi File

@@ -12,12 +12,7 @@ void setError(std::string *error, const std::string &message)
} }
} }


/**
* @brief 检查对象集合中是否存在重复 id
* @tparam TItem 包含 id 成员的对象类型
* @param items 需要检查的对象集合
* @return 存在重复 id 时返回 true 否则返回 false
*/
// 通过相邻区间查找避免为不同领域对象重复实现标识唯一性校验
template <typename TItem> template <typename TItem>
bool containsDuplicateId(const std::vector<TItem> &items) bool containsDuplicateId(const std::vector<TItem> &items)
{ {
@@ -59,6 +54,7 @@ bool Project::validate(std::string *error) const
} }
for (const HmiPage &page : hmiPages) for (const HmiPage &page : hmiPages)
{ {
// 工程聚合校验会向下委托页面和控件的完整规则
if (!page.validate(error)) if (!page.validate(error))
{ {
return false; return false;


+ 2
- 0
app/src/domain/register_address.cpp Vedi File

@@ -4,6 +4,7 @@ namespace {


bool isSupportedArea(RegisterArea area) bool isSupportedArea(RegisterArea area)
{ {
// 地址值对象只承认项目定义的 M 区和 D 区
return area == RegisterArea::M || area == RegisterArea::D; return area == RegisterArea::M || area == RegisterArea::D;
} }


@@ -43,6 +44,7 @@ std::string RegisterAddress::toString() const
areaName = "D"; areaName = "D";
break; break;
default: default:
// 未知枚举值不能伪装成有效 PLC 地址
return "InvalidRegisterAddress"; return "InvalidRegisterAddress";
} }




+ 2
- 0
app/src/domain/runtime_state.cpp Vedi File

@@ -12,6 +12,7 @@ ModePolicy RuntimeState::policy() const


ModeTransitionResult RuntimeState::enterEditing() ModeTransitionResult RuntimeState::enterEditing()
{ {
// 两种运行态都必须先回到编辑态,作为后续模式切换的唯一中转点
if (mode_ == ApplicationMode::Editing) if (mode_ == ApplicationMode::Editing)
{ {
return {false, ModeTransitionError::AlreadyInRequestedMode}; return {false, ModeTransitionError::AlreadyInRequestedMode};
@@ -46,6 +47,7 @@ ModeTransitionResult RuntimeState::enterOnlineRunning(bool initial_plc_read_comp
} }
if (!initial_plc_read_completed) if (!initial_plc_read_completed)
{ {
// 未读取 PLC 时禁止进入真机态,防止用未知缓存值驱动界面
return {false, ModeTransitionError::InitialPlcReadRequired}; return {false, ModeTransitionError::InitialPlcReadRequired};
} }
mode_ = ApplicationMode::OnlineRunning; mode_ = ApplicationMode::OnlineRunning;


+ 24
- 3
app/src/domain/runtime_state.h Vedi File

@@ -30,15 +30,36 @@ constexpr ModePolicy policyForMode(ApplicationMode mode)
{ {
case ApplicationMode::Editing: case ApplicationMode::Editing:
{ {
return {true, false, false, false, false};
// 编辑态只允许修改工程,不连接寄存器,也不执行软件逻辑
return {
true, // allowsProjectEditing 允许编辑工程
false, // usesVirtualRegisters 不使用虚拟寄存器
false, // usesPlcRegisters 不使用 PLC 寄存器
false, // runsLogicExecutor 不运行软件逻辑执行器
false // requiresInitialPlcRead 不要求 PLC 初次读取
};
} }
case ApplicationMode::OfflineRunning: case ApplicationMode::OfflineRunning:
{ {
return {false, true, false, true, false};
// 离线运行态禁止改工程,使用虚拟 M/D 并运行软件逻辑
return {
false, // allowsProjectEditing 禁止编辑工程
true, // usesVirtualRegisters 使用虚拟寄存器
false, // usesPlcRegisters 不使用 PLC 寄存器
true, // runsLogicExecutor 运行软件逻辑执行器
false // requiresInitialPlcRead 不要求 PLC 初次读取
};
} }
case ApplicationMode::OnlineRunning: case ApplicationMode::OnlineRunning:
{ {
return {false, false, true, false, true};
// 真机运行态禁止改工程,使用 PLC 数据且禁止软件逻辑重复控制 PLC
return {
false, // allowsProjectEditing 禁止编辑工程
false, // usesVirtualRegisters 不使用虚拟寄存器
true, // usesPlcRegisters 使用 PLC 寄存器
false, // runsLogicExecutor 不运行软件逻辑执行器
true // requiresInitialPlcRead 必须先完成 PLC 初次读取
};
} }
default: default:
{ {


+ 2
- 0
app/src/main.cpp Vedi File

@@ -21,9 +21,11 @@ int main(int argc, char *argv[])
application.setApplicationName(QObject::tr("综合平台编程器")); application.setApplicationName(QObject::tr("综合平台编程器"));
application.setOrganizationName(QStringLiteral("QtProXinJe")); application.setOrganizationName(QStringLiteral("QtProXinJe"));


// 组合根负责创建具体实现并将抽象依赖注入服务和 UI
JsonProjectStorage project_storage; JsonProjectStorage project_storage;
ProjectService project_service(project_storage); ProjectService project_service(project_storage);
HmiEditorService hmi_editor_service(project_service); HmiEditorService hmi_editor_service(project_service);
// 当前离线模式使用内存仓库,后续真机模式替换为 PLC 缓存实现
VirtualRegisterRepository virtual_register_repository; VirtualRegisterRepository virtual_register_repository;
HmiRuntimeService hmi_runtime_service(virtual_register_repository); HmiRuntimeService hmi_runtime_service(virtual_register_repository);
RuntimeModeService runtime_mode_service; RuntimeModeService runtime_mode_service;


+ 14
- 1
app/src/services/hmi_editor_service.cpp Vedi File

@@ -83,6 +83,7 @@ HmiEditorService::HmiEditorService(ProjectService &project_service)
{ {
} }


// 遍历工程所有 HmiPage,根据页面 ID 查找页面,找不到返回空指针
const HmiPage *HmiEditorService::findPage(const std::string &page_id) const const HmiPage *HmiEditorService::findPage(const std::string &page_id) const
{ {
const Project &project = project_service_.project(); const Project &project = project_service_.project();
@@ -96,6 +97,7 @@ const HmiPage *HmiEditorService::findPage(const std::string &page_id) const
return page == project.hmiPages.cend() ? nullptr : &*page; return page == project.hmiPages.cend() ? nullptr : &*page;
} }


// 先找到页面 → 在页面内查找指定控件;页面不存在 / 控件不存在都返回 nullptr
const HmiControl *HmiEditorService::findControl( const HmiControl *HmiEditorService::findControl(
const std::string &page_id, const std::string &control_id) const const std::string &page_id, const std::string &control_id) const
{ {
@@ -114,12 +116,14 @@ const HmiControl *HmiEditorService::findControl(
return control == page->controls.cend() ? nullptr : &*control; return control == page->controls.cend() ? nullptr : &*control;
} }


// 获取第一个页面 ID;没有页面返回空字符串
std::string HmiEditorService::firstPageId() const std::string HmiEditorService::firstPageId() const
{ {
const Project &project = project_service_.project(); const Project &project = project_service_.project();
return project.hmiPages.empty() ? std::string{} : project.hmiPages.front().id; return project.hmiPages.empty() ? std::string{} : project.hmiPages.front().id;
} }


// 保证工程至少存在一个 HMI 页面
HmiEditorResult HmiEditorService::ensureDefaultPage() HmiEditorResult HmiEditorService::ensureDefaultPage()
{ {
if (!project_service_.project().hmiPages.empty()) if (!project_service_.project().hmiPages.empty())
@@ -127,6 +131,7 @@ HmiEditorResult HmiEditorService::ensureDefaultPage()
return {true, HmiEditorError::None, {}, firstPageId()}; return {true, HmiEditorError::None, {}, firstPageId()};
} }


// 仅在首个控件操作前创建默认页面,空工程仍可正常保存
HmiPage page; HmiPage page;
page.id = "page-1"; page.id = "page-1";
page.name = "主操作页面"; page.name = "主操作页面";
@@ -145,6 +150,7 @@ HmiEditorResult HmiEditorService::addControl(
return failure(HmiEditorError::PageNotFound, "HMI page was not found"); return failure(HmiEditorError::PageNotFound, "HMI page was not found");
} }


// 新控件初始不绑定寄存器,避免自动分配地址造成误写风险
HmiControl control = makeControl(*page, type); HmiControl control = makeControl(*page, type);
Project &project = project_service_.editProject(); Project &project = project_service_.editProject();
auto target_page = std::find_if( auto target_page = std::find_if(
@@ -193,6 +199,7 @@ HmiEditorResult HmiEditorService::removeControl(
return {true, HmiEditorError::None, {}, control_id}; return {true, HmiEditorError::None, {}, control_id};
} }


// 移动 / 缩放控件
HmiEditorResult HmiEditorService::moveControl( HmiEditorResult HmiEditorService::moveControl(
const std::string &page_id, const std::string &page_id,
const std::string &control_id, const std::string &control_id,
@@ -209,8 +216,10 @@ HmiEditorResult HmiEditorService::moveControl(
return failure(HmiEditorError::ControlNotFound, "HMI control was not found"); return failure(HmiEditorError::ControlNotFound, "HMI control was not found");
} }


// 先校验候选位置,失败时不修改工程模型
HmiControl candidate = *control; HmiControl candidate = *control;
candidate.bounds = bounds; candidate.bounds = bounds;
// 更新前保留原控件,只有全部编辑规则通过才覆盖原值
std::string error; std::string error;
if (!validateEditableControl(*page, candidate, &error)) if (!validateEditableControl(*page, candidate, &error))
{ {
@@ -228,6 +237,7 @@ HmiEditorResult HmiEditorService::moveControl(
return {true, HmiEditorError::None, {}, control_id}; return {true, HmiEditorError::None, {}, control_id};
} }


// 更新控件全部属性
HmiEditorResult HmiEditorService::updateControl( HmiEditorResult HmiEditorService::updateControl(
const std::string &page_id, const std::string &page_id,
const std::string &control_id, const std::string &control_id,
@@ -327,10 +337,11 @@ bool HmiEditorService::hasDuplicateControlId(
page.controls.cbegin(), page.controls.cend(), page.controls.cbegin(), page.controls.cend(),
[&excluded_id, &candidate_id](const HmiControl &item) [&excluded_id, &candidate_id](const HmiControl &item)
{ {
return item.id != excluded_id && item.id == candidate_id;
return item.id != excluded_id && item.id == candidate_id; // 如果这个控件不是正在编辑的原控件,并且它的 ID 等于准备使用的新 ID
}); });
} }


// 根据控件类型生成默认样式、文字、大小、初始坐标
HmiControl HmiEditorService::makeControl( HmiControl HmiEditorService::makeControl(
const HmiPage &page, HmiControlType type) const HmiPage &page, HmiControlType type)
{ {
@@ -346,9 +357,11 @@ HmiControl HmiEditorService::makeControl(
return control; return control;
} }


// 生成页面内不重复控件 ID,前缀 + 自增数字
std::string HmiEditorService::makeUniqueId( std::string HmiEditorService::makeUniqueId(
const HmiPage &page, const std::string &prefix) const HmiPage &page, const std::string &prefix)
{ {
// 同类控件从 1 递增命名,保证页面内标识稳定且唯一
int suffix = 1; int suffix = 1;
while (true) while (true)
{ {


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

@@ -118,18 +118,47 @@ public:
const HmiControl &control); const HmiControl &control);


private: private:
/**
* @brief 校验控件编辑后仍满足页面内的基础约束
* @param page 控件所属页面
* @param control 待校验控件
* @param error 校验失败时写入原因,可为 nullptr
* @return 控件标识、尺寸、位置和寄存器绑定区域均有效时返回 true
*/
static bool validateEditableControl( static bool validateEditableControl(
const HmiPage &page, const HmiPage &page,
const HmiControl &control, const HmiControl &control,
std::string *error); std::string *error);
/**
* @brief 检查候选控件标识是否与页面内其他控件重复
* @param page 待检查页面
* @param excluded_id 更新场景中需要排除的原控件标识
* @param candidate_id 待使用的控件标识
* @return 存在同标识的其他控件时返回 true
*/
static bool hasDuplicateControlId( static bool hasDuplicateControlId(
const HmiPage &page, const HmiPage &page,
const std::string &excluded_id, const std::string &excluded_id,
const std::string &candidate_id); const std::string &candidate_id);
/**
* @brief 按控件类型创建带默认属性的新控件
* @param page 新控件所属页面,用于确定初始位置和唯一标识
* @param type 新控件类型
* @return 未绑定寄存器的默认控件配置
*/
static HmiControl makeControl( static HmiControl makeControl(
const HmiPage &page, HmiControlType type); const HmiPage &page, HmiControlType type);
/**
* @brief 在页面内生成带指定前缀的唯一控件标识
* @param page 待检查页面
* @param prefix 控件类型对应的标识前缀
* @return 从 1 开始递增且不与现有控件重复的标识
*/
static std::string makeUniqueId( static std::string makeUniqueId(
const HmiPage &page, const std::string &prefix); const HmiPage &page, const std::string &prefix);


/**
* @brief 非拥有的工程服务依赖,由应用入口保证生命周期
*/
ProjectService &project_service_; ProjectService &project_service_;
}; };

+ 4
- 0
app/src/services/hmi_runtime_service.cpp Vedi File

@@ -32,6 +32,7 @@ HmiRuntimeService::HmiRuntimeService(RegisterRepository &repository)


HmiRuntimeReadResult HmiRuntimeService::readControl(const HmiControl &control) const HmiRuntimeReadResult HmiRuntimeService::readControl(const HmiControl &control) const
{ {
// 标签等静态控件不参与寄存器读写
if (!isBitControl(control.type) && !isWordControl(control.type)) if (!isBitControl(control.type) && !isWordControl(control.type))
{ {
return readFailure(HmiRuntimeError::UnsupportedControl); return readFailure(HmiRuntimeError::UnsupportedControl);
@@ -46,6 +47,7 @@ HmiRuntimeReadResult HmiRuntimeService::readControl(const HmiControl &control) c
} }
if (isBitControl(control.type)) if (isBitControl(control.type))
{ {
// 按控件类型选择仓库的 M 位读取契约
const BitReadResult result = repository_.readBit(*control.binding); const BitReadResult result = repository_.readBit(*control.binding);
if (!result.succeeded) if (!result.succeeded)
{ {
@@ -76,6 +78,7 @@ HmiRuntimeWriteResult HmiRuntimeService::toggleButton(const HmiControl &control)
{ {
return writeFailure(current.error); return writeFailure(current.error);
} }
// 先读后取反实现按钮点击切换 M 位语义
const RegisterWriteResult result = repository_.writeBit( const RegisterWriteResult result = repository_.writeBit(
*control.binding, !current.bit_value); *control.binding, !current.bit_value);
return result.succeeded return result.succeeded
@@ -106,6 +109,7 @@ HmiRuntimeWriteResult HmiRuntimeService::writeNumericInput(


HmiRuntimeError HmiRuntimeService::repositoryError(RegisterError error) HmiRuntimeError HmiRuntimeService::repositoryError(RegisterError error)
{ {
// 领域仓库错误被收敛为 HMI 可理解的运行错误
switch (error) switch (error)
{ {
case RegisterError::InvalidAddress: case RegisterError::InvalidAddress:


+ 5
- 5
app/src/services/hmi_runtime_service.h Vedi File

@@ -17,11 +17,11 @@
*/ */
enum class HmiRuntimeError enum class HmiRuntimeError
{ {
None,
UnsupportedControl,
MissingBinding,
InvalidBinding,
RepositoryFailure
None, // 无错误
UnsupportedControl, // 控件类型不支持寄存器读写
MissingBinding, // 控件未配置寄存器绑定
InvalidBinding, // 控件绑定地址无效或地址区域不匹配
RepositoryFailure // 寄存器仓库读取或写入失败
}; };


/** /**


+ 7
- 1
app/src/services/project_service.cpp Vedi File

@@ -9,7 +9,7 @@


namespace { namespace {


// 使用高精度时间戳和随机值生成工程标识
// 生成唯一工程 ID:project-时间戳-随机数
std::string generateProjectId() std::string generateProjectId()
{ {
const auto timestamp = static_cast<unsigned long long>( const auto timestamp = static_cast<unsigned long long>(
@@ -51,6 +51,7 @@ bool ProjectService::hasCurrentFile() const
return !current_file_path_.empty(); return !current_file_path_.empty();
} }


// 查询是否存在未保存修改
bool ProjectService::isModified() const bool ProjectService::isModified() const
{ {
return modified_; return modified_;
@@ -82,6 +83,7 @@ ProjectOperationResult ProjectService::save()
ProjectStorageError::None, ProjectStorageError::None,
"project file path is required"}; "project file path is required"};
} }
// 复用另存为流程,确保两种保存方式拥有相同的校验和错误处理
return saveAs(current_file_path_); return saveAs(current_file_path_);
} }


@@ -149,15 +151,18 @@ ProjectOperationResult ProjectService::load(const std::string &file_path)
return {true, ProjectServiceError::None, ProjectStorageError::None, {}}; return {true, ProjectServiceError::None, ProjectStorageError::None, {}};
} }


// 创建一个全新空白工程实例
Project ProjectService::makeNewProject(const std::string &name) Project ProjectService::makeNewProject(const std::string &name)
{ {
Project project; Project project;
project.metadata.id = generateProjectId(); project.metadata.id = generateProjectId();
project.metadata.name = name; project.metadata.name = name;
// 新工程固定使用当前存储格式版本
project.metadata.formatVersion = "1.0"; project.metadata.formatVersion = "1.0";
return project; return project;
} }


// 判断字符串是不是空白:空字符串 / 全是空格、制表符都算空白
bool ProjectService::isBlank(const std::string &value) bool ProjectService::isBlank(const std::string &value)
{ {
return value.empty() return value.empty()
@@ -170,6 +175,7 @@ bool ProjectService::isBlank(const std::string &value)
}); });
} }


// 包装存储层失败结果
ProjectOperationResult ProjectService::storageFailure( ProjectOperationResult ProjectService::storageFailure(
ProjectStorageError error, const std::string &message) ProjectStorageError error, const std::string &message)
{ {


+ 2
- 0
app/src/services/runtime_mode_service.cpp Vedi File

@@ -30,11 +30,13 @@ ModeTransitionResult RuntimeModeService::enterOfflineRunning()


ModeTransitionResult RuntimeModeService::enterOnlineRunning() ModeTransitionResult RuntimeModeService::enterOnlineRunning()
{ {
// 首次 PLC 读取状态由服务维护,再交给领域状态机统一裁决
return state_.enterOnlineRunning(initial_plc_read_completed_); return state_.enterOnlineRunning(initial_plc_read_completed_);
} }


void RuntimeModeService::setInitialPlcReadCompleted(bool completed) void RuntimeModeService::setInitialPlcReadCompleted(bool completed)
{ {
// 通信服务完成有效读回后才允许把此标志设为 true
initial_plc_read_completed_ = completed; initial_plc_read_completed_ = completed;
} }




+ 30
- 1
app/src/services/runtime_mode_service.h Vedi File

@@ -14,15 +14,44 @@
class RuntimeModeService class RuntimeModeService
{ {
public: public:
/**
* @brief 获取当前应用运行模式
* @return 当前的编辑、离线运行或真机运行模式
*/
ApplicationMode mode() const; ApplicationMode mode() const;
/**
* @brief 获取当前模式下允许的工程编辑和寄存器使用策略
* @return 与当前运行模式对应的能力策略
*/
ModePolicy policy() const; ModePolicy policy() const;


/**
* @brief 退出离线或真机运行态并返回编辑态
* @return 已处于编辑态或状态机拒绝切换时返回失败结果
*/
ModeTransitionResult enterEditing(); ModeTransitionResult enterEditing();
/**
* @brief 从编辑态进入离线运行态
* @return 非编辑态进入时返回必须先回到编辑态的失败结果
*/
ModeTransitionResult enterOfflineRunning(); ModeTransitionResult enterOfflineRunning();
/**
* @brief 从编辑态进入真机运行态
* @return 未完成 PLC 初次读取或非编辑态进入时返回失败结果
*/
ModeTransitionResult enterOnlineRunning(); ModeTransitionResult enterOnlineRunning();


// 由后续 PLC 通信服务在首次读取成功或缓存失效时更新
/**
* @brief 更新 PLC 是否已完成首次有效读取
* @param completed 为 true 时允许在编辑态进入真机运行模式
*
* 由 PLC 通信服务在首次读取成功或缓存失效时调用
*/
void setInitialPlcReadCompleted(bool completed); void setInitialPlcReadCompleted(bool completed);
/**
* @brief 查询 PLC 是否已完成首次有效读取
* @return 已建立可用于进入真机运行态的 PLC 缓存时返回 true
*/
bool initialPlcReadCompleted() const; bool initialPlcReadCompleted() const;


private: private:


+ 47
- 0
app/src/ui/hmi_editor_widget.cpp Vedi File

@@ -20,9 +20,11 @@


namespace { namespace {


// 将领域层 HMI 控件投影为可绘制、可选择和可交互的场景图元
class HmiGraphicsItem final : public QGraphicsItem class HmiGraphicsItem final : public QGraphicsItem
{ {
public: public:
// 保存控件快照、页面边界和回调,使图元不直接依赖服务层
HmiGraphicsItem( HmiGraphicsItem(
const HmiControl &control, const HmiControl &control,
int page_width, int page_width,
@@ -35,11 +37,17 @@ public:
moved_(std::move(moved)), moved_(std::move(moved)),
activated_(std::move(activated)) activated_(std::move(activated))
{ {
// 领域坐标直接作为图元在场景中的初始位置
setPos(control_.bounds.x, control_.bounds.y); setPos(control_.bounds.x, control_.bounds.y);
// 所有控件均可选中,便于主窗口显示对应属性
setFlag(ItemIsSelectable, true); setFlag(ItemIsSelectable, true);
// 开启可选中
setFlag(ItemSendsGeometryChanges, true);
// 画布只处理左键交互,保留其他按键给视图默认行为
setAcceptedMouseButtons(Qt::LeftButton); setAcceptedMouseButtons(Qt::LeftButton);
} }


// 返回图元自身坐标系中的矩形范围,用于绘制和命中测试
QRectF boundingRect() const override QRectF boundingRect() const override
{ {
return {0, return {0,
@@ -53,6 +61,7 @@ public:
const QStyleOptionGraphicsItem *option, const QStyleOptionGraphicsItem *option,
QWidget *) override QWidget *) override
{ {
// 留出一个像素边距,避免描边被图元边界裁剪
const QRectF rect = boundingRect().adjusted(1, 1, -1, -1); const QRectF rect = boundingRect().adjusted(1, 1, -1, -1);
painter->setRenderHint(QPainter::Antialiasing, true); painter->setRenderHint(QPainter::Antialiasing, true);
painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1)); painter->setPen(QPen(QColor(QStringLiteral("#47545f")), 1));
@@ -61,6 +70,7 @@ public:
{ {
case HmiControlType::Button: case HmiControlType::Button:
{ {
// 按钮显示文字,运行态点击行为由鼠标事件处理
painter->setBrush(QColor(QStringLiteral("#dcece3"))); painter->setBrush(QColor(QStringLiteral("#dcece3")));
painter->drawRoundedRect(rect, 4, 4); painter->drawRoundedRect(rect, 4, 4);
painter->setPen(QColor(QStringLiteral("#205c3b"))); painter->setPen(QColor(QStringLiteral("#205c3b")));
@@ -69,6 +79,7 @@ public:
} }
case HmiControlType::Indicator: case HmiControlType::Indicator:
{ {
// 指示灯颜色由最近一次读取到的 M 位值决定
const qreal diameter = std::min(rect.width(), rect.height() - 18.0); const qreal diameter = std::min(rect.width(), rect.height() - 18.0);
const QRectF lamp( const QRectF lamp(
rect.center().x() - diameter / 2.0, rect.center().x() - diameter / 2.0,
@@ -88,6 +99,7 @@ public:
} }
case HmiControlType::NumericDisplay: case HmiControlType::NumericDisplay:
{ {
// 数值显示为只读样式,文本由运行值刷新
painter->setBrush(QColor(QStringLiteral("#edf2f6"))); painter->setBrush(QColor(QStringLiteral("#edf2f6")));
painter->drawRect(rect); painter->drawRect(rect);
painter->setPen(QColor(QStringLiteral("#24313b"))); painter->setPen(QColor(QStringLiteral("#24313b")));
@@ -98,6 +110,7 @@ public:
} }
case HmiControlType::NumericInput: case HmiControlType::NumericInput:
{ {
// 数值输入以白色编辑框样式呈现,双击后才请求写入
painter->setBrush(QColor(QStringLiteral("#ffffff"))); painter->setBrush(QColor(QStringLiteral("#ffffff")));
painter->drawRoundedRect(rect, 3, 3); painter->drawRoundedRect(rect, 3, 3);
painter->setPen(QColor(QStringLiteral("#24313b"))); painter->setPen(QColor(QStringLiteral("#24313b")));
@@ -109,6 +122,7 @@ public:
case HmiControlType::Label: case HmiControlType::Label:
default: default:
{ {
// 标签只显示固定文本,不绑定寄存器运行值
painter->setPen(QColor(QStringLiteral("#24313b"))); painter->setPen(QColor(QStringLiteral("#24313b")));
painter->drawText(rect, Qt::AlignCenter, painter->drawText(rect, Qt::AlignCenter,
QString::fromUtf8(control_.text.data(), QString::fromUtf8(control_.text.data(),
@@ -119,22 +133,26 @@ public:


if ((option->state & QStyle::State_Selected) != 0) if ((option->state & QStyle::State_Selected) != 0)
{ {
// 选中框独立于控件类型,提示当前可编辑对象
painter->setBrush(Qt::NoBrush); painter->setBrush(Qt::NoBrush);
painter->setPen(QPen(QColor(QStringLiteral("#1677a8")), 2)); painter->setPen(QPen(QColor(QStringLiteral("#1677a8")), 2));
painter->drawRect(boundingRect().adjusted(0, 0, -1, -1)); painter->drawRect(boundingRect().adjusted(0, 0, -1, -1));
} }
} }


// 向场景和外部控件返回该图元对应的领域控件标识
const std::string &controlId() const const std::string &controlId() const
{ {
return control_.id; return control_.id;
} }


// 编辑态允许拖动,运行态保留点击或双击交互
void setInteractionEnabled(bool editable) void setInteractionEnabled(bool editable)
{ {
setFlag(ItemIsMovable, editable); setFlag(ItemIsMovable, editable);
} }


// 缓存运行服务读出的值并触发 Qt 重绘
void setRuntimeValue(bool bit_value, std::int16_t word_value, bool available) void setRuntimeValue(bool bit_value, std::int16_t word_value, bool available)
{ {
bit_value_ = bit_value; bit_value_ = bit_value;
@@ -144,10 +162,13 @@ public:
} }


protected: protected:
// 拖拽过程中将新位置限制在页面可见边界内
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override QVariant itemChange(GraphicsItemChange change, const QVariant &value) override
{ {
// 只有开启 ItemIsMovable 拖拽时,才做坐标钳位
if (change == ItemPositionChange && flags().testFlag(ItemIsMovable)) if (change == ItemPositionChange && flags().testFlag(ItemIsMovable))
{ {
// 在图元层预先截断拖拽坐标,避免控件视觉上越出页面
QPointF position = value.toPointF(); QPointF position = value.toPointF();
const qreal maximum_x = std::max( const qreal maximum_x = std::max(
0.0, static_cast<double>(page_width_ - control_.bounds.width)); 0.0, static_cast<double>(page_width_ - control_.bounds.width));
@@ -160,24 +181,30 @@ protected:
return QGraphicsItem::itemChange(change, value); return QGraphicsItem::itemChange(change, value);
} }


// 运行态点击按钮时通知外层执行 M 位切换
void mousePressEvent(QGraphicsSceneMouseEvent *event) override void mousePressEvent(QGraphicsSceneMouseEvent *event) override
{ {
// 条件:!ItemIsMovable 【也就是运行模式】 + 是按钮 + 有回调
if (!flags().testFlag(ItemIsMovable) if (!flags().testFlag(ItemIsMovable)
&& control_.type == HmiControlType::Button && activated_) && control_.type == HmiControlType::Button && activated_)
{ {
// 运行态单击按钮才触发 M 位切换,编辑态只用于选择和拖动
setSelected(true); setSelected(true);
activated_(control_.id); activated_(control_.id);
event->accept(); event->accept();
return; return;
} }
// 编辑模式:不进if分支,执行基类事件——只做选中、拖拽
QGraphicsItem::mousePressEvent(event); QGraphicsItem::mousePressEvent(event);
} }


// 运行态双击数值输入时通知外层弹出数值编辑对话框
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override
{ {
if (!flags().testFlag(ItemIsMovable) if (!flags().testFlag(ItemIsMovable)
&& control_.type == HmiControlType::NumericInput && activated_) && control_.type == HmiControlType::NumericInput && activated_)
{ {
// 数值输入使用双击,避免普通选择操作意外写入 D 字
setSelected(true); setSelected(true);
activated_(control_.id); activated_(control_.id);
event->accept(); event->accept();
@@ -186,9 +213,11 @@ protected:
QGraphicsItem::mouseDoubleClickEvent(event); QGraphicsItem::mouseDoubleClickEvent(event);
} }


// 拖拽结束后才提交最终坐标,避免移动过程频繁修改领域模型
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override
{ {
QGraphicsItem::mouseReleaseEvent(event); QGraphicsItem::mouseReleaseEvent(event);
// 只有ItemIsMovable打开(编辑态),松开鼠标才提交位置给业务层
if (flags().testFlag(ItemIsMovable) && moved_) if (flags().testFlag(ItemIsMovable) && moved_)
{ {
moved_(control_.id, pos()); moved_(control_.id, pos());
@@ -196,6 +225,7 @@ protected:
} }


private: private:
// 根据控件类型和运行数据组合当前应绘制的文字
QString textWithValue() const QString textWithValue() const
{ {
const QString text = QString::fromUtf8( const QString text = QString::fromUtf8(
@@ -212,16 +242,25 @@ private:
return text; return text;
} }


// 图元创建时的领域控件快照,提供类型、尺寸、文本和标识
HmiControl control_; HmiControl control_;
// 当前页面宽度,用于限制图元横向拖拽范围
int page_width_ = 0; int page_width_ = 0;
// 当前页面高度,用于限制图元纵向拖拽范围
int page_height_ = 0; int page_height_ = 0;
// 拖拽完成后回调 HmiEditorWidget 提交控件新位置
std::function<void(const std::string &, const QPointF &)> moved_; std::function<void(const std::string &, const QPointF &)> moved_;
// 运行模式点击按钮 / 双击输入框回调,通知外层做寄存器读写
std::function<void(const std::string &)> activated_; std::function<void(const std::string &)> activated_;
// 指示灯读取到的 M 位值
bool bit_value_ = false; bool bit_value_ = false;
// 数值控件读取到的 D 字值
std::int16_t word_value_ = 0; std::int16_t word_value_ = 0;
// 标记当前缓存值是否来自一次成功的运行时读取
bool has_runtime_value_ = false; bool has_runtime_value_ = false;
}; };


// 将场景通用图元安全转换为本文件定义的 HMI 控件图元
HmiGraphicsItem *asHmiItem(QGraphicsItem *item) HmiGraphicsItem *asHmiItem(QGraphicsItem *item)
{ {
return dynamic_cast<HmiGraphicsItem *>(item); return dynamic_cast<HmiGraphicsItem *>(item);
@@ -247,6 +286,7 @@ HmiEditorWidget::HmiEditorWidget(
this, &HmiEditorWidget::handleSelectionChanged); this, &HmiEditorWidget::handleSelectionChanged);
} }


// 切换显示页面
void HmiEditorWidget::setPageId(const std::string &page_id) void HmiEditorWidget::setPageId(const std::string &page_id)
{ {
if (page_id_ == page_id) if (page_id_ == page_id)
@@ -280,8 +320,10 @@ void HmiEditorWidget::setRuntimeActive(bool active)
} }
} }


// 全部重新加载当前页面,把内存模型 HmiPage 渲染成画面上图形
void HmiEditorWidget::reloadPage() void HmiEditorWidget::reloadPage()
{ {
// 画布始终从当前领域页面重建,避免保留已删除控件的图元
scene_->clear(); scene_->clear();
const HmiPage *page = editor_service_.findPage(page_id_); const HmiPage *page = editor_service_.findPage(page_id_);
if (page == nullptr) if (page == nullptr)
@@ -318,6 +360,7 @@ void HmiEditorWidget::reloadPage()
refreshRuntimeValues(); refreshRuntimeValues();
} }


// 遍历场景所有图元,找到对应 id 的图元,设置选中,视图滚动到把控件显示出来
void HmiEditorWidget::selectControl(const std::string &control_id) void HmiEditorWidget::selectControl(const std::string &control_id)
{ {
for (QGraphicsItem *item : scene_->items()) for (QGraphicsItem *item : scene_->items())
@@ -365,6 +408,7 @@ void HmiEditorWidget::refreshRuntimeValues()
{ {
continue; continue;
} }
// 运行值通过服务读取,图元不直接接触寄存器仓库
const HmiRuntimeReadResult value = runtime_service_.readControl(*control); const HmiRuntimeReadResult value = runtime_service_.readControl(*control);
control_item->setRuntimeValue(value.bit_value, value.word_value, value.succeeded); control_item->setRuntimeValue(value.bit_value, value.word_value, value.succeeded);
} }
@@ -389,6 +433,7 @@ void HmiEditorWidget::fitCurrentPage()


void HmiEditorWidget::updateItemInteractions() void HmiEditorWidget::updateItemInteractions()
{ {
// 遍历所有控件图元,统一设置flag
for (QGraphicsItem *item : scene_->items()) for (QGraphicsItem *item : scene_->items())
{ {
HmiGraphicsItem *control_item = asHmiItem(item); HmiGraphicsItem *control_item = asHmiItem(item);
@@ -412,6 +457,7 @@ void HmiEditorWidget::handleControlMoved(
{ {
return; return;
} }
// 鼠标坐标取整后再交给服务校验并写回模型
HmiRect bounds = control->bounds; HmiRect bounds = control->bounds;
bounds.x = static_cast<int>(std::lround(position.x())); bounds.x = static_cast<int>(std::lround(position.x()));
bounds.y = static_cast<int>(std::lround(position.y())); bounds.y = static_cast<int>(std::lround(position.y()));
@@ -438,6 +484,7 @@ void HmiEditorWidget::handleControlActivated(const std::string &control_id)
return; return;
} }
HmiRuntimeWriteResult result; HmiRuntimeWriteResult result;
// 只有可写控件允许激活,其余控件只展示最新运行值
if (control->type == HmiControlType::Button) if (control->type == HmiControlType::Button)
{ {
result = runtime_service_.toggleButton(*control); result = runtime_service_.toggleButton(*control);


+ 11
- 0
app/src/ui/hmi_editor_widget.h Vedi File

@@ -92,16 +92,27 @@ protected:
void resizeEvent(QResizeEvent *event) override; void resizeEvent(QResizeEvent *event) override;


private: private:
// 按页面尺寸缩放视图,保证完整画布保持可见
void fitCurrentPage(); void fitCurrentPage();
// 将当前编辑开关同步到所有控件图元
void updateItemInteractions(); void updateItemInteractions();
// 将场景选择转换为控件标识并通知属性面板
void handleSelectionChanged(); void handleSelectionChanged();
// 将图元拖动后的坐标提交给 HMI 编辑服务
void handleControlMoved(const std::string &control_id, const QPointF &position); void handleControlMoved(const std::string &control_id, const QPointF &position);
// 在运行态处理按钮点击或数值输入激活
void handleControlActivated(const std::string &control_id); void handleControlActivated(const std::string &control_id);


// 提供控件查找、移动和属性更新能力,不直接操作 Qt 图元数据
HmiEditorService &editor_service_; HmiEditorService &editor_service_;
// 提供运行态寄存器读写能力,画布不直接访问寄存器仓库
HmiRuntimeService &runtime_service_; HmiRuntimeService &runtime_service_;
// 持有所有页面图元和页面边框
QGraphicsScene *scene_ = nullptr; QGraphicsScene *scene_ = nullptr;
// 当前画布投影的页面标识
std::string page_id_; std::string page_id_;
// 控制是否允许拖动和编辑控件
bool editing_enabled_ = true; bool editing_enabled_ = true;
// 控制是否刷新运行值并响应运行态控件操作
bool runtime_active_ = false; bool runtime_active_ = false;
}; };

+ 32
- 4
app/src/ui/main_window.cpp Vedi File

@@ -134,10 +134,10 @@ MainWindow::MainWindow(
hmi_runtime_service_(hmi_runtime_service) hmi_runtime_service_(hmi_runtime_service)
{ {
ui_->setupUi(this); ui_->setupUi(this);
configureAppearance();
configureActions();
configurePropertyEditor();
configureHmiEditor();
configureAppearance(); // 窗口样式、布局、状态栏标签初始化,纯视觉代码,无业务逻辑
configureActions(); // 创建所有菜单 Action、工具栏按钮,绑定点击槽函数
configurePropertyEditor(); // 动态构建属性表单:ID、文本、坐标宽高、寄存器区域 (M/D)、地址索引 + 应用按钮
configureHmiEditor(); // 创建画布控件HmiEditorWidget嵌入主窗口,绑定画布信号:选中控件、控件拖动修改、编辑器报错;同时启动运行刷新定时器
hmi_editor_service_.ensureDefaultPage(); hmi_editor_service_.ensureDefaultPage();
refreshProjectUi(); refreshProjectUi();
updateModeUi(tr("系统已进入编辑态")); updateModeUi(tr("系统已进入编辑态"));
@@ -290,12 +290,14 @@ void MainWindow::configureHmiEditor()
connect(hmi_editor_widget_, &HmiEditorWidget::editorError, connect(hmi_editor_widget_, &HmiEditorWidget::editorError,
this, this,
[this](const QString &message) { statusBar()->showMessage(message, 5000); }); [this](const QString &message) { statusBar()->showMessage(message, 5000); });
// 运行态定时投影仓库最新值,编辑态不触发寄存器读取
runtime_refresh_timer_ = new QTimer(this); runtime_refresh_timer_ = new QTimer(this);
runtime_refresh_timer_->setInterval(150); runtime_refresh_timer_->setInterval(150);
connect(runtime_refresh_timer_, &QTimer::timeout, connect(runtime_refresh_timer_, &QTimer::timeout,
this, this,
[this] [this]
{ {
// 离线 / 联机运行模式
if (runtime_mode_service_.mode() != ApplicationMode::Editing) if (runtime_mode_service_.mode() != ApplicationMode::Editing)
{ {
hmi_editor_widget_->refreshRuntimeValues(); hmi_editor_widget_->refreshRuntimeValues();
@@ -352,6 +354,7 @@ void MainWindow::configurePropertyEditor()


void MainWindow::refreshProjectUi() void MainWindow::refreshProjectUi()
{ {
// 工程树只展示模型摘要,画布内容由 HmiEditorWidget 单独投影
const std::string current_page_id = hmi_editor_service_.firstPageId(); const std::string current_page_id = hmi_editor_service_.firstPageId();
if (hmi_editor_widget_ != nullptr) if (hmi_editor_widget_ != nullptr)
{ {
@@ -376,14 +379,23 @@ void MainWindow::refreshProjectUi()
ui_->projectTree->expandAll(); ui_->projectTree->expandAll();
} }


// 根据控件ID加载控件属性到右侧属性面板
void MainWindow::showControlProperties(const std::string &control_id) void MainWindow::showControlProperties(const std::string &control_id)
{ {
// 更新窗口全局状态:记录当前选中控件ID
selected_control_id_ = control_id; selected_control_id_ = control_id;

// 在第一页画面中,根据ID查找目标控件,获取只读模型指针
const HmiControl *control = hmi_editor_service_.findControl( const HmiControl *control = hmi_editor_service_.findControl(
hmi_editor_service_.firstPageId(), control_id); hmi_editor_service_.firstPageId(), control_id);
// 判断是否查询到有效控件
const bool has_control = control != nullptr; const bool has_control = control != nullptr;

// 更新属性面板顶部状态标签,显示选中控件ID或提示未选择
ui_->selectionValueLabel->setText( ui_->selectionValueLabel->setText(
has_control ? fromUtf8(control->id) : tr("未选择")); has_control ? fromUtf8(control->id) : tr("未选择"));

// 批量控制所有属性编辑组件可用性:无选中控件时整体置灰,禁止编辑
for (QWidget *widget : {static_cast<QWidget *>(control_id_edit_), for (QWidget *widget : {static_cast<QWidget *>(control_id_edit_),
static_cast<QWidget *>(control_text_edit_), static_cast<QWidget *>(control_text_edit_),
static_cast<QWidget *>(control_x_spin_box_), static_cast<QWidget *>(control_x_spin_box_),
@@ -396,21 +408,33 @@ void MainWindow::showControlProperties(const std::string &control_id)
{ {
widget->setEnabled(has_control); widget->setEnabled(has_control);
} }

// 未选中有效控件,无需填充属性,直接退出
if (!has_control) if (!has_control)
{ {
return; return;
} }

// 将控件基础信息填充至UI输入框
control_id_edit_->setText(fromUtf8(control->id)); control_id_edit_->setText(fromUtf8(control->id));
control_text_edit_->setText(fromUtf8(control->text)); control_text_edit_->setText(fromUtf8(control->text));
// 控件位置坐标
control_x_spin_box_->setValue(control->bounds.x); control_x_spin_box_->setValue(control->bounds.x);
control_y_spin_box_->setValue(control->bounds.y); control_y_spin_box_->setValue(control->bounds.y);
// 控件宽高尺寸
control_width_spin_box_->setValue(control->bounds.width); control_width_spin_box_->setValue(control->bounds.width);
control_height_spin_box_->setValue(control->bounds.height); control_height_spin_box_->setValue(control->bounds.height);

// 根据控件类型获取默认寄存器区域:按钮/指示灯默认M区;数值类控件默认D区
const RegisterArea default_area = usesMAddress(control->type) const RegisterArea default_area = usesMAddress(control->type)
? RegisterArea::M : RegisterArea::D; ? RegisterArea::M : RegisterArea::D;
// 优先使用控件已绑定的寄存器区域;无绑定则使用类型对应的默认区域
const RegisterArea area = control->binding.has_value() const RegisterArea area = control->binding.has_value()
? control->binding->area() : default_area; ? control->binding->area() : default_area;

// 同步寄存器区域下拉框选中项(0=M,1=D,与combo初始化顺序严格对应)
binding_area_combo_box_->setCurrentIndex(area == RegisterArea::M ? 0 : 1); binding_area_combo_box_->setCurrentIndex(area == RegisterArea::M ? 0 : 1);
// 填充寄存器地址索引;无绑定时默认地址为0
binding_index_spin_box_->setValue( binding_index_spin_box_->setValue(
control->binding.has_value() ? control->binding->index() : 0); control->binding.has_value() ? control->binding->index() : 0);
} }
@@ -458,6 +482,7 @@ void MainWindow::applySelectedControlProperties()
{ {
return; return;
} }
// 从现有控件复制不可见属性,避免属性面板覆盖扩展配置
HmiControl control = *old_control; HmiControl control = *old_control;
control.id = toUtf8(control_id_edit_->text()); control.id = toUtf8(control_id_edit_->text());
control.text = toUtf8(control_text_edit_->text()); control.text = toUtf8(control_text_edit_->text());
@@ -566,6 +591,7 @@ void MainWindow::showProjectResult(


void MainWindow::requestMode(ApplicationMode requested_mode) void MainWindow::requestMode(ApplicationMode requested_mode)
{ {
// UI 仅转发模式意图,合法性由服务层和领域状态机决定
ModeTransitionResult result; ModeTransitionResult result;
switch (requested_mode) switch (requested_mode)
{ {
@@ -605,6 +631,7 @@ void MainWindow::updateModeUi(const QString &message)
const ApplicationMode mode = runtime_mode_service_.mode(); const ApplicationMode mode = runtime_mode_service_.mode();
const ModePolicy policy = runtime_mode_service_.policy(); const ModePolicy policy = runtime_mode_service_.policy();
restoreCurrentModeAction(); restoreCurrentModeAction();
// 将同一份模式策略同步到所有可编辑入口,避免只禁用部分操作
ui_->projectDock->setEnabled(policy.allowsProjectEditing); ui_->projectDock->setEnabled(policy.allowsProjectEditing);
ui_->propertiesDock->setEnabled(policy.allowsProjectEditing); ui_->propertiesDock->setEnabled(policy.allowsProjectEditing);
hmi_editor_widget_->setEditingEnabled(policy.allowsProjectEditing); hmi_editor_widget_->setEditingEnabled(policy.allowsProjectEditing);
@@ -646,6 +673,7 @@ void MainWindow::updateModeUi(const QString &message)
ui_->outputList->scrollToBottom(); ui_->outputList->scrollToBottom();
} }


// 根据当前真实运行模式,同步更新模式工具栏单选按钮选中状态
void MainWindow::restoreCurrentModeAction() void MainWindow::restoreCurrentModeAction()
{ {
ui_->editingModeAction->setChecked(runtime_mode_service_.mode() ui_->editingModeAction->setChecked(runtime_mode_service_.mode()


+ 14
- 0
app/src/ui/main_window.h Vedi File

@@ -65,19 +65,33 @@ public:
~MainWindow() override; ~MainWindow() override;


private: private:
// 配置菜单和工具栏动作
void configureActions(); void configureActions();
// 配置主窗口外观和状态栏
void configureAppearance(); void configureAppearance();
// 创建并连接 HMI 编辑画布
void configureHmiEditor(); void configureHmiEditor();
// 创建并连接控件属性编辑表单
void configurePropertyEditor(); void configurePropertyEditor();
// 刷新工程树和当前 HMI 页面信息
void refreshProjectUi(); void refreshProjectUi();
// 显示指定 HMI 控件的可编辑属性
void showControlProperties(const std::string &control_id); void showControlProperties(const std::string &control_id);
// 向当前页面添加指定类型的 HMI 控件
void addHmiControl(HmiControlType type); void addHmiControl(HmiControlType type);
// 删除当前选中的 HMI 控件
void deleteSelectedControl(); void deleteSelectedControl();
// 将属性表单内容应用到当前选中控件
void applySelectedControlProperties(); void applySelectedControlProperties();
// 创建新的工程并刷新编辑界面
void createNewProject(); void createNewProject();
// 保存当前工程到已关联的路径
void saveProject(); void saveProject();
// 将当前工程保存到用户指定的路径
void saveProjectAs(); void saveProjectAs();
// 从用户指定的路径加载工程
void loadProject(); void loadProject();
// 在状态栏和输出面板显示工程操作结果
void showProjectResult(const QString &action, const QString &message, bool succeeded); void showProjectResult(const QString &action, const QString &message, bool succeeded);


/** /**


+ 7
- 0
app/tests/domain_tests.cpp Vedi File

@@ -23,6 +23,7 @@ void require(bool condition, const std::string &message)


void testRegisterAddressBoundaries() void testRegisterAddressBoundaries()
{ {
// 覆盖 M/D 地址允许范围及未知枚举值的拒绝路径
require(RegisterAddress{RegisterArea::M, 0}.isValid(), require(RegisterAddress{RegisterArea::M, 0}.isValid(),
"M0 must be valid"); "M0 must be valid");
require(RegisterAddress{RegisterArea::D, 4000}.isValid(), require(RegisterAddress{RegisterArea::D, 4000}.isValid(),
@@ -37,6 +38,7 @@ void testRegisterAddressBoundaries()


void testRegisterRepositorySeparatesAreas() void testRegisterRepositorySeparatesAreas()
{ {
// 验证离线仓库不会把 M 位和 D 字交叉解释
VirtualRegisterRepository repository; VirtualRegisterRepository repository;
const RegisterAddress m0{RegisterArea::M, 0}; const RegisterAddress m0{RegisterArea::M, 0};
const RegisterAddress d0{RegisterArea::D, 0}; const RegisterAddress d0{RegisterArea::D, 0};
@@ -54,6 +56,7 @@ void testRegisterRepositorySeparatesAreas()


Project makeValidProject() Project makeValidProject()
{ {
// 构造包含 HMI 绑定和逻辑连接的最小合法工程作为测试基线
HmiControl start_button; HmiControl start_button;
start_button.id = "start-button"; start_button.id = "start-button";
start_button.type = HmiControlType::Button; start_button.type = HmiControlType::Button;
@@ -92,6 +95,7 @@ Project makeValidProject()


void testLogicNodeConfigurationBoundaries() void testLogicNodeConfigurationBoundaries()
{ {
// 触点只能绑定 M 区,数值比较只能绑定 D 区
LogicNode contact; LogicNode contact;
contact.id = "contact"; contact.id = "contact";
contact.config = ContactNodeConfig{ contact.config = ContactNodeConfig{
@@ -115,6 +119,7 @@ void testLogicNodeConfigurationBoundaries()


void testModelsValidateBindingsAndIdentifiers() void testModelsValidateBindingsAndIdentifiers()
{ {
// 聚合验证必须拒绝错误绑定、重复标识和越界控件
Project project = makeValidProject(); Project project = makeValidProject();
require(project.validate(), "valid project model must pass validation"); require(project.validate(), "valid project model must pass validation");


@@ -141,6 +146,7 @@ void testModelsValidateBindingsAndIdentifiers()


void testRuntimeStateBoundaries() void testRuntimeStateBoundaries()
{ {
// 运行模式测试覆盖离线和真机的互斥及 PLC 首读前置条件
RuntimeState state; RuntimeState state;
require(state.policy().allowsProjectEditing, "editing mode must allow project editing"); require(state.policy().allowsProjectEditing, "editing mode must allow project editing");
require(state.enterOfflineRunning().succeeded, "editing may enter offline running"); require(state.enterOfflineRunning().succeeded, "editing may enter offline running");
@@ -167,6 +173,7 @@ int main()
{ {
try try
{ {
// 每个测试函数独立覆盖一个领域边界,首个异常即终止测试进程
testRegisterAddressBoundaries(); testRegisterAddressBoundaries();
testRegisterRepositorySeparatesAreas(); testRegisterRepositorySeparatesAreas();
testLogicNodeConfigurationBoundaries(); testLogicNodeConfigurationBoundaries();


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

@@ -14,6 +14,7 @@ namespace {
class TestProjectStorage final : public ProjectStorage class TestProjectStorage final : public ProjectStorage
{ {
public: public:
// 编辑服务测试只关注内存模型变更,不依赖真实文件系统
ProjectSaveResult save(const Project &, const std::string &) override ProjectSaveResult save(const Project &, const std::string &) override
{ {
return {true, ProjectStorageError::None, {}}; return {true, ProjectStorageError::None, {}};
@@ -35,6 +36,7 @@ void require(bool condition, const std::string &message)


void testControlEditing() void testControlEditing()
{ {
// 覆盖控件创建、移动、绑定校验、重命名冲突和删除流程
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage); ProjectService project_service(storage);
HmiEditorService service(project_service); HmiEditorService service(project_service);
@@ -81,6 +83,7 @@ void testControlEditing()


void testRuntimeUsesRegisterRepository() void testRuntimeUsesRegisterRepository()
{ {
// 运行服务只能经由仓库接口读写 M/D,不依赖具体离线实现
VirtualRegisterRepository repository; VirtualRegisterRepository repository;
HmiRuntimeService runtime_service(repository); HmiRuntimeService runtime_service(repository);


@@ -123,6 +126,7 @@ int main()
{ {
try try
{ {
// 编辑和运行场景分别验证服务层两条独立职责
testControlEditing(); testControlEditing();
testRuntimeUsesRegisterRepository(); testRuntimeUsesRegisterRepository();
} }


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

@@ -22,6 +22,7 @@ namespace {
class TestProjectStorage final : public ProjectStorage class TestProjectStorage final : public ProjectStorage
{ {
public: public:
// 主窗口测试使用无副作用存储实现,隔离文件对 UI 行为的影响
ProjectSaveResult save(const Project &, const std::string &) override ProjectSaveResult save(const Project &, const std::string &) override
{ {
return {true, ProjectStorageError::None, {}}; return {true, ProjectStorageError::None, {}};
@@ -44,6 +45,7 @@ void require(bool condition, const std::string &message)
template<typename ObjectType> template<typename ObjectType>
ObjectType *requiredChild(MainWindow &window, const char *name) ObjectType *requiredChild(MainWindow &window, const char *name)
{ {
// 通过 objectName 取得 Designer 组件,缺失时给出明确测试失败信息
ObjectType *child = window.findChild<ObjectType *>(QString::fromLatin1(name)); ObjectType *child = window.findChild<ObjectType *>(QString::fromLatin1(name));
require(child != nullptr, std::string("missing UI object ") + name); require(child != nullptr, std::string("missing UI object ") + name);
return child; return child;
@@ -51,6 +53,7 @@ ObjectType *requiredChild(MainWindow &window, const char *name)


void testModeActionsControlEditingAvailability() void testModeActionsControlEditingAvailability()
{ {
// 验证模式动作会同步禁用编辑入口,并在失败时恢复当前选择
TestProjectStorage storage; TestProjectStorage storage;
ProjectService project_service(storage); ProjectService project_service(storage);
HmiEditorService editor_service(project_service); HmiEditorService editor_service(project_service);
@@ -134,6 +137,7 @@ void testModeActionsControlEditingAvailability()


int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
// 无窗口平台使 Qt Widgets 测试可在自动化环境稳定运行
qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("offscreen")); qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("offscreen"));
QApplication application(argc, argv); QApplication application(argc, argv);




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

@@ -22,6 +22,7 @@ void require(bool condition, const std::string &message)


Project makeExampleProject() Project makeExampleProject()
{ {
// 构造覆盖四种 HMI 控件和三种逻辑节点的完整 JSON 往返样本
HmiControl start_button; HmiControl start_button;
start_button.id = "start-button"; start_button.id = "start-button";
start_button.type = HmiControlType::Button; start_button.type = HmiControlType::Button;
@@ -98,6 +99,7 @@ Project makeExampleProject()


void writeText(const QString &path, const QByteArray &content) void writeText(const QString &path, const QByteArray &content)
{ {
// 直接写入故障样本文件,以验证加载失败时的保护行为
QFile file(path); QFile file(path);
require(file.open(QIODevice::WriteOnly), "test file must be writable"); require(file.open(QIODevice::WriteOnly), "test file must be writable");
require(file.write(content) == content.size(), "test file must be written completely"); require(file.write(content) == content.size(), "test file must be written completely");
@@ -112,6 +114,7 @@ QByteArray readBytes(const QString &path)


void testEmptyProjectRoundTrip() void testEmptyProjectRoundTrip()
{ {
// 空工程是合法工程,保存再加载后不应凭空产生页面或逻辑
QTemporaryDir directory; QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid"); require(directory.isValid(), "temporary directory must be valid");


@@ -136,6 +139,7 @@ void testEmptyProjectRoundTrip()


void testExampleProjectRoundTrip() void testExampleProjectRoundTrip()
{ {
// 验证各层嵌套字段往返后保持不变且序列化结果稳定
QTemporaryDir directory; QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid"); require(directory.isValid(), "temporary directory must be valid");


@@ -186,6 +190,7 @@ void testExampleProjectRoundTrip()


void testInvalidFiles() void testInvalidFiles()
{ {
// 非法文件必须被拒绝,并且不得覆盖服务中当前工程
QTemporaryDir directory; QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid"); require(directory.isValid(), "temporary directory must be valid");


@@ -219,6 +224,7 @@ void testInvalidFiles()


void testServiceStateAndSaveErrors() void testServiceStateAndSaveErrors()
{ {
// 保存路径和修改标记只在成功持久化后更新
QTemporaryDir directory; QTemporaryDir directory;
require(directory.isValid(), "temporary directory must be valid"); require(directory.isValid(), "temporary directory must be valid");


@@ -252,6 +258,7 @@ int main()
{ {
try try
{ {
// 工程服务和 JSON 存储在同一测试进程中验证完整闭环
testEmptyProjectRoundTrip(); testEmptyProjectRoundTrip();
testExampleProjectRoundTrip(); testExampleProjectRoundTrip();
testInvalidFiles(); testInvalidFiles();


+ 2
- 0
app/tests/runtime_mode_service_tests.cpp Vedi File

@@ -16,6 +16,7 @@ void require(bool condition, const std::string &message)


void testModeTransitions() void testModeTransitions()
{ {
// 验证服务将 PLC 首读状态与领域模式切换规则正确组合
RuntimeModeService service; RuntimeModeService service;


require(service.mode() == ApplicationMode::Editing, require(service.mode() == ApplicationMode::Editing,
@@ -54,6 +55,7 @@ int main()
{ {
try try
{ {
// 运行模式只有这一组状态机边界测试
testModeTransitions(); testModeTransitions();
} }
catch (const std::exception &error) catch (const std::exception &error)


Caricamento…
Annulla
Salva