跳转到内容

实战项目:编辑器UI系统

本章节将展示如何使用RmlUi构建一个专业的编辑器UI系统。这是一个与游戏UI完全不同的应用场景,展示RmlUi在工具应用中的强大能力。


项目概述

1.1 项目目标

构建一个2D关卡编辑器的完整UI系统,包括:

  • 主界面布局(菜单栏、工具栏、状态栏)
  • 层级管理面板
  • 场景树视图
  • 属性检查器
  • 工具箱
  • 预览窗口
  • 日志输出窗口
  • 多文档支持
  • 主题系统(亮色/暗色模式)

1.2 技术特点

  • MDI(多文档界面)设计
  • 可停靠面板系统
  • 实时预览
  • 拖拽支持
  • 快捷键系统
  • 撤销/重做功能
  • 自动保存

项目架构

2.1 编辑器架构

EditorApp
├── EditorUIManager (编辑器UI管理器)
│   ├── MainWindow (主窗口)
│   │   ├── MenuBar (菜单栏)
│   │   ├── ToolBar (工具栏)
│   │   ├── StatusBar (状态栏)
│   ├── PanelManager (面板管理器)
│   │   ├── SceneTreePanel (场景树)
│   │   ├── LayerPanel (层级面板)
│   │   ├── PropertyPanel (属性检查器)
│   │   ├── ToolboxPanel (工具箱)
│   │   ├── LogPanel (日志面板)
│   │   └── AssetBrowserPanel (资源浏览器)
│   ├── DocumentManager (文档管理器)
│   │   ├── EditorDocument (编辑器文档)
│   │   ├── SceneDocument (场景文档)
│   │   └── PreviewWindow (预览窗口)
│   └── CommandManager (命令管理器)
├── EditorDataManager (编辑器数据管理)
├── ThemeManager (主题管理器)
└── EditorPluginSystem (编辑器插件系统)

2.2 项目结构

EditorUI/
├── src/
│   ├── main.cpp
│   ├── EditorApp.h/cpp
│   ├── ui/
│   │   ├── EditorUIManager.h/cpp
│   │   ├── MainWindow.h/cpp
│   │   ├── panels/
│   │   │   ├── SceneTreePanel.h/cpp
│   │   │   ├── LayerPanel.h/cpp
│   │   │   ├── PropertyPanel.h/cpp
│   │   │   ├── ToolboxPanel.h/cpp
│   │   │   ├── LogPanel.h/cpp
│   │   │   └── AssetBrowserPanel.h/cpp
│   │   ├── menubar/
│   │   │   ├── FileMenu.h/cpp
│   │   │   ├── EditMenu.h/cpp
│   │   │   ├── ViewMenu.h/cpp
│   │   │   └── HelpMenu.h/cpp
│   │   ├── toolbar/
│   │   │   ├── StandardTools.h/cpp
│   │   │   ├── TransformTools.h/cpp
│   │   │   └── ObjectTools.h/cpp
│   │   ├── statusbar/
│   │   └── StatusBar.h/cpp
│   │   └── documents/
│   │       ├── EditorDocument.h/cpp
│   │       └── SceneDocument.h/cpp
│   ├── core/
│   │   ├── DocumentManager.h/cpp
│   │   ├── CommandManager.h/cpp
│   │   ├── UndoRedoSystem.h/cpp
│   │   └── AutoSaveManager.h/cpp
│   └── plugins/
│       ├── EditorPlugin.h/cpp
│       └── ExamplePlugin.h/cpp
├── data/
│   ├── ui/
│   │   ├── main_window.rml/rcss
│   │   ├── panels/
│   │   │   ├── scene_tree.rml/rcss
│   │   │   ├── layer.rml/rcss
│   │   │   ├── property.rml/rcss
│   │   │   ├── toolbox.rml/rcss
│   │   │   ├── log.rml/rcss
│   │   │   └── asset_browser.rml/rcss
│   │   ├── menubar/
│   │   │   ├── file.rml/rcss
│   │   │   ├── edit.rml/rcss
│   │   │   ├── view.rml/rcss
│   │ │   └── help.rml/rcss
│   │   ├── toolbar/
│   │   │   ├── standard.rml/rcss
│   │   │   ├── transform.rml/rcss
│   │   │   └── object.rml/rcss
│   │   └── statusbar.rml/rcss
│   ├── icons/
│   ├── themes/
│   │   ├── light.json
│   │   └── dark.json
│   └── fonts/
└── CMakeLists.txt

核心实现

3.1 主窗口(MainWindow)

cpp
// MainWindow.h
#pragma once
#include "EditorUIManager.h"
#include <RmlUi/Core/EventListener.h>

class MainWindow : public Rml::EventListener
{
public:
    MainWindow(Rml::Context* context, EditorUIManager* ui_manager);
    ~MainWindow();

    bool Initialize();
    void Shutdown();

    void Update(float delta_time);

    void ProcessEvent(Rml::Event& event) override;

private:
    void CreateLayout();
    void CreateMenuBar();
    void CreateToolBar();
    void CreateStatusBar();
    void CreatePanels();
    void CreateDockingSystem();

private:
    Rml::Context* context_;
    EditorUIManager* ui_manager_;
    
    Rml::ElementDocument* document_;
    Rml::Element* menu_bar_;
    Rml::Element* tool_bar_;
    Rml::Element* status_bar_;
    Rml::Element* main_content_;
    
    // 面板
    std::vector<Rml::Element*> panels_;
    std::vector<Rml::Element*> docked_panels_;
};

// MainWindow.cpp
MainWindow::MainWindow(Rml::Context* context, EditorUIManager* ui_manager)
    : context_(context), ui_manager_(ui_manager), document_(nullptr)
{
}

bool MainWindow::Initialize()
{
    document_ = context_->LoadDocument("ui/main_window.rml");
    if (!document_)
        return false;
    
    CreateLayout();
    CreateMenuBar();
    CreateToolBar();
    CreateStatusBar();
    CreatePanels();
    CreateDockingSystem();
    
    document_->Show();
    return true;
}

void MainWindow::CreateLayout()
{
    menu_bar_ = document_->GetElementById("menu-bar");
    tool_bar_ = document_->GetElementById("tool-bar");
    status_bar_ = document_->GetElementById("status-bar");
    main_content_ = document_->GetElementById("main-content");
}

void MainWindow::CreateMenuBar()
{
    // 创建菜单项
    CreateMenu("file", "File", {
        {"new", "新建", "Ctrl+N", "新建场景"},
        {"open", "打开", "Ctrl+O", "打开场景"},
        {"save", "保存", "Ctrl+S", "保存场景"},
        {"save_as", "另存为", "Ctrl+Shift+S", "另存为场景"},
        {"separator"},
        {"export", "导出", "Ctrl+E", "导出场景"},
        {"separator"},
        {"exit", "退出", "Alt+F4", "退出编辑器"}
    });
    
    CreateMenu("edit", "Edit", {
        {"undo", "撤销", "Ctrl+Z", "撤销"},
        {"redo", "重做", "Ctrl+Y", "重做"},
        {"separator"},
        {"cut", "剪切", "Ctrl+X", "剪切"},
        {"copy", "复制", "Ctrl+C", "复制"},
        {"paste", "粘贴", "Ctrl+V", "粘贴"},
        {"delete", "删除", "Delete", "删除选中对象"},
        {"separator"},
        {"select_all", "全选", "Ctrl+A", "全选"},
        {"deselect_all", "取消选择", "Ctrl+D", "取消选择"}
    });
}

void MainWindow::CreateToolBar()
{
    // 创建工具栏按钮
    CreateToolButton("select", "选择工具", "V", "选择对象");
    CreateToolButton("move", "移动工具", "G", "移动对象");
    CreateToolButton("rotate", "旋转工具", "R", "旋转对象");
    CreateToolButton("scale", "缩放工具", "S", "缩放对象");
    CreateToolButton("separator");
    CreateToolButton("brush", "画笔工具", "B", "画笔工具");
    CreateToolButton("eraser", "橡皮擦", "E", "橡皮擦");
    CreateToolButton("fill", "填充工具", "F", "填充区域");
    CreateToolButton("text", "文本工具", "T", "插入文本");
}

void MainWindow::CreatePanels()
{
    // 创建并注册面板
    panels_.push_back(ui_manager_->CreatePanel("scene_tree", "场景树", "left"));
    panels_.push_back(ui_manager_->CreatePanel("layer", "层级", "left"));
    panels_.push_back(ui_manager_->CreatePanel("property", "属性", "right"));
    panels_.push_back(ui_manager_->CreatePanel("toolbox", "工具箱", "right"));
    panels_.push_back(ui_manager_->CreatePanel("log", "日志", "bottom"));
    panels_.push_back(ui_manager_->CreatePanel("asset_browser", "资源", "left"));
    
    // 设置默认布局
    SetDefaultLayout();
}

void MainWindow::ProcessEvent(Rml::Event& event)
{
    if (event.GetId() == Rml::EventId::Click)
    {
        Rml::String id = event.GetTargetElement()->GetId();
        
        if (id.find("tool-") == 0)
        {
            std::string tool = id.substr(5);  // "tool-" 的长度
            SelectTool(tool);
        }
    }
}

3.2 面板系统

cpp
// PanelManager.h
#pragma once
#include <RmlUi/Core/Context.h>
#include <RmlUi/Core/Element.h>
#include <string>
#include <memory>
#include <vector>

class PanelManager : public Rml::EventListener
{
public:
    PanelManager(Rml::Context* context);
    ~PanelManager();

    bool Initialize();
    void Shutdown();

    // 创建面板
    Rml::Element* CreatePanel(const std::string& name, const std::string& title, const std::string& default_dock);
    
    // 面板控制
    void ShowPanel(const std::string& name);
    void HidePanel(const std::string& name);
    void TogglePanel(const std::string& name);
    void DockPanel(const std::string& name, const std::string& dock_position);
    void UndockPanel(const std::string& name);
    
    // 布局管理
    void SaveLayout(const std::string& layout_name);
    void LoadLayout(const std::string& layout_name);
    void ResetLayout();

    void ProcessEvent(Rml::Event& event) override;

private:
    void CreatePanelContent(Rml::Element* panel, const std::string& name);

private:
    Rml::Context* context_;
    std::unordered_map<std::string, Rml::Element*> panels_;
    std::vector<std::string> visible_panels_;
    
    Rml::Element* dock_container_;
    Rml::Element* float_panels_;
};

// PanelManager.cpp
bool PanelManager::Initialize()
{
    // 创建停靠容器
    dock_container_ = context_->CreateElement("div");
    dock_container_->SetClass("dock-container");
    document_->AppendChild(dock_container_);
    
    return true;
}

Rml::Element* PanelManager::CreatePanel(const std::string& name, const std::string& title, const std::string& default_dock)
{
    // 创建面板
    Rml::Element* panel = context_->CreateElement("div");
    panel->SetClass("panel");
    panel->SetAttribute("data-panel", name);
    
    // 创建面板头部
    Rml::Element* header = context_->CreateElement("div");
    header->SetClass("panel-header");
    header->SetInnerRML("<span>" + title + "</span>");
    
    // 创建关闭按钮
    Rml::Element* close_btn = context_->CreateElement("button");
    close_btn->SetClass("panel-close");
    close_btn->SetInnerRML("×");
    close_btn->SetAttribute("data-panel", name);
    close_btn->AddEventListener(Rml::EventId::Click, this);
    
    header->AppendChild(close_btn);
    panel->AppendChild(header);
    
    // 创建面板内容区域
    Rml::Element* content = context_->CreateElement("div");
    content->SetClass("panel-content");
    panel->AppendChild(content);
    
    // 添加到主窗口
    Rml::Element* main_content = document_->GetElementById("main-content");
    main_content->AppendChild(panel);
    
    // 保存面板引用
    panels_[name] = panel;
    visible_panels_.push_back(name);
    
    // 停靠到默认位置
    DockPanel(name, default_dock);
    
    return panel;
}

void PanelManager::DockPanel(const std::string& name, const std::string& dock_position)
{
    auto it = panels_.find(name);
    if (it == panels_.end())
        return;
    
    Rml::Element* panel = it->second;
    
    // 根据停靠位置设置样式
    if (dock_position == "left")
    {
        panel->SetClass("panel docked-left");
    }
    else if (dock_position == "right")
    {
        panel->SetClass("panel docked-right");
    }
    else if (dock_position == "top")
    {
        panel->SetClass("panel docked-top");
    }
    else if (dock_position == "bottom")
    {
        panel->SetClass("panel docked-bottom");
    }
}

void PanelManager::ProcessEvent(Rml::Event& event)
{
    if (event.GetId() == Rml::EventId::Click)
    {
        Rml::Element* target = event.GetTargetElement();
        
        if (target->HasAttribute("data-panel"))
        {
            std::string panel_name = target->GetAttribute("data-panel")->Get<std::string>();
            
            if (target->GetId() == "close-btn")
            {
                ClosePanel(panel_name);
            }
            else if (target->GetClass().find("dock-btn") != std::string::npos)
            {
                std::string dock_pos = target->GetAttribute("data-dock")->Get<std::string>();
                DockPanel(panel_name, dock_pos);
            }
        }
    }
}

3.3 属性检查器

cpp
// PropertyPanel.h
#pragma once
#include "UIPanel.h"
#include <unordered_map>
#include <variant>

class PropertyPanel : public UIPanel
{
public:
    PropertyPanel(Rml::Context* context, EditorUIManager* ui_manager);
    ~PropertyPanel() = default;

    void UpdateSelection(const std::vector<std::string>& selected_objects);

private:
    void CreateProperties();
    void UpdatePropertyValues();
    void ClearProperties();

private:
    std::vector<std::string> selected_objects_;
    
    Rml::Element* properties_container_;
    std::unordered_map<std::string, Rml::Element*> property_elements_;
};

// PropertyPanel.cpp
PropertyPanel::PropertyPanel(Rml::Context* context, EditorUIManager* ui_manager)
    : UIPanel(context, ui_manager)
{
}

void PropertyPanel::UpdateSelection(const std::vector<std::string>& selected_objects)
{
    selected_objects_ = selected_objects;
    ClearProperties();
    
    if (selected_objects.empty())
    {
        ShowEmptyState();
        return;
    }
    
    // 创建属性项
    CreateTransformProperties();
    CreateRenderProperties();
    CreateComponentProperties();
    
    document_->DispatchEvent(Rml::StringId("properties_updated"), {});
}

void PropertyPanel::CreateTransformProperties()
{
    Rml::Element* section = document_->CreateElement("div");
    section->SetClass("property-section");
    section->SetInnerRML("<h3>变换</h3>");
    properties_container_->AppendChild(section);
    
    // 位置
    CreateProperty("position_x", "位置 X", "number", "0");
    CreateProperty("position_y", "位置 Y", "number", "0");
    CreateProperty("position_z", "位置 Z", "number", "0");
    
    // 旋转
    CreateProperty("rotation_x", "旋转 X", "number", "0");
    CreateProperty("rotation_y", "旋转 Y", "number", "0");
    CreateProperty("rotation_z", "旋转 Z", "number", "0");
    
    // 缩放
    CreateProperty("scale_x", "缩放 X", "number", "1");
    CreateProperty("scale_y", "缩放 Y", "number", "1");
    CreateProperty("scale_z", "缩放 Z", "number", "1");
}

void PropertyPanel::CreateProperty(
    const std::string& id,
    const std::string& label,
    const std::string& type,
    const std::string& default_value)
{
    Rml::Element* property_row = document_->CreateElement("div");
    property_row->SetClass("property-row");
    
    // 标签
    Rml::Element* label = document_->CreateElement("label");
    label->SetInnerRML(label);
    property_row->AppendChild(label);
    
    // 输入控件
    Rml::Element* input = document_->CreateElement("input");
    input->SetClass("property-input");
    input->SetAttribute("id", id);
    
    if (type == "number")
    {
        input->SetAttribute("type", "number");
        input->SetAttribute("value", default_value);
        input->SetAttribute("step", "0.1");
    }
    
    property_row->AppendChild(input);
    
    // 添加到容器
    properties_container_->AppendChild(property_row);
    
    // 保存引用
    property_elements_[id] = input;
    
    // 添加事件监听
    input->AddEventListener(Rml::EventId::Change, this);
}

3.4 场景树面板

cpp
// SceneTreePanel.h
#pragma once
#include "UIPanel.h"
#include <vector>

class SceneTreePanel : public UIPanel, public Rml::EventListener
{
public:
    SceneTreePanel(Rml::Context* context, EditorUIManager* ui_manager);
    ~SceneTreePanel() = default;

    void SetSceneData(const std::vector<SceneNode>& nodes);
    void UpdateSceneData(const std::vector<SceneNode>& nodes);

    void ProcessEvent(Rml::Event& event) override;

private:
    void CreateTree(const std::vector<SceneNode>& nodes);
    void UpdateTree(const std::vector<SceneNode>& nodes);
    void OnNodeSelected(const std::string& node_id);
    void OnNodeExpanded(const std::string& node_id);
    void OnNodeCollapsed(const std::string& node_id);

private:
    Rml::Element* tree_container_;
    std::unordered_map<std::string, Rml::Element*> node_elements_;
};

// SceneTreePanel.cpp
SceneTreePanel::SceneTreePanel(Rml::Context* context, EditorUIManager* ui_manager)
    : UIPanel(context, ui_manager)
{
}

void SceneTreePanel::SetSceneData(const std::vector<SceneNode>& nodes)
{
    CreateTree(nodes);
}

void SceneTreePanel::CreateTree(const std::vector<SceneNode>& nodes)
{
    if (!tree_container_)
        tree_container_ = document_->GetElementById("scene-tree");
    
    if (!tree_container_)
        return;
    
    tree_container_->InnerRML("");
    node_elements_.clear();
    
    CreateNodeRecursive(tree_container_, nodes);
}

void SceneTreePanel::CreateNodeRecursive(Rml::Element* parent, const std::vector<SceneNode>& nodes)
{
    for (const auto& node : nodes)
    {
        // 创建节点元素
        Rml::Element* node_element = document_->CreateElement("div");
        node_element->SetClass("tree-node");
        node_element->SetAttribute("data-node-id", node.id);
        
        // 节点图标
        Rml::Element* icon = document_->CreateElement("span");
        icon->SetClass("tree-icon");
        icon->SetInnerRML(node.icon);
        node_element->AppendChild(icon);
        
        // 节点名称
        Rml::Element* label = document_->Element->CreateElement("span");
        label->SetClass("tree-label");
        label->SetInnerRML(node.name);
        node_element->AppendChild(label);
        
        // 折叠/展开按钮
        Rml::Element* expand_btn = document_->CreateElement("span");
        expand_btn->SetClass("tree-expand");
        expand_btn->SetInnerRML(node.has_children ? "+" : " ");
        expand_btn->SetAttribute("data-node-id", node.id);
        expand_btn->AddEventListener(Rml::EventId::Click, this);
        node_element->AppendChild(expand_btn);
        
        parent->AppendChild(node_element);
        
        // 保存引用
        node_elements_[node.id] = node_element;
        
        // 递归创建子节点
        if (!node.children.empty())
        {
            Rml::Element* children_container = document_->CreateElement("div");
            children_container->SetClass("tree-children");
            children_container->SetAttribute("data-node-id", node.id);
            children_container->SetDisplay("none");  // 默认折叠
            
            CreateNodeRecursive(children_container, node.children);
            node_element->AppendChild(children_container);
        }
    }
}

void SceneTreePanel::ProcessEvent(Rml::Event& event)
{
    if (event.GetId() == Rml::EventId::Click)
    {
        Rml::Element* target = event.GetTargetElement();
        
        // 处理节点点击
        if (target->HasAttribute("data-node-id"))
        {
            std::string node_id = target->GetAttribute("data-node-id")->Get<std::string>();
            OnNodeSelected(node_id);
        }
        // 处理折叠/展开按钮
        else if (target->HasAttribute("data-node-id") && 
                 target->GetClass() == "tree-expand")
        {
            std::string node_id = target->GetAttribute("data-node-id")->Get<std::string>();
            std::string current_text = target->GetInnerRML();
            
            if (current_text == "+")
            {
                OnNodeExpanded(node_id);
            }
            else
            {
                OnNodeCollapsed(node_id);
            }
        }
    }
}

UI 文档实现

4.1 主界面布局

xml
<rml>
<head>
    <title>2D关卡编辑器</title>
    <link type="text/rcss" href="ui/main_window.rcss"/>
</head>
<body>
    <div class="editor-window">
        <!-- 菜单栏 -->
        <nav id="menu-bar" class="menu-bar">
            <div class="menu-item menu-file">
                <span>文件(F)</span>
                <div class="menu-dropdown">
                    <div class="menu-item" data-action="new">新建</div>
                    <div class="menu-item" data-action="open">打开...</div>
                    <div class="menu-separator"></div>
                    <div class="menu-item" data-action="save">保存</div>
                    <div class="menu-item" data-action="save_as">另存为...</div>
                    <div class="menu-separator"></div>
                    <div class="menu-item" data-action="export">导出</div>
                    <div class="menu-separator"></div>
                    <div class="menu-item danger" data-action="exit">退出</div>
                </div>
            </div>
            
            <div class="menu-item menu-edit">
                <span>编辑(E)</span>
                <div class="menu-dropdown">
                    <div class="menu-item" data-action="undo">撤销</div>
                    <div class="menu-item" data-action="redo">重做</div>
                    <div class="menu-separator"></div>
                    <div class="menu-item" data-action="cut">剪切</div>
                    <div class="menu-item" data-action="copy">复制</div>
                    <div class="menu-item" data-action="paste">粘贴</div>
                    <div class="menu-item" data-action="delete">删除</div>
                    <div class="menu-separator"></div>
                    <div class="menu-item" data-action="select_all">全选</div>
                </div>
            </div>
            
            <div class="menu-item menu-view">
                <span>视图(V)</span>
                <div class="menu-dropdown">
                    <div class="menu-item" data-action="zoom_in">放大</div>
                    <div class="menu-item" data-action="zoom_out">缩小</div>
                    <div class="menu-item" data-action="zoom_fit">适应窗口</div>
                    <div class="menu-item" data-action="zoom_100">100%</div>
                    <div class="menu-separator"></div>
                    <div class="menu-item" data-action="toggle_grid">显示网格</div>
                    <div class="menu-item" data-action="toggle_snap">开启/关闭对齐</div>
                    <div class="menu-item" data-action="toggle_rulers">显示标尺</div>
                </div>
            </div>
            
            <div class="menu-item menu-help">
                <span>帮助(H)</span>
                <div class="menu-dropdown">
                    <div class="menu-item" data-action="documentation">文档</div>
                    <div class="menu-item" data-action="shortcuts">快捷键</div>
                    <div class="menu-item" data-action="about">关于</div>
                </div>
            </div>
        </nav>
        
        <!-- 工具栏 -->
        <div id="tool-bar" class="tool-bar">
            <div class="tool-group">
                <button id="tool-select" class="tool-button active" data-tool="select">
                    <span class="tool-icon">⬚</span>
                    <span class="tool-label">选择</span>
                </button>
                <button id="tool-move" class="tool-button" data-tool="move">
                    <span class="tool-icon">✥</span>
                    <span class="tool-label">移动</span>
                </button>
                <button id="tool-rotate" class="tool-button" data-tool="rotate">
                    <span class="tool-icon">🔄</span>
                    <span class="tool-label">旋转</span>
                </button>
                <button id="tool-scale" class="tool-button" data-tool="scale">
                    <span class="tool-icon">📐</span>
                    <span class="tool-label">缩放</span>
                </button>
            </div>
            
            <div class="tool-separator"></div>
            
            <div class="tool-group">
                <button id="tool-brush" class="tool-button" data-tool="brush">
                    <span class="tool-icon">🖌️</span>
                    <span class="tool-label">画笔</span>
                </button>
                <button id="tool-eraser" class="tool-button" data-tool="eraser">
                    <span class="tool-icon">🧹</span>
                    <span class="tool-label">橡皮擦</span>
                </button>
                <button id="tool-fill" class="tool-button" data-tool="fill">
                    <span class="tool-icon">🪣</span>
                    <span class="tool-label">填充</span>
                </button>
                <button id="tool-text" class="tool-button" data-tool="text">
                    <span class="tool-icon">📝</span>
                    <span class="tool-label">文本</span>
                </button>
            </div>
            
            <div class="tool-separator"></div>
            
            <div class="tool-group">
                <button id="tool-snap" class="tool-button toggle-button" data-toggle="false">
                    <span class="tool-icon">🧲</span>
                    <span class="tool-label">对齐</span>
                </button>
                <button id="tool-grid" class="tool-button toggle-button" data-toggle="false">
                    <span class="tool-icon">⊞</span>
                    <span class="tool-label">网格</span>
                </button>
                <button id="tool-rulers" class="tool-button toggle-button" data-toggle="false">
                    <span class="tool-icon">📏</span>
                    <span class="tool-label">标尺</span>
                </button>
            </div>
            
            <div class="tool-separator"></div>
            
            <div class="tool-group">
                <button id="tool-undo" class="tool-button">
                    <span class="tool-icon">↶</span>
                    <span class="tool-label">撤销</span>
                </button>
                <button id="tool-redo" class="tool-button">
                    <span class="tool-icon">↷</span>
                    <span class="tool-label">重做</span>
                </button>
            </div>
        </div>
        
        <!-- 主内容区 -->
        <div id="main-content" class="main-content">
            <!-- 左侧面板 -->
            <div id="left-panels" class="left-panels">
                <div id="scene-tree-panel" class="panel panel-left">
                    <div class="panel-header">
                        <h3>场景树</h3>
                        <div class="panel-buttons">
                            <button class="panel-btn" data-action="expand_all">展开全部</button>
                            <button class="panel-btn" data-action="collapse_all">折叠全部</button>
                        </div>
                    </div>
                    <div id="scene-tree" class="panel-content scene-tree"></div>
                </div>
                
                <div id="layer-panel" class="panel panel-left">
                    <div class="panel-header">
                        <h3>层级</h3>
                        <div class="panel-buttons">
                            <button class="panel-btn" data-action="add_layer">+ 添加图层</button>
                            <button class="panel-btn" data-action="duplicate_layer">复制图层</button>
                        </div>
                    </div>
                    <div id="layer-list" class="panel-content layer-list"></div>
                </div>
            </div>
            
            <!-- 中央工作区 -->
            <div id="workspace" class="workspace">
                <div id="canvas-container" class="canvas-container">
                    <canvas id="editor-canvas"></canvas>
                    <div id="grid-overlay" class="grid-overlay" style="display: none;"></div>
                    <div id="rulers" class="rulers" style="display: none;">
                        <div class="ruler-horizontal"></div>
                        <div class="ruler-vertical"></div>
                    </div>
                </div>
            </div>
            
            <!-- 右侧面板 -->
            <div id="right-panels" class="right-panels">
                <div id="property-panel" class="panel panel-right">
                    <div class="panel-header">
                        <h3>属性</h3>
                        <div class="panel-buttons">
                            <button class="panel-btn" data-action="reset_properties">重置</button>
                        </div>
                    </div>
                    <div id="properties" class="panel-content properties">
                        <div class="no-selection">
                            <span>🎯 未选择对象</span>
                        </div>
                    </div>
                </div>
                
                <div id="toolbox-panel" class="panel panel-right">
                    <div class="panel-header">
                        <h3>工具箱</h3>
                    </div>
                    <div id="toolbox" class="panel-content toolbox">
                        <div class="toolbox-category">
                            <h4>基本</h4>
                            <div class="toolbox-item" data-object-type="rectangle">矩形</div>
                            <div class="toolbox-item" data-object-type="circle">圆形</div>
                            <div class="toolbox-item" data-object-type="line">直线</div>
                            <div class="toolbox-item" data-object-type="text">文本</div>
                        </div>
                        <div class="toolbox-category">
                            <h4>装饰</h4>
                            <div class="toolbox-item" data-object-type="star">星星</div>
                            <div class="toolbox-item" data-object-type="polygon">多边形</div>
                            <div class="toolbox-item" data-object-type="bezier">贝塞尔曲线</div>
                        </div>
                        <div class="toolbox-category">
                            <h4>UI元素</h4>
                            <div class="toolbox-item" data-object-type="button">按钮</div>
                            <div class="toolbox-item" data-object-type="input">输入框</div>
                            <div class="toolbox-item" data-object-type="image">图片</div>
                        </div>
                    </div>
                </div>
                
                <div id="asset-browser-panel" class="panel panel-right">
                    <div class="panel-header">
                        <h3>资源浏览器</h3>
                        <div class="panel-buttons">
                            <button class="panel-btn" data-action="refresh">刷新</button>
                            <button class="panel-btn" data-action="import">导入</button>
                        </div>
                    </div>
                    <div id="asset-browser" class="panel-content asset-browser">
                        <div class="asset-filter">
                            <input type="text" id="asset-search" placeholder="搜索资源..."/>
                        </div>
                        <div id="asset-list" class="asset-list"></div>
                    </div>
                </div>
            </div>
            
            <!-- 底部面板 -->
            <div id="bottom-panels" class="bottom-panels">
                <div id="log-panel" class="panel panel-bottom">
                    <div class="panel-header">
                        <h3>日志</h3>
                        <div class="panel-buttons">
                            <button class="panel-btn" data-action="clear">清空</button>
                            <button class="panel-btn" data-action="copy">复制</button>
                        </div>
                    </div>
                    <div id="log-output" class="panel-content log-output"></div>
                </div>
            </div>
        </div>
        
        <!-- 状态栏 -->
        <footer id="status-bar" class="status-bar">
            <div class="status-left">
                <span id="status-info">就绪</span>
            </div>
            <div class="status-center">
                <span id="cursor-position">X: 0 Y: 0</span>
                <span class="status-separator">|</span>
                <span id="selection-info">未选择</span>
            </div>
            <div class="status-right">
                <span id="zoom-level">100%</span>
                <span class="status-separator">|</span>
                <span id="fps-counter">60 FPS</span>
                <span class="status-separator">|</span>
                <span id="auto-save-status">已自动保存</span>
            </div>
        </footer>
    </div>
</body>
</rml>

4.2 属性面板

xml
<rml>
<head>
    <title>属性面板</title>
    <link type="text/rcss" href="ui/property.rcss"/>
</head>
<body>
    <div class="panel">
        <div class="panel-header">
            <h3>属性</h3>
            <div class="panel-buttons">
                <button id="btn-reset">重置</button>
            </div>
        </div>
        
        <div class="panel-content">
            <div id="properties" class="properties">
                <div class="no-selection">
                    <span>🎯 未选择对象</span>
                </div>
            </div>
        </div>
    </div>
</body>
</rml>

数据模型和命令系统

5.1 命令系统

cpp
// CommandManager.h
#pragma once
#include <RmlUi/Core/Types.h>
#include <functional>
#include <vector>
#include <memory>

class ICommand
{
public:
    virtual ~ICommand() = default;
    
    virtual void Execute() = 0;
    virtual void Undo() = 0;
    virtual std::string GetDescription() const = 0;
};

class CommandManager
{
public:
    static CommandManager& Instance()
    {
        static CommandManager instance;
        return instance;
    }

    // 命令执行
    void ExecuteCommand(std::unique_ptr<ICommand> command);
    void Undo();
    void Redo();
    
    // 状态查询
    bool CanUndo() const;
    bool CanRedo() const;
    std::string GetUndoDescription() const;
    std::string GetRedoDescription() const;

private:
    std::vector<std::unique_ptr<ICommand>> undo_stack_;
    std::vector<std::unique_ptr<ICommand>> redo_stack_;
    int current_index_;
};

// CommandManager.cpp
void CommandManager::ExecuteCommand(std::unique_ptr<ICommand> command)
{
    // 如果不在栈顶,删除之后的命令
    if (current_index_ < (int)undo_stack_.size() - 1)
    {
        redo_stack_.clear();
    }
    
    // 执行命令
    command->Execute();
    
    // 添加到撤销栈
    undo_stack_.push_back(std::move(command));
    current_index_++;
    
    // 清空重做栈
    redo_stack_.clear();
}

void CommandManager::Undo()
{
    if (!CanUndo())
        return;
    
    // 撤销当前命令
    undo_stack_[current_index_]->Undo();
    current_index_--;
    
    // 将命令移动到重做栈
    redo_stack_.push_back(std::move(undo_stack_[current_index_ + 1]));
}

void CommandManager::Redo()
{
    if (!CanRedo())
        return;
    
    // 重做上一个命令
    redo_stack_.back()->Execute();
    current_index_++;
    
    // 将命令移回撤销栈
    undo_stack_[current_index_ - 1] = std::move(redo_stack_.back());
    redo_stack_.pop_back();
}

5.2 具体命令实现

cpp
// MoveObjectCommand.h
#pragma once
#include "ICommand.h"
#include <string>
#include <RmlUi/Vector2f.h>

class MoveObjectCommand : public ICommand
{
public:
    MoveObjectCommand(const std::string& object_id, 
                   const Rml::Vector2f& from, 
                   const Rml::Vector2f& to);
    
    void Execute() override;
    void Undo() override;
    std::string GetDescription() const override;

private:
    std::string object_id_;
    Rml::Vector2f from_;
    Rml::Vector2f to_;
};

// MoveObjectCommand.cpp
MoveObjectCommand::MoveObjectCommand(
    const std::string& object_id,
    const Rml::Vector2f& from,
    const Rml::Vector2f& to)
    : object_id_(object_id), from_(from), to_(to)
{
}

void MoveObjectCommand::Execute()
{
    auto& object = GetObject(object_id_);
    object->SetPosition(to_);
}

void MoveObjectCommand::Undo()
{
    auto& object = GetObject(object_id_);
    object->SetPosition(from_);
}

std::string MoveObjectCommand::GetDescription() const
{
    return "移动对象 " + object_id_;
}

// SetPropertyCommand.h
#pragma once
#include "ICommand.h"
#include <string>
#include <variant>

class SetPropertyCommand : public ICommand
{
public:
    SetPropertyCommand(
        const std::string& object_id,
        const std::string& property,
        const std::variant<float, int, std::string, Rml::Vector2f>& from,
        const std::variant<float, int, std::string, Rml::Vector2f>& to);
    
    void Execute() override;
    void Undo() override;
    std::string GetDescription() const override;

private:
    std::string object_id_;
    std::string property_;
    std::variant<float, int, std::string, Rml::Vector2f> from_;
    std::variant<float, int, std::string, Rml::Vector2f> to_;
};

// SetPropertyCommand.cpp
SetPropertyCommand::SetPropertyCommand(
    const std::string& object_id,
    const std::string& property,
    const std::variant<float, int, std::string, Rml::Vector2f>& from,
    const std::varint<float, int, std::string, Rml::Vector2f>& to)
    : object_id_(object_id), property_(property), from_(from), to_(to)
{
}

void SetPropertyCommand::Execute()
{
    auto& object = GetObject(object_id_);
    object->SetProperty(property_, to_);
}

void SetPropertyCommand::Undo()
{
    auto& object = GetObject(object_id_);
    object->SetProperty(property_, from_);
}

std::string SetPropertyCommand::GetDescription() const
{
    return "设置 " + property_ + " 属性";
}

样式系统

6.1 编辑器主题

css
/* ui/main_window.rcss */
:root {
    /* 编辑器颜色 */
    --editor-bg: #1e1e1e1;
    --editor-surface: #252526;
    --editor-border: #3e3e3e;
    --editor-text: #e0e0e0;
    --editor-text-dim: #888;
    --editor-accent: #61dafb;
    --editor-selection: #264f78;
    
    /* 面板颜色 */
    --panel-bg: #2d2d2d;
    --panel-header: #383838;
    --panel-border: #3e3e3e;
    
    /* 状态栏颜色 */
    --status-bg: #2d2d2d;
    --status-text: #888;
    
    /* 工具栏颜色 */
    --toolbar-bg: #252526;
    --toolbar-border: #3e3e3e;
    
    /* 网格颜色 */
    --grid-color: rgba(255, 255, 255, 0.05);
    --ruler-color: rgba(255, 255, 255, 0.3);
    
    /* 字体 */
    --font-ui: 'Segoe UI', 'Roboto', Arial, sans-serif;
    --font-mono: 'Consolas', 'Courier New', monospace;
    
    /* 尺寸 */
    --menu-height: 28px;
    --toolbar-height: 40px;
    --status-height: 24px;
}

body {
    background: var(--editor-bg);
    color: var(--editor-text);
    font-family: var(--font-ui);
    font-size: 13px;
    margin: 0;
    padding: 0;
    overflow: hidden;
}

.editor-window {
    display: flex;
    flex-direction: column;
    height: 100vh;
}

/* 菜单栏 */
.menu-bar {
    display: flex;
    height: var(--menu-height);
    background: var(--toolbar-bg);
    border-bottom: 1px solid var(--editor-border);
}

.menu-item {
    position: relative;
    padding: 0 15px;
    height: 100%;
    display: flex;
    align-items: center;
    cursor: pointer;
    user-select: none;
}

.menu-item:hover {
    background: var(--panel-header);
}

.menu-dropdown {
    position: absolute;
    top: 100%;
    left: 0;
    background: var(--editor-surface);
    border: 1px solid var(--editor-border);
    min-width: 180px;
    display: none;
    z-index: 1000;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}

.menu-item:hover .menu-dropdown {
    display: block;
}

.menu-item .menu-dropdown {
    padding: 4px 0;
}

.menu-item .menu-item {
    padding: 6px 12px;
}

.menu-item .menu-separator {
    height: 1px;
    background: var(--editor-border);
    margin: 4px 0;
}

.menu-item .menu-item:hover {
    background: var(--editor-accent);
    color: var(--editor-bg);
}

/* 工具栏 */
.tool-bar {
    display: flex;
    height: var(--toolbar-height);
    background: var(--toolbar-bg);
    border-bottom: 1px solid var(--editor-border);
    padding: 0 10px;
    gap: 4px;
    align-items: center;
}

.tool-group {
    display: flex;
    gap: 4px;
}

.tool-separator {
    width: 1px;
    height: 24px;
    background: var(--editor-border);
    margin: 0 4px;
}

.tool-button {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    background: transparent;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    color: var(--editor-text);
    transition: background 0.1s;
}

.tool-button:hover {
    background: var(--panel-header);
}

.tool-button.active {
    background: var(--editor-accent);
    color: var(--editor-bg);
}

.tool-icon {
    font-size: 16px;
    line-height: 1;
}

.tool-label {
    font-size: 10px;
    margin-top: 2px;
}

/* 主内容区 */
.main-content {
    flex: 1;
    display: flex;
    overflow: hidden;
}

/* 面板系统 */
.left-panels,
.right-panels {
    display: flex;
    flex-direction: column;
    background: var(--editor-surface);
    border: 1px solid var(--editor-border);
    z-index: 10;
}

.left-panels {
    border-right: 1px solid var--editor-border);
}

.right-panels {
    border-left: 1px solid var(--editor-border);
}

.panel {
    display: flex;
    flex-direction: column;
    border-bottom: 1px solid var(--editor-border);
}

.panel:last-child {
    border-bottom: none;
}

.panel-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 12px;
    background: var(--panel-header);
    border-bottom: 1px solid var(--editor-border);
}

.panel-header h3 {
    margin: 0;
    font-size: 12px;
    font-weight: 500;
    color: var(--editor-text);
}

.panel-buttons {
    display: flex;
    gap: 4px;
}

.panel-btn {
    background: transparent;
    border: none;
    color: var(--editor-text-dim);
    cursor: pointer;
    font-size: 12px;
    padding: 2px 6px;
    border-radius: 2px;
}

.panel-btn:hover {
    background: var(--editor-accent);
    color: var(--editor-bg);
}

.panel-content {
    flex: 1;
    overflow: auto;
    padding: 8px;
}

/* 工作区 */
.workspace {
    flex: 1;
    background: var(--editor-bg);
    position: relative;
    overflow: hidden;
}

.canvas-container {
    width: 100%;
    height: 100%;
    position: relative;
}

#editor-canvas {
    width: 100%;
    height: 100%;
    display: block;
}

.grid-overlay {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    pointer-events: none;
    background-image: 
        linear-gradient(var(--grid-color) 1px, transparent 1px),
        linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
    background-size: 20px 20px;
    background-position: 0 0, 0 0;
}

/* 场景树 */
.scene-tree {
    font-family: var(--font-ui);
    font-size: 12px;
}

.tree-node {
    display: flex;
    align-items: center;
    padding: 2px 4px;
    cursor: pointer;
    user-select: none;
}

.tree-node:hover {
    background: var(--panel-header);
}

.tree-node.selected {
    background: var(--editor-selection);
}

.tree-icon {
    margin-right: 6px;
    font-size: 14px;
}

.tree-label {
    flex: 1;
}

.tree-expand {
    cursor: pointer;
    margin-right: 4px;
}

.tree-children {
    padding-left: 20px;
    display: none;
}

.tree-children.expanded {
    display: block;
}

完整实现

7.1 主程序

cpp
// main.cpp
#include "EditorApp.h"
#include <RmlUi/Core.h>
#include <RmlUi_Backend.h>

int main()
{
    const int window_width = 1920;
    const int window_height = 1080;

    // 初始化后端
    if (!Backend::Initialize("2D Level Editor", window_width, window_height, true))
        return -1;

    // 设置接口
    Rml::SetSystemInterface(Backend::GetSystemInterface());
    Rml::SetRenderInterface(Backend::GetRenderInterface());

    // 初始化 RmlUi
    Rml::Initialise();

    // 创建 Context
    Rml::Context* context = Rml::CreateContext("editor", Rml::Vector2i(window_width, window_height));
    if (!context)
    {
        Rml::Shutdown();
        Backend::Shutdown();
        return -1;
    }

    // 加载字体
    Rml::LoadFontFace("data/fonts/SegoeUI-Regular.ttf");
    Rml::LoadFontFace("data/fonts/SegoeUI-Bold.ttf");
    Rml::LoadFontFace("data/fonts/Consolas.ttf");

    // 初始化编辑器应用
    EditorApp app;
    if (!app.Initialize(context))
    {
        Rml::Shutdown();
        Backend::Shutdown();
        return -1;
    }

    // 主循环
    bool running = true;
    while (running)
    {
        running = Backend::ProcessEvents(context, nullptr, true);
        
        app.Update();
        app.Render();
    }

    // 清理
    app.Shutdown();
    Rml::Shutdown();
    Backend::Shutdown();

    return 0;
}

7.2 编辑器应用

cpp
// EditorApp.h
#pragma once
#include <RmlUi/Core/Context.h>
#include <memory>

class EditorUIManager;

class EditorApp
{
public:
    EditorApp();
    ~EditorApp();

    bool Initialize(Rml::Context* context);
    void Shutdown();

    void Update(float delta_time);
    void Render();

    // 编辑器事件
    void OnNewScene();
    void OnOpenScene();
    void OnSaveScene();
    void OnExportScene();
    
    void OnUndo();
    void OnRedo();
    
    void OnObjectSelected(const std::string& object_id);
    void OnObjectModified(const std::string& object_id);

    // 获取UI管理器
    EditorUIManager* GetUIManager() { return ui_manager_.get(); }

private:
    Rml::Context* context_;
    std::unique_ptr<EditorUIManager> ui_manager_;
    
    // 编辑器状态
    bool is_scene_open_;
    std::string current_scene_path_;
    bool is_modified_;
    
    // 编辑器数据
    std::unique_ptr<SceneData> scene_data_;
    std::unique_ptr<CommandManager> command_manager_;
};

// EditorApp.cpp
EditorApp::EditorApp()
    : context_(nullptr), is_scene_open_(false), is_modified_(false)
{
}

bool EditorApp::Initialize(Rml::Context* context)
{
    context_ = context;
    
    // 创建UI管理器
    ui_manager_ = std::make_unique<EditorUIManager>(this);
    if (!ui_manager_->Initialize(context))
        return false;
    
    // 创建命令管理器
    command_manager_ = std::make_unique<CommandManager>();
    
    // 加载空场景
    OnNewScene();
    
    return true;
}

void EditorApp::Update(float delta_time)
{
    ui_manager_->Update(delta_time);
    
    // 自动保存检查
    if (is_modified_ && ShouldAutoSave())
    {
        OnSaveScene();
    }
}

void EditorApp::Render()
{
    Backend::BeginFrame();
    
    // 渲染编辑器场景
    RenderScene();
    
    // 渲染UI
    context_->Render();
    
    Backend::PresentFrame();
}

void EditorApp::OnNewScene()
{
    is_scene_open_ = true;
    current_scene_path_ = "";
    is_modified_ = false;
    
    // 创建新场景数据
    scene_data_ = std::make_unique<SceneData>();
    scene_data_->Initialize();
    
    // 更新UI
    ui_manager_->UpdateSceneTree(scene_data_->GetSceneNodes());
    ui_manager_->UpdateTitle("新建场景");
    
    // 清空撤销/重做栈
    command_manager_->Clear();
    
    Rml::Log::Message(Rml::Log::LT_INFO, "Created new scene");
}

void EditorApp::OnObjectSelected(const std::string& object_id)
{
    // 更新属性面板
    ui_manager_->UpdatePropertyPanel(object_id);
    
    // 更新状态栏
    ui_manager_->UpdateSelectionInfo(object_id);
}

void EditorApp::OnObjectModified(const std::string& object_id)
{
    is_modified_ = true;
    
    // 更新UI
    ui_manager_->UpdatePropertyPanel(object_id);
    
    // 创建命令
    // ...
}

项目总结

8.1 实现的功能

完整的编辑器UI系统

  • 主窗口(菜单栏、工具栏、状态栏)
  • 可停靠面板系统
  • 场景树视图
  • 属性检查器
  • 工具箱
  • 日志输出窗口
  • 资源浏览器

高级编辑器功能

  • 撤销/重做系统
  • 多文档支持
  • 自动保存
  • 快捷键系统
  • 主题切换

深度定制

  • 自定义面板组件
  • 可停靠系统
  • 拖拽支持
  • 实时预览
  • 命令模式

8.2 与游戏UI的对比

特性游戏UI编辑器UI
界面类型游戏化、沉浸式工具化、专业性
主要需求视觉效果、流畅动画功能性、效率
布局风格固定布局可停靠、可调整
交互方式简单直观快捷键丰富
性能要求实时响应响应快速即可

8.3 代码统计

  • 总文件数: 40+
  • 代码行数: 6000+ 行
  • C++文件: 20+
  • RML文件: 8+
  • RCSS文件: 8+
  • JSON配置: 6+

学习价值

通过这个编辑器UI项目,你应该学习到:

  1. 不同的UI设计思路
  2. 复杂的面板系统实现
  3. 可停靠系统的架构
  4. 命令模式的实际应用
  5. 工具类应用的UI设计

🎯 项目对比

游戏UI vs 编辑器UI

方面游戏UI编辑器UI
重点视觉效果、沉浸感功能性、效率
布局固定布局可停靠、可调整
交互简单直观快捷键丰富
性能实时响应响应快速即可
复杂度中等较高

两种UI类型都展示了:

  • ✅ RmlUi 的强大功能
  • ✅ 架构设计的重要性
  • ✅ 深度定制的价值
  • ✅ 生产级代码的必要性

🎊 实战项目完成

现在你有了两个完整的实战项目:

  1. 游戏UI系统 - 展示沉浸式、动画丰富的UI
  2. 编辑器UI系统 - 展示工具化、功能强大的UI

这两个项目展示了 RmlUi 在不同应用场景下的强大能力。你现在可以根据自己的需求,选择合适的架构模式和设计风格!

继续探索,构建属于你的完美UI系统!

基于 MIT 许可发布