SexyUI Editor is a PyQt6-based visual UI editor for the Sexy framework used in Plants vs. Zombies (PvZ). It supports code generation for both C++ and C# (.NET) versions.
SexyUIEditor/
├── core/ # Core business logic
│ ├── generators/ # Code generation modules (organized by language and framework)
│ │ ├── __init__.py
│ │ ├── base.py # Base class and common constants
│ │ ├── csharp_legacy.py # C# complete generator
│ │ ├── cpp/ # C++ modular generators
│ │ │ ├── __init__.py
│ │ │ ├── sexy.py # C++ Sexy framework widgets
│ │ │ ├── pvz.py # C++ PvZ-specific widgets
│ │ │ └── extended.py # C++ extended widgets
│ │ └── csharp/ # C# modular generators
│ │ ├── __init__.py
│ │ ├── sexy.py # C# Sexy framework widgets
│ │ ├── pvz.py # C# PvZ-specific widgets
│ │ └── extended.py # C# extended widgets
│ ├── __init__.py
│ ├── code_generator.py # Main code generator (integrates all generators)
│ ├── code_parser.py # Code parsing utilities
│ ├── code_sync.py # Code synchronization
│ ├── component_registry.py # Widget component definitions
│ ├── extension_manager.py # Extension component management
│ ├── i18n.py # Internationalization
│ ├── predefined_actions.py # Predefined action handlers
│ ├── project.py # Project data model
│ ├── resource_manager.py # Resource management
│ ├── resource_groups.py # Lazy-loaded resource group parsing
│ ├── net_resources.py # .NET resource management
│ └── undo_manager.py # Undo/Redo functionality
├── ui/ # User interface components
│ ├── canvas.py # Visual editing canvas
│ ├── property_panel.py # Property editing panel
│ ├── event_config.py # Event configuration dialog
│ ├── image_picker.py # Image/font resource picker
│ ├── code_view.py # Code preview window
│ ├── preview_window.py # Interface preview window
│ └── ...
├── Content/ # .NET version game resources
│ ├── images/ # Image resources
│ ├── fonts/ # Font resources
│ ├── resources.xml # Resource definitions
│ └── atlas_definitions.json # Atlas image slicing definitions
├── SexyUIExtensions/ # Extension components directory
│ ├── cpp/ # C++ extensions
│ └── csharp/ # C# extensions
├── docs/ # Documentation
│ ├── CORE_ARCHITECTURE.md # This file (Chinese)
│ └── CORE_ARCHITECTURE_EN.md # English version
├── main.py # Application entry point
└── test.sexyui # Example project file
The editor supports two output targets:
- Project file extension:
.sexyui - Resolution: 800x600
- Image references:
Sexy::IMAGE_xxx
C++ mode supports two project structures, differing mainly in header include paths:
QE Structure (Default):
#include "../../SexyAppFramework/Widget.h"
#include "../../SexyAppFramework/ButtonListener.h"
#include "../../SexyAppFramework/ButtonWidget.h"Portable Structure:
#include "widget/Widget.h"
#include "widget/ButtonListener.h"
#include "widget/ButtonWidget.h"Project structures are managed by the HeaderIncludeManager class in core/header_includes.py, supporting:
- Automatic generation of correct header include paths based on project structure
- Switching between QE/Portable structures in the toolbar
- Default to QE structure
- Project file extension:
.cssexyui - Resolution: 800x480
- Image references:
Resources.IMAGE_xxxorAtlasResources.IMAGE_xxx
The .NET version uses an Atlas (sprite sheet) system that packs multiple small images into one large image.
Content/atlas_definitions.json defines how to slice small images from large images:
{
"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},
...
}
}
}Images with the following prefixes are recognized as Atlas sub-images:
IMAGE_DIALOG_*- Dialog componentsIMAGE_BUTTON_*- Button componentsIMAGE_OPTIONS_*- Option controlsIMAGE_REANIM_*- Animation resources- And more...
Atlas sub-images use the AtlasResources prefix in C# code:
// Normal images
Resources.IMAGE_BACKGROUND1
// Atlas sub-images
AtlasResources.IMAGE_DIALOG_TOPLEFT
AtlasResources.IMAGE_BUTTON_MIDDLEIn C# mode, the image picker will:
- Display all available resources (including Atlas sub-images)
- Highlight Atlas sub-images in cyan
- Correctly display sliced images in preview
The .NET version uses a lazy-loading mechanism where certain resources require explicit resource group loading.
core/resource_groups.py parses the Content/resources.xml file to build a mapping from image IDs to resource groups:
from core.resource_groups import get_delay_load_call
# Get resource loading call
call = get_delay_load_call("IMAGE_BACKGROUND_MUSHROOMGARDEN", resources_path)
# Returns: 'mApp.DelayLoadBackgroundResource("DelayLoad_MushroomGarden");'The generator automatically detects lazy-loaded resources and adds loading calls in the constructor:
public MainWidget(LawnApp theApp)
{
mApp = theApp;
Resize(0, 0, 800, 480);
mApp.DelayLoadBackgroundResource("DelayLoad_MushroomGarden"); // Auto-added
// ...
}The code generators are organized hierarchically by language and widget source:
core/generators/
├── __init__.py # Module exports
├── base.py # Base class and common constants
├── csharp_legacy.py # C# complete generator
├── cpp/ # C++ modular generators
│ ├── __init__.py
│ ├── sexy.py # C++ Sexy framework widgets
│ ├── pvz.py # C++ PvZ-specific widgets
│ └── extended.py # C++ extended widgets
└── csharp/ # C# modular generators
├── __init__.py
├── sexy.py # C# Sexy framework widgets
├── pvz.py # C# PvZ-specific widgets
└── extended.py # C# extended widgets
Handles standard Sexy framework widgets:
ButtonWidget- Basic buttonEditWidget- Text input boxCheckbox- CheckboxSlider- SliderDialog- DialogDialogButton- Dialog button (supports image stretching via DrawImageBox)ListWidget- List controlScrollWidget- Scroll containerHyperlinkWidget- HyperlinkScrollbuttonWidget- Scroll buttonTextWidget- Text display control
Handles Plants vs. Zombies specific widgets:
GameButton- PVZ game button (non-Widget type)NewLawnButton- PVZ new style buttonLawnDialog- PVZ dialogLawnStoneButton- PVZ stone buttonLawnEditWidget- PVZ edit control
Handles custom extended widgets:
Label- Custom text label (usesTodDrawStringWrappedfor auto-wrapping)ImageBox- Custom image box (usesTodDrawImageScaledFfor scaling)
Handles .NET version Sexy framework widgets, generates C# syntax code.
Handles .NET version PvZ-specific widgets, supports Atlas sub-image references.
Handles .NET version custom extended widgets.
Contains:
- Common constants (SKIP_PROPS, WIDGET_PROPS, COLOR_PROPS, etc.)
- Widget type classifications (SEXY_WIDGETS, PVZ_WIDGETS, CUSTOM_WIDGETS, etc.)
- Utility methods (color parsing, variable naming, listener detection, etc.)
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" # Not managed by WidgetManager
}DialogButton,Dialog,LawnDialogsupport image stretching viaDrawImageBoxNewLawnButtondoes not support image stretching (usesDrawButtonImage)ImageBoxsupports custom scaling viaTodDrawImageScaledF
NewLawnButtonusesSetFont()method to set normal fontmHiliteFontis set directly as a propertyLabelusesTodDrawStringWrappedfor auto-wrapping
NewLawnButtonhasmUniformImageproperty- When set, automatically applies to
mButtonImage,mOverImage,mDownImage - Individual image properties can override the uniform image
- .NET version: Resources loaded from
Content/resources.xml - Atlas sub-images loaded from
Content/atlas_definitions.json - Automatic detection of resource groups for lazy-loaded resources
- Default export to the same directory as the config file
- Supports "Export All Interfaces" functionality
- Auto-syncs user code from code files in the same directory before export
- Import interfaces from other project files
- File filter automatically adjusts based on current project type:
- C++ projects: Only shows
.sexyuifiles - C# projects: Only shows
.cssexyuifiles
- C++ projects: Only shows
- Automatically skips interfaces with duplicate class names
Before export, automatically:
- Detects existing code files in the same directory
- Extracts user code from code files (
// [[[USER_xxx]]]regions) - Merges user code into project data
- Preserves user modifications when generating new code
When saving a project, automatically:
- Detects code files in the same directory
- Associates code file paths with interface settings
If auto-association doesn't work, you can manually associate via menu:
- Menu Path: Sync → Associate Source File...
- C++ Mode: Select .h and .cpp files
- C# Mode: Select .cs file
- File paths are saved in interface settings after association
SexyUIExtensions/
├── cpp/ # C++ extension components
│ ├── Label.json # Label component definition
│ ├── Label.h # Label header file
│ ├── Label.cpp # Label source file
│ ├── ImageBox.json # ImageBox component definition
│ ├── ImageBox.h # ImageBox header file
│ └── ImageBox.cpp # ImageBox source file
└── csharp/ # C# extension components
├── Label.json # Label component definition
├── Label.cs # Label source file
├── ImageBox.json # ImageBox component definition
└── ImageBox.cs # ImageBox source file
{
"class_name": "Label",
"display_name": "Text Label",
"description": "Text label drawn using TodDrawStringWrapped",
"parent_class": "Widget",
"category": "extension",
"is_container": false,
"properties": [
{
"name": "mX",
"display_name": "X Position",
"type": "INT",
"default": 0,
"category": "geometry"
},
...
]
}ExtensionManagerclass handles loading and managing extension components- Supports loading different extension components by platform (C++/C#)
- Extension components appear in the "Extension" category of the component panel
- Source files for extension components are automatically included when exporting code
Events are configured through the widget's event_actions property:
"event_actions": {
"ButtonDepress": [
{
"action_type": "predefined",
"predefined_id": "switch_to_project_interface",
"params": {
"interface_class": "testWidget",
"interface_id": "testWidget::INTERFACE_ID"
}
}
]
}- Widget type interfaces: Added via
mWidgetManager->AddWidget(), removed viamWidgetManager->RemoveWidget() - Dialog type interfaces: Added via
mApp->AddDialog(), removed viamApp->KillDialog()
Built-in interfaces (like store, almanac) are all Dialog types that display as overlays on the current interface.
Important: When showing built-in interfaces, you do NOT need to close the current Widget interface first!
// Wrong configuration - will cause inability to return after store closes
"event_actions": {
"ButtonDepress": [
{ "predefined_id": "close_current_widget" }, // Wrong!
{ "predefined_id": "show_store" }
]
}
// Correct configuration - store is a Dialog, overlays on Widget
"event_actions": {
"ButtonDepress": [
{ "predefined_id": "show_store" }
]
}| Action | Description | Use Case |
|---|---|---|
open_project_interface |
Open project interface (overlay) | Doesn't close current interface |
switch_to_project_interface |
Switch to project interface | Closes current interface before opening new one |
close_current_dialog |
Close current Dialog | Dialog types only |
close_current_widget |
Close current Widget | Widget types only |
Priority of user code in event handlers:
- User-defined code - Highest priority, preserved in
// [[[HANDLER_xxx]]]regions - Editor-configured event code - Generated when no user code exists
- Empty placeholder - Generated when neither exists
{
"settings": {
"namespace": "",
"header_include": "",
"target_platform": "cpp"
},
"interfaces": {
"interface_id": {
"settings": {
"id": "interface_id",
"name": "Display Name",
"class_name": "ClassName",
"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": "Display Name",
"class_name": "ClassName",
"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"
}-
Header File Generation (
generate_header_for_interface)- Determine required listeners
- Generate class declaration with base class
- Add virtual method declarations
- Add member variables
-
CPP File Generation (
generate_cpp_for_interface)- Generate include statements
- Generate constructor and widget initialization
- Generate Draw method (draw custom widgets first, then call Widget::Draw)
- Generate Update method
- Generate mouse handlers (for non-Widget types)
- Generate event handlers
- Add user code regions
- Single File Generation (
generate_csharp)- Generate using statements
- Generate namespace and class declaration
- Generate constructor and widget initialization
- Generate Draw method
- Generate Update method
- Generate event handlers
- Add user code regions
// [[[USER_INCLUDES]]]- Custom header includes// [[[USER_FORWARD_DECLARATIONS]]]- Forward declarations and enum definitions (before class definition)// [[[USER_DECLARATIONS]]]- Member declarations// [[[USER_INIT]]]- Initialization code// [[[USER_DESTROY]]]- Cleanup code// [[[USER_DRAW]]]- Custom drawing code// [[[USER_UPDATE]]]- Update logic// [[[USER_FUNCTIONS]]]- Custom functions// [[[USER_POST_CLASS]]]- Post-class definitions (helper classes, etc., after class definition)// [[[HANDLER_widget_id]]]- Event handler code
// [[[USER_INCLUDES]]]- Custom using statements// [[[USER_DECLARATIONS]]]- Member declarations// [[[USER_INIT]]]- Initialization code// [[[USER_DESTROY]]]- Cleanup code// [[[USER_DRAW]]]- Custom drawing code// [[[USER_UPDATE]]]- Update logic// [[[USER_FUNCTIONS]]]- Custom functions// [[[HANDLER_widget_id]]]- Event handler code// [[[EDIT_widget_id]]]- Edit widget text changed handler// [[[CHECKBOX_widget_id]]]- Checkbox state changed handler// [[[SLIDER_widget_id]]]- Slider value changed handler
The editor supports multiple languages through the i18n system:
i18n/zh_CN.json- Chinese (Simplified)i18n/en.json- English
from core.i18n import tr
# Get translated text with fallback
text = tr("menu.file", "&File")- Add translation key-value pairs to
i18n/zh_CN.jsonandi18n/en.json - Use the
tr()function in code to retrieve translations - The system automatically loads the appropriate language based on user settings
The build.bat script uses Nuitka to create a standalone executable:
python -m nuitka ^
--standalone ^
--output-filename=SexyUIEditor.exe ^
--include-data-dir=i18n=i18n ^
--include-data-dir=resources=resources ^
--include-data-dir=SexyUIExtensions=SexyUIExtensions ^
...i18n/- Translation filesresources/- Icons and UI resourcesSexyUIExtensions/- Extension components
After building, manually copy:
pak/- Game resource filesContent/- .NET game resources
Copyright (C) 2026 StackAndPointer
This project is open-source software. Contributions are welcome!
Special thanks to PopCap Games for open-sourcing the Sexy framework, which made this editor possible.