表格添加四叉树渲染优化逻辑。

This commit is contained in:
2026-06-30 19:53:12 +08:00
parent f7e489e924
commit 40fbfed45c
7 changed files with 1104 additions and 189 deletions
+27 -26
View File
@@ -176,18 +176,26 @@ namespace XericUIEditor.XTable
[MenuItem("GameObject/Xeric UI/Table/Xeric UI Action Table", false, 60)]
private static void CreateTable(MenuCommand menuCommand)
{
// 1. 创建表格内容节点
GameObject contentGo = new GameObject("Content",
typeof(RectTransform), typeof(CanvasRenderer), typeof(XericUIActionTable));
// 1. 创建根节点:ScrollRect + Image + XericUIActionTable 共存于同一 GameObject
GameObject rootGo = new GameObject("Xeric UI Action Table",
typeof(RectTransform), typeof(ScrollRect), typeof(Image), typeof(XericUIActionTable));
RectTransform rootRt = rootGo.GetComponent<RectTransform>();
rootRt.sizeDelta = new Vector2(800, 600);
rootGo.GetComponent<Image>().color = new Color(0.1f, 0.1f, 0.1f, 0.3f);
ScrollRect scrollRect = rootGo.GetComponent<ScrollRect>();
XericUIActionTable table = rootGo.GetComponent<XericUIActionTable>();
// 2. 创建 Content(纯 RectTransform,由表格组件管理动态子对象)
GameObject contentGo = new GameObject("Content", typeof(RectTransform));
RectTransform contentRt = contentGo.GetComponent<RectTransform>();
contentRt.anchorMin = new Vector2(0, 1);
contentRt.anchorMax = new Vector2(0, 1);
contentRt.pivot = new Vector2(0, 1);
contentRt.anchoredPosition = Vector2.zero;
contentRt.sizeDelta = new Vector2(800, 600);
XericUIActionTable table = contentGo.GetComponent<XericUIActionTable>();
// 2. 创建 Viewport(Mask 裁剪区域)
// 3. 创建 Viewport(Mask 裁剪区域)
GameObject viewportGo = new GameObject("Viewport",
typeof(RectTransform), typeof(Image), typeof(Mask));
viewportGo.GetComponent<Image>().color = new Color(1, 1, 1, 0.01f);
@@ -197,7 +205,7 @@ namespace XericUIEditor.XTable
vpRt.sizeDelta = Vector2.zero;
contentRt.SetParent(vpRt, false);
// 3. 创建水平滚动条
// 4. 创建水平滚动条
GameObject hScrollbarGo = new GameObject("Scrollbar Horizontal",
typeof(RectTransform), typeof(Scrollbar), typeof(Image));
RectTransform hBarRt = hScrollbarGo.GetComponent<RectTransform>();
@@ -217,7 +225,7 @@ namespace XericUIEditor.XTable
RectTransform hSlidingRt = hSlidingGo.GetComponent<RectTransform>();
hSlidingRt.anchorMin = Vector2.zero;
hSlidingRt.anchorMax = Vector2.one;
hSlidingRt.sizeDelta = new Vector2(-20, -20);
hSlidingRt.sizeDelta = new Vector2(-20, 0);
GameObject hHandleGo = new GameObject("Handle",
typeof(RectTransform), typeof(Image));
@@ -225,11 +233,11 @@ namespace XericUIEditor.XTable
RectTransform hHandleRt = hHandleGo.GetComponent<RectTransform>();
hHandleRt.anchorMin = Vector2.zero;
hHandleRt.anchorMax = Vector2.one;
hHandleRt.sizeDelta = new Vector2(-20, -20);
hHandleRt.sizeDelta = new Vector2(-10, -10);
hHandleGo.GetComponent<Image>().color = new Color(0.4f, 0.4f, 0.4f, 0.8f);
hBar.handleRect = hHandleRt;
// 4. 创建垂直滚动条
// 5. 创建垂直滚动条
GameObject vScrollbarGo = new GameObject("Scrollbar Vertical",
typeof(RectTransform), typeof(Scrollbar), typeof(Image));
RectTransform vBarRt = vScrollbarGo.GetComponent<RectTransform>();
@@ -249,7 +257,7 @@ namespace XericUIEditor.XTable
RectTransform vSlidingRt = vSlidingGo.GetComponent<RectTransform>();
vSlidingRt.anchorMin = Vector2.zero;
vSlidingRt.anchorMax = Vector2.one;
vSlidingRt.sizeDelta = new Vector2(-20, -20);
vSlidingRt.sizeDelta = new Vector2(0, -20);
GameObject vHandleGo = new GameObject("Handle",
typeof(RectTransform), typeof(Image));
@@ -257,18 +265,11 @@ namespace XericUIEditor.XTable
RectTransform vHandleRt = vHandleGo.GetComponent<RectTransform>();
vHandleRt.anchorMin = Vector2.zero;
vHandleRt.anchorMax = Vector2.one;
vHandleRt.sizeDelta = new Vector2(-20, -20);
vHandleRt.sizeDelta = new Vector2(-10, -10);
vHandleGo.GetComponent<Image>().color = new Color(0.4f, 0.4f, 0.4f, 0.8f);
vBar.handleRect = vHandleRt;
// 5. 创建 ScrollView 根节点
GameObject scrollGo = new GameObject("Xeric UI Action Table",
typeof(RectTransform), typeof(ScrollRect), typeof(Image));
RectTransform scrollRt = scrollGo.GetComponent<RectTransform>();
scrollRt.sizeDelta = new Vector2(800, 600);
scrollGo.GetComponent<Image>().color = new Color(0.1f, 0.1f, 0.1f, 0.3f);
ScrollRect scrollRect = scrollGo.GetComponent<ScrollRect>();
// 6. 配置 ScrollRect
scrollRect.content = contentRt;
scrollRect.viewport = vpRt;
scrollRect.horizontalScrollbar = hBar;
@@ -278,11 +279,11 @@ namespace XericUIEditor.XTable
scrollRect.movementType = ScrollRect.MovementType.Clamped;
// 组装层级
vpRt.SetParent(scrollRt, false);
hScrollbarGo.transform.SetParent(scrollRt, false);
vScrollbarGo.transform.SetParent(scrollRt, false);
vpRt.SetParent(rootRt, false);
hScrollbarGo.transform.SetParent(rootRt, false);
vScrollbarGo.transform.SetParent(rootRt, false);
// 绑定 ScrollRect 到表格(通过 SerializedObject 正确回写)
// 绑定 ScrollRect 到表格
using (SerializedObject so = new SerializedObject(table))
{
SerializedProperty prop = so.FindProperty("m_ScrollRect");
@@ -295,10 +296,10 @@ namespace XericUIEditor.XTable
// 挂载到 Canvas
GameObject parent = GetOrCreateCanvas();
scrollRt.SetParent(parent.transform, false);
rootRt.SetParent(parent.transform, false);
Undo.RegisterCreatedObjectUndo(scrollGo, "Create Xeric UI Action Table");
Selection.activeGameObject = scrollGo;
Undo.RegisterCreatedObjectUndo(rootGo, "Create Xeric UI Action Table");
Selection.activeGameObject = rootGo;
}
private static GameObject GetOrCreateCanvas()
+57 -51
View File
@@ -1,7 +1,7 @@
using UnityEditor;
using UnityEngine;
using XericLibrary.Runtime.MacroLibrary;
using XericLibrary.Runtime.SuperStyleSheet;
using XericUI.XTable.Core;
using XericUI.XTable.Mapping;
@@ -179,7 +179,9 @@ namespace XericUIEditor.XTable
Canvas.ForceUpdateCanvases();
#endif
RectTransform tableRt = table.GetComponent<RectTransform>();
// 使用 Content 的 RectTransform 进行定位计算(而非表格组件所在的 ScrollRect 根节点)
RectTransform tableRt = table.ContentTransform.RectTransform();
if (tableRt == null) tableRt = table.GetComponent<RectTransform>();
float[] rh = table.RowHeights;
float[] cw = table.ColWidths;
if (rh == null || cw == null || rh.Length == 0 || cw.Length == 0) return;
@@ -361,31 +363,32 @@ namespace XericUIEditor.XTable
s_DragStartCol = col;
if (Event.current.shift && table.SelectedRow >= 0)
{
if (!table.HasMultiSelection)
table.StartMultiSelect(table.SelectedRow, table.SelectedCol);
table.ExtendMultiSelect(r, col);
}
{
if (!table.HasMultiSelection)
table.SelectCell(table.SelectedRow, table.SelectedCol, r, col);
else
{
table.StartMultiSelect(r, col);
}
table.RefreshView();
SyncEditText(table);
Event.current.Use();
SceneView.RepaintAll();
table.SelectionHandle.ExtendTo(r, col);
}
else
{
table.SelectCell(r, col);
}
y += cellH;
table.RefreshView();
SyncEditText(table);
Event.current.Use();
SceneView.RepaintAll();
}
y += cellH;
}
}
#endregion
#endregion
#region 列标签
#region 列标签
private static void DrawColumnLabels(float[] cw, Rect tableRect,
private static void DrawColumnLabels(float[] cw, Rect tableRect,
float xScale, float yScale, XericUIActionTable table)
{
var style = new GUIStyle(GUI.skin.label)
@@ -436,25 +439,26 @@ namespace XericUIEditor.XTable
s_DragStartCol = c;
if (Event.current.shift && table.SelectedCol >= 0)
{
if (!table.HasMultiSelection)
table.StartMultiSelect(table.SelectedRow, table.SelectedCol);
table.ExtendMultiSelect(row, c);
}
{
if (!table.HasMultiSelection)
table.SelectCell(table.SelectedRow, table.SelectedCol, row, c);
else
{
table.StartMultiSelect(row, c);
}
table.RefreshView();
SyncEditText(table);
Event.current.Use();
SceneView.RepaintAll();
table.SelectionHandle.ExtendTo(row, c);
}
else
{
table.SelectCell(row, c);
}
x += cellW;
table.RefreshView();
SyncEditText(table);
Event.current.Use();
SceneView.RepaintAll();
}
x += cellW;
}
}
#endregion
@@ -972,15 +976,16 @@ namespace XericUIEditor.XTable
s_DragStartCol = mdCol;
if (e.shift && table.SelectedRow >= 0 && table.SelectedCol >= 0)
{
if (!table.HasMultiSelection)
table.StartMultiSelect(table.SelectedRow, table.SelectedCol);
table.ExtendMultiSelect(mdRow, mdCol);
}
{
if (!table.HasMultiSelection)
table.SelectCell(table.SelectedRow, table.SelectedCol, mdRow, mdCol);
else
{
table.StartMultiSelect(mdRow, mdCol);
}
table.SelectionHandle.ExtendTo(mdRow, mdCol);
}
else
{
table.SelectCell(mdRow, mdCol);
}
table.RefreshView();
SyncEditText(table);
@@ -994,7 +999,7 @@ namespace XericUIEditor.XTable
CellFromMouse(tr, xScale, yScale, out int dragRow, out int dragCol);
if (dragRow < 0 || dragCol < 0) break;
table.ExtendMultiSelect(dragRow, dragCol);
table.SelectionHandle.ExtendTo(dragRow, dragCol);
table.RefreshView();
SyncEditText(table);
e.Use();
@@ -1127,16 +1132,17 @@ namespace XericUIEditor.XTable
}
if (shift)
{
if (!table.HasMultiSelection && table.SelectedRow >= 0 && table.SelectedCol >= 0)
table.StartMultiSelect(table.SelectedRow, table.SelectedCol);
table.ExtendMultiSelect(row, col);
}
{
if (!table.HasMultiSelection && table.SelectedRow >= 0 && table.SelectedCol >= 0)
table.SelectCell(table.SelectedRow, table.SelectedCol, row, col);
else
{
RedirectToMergeSource(ref row, ref col);
table.SelectCell(row, col);
}
table.SelectionHandle.ExtendTo(row, col);
}
else
{
RedirectToMergeSource(ref row, ref col);
table.SelectCell(row, col);
}
table.RefreshView();
SyncEditText(table);
+331
View File
@@ -0,0 +1,331 @@
using System.Collections.Generic;
using UnityEngine;
namespace XericUI.XTable.Core
{
/// <summary>
/// 表格四叉树构建用的单元格元数据。
/// </summary>
public struct XTableQuadtreeCell
{
public int Row;
public int Col;
public string Namespace;
}
/// <summary>
/// 四叉树查询结果——一个均匀象限的可见部分。
/// </summary>
public struct QuadrantResult
{
/// <summary>该象限在表格像素空间中的区域(左上角坐标系,y 向下为正)</summary>
public Rect Region;
/// <summary>统一命名空间(空字符串 = 空区域)</summary>
public string Namespace;
/// <summary>该象限包含的单元格坐标列表</summary>
public List<(int row, int col)> Cells;
}
/// <summary>
/// 表格四叉树——基于单元格 <see cref="XTableQuadtreeCell.Namespace"/> 进行递归区域分割。
/// 若一个象限内存在不同命名空间的单元格,则继续分裂,直到所有差异分开。
/// 渲染阶段通过 <see cref="QueryVisible"/> 获取可见范围内的所有均匀象限。
/// </summary>
public class XTableQuadtree
{
#region 内部节点
private class Node
{
/// <summary>当前象限在表格像素空间中的区域</summary>
public Rect Region;
/// <summary>
/// null = 非均匀节点(有子节点),非 null = 均匀叶子节点。
/// 空字符串 "" 表示该区域无任何单元格。
/// </summary>
public string UniformNamespace;
/// <summary>子象限(null 表示叶子节点)</summary>
public Node[] Children;
/// <summary>该均匀象限包含的单元格坐标(仅叶子均匀节点有效)</summary>
public List<(int, int)> Cells;
public bool IsUniform => UniformNamespace != null;
}
#endregion
#region 字段
private Node m_Root;
private float[] m_RowHeights;
private float[] m_ColWidths;
/// <summary>四叉树是否已构建完成</summary>
public bool IsBuilt { get; private set; }
#endregion
#region 构建
/// <summary>
/// 全量构建四叉树。
/// </summary>
/// <param name="totalWidth">表格总宽度(像素)</param>
/// <param name="totalHeight">表格总高度(像素)</param>
/// <param name="rowHeights">行高数组(索引 = 行号)</param>
/// <param name="colWidths">列宽数组(索引 = 列号)</param>
/// <param name="cells">所有有数据的单元格元数据列表</param>
public void Build(float totalWidth, float totalHeight,
float[] rowHeights, float[] colWidths,
List<XTableQuadtreeCell> cells)
{
if (rowHeights == null || colWidths == null || cells == null)
{
IsBuilt = false;
return;
}
m_RowHeights = rowHeights;
m_ColWidths = colWidths;
m_Root = new Node { Region = new Rect(0, 0, totalWidth, totalHeight) };
int maxRow = rowHeights.Length - 1;
int maxCol = colWidths.Length - 1;
if (maxRow >= 0 && maxCol >= 0)
BuildNode(m_Root, cells, 0, maxRow, 0, maxCol);
else
m_Root.UniformNamespace = "";
IsBuilt = true;
}
/// <summary>强制重建(外部入口,通常由表格组件在数据/布局变更后调用)</summary>
public void ForceRebuild(float totalWidth, float totalHeight,
float[] rowHeights, float[] colWidths,
List<XTableQuadtreeCell> cells)
{
Clear();
Build(totalWidth, totalHeight, rowHeights, colWidths, cells);
}
private void BuildNode(Node node, List<XTableQuadtreeCell> allCells,
int minRow, int maxRow, int minCol, int maxCol)
{
// 收集当前行列范围内的单元格
var cellsInRange = new List<XTableQuadtreeCell>();
for (int i = 0; i < allCells.Count; i++)
{
var cell = allCells[i];
if (cell.Row >= minRow && cell.Row <= maxRow &&
cell.Col >= minCol && cell.Col <= maxCol)
cellsInRange.Add(cell);
}
// 空区域
if (cellsInRange.Count == 0)
{
node.UniformNamespace = "";
return;
}
// 检查是否所有单元格命名空间一致
string firstNs = NormalizeNs(cellsInRange[0].Namespace);
bool allSame = true;
for (int i = 1; i < cellsInRange.Count; i++)
{
if (NormalizeNs(cellsInRange[i].Namespace) != firstNs)
{
allSame = false;
break;
}
}
if (allSame)
{
node.UniformNamespace = firstNs;
node.Cells = new List<(int, int)>(cellsInRange.Count);
for (int i = 0; i < cellsInRange.Count; i++)
node.Cells.Add((cellsInRange[i].Row, cellsInRange[i].Col));
return;
}
// 无法继续分裂(单行单列),强制标记为均匀
if (minRow >= maxRow && minCol >= maxCol)
{
node.UniformNamespace = firstNs;
node.Cells = new List<(int, int)>(cellsInRange.Count);
for (int i = 0; i < cellsInRange.Count; i++)
node.Cells.Add((cellsInRange[i].Row, cellsInRange[i].Col));
return;
}
// 分裂:在行列索引空间取中点
int midRow = (minRow + maxRow) / 2;
int midCol = (minCol + maxCol) / 2;
node.Children = new Node[4];
// 左上 (minRow..midRow, minCol..midCol)
node.Children[0] = MakeChild(minRow, midRow, minCol, midCol);
BuildNode(node.Children[0], allCells, minRow, midRow, minCol, midCol);
// 右上 (minRow..midRow, midCol+1..maxCol)
node.Children[1] = MakeChild(minRow, midRow, midCol + 1, maxCol);
BuildNode(node.Children[1], allCells, minRow, midRow, midCol + 1, maxCol);
// 左下 (midRow+1..maxRow, minCol..midCol)
node.Children[2] = MakeChild(midRow + 1, maxRow, minCol, midCol);
BuildNode(node.Children[2], allCells, midRow + 1, maxRow, minCol, midCol);
// 右下 (midRow+1..maxRow, midCol+1..maxCol)
node.Children[3] = MakeChild(midRow + 1, maxRow, midCol + 1, maxCol);
BuildNode(node.Children[3], allCells, midRow + 1, maxRow, midCol + 1, maxCol);
}
private Node MakeChild(int minRow, int maxRow, int minCol, int maxCol)
{
float x = GetColLeftX(minCol);
float y = GetRowTopY(minRow);
float w = GetColRightX(maxCol) - x;
float h = GetRowBottomY(maxRow) - y;
return new Node { Region = new Rect(x, y, w, h) };
}
#endregion
#region 查询
/// <summary>
/// 查询可见矩形内的所有均匀象限。
/// </summary>
/// <param name="visibleRect">可见矩形(左上角坐标系)</param>
/// <param name="results">结果列表(复用外部容器)</param>
public void QueryVisible(Rect visibleRect, List<QuadrantResult> results)
{
if (results == null)
results = new List<QuadrantResult>();
if (m_Root == null)
return;
QueryNode(m_Root, visibleRect, results);
}
private void QueryNode(Node node, Rect visibleRect, List<QuadrantResult> results)
{
// 不相交则跳过
if (!RectIntersects(node.Region, visibleRect))
return;
if (node.IsUniform)
{
// 裁剪到可见范围
Rect clipped = RectIntersection(node.Region, visibleRect);
if (clipped.width > 0 && clipped.height > 0)
{
results.Add(new QuadrantResult
{
Region = clipped,
Namespace = node.UniformNamespace,
Cells = node.Cells
});
}
return;
}
// 非均匀节点:递归查询子节点
if (node.Children != null)
{
for (int i = 0; i < node.Children.Length; i++)
{
if (node.Children[i] != null)
QueryNode(node.Children[i], visibleRect, results);
}
}
}
#endregion
#region 清理
/// <summary>清空四叉树</summary>
public void Clear()
{
m_Root = null;
m_RowHeights = null;
m_ColWidths = null;
IsBuilt = false;
}
#endregion
#region 坐标计算
private float GetRowTopY(int row)
{
float y = 0;
for (int i = 0; i < row && i < m_RowHeights.Length; i++)
y += m_RowHeights[i];
return y;
}
private float GetRowBottomY(int row)
{
float y = 0;
for (int i = 0; i <= row && i < m_RowHeights.Length; i++)
y += m_RowHeights[i];
return y;
}
private float GetColLeftX(int col)
{
float x = 0;
for (int i = 0; i < col && i < m_ColWidths.Length; i++)
x += m_ColWidths[i];
return x;
}
private float GetColRightX(int col)
{
float x = 0;
for (int i = 0; i <= col && i < m_ColWidths.Length; i++)
x += m_ColWidths[i];
return x;
}
#endregion
#region 工具
/// <summary>命名空间规范化:null、空串、默认命名空间统一为 "xeric_table_default"</summary>
private static string NormalizeNs(string ns)
{
if (string.IsNullOrEmpty(ns) || ns == "xeric_table_default")
return "xeric_table_default";
return ns;
}
private static bool RectIntersects(Rect a, Rect b)
{
return a.x < b.xMax && a.xMax > b.x &&
a.y < b.yMax && a.yMax > b.y;
}
private static Rect RectIntersection(Rect a, Rect b)
{
float x = Mathf.Max(a.x, b.x);
float y = Mathf.Max(a.y, b.y);
float xMax = Mathf.Min(a.xMax, b.xMax);
float yMax = Mathf.Min(a.yMax, b.yMax);
return new Rect(x, y, Mathf.Max(0, xMax - x), Mathf.Max(0, yMax - y));
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 90a1712e28eeec248b95350379e42764
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,194 @@
using System;
using UnityEngine;
namespace XericUI.XTable.Core
{
/// <summary>
/// 表格选择句柄——统一管理单选/多选状态。
/// 外部可创建此句柄并通过表格的 SetSelectionHandle 或直接操作句柄来控制选区。
/// 选区变更时触发 OnSelectionChanged 事件,表格监听此事件进行重绘。
/// </summary>
public class XTableSelectionHandle
{
#region 属性
/// <summary>起始行(多选锚点行,单选当前行)</summary>
public int StartRow { get; private set; } = -1;
/// <summary>起始列(多选锚点列,单选当前列)</summary>
public int StartCol { get; private set; } = -1;
/// <summary>结束行(仅多选有效,等于 StartRow 时为单选)</summary>
public int EndRow { get; private set; } = -1;
/// <summary>结束列(仅多选有效,等于 StartCol 时为单选)</summary>
public int EndCol { get; private set; } = -1;
/// <summary>是否有有效选择</summary>
public bool HasSelection => StartRow >= 0 && StartCol >= 0;
/// <summary>是否为多选(起止不同)</summary>
public bool HasMultiSelection =>
HasSelection &&
(StartRow != EndRow || StartCol != EndCol);
/// <summary>选中区域最小行</summary>
public int MinRow => HasSelection ? Mathf.Min(StartRow, EndRow) : -1;
/// <summary>选中区域最大行</summary>
public int MaxRow => HasSelection ? Mathf.Max(StartRow, EndRow) : -1;
/// <summary>选中区域最小列</summary>
public int MinCol => HasSelection ? Mathf.Min(StartCol, EndCol) : -1;
/// <summary>选中区域最大列</summary>
public int MaxCol => HasSelection ? Mathf.Max(StartCol, EndCol) : -1;
#endregion
#region 事件
/// <summary>选区变化时触发</summary>
public event Action<XTableSelectionHandle> OnSelectionChanged;
#endregion
#region 公开方法
/// <summary>
/// 单选一个单元格(同时清除多选范围)
/// </summary>
public void Select(int row, int col)
{
StartRow = row;
StartCol = col;
EndRow = row;
EndCol = col;
NotifyChanged();
}
/// <summary>
/// 多选一个范围
/// </summary>
public void Select(int startRow, int startCol, int endRow, int endCol)
{
StartRow = startRow;
StartCol = startCol;
EndRow = endRow;
EndCol = endCol;
NotifyChanged();
}
/// <summary>
/// 扩展多选范围到目标格(保持原有锚点)
/// </summary>
public void ExtendTo(int row, int col)
{
if (!HasSelection)
{
Select(row, col);
return;
}
EndRow = row;
EndCol = col;
NotifyChanged();
}
/// <summary>
/// 清除选择
/// <summary>清除选择</summary>
public void Clear()
{
if (!HasSelection) return;
StartRow = -1;
StartCol = -1;
EndRow = -1;
EndCol = -1;
NotifyChanged();
}
/// <summary>将选区裁剪到 [0, maxRow] × [0, maxCol] 范围</summary>
public void ClampTo(int maxRow, int maxCol)
{
if (!HasSelection) return;
int oldStartRow = StartRow, oldStartCol = StartCol;
int oldEndRow = EndRow, oldEndCol = EndCol;
StartRow = Mathf.Clamp(StartRow, -1, maxRow);
StartCol = Mathf.Clamp(StartCol, -1, maxCol);
EndRow = Mathf.Clamp(EndRow, -1, maxRow);
EndCol = Mathf.Clamp(EndCol, -1, maxCol);
if (StartRow < 0 || StartCol < 0)
Clear();
else if (oldStartRow != StartRow || oldStartCol != StartCol ||
oldEndRow != EndRow || oldEndCol != EndCol)
NotifyChanged();
}
/// <summary>
/// 判断指定坐标是否在选中范围内
/// </summary>
public bool IsInRange(int row, int col)
{
if (!HasSelection) return false;
int minR = Mathf.Min(StartRow, EndRow);
int maxR = Mathf.Max(StartRow, EndRow);
int minC = Mathf.Min(StartCol, EndCol);
int maxC = Mathf.Max(StartCol, EndCol);
return row >= minR && row <= maxR && col >= minC && col <= maxC;
}
/// <summary>左移一格</summary>
public int MoveLeft(int maxCol = 0)
{
if (!HasSelection) return -1;
int newCol = Mathf.Max(maxCol, StartCol - 1);
if (newCol != StartCol)
Select(StartRow, newCol);
return newCol;
}
/// <summary>右移一格</summary>
public int MoveRight(int maxCol)
{
if (!HasSelection) return -1;
int newCol = Mathf.Min(maxCol, StartCol + 1);
if (newCol != StartCol)
Select(StartRow, newCol);
return newCol;
}
/// <summary>上移一格</summary>
public int MoveUp(int maxRow = 0)
{
if (!HasSelection) return -1;
int newRow = Mathf.Max(maxRow, StartRow - 1);
if (newRow != StartRow)
Select(newRow, StartCol);
return newRow;
}
/// <summary>下移一格</summary>
public int MoveDown(int maxRow)
{
if (!HasSelection) return -1;
int newRow = Mathf.Min(maxRow, StartRow + 1);
if (newRow != StartRow)
Select(newRow, StartCol);
return newRow;
}
#endregion
#region 内部
private void NotifyChanged()
{
OnSelectionChanged?.Invoke(this);
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 872c2bc5f6f1a3f47a655bdf059656bd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -9,6 +9,7 @@ using XericUI.XTable.Core;
using XericUI.XTable.Mapping;
using XericUI.XTable.Rendering.Elements;
using XericLibrary.Runtime.SuperStyleSheet;
using XericUI.XTable.Core;
#if UNITY_EDITOR
using UnityEditor;
#endif
@@ -53,17 +54,16 @@ namespace XericUI.XTable.Rendering.Component
[System.NonSerialized] private CellAssembler m_Assembler;
[System.NonSerialized] private XTableDirtyFlag m_DirtyFlag;
private int m_SelectedRow = -1;
private int m_SelectedCol = -1;
// 多选范围
private int m_SelectStartRow = -1;
private int m_SelectStartCol = -1;
private int m_SelectEndRow = -1;
private int m_SelectEndCol = -1;
[System.NonSerialized] private XTableSelectionHandle m_SelectionHandle;
[System.NonSerialized] private GameObject m_BackgroundGo;
/// <summary>预制体实例缓存,key 为 (row, col),用于复用已实例化的预制体</summary>
[System.NonSerialized] private Dictionary<(int, int), GameObject> m_PrefabInstances;
/// <summary>表格四叉树——按 StyleNamespace 分区,渲染阶段查询可见象限</summary>
[System.NonSerialized] private XTableQuadtree m_Quadtree;
private float m_TotalWidth;
private float m_TotalHeight;
@@ -100,14 +100,26 @@ namespace XericUI.XTable.Rendering.Component
public float[] ColWidths => m_ColWidths;
public string DefaultStyleNamespace => m_DefaultStyleNamespace;
public int SelectedRow => m_SelectedRow;
public int SelectedCol => m_SelectedCol;
public int SelectStartRow => m_SelectStartRow;
public int SelectStartCol => m_SelectStartCol;
public int SelectEndRow => m_SelectEndRow;
public int SelectEndCol => m_SelectEndCol;
public bool HasMultiSelection => m_SelectStartRow >= 0 && m_SelectEndRow >= 0;
/// <summary>选中操作句柄——外部可通过此句柄控制选区,或直接操作 StartRow/StartCol 等属性</summary>
public XTableSelectionHandle SelectionHandle
{
get
{
if (m_SelectionHandle == null)
m_SelectionHandle = new XTableSelectionHandle();
return m_SelectionHandle;
}
}
// ── 向后兼容属性(供 Editor 脚本使用)──
public int SelectedRow => SelectionHandle.StartRow;
public int SelectedCol => SelectionHandle.StartCol;
public int SelectStartRow => SelectionHandle.StartRow;
public int SelectStartCol => SelectionHandle.StartCol;
public int SelectEndRow => SelectionHandle.EndRow;
public int SelectEndCol => SelectionHandle.EndCol;
public bool HasMultiSelection => SelectionHandle.HasMultiSelection;
/// <summary>动态子对象在 Hierarchy 中是否可见(仅编辑器有效)</summary>
public bool ChildrenVisible
@@ -119,12 +131,214 @@ namespace XericUI.XTable.Rendering.Component
/// <summary>判断单元格是否在多选范围内</summary>
public bool IsInSelectionRange(int row, int col)
{
if (!HasMultiSelection) return false;
int minR = Mathf.Min(m_SelectStartRow, m_SelectEndRow);
int maxR = Mathf.Max(m_SelectStartRow, m_SelectEndRow);
int minC = Mathf.Min(m_SelectStartCol, m_SelectEndCol);
int maxC = Mathf.Max(m_SelectStartCol, m_SelectEndCol);
return row >= minR && row <= maxR && col >= minC && col <= maxC;
return SelectionHandle.IsInRange(row, col);
}
/// <summary>
/// 动态子对象挂载的父节点。
/// 若绑定了 ScrollRect 则使用其 Content,否则回退到自身的 rectTransform。
/// </summary>
public Transform ContentTransform
{
get
{
if (m_ScrollRect != null && m_ScrollRect.content != null)
return m_ScrollRect.content;
return rectTransform;
}
}
/// <summary>
/// 基于四叉树查询结果渲染可见象限。
/// 每个均匀象限渲染整块背景+边框,然后遍历象限内单元格渲染文本/预制体/选择高亮。
/// </summary>
private void RenderQuadrants(List<QuadrantResult> quadrants)
{
string ns = m_DefaultStyleNamespace;
#if UNITY_EDITOR
if (!Application.isPlaying)
{
Color borderColor = GetCellBorderColor(ns);
float borderWidth = GetCellBorderWidth(ns);
Color selectionColor = GetCellSelectionBgColor(ns);
Color multiSelectColor = new Color(selectionColor.r, selectionColor.g, selectionColor.b, selectionColor.a * 0.4f);
Color focusColor = new Color(selectionColor.r + 0.04f, selectionColor.g + 0.13f, selectionColor.b + 0.13f, selectionColor.a * 1.4f);
foreach (var q in quadrants)
{
if (string.IsNullOrEmpty(q.Namespace)) continue;
Rect r = q.Region;
string qNs = q.Namespace;
// 象限整块背景
var bgImg = m_Assembler.GetImage("quad_bg", qNs);
bgImg.Rect.anchoredPosition = new Vector2(r.x, -r.y);
bgImg.Rect.sizeDelta = new Vector2(r.width, r.height);
bgImg.Image.color = GetCellBgColor(qNs);
// 象限底部边框
var hLine = m_Assembler.GetImage("quad_hline");
hLine.Rect.anchoredPosition = new Vector2(r.x, -r.y - r.height);
hLine.Rect.sizeDelta = new Vector2(r.width, borderWidth);
hLine.Image.color = borderColor;
// 象限右侧边框
var vLine = m_Assembler.GetImage("quad_vline");
vLine.Rect.anchoredPosition = new Vector2(r.x + r.width - borderWidth, -r.y);
vLine.Rect.sizeDelta = new Vector2(borderWidth, r.height);
vLine.Image.color = borderColor;
// 渲染象限内各单元格的文本/预制体/选择高亮
if (q.Cells != null)
{
foreach (var (row, col) in q.Cells)
{
// 跳过被合并覆盖的单元格
if (TableData.IsCellMerged(row, col)) continue;
float cellX = GetColLeftX(col);
float cellY = -GetRowTopY(row);
float cellW = m_ColWidths[col];
float cellH = m_RowHeights[row];
// 合并源:扩展尺寸
if (TableData.IsMergeSource(row, col, out int mergeRS, out int mergeCS))
{
for (int i = 1; i < mergeCS && col + i < m_ColWidths.Length; i++)
cellW += m_ColWidths[col + i];
for (int i = 1; i < mergeRS && row + i < m_RowHeights.Length; i++)
cellH += m_RowHeights[row + i];
}
XTableCellData data = TableData.GetCell(row, col);
string cellNS = (data != null && !string.IsNullOrEmpty(data.StyleNamespace))
? data.StyleNamespace : ns;
// 选择高亮
if (IsInSelectionRange(row, col))
{
var selImg = m_Assembler.GetImage("cell_multiselect");
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
selImg.Rect.sizeDelta = new Vector2(cellW, cellH);
selImg.Image.color = (row == SelectionHandle.StartRow && col == SelectionHandle.StartCol)
? focusColor : multiSelectColor;
}
else if (row == SelectionHandle.StartRow && col == SelectionHandle.StartCol)
{
var selImg = m_Assembler.GetImage("cell_select");
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
selImg.Rect.sizeDelta = new Vector2(cellW, cellH);
selImg.Image.color = selectionColor;
}
if (data == null) continue;
if (!string.IsNullOrEmpty(data.Text))
{
var txt = m_Assembler.GetText("cell_text", cellNS);
txt.Rect.anchoredPosition = new Vector2(cellX + 4, cellY);
txt.Rect.sizeDelta = new Vector2(cellW - 8, cellH);
txt.Text.text = data.Text;
txt.Text.color = GetCellFgColor(cellNS);
txt.Text.fontSize = GetCellFontSize(cellNS);
txt.Text.alignment = TextAlignmentOptions.Midline;
txt.Text.font = GetCellFontAsset(cellNS) ?? txt.Text.font;
}
if (data.Prefab != null)
PositionPrefabInstance(row, col, data.Prefab, cellX, cellY, cellW, cellH);
}
}
}
return;
}
#endif
// 运行时模式
Color rtSelectionColor = RT_SELECTION_COLOR;
Color rtMultiSelectColor = RT_MULTI_SELECT_COLOR;
Color rtFocusColor = RT_FOCUS_COLOR;
foreach (var q in quadrants)
{
if (string.IsNullOrEmpty(q.Namespace)) continue;
Rect r = q.Region;
string qNs = q.Namespace;
var bgImg = m_Assembler.GetImage("quad_bg", qNs);
bgImg.Rect.anchoredPosition = new Vector2(r.x, -r.y);
bgImg.Rect.sizeDelta = new Vector2(r.width, r.height);
bgImg.Image.color = GetCellBgColor(qNs);
var hLine = m_Assembler.GetImage("quad_hline");
hLine.Rect.anchoredPosition = new Vector2(r.x, -r.y - r.height);
hLine.Rect.sizeDelta = new Vector2(r.width, RT_BORDER_WIDTH);
hLine.Image.color = RT_BORDER_COLOR;
var vLine = m_Assembler.GetImage("quad_vline");
vLine.Rect.anchoredPosition = new Vector2(r.x + r.width - RT_BORDER_WIDTH, -r.y);
vLine.Rect.sizeDelta = new Vector2(RT_BORDER_WIDTH, r.height);
vLine.Image.color = RT_BORDER_COLOR;
if (q.Cells != null)
{
foreach (var (row, col) in q.Cells)
{
// 跳过被合并覆盖的单元格
if (TableData.IsCellMerged(row, col)) continue;
float cellX = GetColLeftX(col);
float cellY = -GetRowTopY(row);
float cellW = m_ColWidths[col];
float cellH = m_RowHeights[row];
// 合并源:扩展尺寸
if (TableData.IsMergeSource(row, col, out int mergeRS, out int mergeCS))
{
for (int i = 1; i < mergeCS && col + i < m_ColWidths.Length; i++)
cellW += m_ColWidths[col + i];
for (int i = 1; i < mergeRS && row + i < m_RowHeights.Length; i++)
cellH += m_RowHeights[row + i];
}
XTableCellData data = TableData.GetCell(row, col);
string cellNS = (data != null && !string.IsNullOrEmpty(data.StyleNamespace))
? data.StyleNamespace : ns;
if (IsInSelectionRange(row, col))
{
var selImg = m_Assembler.GetImage("cell_multiselect");
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
selImg.Rect.sizeDelta = new Vector2(cellW, cellH);
selImg.Image.color = (row == SelectionHandle.StartRow && col == SelectionHandle.StartCol)
? rtFocusColor : rtMultiSelectColor;
}
else if (row == SelectionHandle.StartRow && col == SelectionHandle.StartCol)
{
var selImg = m_Assembler.GetImage("cell_select");
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
selImg.Rect.sizeDelta = new Vector2(cellW, cellH);
selImg.Image.color = rtSelectionColor;
}
if (data == null) continue;
if (!string.IsNullOrEmpty(data.Text))
{
var txt = m_Assembler.GetText("cell_text", cellNS);
txt.Rect.anchoredPosition = new Vector2(cellX + 4, cellY);
txt.Rect.sizeDelta = new Vector2(cellW - 8, cellH);
txt.Text.text = data.Text;
}
if (data.Prefab != null)
PositionPrefabInstance(row, col, data.Prefab, cellX, cellY, cellW, cellH);
}
}
}
}
#endregion
@@ -141,7 +355,7 @@ namespace XericUI.XTable.Rendering.Component
if (m_TableData == null)
m_TableData = new XTableData();
m_Assembler = new CellAssembler(rectTransform, m_DefaultStyleNamespace);
m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace);
RecalculateTotalSize();
EnsureDefaultStyleNamespace();
CreateDefaultSampleData();
@@ -191,6 +405,7 @@ namespace XericUI.XTable.Rendering.Component
// 销毁所有动态生成的对象
if (m_Assembler != null)
m_Assembler.DestroyAll();
ClearPrefabInstances();
if (m_BackgroundGo != null)
{
if (Application.isPlaying) Destroy(m_BackgroundGo);
@@ -422,13 +637,8 @@ namespace XericUI.XTable.Rendering.Component
newArr[i] = m_RowHeights[i + 1];
m_RowHeights = newArr;
// 更新选中状态
if (m_SelectedRow >= m_RowHeights.Length)
m_SelectedRow = Mathf.Max(-1, m_RowHeights.Length - 1);
if (m_SelectStartRow >= m_RowHeights.Length)
m_SelectStartRow = Mathf.Max(-1, m_RowHeights.Length - 1);
if (m_SelectEndRow >= m_RowHeights.Length)
m_SelectEndRow = Mathf.Max(-1, m_RowHeights.Length - 1);
// 裁剪选区到新尺寸
ClampSelectionToBounds();
MarkDirty(TableDirtyType.LayoutChanged | TableDirtyType.DataChanged | TableDirtyType.SelectionChanged);
#if UNITY_EDITOR
@@ -474,13 +684,8 @@ namespace XericUI.XTable.Rendering.Component
newArr[i] = m_ColWidths[i + 1];
m_ColWidths = newArr;
// 更新选中状态
if (m_SelectedCol >= m_ColWidths.Length)
m_SelectedCol = Mathf.Max(-1, m_ColWidths.Length - 1);
if (m_SelectStartCol >= m_ColWidths.Length)
m_SelectStartCol = Mathf.Max(-1, m_ColWidths.Length - 1);
if (m_SelectEndCol >= m_ColWidths.Length)
m_SelectEndCol = Mathf.Max(-1, m_ColWidths.Length - 1);
// 裁剪选区到新尺寸
ClampSelectionToBounds();
MarkDirty(TableDirtyType.LayoutChanged | TableDirtyType.DataChanged | TableDirtyType.SelectionChanged);
#if UNITY_EDITOR
@@ -488,53 +693,38 @@ namespace XericUI.XTable.Rendering.Component
#endif
}
/// <summary>设置外部选择句柄(替换内部句柄)</summary>
public void SetSelectionHandle(XTableSelectionHandle handle)
{
if (m_SelectionHandle != null)
m_SelectionHandle.OnSelectionChanged -= OnHandleSelectionChanged;
m_SelectionHandle = handle;
if (m_SelectionHandle != null)
m_SelectionHandle.OnSelectionChanged += OnHandleSelectionChanged;
MarkDirty(TableDirtyType.SelectionChanged);
}
/// <summary>单选一个单元格</summary>
public void SelectCell(int row, int col)
{
ClearMultiSelection();
m_SelectedRow = row;
m_SelectedCol = col;
SelectionHandle.Select(row, col);
MarkDirty(TableDirtyType.SelectionChanged);
}
/// <summary>多选一个范围</summary>
public void SelectCell(int startRow, int startCol, int endRow, int endCol)
{
SelectionHandle.Select(startRow, startCol, endRow, endCol);
MarkDirty(TableDirtyType.SelectionChanged);
}
/// <summary>清除选择</summary>
public void ClearSelection()
{
m_SelectedRow = -1;
m_SelectedCol = -1;
ClearMultiSelection();
SelectionHandle.Clear();
MarkDirty(TableDirtyType.SelectionChanged);
}
/// <summary>开始多选(以当前格为锚点)</summary>
public void StartMultiSelect(int row, int col)
{
m_SelectStartRow = row;
m_SelectStartCol = col;
m_SelectEndRow = row;
m_SelectEndCol = col;
m_SelectedRow = row;
m_SelectedCol = col;
MarkDirty(TableDirtyType.SelectionChanged);
}
/// <summary>扩展多选范围到目标格</summary>
public void ExtendMultiSelect(int row, int col)
{
m_SelectEndRow = row;
m_SelectEndCol = col;
m_SelectedRow = row;
m_SelectedCol = col;
MarkDirty(TableDirtyType.SelectionChanged);
}
/// <summary>清除多选范围</summary>
public void ClearMultiSelection()
{
m_SelectStartRow = -1;
m_SelectStartCol = -1;
m_SelectEndRow = -1;
m_SelectEndCol = -1;
}
/// <summary>通过文本选择一个单元格(如 "A0")</summary>
public bool SelectCellByText(string text)
{
@@ -551,30 +741,36 @@ namespace XericUI.XTable.Rendering.Component
out int sr, out int sc, out int er, out int ec))
return false;
ClearSelection();
m_SelectStartRow = sr;
m_SelectStartCol = sc;
m_SelectEndRow = er;
m_SelectEndCol = ec;
m_SelectedRow = sr;
m_SelectedCol = sc;
MarkDirty(TableDirtyType.SelectionChanged);
SelectCell(sr, sc, er, ec);
return true;
}
/// <summary>获取当前选区的文本描述(单格 "A0",多格 "A0:B1")</summary>
public string GetSelectionText()
{
if (m_SelectedRow < 0 || m_SelectedCol < 0) return string.Empty;
if (HasMultiSelection &&
(m_SelectStartRow != m_SelectEndRow || m_SelectStartCol != m_SelectEndCol))
if (!SelectionHandle.HasSelection) return string.Empty;
if (SelectionHandle.HasMultiSelection)
{
return XTableCoordinateFormat.RangeToText(
m_SelectStartRow, m_SelectStartCol,
m_SelectEndRow, m_SelectEndCol);
SelectionHandle.MinRow, SelectionHandle.MinCol,
SelectionHandle.MaxRow, SelectionHandle.MaxCol);
}
return XTableCoordinateFormat.CellToText(m_SelectedRow, m_SelectedCol);
return XTableCoordinateFormat.CellToText(
SelectionHandle.StartRow, SelectionHandle.StartCol);
}
private void OnHandleSelectionChanged(XTableSelectionHandle handle)
{
MarkDirty(TableDirtyType.SelectionChanged);
}
/// <summary>裁剪选区到当前行列尺寸范围内</summary>
private void ClampSelectionToBounds()
{
int maxR = (m_RowHeights?.Length ?? 0) - 1;
int maxC = (m_ColWidths?.Length ?? 0) - 1;
SelectionHandle.ClampTo(maxR, maxC);
}
public void RefreshView()
@@ -583,7 +779,7 @@ namespace XericUI.XTable.Rendering.Component
if (m_Assembler == null)
{
if (rectTransform != null)
m_Assembler = new CellAssembler(rectTransform, m_DefaultStyleNamespace);
m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace);
else return;
}
@@ -607,14 +803,18 @@ namespace XericUI.XTable.Rendering.Component
bgRt.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight);
}
// 清理上一次渲染的预制体实例
ClearPrefabInstances();
// 回收池
m_Assembler.ReturnAll();
// 额外清理孤立的直接子对象(防止域重载或异常流程产生的残留)
Transform poolRoot = m_Assembler.PoolRoot;
for (int i = rectTransform.childCount - 1; i >= 0; i--)
Transform contentT = ContentTransform;
for (int i = contentT.childCount - 1; i >= 0; i--)
{
var child = rectTransform.GetChild(i);
var child = contentT.GetChild(i);
if ((poolRoot != null && child == poolRoot)) continue;
if (m_BackgroundGo != null && child.gameObject == m_BackgroundGo) continue;
@@ -625,14 +825,21 @@ namespace XericUI.XTable.Rendering.Component
DestroyImmediate(child.gameObject);
}
// 计算可见范围
int sr, er, sc, ec;
CalcVisibleRange(out sr, out er, out sc, out ec);
if (er <= sr || ec <= sc) return;
// 渲染每个可见单元格
RenderCells(sr, er, sc, ec);
// 渲染可见单元格:优先使用四叉树查询
if (m_Quadtree != null && m_Quadtree.IsBuilt)
{
var quadrants = new List<QuadrantResult>();
m_Quadtree.QueryVisible(m_VisibleRect, quadrants);
RenderQuadrants(quadrants);
}
else
{
// 回退:传统行列遍历
int sr, er, sc, ec;
CalcVisibleRange(out sr, out er, out sc, out ec);
if (er > sr && ec > sc)
RenderCells(sr, er, sc, ec);
}
m_DirtyFlag.Clear();
OnTableRefreshed?.Invoke();
@@ -667,13 +874,14 @@ namespace XericUI.XTable.Rendering.Component
/// <summary>销毁 transform 下所有子对象(用于域重载后清理残留)</summary>
private void ClearAllChildren()
{
if (rectTransform == null) return;
Transform t = ContentTransform;
if (t == null) return;
#if UNITY_EDITOR
// 编辑器模式下先重置 HideFlags 再销毁,防止 DontSave 对象拒绝销毁
while (rectTransform.childCount > 0)
while (t.childCount > 0)
{
var child = rectTransform.GetChild(0);
var child = t.GetChild(0);
child.gameObject.hideFlags = HideFlags.None;
if (Application.isPlaying)
Destroy(child.gameObject);
@@ -681,9 +889,9 @@ namespace XericUI.XTable.Rendering.Component
DestroyImmediate(child.gameObject);
}
#else
for (int i = rectTransform.childCount - 1; i >= 0; i--)
for (int i = t.childCount - 1; i >= 0; i--)
{
var child = rectTransform.GetChild(i);
var child = t.GetChild(i);
Destroy(child.gameObject);
}
#endif
@@ -694,10 +902,11 @@ namespace XericUI.XTable.Rendering.Component
{
m_ChildrenVisible = visible;
#if UNITY_EDITOR
if (rectTransform == null) return;
for (int i = rectTransform.childCount - 1; i >= 0; i--)
Transform t = ContentTransform;
if (t == null) return;
for (int i = t.childCount - 1; i >= 0; i--)
{
var child = rectTransform.GetChild(i);
var child = t.GetChild(i);
child.gameObject.hideFlags = visible
? HideFlags.DontSave
: HideFlags.DontSave | HideFlags.HideInHierarchy;
@@ -715,7 +924,7 @@ namespace XericUI.XTable.Rendering.Component
m_TableData = new XTableData();
m_Assembler = new CellAssembler(rectTransform, m_DefaultStyleNamespace);
m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace);
RecalculateTotalSize();
CreateDefaultSampleData();
CreateBackground();
@@ -758,6 +967,48 @@ namespace XericUI.XTable.Rendering.Component
SetChildrenVisible(m_ChildrenVisible);
}
/// <summary>
/// 清空所有数据(单元格文本、合并信息),但保留行列排布和样式。
/// 等价于"清空内容但不改变表格结构"。
/// </summary>
public void ClearData()
{
if (m_TableData != null)
{
m_TableData.ClearAllMerges();
m_TableData.BlockMap.Clear();
}
ClearPrefabInstances();
m_Quadtree?.Clear();
ClearSelection();
MarkDirty(TableDirtyType.DataChanged);
RefreshView();
}
/// <summary>
/// 清空表格所有内容——数据、行列、样式,等同 <see cref="FullRebuild"/> 但不创建默认示例数据。
/// </summary>
public void ClearTable()
{
m_Assembler?.DestroyAll();
ClearPrefabInstances();
m_Quadtree?.Clear();
ClearAllChildren();
m_BackgroundGo = null;
m_TableData = new XTableData();
m_RowHeights = null;
m_ColWidths = null;
ClearSelection();
m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace);
MarkDirty(TableDirtyType.All);
RecalculateTotalSize();
CreateBackground();
RefreshView();
SetChildrenVisible(m_ChildrenVisible);
}
/// <summary>仅强制刷新样式,不重建数据结构</summary>
public void ForceStyleRefresh()
{
@@ -848,10 +1099,10 @@ namespace XericUI.XTable.Rendering.Component
var selImg = m_Assembler.GetImage("cell_multiselect");
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
selImg.Rect.sizeDelta = new Vector2(cellW, cellH);
selImg.Image.color = (r == m_SelectedRow && c == m_SelectedCol)
selImg.Image.color = (r == SelectionHandle.StartRow && c == SelectionHandle.StartCol)
? focusColor : multiSelectColor;
}
else if (r == m_SelectedRow && c == m_SelectedCol)
else if (r == SelectionHandle.StartRow && c == SelectionHandle.StartCol)
{
var selImg = m_Assembler.GetImage("cell_select");
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
@@ -884,6 +1135,10 @@ namespace XericUI.XTable.Rendering.Component
txt.Text.alignment = TextAlignmentOptions.Midline;
txt.Text.font = GetCellFontAsset(cellNS) ?? txt.Text.font;
}
// 预制体渲染
if (data.Prefab != null)
PositionPrefabInstance(r, c, data.Prefab, cellX, cellY, cellW, cellH);
}
}
return;
@@ -925,10 +1180,10 @@ namespace XericUI.XTable.Rendering.Component
var selImg = m_Assembler.GetImage("cell_multiselect");
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
selImg.Rect.sizeDelta = new Vector2(cellW, cellH);
selImg.Image.color = (r == m_SelectedRow && c == m_SelectedCol)
selImg.Image.color = (r == SelectionHandle.StartRow && c == SelectionHandle.StartCol)
? rtFocusColor : rtMultiSelectColor;
}
else if (r == m_SelectedRow && c == m_SelectedCol)
else if (r == SelectionHandle.StartRow && c == SelectionHandle.StartCol)
{
var selImg = m_Assembler.GetImage("cell_select");
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
@@ -956,12 +1211,71 @@ namespace XericUI.XTable.Rendering.Component
txt.Text.text = data.Text;
// 运行时样式由 StyleBoundElement 被动更新,此处仅设置文本内容
}
// 预制体渲染
if (data.Prefab != null)
PositionPrefabInstance(r, c, data.Prefab, cellX, cellY, cellW, cellH);
}
}
}
#endregion
#region 预制体管理
/// <summary>获取或创建指定单元格的预制体实例</summary>
private GameObject GetOrCreatePrefabInstance(int row, int col, GameObject prefab)
{
if (m_PrefabInstances == null)
m_PrefabInstances = new Dictionary<(int, int), GameObject>();
var key = (row, col);
if (m_PrefabInstances.TryGetValue(key, out GameObject existing) && existing != null)
return existing;
GameObject instance = Instantiate(prefab, ContentTransform);
instance.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy;
instance.name = $"_prefab_{row}_{col}";
m_PrefabInstances[key] = instance;
return instance;
}
/// <summary>定位预制体实例到指定单元格区域(锚点左上角,枢轴左上角)</summary>
private void PositionPrefabInstance(int row, int col, GameObject prefab, float cellX, float cellY, float cellW, float cellH)
{
GameObject instance = GetOrCreatePrefabInstance(row, col, prefab);
if (instance == null) return;
RectTransform prt = instance.GetComponent<RectTransform>();
if (prt == null) prt = instance.AddComponent<RectTransform>();
prt.anchorMin = new Vector2(0, 1);
prt.anchorMax = new Vector2(0, 1);
prt.pivot = new Vector2(0, 1);
prt.anchoredPosition = new Vector2(cellX, cellY);
prt.sizeDelta = new Vector2(cellW, cellH);
}
/// <summary>销毁所有已缓存的预制体实例</summary>
private void ClearPrefabInstances()
{
if (m_PrefabInstances == null) return;
foreach (var kv in m_PrefabInstances)
{
if (kv.Value != null)
{
if (Application.isPlaying)
Destroy(kv.Value);
else
DestroyImmediate(kv.Value);
}
}
m_PrefabInstances.Clear();
}
#endregion
#region 坐标计算
private float GetRowTopY(int row)
@@ -1116,7 +1430,14 @@ namespace XericUI.XTable.Rendering.Component
#region 其他
private void MarkDirty(TableDirtyType type) => m_DirtyFlag.Mark(type);
private void MarkDirty(TableDirtyType type)
{
m_DirtyFlag.Mark(type);
// 数据或布局变更时重建四叉树
if ((type & (TableDirtyType.DataChanged | TableDirtyType.LayoutChanged)) != 0)
MaintainQuadtree();
}
/// <summary>运行时样式变更回调:标记脏数据,下一帧重绘</summary>
private void OnPoolStyleChanged() => MarkDirty(TableDirtyType.StyleChanged);
@@ -1125,6 +1446,46 @@ namespace XericUI.XTable.Rendering.Component
#endregion
#region 四叉树
/// <summary>强制重建四叉树(外部调用入口,适合编辑器调试或手动刷新)</summary>
public void ForceRebuildQuadtree()
{
if (m_Quadtree == null)
m_Quadtree = new XTableQuadtree();
MaintainQuadtree();
}
/// <summary>从当前 XTableData 收集单元格信息并重建四叉树</summary>
private void MaintainQuadtree()
{
if (m_TableData == null || m_RowHeights == null || m_ColWidths == null)
return;
if (m_Quadtree == null)
m_Quadtree = new XTableQuadtree();
// 收集所有有数据的单元格(跳过被合并覆盖的子单元格)
var cells = new List<XTableQuadtreeCell>();
for (int r = 0; r < m_RowHeights.Length; r++)
{
for (int c = 0; c < m_ColWidths.Length; c++)
{
if (m_TableData.IsCellMerged(r, c)) continue;
var data = m_TableData.GetCell(r, c);
if (data == null) continue;
string ns = !string.IsNullOrEmpty(data.StyleNamespace)
? data.StyleNamespace : m_DefaultStyleNamespace;
cells.Add(new XTableQuadtreeCell { Row = r, Col = c, Namespace = ns });
}
}
m_Quadtree.Build(m_TotalWidth, m_TotalHeight, m_RowHeights, m_ColWidths, cells);
}
#endregion
#region IPointerClickHandler
public void OnPointerClick(PointerEventData eventData)