SexyUI Editor 是一个基于 PyQt6 的可视化 UI 编辑器,用于 Plants vs. Zombies (PvZ) 中使用的 Sexy 框架。它支持为 C++ 和 C# (.NET) 版本生成代码。
SexyUIEditor/
├── core/ # 核心业务逻辑
│ ├── generators/ # 代码生成模块(按语言和框架拆分)
│ │ ├── __init__.py
│ │ ├── base.py # 基类和公共常量
│ │ ├── csharp_legacy.py # C# 完整生成器
│ │ ├── cpp/ # C++ 模块化生成器
│ │ │ ├── __init__.py
│ │ │ ├── sexy.py # C++ Sexy框架控件
│ │ │ ├── pvz.py # C++ PvZ特定控件
│ │ │ └── extended.py # C++ 扩展控件
│ │ └── csharp/ # C# 模块化生成器
│ │ ├── __init__.py
│ │ ├── sexy.py # C# Sexy框架控件
│ │ ├── pvz.py # C# PvZ特定控件
│ │ └── extended.py # C# 扩展控件
│ ├── __init__.py
│ ├── code_generator.py # 主代码生成器(整合所有生成器)
│ ├── code_parser.py # 代码解析工具
│ ├── code_sync.py # 代码同步
│ ├── component_registry.py # 控件组件定义
│ ├── i18n.py # 国际化
│ ├── predefined_actions.py # 预定义动作处理器
│ ├── project.py # 项目数据模型
│ ├── resource_manager.py # 资源管理
│ ├── resource_groups.py # 延迟加载资源组解析
│ ├── net_resources.py # .NET 资源管理
│ └── undo_manager.py # 撤销/重做功能
├── ui/ # 用户界面组件
│ ├── canvas.py # 可视化编辑画布
│ ├── property_panel.py # 属性编辑面板
│ ├── event_config.py # 事件配置对话框
│ ├── image_picker.py # 图片/字体资源选择器
│ ├── code_view.py # 代码预览窗口
│ ├── preview_window.py # 界面预览窗口
│ └── ...
├── Content/ # .NET 版本游戏资源文件
│ ├── images/ # 图片资源
│ ├── fonts/ # 字体资源
│ ├── resources.xml # 资源定义
│ └── atlas_definitions.json # Atlas 图片切分定义
├── docs/ # 文档
│ └── CORE_ARCHITECTURE.md # 本文件
├── main.py # 应用程序入口
└── test.sexyui # 示例项目文件
编辑器支持两种输出目标:
- 项目文件后缀:
.sexyui - 分辨率:800x600
- 图片引用:
Sexy::IMAGE_xxx
C++ 模式支持两种项目结构,主要区别在于头文件包含路径:
QE 结构 (默认):
#include "../../SexyAppFramework/Widget.h"
#include "../../SexyAppFramework/ButtonListener.h"
#include "../../SexyAppFramework/ButtonWidget.h"Portable 结构:
#include "widget/Widget.h"
#include "widget/ButtonListener.h"
#include "widget/ButtonWidget.h"项目结构通过 core/header_includes.py 中的 HeaderIncludeManager 类管理,支持:
- 自动根据项目结构生成正确的头文件包含路径
- 在工具栏中切换 QE/Portable 结构
- 默认使用 QE 结构
- 项目文件后缀:
.cssexyui - 分辨率:800x480
- 图片引用:
Resources.IMAGE_xxx或AtlasResources.IMAGE_xxx
.NET 版本使用 Atlas(精灵图)系统,将多个小图片打包到一张大图中。
Content/atlas_definitions.json 定义了如何从大图中切分小图:
{
"IMAGE_DIALOG": {
"sub_images": {
"IMAGE_DIALOG_TOPLEFT": {"x": 515, "y": 185, "width": 57, "height": 52},
"IMAGE_BUTTON_MIDDLE": {"x": 624, "y": 185, "width": 25, "height": 30},
...
}
}
}以下前缀的图片会被识别为 Atlas 子图片:
IMAGE_DIALOG_*- 对话框组件IMAGE_BUTTON_*- 按钮组件IMAGE_OPTIONS_*- 选项控件IMAGE_REANIM_*- 动画资源- 等等...
Atlas 子图片在 C# 代码中使用 AtlasResources 前缀:
// 普通图片
Resources.IMAGE_BACKGROUND1
// Atlas 子图片
AtlasResources.IMAGE_DIALOG_TOPLEFT
AtlasResources.IMAGE_BUTTON_MIDDLE在 C# 模式下,图片选择器会:
- 显示所有可用资源(包括 Atlas 子图片)
- Atlas 子图片以青色高亮显示
- 预览时正确显示切分后的图片
.NET 版本使用延迟加载机制,某些资源需要显式加载资源组才能使用。
core/resource_groups.py 解析 Content/resources.xml 文件,建立图片ID到资源组的映射:
from core.resource_groups import get_delay_load_call
# 获取资源加载调用
call = get_delay_load_call("IMAGE_BACKGROUND_MUSHROOMGARDEN", resources_path)
# 返回: 'mApp.DelayLoadBackgroundResource("DelayLoad_MushroomGarden");'生成器会自动检测延迟加载资源,并在构造函数中添加加载调用:
public MainWidget(LawnApp theApp)
{
mApp = theApp;
Resize(0, 0, 800, 480);
mApp.DelayLoadBackgroundResource("DelayLoad_MushroomGarden"); // 自动添加
// ...
}代码生成器按语言和控件来源分层组织:
core/generators/
├── __init__.py # 模块导出
├── base.py # 基础类和公共常量
├── csharp_legacy.py # C# 完整生成器
├── cpp/ # C++ 模块化生成器
│ ├── __init__.py
│ ├── sexy.py # C++ Sexy框架控件
│ ├── pvz.py # C++ PvZ特定控件
│ └── extended.py # C++ 扩展控件
└── csharp/ # C# 模块化生成器
├── __init__.py
├── sexy.py # C# Sexy框架控件
├── pvz.py # C# PvZ特定控件
└── extended.py # C# 扩展控件
处理标准 Sexy 框架控件:
ButtonWidget- 基本按钮EditWidget- 文本输入框Checkbox- 复选框Slider- 滑块Dialog- 对话框DialogButton- 对话框按钮(支持通过 DrawImageBox 拉伸图片)ListWidget- 列表控件ScrollWidget- 滚动容器HyperlinkWidget- 超链接ScrollbuttonWidget- 滚动按钮TextWidget- 文本显示控件
处理 Plants vs. Zombies 特定控件:
GameButton- PVZ 游戏按钮(非 Widget 类型)NewLawnButton- PVZ 新风格按钮LawnDialog- PVZ 对话框LawnStoneButton- PVZ 石头按钮LawnEditWidget- PVZ 编辑控件
处理自定义扩展控件:
Label- 自定义文本标签(使用TodDrawStringWrapped实现自动换行)ImageBox- 自定义图片框(使用TodDrawImageScaledF实现缩放)
处理 .NET 版本的 Sexy 框架控件,生成 C# 语法代码。
处理 .NET 版本的 PvZ 特定控件,支持 Atlas 子图片引用。
处理 .NET 版本的自定义扩展控件。
包含:
- 公共常量(SKIP_PROPS, WIDGET_PROPS, COLOR_PROPS 等)
- 控件类型分类(SEXY_WIDGETS, PVZ_WIDGETS, CUSTOM_WIDGETS 等)
- 工具方法(颜色解析、变量命名、监听器检测等)
SEXY_WIDGETS = {
"ButtonWidget", "EditWidget", "Checkbox", "Slider", "Dialog",
"DialogButton", "ListWidget", "ScrollWidget", "HyperlinkWidget",
"ScrollbuttonWidget", "TextWidget"
}
PVZ_WIDGETS = {
"NewLawnButton", "LawnDialog", "LawnStoneButton",
"LawnEditWidget", "GameButton"
}
CUSTOM_WIDGETS = {
"Label", "ImageBox"
}
NON_WIDGET_TYPES = {
"GameButton", "Label", "ImageBox" # 不由 WidgetManager 管理
}DialogButton、Dialog、LawnDialog支持通过DrawImageBox进行图片拉伸NewLawnButton不支持图片拉伸(使用DrawButtonImage)ImageBox支持通过TodDrawImageScaledF进行自定义缩放
NewLawnButton使用SetFont()方法设置正常字体mHiliteFont直接作为属性设置Label使用TodDrawStringWrapped实现自动换行
NewLawnButton有mUniformImage属性- 设置后自动应用到
mButtonImage、mOverImage、mDownImage - 单独的图片属性可以覆盖统一图片
- .NET 版本:资源从
Content/resources.xml加载 - Atlas 子图片从
Content/atlas_definitions.json加载 - 自动检测延迟加载资源的资源组
- 默认导出到配置文件所在目录
- 支持"导出所有界面"功能
- 导出前自动同步同目录下的代码文件中的用户代码
- 从其他项目文件导入界面
- 文件过滤器根据当前项目类型自动调整:
- C++ 项目:只显示
.sexyui文件 - C# 项目:只显示
.cssexyui文件
- C++ 项目:只显示
- 自动跳过重复类名的界面
导出前会自动:
- 检测同目录下已存在的代码文件
- 从代码文件中提取用户代码(
// [[[USER_xxx]]]区域) - 将用户代码合并到项目数据中
- 生成新代码时保留用户修改
保存项目时自动:
- 检测同目录下的代码文件
- 将代码文件路径关联到界面设置中
如果自动关联未生效,可以通过菜单手动关联:
- 菜单路径:Sync → Associate Source File...
- C++ 模式:选择 .h 和 .cpp 文件
- C# 模式:选择 .cs 文件
- 关联后会在界面设置中保存文件路径
SexyUIExtensions/
├── cpp/ # C++ 扩展组件
│ ├── Label.json # Label 组件定义
│ ├── Label.h # Label 头文件
│ ├── Label.cpp # Label 源文件
│ ├── ImageBox.json # ImageBox 组件定义
│ ├── ImageBox.h # ImageBox 头文件
│ └── ImageBox.cpp # ImageBox 源文件
└── csharp/ # C# 扩展组件
├── Label.json # Label 组件定义
├── Label.cs # Label 源文件
├── ImageBox.json # ImageBox 组件定义
└── ImageBox.cs # ImageBox 源文件
{
"class_name": "Label",
"display_name": "文本标签",
"description": "使用TodDrawStringWrapped绘制的文本标签",
"parent_class": "Widget",
"category": "extension",
"is_container": false,
"properties": [
{
"name": "mX",
"display_name": "X坐标",
"type": "INT",
"default": 0,
"category": "geometry"
},
...
]
}ExtensionManager类负责加载和管理扩展组件- 支持按平台(C++/C#)加载不同的扩展组件
- 扩展组件会在组件面板的"扩展"分类中显示
- 导出代码时自动包含扩展组件的源文件
事件通过控件的 event_actions 属性配置:
"event_actions": {
"ButtonDepress": [
{
"action_type": "predefined",
"predefined_id": "switch_to_project_interface",
"params": {
"interface_class": "testWidget",
"interface_id": "testWidget::INTERFACE_ID"
}
}
]
}- Widget 类型界面:使用
mWidgetManager->AddWidget()添加,mWidgetManager->RemoveWidget()移除 - Dialog 类型界面:使用
mApp->AddDialog()添加,mApp->KillDialog()移除
内置界面(如商店、图鉴)都是 Dialog 类型,会叠加在当前界面上显示。
重要:显示内置界面时,不需要先关闭当前 Widget 界面!
// 错误配置 - 会导致商店关闭后无法返回
"event_actions": {
"ButtonDepress": [
{ "predefined_id": "close_current_widget" }, // 错误!
{ "predefined_id": "show_store" }
]
}
// 正确配置 - 商店是 Dialog,会叠加在 Widget 上
"event_actions": {
"ButtonDepress": [
{ "predefined_id": "show_store" }
]
}| 动作 | 说明 | 适用场景 |
|---|---|---|
open_project_interface |
打开项目界面(叠加) | 不关闭当前界面 |
switch_to_project_interface |
切换到项目界面 | 先关闭当前界面再打开新界面 |
close_current_dialog |
关闭当前 Dialog | 仅 Dialog 类型 |
close_current_widget |
关闭当前 Widget | 仅 Widget 类型 |
事件处理器中的用户代码优先级:
- 用户自定义代码 - 最高优先级,保留在
// [[[HANDLER_xxx]]]区域内 - 编辑器配置的事件代码 - 当没有用户代码时生成
- 空占位符 - 当两者都没有时生成
{
"settings": {
"namespace": "",
"header_include": "",
"target_platform": "cpp"
},
"interfaces": {
"interface_id": {
"settings": {
"id": "interface_id",
"name": "显示名称",
"class_name": "类名",
"parent_class": "Widget",
"interface_type": "main|dialog|widget",
"width": 800,
"height": 600,
"background_image": "IMAGE_NAME",
"background_stretch": false,
"background_color": "0,0,0",
"listeners": ["ButtonListener"]
},
"widgets": { ... },
"root_widget_ids": [...],
"user_code": { ... }
}
},
"main_interface_id": "interface_id",
"current_interface_id": "interface_id"
}{
"settings": {
"namespace": "",
"header_include": "",
"target_platform": "csharp"
},
"interfaces": {
"interface_id": {
"settings": {
"id": "interface_id",
"name": "显示名称",
"class_name": "类名",
"parent_class": "Widget",
"interface_type": "main|dialog|widget",
"width": 800,
"height": 480,
"background_image": "IMAGE_NAME",
"background_stretch": false,
"background_color": "0,0,0",
"listeners": ["ButtonListener"]
},
"widgets": { ... },
"root_widget_ids": [...],
"user_code": { ... }
}
},
"main_interface_id": "interface_id",
"current_interface_id": "interface_id"
}-
头文件生成 (
generate_header_for_interface)- 确定所需的监听器
- 生成带有基类的类声明
- 添加虚方法声明
- 添加成员变量
-
CPP 文件生成 (
generate_cpp_for_interface)- 生成包含语句
- 生成构造函数和控件初始化
- 生成 Draw 方法(先绘制自定义控件,然后调用 Widget::Draw)
- 生成 Update 方法
- 生成鼠标处理器(针对非 Widget 类型)
- 生成事件处理器
- 添加用户代码区域
- 单一文件生成 (
generate_csharp)- 生成 using 语句
- 生成命名空间和类声明
- 生成构造函数和控件初始化
- 生成 Draw 方法
- 生成 Update 方法
- 生成事件处理器
- 添加用户代码区域
// [[[USER_INCLUDES]]]- 自定义头文件包含// [[[USER_FORWARD_DECLARATIONS]]]- 前向声明和枚举定义(在类定义之前)// [[[USER_DECLARATIONS]]]- 成员声明// [[[USER_INIT]]]- 初始化代码// [[[USER_DESTROY]]]- 清理代码// [[[USER_DRAW]]]- 自定义绘制代码// [[[USER_UPDATE]]]- 更新逻辑// [[[USER_FUNCTIONS]]]- 自定义函数// [[[USER_POST_CLASS]]]- 类后定义(辅助类等,在类定义之后)// [[[HANDLER_widget_id]]]- 事件处理器代码
// [[[USER_INCLUDES]]]- 自定义 using 语句// [[[USER_DECLARATIONS]]]- 成员声明// [[[USER_INIT]]]- 初始化代码// [[[USER_DESTROY]]]- 清理代码// [[[USER_DRAW]]]- 自定义绘制代码// [[[USER_UPDATE]]]- 更新逻辑// [[[USER_FUNCTIONS]]]- 自定义函数// [[[HANDLER_widget_id]]]- 事件处理器代码