From 40fbfed45c3f66327638c04ec4f71087d5db2cfd Mon Sep 17 00:00:00 2001 From: lrc <571244399@qq.com> Date: Tue, 30 Jun 2026 19:53:12 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A8=E6=A0=BC=E6=B7=BB=E5=8A=A0=E5=9B=9B?= =?UTF-8?q?=E5=8F=89=E6=A0=91=E6=B8=B2=E6=9F=93=E4=BC=98=E5=8C=96=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Editor/XTable/XericUIActionTableEditor.cs | 53 +- .../XTable/XericUIActionTableSceneEditor.cs | 108 ++-- Runtime/XTable/Core/XTableQuadtree.cs | 331 ++++++++++ Runtime/XTable/Core/XTableQuadtree.cs.meta | 11 + Runtime/XTable/Core/XTableSelectionHandle.cs | 194 ++++++ .../XTable/Core/XTableSelectionHandle.cs.meta | 11 + .../Rendering/Component/XericUIActionTable.cs | 585 ++++++++++++++---- 7 files changed, 1104 insertions(+), 189 deletions(-) create mode 100644 Runtime/XTable/Core/XTableQuadtree.cs create mode 100644 Runtime/XTable/Core/XTableQuadtree.cs.meta create mode 100644 Runtime/XTable/Core/XTableSelectionHandle.cs create mode 100644 Runtime/XTable/Core/XTableSelectionHandle.cs.meta diff --git a/Editor/XTable/XericUIActionTableEditor.cs b/Editor/XTable/XericUIActionTableEditor.cs index c686f88..a147cef 100644 --- a/Editor/XTable/XericUIActionTableEditor.cs +++ b/Editor/XTable/XericUIActionTableEditor.cs @@ -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(); + rootRt.sizeDelta = new Vector2(800, 600); + rootGo.GetComponent().color = new Color(0.1f, 0.1f, 0.1f, 0.3f); + + ScrollRect scrollRect = rootGo.GetComponent(); + XericUIActionTable table = rootGo.GetComponent(); + + // 2. 创建 Content(纯 RectTransform,由表格组件管理动态子对象) + GameObject contentGo = new GameObject("Content", typeof(RectTransform)); RectTransform contentRt = contentGo.GetComponent(); 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(); - // 2. 创建 Viewport(Mask 裁剪区域) + // 3. 创建 Viewport(Mask 裁剪区域) GameObject viewportGo = new GameObject("Viewport", typeof(RectTransform), typeof(Image), typeof(Mask)); viewportGo.GetComponent().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(); @@ -217,7 +225,7 @@ namespace XericUIEditor.XTable RectTransform hSlidingRt = hSlidingGo.GetComponent(); 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(); hHandleRt.anchorMin = Vector2.zero; hHandleRt.anchorMax = Vector2.one; - hHandleRt.sizeDelta = new Vector2(-20, -20); + hHandleRt.sizeDelta = new Vector2(-10, -10); hHandleGo.GetComponent().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(); @@ -249,7 +257,7 @@ namespace XericUIEditor.XTable RectTransform vSlidingRt = vSlidingGo.GetComponent(); 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(); vHandleRt.anchorMin = Vector2.zero; vHandleRt.anchorMax = Vector2.one; - vHandleRt.sizeDelta = new Vector2(-20, -20); + vHandleRt.sizeDelta = new Vector2(-10, -10); vHandleGo.GetComponent().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(); - scrollRt.sizeDelta = new Vector2(800, 600); - scrollGo.GetComponent().color = new Color(0.1f, 0.1f, 0.1f, 0.3f); - - ScrollRect scrollRect = scrollGo.GetComponent(); + // 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() diff --git a/Editor/XTable/XericUIActionTableSceneEditor.cs b/Editor/XTable/XericUIActionTableSceneEditor.cs index 412ad3b..7cabcce 100644 --- a/Editor/XTable/XericUIActionTableSceneEditor.cs +++ b/Editor/XTable/XericUIActionTableSceneEditor.cs @@ -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(); + // 使用 Content 的 RectTransform 进行定位计算(而非表格组件所在的 ScrollRect 根节点) + RectTransform tableRt = table.ContentTransform.RectTransform(); + if (tableRt == null) tableRt = table.GetComponent(); 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); diff --git a/Runtime/XTable/Core/XTableQuadtree.cs b/Runtime/XTable/Core/XTableQuadtree.cs new file mode 100644 index 0000000..1ec3b63 --- /dev/null +++ b/Runtime/XTable/Core/XTableQuadtree.cs @@ -0,0 +1,331 @@ +using System.Collections.Generic; + +using UnityEngine; + +namespace XericUI.XTable.Core +{ + /// + /// 表格四叉树构建用的单元格元数据。 + /// + public struct XTableQuadtreeCell + { + public int Row; + public int Col; + public string Namespace; + } + + /// + /// 四叉树查询结果——一个均匀象限的可见部分。 + /// + public struct QuadrantResult + { + /// 该象限在表格像素空间中的区域(左上角坐标系,y 向下为正) + public Rect Region; + + /// 统一命名空间(空字符串 = 空区域) + public string Namespace; + + /// 该象限包含的单元格坐标列表 + public List<(int row, int col)> Cells; + } + + /// + /// 表格四叉树——基于单元格 进行递归区域分割。 + /// 若一个象限内存在不同命名空间的单元格,则继续分裂,直到所有差异分开。 + /// 渲染阶段通过 获取可见范围内的所有均匀象限。 + /// + public class XTableQuadtree + { + #region 内部节点 + + private class Node + { + /// 当前象限在表格像素空间中的区域 + public Rect Region; + + /// + /// null = 非均匀节点(有子节点),非 null = 均匀叶子节点。 + /// 空字符串 "" 表示该区域无任何单元格。 + /// + public string UniformNamespace; + + /// 子象限(null 表示叶子节点) + public Node[] Children; + + /// 该均匀象限包含的单元格坐标(仅叶子均匀节点有效) + public List<(int, int)> Cells; + + public bool IsUniform => UniformNamespace != null; + } + + #endregion + + #region 字段 + + private Node m_Root; + private float[] m_RowHeights; + private float[] m_ColWidths; + + /// 四叉树是否已构建完成 + public bool IsBuilt { get; private set; } + + #endregion + + #region 构建 + + /// + /// 全量构建四叉树。 + /// + /// 表格总宽度(像素) + /// 表格总高度(像素) + /// 行高数组(索引 = 行号) + /// 列宽数组(索引 = 列号) + /// 所有有数据的单元格元数据列表 + public void Build(float totalWidth, float totalHeight, + float[] rowHeights, float[] colWidths, + List 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; + } + + /// 强制重建(外部入口,通常由表格组件在数据/布局变更后调用) + public void ForceRebuild(float totalWidth, float totalHeight, + float[] rowHeights, float[] colWidths, + List cells) + { + Clear(); + Build(totalWidth, totalHeight, rowHeights, colWidths, cells); + } + + private void BuildNode(Node node, List allCells, + int minRow, int maxRow, int minCol, int maxCol) + { + // 收集当前行列范围内的单元格 + var cellsInRange = new List(); + 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 查询 + + /// + /// 查询可见矩形内的所有均匀象限。 + /// + /// 可见矩形(左上角坐标系) + /// 结果列表(复用外部容器) + public void QueryVisible(Rect visibleRect, List results) + { + if (results == null) + results = new List(); + if (m_Root == null) + return; + + QueryNode(m_Root, visibleRect, results); + } + + private void QueryNode(Node node, Rect visibleRect, List 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 清理 + + /// 清空四叉树 + 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 工具 + + /// 命名空间规范化:null、空串、默认命名空间统一为 "xeric_table_default" + 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 + } +} diff --git a/Runtime/XTable/Core/XTableQuadtree.cs.meta b/Runtime/XTable/Core/XTableQuadtree.cs.meta new file mode 100644 index 0000000..02daff4 --- /dev/null +++ b/Runtime/XTable/Core/XTableQuadtree.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 90a1712e28eeec248b95350379e42764 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Core/XTableSelectionHandle.cs b/Runtime/XTable/Core/XTableSelectionHandle.cs new file mode 100644 index 0000000..711cf19 --- /dev/null +++ b/Runtime/XTable/Core/XTableSelectionHandle.cs @@ -0,0 +1,194 @@ +using System; + +using UnityEngine; + +namespace XericUI.XTable.Core +{ + /// + /// 表格选择句柄——统一管理单选/多选状态。 + /// 外部可创建此句柄并通过表格的 SetSelectionHandle 或直接操作句柄来控制选区。 + /// 选区变更时触发 OnSelectionChanged 事件,表格监听此事件进行重绘。 + /// + public class XTableSelectionHandle + { + #region 属性 + + /// 起始行(多选锚点行,单选当前行) + public int StartRow { get; private set; } = -1; + + /// 起始列(多选锚点列,单选当前列) + public int StartCol { get; private set; } = -1; + + /// 结束行(仅多选有效,等于 StartRow 时为单选) + public int EndRow { get; private set; } = -1; + + /// 结束列(仅多选有效,等于 StartCol 时为单选) + public int EndCol { get; private set; } = -1; + + /// 是否有有效选择 + public bool HasSelection => StartRow >= 0 && StartCol >= 0; + + /// 是否为多选(起止不同) + public bool HasMultiSelection => + HasSelection && + (StartRow != EndRow || StartCol != EndCol); + + /// 选中区域最小行 + public int MinRow => HasSelection ? Mathf.Min(StartRow, EndRow) : -1; + + /// 选中区域最大行 + public int MaxRow => HasSelection ? Mathf.Max(StartRow, EndRow) : -1; + + /// 选中区域最小列 + public int MinCol => HasSelection ? Mathf.Min(StartCol, EndCol) : -1; + + /// 选中区域最大列 + public int MaxCol => HasSelection ? Mathf.Max(StartCol, EndCol) : -1; + + #endregion + + #region 事件 + + /// 选区变化时触发 + public event Action OnSelectionChanged; + + #endregion + + #region 公开方法 + + /// + /// 单选一个单元格(同时清除多选范围) + /// + public void Select(int row, int col) + { + StartRow = row; + StartCol = col; + EndRow = row; + EndCol = col; + NotifyChanged(); + } + + /// + /// 多选一个范围 + /// + public void Select(int startRow, int startCol, int endRow, int endCol) + { + StartRow = startRow; + StartCol = startCol; + EndRow = endRow; + EndCol = endCol; + NotifyChanged(); + } + + /// + /// 扩展多选范围到目标格(保持原有锚点) + /// + public void ExtendTo(int row, int col) + { + if (!HasSelection) + { + Select(row, col); + return; + } + EndRow = row; + EndCol = col; + NotifyChanged(); + } + + /// + /// 清除选择 + /// 清除选择 + public void Clear() + { + if (!HasSelection) return; + StartRow = -1; + StartCol = -1; + EndRow = -1; + EndCol = -1; + NotifyChanged(); + } + + /// 将选区裁剪到 [0, maxRow] × [0, maxCol] 范围 + 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(); + } + + /// + /// 判断指定坐标是否在选中范围内 + /// + 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; + } + + /// 左移一格 + 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; + } + + /// 右移一格 + public int MoveRight(int maxCol) + { + if (!HasSelection) return -1; + int newCol = Mathf.Min(maxCol, StartCol + 1); + if (newCol != StartCol) + Select(StartRow, newCol); + return newCol; + } + + /// 上移一格 + 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; + } + + /// 下移一格 + 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 + } +} diff --git a/Runtime/XTable/Core/XTableSelectionHandle.cs.meta b/Runtime/XTable/Core/XTableSelectionHandle.cs.meta new file mode 100644 index 0000000..74fc87a --- /dev/null +++ b/Runtime/XTable/Core/XTableSelectionHandle.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 872c2bc5f6f1a3f47a655bdf059656bd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs index 6e23ddd..178a5cc 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs @@ -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; + /// 预制体实例缓存,key 为 (row, col),用于复用已实例化的预制体 + [System.NonSerialized] private Dictionary<(int, int), GameObject> m_PrefabInstances; + + /// 表格四叉树——按 StyleNamespace 分区,渲染阶段查询可见象限 + [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; + /// 选中操作句柄——外部可通过此句柄控制选区,或直接操作 StartRow/StartCol 等属性 + 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; /// 动态子对象在 Hierarchy 中是否可见(仅编辑器有效) public bool ChildrenVisible @@ -119,12 +131,214 @@ namespace XericUI.XTable.Rendering.Component /// 判断单元格是否在多选范围内 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); + } + + /// + /// 动态子对象挂载的父节点。 + /// 若绑定了 ScrollRect 则使用其 Content,否则回退到自身的 rectTransform。 + /// + public Transform ContentTransform + { + get + { + if (m_ScrollRect != null && m_ScrollRect.content != null) + return m_ScrollRect.content; + return rectTransform; + } + } + + /// + /// 基于四叉树查询结果渲染可见象限。 + /// 每个均匀象限渲染整块背景+边框,然后遍历象限内单元格渲染文本/预制体/选择高亮。 + /// + private void RenderQuadrants(List 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 } + /// 设置外部选择句柄(替换内部句柄) + 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); + } + + /// 单选一个单元格 public void SelectCell(int row, int col) { - ClearMultiSelection(); - m_SelectedRow = row; - m_SelectedCol = col; + SelectionHandle.Select(row, col); MarkDirty(TableDirtyType.SelectionChanged); } + /// 多选一个范围 + public void SelectCell(int startRow, int startCol, int endRow, int endCol) + { + SelectionHandle.Select(startRow, startCol, endRow, endCol); + MarkDirty(TableDirtyType.SelectionChanged); + } + + /// 清除选择 public void ClearSelection() { - m_SelectedRow = -1; - m_SelectedCol = -1; - ClearMultiSelection(); + SelectionHandle.Clear(); MarkDirty(TableDirtyType.SelectionChanged); } - /// 开始多选(以当前格为锚点) - 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); - } - - /// 扩展多选范围到目标格 - public void ExtendMultiSelect(int row, int col) - { - m_SelectEndRow = row; - m_SelectEndCol = col; - m_SelectedRow = row; - m_SelectedCol = col; - MarkDirty(TableDirtyType.SelectionChanged); - } - - /// 清除多选范围 - public void ClearMultiSelection() - { - m_SelectStartRow = -1; - m_SelectStartCol = -1; - m_SelectEndRow = -1; - m_SelectEndCol = -1; - } - /// 通过文本选择一个单元格(如 "A0") 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; } /// 获取当前选区的文本描述(单格 "A0",多格 "A0:B1") 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); + } + + /// 裁剪选区到当前行列尺寸范围内 + 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(); + 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 /// 销毁 transform 下所有子对象(用于域重载后清理残留) 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); } + /// + /// 清空所有数据(单元格文本、合并信息),但保留行列排布和样式。 + /// 等价于"清空内容但不改变表格结构"。 + /// + public void ClearData() + { + if (m_TableData != null) + { + m_TableData.ClearAllMerges(); + m_TableData.BlockMap.Clear(); + } + ClearPrefabInstances(); + m_Quadtree?.Clear(); + ClearSelection(); + MarkDirty(TableDirtyType.DataChanged); + RefreshView(); + } + + /// + /// 清空表格所有内容——数据、行列、样式,等同 但不创建默认示例数据。 + /// + 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); + } + /// 仅强制刷新样式,不重建数据结构 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 预制体管理 + + /// 获取或创建指定单元格的预制体实例 + 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; + } + + /// 定位预制体实例到指定单元格区域(锚点左上角,枢轴左上角) + 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(); + if (prt == null) prt = instance.AddComponent(); + + 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); + } + + /// 销毁所有已缓存的预制体实例 + 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(); + } /// 运行时样式变更回调:标记脏数据,下一帧重绘 private void OnPoolStyleChanged() => MarkDirty(TableDirtyType.StyleChanged); @@ -1125,6 +1446,46 @@ namespace XericUI.XTable.Rendering.Component #endregion + #region 四叉树 + + /// 强制重建四叉树(外部调用入口,适合编辑器调试或手动刷新) + public void ForceRebuildQuadtree() + { + if (m_Quadtree == null) + m_Quadtree = new XTableQuadtree(); + MaintainQuadtree(); + } + + /// 从当前 XTableData 收集单元格信息并重建四叉树 + 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(); + 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)