UE5 UI 模块简介(参考 Lyra)
1. 基本框架(CommonUI + CommonGame 分层)
- 大致UI框架如上图所示(简化了部分),最顶层的 Layout 由 GameUISubSystem 驱动创建,创建的时机是 LocalPlayer 被添加的时候,具体由 GameUIPolicy 来执行
1 | void UGameUIPolicy::NotifyPlayerAdded(UCommonLocalPlayer* LocalPlayer) |
- Layout 里面具体定义哪些层级由 GameUIPolicy的LayoutClass 决定,可自行配置,我参考的是 lyra 项目定义的四层结构,每层的含义如下:
Game—— 游戏界面元素,例如 HUD。GameMenu—— 专用于游戏过程的“菜单”,例如游戏内的物品栏界面。Menu—— 常规菜单界面,例如设置画面。Modal—— 模态对话框,例如确认提示框或错误提示框。
- LayoutClass 本身是一个 CommonUserWidget, 如下图:
- 每个 Layer 是一个
CommonActivatableWidgetStack,是一个栈结构,每次 push / pop 只激活栈顶的ActivatableWidget。四个 Layer 之间,越靠下,优先级越高,Modal 优先级最高,Game 优先级最低,优先级高的 Layer 会优先拦截输入
1.1 为什么要做 Layer 分层?
CommonUI 之所以要把 UI 拆成多个 Layer,本质上是把 UI 的 空间顺序、输入优先级、生命周期、平台行为 四个维度统一抽象为一个 GameplayTag 驱动的声明式系统,避免传统 UMG 中各种手动管理 ZOrder / Focus / Visibility / InputMode 带来的混乱问题。具体价值体现在以下几个方面:
Z-Order(绘制顺序)的确定性管理
- 传统 UMG 中通过
AddToViewport(ZOrder)手动指定数值,多个模块各自约定数值时极易冲突(比如 A 模块和 B 模块都用了 ZOrder=100)。 - Layer 机制下,每个 Layer 在 Layout 中按顺序排布,越靠后的 Layer ZOrder 越高,Modal 永远盖在 Menu 之上,Menu 永远盖在 Game 之上,无需手动协调。
- 传统 UMG 中通过
输入路由(Input Routing)与焦点管理
- 这是 Layer 分层最核心的价值,也是 UMG 原生最薄弱的部分。
UCommonUIActionRouterBase依据 Layer 决定:当前应使用哪种 Input Mode、哪个 Widget 接收按键、是否捕获鼠标、是否显示光标。- Modal Layer 推入新 Widget 时,焦点会自动从下层 Layer 转移过来;Modal 关闭后焦点自动归还,完全消除「点击穿透」「焦点丢失」这一类 Bug。
生命周期与激活栈(Activation Stack)
- 每个 Layer 对应一个
CommonActivatableWidgetStack,遵循 栈语义(LIFO):Push 时旧 Widget Deactivate(保留实例)、新 Widget Activate;Pop 时新 Widget 销毁、旧 Widget 重新 Activate。 - 这意味着「主菜单 → 设置 → 音频设置 → 按 B 返回」这种多级菜单导航天然支持,不必重建上一级页面,性能更优、状态自动保留。
- 每个 Layer 对应一个
平台无关的输入抽象(PC / 主机 / 移动端)
- CommonUI 诞生于《堡垒之夜》的跨平台需求,Layer 在这里承担了「不同界面层使用不同输入模式」的职责。
- 例如
GameLayer 用ECommonInputMode::Game(输入透传给角色),MenuLayer 用ECommonInputMode::Menu(输入只给 UI),框架根据当前最顶层 Layer 自动切换,无需手写一堆if (IsController) ... else ...。
流程控制的天然隔离
- 加载界面、网络断连提示、确认弹窗这类 必须打断一切其他交互 的 UI,天然属于最顶层的 Modal Layer。
- 推入 Modal Layer 后,下层 Game / Menu 的输入会被自动屏蔽,无需写任何「禁用其他 UI」的额外逻辑。
可以用一个对照表直观地看出 Layer 解决了哪些痛点:
| 痛点 | 传统 UMG | CommonUI + Layer |
|---|---|---|
| Z-Order 冲突 | 手动协调数字 | Layer Tag 天然有序 |
| 输入穿透 | 需手动 SetVisibility(HitTestInvisible) |
自动屏蔽下层 |
| 焦点丢失(手柄) | 手动 SetUserFocus,常出 Bug |
框架自动管理 |
| 返回上一级菜单 | 自己实现栈 | 内置 Stack 语义 |
| Modal 弹窗 | 自己写遮罩 + 禁用按钮 | Push 到 Modal Layer 即可 |
| 跨平台输入切换 | 大量 if/else | InputMode 声明式配置 |
2. Widget 类型及其应用场景
- UE 中 UI widget 有三种类型,UCommonActivatableWidget,UCommonUserWidget和UUserWidget, 三者的关系是:UCommonActivatableWidget 继承自 UCommonUserWidget,而 UCommonUserWidget 继承自 UUserWidget。
下面是它们的特点介绍和具体应用场景:
UUserWidget(最基础的 UMG 原生类)- 概念:它是 UE 引擎最原始、最基础的 UI 蓝图父类。所有你在 UE 里右键创建的普通 Widget Blueprint 默认都是它。
- 特点:
- “傻瓜式”渲染: 它只负责显示图片、文字,播放简单的 UI 动画。
- 缺乏系统性输入管理: 它接收点击、悬停,但不知道什么是“手柄导航”、什么是“全局返回键”、什么是“UI层级”。
- 无状态管理: 只有“创建(Construct)”和“销毁(Destruct)”,没有“激活”和“挂起”的概念。
- 应用场景:
- 不需要复杂交互的 HUD: 屏幕上的血条、小地图、准星、得分显示。
- 非 CommonUI 项目: 如果你的项目是简单的手机游戏,或者纯鼠标点击的 PC 游戏,完全不需要引入 CommonUI,直接全部用 UUserWidget 即可。
- 极简的子控件: 一个纯展示用的底板背景、一条纯文本的分隔线。
UCommonUserWidget(CommonUI 的基础组件)- 概念: 它是 CommonUI 插件提供的一个扩展基类。它本质上还是一个普通控件,但它“听得懂” CommonUI 系统的语言。它的核心价值是接入了 CommonUI 的 Input Action 路由系统 (
UCommonUIActionRouterBase),并把若干本地玩家级子系统的访问入口封装成了便捷 getter。 - 特点:
- 内置 CommonUI 输入路由接入: 通过
RegisterUIActionBinding(...)可声明式地绑定一个FDataTableRowHandle(指向CommonInputActions数据表的某一行),由UCommonUIActionRouterBase根据当前输入设备自动分发按键事件,并可联动底部 Action Bar 的按键提示。这套机制UUserWidget不具备。 - 便捷访问 CommonUI 子系统: 直接提供
GetInputSubsystem()、GetActionRouter()、以及重写后返回UCommonLocalPlayer*的GetOwningLocalPlayer<T>()等便捷接口,省去手写GetGameInstance()->GetSubsystem<...>()的样板代码。 - 额外的输入提示开关: 暴露
bDisplayInputActionWhenNotInteractable、bAlwaysListenForInputAction等属性,控制 Action 提示在控件不可交互时是否仍显示、绑定是否在隐藏后仍持续监听。 - 不拦截输入、无激活/失活生命周期: 它依然是一个“零件”,没有独占输入的能力,必须依附于更大的页面(典型情况下是某个
UCommonActivatableWidget)。
- 内置 CommonUI 输入路由接入: 通过
- 关于“感知输入设备切换”的澄清: 设备切换检测本身由
UCommonInputSubsystem提供(一个ULocalPlayerSubsystem),任何能拿到LocalPlayer的UUserWidget也能订阅它的OnInputMethodChangedNative委托来响应键鼠↔手柄切换。UCommonUserWidget只是把访问入口封装为GetInputSubsystem(),并非独有能力。 - 关于“样式系统”的澄清: CommonUI 的 Style 资产(
UCommonButtonStyle、UCommonTextStyle等)作用于UCommonButtonBase、UCommonTextBlock这类具体控件,不是UCommonUserWidget自身的能力;它本身并不持有任何 Style 资产,也不参与所谓“全局换肤”。 - 应用场景(作为子控件):
- 需要参与 Input Action 路由的功能型组件: 例如一个自定义面板,希望声明“按 X / F 接受、按 B / Esc 取消”,并自动出现在底部 Action Bar 中。
- 复杂的自定义组件: 比如“角色属性条”(包含图标、文字、进度条),其内部按钮多为
UCommonButtonBase,作为容器使用UCommonUserWidget可顺手访问ActionRouter/InputSubsystem。 - 动态提示控件: 一个提示面板,当玩家用键盘时显示 [F] 拾取,切到手柄时瞬间变成 [X] 拾取(注:此场景
UUserWidget也能做到,只是用UCommonUserWidget写起来更简洁)。
- 概念: 它是 CommonUI 插件提供的一个扩展基类。它本质上还是一个普通控件,但它“听得懂” CommonUI 系统的语言。它的核心价值是接入了 CommonUI 的 Input Action 路由系统 (
UCommonActivatableWidget(具有路由能力的完整页面/弹窗)- 概念:它是 CommonUI 的核心灵魂。它不再是一个“零件”,而是一个具有生命周期、能独占输入、能管理焦点的“完整页面”或“模态弹窗”
- 特点:
- 状态管理: 引入了 Activate (激活) 和 Deactivate (失活) 的概念。页面可以被隐藏挂起,而不是直接销毁。
- 输入模式切换:GetDesiredInputConfig() — 激活时自动切换输入模式。
- 接管焦点: 当它被 Activate 时,它可以自动把手柄/键盘焦点抓取到自己设定的默认按钮上。
- 模态支持:bIsModal — 作为输入路由的根节点,阻断父级操作。
- 自带“返回”逻辑:原生支持 Back Handler。玩家按 ESC 或手柄 B键 时,它会自动响应并关闭自己(Deactivate),而不需要你写一堆按键绑定事件。
- 配合 UI 栈 (Stack):它可以被推入(Push)到 CommonActivatableWidgetStack 中,实现像浏览器网页一样的“前进、后退”历史记录。
- 应用场景(作为大页面/弹窗):
- 全屏主菜单: 游戏开始画面的主菜单(包含开始游戏、设置、退出)。
- 暂停菜单界面 (Pause Menu): 游戏中按 ESC 弹出的菜单,激活时阻断游戏底层输入。
- 设置页面 (Settings Screen): 以及里面的“音频设置”、“画面设置”等各个大分页。
- 确认弹窗 (Modal Dialog): “确定要删除存档吗?[是/否]” —— 这种必须玩家处理完才能进行下一步的弹窗。
- 背包/角色全屏界面 (Inventory UI): 玩家打开背包时,整个屏幕的控制权交给这个界面。
推荐的层级结构基于上述三种 Widget 的特点,推荐采用 「Activatable 当页面 / Common 当组件 / User 当零件」 的分层组合方式:
1
2
3
4
5
6
7
8
9
10
11
12
13UCommonActivatableWidget (页面级根节点,提供输入路由 / 焦点 / 返回栈)
│
├── UCommonUserWidget (功能型子控件,参与 CommonUI Input Action 路由)
│ │
│ ├── UCommonUserWidget (更细粒度的子组件,例如带图标的按钮)
│ │
│ └── UUserWidget (纯展示零件,例如底板 / 分隔线 / 装饰图)
│
├── UCommonUserWidget (另一个功能型子控件,例如属性条)
│ │
│ └── UUserWidget (纯展示的进度条贴图 / 文本)
│
└── UUserWidget (页面内不需要参与输入路由的纯展示元素)分层原则:
- 页面级(顶层)必须用
UCommonActivatableWidget:负责输入独占、焦点管理、返回键处理、压栈出栈等”页面”职责。一个全屏菜单 / 弹窗 / HUD 主面板就是一个 Activatable。 - 中间组件优先用
UCommonUserWidget:当这个控件需要参与 CommonUI 的 Input Action 路由(声明式按键绑定、Action Bar 提示),或需要便捷访问ActionRouter/InputSubsystem时,使用它。注意:单纯响应“键鼠 ↔ 手柄”切换并不构成必须用它的理由——UUserWidget通过订阅UCommonInputSubsystem::OnInputMethodChangedNative同样可以做到。 - 末端纯展示控件用
UUserWidget:仅用于显示图片 / 文字 / 简单动画,不参与输入路由、不关心输入设备、不需要全局样式联动的”叶子节点”。 - 避免反向嵌套:不要把
UCommonActivatableWidget当作普通子控件嵌进UUserWidget里,否则它的输入路由和返回栈能力会失效(因为父节点不在 CommonUI 的输入路由树上)。
- 页面级(顶层)必须用
3. 输入模式的切换
- ActivatableWidget 有一个 GetDesiredInputConfig 方法用于返回当前的输入模式给输入系统,输入系统通过不同的输入模式来启用对应的 IMC(InputMappingContext)
1 | TOptional<FUIInputConfig> ULyraActivatableWidget::GetDesiredInputConfig() const |
1 | FGameplayTagContainer UCommonUIActionRouterBase::GetGameplayTagsForInputMode(const ECommonInputMode Mode) const |
- 每个 InputMode 对应一个 Tag,增强输入系统会通过当前的 tag 来对 IMC 进行过滤,IMC的InputModeFilterOptions 可以配置过滤 tag,我们可以将 GAME_IMC 的过滤 tag 配置成 TAG_InputModeGame,UI_IMC 的过滤 tag 配置成 TAG_InputModeMenu
- 对于 HUD 界面,可以将 InputMode 设置为 ALL(根据自己的需求),UI_IMC 和 GAME_IMC 同时生效,对于打开设置界面和系统界面的action是配置在UI_IMC中,按键监听操作交给 HUD 去监听处理。当系统界面打开时,会将 InputMode 设置为 Menu, 只让 UI_IMC 生效。
- UActivatableWidget 中有一个 InputConfig 属性用于设置 InputMode,直接在蓝图中配置即可,对于二级界面的 ActivatableWidget,如果不需要改变输入模式,保持默认值即可,正常情况下,输入模式只需要配置在一级界面
- 对于 UI_IMC 和 GAME_IMC 存在按键冲突的问题可以通过设置GAME_IMC的优先级更高,使得 GAME_IMC 的 action 优先触发
3.1 场景题
- 考虑到之前遇到的一个场景,道具拾取的 action 和技能释放的 action 在手柄模式可能存在按键冲突(绑在一个键上),需求上希望优先触发拾取操作,如何解决?
- 由于按键优先级覆盖操作只能以 IMC 为HoldActionHelper.h单位,可以通过为拾取action新增一个IMC,让它的优先级高于 GAME_IMC,然后在拾取 tips 出现的时候注册 IMC,隐藏的时候取消注册,从而达到目的。
- 如果拾取是用 GameplayAbilitySystem 实现的,可以通过 Tag 阻止技能触发。
- 拾取交互组件一个 State.CanInteract 的 GameplayTag。
- 在 GA_Cast 的 Activation Blocked Tags 中加入 State.CanInteract。
- 当玩家进入拾取范围时给 Pawn 加这个 Tag,离开时移除。这样按 A 时:
- GA_Cast 因为 Blocked Tag 不会激活;
- 拾取交互正常执行。
4. Widget如何监听按键输入
- CommonUI 对于按键输入的监听做了自己的封装,需要用 CommonUI 的 RegisterUIActionBinding 来实现,使用方式如下:
1 | void UHUDLayout::NativeOnInitialized() |
- 对于 FBindUIActionArgs 需要指定 InputMode 参数,InputMode 与当前的 widget 启用的 InputMode 保持一致,不一致的话会被拦截。
- 另外,CommonUI 的长按只支持了旧输入系统,RegisterUIActionBinding 之后会生成 FUIActionBinding,FUIActionBinding 有一个 HoldMappings 属性用于处理长按逻辑,对于旧输入系统 CommonUI 会从 DataTable 配置中读取长按配置,对于增强输入系统则没有支持配置读取(Enhanced Input v1.0),但是可以从代码层面手动注入配置:
- HoldActionHelper 和 HoldActionHelper
- 用法在 HoldActionHelper.h 的注释中有说明
5. CommonUI 输入路由流程
- UCommonUIActionRouterBase 是 CommonUI 输入路由系统的核心,它通过一棵 Activatable Tree(可激活树) 来维护 UCommonActivatableWidget、UCommonUserWidget、UUserWidget 之间的嵌套关系。
- FActivatableTreeNode — 树节点,每个节点持有一个 UCommonActivatableWidget 的弱引用,并拥有 Children 子节点数组和 Parent 父节点指针
- FActivatableTreeRoot — 树根节点,继承自 FActivatableTreeNode,额外追踪 LeafMostActiveNode(最深层已激活节点)
- 只有 UCommonActivatableWidget 才能成为树节点,普通 UCommonUserWidget / UUserWidget 不会成为节点,但会挂载到最近的父级 ActivatableWidget 节点上
- 树形嵌套示意图如下:
1 | UCommonUIActionRouterBase |
- 当按键输入触发时,UCommonUIActionRouterBase 会对已激活的优先级最高的 FActivatableTreeRoot 进行深度优先遍历,实现代码如下所示,子节点优先消费输入,由 IsReceivingInput 实现可知,当父界面 Deactivate 的时候,子节点无法接收输入。
1 | virtual bool IsReceivingInput() const override { return bCanReceiveInput && IsWidgetActivated(); } |
5.1 场景分析
- 父子界面是 UCommonActivatableWidget,绑定了相同的 InputAction :
1
2
3父 ActivatableWidget (Node_A) ← 绑定了 IA_Confirm
│
├── 子 ActivatableWidget (Node_B) ← 也绑定了 IA_Confirm- 子界面 Node_B 会触发回调,父界面 Node_A 不触发(前提是 Node_B 必须处于 Activated)
- 父界面是 UCommonActivatableWidget,子界面是UCommonUserWidget :
1
2
3父 ActivatableWidget (Node_A) ← 绑定了 IA_Confirm
│
├── 子 CommonUserWidget (非节点) ← 也绑定了 IA_Confirm- 两者的 bindings 都挂载在同一个 Node_A 上,按注册顺序谁先匹配谁触发,注册顺序由 OnWidgetRebuilt 的调用顺序决定——UE Widget 树的构建是自内向外(子 Widget 先 Rebuild),所以通常子 UCommonUserWidget 的 binding 先注册,因此子界面先触发。也可以通过
FBindUIActionArgs中的PriorityWithinCollection字段来设置优先级来控制调用顺序 - 如果希望父子界面都能响应输入,可以设置 BindArgs.bConsumeInput = false 来实现
- 两者的 bindings 都挂载在同一个 Node_A 上,按注册顺序谁先匹配谁触发,注册顺序由 OnWidgetRebuilt 的调用顺序决定——UE Widget 树的构建是自内向外(子 Widget 先 Rebuild),所以通常子 UCommonUserWidget 的 binding 先注册,因此子界面先触发。也可以通过
6. 手柄导航
6.1 设置默认焦点
- UCommonActivatableWidget 的 NativeGetDesiredFocusTarget 虚函数方法可用于设置 widget 被激活时的需要获取焦点的控件
- 当 UCommonActivatableWidget 被激活时,会递归找到最深的激活的 ActivatableWidget 子节点,调用子节点的 NativeGetDesiredFocusTarget 方法来设置为默认焦点
6.2 可导航控件
- 如果想让widget中的某个控件可被导航,需要将 bIsFocusable = true,CommonUI 提供了内置的可导航控件,基本上能满足开发需求。这些控件默认 bIsFocusable = true,无需额外设置
- UCommonButtonBase : 最常用,自带聚焦、悬停、选中状态
- UCommonListView / UCommonTreeView / UCommonTileView : 列表导航,自带 bSelectItemOnNavigation
- UCommonRotator : 左右选择器,覆写了 NativeOnNavigation 拦截左右方向
- UCommonActionWidget : 绑定到指定的InputAction,根据当前的输入设备自动显示按键图标
6.3 控制导航方向和边界行为
- 当 DPad/左摇杆 按下时,Slate 通过 FNavigationConfig 将输入转为 EUINavigation 方向,然后按照 搜索算法搜索下一个可聚焦控件。
- 只有Visibility为Visible状态的widget才有资格参与导航.
- 导航规则有六种,可在编辑器中配置,选中控件 → Details → Navigation 面板,可以为
Up/Down/Left/Right/Next/Previous六个方向分别设置规则:- Escape (逃逸 / 默认自动)
- 含义:这是系统默认的规则。它的意思是:“引擎,请你帮我自动计算最近的控件”。当按下方向键时,引擎会在该方向上寻找几何位置最近的可聚焦控件。如果在当前容器内找不到,焦点就会“逃逸(Escape)”到父级容器,让父级容器去寻找该方向上的下一个控件。
- 使用示例(90%的情况) :
- 垂直主菜单:你放了一个Vertical Box,里面有“开始游戏”、“设置”、“退出”三个按钮。你全部保持默认的 Escape。当在“开始游戏”上按下时,引擎自动发现下面是“设置”,焦点就顺理成章地过去了。
- Stop (停止)
- 含义:在这个方向上绝对禁止焦点移动。就像撞到了一堵空气墙,按下该方向键不会发生任何反应,焦点留在原地。
- 使用示例 :
- 防止误触出界:假设你有一个位于屏幕左侧的装备栏(Grid Panel)。为了防止玩家在最左侧的一列按下“左”键时,焦点意外跑到屏幕背后隐藏的其他 UI 上,你可以选中最左列的所有格子,把它们的 Left 导航规则强制设置为 Stop。
- Wrap (循环 / 环绕)
- 含义:当在该方向上走到尽头(没有下一个控件)时,焦点会自动“瞬移”回当前容器内相反方向的最远端。
- 使用示例 :
- 角色选择界面:你有一排水平排列的角色头像(Horizontal Box)。玩家当前选中了最右边的角色,如果他再按一次“右”,焦点会自动回到最左边的第一个角色身上。此时只需将这排按钮的最右侧控件的 Right 规则设为 Wrap(或在整个容器级别设置)。
- Explicit (显式 / 指定)
- 含义:放弃引擎的自动计算, 硬编码(手动绑定) 下一步必须去哪个具体地控件。选择此项后,下方会出现一个吸管工具或下拉框,让你直接指定目标 Widget。
- 使用示例 :
- 不对称或奇特的 UI 布局:假设你的 UI 是星系图,星球按钮散乱地分布在屏幕上。引擎的自动计算(Escape)很容易算错最近的按钮。此时你可以使用 Explicit:在地球按钮上,强行设置按下 Up 必须跳转到火星按钮,按下 Right 必须跳转到木星按钮,即使它们在几何位置上并不完全对齐。
- Custom (自定义 - 蓝图逻辑)
- 含义:将这个方向的导航权限完全交给蓝图或 C++ 代码来决定。选择后,你可以绑定一个返回值为 Widget 的函数(Delegate)。每次按下该方向键,都会执行这个函数,函数返回哪个控件,焦点就去哪个控件。
- 使用示例 :
- 带条件判断的导航:在天赋树界面中,玩家在一个节点上按下“下”。在 Custom 绑定的蓝图函数里,你写了个逻辑:如果“火球术”已经解锁,返回“大火球”按钮作为下一个焦点;如果“火球术”未解锁,返回一个带有锁图标的提示按钮作为焦点。
- Custom Boundary (自定义边界 - 蓝图逻辑)
- 含义:它和
Custom类似,都需要绑定一个蓝图函数。区别在于触发时机:Custom是只要按下方向键就触发;而Custom Boundary只有在引擎使用默认自动计算发现“这个方向上已经没有控件了,即将跨越边界(逃逸)”时,才会触发你写的蓝图逻辑。 - 使用示例 :
- 复杂的跨面板导航:屏幕左边是“背包网格”,右边是“角色装备槽”。在背包内部移动时,你希望用默认的上下左右(它会自动在格子里移动)。但是,当你处于背包的最右侧边缘格子上,再按“右”时(即将离开背包边界),触发
Custom Boundary。在蓝图里你可以写:如果现在正在比较戒指,就聚焦到右边的“戒指装备槽”;如果是武器,就聚焦到“武器装备槽”。
- 复杂的跨面板导航:屏幕左边是“背包网格”,右边是“角色装备槽”。在背包内部移动时,你希望用默认的上下左右(它会自动在格子里移动)。但是,当你处于背包的最右侧边缘格子上,再按“右”时(即将离开背包边界),触发
- 含义:它和
- Escape (逃逸 / 默认自动)
6.4 自动导航模式下的搜索算法
- 导航系统内置了两种搜索算法:
- 正交扫描:沿导航方向的矩形扫描带进行逐格扫描,找到最近的对侧边缘 Widget,简单可靠
- 零近距离:搜索锥 + Minkowski 距离,可处理非轴对齐的布局,更智能
- 默认走固定的正交扫描算法。只有开启实验模式后,Widget 上配置的 NavigationMethod 才会生效
- 开启后
评论











