IMGUI(即时模式 GUI)
IMGUI 是 GUI、GUILayout、EditorGUI 这套“每个事件都重新调用一次绘制代码”的界面框架。它主要用在大量编辑器窗口和 OnGUI 中,也是 UI Toolkit 中 IMGUIContainer 的内容。本页解释一次 IMGUI 处理周期的结构:原生如何开始和结束 GUI、GUILayout 的两遍布局、控件 ID 的约束,以及 IMGUI 如何嵌入 UI Toolkit。
父页面:编辑器架构 · 相关:窗口系统、UI Toolkit
代码位置:IMGUI 核心是引擎模块 Modules/IMGUI,被编译进 UnityEngine,运行时游戏中的 OnGUI 也使用它;编辑器扩展控件位于 Editor/Mono/EditorGUI.cs、EditorGUILayout.cs 等文件。
相关源码文件#
Modules/IMGUI/GUIUtility.cs、GUIUtility.bindings.csModules/IMGUI/GUILayoutUtility.cs、LayoutGroup.cs、LayoutEntry.csModules/IMGUI/GUI.cs、GUILayout.csModules/UIElements/Core/IMGUIContainer.cs
1. 一次 GUI 处理周期#
证据:BeginGUI 记录皮肤模式和所属对象 ID,重置全局状态(皮肤、guiIsExiting、GUI.changed、滚动视图状态),并在使用布局时调用 GUILayoutUtility.Begin GUIUtility.cs:301-314、GUIUtility.cs:366-373。EndGUI 在 Layout 事件中执行布局计算,选回所属对象的布局缓存,清理 GUIContent 静态缓存,并在 finally 中退出 GUI GUIUtility.cs:330-347。“原生调用 OnGUI”这一步在 C# 中看不到,是推断。
异常路径:EndGUIFromException 退出 GUI,并由 ShouldRethrowException 决定是否重抛 GUIUtility.cs:349-356;IsExitGUIException 会剥开 TargetInvocationException,以识别通过反射调用时包装起来的 ExitGUIException GUIUtility.cs:375-380。所属对象销毁时,DestroyGUI 移除它的布局缓存 GUIUtility.cs:316-320。
2. GUILayout 的两遍模型#
GUILayout 自动布局依赖同一段 OnGUI 代码至少运行两次:
- Layout 事件:
GUILayoutUtility.Begin新建顶层GUILayoutGroup并清空分组栈,控件调用只登记尺寸需求。EndGUI随后调用GUILayoutUtility.Layout()计算矩形GUILayoutUtility.cs:153-170。 - 其它事件(Repaint、输入):
Begin复用缓存中的顶层分组和分组栈,控件按登记顺序取回已计算的矩形GUILayoutUtility.cs:164-169。
布局缓存按所属对象的 EntityId 存在 s_StoredLayouts 中,窗口内的 GUILayout.Window 则按窗口 ID 存在 s_StoredWindows 中 GUILayoutUtility.cs:86-138。
控件 ID 与 ExitGUI#
GetControlID 系列为控件分配 ID,可以用提示值(如 GUIContent.hash)帮助在不同事件之间匹配同一个控件 GUIUtility.cs:106-150。ExitGUI 的文档说明了核心约束:在一次 GUI 循环中,每个事件请求控件 ID 的顺序必须相同,并且事件循环不能重入。如果某个值的变化会改变接下来显示哪些控件,就应调用 ExitGUI 结束本轮,否则可能出现“Getting control 0's position in a group with only 0 controls”这类错误 GUIUtility.cs:266-276。
ExitGUI 总是抛出 ExitGUIException,异常构造函数会设置 guiIsExiting = true GUIUtility.cs:28-46。UI Toolkit 的事件分发器会捕获它,并在处理完队列后重新抛出(见 事件分发)。
3. 在 UI Toolkit 中运行 IMGUI#
IMGUIContainer 是一个 VisualElement,内部运行一段 IMGUI 代码:
- 每个容器有自己的
GUILayoutUtility.LayoutCacheIMGUIContainer.cs:91-99。 DoOnGUI在调用处理函数前记录GUIClip数量和guiDepth,用于调用后的完整性检查IMGUIContainer.cs:287-300。BeginContainerGUI开始容器 GUI,并通过GUILayoutUtility.BeginContainer(cache)切换到容器自己的布局缓存IMGUIContainer.cs:1106-1135。BeginContainer的两遍逻辑与Begin相同,只是始终使用传入的缓存GUILayoutUtility.cs:172-188。
窗口系统 中说明过,每个编辑器窗口的 OnGUI 都运行在面板根部的这样一个容器中。因此,编辑器中的 IMGUI 事件实际上先进入 UI Toolkit 的事件分发,再由容器转为 IMGUI 处理周期。
4. 局限#
GUI.cs(约 323 KB)和GUILayout.cs(约 284 KB)包含大量控件实现,本页没有逐个分析。- 运行时游戏中
OnGUI的调度在原生侧;PlayerLoop 中有PreUpdate.IMGUISendQueuedEvents和PostLateUpdate.GUIClearEvents这样的阶段PlayerLoop.bindings.cs:171、PlayerLoop.bindings.cs:316,但仓库中看不到它们具体如何调用OnGUI。