环境与工具说明
| 项 | 说明 |
|---|---|
| 代码生成 | 本系列组件库代码由 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. 实现步骤
- 算深度 :
depth = max(各列 headerParents.Count + 1) - 铺格子 :无上级 →
title从第 0 行rowSpan = depth;有上级 → 上级占上行,title占剩余行 - 横向合并 :同一行、相邻、文案相同且
rowSpan相同 → 合并为一个单元格 - 绝对定位绘制 :去掉单行
HorizontalLayoutGroup,按列宽前缀和算x/y/w/h - 冻结片对齐 :左/中/右三片用同一
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. 实现步骤
- 仅在 叶表头 且
resizable时,右侧挂 6px 透明热区 IBeginDrag / IDrag / IEndDrag累计delta.x- 拖动中:
column.width = max(40, width + delta),同步_resolvedColumns - 松手:
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. 实现步骤
- 叶表头
sortable→ 挂Button,文案旁显示▲ / ▼ - 点击循环:升序 → 降序 → 取消
- 写入
_sortProp/_sortOrder,触发OnSortChanged _page = 1,RequestCurrentPage(),把排序塞进ElePageRequest- 业务侧按
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
Build(config)生成:上一页 / 下一页 / 页码信息 / 每页条数 / 跳转SetTotal(total, page)由BindPage调用,刷新「1/4页 1-10/37」- 上一页、下一页、跳转 →
OnPageChanged(page) - 选每页条数 →
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
七、落地检查清单
- 多行表头:相邻
headerParents相同是否合并;无上级列是否跨满行高 - 冻结开着时,左/中/右表头行数是否一致
resizable列能否拖宽,松手后表体是否对齐sortable列点击是否出现 ▲▼,请求是否带sortProp- 改每页条数是否回到第 1 页;上一页/下一页/跳转是否正确
- 主题切换后表头/分页条是否仍吃
EleUiStyle
菜单验证:
text
EleUnityUI → Samples → Table → Reset Bool1 Config To Preset
→ Generate → Spawn Bool1 Preview In Scene
八、小结
- 多行表头 :
headerParents+ Planner 合并 + 绝对定位绘制 - 拖拽改宽 :叶表头右缘手柄 → 写
width→ 重算列宽并刷行 - 排序 :表头状态机 +
ElePageRequest.sort*,排序逻辑在业务 - 每页条数 / 翻页 :
EleTablePagination事件 →RequestCurrentPage→BindPage - 视觉:继续只用设计变量,不写死色号
下篇预告
04|Table 表格组件(下):行选中与全选、斑马纹/悬停、列显示、工具栏与弹窗编辑联动。
若你也在 UGUI 里做过合并表头或表头跟滚,欢迎留言踩过的坑------冻结片统一 depth、分页下拉独立 Canvas,都是实战里踩出来的。