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

环境与工具说明

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

建议使用 Unity 2022.3 LTS 同系列版本打开工程,避免序列化 / UGUI 行为差异。本文实现均在 2022.3.50f1c1 下验证。


Table 表格组件:多行表头、拖拽改宽、排序与分页

上篇定好了 设计变量EleUiStyle)。本篇聚焦 Table 的核心交互能力,全部对照 EleUnityUI 现有实现:配置怎么写、关键步骤怎么走、核心代码在哪。

涉及文件(改功能时按这个找):

能力 主要文件
配置模型 EleTableModels.csEleTableColumnConfig / ElePaginationConfig
多行表头算法 EleTableHeaderPlanner.cs
表头绘制 / 排序 / 拖拽 EleTable.Headers.csEleTableColumnResizeHandle.cs
翻页请求 EleTable.Paging.cs
翻页条 UI EleTablePagination.cs
绑数据 EleTable.Binding.cs + DemoHost

颜色一律走设计变量:表头 HeaderBg + TextPrimary,边框 Border,排序悬停 RowHover,滚动条 Primary


资源地址:https://download.csdn.net/download/zhchyun2008/93250936

一、整体数据流(先建立全局图)

text 复制代码
Config(列 / 分页 / 冻结)
    → RebuildStructure / BindPage
        → ResolveColumnWidths → SplitFreezeSlices
        → BuildAllHeaders(多行表头 + 排序点 + 拖拽柄)
        → 生成行 EleTableRowView
    → 用户操作
        点排序 / 改页码 / 改每页条数
            → RequestCurrentPage(ElePageRequest)
            → 业务拉数 → BindPage(ElePageResult)

业务侧最小接入:

csharp 复制代码
table.OnRequestPage += req =>
{
    // 使用 req.page / pageSize / sortProp / sortOrder / query
    table.BindPage(new ElePageResult
    {
        total = total,
        page = req.page,
        pageSize = req.pageSize,
        rows = pageRows
    });
};
table.RequestCurrentPage();

二、多行标题(headerParents)

1. 配置怎么写

叶列仍是 columns 里的一项;分组靠 headerParents

csharp 复制代码
// 跨两行(无上级)
new EleTableColumnConfig {
    title = "轴资源名", prop = "axisName",
    // headerParents 留空
}

// 上行「自定义名称」,下行「控制阀」
new EleTableColumnConfig {
    title = "控制阀", prop = "valve",
    headerParents = new List<string> { "自定义名称" }
}

// 上行「位置1」,下行「POS名」------相邻同名自动横向合并
new EleTableColumnConfig {
    title = "POS名", prop = "pos1Name",
    headerParents = new List<string> { "位置1" }
}

表级高度:

csharp 复制代码
headerHeight = 28f;      // 单行默认
headerRowHeight = 28f;   // 多行时「每一行」高度;总高 ≈ 行数 × 本值

2. 实现步骤

  1. 算深度depth = max(各列 headerParents.Count + 1)
  2. 铺格子 :无上级 → title 从第 0 行 rowSpan = depth;有上级 → 上级占上行,title 占剩余行
  3. 横向合并 :同一行、相邻、文案相同且 rowSpan 相同 → 合并为一个单元格
  4. 绝对定位绘制 :去掉单行 HorizontalLayoutGroup,按列宽前缀和算 x/y/w/h
  5. 冻结片对齐 :左/中/右三片用同一 globalDepth,避免行数不一致

3. 关键代码

规划(EleTableHeaderPlanner

csharp 复制代码
public static int GetDepth(IList<EleTableColumnConfig> cols)
{
    int max = 1;
    foreach (var c in cols)
    {
        if (c == null) continue;
        int d = (c.headerParents?.Count ?? 0) + 1;
        if (d > max) max = d;
    }
    return max;
}

// 无上级:title 跨满 depth
labels[c, 0] = col.title;
rowSpans[c, 0] = depth;

// 有上级:parents[i] 在第 i 行,title 在 leafRow 并跨满剩余
for (int p = 0; p < parentCount; p++)
{
    labels[c, p] = parents[p];
    rowSpans[c, p] = 1;
}
int leafRow = Mathf.Min(parentCount, depth - 1);
labels[c, leafRow] = col.title;
rowSpans[c, leafRow] = depth - leafRow;

绘制(BuildHeaderInto

csharp 复制代码
int depth = EleTableHeaderPlanner.GetDepth(_resolvedColumns);
float rowH = config.ResolveHeaderRowHeight();
float totalH = rowH * depth;

// 列宽前缀和
xOffsets[0] = 0;
for (int i = 0; i < cols.Count; i++)
    xOffsets[i + 1] = xOffsets[i] + widths[i];

foreach (var cell in EleTableHeaderPlanner.PlanForcedDepth(cols, depth))
{
    float x = xOffsets[cell.startCol];
    float w = xOffsets[cell.startCol + cell.colSpan] - x;
    float y = cell.row * rowH;
    float h = cell.rowSpan * rowH;
    // RectTransform:锚点左上,anchoredPosition=(x,-y),sizeDelta=(w,h)
}

表架高度用 GetHeaderTotalHeight(),冻结头 / Mid 头一起加高,避免内容顶穿表头。


三、列拖拽改宽(resizable)

1. 配置

csharp 复制代码
new EleTableColumnConfig {
    title = "角度", prop = "pos1Angle",
    width = 70f,
    resizable = true   // 允许拖右缘
}
// 开关列、操作列建议 resizable = false

2. 实现步骤

  1. 仅在 叶表头resizable 时,右侧挂 6px 透明热区
  2. IBeginDrag / IDrag / IEndDrag 累计 delta.x
  3. 拖动中:column.width = max(40, width + delta),同步 _resolvedColumns
  4. 松手:ResolveColumnWidthsSplitFreezeSlicesApplyContentWidth → 重建表头 → BindPage 刷新行宽

3. 关键代码

手柄(EleTableColumnResizeHandle

csharp 复制代码
public void OnDrag(PointerEventData e)
{
    _accumulate += e.delta.x;
    if (Mathf.Abs(_accumulate) >= 1f)
    {
        OnWidthDelta?.Invoke(_prop, _accumulate);
        _accumulate = 0f;
    }
}

写回宽度(EleTable.Headers

csharp 复制代码
void HandleColumnResizeDelta(string prop, float delta)
{
    var col = /* 按 prop 找到 config.columns 项 */;
    if (col == null || !col.resizable) return;
    col.width = Mathf.Max(40f, col.width + delta);
    SyncResolvedColumnWidth(prop, col.width);
}

void HandleColumnResizeEnded()
{
    _columnWidths = EleUiFactory.ResolveColumnWidths(_resolvedColumns, viewportW, out var total);
    SplitFreezeSlices();
    ApplyContentWidth(total);
    BuildAllHeaders();
    if (_lastPageResult != null) BindPage(_lastPageResult);
}

要点:宽度存在 Config 里,松手后整表按新宽度重算,冻结列与中间滚动区一起更新。


四、列排序(sortable)

1. 配置

csharp 复制代码
new EleTableColumnConfig {
    title = "气缸编号", prop = "cylinderNo",
    sortable = true
}

2. 实现步骤

  1. 叶表头 sortable → 挂 Button,文案旁显示 ▲ / ▼
  2. 点击循环:升序 → 降序 → 取消
  3. 写入 _sortProp / _sortOrder,触发 OnSortChanged
  4. _page = 1RequestCurrentPage(),把排序塞进 ElePageRequest
  5. 业务侧按 sortProp/sortOrder 排好再 BindPage(DemoHost 已示例)

3. 关键代码

csharp 复制代码
void HandleHeaderSortClicked(string prop)
{
    if (_sortProp == prop)
    {
        if (_sortOrder == EleSortOrder.Asc) _sortOrder = EleSortOrder.Desc;
        else if (_sortOrder == EleSortOrder.Desc)
        {
            _sortOrder = EleSortOrder.None;
            _sortProp = "";
        }
        else _sortOrder = EleSortOrder.Asc;
    }
    else
    {
        _sortProp = prop;
        _sortOrder = EleSortOrder.Asc;
    }
    OnSortChanged?.Invoke(_sortProp, _sortOrder);
    _page = 1;
    RequestCurrentPage();
}

请求体:

csharp 复制代码
var pageReq = new ElePageRequest
{
    page = _page,
    pageSize = _pageSize,
    query = _query,
    sortProp = _sortProp,
    sortOrder = _sortOrder
};
OnRequestPage?.Invoke(pageReq);

业务排序示例:

csharp 复制代码
if (!string.IsNullOrEmpty(req.sortProp) && req.sortOrder != EleSortOrder.None)
{
    bool desc = req.sortOrder == EleSortOrder.Desc;
    all.Sort((a, b) =>
    {
        var va = a.Get(req.sortProp);
        var vb = b.Get(req.sortProp);
        int cmp = string.Compare(va, vb, StringComparison.OrdinalIgnoreCase);
        if (float.TryParse(va, out var fa) && float.TryParse(vb, out var fb))
            cmp = fa.CompareTo(fb);
        return desc ? -cmp : cmp;
    });
}

库内 只负责 UI 状态与请求参数,真正排序放业务 / 服务端,和 Element Table 的用法一致。


五、每页记录数 + 翻页

1. 配置

csharp 复制代码
pagination = new ElePaginationConfig
{
    enabled = true,
    pageSize = 10,
    pageSizeOptions = new List<int> { 10, 20, 50, 100 },
    showTotal = true,
    showSizes = true,   // 「xx 条/页」下拉
    showJumper = true,  // 前往第 N 页
    industrialStyle = true
};

2. 实现步骤

翻页条 EleTablePagination

  1. Build(config) 生成:上一页 / 下一页 / 页码信息 / 每页条数 / 跳转
  2. SetTotal(total, page)BindPage 调用,刷新「1/4页 1-10/37」
  3. 上一页、下一页、跳转 → OnPageChanged(page)
  4. 选每页条数 → OnPageSizeChanged(size)

表格 EleTable.Paging

csharp 复制代码
void HandlePageChanged(int page)
{
    _page = page;
    RequestCurrentPage();
}

void HandlePageSizeChanged(int size)
{
    _pageSize = size;
    _page = 1;          // 改页大小必须回到第 1 页
    RequestCurrentPage();
}

绑页 BindPage

csharp 复制代码
_page = result.page;
_pageSize = result.pageSize;
// ...重建表架、表头、行...
pagination.SetTotal(result.total, result.page);

3. 关键代码:每页条数下拉

csharp 复制代码
// EleTablePagination.EnsureUi
pageSizeButton = MakePagerButton("PageSize", $"{_pageSize} 条/页 ▾", ...);
pageSizeButton.onClick.AddListener(ToggleDropdown);

foreach (var size in _config.pageSizeOptions)
{
    var optSize = size;
    var opt = MakePagerButton($"Opt_{optSize}", $"{optSize} 条/页", ...);
    opt.onClick.AddListener(() => SelectPageSize(optSize));
}

void SelectPageSize(int size)
{
    _pageSize = size;
    CloseDropdown();
    OnPageSizeChanged?.Invoke(_pageSize);
}

下拉挂独立 CanvasoverrideSorting),避免被表格 RectMask2D 裁掉、点不中。

4. 业务侧切片

csharp 复制代码
int start = (req.page - 1) * req.pageSize;
var slice = all.GetRange(start, Math.Min(req.pageSize, all.Count - start));
table.BindPage(new ElePageResult
{
    total = all.Count,
    page = req.page,
    pageSize = req.pageSize,
    rows = slice
});

六、五块能力如何串在一起

用户动作 表格内部 业务
点可排序表头 _sortProp/_sortOrderRequestCurrentPage 按 sort 排序后切片
拖列宽松手 column.width,重建表头+行 一般无需请求
上一页 / 下一页 / 跳转 _page 变,RequestCurrentPage 按 page 切片
改「20 条/页」 _pageSize 变,_page=1,请求 按新 size 切片
BindPage 多行表头按当前列宽重画 ---

配置示例(Bool1 预设思路):

csharp 复制代码
// 位置1 分组 + 可排序可拖宽
headerParents = new List<string> { "位置1" },
sortable = true,
resizable = true

// 改变方向:跨行表头,不可排序不可拖
headerParents 为空,
sortable = false,
resizable = false

七、落地检查清单

  1. 多行表头:相邻 headerParents 相同是否合并;无上级列是否跨满行高
  2. 冻结开着时,左/中/右表头行数是否一致
  3. resizable 列能否拖宽,松手后表体是否对齐
  4. sortable 列点击是否出现 ▲▼,请求是否带 sortProp
  5. 改每页条数是否回到第 1 页;上一页/下一页/跳转是否正确
  6. 主题切换后表头/分页条是否仍吃 EleUiStyle

菜单验证:

text 复制代码
EleUnityUI → Samples → Table → Reset Bool1 Config To Preset
→ Generate → Spawn Bool1 Preview In Scene

八、小结

  1. 多行表头headerParents + Planner 合并 + 绝对定位绘制
  2. 拖拽改宽 :叶表头右缘手柄 → 写 width → 重算列宽并刷行
  3. 排序 :表头状态机 + ElePageRequest.sort*,排序逻辑在业务
  4. 每页条数 / 翻页EleTablePagination 事件 → RequestCurrentPageBindPage
  5. 视觉:继续只用设计变量,不写死色号

下篇预告

04|Table 表格组件(下):行选中与全选、斑马纹/悬停、列显示、工具栏与弹窗编辑联动。

若你也在 UGUI 里做过合并表头或表头跟滚,欢迎留言踩过的坑------冻结片统一 depth、分页下拉独立 Canvas,都是实战里踩出来的。

相关推荐
林川~0111 小时前
Unity 万能物理检测工具:射线检测 / 范围检测 / 层级过滤 / 编辑器可视化(可直接拿去用)
游戏·unity·c#·射线检测·通用工具
yi碗汤园18 小时前
MQTT消息队列遥测传输协议
网络·网络协议·unity
林川~011 天前
Unity 常用 API 与核心类全解:从生命周期到协程,附性能避坑清单(Unity 6 适配)
unity·游戏引擎·常用api
sensen_kiss1 天前
CPT306 Coursework 1 个人游戏作业——坦克大战
unity·游戏程序·游戏策划
蒸鱼Yuzheng1 天前
Unity 与 Unreal 测试工具怎么回答:Profiler、日志、自动化与证据链
测试工具·unity·性能分析·游戏测试·unrealengine
野区捕龙为宠1 天前
Unity WebGL平台 通过 URL 加载图片(UGUI RawImage / 模型材质)
unity·团结引擎
阿松爱学习2 天前
【Unity开发】FileStream 详解及用法指南
unity·c#·unity开发
想做后端的前端2 天前
Unity-UIGI优化Rebatch与Rebuild
unity
玖玥拾2 天前
Unity Shader 基础与图形学(一)
unity·图形渲染·图形学·shader
百里香酚兰3 天前
【Unity学习笔记】如何把粉色Prefab还原
笔记·学习·unity