Unity UI Toolkit实战:5分钟构建游戏开始菜单,告别UGUI开发痛点

Unity UI Toolkit实战:5分钟构建游戏开始菜单,告别UGUI开发痛点

1. 项目概述:为什么选择UI Toolkit做开始菜单?

如果你是一个Unity开发者,尤其是从UGUI时代过来的,一提到做UI,脑子里可能立刻会浮现出在Hierarchy里拖拽一堆GameObject、调整RectTransform锚点、为每个按钮写OnClick事件的场景。对于游戏开始菜单这种结构相对固定、但需要精致呈现的界面,用UGUI做当然没问题,但维护和迭代起来,尤其是当美术想调整样式、程序想复用逻辑时,合并冲突和手动调整的工作量就上来了。

我最近在一个小体量的独立游戏项目中,彻底用Unity的新UI系统——UI Toolkit重构了所有运行时UI,其中就包括游戏开始菜单。实战下来,最大的感受就是:效率提升太明显了。UI Toolkit借鉴了现代Web前端(HTML/CSS/JS)的开发模式,将结构(UXML)、样式(USS)和行为(C#)分离。这意味着美术或策划可以在UI Builder里像搭积木一样设计界面、调整样式,而程序员只需要关心C#脚本里的逻辑绑定。双方工作在完全不同的文件上,从根本上避免了场景文件(.unity)的合并冲突。

对于“游戏开始菜单”这个具体需求,它通常包含Logo、开始游戏、设置、退出等几个核心按钮,可能还有背景、版本号等元素。用UI Toolkit来实现,你几乎可以在5分钟内搭出视觉框架,剩下的时间就是精细调整样式和绑定事件。这篇文章,我就带你走一遍这个流程,从零开始,用UI Toolkit创建一个美观、可交互的游戏开始菜单,并附上完整的UXML配置代码和关键C#脚本,让你能直接复制粘贴,快速应用到自己的项目中。

2. UI Toolkit核心概念与工作流解析

在动手之前,我们需要统一一下认知。UI Toolkit虽然理念先进,但如果你习惯了UGUI的“所见即所得”(直接在Scene视图里摆),初期可能会觉得有点“绕”。理解它的三个核心文件和一个工具,是高效开发的关键。

2.1 核心文件:UXML, USS, C#

UXML文件:这是UI的“骨架”或结构定义文件,类似于HTML。它用XML格式描述了界面由哪些视觉元素(VisualElement)组成,以及它们的层级关系。比如,一个<Button>标签就代表一个按钮。你不需要在Hierarchy里创建GameObject,所有结构都在这个文本文件里定义。

USS文件:这是UI的“皮肤”或样式表文件,完全对标CSS。它定义了视觉元素的外观:颜色、大小、字体、边距、背景图等。通过为元素添加类(Class)或指定其名称(Name),USS规则可以批量地、一致地控制UI样式。想全局换字体?改一个USS文件就行。

C#脚本:这是UI的“大脑”或逻辑控制器。它负责加载UXML/USS,查找界面中的特定元素(如那个叫“StartButton”的按钮),并为它们绑定回调函数(如点击事件)。UI的逻辑和表现完全解耦。

2.2 核心工具:UI Builder

这是Unity编辑器内置的可视化编辑工具(Window > UI Toolkit > UI Builder)。你可以把它想象成Unity的“Dreamweaver”或一个简化的前端IDE。在UI Builder里,你可以:

  1. 可视化编辑:直接拖拽控件(如Button, Label, VisualElement)来搭建界面,实时预览效果。
  2. 编辑UXML和USS:左侧是层级结构(对应UXML),右侧是检视器,可以修改元素属性,并直接编辑其USS样式。
  3. 无需运行游戏:所有编辑和预览都在编辑状态下完成,极大提升了迭代速度。

2.3 与UGUI的核心差异与优势

为什么我要“折腾”新东西?因为UI Toolkit在项目协作和后期维护上优势显著:

  • 无场景依赖:UGUI的Canvas和所有UI元素都是场景中的GameObject。UI Toolkit的界面通过UIDocument组件加载,而UIDocument只是一个指向UXML/USS文件的“引用”。UI资源本身是独立的资产文件。
  • 样式与逻辑分离:美术调整按钮颜色、大小,只需要改USS文件,完全不用碰C#代码。程序增加新功能,也基本不用关心UXML的具体结构,只需要通过名称或类去查找元素。
  • 性能潜力:对于复杂UI,UI Toolkit的绘制合批效率理论上更高,因为它基于更底层的即时模式(Immediate Mode)GUI系统构建,但采用了保留模式(Retained Mode)的API。不过对于简单的开始菜单,性能差异感知不强,主要是开发体验的提升。

3. 5分钟快速搭建:开始菜单视觉框架

理论说再多不如动手。我们现在就打开Unity(建议使用2021 LTS或更新版本),在5分钟内把开始菜单的静态界面搭出来。

3.1 创建UI Document与样式表

  1. 创建UXML文件:在Project窗口右键,选择Create > UI Toolkit > UI Document,命名为MainMenu.uxml。这创建了我们的界面结构文件。
  2. 创建USS文件:同样右键,选择Create > UI Toolkit > StyleSheet,命名为MainMenuStyles.uss。这将是我们的样式文件。
  3. 打开UI Builder:菜单栏选择Window > UI Toolkit > UI Builder。将MainMenu.uxml文件拖入UI Builder窗口,或者点击UI Builder顶部的“Open”按钮选择它。

现在,你的UI Builder应该分为三栏:左侧是“Library”(控件库)和“Hierarchy”(层级视图),中间是“Viewport”(实时预览),右侧是“Inspector”(属性检视器)和“StyleSheets”(样式表)。

3.2 使用UI Builder拖拽布局

我们的开始菜单结构很简单:一个全屏背景,上面居中放置一个容器,容器内有游戏Logo、几个垂直排列的按钮。

  1. 添加根VisualElement:在Hierarchy中,默认有一个<ui:UXML>根节点。我们选中它,在Inspector中将其“Name”设置为MainMenuRoot。这不是必须的,但给根元素起名是好习惯。
  2. 设置全屏背景
    • 从Library的“Controls”中,拖拽一个VisualElement到Hierarchy的MainMenuRoot下。
    • 选中这个新的VisualElement,在Inspector的“StyleSheet”面板,点击“+”号,选择“New USS Rule”。这会创建一个新的样式规则并自动添加到MainMenuStyles.uss
    • 在弹窗中,我们可以用“Selector”来命名这个规则。为了语义化,我们输入.background(注意前面的点,表示这是一个类选择器)。点击“Create”。
    • 现在,在Inspector的“Styles”标签页,我们就可以为这个元素添加样式了。找到“Background”折叠栏,设置Background Color为一个深色(如RGBA: 0.1, 0.1, 0.2, 1)。然后找到“Size”折叠栏,将WidthHeight都设置为100%,即100%。这样它就铺满全屏了。
  3. 创建居中菜单容器
    • 在背景VisualElement下,再拖入一个VisualElement。选中它,同样创建一个新的USS规则,命名为.menu-container
    • 关键步骤来了:我们要让它居中。在“Styles”标签页,找到“Layout”下的Position,设置为Absolute。然后设置Left,Top,Right,Bottom都为0。这会让它相对于父元素(全屏背景)的四个边距都为0,即填满父元素。
    • 接着,找到“Flex”折叠栏(UI Toolkit默认使用Flexbox布局)。将Justify Content设置为Center(主轴居中),Align Items设置为Center(交叉轴居中)。这样,这个容器内部的所有子元素都会在水平和垂直方向居中。
  4. 添加Logo和按钮
    • .menu-container内,先拖入一个Label,将其“Text”属性改为“MY AWESOME GAME”,创建一个USS规则.game-title,设置一个较大的字体(如font-size: 48px)和醒目的颜色。
    • 接着,拖入一个VisualElement作为按钮的垂直容器。为其创建规则.button-group,在“Flex”栏将Flex Direction设置为Column(垂直排列)。
    • 最后,向.button-group内拖入三个Button控件。依次选中它们,在Inspector顶部的“Name”字段(不是USS类)分别命名为:StartButton,SettingsButton,QuitButton。同时,修改它们的“Text”属性为“开始游戏”、“设置”、“退出游戏”。

至此,一个最基础的开始菜单视觉框架就完成了。整个过程几乎就是拖拽和点选,你可以在Viewport里实时看到效果。如果觉得按钮太丑,别急,下一步我们就用USS来美化它。

注意:在UI Builder中为元素设置“Name”非常重要。这是后续在C#脚本中查找并绑定该元素的主要依据。USS的“Class”主要用于样式控制,虽然也能用于查找,但“Name”的优先级更高、更精确。

4. 深度美化:使用USS编写专业级样式

默认的Unity按钮样式比较简陋。我们要让开始菜单看起来像那么回事,就得靠USS。USS的语法和CSS高度相似,学习成本很低。

4.1 编写基础按钮样式

我们不想给每个按钮单独写样式,而是定义一个通用的按钮样式类,然后让所有按钮都应用它。

  1. 在UI Builder右侧的“StyleSheets”面板,你应该能看到我们之前创建的MainMenuStyles.uss。选中它,下方会显示其内容。你也可以在Project窗口中双击这个文件,用任何文本编辑器打开。
  2. 我们添加一个通用按钮样式。在文件末尾添加:
    /* 通用按钮基础样式 */ .menu-button { width: 200px; height: 50px; margin-top: 15px; margin-bottom: 15px; background-color: rgba(30, 30, 46, 0.8); border-left-color: rgb(70, 130, 180); border-top-color: rgb(70, 130, 180); border-right-color: rgb(30, 60, 90); border-bottom-color: rgb(30, 60, 90); border-width: 2px; border-radius: 5px; color: rgb(220, 220, 220); font-size: 18px; -unity-font-style: bold; -unity-text-align: middle-center; }
    这段代码定义了一个名为.menu-button的类。它设置了按钮的尺寸、间距(margin)、背景色、边框(模拟一点立体感)、圆角、文字颜色和大小。
  3. 应用样式:回到UI Builder,在Hierarchy中按住Ctrl键多选三个按钮。在Inspector的“StyleSheet”面板,点击“Add existing USS Class”,然后输入menu-button(不需要前面的点)。你会发现三个按钮瞬间变样了。

4.2 添加交互状态样式(悬停、点击)

好的UI需要有反馈。我们为按钮添加悬停和点击时的样式变化。

MainMenuStyles.uss中继续添加:

/* 按钮悬停状态 */ .menu-button:hover { background-color: rgba(50, 50, 70, 0.9); border-left-color: rgb(100, 160, 210); border-top-color: rgb(100, 160, 210); color: rgb(255, 255, 255); scale: 1.05; transition-property: background-color, border-color, color, scale; transition-duration: 0.2s; } /* 按钮点击(激活)状态 */ .menu-button:active { background-color: rgba(70, 70, 100, 1); border-color: rgb(120, 180, 230); scale: 0.98; }

这里用到了伪类选择器:hover:active。当鼠标悬停在有.menu-button类的元素上时,会应用:hover里的样式;点击时则应用:active样式。transition-*属性用于创建平滑的动画效果。

4.3 为特定按钮添加特殊样式

比如,我们想将“退出游戏”按钮设置为红色以示警告。我们可以通过组合选择器来实现。

首先,确保“QuitButton”已经按之前步骤命名了(Name属性)。然后在USS文件中添加:

/* 专门针对名为QuitButton的菜单按钮 */ .menu-button#QuitButton { background-color: rgba(80, 30, 30, 0.8); border-left-color: rgb(180, 70, 70); border-top-color: rgb(180, 70, 70); border-right-color: rgb(90, 30, 30); border-bottom-color: rgb(90, 30, 30); } .menu-button#QuitButton:hover { background-color: rgba(110, 40, 40, 0.9); border-left-color: rgb(210, 100, 100); border-top-color: rgb(210, 100, 100); }

选择器.menu-button#QuitButton的意思是:同时拥有.menu-button类和id(在UI Toolkit中对应元素的Name属性)为QuitButton的元素。这个规则的优先级会比单纯的.menu-button高,因此能覆盖基础样式,实现特殊化。

实操心得:USS选择器的优先级规则和CSS一致:内联样式(直接在元素上写的) > ID选择器 > 类选择器 > 类型选择器(如Button)。合理利用类和ID的组合,可以构建出非常灵活且易于维护的样式系统。建议将通用样式定义为类(如.menu-button),将针对单个元素的特殊样式通过ID来覆盖。

5. 注入灵魂:C#脚本逻辑绑定与事件处理

界面好看了,但点按钮还没反应。现在我们来编写C#脚本,让按钮“活”起来。

5.1 创建UI文档控制器脚本

在Project中创建一个C#脚本,命名为MainMenuController.cs

using UnityEngine; using UnityEngine.UIElements; public class MainMenuController : MonoBehaviour { // 序列化字段,用于在Inspector中绑定UIDocument组件 [SerializeField] private UIDocument m_MenuDocument; // 持有界面元素的引用 private Button m_StartButton; private Button m_SettingsButton; private Button m_QuitButton; private void OnEnable() { // 获取根视觉元素 var root = m_MenuDocument.rootVisualElement; // 通过名称查询按钮元素 m_StartButton = root.Q<Button>("StartButton"); m_SettingsButton = root.Q<Button>("SettingsButton"); m_QuitButton = root.Q<Button>("QuitButton"); // 检查元素是否成功找到(避免空引用) if (m_StartButton == null) Debug.LogError("StartButton not found!"); if (m_SettingsButton == null) Debug.LogError("SettingsButton not found!"); if (m_QuitButton == null) Debug.LogError("QuitButton not found!"); // 注册按钮点击事件回调 m_StartButton.clicked += OnStartButtonClicked; m_SettingsButton.clicked += OnSettingsButtonClicked; m_QuitButton.clicked += OnQuitButtonClicked; } private void OnDisable() { // 非常重要!在脚本禁用或对象销毁时,取消注册事件,防止内存泄漏 if (m_StartButton != null) m_StartButton.clicked -= OnStartButtonClicked; if (m_SettingsButton != null) m_SettingsButton.clicked -= OnSettingsButtonClicked; if (m_QuitButton != null) m_QuitButton.clicked -= OnQuitButtonClicked; } private void OnStartButtonClicked() { Debug.Log("开始游戏按钮被点击!"); // 在这里加载游戏场景 // SceneManager.LoadScene("GameScene"); } private void OnSettingsButtonClicked() { Debug.Log("设置按钮被点击!"); // 在这里打开设置面板,可以是一个新的UXML界面 // 例如:m_SettingsPanel.RemoveFromClassList("hidden"); } private void OnQuitButtonClicked() { Debug.Log("退出游戏按钮被点击!"); #if UNITY_EDITOR UnityEditor.EditorApplication.isPlaying = false; #else Application.Quit(); #endif } }

代码解析

  1. root.Q<T>(name):这是UI Toolkit中最常用的元素查询方法。QQuery的简写,<Button>指定了要查找的元素类型,"StartButton"是我们在UI Builder中为按钮设置的Name。这个方法会返回找到的第一个匹配元素。
  2. clicked事件:Button元素有一个clickable属性,其下的clicked事件用于响应点击。使用+=来注册回调方法。
  3. 事件注销:在OnDisable中注销事件是至关重要的好习惯。如果不这样做,当这个UI被禁用或销毁,但事件回调仍然被持有,就可能引发空引用异常或内存泄漏。
  4. 平台编译指令:在退出游戏的部分,我们使用了#if UNITY_EDITOR。这是因为在Unity编辑器中Application.Quit()无效,我们需要用EditorApplication.isPlaying = false来停止播放模式。

5.2 在场景中组装并测试

  1. 在场景中创建一个空GameObject,命名为“UI - Main Menu”。
  2. 选中这个GameObject,在Inspector中点击“Add Component”,添加一个UIDocument组件。
  3. 将我们之前创建的MainMenu.uxml拖拽到UIDocument组件的“Source Asset”栏。
  4. UIDocument组件的“Panel Settings”处,可以创建一个新的Panel Settings Asset(Create > UI Toolkit > Panel Settings),用于全局控制此UI的缩放模式、渲染纹理等。对于简单的开始菜单,使用默认设置通常即可。
  5. 继续为这个GameObject添加我们刚写的MainMenuController组件。
  6. 将GameObject身上的UIDocument组件拖拽到MainMenuController的“Menu Document”字段进行绑定。
  7. 点击Play运行游戏。你应该能看到我们设计的开始菜单,并且点击按钮会在Console中看到对应的日志输出。

6. 完整UXML配置与结构解析

经过UI Builder的可视化编辑,我们的MainMenu.uxml文件已经自动生成。理解它的结构有助于我们进行更高级的定制或调试。以下是经过简化和注释的典型结构:

<?xml version="1.0" encoding="utf-8"?> <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False"> <!-- 根元素,通常用于挂载样式表或作为查询起点 --> <ui:VisualElement name="MainMenuRoot"> <!-- 全屏背景 --> <ui:VisualElement class="background"> <!-- 居中菜单容器,使用Flexbox实现居中 --> <ui:VisualElement class="menu-container"> <!-- 游戏标题 --> <ui:Label text="MY AWESOME GAME" class="game-title" /> <!-- 按钮组容器,垂直排列 --> <ui:VisualElement class="button-group"> <!-- 开始游戏按钮,注意其name属性用于C#查找 --> <ui:Button text="开始游戏" name="StartButton" class="menu-button" /> <!-- 设置按钮 --> <ui:Button text="设置" name="SettingsButton" class="menu-button" /> <!-- 退出按钮,除了通用类,还应用了特定ID样式 --> <ui:Button text="退出游戏" name="QuitButton" class="menu-button" /> </ui:VisualElement> </ui:VisualElement> </ui:VisualElement> </ui:VisualElement> </ui:UXML>

关键点解析

  • <ui:VisualElement>:这是所有UI元素的基础,相当于一个<div>。可以用于布局容器或简单的图形元素。
  • <ui:Button>/<ui:Label>:预定义的控件,带有默认的行为和样式。
  • name属性:这是元素的唯一标识符(在同一个UIDocument根下),C#脚本通过它来查找元素。务必保持唯一性
  • class属性:用于关联USS样式表中的类选择器。一个元素可以有多个类,用空格分隔,例如class="menu-button highlight"
  • 层级结构:UXML的嵌套结构直接决定了UI的渲染层级和布局关系。父元素(如.button-group)的布局属性(如flex-direction: column)会直接影响其所有子元素。

7. 进阶技巧与常见问题排查

掌握了基础流程后,下面这些技巧和坑点能让你用得更顺手。

7.1 响应式布局与屏幕适配

UI Toolkit默认使用Flexbox布局,这本身对响应式就很友好。但为了完美适配不同分辨率,你还需要配置Panel Settings

  1. 创建Panel Settings:在Project中Create > UI Toolkit > Panel Settings。将其赋值给场景中UIDocument组件的“Panel Settings”字段。
  2. 配置缩放模式:在Panel Settings资产中,找到“Scale Mode”:
    • Constant Pixel Size:UI始终保持相同像素大小。不适合多分辨率。
    • Scale With Screen Size最常用。根据参考分辨率缩放。你需要设置一个“Reference Resolution”(如1920x1080)。当屏幕大于或小于这个分辨率时,UI会按比例缩放。
    • Constant Physical Size:根据屏幕DPI调整,用于需要精确物理尺寸的应用(如AR/VR)。
  3. 在USS中使用相对单位:尽量使用百分比(%)、视口单位(vw,vh)或flex-grow等弹性属性,而非固定像素(px)。例如,让背景容器始终占满屏幕:width: 100%; height: 100%;

7.2 动态加载与UI管理

一个游戏不可能只有一个开始菜单界面。如何管理多个UI的切换?

推荐模式:使用单例或管理器创建一个UIManager单例类,负责所有UI面板的加载、显示、隐藏和销毁。

public class UIManager : MonoBehaviour { public static UIManager Instance { get; private set; } [SerializeField] private UIDocument m_MainMenuDocument; [SerializeField] private UIDocument m_SettingsDocument; // ... 其他UI文档 private VisualElement m_Root; private void Awake() { if (Instance != null && Instance != this) Destroy(gameObject); else Instance = this; m_Root = GetComponent<UIDocument>().rootVisualElement; // 默认隐藏所有子UI,只显示主菜单 HideAllPanels(); ShowPanel(m_MainMenuDocument.rootVisualElement); } public void ShowMainMenu() => ShowPanel(m_MainMenuDocument.rootVisualElement); public void ShowSettings() => ShowPanel(m_SettingsDocument.rootVisualElement); private void ShowPanel(VisualElement panel) { HideAllPanels(); panel.RemoveFromClassList("hidden"); // 假设有一个.hidden类用于隐藏 } private void HideAllPanels() { m_MainMenuDocument.rootVisualElement.AddToClassList("hidden"); m_SettingsDocument.rootVisualElement.AddToClassList("hidden"); // ... } }

在USS中定义.hidden { display: none; }。通过添加或移除这个类,可以轻松控制UI的显示和隐藏。

7.3 常见问题与解决方案速查表

问题现象可能原因解决方案
按钮点击无反应,Console无错误1. C#脚本中元素Name拼写错误。
2. 事件回调未正确注册(脚本未启用或OnEnable未执行)。
3. 按钮被其他透明元素遮挡。
1. 检查UI Builder中按钮的Name属性与C#代码中的字符串是否完全一致(区分大小写)。
2. 在OnEnable方法开始处加Debug.Log,确认脚本已启用。检查UIDocument组件是否已正确赋值。
3. 在UI Builder中检查层级,确保按钮在最上层。或检查按钮及父元素的picking-mode是否为Position(默认)而非Ignore
UI在Game视图不显示1. 场景中UIDocument组件的“Source Asset”未赋值。
2.UIDocument所在的GameObject被禁用。
3. Camera的渲染层未包含UI层(如果使用World Space渲染)。
1. 确认MainMenu.uxml已拖入UIDocument的对应栏位。
2. 检查Hierarchy中该GameObject的激活复选框。
3. 对于Screen Space渲染通常没问题。对于World Space,检查Camera的Culling Mask。
样式(USS)未生效1. USS文件未关联到UXML或UIDocument
2. USS选择器写错或优先级被覆盖。
3. 样式属性不被支持。
1. 在UI Builder的“StyleSheets”面板确认USS文件已添加。或在UIDocument组件的“Style Sheets”列表中添加。
2. 使用UI Toolkit Debugger(Window > UI Toolkit > Debugger)选中元素,查看其最终计算的样式,检查哪个规则生效了。
3. 并非所有CSS属性都被支持。查阅Unity官方USS属性文档。
UI布局错乱1. Flexbox布局属性设置不当。
2. 元素尺寸使用了冲突的单位或计算。
3. 父容器没有设置明确尺寸。
1. 理解Flexbox主轴、交叉轴概念。在UI Builder中多尝试Flex Direction,Justify Content,Align Items等属性。
2. 避免同时设置width: 100%和固定的left/right值。优先使用Flex属性(flex-grow,flex-shrink)。
3. 确保包含子元素的容器有明确的尺寸(百分比或固定值),否则子元素可能无法正确布局。
性能问题(复杂UI卡顿)1. 过度复杂的嵌套层级。
2. 频繁的动态样式更改(如每帧修改颜色)。
3. 使用了高分辨率背景图且未优化。
1. 简化UXML结构,减少不必要的嵌套VisualElement
2. 避免在Update中修改样式。使用事件驱动或缓存引用。
3. 确保UI使用的纹理格式为Sprite(2D and UI)并合理压缩,启用Mipmaps对于会缩放的UI也有帮助。

7.4 从UGUI迁移的思维转换

如果你有丰富的UGUI经验,迁移到UI Toolkit需要转变几个思维:

  • 从GameObject到VisualElement:不再有成千上万的GameObject在Hierarchy里,所有东西都在UXML文件里。
  • 从RectTransform到Flexbox:忘掉锚点和轴心点,拥抱flex-direction,justify-content,align-items。一开始可能不习惯,但一旦掌握,布局效率极高。
  • 从Image/Text组件到USS样式:不再需要给每个UI元素挂载一堆组件,大部分外观都在USS里统一控制。
  • 从OnClick()到事件回调:UGUI的按钮事件在Inspector里拖拽,UI Toolkit需要在C#里用代码注册。虽然少了点“可视化”,但更清晰、易于版本管理。

我个人在实际项目中的体会是,对于新项目或需要大规模重构UI的老项目,花一点时间学习UI Toolkit是绝对值得的。它带来的协作流畅度和维护便利性,在项目后期会节省大量时间。尤其是那个“5分钟搞定界面框架,剩下时间精细调整”的体验,一旦习惯就回不去了。最后一个小技巧:多使用UI Toolkit Debugger,它是你排查UI问题的“火眼金睛”,能清晰展示整个视觉树、应用的样式及其优先级,是学习UI Toolkit工作原理的绝佳工具。