【Unity UI 进阶】仿 Element UI 打造企业级 Unity UI 组件库(05)

【Unity UI 进阶】仿 Element UI 打造企业级 Unity UI 组件库(05)

环境与工具说明

说明
代码生成 本系列组件库代码由 Cursor(AI 编程助手)辅助生成与迭代,再结合工程内联调、重构落地
Unity 版本 2022.3.50f1c1(Unity 2022.3 LTS)
UI 框架 UGUI(UnityEngine.UI
示例工程 EleUnityUI(仿 Element UI 企业级组件库)

建议使用 Unity 2022.3 LTS 同系列版本打开工程。本文实现均在 2022.3.50f1c1 下验证。上篇(04)讲完 Table 的选中、列显示与弹窗联动;本篇进入独立组件 EleTabs


Tabs 标签页:图标、下划线指示、内容面板与 Table 内页签的区别

Element 的 Tabs 是「页面级分区 」:上面一排标签,下面一整块内容区,切换时只显示对应面板。EleUnityUI 的 EleTabs 按同一心智模型实现,并刻意与 Table 顶部的「筛选页签」区分开。

本篇覆盖:

  1. 图标 --- 配置 icon 键,走 EleUiIcons
  2. 下划线指示 --- 主题色横条跟随当前 Tab 滑动
  3. 内容面板 --- ContentHost/Panel_{tabCode},Prefab 里可继续塞 UI
  4. 与 Table 内页签的区别 --- 何时用 EleTabs,何时用 EleTable.showTabs

涉及文件:

能力 主要文件
运行时 Runtime/Tabs/EleTabs.cs
配置模型 Runtime/Tabs/EleTabsModels.cs
Config 资产 Runtime/Tabs/EleTabsConfigAsset.cs
示例生成 Editor/Tabs/EleTabsSampleGenerator.cs
图标 Runtime/Common/EleUiIcons.cs
Table 内页签(对比) Runtime/Tables/EleTable.Structure.csEleTableModels.csEleTableTabConfig

颜色继续走 设计变量Primary(激活文字 / 下划线 / 图标)、TextPrimary / TextRegularWhiteBorderLighter


一、整体结构(先建立全局图)

text 复制代码
EleTabs(根)
├── TabBar                    ← 标签栏(固定高度 tabBarHeight)
│   ├── TabRow                ← 横向排布 Tab 按钮
│   │   ├── Tab_user
│   │   ├── Tab_config
│   │   └── ...
│   ├── Divider               ← 底部分割线
│   └── Underline             ← 蓝色下划线指示器
└── ContentHost               ← 内容区(stretch 填满 TabBar 下方)
    ├── Panel_user            ← 仅当前 Tab 显示
    ├── Panel_config
    └── Panel_role

数据流:

text 复制代码
EleTabsConfig.tabs
    → BuildTabs() 生成按钮 + EnsurePanel()
    → SelectTab(tabCode)
        → ApplyActiveStyles()(文字/图标色 + 面板显隐)
        → MoveUnderline()(下划线对齐当前按钮)
        → OnTabChanged(tabCode, tabName)

业务侧最小接入:

csharp 复制代码
tabs.OnTabChanged += (code, name) =>
{
    Debug.Log($"切换到 {name} ({code})");
};

// 往某个面板里动态挂内容
var panel = tabs.GetPanel("config");

二、配置模型

1. 单项 EleTabItemConfig

csharp 复制代码
new EleTabItemConfig
{
    tabName = "配置管理",      // 显示文字
    tabCode = "config",        // 唯一编码 → 面板名 Panel_config
    icon = EleUiIcons.Settings,// 图标键,空=无图标
    active = true              // 初始选中(多项时取第一个 active)
}

2. 整体 EleTabsConfig

csharp 复制代码
public class EleTabsConfig
{
    public string title = "EleTabs";
    public EleTableTheme theme = EleTableTheme.ElementLight;
    public float tabBarHeight = 40f;
    public float iconSize = 14f;
    public float underlineHeight = 2f;
    public bool stretchPanels = true;
    public List<EleTabItemConfig> tabs = new List<EleTabItemConfig>();
}

示例预设(EleTabsConfig.CreateSample())包含四个 Tab:用户管理 / 配置管理 / 角色管理 / 定时任务补偿,分别对应 user / config / role / schedule 图标键。

3. 资源路径

资源 路径
Config Resources/Tabs/EleTabsSampleConfig.asset
Prefab Prefabs/Tabs/EleTabsSample.prefab

菜单:

text 复制代码
EleUnityUI → Samples → Tabs → Generate EleTabs Sample
EleUnityUI → Samples → Tabs → Spawn EleTabs Preview In Scene
GameObject → EleUnityUI → Tabs → Sample

三、图标

1. 配置方式

icon字符串键 ,由 EleUiIcons.Get(icon) 取 Sprite。常用键:

常量 典型用途
user EleUiIcons.User 用户 / 账号
settings EleUiIcons.Settings 配置
role EleUiIcons.Role 角色权限
clock EleUiIcons.Clock 定时 / 任务

键不存在或 Sprite 为空时,不画图标,只显示文字,不会报错。

2. 实现步骤

  1. CreateTabButton 里判断 item.icon 是否为空
  2. EleUiIcons.Get 成功则创建 ImagepreserveAspect = true
  3. 尺寸取 config.iconSize(默认 14)
  4. 与 Label 横向 HLayout,间距 6px,左右 padding 12
  5. 激活态:icon.color = Primary;未激活:TextRegular

3. 关键代码

csharp 复制代码
if (!string.IsNullOrEmpty(item.icon))
{
    var sp = EleUiIcons.Get(item.icon);
    if (sp != null)
    {
        iconImg = EleUiFactory.CreateImage("Icon", content, EleUiStyle.TextRegular);
        iconImg.sprite = sp;
        iconImg.preserveAspect = true;
        float s = Mathf.Max(12f, config.iconSize);
        EleUiFactory.AddFixedColumn(iconImg.gameObject, s, s);
    }
}

// ApplyActiveStyles
if (t.icon != null)
    t.icon.color = on ? EleUiStyle.Primary : EleUiStyle.TextRegular;

扩展新图标:在 EleUiIcons 增加常量 + Get() 分支 + 绘制函数即可,Tabs 侧不用改。


四、下划线指示器

Element Tabs 的标志性交互:底边主题色横条 滑到当前标签下方。EleTabs 用独立 Underline 节点实现,而不是给按钮加 Border(避免和 TabBar 底部分割线抢层级)。

1. 创建

BuildTabs 末尾:

csharp 复制代码
var underImg = EleUiFactory.CreateImage("Underline", tabBarRoot, EleUiStyle.Primary);
underImg.raycastTarget = false;
underline = underImg.rectTransform;
underline.anchorMin = new Vector2(0f, 0f);
underline.anchorMax = new Vector2(0f, 0f);
underline.pivot = new Vector2(0.5f, 0f);
underline.sizeDelta = new Vector2(48f, Mathf.Max(2f, config.underlineHeight));
underline.SetAsLastSibling();   // 盖在 Divider 上方

underlineHeight 默认 2px,工业风可略加厚。

2. 跟随当前 Tab:MoveUnderline

csharp 复制代码
void MoveUnderline(RectTransform tabBtn)
{
    Canvas.ForceUpdateCanvases();
    LayoutRebuilder.ForceRebuildLayoutImmediate(tabBarRoot);

    var world = tabBtn.TransformPoint(tabBtn.rect.center);
    var local = tabBarRoot.InverseTransformPoint(world);
    float width = Mathf.Max(32f, tabBtn.rect.width * 0.72f);
    underline.sizeDelta = new Vector2(width, Mathf.Max(2f, config.underlineHeight));
    underline.anchoredPosition = new Vector2(local.x, 0f);
}

要点:

  • 切换前先 ForceRebuildLayoutImmediate,避免 HLayout 未算完导致偏移
  • 宽度取按钮宽度的 72%,比整宽略短,更接近 Element 视觉
  • pivot 在底边中点,anchoredPosition.y = 0 贴在 TabBar 底部

3. 激活样式(文字)

csharp 复制代码
bool on = t.tabCode == currentTabCode;
var color = on ? EleUiStyle.Primary : EleUiStyle.TextPrimary;
if (t.label != null) t.label.color = color;

按钮本身用近乎透明的 Button 色块 + 轻微 Primary 悬停,高亮主要靠文字、图标和下划线,而不是整块背景变色。


五、内容面板 Panel_{tabCode}

1. 命名约定

每个 Tab 对应内容区子节点:

text 复制代码
ContentHost/Panel_{tabCode}

例如 tabCode = "config"Panel_configGetPanel("config") 就是取这个 RectTransform

2. EnsurePanel:复用优先

csharp 复制代码
RectTransform EnsurePanel(EleTabItemConfig item)
{
    var name = $"Panel_{item.tabCode}";
    var existing = contentRoot.Find(name) as RectTransform;
    if (existing != null)
    {
        EleUiFactory.Stretch(existing);
        return existing;
    }

    var panel = EleUiFactory.CreateImage(name, contentRoot, EleUiStyle.White);
    EleUiFactory.Stretch(panel.rectTransform);
    // 首次创建时加占位 Label,Prefab 里可删
    return panel.rectTransform;
}

已存在的面板不会销毁 ,你在 Prefab 里手动加的子 UI(Table、表单、图表)会在 Rebuild 后保留。

3. 显隐逻辑

csharp 复制代码
void ApplyActiveStyles()
{
    foreach (var t in _tabs)
    {
        bool on = t.tabCode == currentTabCode;
        // ...文字 / 图标色...
        if (t.panel != null) t.panel.gameObject.SetActive(on);
    }
}

一次只显示一个 Panel_*,其余 SetActive(false)。不做 Addressables / 懒加载时,所有面板在层级里同时存在,只是隐藏------简单、适合企业后台体量。

4. Prefab 工作流(推荐)

  1. Generate EleTabs Sample 生成 Prefab
  2. 打开 Prefabs/Tabs/EleTabsSample.prefab
  3. ContentHost/Panel_config 下拖入你的 UI(或挂脚本动态创建)
  4. 删掉占位 Placeholder 文本(可选)
  5. Play 时 StartRebindButtons() 保留子节点,只重绑点击
csharp 复制代码
public RectTransform GetPanel(string tabCode)
{
    foreach (var t in _tabs)
        if (t.tabCode == tabCode) return t.panel;
    return null;
}

代码里往面板塞内容:

csharp 复制代码
var rt = tabs.GetPanel("user");
var table = Instantiate(tablePrefab, rt);
EleUiFactory.Stretch(table.GetComponent<RectTransform>());

5. 重建时清理策略

ClearTabBarChildrenKeepPanels

  • 只清 TabBar 子节点(按钮、分割线、下划线会重建)
  • ContentHost 里:配置中仍存在的 Panel_{tabCode} 保留;配置删掉的 Tab 对应面板才销毁

这样改 tabs 列表后点「按 Config 重建 Tabs UI」,不会误删你辛苦摆好的面板内容。


六、与 Table 内筛选页签的区别

这是本篇最容易混淆的一点:名字都叫 Tab,职责完全不同。

1. 对照表

维度 EleTabs(独立组件) EleTable.showTabs(表内页签)
定位 页面 / 模块级分区 同一张表的数据 分类筛选
配置 EleTabsConfigEleTabItemConfigtabCode / tabName / icon EleTableConfig.tabsEleTableTabConfigkey / label
内容区 Panel_{tabCode} 整块 UI 切换 没有独立面板,始终是同一张表
切换后果 显示/隐藏面板;OnTabChanged(code, name) _activeTabKey = key回到第 1 页RequestCurrentPage()
视觉 文字 + 图标 + 滑动下划线 小按钮块,TabActive / TabIdle 背景色
典型场景 「用户管理 | 角色管理 | 系统配置」三个子页 「全部 | 进行中 | 已归档」筛同一数据源
与查询栏 无关 常与 queryBar(关键字、状态下拉)叠加使用

Config 里已写明:

csharp 复制代码
[Header("页签(表内筛选 Tab,不是独立 EleTabs)")]
public bool showTabs;
public List<EleTableTabConfig> tabs;

2. Table 内页签实现(精简)

csharp 复制代码
void BuildTabs()
{
    tabsRoot.gameObject.SetActive(config.showTabs && config.tabs.Count > 0);
    foreach (var tab in config.tabs)
    {
        bool active = tab.key == _activeTabKey;
        var btn = EleUiFactory.CreateEleButton($"Tab_{tab.key}", tabsRoot, tab.label, ...);
        var key = tab.key;
        btn.onClick.AddListener(() =>
        {
            _activeTabKey = key;
            BuildTabs();              // 刷新按钮高亮
            OnTabChanged?.Invoke(key);
            _page = 1;
            RequestCurrentPage();     // 重新拉表数据
        });
    }
}

业务在 OnTabChangedOnRequestPage 里根据 ActiveTabKey 过滤数据(Demo 里也可配合 statusFilter)。

3. 和「筛选」QueryBar 的关系

Table 还有一套 查询栏showQuery + EleTableQueryBar):

  • 页签 Tab:粗粒度分类(如业务线、单据状态组)
  • 查询栏:细粒度条件(关键字、日期、下拉枚举)
  • 状态筛选 showStatusFilters:历史 / 新增等行状态(走 statusFilter 字段)

三者可并存,但 都不会 创建 Panel_*------数据始终绑定到同一个 EleTable 实例。

4. 选型建议

text 复制代码
需要切换「整块不同 UI」     → EleTabs + 各 Panel 里放 Table / 表单 / 图表
需要切换「同一表格不同数据集」 → EleTable.showTabs + OnRequestPage 里按 key 过滤
需要临时搜几条               → queryBar,不必为此单独做 EleTabs

常见组合:外层 EleTabs 分模块,某个 Panel_* 里再放一张 EleTable(表内页签管该模块下的数据视图)。


七、API 与生命周期

API / 事件 说明
SetConfig(config, rebuild) 换配置并重建
Rebuild() / EditorRebuild() 编辑器按钮「按 Config 重建 Tabs UI」
SelectTab(tabCode, notify) 代码切换;notify=false 不抛事件
GetPanel(tabCode) 取内容面板
CurrentTabCode / CurrentTabName 当前选中
OnTabChanged (tabCode, tabName)

生命周期:

  1. AwakeApplyTheme
  2. Start:若未 Build 则 Rebuild();若 Prefab 已构建则 RebindButtons() + 恢复 currentTabCode
  3. 初始 Tab:优先 currentTabCode 序列化值 → 否则第一个 active=true → 否则列表第一项

八、落地检查清单

  1. 每个 tabCode 唯一,且与 Panel_{tabCode} 一致
  2. 图标键能在 EleUiIcons.Get 里解析
  3. 切换 Tab 时下划线是否对齐、文字/图标是否变 Primary
  4. 非当前 Panel_* 是否 SetActive(false)
  5. Prefab 里手改面板内容后,Play 是否仍在(RebindButtons 路径)
  6. 从 Config 删掉某个 Tab 后,对应孤立面板是否被清理
  7. 不要 把「筛表」需求硬塞进 EleTabs------应使用 EleTable.showTabs

验证菜单:

text 复制代码
EleUnityUI → Samples → Tabs → Generate EleTabs Sample
EleUnityUI → Samples → Tabs → Spawn EleTabs Preview In Scene

九、小结

  1. EleTabs = 标签栏 + 下划线 + 多面板,配置驱动,Panel_{tabCode} 可在 Prefab 扩展
  2. 图标EleUiIcons 键;激活态与下划线同色 Primary
  3. 下划线 独立节点 + 布局刷新后按按钮中心定位,宽度约为按钮 72%
  4. 重建保面板:只清 TabBar,ContentHost 按 tabCode 复用
  5. Table 内页签 是数据筛选,无 Panel_*,切换会 RequestCurrentPage------与 EleTabs 互补而非替代

系列回顾与下篇

内容
03 Table 上:多行表头、拖宽、排序、分页
04 Table 下:选中、斑马纹、列显示、工具栏与弹窗
05 Tabs:图标、下划线、内容面板、与表内页签区别(本文)

06|NavMenu 导航条 :1~3 级菜单、maxDepth、顶栏与内容区联动,以及和 Tabs 组合成典型后台布局。

若你在 UGUI 里做过「Tab 内容被父级 Layout 挤没高度」或「下划线 Play 才偏一格」,欢迎留言------ContentHost 用手工锚点 stretch、切换前 ForceRebuildLayoutImmediate 都是为这类坑准备的。

相关推荐
tanghonghanhaoli3 小时前
【图书翻译】有意义的游戏设计:桌面游戏的方法论与心理学(第1、2章)
前端·游戏·ui
_ZHOURUI_H_4 小时前
不做完整 ECS,只优化数据布局:Unity EasyECS 到底是什么
unity·性能优化·游戏引擎
bugcome_com5 小时前
Avalonia UI 样式进阶实战:外置样式 + MVVM 主题切换 + 样式优先级全解析
ui
李高钢1 天前
【WPF】高级 UI 与性能优化实战:从卡顿到丝滑
ui·性能优化·wpf
MOONICK1 天前
QT的UI开发框架
qt·ui
郝学胜-神的一滴1 天前
Horse3D 游戏引擎研发笔记(七):Clydesdale——从流式日志到多输出订阅
c++·qt·unity·游戏引擎·图形渲染·unreal engine·opengl
江上闲人.2 天前
Wardogs战犬\战狗Beta测试启动报错UE崩溃卡顿?解决方法来了
游戏·游戏引擎·虚幻·战犬·战狗·wardogs
界面开发小八哥2 天前
界面控件DevExpress Blazor v26.1新版亮点 - Filter Builder等功能升级
ui·界面控件·blazor·devexpress·ui开发