# 【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.cs → EleTableColumnConfig / ElePaginationConfig
多行表头算法 EleTableHeaderPlanner.cs
表头绘制 / 排序 / 拖拽 EleTable.Headers.cs、EleTableColumnResizeHandle.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. 松手:ResolveColumnWidths → SplitFreezeSlices → ApplyContentWidth → 重建表头 → 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 = 1,RequestCurrentPage(),把排序塞进 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);
}

下拉挂独立 Canvas(overrideSorting),避免被表格 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/_sortOrder,RequestCurrentPage 按 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 事件 → RequestCurrentPage → BindPage
  5. 视觉:继续只用设计变量,不写死色号

下篇预告

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

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

相关推荐
Martina_03213 天前
AI生成3D场景导入 Unity/Unreal 后,NPC 总串层?用6步检查导航高度与跨层连接
人工智能·游戏·数学建模·3d·unity·自然语言处理·aigc
小贺儿开发4 天前
Unity 局域网遥控幻灯片展示工具 3.0
科技·http·unity·工具·ppt·通信·网页
WarPigs6 天前
Unity对象不应该使用null传播的问题
unity
每日出拳老爷子7 天前
【VR】【Unity】抓走转放系列21|一天搭完门和抽屉工具槽:小场景按这个次序拼装
unity·vr·驱动·排错·snap zone
淡海水7 天前
02-02-原理篇-分代GC
算法·unity·c#·游戏引擎·.net·gc
小贺儿开发7 天前
Unity 文物新生 会讲故事的文物展墙
人工智能·科技·unity·ai·视频·互动·演示
淡海水8 天前
02-03-原理篇-大对象堆
unity·c#·游戏引擎·.net·gc
WarPigs8 天前
Unity在Create菜单添加创建自定义脚本模板选项
unity
淡海水8 天前
02-01-原理篇-Mark-Sweep与变种算法
算法·unity·c#·游戏引擎·.net·gc
babywang09 天前
Unity笔记--塔防游戏中的ECS架构:从原理到实战的全面解析
unity·ecs架构