diff --git a/Editor/XTable/XericUIActionTableEditor.cs b/Editor/XTable/XericUIActionTableEditor.cs index d75153e..904a259 100644 --- a/Editor/XTable/XericUIActionTableEditor.cs +++ b/Editor/XTable/XericUIActionTableEditor.cs @@ -8,7 +8,7 @@ namespace XericUIEditor.XTable { /// /// XericUIActionTable 的 Inspector 编辑器—— - /// 折叠分组:[样式设定] [尺寸设定] [编辑测试] [数据浏览]。 + /// 折叠分组:[样式设定] [尺寸设定] [调试] [数据浏览]。 /// 使用 EditorGUILayout.Foldout 避免与数组 PropertyField 内部的折叠冲突。 /// [CustomEditor(typeof(XericUIActionTable))] @@ -24,23 +24,11 @@ namespace XericUIEditor.XTable // 折叠状态 private static bool s_StyleFoldout = true; private static bool s_SizeFoldout = true; - private static bool s_EditTestFoldout; + private static bool s_DebugFoldout; private static bool s_DataBrowseFoldout; - // 编辑测试参数 - private int m_BatchRowCount = 10; - private int m_BatchColCount = 10; - private float m_BatchCellHeight = 40f; - private float m_BatchCellWidth = 100f; - - private int m_MergeStartRow; - private int m_MergeStartCol; - private int m_MergeRowSpan = 2; - private int m_MergeColSpan = 2; - - private int m_EditRow; - private int m_EditCol; - private string m_EditText = ""; + // 调试参数 + private bool m_ShowChildren; private void OnEnable() { @@ -81,55 +69,35 @@ namespace XericUIEditor.XTable EditorGUI.indentLevel--; } - // ===== 编辑测试 ===== + // ===== 调试 ===== EditorGUILayout.Space(4); - s_EditTestFoldout = EditorGUILayout.Foldout(s_EditTestFoldout, "编辑测试", true); - if (s_EditTestFoldout) + s_DebugFoldout = EditorGUILayout.Foldout(s_DebugFoldout, "调试", true); + if (s_DebugFoldout) { EditorGUI.indentLevel++; - EditorGUILayout.LabelField("批量生成尺寸", EditorStyles.miniBoldLabel); - m_BatchRowCount = EditorGUILayout.IntField("行数", m_BatchRowCount); - m_BatchColCount = EditorGUILayout.IntField("列数", m_BatchColCount); - m_BatchCellHeight = EditorGUILayout.FloatField("单元格高度", m_BatchCellHeight); - m_BatchCellWidth = EditorGUILayout.FloatField("单元格宽度", m_BatchCellWidth); - if (GUILayout.Button("应用批量尺寸", GUILayout.Height(22))) - ApplyBatchRowColSizes(); + if (GUILayout.Button("重新生成表格", GUILayout.Height(26))) + { + m_Table.FullRebuild(); + m_Table.SetChildrenVisible(!m_ShowChildren); + } - EditorGUILayout.Space(6); + EditorGUILayout.Space(4); - EditorGUILayout.LabelField("合并单元格", EditorStyles.miniBoldLabel); - m_MergeStartRow = EditorGUILayout.IntField("起始行", m_MergeStartRow); - m_MergeStartCol = EditorGUILayout.IntField("起始列", m_MergeStartCol); - m_MergeRowSpan = EditorGUILayout.IntField("行跨度", m_MergeRowSpan); - m_MergeColSpan = EditorGUILayout.IntField("列跨度", m_MergeColSpan); - EditorGUILayout.BeginHorizontal(); - if (GUILayout.Button("合并", GUILayout.Height(22))) - EditorApplication.delayCall += () => m_Table.MergeCells(m_MergeStartRow, m_MergeStartCol, m_MergeRowSpan, m_MergeColSpan); - if (GUILayout.Button("取消合并", GUILayout.Height(22))) - EditorApplication.delayCall += () => m_Table.UnmergeCells(m_MergeStartRow, m_MergeStartCol); - EditorGUILayout.EndHorizontal(); + bool newShow = EditorGUILayout.Toggle("显示子项", m_ShowChildren); + if (newShow != m_ShowChildren) + { + m_ShowChildren = newShow; + m_Table.SetChildrenVisible(m_ShowChildren); + } - EditorGUILayout.Space(6); - - EditorGUILayout.LabelField("单元格编辑", EditorStyles.miniBoldLabel); - m_EditRow = EditorGUILayout.IntField("行", m_EditRow); - m_EditCol = EditorGUILayout.IntField("列", m_EditCol); - - var existing = m_Table.GetCellData(m_EditRow, m_EditCol); - if (existing != null && !string.IsNullOrEmpty(existing.Text) && string.IsNullOrEmpty(m_EditText)) - m_EditText = existing.Text; - - m_EditText = EditorGUILayout.TextField("文本内容", m_EditText); - - EditorGUILayout.BeginHorizontal(); - if (GUILayout.Button("应用文本", GUILayout.Height(22))) - EditorApplication.delayCall += () => m_Table.SetCellText(m_EditRow, m_EditCol, m_EditText); - if (GUILayout.Button("选中此格", GUILayout.Height(22))) - EditorApplication.delayCall += () => m_Table.SelectCell(m_EditRow, m_EditCol); - if (GUILayout.Button("清除选中", GUILayout.Height(22))) - EditorApplication.delayCall += () => m_Table.ClearSelection(); - EditorGUILayout.EndHorizontal(); + if (m_ShowChildren) + { + EditorGUILayout.HelpBox( + "子项已显示:所有动态生成的对象将在 Hierarchy 中可见(仍不保存到场景)。\n" + + "用于排查表格元素堆积或渲染异常问题。", + MessageType.Info); + } EditorGUI.indentLevel--; } @@ -166,27 +134,6 @@ namespace XericUIEditor.XTable serializedObject.ApplyModifiedProperties(); } - private void ApplyBatchRowColSizes() - { - serializedObject.Update(); - - if (m_RowHeightsProp != null) - { - m_RowHeightsProp.arraySize = m_BatchRowCount; - for (int i = 0; i < m_BatchRowCount; i++) - m_RowHeightsProp.GetArrayElementAtIndex(i).floatValue = m_BatchCellHeight; - } - - if (m_ColWidthsProp != null) - { - m_ColWidthsProp.arraySize = m_BatchColCount; - for (int i = 0; i < m_BatchColCount; i++) - m_ColWidthsProp.GetArrayElementAtIndex(i).floatValue = m_BatchCellWidth; - } - - serializedObject.ApplyModifiedProperties(); - } - #region Hierarchy 创建菜单 [MenuItem("GameObject/Xeric UI/Table/Xeric UI Action Table", false, 60)] diff --git a/Editor/XTable/XericUIActionTableSceneEditor.cs b/Editor/XTable/XericUIActionTableSceneEditor.cs index a1fc673..73eff35 100644 --- a/Editor/XTable/XericUIActionTableSceneEditor.cs +++ b/Editor/XTable/XericUIActionTableSceneEditor.cs @@ -2,6 +2,7 @@ using UnityEditor; using UnityEngine; +using XericLibrary.Runtime.SuperStyleSheet; using XericUI.XTable.Mapping; using XericUI.XTable.Rendering.Component; @@ -137,10 +138,23 @@ namespace XericUIEditor.XTable // 底部内容输入框获得焦点:种子为当前单元格内容 else if (newFocus == "XTableSceneEditInput" && s_ActiveTable != null) { - int sr = s_ActiveTable.SelectedRow; - int sc = s_ActiveTable.SelectedCol; - if (sr >= 0 && sc >= 0) - s_EditText = s_ActiveTable.GetCellData(sr, sc).Text ?? ""; + SyncEditText(s_ActiveTable); + } + } + + /// 将 s_EditText 同步为当前选中单元格的内容(选中变更时调用) + private static void SyncEditText(XericUIActionTable table) + { + int sr = table.SelectedRow; + int sc = table.SelectedCol; + if (sr >= 0 && sc >= 0) + { + var data = table.GetCellData(sr, sc); + s_EditText = (data != null) ? (data.Text ?? "") : ""; + } + else + { + s_EditText = ""; } } @@ -338,6 +352,7 @@ namespace XericUIEditor.XTable } table.RefreshView(); + SyncEditText(table); Event.current.Use(); SceneView.RepaintAll(); } @@ -412,6 +427,7 @@ namespace XericUIEditor.XTable } table.RefreshView(); + SyncEditText(table); Event.current.Use(); SceneView.RepaintAll(); } @@ -719,7 +735,12 @@ namespace XericUIEditor.XTable private static void DrawInputBar(XericUIActionTable table, float x, float y) { - Rect barRect = new Rect(x, y, 500, 24); + const float barW = 450; + const float barH = 52; + const float rowH = 22; + const float pad = 2; + + Rect barRect = new Rect(x, y, barW, barH); EditorGUI.DrawRect(barRect, s_InputBg); if (table.SelectedRow >= 0 && table.SelectedCol >= 0) @@ -730,17 +751,26 @@ namespace XericUIEditor.XTable normal = { textColor = s_LabelText }, alignment = TextAnchor.MiddleCenter }; - - string cellLabel = table.GetSelectionText(); - GUI.Label(new Rect(barRect.x + 4, barRect.y, 75, barRect.height), - cellLabel, labelStyle); - var inputStyle = new GUIStyle(GUI.skin.textField) { fontSize = 12, - margin = new RectOffset(2, 2, 2, 2) + margin = new RectOffset(1, 1, 1, 1) }; - Rect inputRect = new Rect(barRect.x + 85, barRect.y + 2, 200, barRect.height - 4); + var btnStyle = new GUIStyle(GUI.skin.button) + { + fontSize = 11, + margin = new RectOffset(1, 1, 1, 1), + padding = new RectOffset(4, 4, 2, 2) + }; + + // Row 1: 坐标标签 | 输入框 | 应用按钮 + float btnW = 44; + float row1Y = barRect.y + pad; + float row2Y = barRect.y + rowH + pad + 4; + string cellLabel = table.GetSelectionText(); + + GUI.Label(new Rect(barRect.x + 4, row1Y, 60, rowH), cellLabel, labelStyle); + Rect inputRect = new Rect(barRect.x + 68, row1Y, barW - 68 - btnW - pad * 2, rowH); // 回车提交(仅在 CellEditing 状态下) if (s_State == EditorState.CellEditing && @@ -754,18 +784,40 @@ namespace XericUIEditor.XTable GUI.SetNextControlName("XTableSceneEditInput"); s_EditText = GUI.TextField(inputRect, s_EditText, inputStyle); - var btnStyle = new GUIStyle(GUI.skin.button) - { - fontSize = 11, - margin = new RectOffset(2, 2, 2, 2), - padding = new RectOffset(4, 4, 2, 2) - }; - if (GUI.Button(new Rect(barRect.x + 370, barRect.y + 2, 50, barRect.height - 4), + if (GUI.Button(new Rect(barRect.x + barW - btnW - pad, row1Y, btnW, rowH), "应用", btnStyle)) { GUI.FocusControl(null); ApplyCellEdit(table); } + + // Row 2: 样式命名空间下拉框 + var nsLabelStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 10, + normal = { textColor = new Color(0.6f, 0.6f, 0.6f) }, + alignment = TextAnchor.MiddleRight + }; + GUI.Label(new Rect(barRect.x + 4, row2Y, 36, rowH), "样式:", nsLabelStyle); + + // 获取当前命名空间 + string currentNS = table.GetCellStyleNamespace( + table.SelectedRow, table.SelectedCol) ?? ""; + + // 构建下拉选项(StyleManager.GetAvailableNamespaces 已保证 "default" 在首位) + var namespaces = StyleManager.GetAvailableNamespaces(); + int currentIdx = namespaces.IndexOf(currentNS); + if (currentIdx < 0) currentIdx = 0; + + Rect dropRect = new Rect(barRect.x + 44, row2Y, barW - 44 - pad, rowH); + int newIdx = UnityEditor.EditorGUI.Popup(dropRect, currentIdx, + namespaces.ToArray(), UnityEditor.EditorStyles.popup); + + if (newIdx != currentIdx && newIdx >= 0 && newIdx < namespaces.Count) + { + table.SetCellStyleNamespace(table.SelectedRow, table.SelectedCol, + namespaces[newIdx]); + } } else { @@ -775,7 +827,8 @@ namespace XericUIEditor.XTable normal = { textColor = new Color(0.5f, 0.5f, 0.5f) }, alignment = TextAnchor.MiddleCenter }; - GUI.Label(barRect, "点击左侧行号或上方列名选中单元格 | Shift+方向键多选", hintStyle); + GUI.Label(new Rect(barRect.x, barRect.y, barW, barH / 2), + "点击左侧行号或上方列名选中单元格 | Shift+方向键多选", hintStyle); } } @@ -823,6 +876,7 @@ namespace XericUIEditor.XTable } table.RefreshView(); + SyncEditText(table); e.Use(); SceneView.RepaintAll(); break; @@ -835,6 +889,7 @@ namespace XericUIEditor.XTable table.ExtendMultiSelect(dragRow, dragCol); table.RefreshView(); + SyncEditText(table); e.Use(); SceneView.RepaintAll(); break; @@ -870,6 +925,7 @@ namespace XericUIEditor.XTable table.ClearSelection(); table.RefreshView(); s_EditText = ""; + SyncEditText(table); e.Use(); SceneView.RepaintAll(); return; @@ -899,6 +955,7 @@ namespace XericUIEditor.XTable table.SelectCell(row, col); table.RefreshView(); + SyncEditText(table); e.Use(); SceneView.RepaintAll(); return; @@ -934,6 +991,7 @@ namespace XericUIEditor.XTable } table.RefreshView(); + SyncEditText(table); e.Use(); SceneView.RepaintAll(); } diff --git a/Runtime/XTable/Core/XTableData.cs b/Runtime/XTable/Core/XTableData.cs index b632356..1678927 100644 --- a/Runtime/XTable/Core/XTableData.cs +++ b/Runtime/XTable/Core/XTableData.cs @@ -20,16 +20,23 @@ namespace XericUI.XTable.Core #region 属性 /// 块内列数(默认 ) - public int BlockSizeX { get; private set; } + [field: SerializeField] public int BlockSizeX { get; private set; } /// 块内行数(默认 ) - public int BlockSizeY { get; private set; } + [field: SerializeField] public int BlockSizeY { get; private set; } /// /// 块字典——key = Z 曲线索引(ulong),value = 数据块。 /// 具有哈希表性质,按需创建,键不连续(离散存储)。 + /// 注意:Dictionary 不被 Unity 序列化器原生支持,通过 / 代理序列化。 /// - public Dictionary BlockMap { get; private set; } + [NonSerialized] public Dictionary BlockMap; + + // ── Dictionary 序列化代理 ── + // Unity 无法直接序列化 Dictionary, + // 使用两个平行列表在 OnBeforeSerialize/OnAfterDeserialize 中转换。 + [SerializeField] private List m_BlockKeys = new List(); + [SerializeField] private List m_BlockValues = new List(); /// /// 获取或创建单元格——如果单元格不存在则自动创建空数据。 @@ -74,13 +81,36 @@ namespace XericUI.XTable.Core #region 序列化回调 - public void OnBeforeSerialize() { } + public void OnBeforeSerialize() + { + // Dictionary → 平行列表 + m_BlockKeys.Clear(); + m_BlockValues.Clear(); + if (BlockMap != null) + { + foreach (var kv in BlockMap) + { + m_BlockKeys.Add(kv.Key); + m_BlockValues.Add(kv.Value); + } + } + } public void OnAfterDeserialize() { if (BlockSizeX <= 0) BlockSizeX = DefaultBlockSize; if (BlockSizeY <= 0) BlockSizeY = DefaultBlockSize; - if (BlockMap == null) BlockMap = new Dictionary(); + + // 平行列表 → Dictionary + BlockMap = new Dictionary(); + int count = System.Math.Min( + m_BlockKeys?.Count ?? 0, + m_BlockValues?.Count ?? 0); + for (int i = 0; i < count; i++) + { + if (m_BlockValues[i] != null) + BlockMap[m_BlockKeys[i]] = m_BlockValues[i]; + } } #endregion diff --git a/Runtime/XTable/Core/XTableMergeDescriptor.cs b/Runtime/XTable/Core/XTableMergeDescriptor.cs index 0016ac5..38f7273 100644 --- a/Runtime/XTable/Core/XTableMergeDescriptor.cs +++ b/Runtime/XTable/Core/XTableMergeDescriptor.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using UnityEngine; + namespace XericUI.XTable.Core { /// @@ -8,7 +10,7 @@ namespace XericUI.XTable.Core /// 同一类型支持本地块内引用和跨块引用,通过 区分。 /// [Serializable] - public class XTableMergeDescriptor + public class XTableMergeDescriptor : ISerializationCallbackReceiver { /// 合并区域在本地块内的起始行 public int LocalStartRow; @@ -37,8 +39,12 @@ namespace XericUI.XTable.Core /// 源单元格在块内的列号(跨块时=源块的列、本地时=本块的列) public int SourceLocalCol; - /// 本块内被合并的 (row, col) 集合,查询时快速判断是否需要重定向 - public HashSet<(int row, int col)> MergedCellSet; + /// 本块内被合并的 (row, col) 集合——不参与序列化,通过代理列表同步 + [NonSerialized] public HashSet<(int row, int col)> MergedCellSet; + + // ── HashSet 序列化代理 ── + [SerializeField] private List m_MergedRowList = new List(); + [SerializeField] private List m_MergedColList = new List(); public XTableMergeDescriptor() { @@ -63,5 +69,37 @@ namespace XericUI.XTable.Core targetRow = SourceLocalRow; targetCol = SourceLocalCol; } + + #region 序列化回调 + + public void OnBeforeSerialize() + { + // HashSet<(int,int)> → 两个平行列表 + m_MergedRowList.Clear(); + m_MergedColList.Clear(); + if (MergedCellSet != null) + { + foreach (var (row, col) in MergedCellSet) + { + m_MergedRowList.Add(row); + m_MergedColList.Add(col); + } + } + } + + public void OnAfterDeserialize() + { + // 两个平行列表 → HashSet<(int,int)> + MergedCellSet = new HashSet<(int, int)>(); + int count = System.Math.Min( + m_MergedRowList?.Count ?? 0, + m_MergedColList?.Count ?? 0); + for (int i = 0; i < count; i++) + { + MergedCellSet.Add((m_MergedRowList[i], m_MergedColList[i])); + } + } + + #endregion } } diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs index 32fe9d8..c037199 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs @@ -1,12 +1,9 @@ using System; using System.Collections.Generic; - using TMPro; - using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; - using XericUI.Core.Base; using XericUI.XTable.Core; using XericUI.XTable.Mapping; @@ -15,840 +12,931 @@ using XericLibrary.Runtime.SuperStyleSheet; namespace XericUI.XTable.Rendering.Component { - /// - /// 表格组件——纯数据渲染器。 - /// 一张全幅背景 + 按需拼装单元格文本/图片。 - /// 无行列标题、无样式差异分割线——所有内容由数据定义。 - /// - [ExecuteAlways] - [AddComponentMenu("Xeric UI Vessel/Table/Xeric UI Action Table", 60)] - public class XericUIActionTable : XericUIBehaciour, IPointerClickHandler - { - #region 常量 + /// + /// 表格组件——纯数据渲染器。 + /// 一张全幅背景 + 按需拼装单元格文本/图片。 + /// 无行列标题、无样式差异分割线——所有内容由数据定义。 + /// + [ExecuteAlways] + [AddComponentMenu("Xeric UI Vessel/Table/Xeric UI Action Table", 60)] + public class XericUIActionTable : XericUIBehaciour, IPointerClickHandler + { + #region 常量 - private const string DEFAULT_STYLE_NS = "xeric_table_default"; + private const string DEFAULT_STYLE_NS = "xeric_table_default"; - #endregion + #endregion - #region 序列化字段 + #region 序列化字段 - [SerializeField] private XTableData m_TableData; + [SerializeField] private XTableData m_TableData; - [SerializeField] private float[] m_RowHeights = new float[] { 40f, 30f }; + [SerializeField] private float[] m_RowHeights = new float[] { 40f, 30f }; - [SerializeField] private float[] m_ColWidths = new float[] { 100f, 80f, 120f }; + [SerializeField] private float[] m_ColWidths = new float[] { 100f, 80f, 120f }; - [SerializeField] private string m_DefaultStyleNamespace = DEFAULT_STYLE_NS; + [SerializeField] private string m_DefaultStyleNamespace = DEFAULT_STYLE_NS; - [SerializeField] private ScrollRect m_ScrollRect; + [SerializeField] private ScrollRect m_ScrollRect; - #endregion + #endregion - #region 私有字段 + #region 私有字段 - [System.NonSerialized] private CellAssembler m_Assembler; - [System.NonSerialized] private XTableDirtyFlag m_DirtyFlag; + [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_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; + // 多选范围 + private int m_SelectStartRow = -1; + private int m_SelectStartCol = -1; + private int m_SelectEndRow = -1; + private int m_SelectEndCol = -1; - [System.NonSerialized] private GameObject m_BackgroundGo; + [System.NonSerialized] private GameObject m_BackgroundGo; - private float m_TotalWidth; - private float m_TotalHeight; + private float m_TotalWidth; + private float m_TotalHeight; - // ScrollRect 可见范围 - private Rect m_VisibleRect; + // ScrollRect 可见范围 + private Rect m_VisibleRect; - #endregion + #endregion - #region 事件 + #region 事件 - public event Action OnCellSelected; - public event Action OnTableRefreshed; + public event Action OnCellSelected; + public event Action OnTableRefreshed; - #endregion + #endregion - #region 公开属性 + #region 公开属性 - public XTableData TableData - { - get + public XTableData TableData + { + get + { + if (m_TableData == null) + m_TableData = new XTableData(); + return m_TableData; + } + set + { + m_TableData = value; + MarkDirty(TableDirtyType.DataChanged); + } + } + + public float[] RowHeights => m_RowHeights; + 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; + + /// 判断单元格是否在多选范围内 + 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; + } + + #endregion + + #region 生命周期 + + protected override void Awake() + { + base.Awake(); + + // 域重载后旧子对象可能残留在场景中(DontSave 不自动销毁),先清空再重建 + ClearAllChildren(); + + if (m_TableData == null) + m_TableData = new XTableData(); + + m_Assembler = new CellAssembler(rectTransform); + RecalculateTotalSize(); + EnsureDefaultStyleNamespace(); + CreateDefaultSampleData(); + CreateBackground(); + + if (m_ScrollRect != null) + m_ScrollRect.onValueChanged.AddListener(OnScrollValueChanged); + + if (!Application.isPlaying) + SubscribeEditorUpdate(); + + MarkDirty(TableDirtyType.All); + } + + protected override void OnEnable() + { + base.OnEnable(); + EnsureDefaultStyleNamespace(); + MarkDirty(TableDirtyType.All); + } + + protected override void OnDisable() + { + base.OnDisable(); + } + + protected override void OnDestroy() + { + if (m_ScrollRect != null) + m_ScrollRect.onValueChanged.RemoveListener(OnScrollValueChanged); + UnsubscribeEditorUpdate(); + + // 销毁所有动态生成的对象 + if (m_Assembler != null) + m_Assembler.DestroyAll(); + if (m_BackgroundGo != null) + { + if (Application.isPlaying) Destroy(m_BackgroundGo); + else DestroyImmediate(m_BackgroundGo); + m_BackgroundGo = null; + } + + base.OnDestroy(); + } + + private void Update() + { + if (m_DirtyFlag.IsDirty) RefreshView(); + } + + #endregion + + #region 默认示例数据 + + private void CreateDefaultSampleData() + { + // 仅当数据完全为空时才创建示例数据(首次使用或数据损坏后恢复) + if (m_TableData.BlockCount > 0) return; + + string[,] sample = new string[2, 3] + { + { "Name", "Age", "City" }, + { "Alice", "25", "NYC" } + }; + + for (int r = 0; r < 2; r++) + for (int c = 0; c < 3; c++) + { + var data = m_TableData.GetOrCreateCell(r, c); + data.Text = sample[r, c]; + } + } + + #endregion + + #region 编辑器模式支持 + + [System.NonSerialized] private bool m_EditorUpdateSubscribed; + + private void SubscribeEditorUpdate() + { + if (m_EditorUpdateSubscribed) return; +#if UNITY_EDITOR + UnityEditor.EditorApplication.update += EditorUpdate; + m_EditorUpdateSubscribed = true; +#endif + } + + private void UnsubscribeEditorUpdate() + { + if (!m_EditorUpdateSubscribed) return; +#if UNITY_EDITOR + UnityEditor.EditorApplication.update -= EditorUpdate; + m_EditorUpdateSubscribed = false; +#endif + } + +#if UNITY_EDITOR + private void EditorUpdate() + { + if (this == null || !isActiveAndEnabled) + { + UnsubscribeEditorUpdate(); + return; + } + + if (m_DirtyFlag.IsDirty) + RefreshView(); + } +#endif + + #endregion + + #region 公开方法 + + public void SetCellData(int row, int col, XTableCellData data) + { + TableData.SetCell(row, col, data); + MarkDirty(TableDirtyType.DataChanged); + } + + public void SetCellText(int row, int col, string text) + { + var data = TableData.GetOrCreateCell(row, col); + data.Text = text; + MarkDirty(TableDirtyType.DataChanged); + } + + public XTableCellData GetCellData(int row, int col) + => TableData.GetOrCreateCell(row, col); + + /// 设置单元格的样式命名空间 + public void SetCellStyleNamespace(int row, int col, string styleNamespace) + { + var data = TableData.GetOrCreateCell(row, col); + data.StyleNamespace = styleNamespace; + MarkDirty(TableDirtyType.StyleChanged); + } + + /// 获取单元格的样式命名空间 + public string GetCellStyleNamespace(int row, int col) + { + return TableData.GetCell(row, col)?.StyleNamespace ?? m_DefaultStyleNamespace; + } + + public void MergeCells(int startRow, int startCol, int rowSpan, int colSpan) + { + TableData.MergeCells(startRow, startCol, rowSpan, colSpan); + MarkDirty(TableDirtyType.DataChanged); + } + + public void UnmergeCells(int startRow, int startCol) + { + TableData.UnmergeCells(startRow, startCol); + MarkDirty(TableDirtyType.DataChanged); + } + + /// 在末尾追加一行,默认行高 40 + public void AddRow(float height = 40f) + { +#if UNITY_EDITOR + UnityEditor.Undo.RecordObject(this, "Add Table Row"); +#endif + int newLen = m_RowHeights.Length + 1; + var newArr = new float[newLen]; + System.Array.Copy(m_RowHeights, newArr, m_RowHeights.Length); + newArr[newLen - 1] = height; + m_RowHeights = newArr; + MarkDirty(TableDirtyType.LayoutChanged); +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif + } + + /// 在末尾追加一列,默认列宽 100 + public void AddColumn(float width = 100f) + { +#if UNITY_EDITOR + UnityEditor.Undo.RecordObject(this, "Add Table Column"); +#endif + int newLen = m_ColWidths.Length + 1; + var newArr = new float[newLen]; + System.Array.Copy(m_ColWidths, newArr, m_ColWidths.Length); + newArr[newLen - 1] = width; + m_ColWidths = newArr; + MarkDirty(TableDirtyType.LayoutChanged); +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif + } + + /// 设置指定行的高度 + public void SetRowHeight(int row, float height) + { + if (row < 0 || row >= m_RowHeights.Length) return; + if (height <= 0) return; +#if UNITY_EDITOR + UnityEditor.Undo.RecordObject(this, "Set Row Height"); +#endif + m_RowHeights[row] = height; + MarkDirty(TableDirtyType.LayoutChanged); +#if UNITY_EDITOR + if (!Application.isPlaying) UnityEditor.EditorUtility.SetDirty(this); +#endif + } + + /// 设置指定列的宽度 + public void SetColWidth(int col, float width) + { + if (col < 0 || col >= m_ColWidths.Length) return; + if (width <= 0) return; +#if UNITY_EDITOR + UnityEditor.Undo.RecordObject(this, "Set Column Width"); +#endif + m_ColWidths[col] = width; + MarkDirty(TableDirtyType.LayoutChanged); +#if UNITY_EDITOR + if (!Application.isPlaying) UnityEditor.EditorUtility.SetDirty(this); +#endif + } + + /// 删除指定行,单元格数据上移 + public void RemoveRow(int row) + { + if (row < 0 || row >= m_RowHeights.Length) return; +#if UNITY_EDITOR + UnityEditor.Undo.RecordObject(this, "Remove Table Row"); +#endif + int colCount = m_ColWidths.Length; + + // 先解除涉及该行的合并 + for (int c = 0; c < colCount; c++) + TableData.UnmergeCells(row, c); + + // 将下方行数据上移 + for (int r = row + 1; r < m_RowHeights.Length; r++) + for (int c = 0; c < colCount; c++) + { + var data = TableData.GetCell(r, c); + if (data != null) + TableData.SetCell(r - 1, c, data); + else + TableData.SetCell(r - 1, c, null); // 显式清空原位置 + } + + // 清空最后一行数据 + int lastRow = m_RowHeights.Length - 1; + for (int c = 0; c < colCount; c++) + TableData.SetCell(lastRow, c, null); + + // 缩小行高数组 + int newLen = m_RowHeights.Length - 1; + var newArr = new float[newLen]; + for (int i = 0; i < row; i++) + newArr[i] = m_RowHeights[i]; + for (int i = row; i < newLen; i++) + 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); + + MarkDirty(TableDirtyType.LayoutChanged | TableDirtyType.DataChanged | TableDirtyType.SelectionChanged); +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif + } + + /// 删除指定列,单元格数据左移 + public void RemoveColumn(int col) + { + if (col < 0 || col >= m_ColWidths.Length) return; +#if UNITY_EDITOR + UnityEditor.Undo.RecordObject(this, "Remove Table Column"); +#endif + int rowCount = m_RowHeights.Length; + + // 先解除涉及该列的合并 + for (int r = 0; r < rowCount; r++) + TableData.UnmergeCells(r, col); + + // 将右侧列数据左移 + for (int r = 0; r < rowCount; r++) + for (int c = col + 1; c < m_ColWidths.Length; c++) + { + var data = TableData.GetCell(r, c); + if (data != null) + TableData.SetCell(r, c - 1, data); + else + TableData.SetCell(r, c - 1, null); + } + + // 清空最后一列数据 + int lastCol = m_ColWidths.Length - 1; + for (int r = 0; r < rowCount; r++) + TableData.SetCell(r, lastCol, null); + + // 缩小列宽数组 + int newLen = m_ColWidths.Length - 1; + var newArr = new float[newLen]; + for (int i = 0; i < col; i++) + newArr[i] = m_ColWidths[i]; + for (int i = col; i < newLen; i++) + 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); + + MarkDirty(TableDirtyType.LayoutChanged | TableDirtyType.DataChanged | TableDirtyType.SelectionChanged); +#if UNITY_EDITOR + UnityEditor.EditorUtility.SetDirty(this); +#endif + } + + public void SelectCell(int row, int col) + { + ClearMultiSelection(); + m_SelectedRow = row; + m_SelectedCol = col; + MarkDirty(TableDirtyType.SelectionChanged); + } + + public void ClearSelection() + { + m_SelectedRow = -1; + m_SelectedCol = -1; + ClearMultiSelection(); + 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) + { + if (!XTableCoordinateFormat.TryParseCell(text, out int row, out int col)) + return false; + SelectCell(row, col); + return true; + } + + /// 通过文本选择一个范围(如 "A0:B1", "A:A", "0:0") + public bool SelectRangeByText(string text) + { + if (!XTableCoordinateFormat.TryParseRange(text, + 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); + 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)) + { + return XTableCoordinateFormat.RangeToText( + m_SelectStartRow, m_SelectStartCol, + m_SelectEndRow, m_SelectEndCol); + } + + return XTableCoordinateFormat.CellToText(m_SelectedRow, m_SelectedCol); + } + + public void RefreshView() + { + // 编辑模式下 Awake 可能尚未执行,按需回退初始化 + if (m_Assembler == null) + { + if (rectTransform != null) + m_Assembler = new CellAssembler(rectTransform); + else return; + } + + if (m_RowHeights == null || m_ColWidths == null) return; + if (m_RowHeights.Length == 0 || m_ColWidths.Length == 0) return; + + CreateBackground(); + +#if UNITY_EDITOR + if (!Application.isPlaying) + Canvas.ForceUpdateCanvases(); +#endif + + RecalculateTotalSize(); + UpdateVisibleRect(); + + // 更新背景尺寸 + if (m_BackgroundGo != null) + { + var bgRt = m_BackgroundGo.GetComponent(); + bgRt.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); + } + + // 回收池 + m_Assembler.ReturnAll(); + + // 额外清理孤立的直接子对象(防止域重载或异常流程产生的残留) + Transform poolRoot = m_Assembler.PoolRoot; + for (int i = rectTransform.childCount - 1; i >= 0; i--) + { + var child = rectTransform.GetChild(i); + if ((poolRoot != null && child == poolRoot)) continue; + if (m_BackgroundGo != null && child.gameObject == m_BackgroundGo) continue; + + child.gameObject.hideFlags = HideFlags.None; + if (Application.isPlaying) + Destroy(child.gameObject); + else + 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); + + m_DirtyFlag.Clear(); + OnTableRefreshed?.Invoke(); + } + + #endregion + + #region 背景 + + private void CreateBackground() + { + if (m_BackgroundGo != null) return; + + m_BackgroundGo = new GameObject("_TableBackground", + typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); + m_BackgroundGo.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy; + m_BackgroundGo.transform.SetParent(rectTransform, false); + + RectTransform bgRt = m_BackgroundGo.GetComponent(); + bgRt.anchorMin = new Vector2(0, 1); + bgRt.anchorMax = new Vector2(0, 1); + bgRt.pivot = new Vector2(0, 1); + bgRt.anchoredPosition = Vector2.zero; + bgRt.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); + + var bgImg = m_BackgroundGo.GetComponent(); + bgImg.color = new Color(0.18f, 0.18f, 0.18f, 1f); + bgImg.raycastTarget = false; + } + + /// 销毁 transform 下所有子对象(用于域重载后清理残留) + private void ClearAllChildren() + { + if (rectTransform == null) return; + +#if UNITY_EDITOR + // 编辑器模式下先重置 HideFlags 再销毁,防止 DontSave 对象拒绝销毁 + while (rectTransform.childCount > 0) + { + var child = rectTransform.GetChild(0); + child.gameObject.hideFlags = HideFlags.None; + if (Application.isPlaying) + Destroy(child.gameObject); + else + DestroyImmediate(child.gameObject); + } +#else + for (int i = rectTransform.childCount - 1; i >= 0; i--) { - if (m_TableData == null) - m_TableData = new XTableData(); - return m_TableData; + var child = rectTransform.GetChild(i); + Destroy(child.gameObject); } - set { m_TableData = value; MarkDirty(TableDirtyType.DataChanged); } - } - - public float[] RowHeights => m_RowHeights; - 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; - - /// 判断单元格是否在多选范围内 - 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; - } - - #endregion - - #region 生命周期 - - protected override void Awake() - { - base.Awake(); - - if (m_TableData == null) - m_TableData = new XTableData(); - - m_Assembler = new CellAssembler(rectTransform); - RecalculateTotalSize(); - EnsureDefaultStyleNamespace(); - CreateDefaultSampleData(); - CreateBackground(); - - if (m_ScrollRect != null) - m_ScrollRect.onValueChanged.AddListener(OnScrollValueChanged); - - if (!Application.isPlaying) - SubscribeEditorUpdate(); - - MarkDirty(TableDirtyType.All); - } - - protected override void OnEnable() - { - base.OnEnable(); - EnsureDefaultStyleNamespace(); - MarkDirty(TableDirtyType.All); - } - - protected override void OnDisable() - { - base.OnDisable(); - } - - protected override void OnDestroy() - { - if (m_ScrollRect != null) - m_ScrollRect.onValueChanged.RemoveListener(OnScrollValueChanged); - UnsubscribeEditorUpdate(); - - // 销毁所有动态生成的对象 - if (m_Assembler != null) - m_Assembler.DestroyAll(); - if (m_BackgroundGo != null) - { - if (Application.isPlaying) Destroy(m_BackgroundGo); - else DestroyImmediate(m_BackgroundGo); - m_BackgroundGo = null; - } - - base.OnDestroy(); - } - - private void Update() - { - if (m_DirtyFlag.IsDirty) RefreshView(); - } - - #endregion - - #region 默认示例数据 - - private void CreateDefaultSampleData() - { - // 仅当数据完全为空时才创建示例数据(首次使用或数据损坏后恢复) - if (m_TableData.BlockCount > 0) return; - - string[,] sample = new string[2, 3] - { - { "Name", "Age", "City" }, - { "Alice", "25", "NYC" } - }; - - for (int r = 0; r < 2; r++) - for (int c = 0; c < 3; c++) - { - var data = m_TableData.GetOrCreateCell(r, c); - data.Text = sample[r, c]; - } - } - - #endregion - - #region 编辑器模式支持 - - [System.NonSerialized] private bool m_EditorUpdateSubscribed; - - private void SubscribeEditorUpdate() - { - if (m_EditorUpdateSubscribed) return; -#if UNITY_EDITOR - UnityEditor.EditorApplication.update += EditorUpdate; - m_EditorUpdateSubscribed = true; #endif - } + } - private void UnsubscribeEditorUpdate() - { - if (!m_EditorUpdateSubscribed) return; + /// 设置所有动态子对象的可见性(仅编辑器) + public void SetChildrenVisible(bool visible) + { #if UNITY_EDITOR - UnityEditor.EditorApplication.update -= EditorUpdate; - m_EditorUpdateSubscribed = false; + if (rectTransform == null) return; + for (int i = rectTransform.childCount - 1; i >= 0; i--) + { + var child = rectTransform.GetChild(i); + child.gameObject.hideFlags = visible + ? HideFlags.DontSave // 可见但不保存 + : HideFlags.DontSave | HideFlags.HideInHierarchy; // 隐藏且不保存 + } #endif - } - -#if UNITY_EDITOR - private void EditorUpdate() - { - if (this == null || !isActiveAndEnabled) - { - UnsubscribeEditorUpdate(); - return; - } - if (m_DirtyFlag.IsDirty) - RefreshView(); - } -#endif - - #endregion - - #region 公开方法 - - public void SetCellData(int row, int col, XTableCellData data) - { - TableData.SetCell(row, col, data); - MarkDirty(TableDirtyType.DataChanged); - } - - public void SetCellText(int row, int col, string text) - { - var data = TableData.GetOrCreateCell(row, col); - data.Text = text; - MarkDirty(TableDirtyType.DataChanged); - } - - public XTableCellData GetCellData(int row, int col) - => TableData.GetOrCreateCell(row, col); - - public void MergeCells(int startRow, int startCol, int rowSpan, int colSpan) - { - TableData.MergeCells(startRow, startCol, rowSpan, colSpan); - MarkDirty(TableDirtyType.DataChanged); - } - - public void UnmergeCells(int startRow, int startCol) - { - TableData.UnmergeCells(startRow, startCol); - MarkDirty(TableDirtyType.DataChanged); - } - - /// 在末尾追加一行,默认行高 40 - public void AddRow(float height = 40f) - { -#if UNITY_EDITOR - UnityEditor.Undo.RecordObject(this, "Add Table Row"); -#endif - int newLen = m_RowHeights.Length + 1; - var newArr = new float[newLen]; - System.Array.Copy(m_RowHeights, newArr, m_RowHeights.Length); - newArr[newLen - 1] = height; - m_RowHeights = newArr; - MarkDirty(TableDirtyType.LayoutChanged); -#if UNITY_EDITOR - UnityEditor.EditorUtility.SetDirty(this); -#endif - } - - /// 在末尾追加一列,默认列宽 100 - public void AddColumn(float width = 100f) - { -#if UNITY_EDITOR - UnityEditor.Undo.RecordObject(this, "Add Table Column"); -#endif - int newLen = m_ColWidths.Length + 1; - var newArr = new float[newLen]; - System.Array.Copy(m_ColWidths, newArr, m_ColWidths.Length); - newArr[newLen - 1] = width; - m_ColWidths = newArr; - MarkDirty(TableDirtyType.LayoutChanged); -#if UNITY_EDITOR - UnityEditor.EditorUtility.SetDirty(this); -#endif - } - - /// 设置指定行的高度 - public void SetRowHeight(int row, float height) - { - if (row < 0 || row >= m_RowHeights.Length) return; - if (height <= 0) return; -#if UNITY_EDITOR - UnityEditor.Undo.RecordObject(this, "Set Row Height"); -#endif - m_RowHeights[row] = height; - MarkDirty(TableDirtyType.LayoutChanged); -#if UNITY_EDITOR - if (!Application.isPlaying) UnityEditor.EditorUtility.SetDirty(this); -#endif - } - - /// 设置指定列的宽度 - public void SetColWidth(int col, float width) - { - if (col < 0 || col >= m_ColWidths.Length) return; - if (width <= 0) return; -#if UNITY_EDITOR - UnityEditor.Undo.RecordObject(this, "Set Column Width"); -#endif - m_ColWidths[col] = width; - MarkDirty(TableDirtyType.LayoutChanged); -#if UNITY_EDITOR - if (!Application.isPlaying) UnityEditor.EditorUtility.SetDirty(this); -#endif - } - - /// 删除指定行,单元格数据上移 - public void RemoveRow(int row) - { - if (row < 0 || row >= m_RowHeights.Length) return; -#if UNITY_EDITOR - UnityEditor.Undo.RecordObject(this, "Remove Table Row"); -#endif - int colCount = m_ColWidths.Length; - - // 先解除涉及该行的合并 - for (int c = 0; c < colCount; c++) - TableData.UnmergeCells(row, c); - - // 将下方行数据上移 - for (int r = row + 1; r < m_RowHeights.Length; r++) - for (int c = 0; c < colCount; c++) - { - var data = TableData.GetCell(r, c); - if (data != null) - TableData.SetCell(r - 1, c, data); - else - TableData.SetCell(r - 1, c, null); // 显式清空原位置 - } - - // 清空最后一行数据 - int lastRow = m_RowHeights.Length - 1; - for (int c = 0; c < colCount; c++) - TableData.SetCell(lastRow, c, null); - - // 缩小行高数组 - int newLen = m_RowHeights.Length - 1; - var newArr = new float[newLen]; - for (int i = 0; i < row; i++) - newArr[i] = m_RowHeights[i]; - for (int i = row; i < newLen; i++) - 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); - - MarkDirty(TableDirtyType.LayoutChanged | TableDirtyType.DataChanged | TableDirtyType.SelectionChanged); -#if UNITY_EDITOR - UnityEditor.EditorUtility.SetDirty(this); -#endif - } - - /// 删除指定列,单元格数据左移 - public void RemoveColumn(int col) - { - if (col < 0 || col >= m_ColWidths.Length) return; -#if UNITY_EDITOR - UnityEditor.Undo.RecordObject(this, "Remove Table Column"); -#endif - int rowCount = m_RowHeights.Length; - - // 先解除涉及该列的合并 - for (int r = 0; r < rowCount; r++) - TableData.UnmergeCells(r, col); - - // 将右侧列数据左移 - for (int r = 0; r < rowCount; r++) - for (int c = col + 1; c < m_ColWidths.Length; c++) - { - var data = TableData.GetCell(r, c); - if (data != null) - TableData.SetCell(r, c - 1, data); - else - TableData.SetCell(r, c - 1, null); - } - - // 清空最后一列数据 - int lastCol = m_ColWidths.Length - 1; - for (int r = 0; r < rowCount; r++) - TableData.SetCell(r, lastCol, null); - - // 缩小列宽数组 - int newLen = m_ColWidths.Length - 1; - var newArr = new float[newLen]; - for (int i = 0; i < col; i++) - newArr[i] = m_ColWidths[i]; - for (int i = col; i < newLen; i++) - 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); - - MarkDirty(TableDirtyType.LayoutChanged | TableDirtyType.DataChanged | TableDirtyType.SelectionChanged); -#if UNITY_EDITOR - UnityEditor.EditorUtility.SetDirty(this); -#endif - } - - public void SelectCell(int row, int col) - { - ClearMultiSelection(); - m_SelectedRow = row; - m_SelectedCol = col; - MarkDirty(TableDirtyType.SelectionChanged); - } - - public void ClearSelection() - { - m_SelectedRow = -1; - m_SelectedCol = -1; - ClearMultiSelection(); - 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) - { - if (!XTableCoordinateFormat.TryParseCell(text, out int row, out int col)) - return false; - SelectCell(row, col); - return true; - } - - /// 通过文本选择一个范围(如 "A0:B1", "A:A", "0:0") - public bool SelectRangeByText(string text) - { - if (!XTableCoordinateFormat.TryParseRange(text, - 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); - 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)) - { - return XTableCoordinateFormat.RangeToText( - m_SelectStartRow, m_SelectStartCol, - m_SelectEndRow, m_SelectEndCol); - } - return XTableCoordinateFormat.CellToText(m_SelectedRow, m_SelectedCol); - } - - public void RefreshView() - { - // 编辑模式下 Awake 可能尚未执行,按需回退初始化 - if (m_Assembler == null) - { - if (rectTransform != null) - m_Assembler = new CellAssembler(rectTransform); - else return; - } - - if (m_RowHeights == null || m_ColWidths == null) return; - if (m_RowHeights.Length == 0 || m_ColWidths.Length == 0) return; - - CreateBackground(); - -#if UNITY_EDITOR - if (!Application.isPlaying) - Canvas.ForceUpdateCanvases(); -#endif - - RecalculateTotalSize(); - UpdateVisibleRect(); - - // 更新背景尺寸 - if (m_BackgroundGo != null) - { - var bgRt = m_BackgroundGo.GetComponent(); - bgRt.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); - } - - // 回收池 - m_Assembler.ReturnAll(); - - // 计算可见范围 - 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); - - m_DirtyFlag.Clear(); - OnTableRefreshed?.Invoke(); - } - - #endregion - - #region 背景 - - private void CreateBackground() - { - if (m_BackgroundGo != null) return; - - m_BackgroundGo = new GameObject("_TableBackground", - typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); - m_BackgroundGo.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy; - m_BackgroundGo.transform.SetParent(rectTransform, false); - - RectTransform bgRt = m_BackgroundGo.GetComponent(); - bgRt.anchorMin = new Vector2(0, 1); - bgRt.anchorMax = new Vector2(0, 1); - bgRt.pivot = new Vector2(0, 1); - bgRt.anchoredPosition = Vector2.zero; - bgRt.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); - - var bgImg = m_BackgroundGo.GetComponent(); - bgImg.color = new Color(0.18f, 0.18f, 0.18f, 1f); - bgImg.raycastTarget = false; - } - - /// 完全重建表格——销毁所有动态生成的对象并重新初始化 - [ContextMenu("Xeric UI/完全重建表格")] - public void FullRebuild() - { - m_Assembler?.DestroyAll(); - - if (m_BackgroundGo != null) - { - if (Application.isPlaying) Destroy(m_BackgroundGo); - else DestroyImmediate(m_BackgroundGo); - m_BackgroundGo = null; - } - - m_TableData = new XTableData(); - - m_Assembler = new CellAssembler(rectTransform); - RecalculateTotalSize(); - CreateDefaultSampleData(); - CreateBackground(); - MarkDirty(TableDirtyType.All); - RefreshView(); - } - - #endregion - - #region 单元格渲染 - - private void RenderCells(int sr, int er, int sc, int ec) - { - Color fgColor = GetStyleColor(m_DefaultStyleNamespace, "/cell/fg/color", - new Color(0.05f, 0.05f, 0.05f)); - Color selectionColor = new Color(0.18f, 0.42f, 0.82f, 0.25f); - Color multiSelectColor = new Color(0.18f, 0.42f, 0.82f, 0.12f); - Color focusColor = new Color(0.22f, 0.55f, 0.95f, 0.35f); - int fontSize = GetStyleInt(m_DefaultStyleNamespace, "/cell/font/size", 14); - - for (int r = sr; r < er && r < m_RowHeights.Length; r++) - { - for (int c = sc; c < ec && c < m_ColWidths.Length; c++) - { - float cellX = GetColLeftX(c); - float cellY = -GetRowTopY(r); - float cellW = m_ColWidths[c]; - float cellH = m_RowHeights[r]; - XTableCellData data = TableData.GetCell(r, c); - - // 多选范围高亮(覆盖被选中范围内的所有格) - if (IsInSelectionRange(r, c)) - { - 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) - ? focusColor : multiSelectColor; - } - // 单选高亮 - else if (r == m_SelectedRow && c == m_SelectedCol) - { - 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; - } - - // 水平网格线 - var hLine = m_Assembler.GetImage("cell_hline"); - hLine.Rect.anchoredPosition = new Vector2(cellX, cellY - cellH); - hLine.Rect.sizeDelta = new Vector2(cellW, 1f); - hLine.Image.color = new Color(0.35f, 0.35f, 0.35f, 0.5f); - - // 垂直网格线 - var vLine = m_Assembler.GetImage("cell_vline"); - vLine.Rect.anchoredPosition = new Vector2(cellX + cellW - 1, cellY); - vLine.Rect.sizeDelta = new Vector2(1f, cellH); - vLine.Image.color = new Color(0.35f, 0.35f, 0.35f, 0.5f); - - if (data == null) continue; - - // 文本:与 cell 同位置同尺寸,TMP 内部 Midline 对齐实现居中 - if (!string.IsNullOrEmpty(data.Text)) - { - var txt = m_Assembler.GetText("cell_text"); - txt.Rect.anchoredPosition = new Vector2(cellX + 4, cellY); - txt.Rect.sizeDelta = new Vector2(cellW - 8, cellH); - txt.Text.text = data.Text; - txt.Text.color = fgColor; - txt.Text.fontSize = fontSize; - txt.Text.alignment = TextAlignmentOptions.Midline; - } - } - } - } - - #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 GetColLeftX(int col) - { - float x = 0; - for (int i = 0; i < col && i < m_ColWidths.Length; i++) - x += m_ColWidths[i]; - return x; - } - - private void RecalculateTotalSize() - { - m_TotalWidth = 0; - if (m_ColWidths != null) - foreach (float w in m_ColWidths) m_TotalWidth += w; - - m_TotalHeight = 0; - if (m_RowHeights != null) - foreach (float h in m_RowHeights) m_TotalHeight += h; - - if (m_ScrollRect != null && m_ScrollRect.content != null) - m_ScrollRect.content.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); - } - - private void UpdateVisibleRect() - { - m_VisibleRect = new Rect(0, 0, m_TotalWidth, m_TotalHeight); - - if (m_ScrollRect != null && m_ScrollRect.viewport != null) - { - RectTransform vt = m_ScrollRect.viewport; - RectTransform ct = m_ScrollRect.content; - if (vt != null && ct != null) - { - float sx = Mathf.Max(0, -ct.anchoredPosition.x); - float sy = Mathf.Max(0, ct.anchoredPosition.y); - m_VisibleRect = new Rect(sx, sy, vt.rect.width, vt.rect.height); - } - } - } - - private void CalcVisibleRange(out int sr, out int er, out int sc, out int ec) - { - sr = 0; er = 0; sc = 0; ec = 0; - - float accY = 0; - for (int r = 0; r < m_RowHeights.Length; r++) - { - float rowBottom = accY + m_RowHeights[r]; - if (rowBottom > m_VisibleRect.y && accY < m_VisibleRect.yMax) - { - if (er == 0 && sr == 0) sr = r; - er = r + 1; - } - accY = rowBottom; - if (accY > m_VisibleRect.yMax) break; - } - - float accX = 0; - for (int c = 0; c < m_ColWidths.Length; c++) - { - float colRight = accX + m_ColWidths[c]; - if (colRight > m_VisibleRect.x && accX < m_VisibleRect.xMax) - { - if (ec == 0 && sc == 0) sc = c; - ec = c + 1; - } - accX = colRight; - if (accX > m_VisibleRect.xMax) break; - } - - if (sr < 0) sr = 0; - if (sc < 0) sc = 0; - } - - #endregion - - #region 样式 - - private Color GetStyleColor(string ns, string path, Color fallback) - { - StyleValue val = StyleManager.GetValue(ns, path); - return val != null ? (Color)val : fallback; - } - - private int GetStyleInt(string ns, string path, int fallback) - { - StyleValue val = StyleManager.GetValue(ns, path); - return val != null ? (int)val : fallback; - } - - private void EnsureDefaultStyleNamespace() - { - if (!StyleManager.HasStyle(m_DefaultStyleNamespace, "/cell/bg/color")) - { - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, "/cell/bg/color", - new Color(0.18f, 0.18f, 0.18f)); - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, "/cell/fg/color", - new Color(0.05f, 0.05f, 0.05f)); - StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, "/cell/font/size", 14); - StyleManager.ForceRefresh(); - } - } - - #endregion - - #region 其他 - - private void MarkDirty(TableDirtyType type) => m_DirtyFlag.Mark(type); - - private void OnScrollValueChanged(Vector2 _) => MarkDirty(TableDirtyType.ViewChanged); - - #endregion - - #region IPointerClickHandler - - public void OnPointerClick(PointerEventData eventData) - { - RectTransformUtility.ScreenPointToLocalPointInRectangle( - rectTransform, eventData.position, eventData.pressEventCamera, out Vector2 localPoint); - - // rectTransform 锚点在左上角,localPoint 以中心为原点 - float tableX = localPoint.x - rectTransform.rect.xMin; - float tableY = rectTransform.rect.yMax - localPoint.y; - - float scrollY = m_ScrollRect != null ? Mathf.Max(0, m_ScrollRect.content.anchoredPosition.y) : 0; - float scrollX = m_ScrollRect != null ? Mathf.Max(0, -m_ScrollRect.content.anchoredPosition.x) : 0; - tableX += scrollX; - tableY += scrollY; - - int col = -1; - float ax = 0; - for (int c = 0; c < m_ColWidths.Length; c++) - { - if (tableX >= ax && tableX < ax + m_ColWidths[c]) { col = c; break; } - ax += m_ColWidths[c]; - } - - int row = -1; - float ay = 0; - for (int r = 0; r < m_RowHeights.Length; r++) - { - if (tableY >= ay && tableY < ay + m_RowHeights[r]) { row = r; break; } - ay += m_RowHeights[r]; - } - - if (row >= 0 && col >= 0) - { - SelectCell(row, col); - OnCellSelected?.Invoke(row, col); - } - } - - #endregion - } -} + } + + /// 完全重建表格——销毁所有动态生成的对象并重新初始化 + [ContextMenu("Xeric UI/完全重建表格")] + public void FullRebuild() + { + m_Assembler?.DestroyAll(); + ClearAllChildren(); + m_BackgroundGo = null; + + m_TableData = new XTableData(); + + m_Assembler = new CellAssembler(rectTransform); + RecalculateTotalSize(); + CreateDefaultSampleData(); + CreateBackground(); + MarkDirty(TableDirtyType.All); + RefreshView(); + } + + #endregion + + #region 单元格渲染 + + private void RenderCells(int sr, int er, int sc, int ec) + { + Color fgColor = GetStyleColor(m_DefaultStyleNamespace, "/cell/fg/color", + new Color(0.05f, 0.05f, 0.05f)); + Color selectionColor = new Color(0.18f, 0.42f, 0.82f, 0.25f); + Color multiSelectColor = new Color(0.18f, 0.42f, 0.82f, 0.12f); + Color focusColor = new Color(0.22f, 0.55f, 0.95f, 0.35f); + int fontSize = GetStyleInt(m_DefaultStyleNamespace, "/cell/font/size", 14); + + for (int r = sr; r < er && r < m_RowHeights.Length; r++) + { + for (int c = sc; c < ec && c < m_ColWidths.Length; c++) + { + float cellX = GetColLeftX(c); + float cellY = -GetRowTopY(r); + float cellW = m_ColWidths[c]; + float cellH = m_RowHeights[r]; + XTableCellData data = TableData.GetCell(r, c); + + // 多选范围高亮(覆盖被选中范围内的所有格) + if (IsInSelectionRange(r, c)) + { + 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) + ? focusColor + : multiSelectColor; + } + // 单选高亮 + else if (r == m_SelectedRow && c == m_SelectedCol) + { + 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; + } + + // 水平网格线 + var hLine = m_Assembler.GetImage("cell_hline"); + hLine.Rect.anchoredPosition = new Vector2(cellX, cellY - cellH); + hLine.Rect.sizeDelta = new Vector2(cellW, 1f); + hLine.Image.color = new Color(0.35f, 0.35f, 0.35f, 0.5f); + + // 垂直网格线 + var vLine = m_Assembler.GetImage("cell_vline"); + vLine.Rect.anchoredPosition = new Vector2(cellX + cellW - 1, cellY); + vLine.Rect.sizeDelta = new Vector2(1f, cellH); + vLine.Image.color = new Color(0.35f, 0.35f, 0.35f, 0.5f); + + if (data == null) continue; + + // 文本:与 cell 同位置同尺寸,TMP 内部 Midline 对齐实现居中 + if (!string.IsNullOrEmpty(data.Text)) + { + var txt = m_Assembler.GetText("cell_text"); + txt.Rect.anchoredPosition = new Vector2(cellX + 4, cellY); + txt.Rect.sizeDelta = new Vector2(cellW - 8, cellH); + txt.Text.text = data.Text; + txt.Text.color = fgColor; + txt.Text.fontSize = fontSize; + txt.Text.alignment = TextAlignmentOptions.Midline; + } + } + } + } + + #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 GetColLeftX(int col) + { + float x = 0; + for (int i = 0; i < col && i < m_ColWidths.Length; i++) + x += m_ColWidths[i]; + return x; + } + + private void RecalculateTotalSize() + { + m_TotalWidth = 0; + if (m_ColWidths != null) + foreach (float w in m_ColWidths) + m_TotalWidth += w; + + m_TotalHeight = 0; + if (m_RowHeights != null) + foreach (float h in m_RowHeights) + m_TotalHeight += h; + + if (m_ScrollRect != null && m_ScrollRect.content != null) + m_ScrollRect.content.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); + } + + private void UpdateVisibleRect() + { + m_VisibleRect = new Rect(0, 0, m_TotalWidth, m_TotalHeight); + + if (m_ScrollRect != null && m_ScrollRect.viewport != null) + { + RectTransform vt = m_ScrollRect.viewport; + RectTransform ct = m_ScrollRect.content; + if (vt != null && ct != null) + { + float sx = Mathf.Max(0, -ct.anchoredPosition.x); + float sy = Mathf.Max(0, ct.anchoredPosition.y); + m_VisibleRect = new Rect(sx, sy, vt.rect.width, vt.rect.height); + } + } + } + + private void CalcVisibleRange(out int sr, out int er, out int sc, out int ec) + { + sr = 0; + er = 0; + sc = 0; + ec = 0; + + float accY = 0; + for (int r = 0; r < m_RowHeights.Length; r++) + { + float rowBottom = accY + m_RowHeights[r]; + if (rowBottom > m_VisibleRect.y && accY < m_VisibleRect.yMax) + { + if (er == 0 && sr == 0) sr = r; + er = r + 1; + } + + accY = rowBottom; + if (accY > m_VisibleRect.yMax) break; + } + + float accX = 0; + for (int c = 0; c < m_ColWidths.Length; c++) + { + float colRight = accX + m_ColWidths[c]; + if (colRight > m_VisibleRect.x && accX < m_VisibleRect.xMax) + { + if (ec == 0 && sc == 0) sc = c; + ec = c + 1; + } + + accX = colRight; + if (accX > m_VisibleRect.xMax) break; + } + + if (sr < 0) sr = 0; + if (sc < 0) sc = 0; + } + + #endregion + + #region 样式 + + private Color GetStyleColor(string ns, string path, Color fallback) + { + StyleValue val = StyleManager.GetValue(ns, path); + return val != null ? (Color)val : fallback; + } + + private int GetStyleInt(string ns, string path, int fallback) + { + StyleValue val = StyleManager.GetValue(ns, path); + return val != null ? (int)val : fallback; + } + + private void EnsureDefaultStyleNamespace() + { + if (!StyleManager.HasStyle(m_DefaultStyleNamespace, "/cell/bg/color")) + { + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, "/cell/bg/color", + new Color(0.18f, 0.18f, 0.18f)); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, "/cell/fg/color", + new Color(0.05f, 0.05f, 0.05f)); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, "/cell/font/size", 14); + StyleManager.ForceRefresh(); + } + } + + #endregion + + #region 其他 + + private void MarkDirty(TableDirtyType type) => m_DirtyFlag.Mark(type); + + private void OnScrollValueChanged(Vector2 _) => MarkDirty(TableDirtyType.ViewChanged); + + #endregion + + #region IPointerClickHandler + + public void OnPointerClick(PointerEventData eventData) + { + RectTransformUtility.ScreenPointToLocalPointInRectangle( + rectTransform, eventData.position, eventData.pressEventCamera, out Vector2 localPoint); + + // rectTransform 锚点在左上角,localPoint 以中心为原点 + float tableX = localPoint.x - rectTransform.rect.xMin; + float tableY = rectTransform.rect.yMax - localPoint.y; + + float scrollY = m_ScrollRect != null ? Mathf.Max(0, m_ScrollRect.content.anchoredPosition.y) : 0; + float scrollX = m_ScrollRect != null ? Mathf.Max(0, -m_ScrollRect.content.anchoredPosition.x) : 0; + tableX += scrollX; + tableY += scrollY; + + int col = -1; + float ax = 0; + for (int c = 0; c < m_ColWidths.Length; c++) + { + if (tableX >= ax && tableX < ax + m_ColWidths[c]) + { + col = c; + break; + } + + ax += m_ColWidths[c]; + } + + int row = -1; + float ay = 0; + for (int r = 0; r < m_RowHeights.Length; r++) + { + if (tableY >= ay && tableY < ay + m_RowHeights[r]) + { + row = r; + break; + } + + ay += m_RowHeights[r]; + } + + if (row >= 0 && col >= 0) + { + SelectCell(row, col); + OnCellSelected?.Invoke(row, col); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Runtime/XTable/Rendering/Elements/CellAssembler.cs b/Runtime/XTable/Rendering/Elements/CellAssembler.cs index 3edb1ee..1d04594 100644 --- a/Runtime/XTable/Rendering/Elements/CellAssembler.cs +++ b/Runtime/XTable/Rendering/Elements/CellAssembler.cs @@ -39,6 +39,9 @@ namespace XericUI.XTable.Rendering.Elements private Transform m_Parent; private Transform m_PoolRoot; + /// 池根节点(所有回收对象均挂在此节点下,节点本身处于 inactive) + public Transform PoolRoot => m_PoolRoot; + private Stack m_ImagePool = new Stack(); private Stack m_TextPool = new Stack(); private List m_ActiveImages = new List();