4. IMGUI控件详解:Label、Button、TextField、Slider、Toggle等

好,咱们今天来聊聊IMGUI里最常用的那几个控件。说实话,IMGUI这套东西,你只要把基础控件玩熟了,编辑器工具开发就成功了一大半。我个人习惯是把这些控件分成两类:展示类交互类。Label属于前者,剩下的Button、TextField、Slider、Toggle都属于后者。

4.1 Label:最朴素的文本展示

Label说白了就是一段只读文本。你不能点它,不能改它,它就安安静静待在那儿给你看信息。

// 最简单的用法
GUILayout.Label("这是一个标签");

// 带样式的标签
GUILayout.Label("红色警告", GUILayout.Width(200));

// 用GUIStyle定制
GUIStyle style = new GUIStyle();
style.fontSize = 16;
style.normal.textColor = Color.blue;
GUILayout.Label("蓝色大号文字", style);

我在项目中遇到过一个问题:Label的文字太长,把界面撑得乱七八糟。后来我养成了一个习惯——能用GUILayout.Width限制宽度的,绝不偷懒。你想想看,用户拖拽窗口大小的时候,Label自动换行,那个效果有多丑。

小技巧:如果你想让Label自动换行,可以用GUILayout.ExpandWidth(true)配合固定宽度。或者直接用EditorGUILayout.LabelField,它自带自动换行。

4.2 Button:点击触发逻辑

Button是交互的起点。它的返回值是bool,点击的那一帧返回true。

// 普通按钮
if (GUILayout.Button("保存配置"))
{
    Debug.Log("用户点击了保存");
    SaveConfig();
}

// 带图标的按钮
if (GUILayout.Button(EditorGUIUtility.IconContent("d_SaveActive"), GUILayout.Width(30)))
{
    // 用内置图标
}

// 工具栏风格按钮
if (GUILayout.Button("刷新", EditorStyles.toolbarButton))
{
    RefreshData();
}

嗯,这里要注意:Button的点击事件只在那一帧触发。我曾经见过新手把耗时操作直接写在if里面,结果界面卡死。正确的做法是——点击后启动协程,或者用EditorApplication.delayCall延迟执行。

避坑指南:我曾经在OnGUI里写了一个死循环,因为Button点击后调用了某个会重新触发OnGUI的方法。结果Unity直接崩溃。记住:不要在Button回调里做任何会立即触发OnGUI重绘的操作

4.3 TextField:文本输入

TextField需要你手动管理字符串变量。它返回的是用户输入后的新字符串。

private string playerName = "默认名称";

void OnGUI()
{
    // 单行文本输入
    playerName = GUILayout.TextField(playerName, 20); // 最大20字符

    // 带标签的输入框
    playerName = EditorGUILayout.TextField("玩家名称", playerName);

    // 密码输入
    string password = GUILayout.PasswordField(password, '*');
}

为什么TextField要返回新字符串?因为IMGUI是无状态的,它不保存任何控件的内部状态。每次OnGUI调用时,你都得把当前值传进去,它再把修改后的值返回给你。说白了就是——你自己管好自己的数据

我在做资源命名工具时,用TextField让用户输入前缀。有个坑是:用户输入中文时,字符数计算会出问题。后来我改用System.Text.Encoding.UTF8.GetByteCount()来限制字节数,而不是字符数。

4.4 Slider:数值范围调节

Slider适合让用户在一个范围内调整数值。比如音量、速度、缩放比例等。

private float volume = 0.5f;

void OnGUI()
{
    // 水平滑动条
    volume = GUILayout.HorizontalSlider(volume, 0f, 1f);

    // 带数值显示的滑动条
    GUILayout.BeginHorizontal();
    GUILayout.Label("音量", GUILayout.Width(50));
    volume = GUILayout.HorizontalSlider(volume, 0f, 1f);
    GUILayout.Label(Mathf.RoundToInt(volume * 100) + "%", GUILayout.Width(40));
    GUILayout.EndHorizontal();

    // EditorGUILayout的快捷方式
    volume = EditorGUILayout.Slider("音量", volume, 0f, 1f);
}

我个人习惯用EditorGUILayout.Slider,因为它自带Label和数值显示,省得自己拼布局。但如果你需要自定义样式,那就老老实实用GUILayout.HorizontalSlider

核心要点:Slider的第三个参数是当前值,第四第五是范围。每次拖动都会连续触发OnGUI,所以如果你的逻辑很重,记得做防抖处理。

4.5 Toggle:开关切换

Toggle就是布尔值的开关。它返回的是切换后的bool值。

private bool isEnabled = true;

void OnGUI()
{
    // 普通开关
    isEnabled = GUILayout.Toggle(isEnabled, "启用功能");

    // 带图标的开关
    isEnabled = GUILayout.Toggle(isEnabled, EditorGUIUtility.IconContent("d_Toggle"));

    // EditorGUILayout风格
    isEnabled = EditorGUILayout.Toggle("自动保存", isEnabled);

    // ToggleGroup(单选)
    int selected = 0;
    string[] options = {"选项A", "选项B", "选项C"};
    selected = GUILayout.SelectionGrid(selected, options, 3);
}

Toggle有个兄弟叫ToggleGroup,用来做单选。但说实话,我更喜欢用GUILayout.SelectionGrid或者EditorGUILayout.EnumPopup来做单选,代码更清晰。

我曾经犯过一个错误:在Toggle的点击事件里修改了其他控件的状态,导致界面闪烁。后来发现是因为Toggle状态变化触发了OnGUI重绘,重绘时又修改了状态,形成了死循环。解决方案是用一个标志位延迟处理。

4.6 综合示例:一个简单的配置面板

来,咱们把上面这些控件串起来,做一个简单的工具配置面板。

public class ToolConfigWindow : EditorWindow
{
    private string folderPath = "Assets/";
    private float scale = 1.0f;
    private bool autoBackup = true;
    private int qualityLevel = 0;

    [MenuItem("Tools/配置面板")]
    static void OpenWindow()
    {
        GetWindow<ToolConfigWindow>("工具配置");
    }

    void OnGUI()
    {
        GUILayout.Label("基础设置", EditorStyles.boldLabel);

        folderPath = EditorGUILayout.TextField("输出路径", folderPath);
        scale = EditorGUILayout.Slider("缩放比例", scale, 0.1f, 5f);
        autoBackup = EditorGUILayout.Toggle("自动备份", autoBackup);

        GUILayout.Space(10);
        GUILayout.Label("质量设置", EditorStyles.boldLabel);

        string[] qualities = {"低", "中", "高"};
        qualityLevel = GUILayout.SelectionGrid(qualityLevel, qualities, 3);

        GUILayout.Space(10);

        if (GUILayout.Button("应用设置"))
        {
            ApplyConfig();
        }
    }

    void ApplyConfig()
    {
        Debug.Log($"路径:{folderPath},缩放:{scale},备份:{autoBackup},质量:{qualityLevel}");
    }
}

这个例子涵盖了Label、TextField、Slider、Toggle、SelectionGrid和Button。你想想看,是不是大部分工具界面都能用这些控件拼出来?

4.7 控件选择速查表

控件 用途 返回值 常用场景
Label 展示文本 标题、说明、状态提示
Button 触发操作 bool(点击帧为true) 保存、刷新、执行
TextField 文本输入 string 路径、名称、参数
Slider 数值调节 float 音量、速度、大小
Toggle 开关切换 bool 启用/禁用、开关选项

嗯,这一章的内容就到这儿。记住一句话:IMGUI控件的核心就是“传值-接收返回值”这个循环。你只要理解了这一点,剩下的就是熟悉每个控件的参数和样式了。下一章咱们会深入讲布局,到时候你会发现,控件本身不难,难的是怎么把它们摆得好看又好用。