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

相关推荐
Behaviour12 小时前
Unity游戏HybridCLR 热更新接入实战
游戏·unity·游戏引擎
LONGZETECH1 天前
新能源汽车动力电池实训教学痛点与虚拟仿真技术解决方案
c语言·3d·unity·架构·汽车·汽车教学软件
王维志1 天前
UiSplineRenderer
unity·游戏引擎
FairGuard手游加固1 天前
iOS 游戏加固技术解析:代码混淆、资源加密、内存保护与重签名检测
安全·游戏·macos·unity·ios·objective-c
UWA2 天前
GPM 2.0 WebGL 监测能力扩容|覆盖 Unity/Cocos/LayaAir 多引擎微信 & 抖音小游戏
unity·微信·性能优化·小游戏·webgl
Behaviour2 天前
Unity 手游网络同步技术
网络·unity·c#·游戏引擎
niuniudengdeng2 天前
Unity手游APK去广告实战:华为渠道包反编译、广告拦截与“网络不佳“弹窗修复全记录
unity·游戏引擎
Behaviour3 天前
Unity UI循环列表UIScrollViewContent实现
ui·unity·c#·游戏引擎
_ZHOURUI_H_3 天前
Unity EasyECS:并不是所有字段都适合 SoA,Unity 项目中应该怎样拆数据
游戏·unity·性能优化·架构·游戏引擎
TO_ZRG3 天前
Unity 编辑器之 SettingsProvider、Hierarchy、Project、SearchField
java·unity·编辑器