From 84b60c68351efacdbd6ba182591b8c4be2744f28 Mon Sep 17 00:00:00 2001
From: lrc <571244399@qq.com>
Date: Sat, 27 Jun 2026 22:07:35 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=A1=A8=E6=A0=BC=E7=9A=84?=
=?UTF-8?q?=E5=88=B7=E6=96=B0=E5=B7=A5=E4=BD=9C=E6=B5=81=E7=A8=8B=EF=BC=8C?=
=?UTF-8?q?=E4=BC=98=E5=8C=96=E8=A1=A8=E6=A0=BC=E7=9A=84=E6=93=8D=E4=BD=9C?=
=?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E8=A1=A8=E6=A0=BC=E7=9A=84=E5=BA=8F?=
=?UTF-8?q?=E5=88=97=E5=8C=96=E5=B7=A5=E4=BD=9C=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Editor/XTable/XericUIActionTableEditor.cs | 105 +-
.../XTable/XericUIActionTableSceneEditor.cs | 98 +-
Runtime/XTable/Core/XTableData.cs | 40 +-
Runtime/XTable/Core/XTableMergeDescriptor.cs | 44 +-
.../Rendering/Component/XericUIActionTable.cs | 1712 +++++++++--------
.../Rendering/Elements/CellAssembler.cs | 3 +
6 files changed, 1083 insertions(+), 919 deletions(-)
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();