# 【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,都是实战里踩出来的。

相关推荐
淡海水14 小时前
15-02-YooAsset面试篇-Unity架构设计与源码
java·unity·面试·c#·游戏引擎·yooasset
郝学胜-神的一滴20 小时前
中级OpenGL教程 030:gl_FragCoord解锁区域渲染与深度可视化新姿势
c++·unity·游戏引擎·图形渲染·unreal engine·opengl
ellis197020 小时前
u3d插件xLua[四]Lua访问C#源码分析,反射,生成代码
unity
2401_894915531 天前
GEO 定位优化源码搭建常见报错排查:数据库、伪静态、接口调试
java·数据库·网络协议·tcp/ip·spring·unity
xcLeigh2 天前
Unity基础:C#脚本的创建与挂载——让你的游戏物体活起来
游戏·unity·c#·教程
DaLiangChen3 天前
Vuforia 地面识别案例
unity·ar