From 0ea214a23f9d6aaad32adac7ad64f1a8c2088870 Mon Sep 17 00:00:00 2001 From: lrc <571244399@qq.com> Date: Sat, 27 Jun 2026 10:27:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=A1=A8=E6=A0=BC=E5=8D=95?= =?UTF-8?q?=E5=85=83=E8=BD=AC=E6=8D=A2=E5=B7=A5=E5=85=B7=E3=80=82=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=A1=A8=E6=A0=BC=E6=95=B0=E6=8D=AE=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E4=BF=9D=E5=AD=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../XTable/XericUIActionTableSceneEditor.cs | 24 +--- Runtime/XTable/Core/XTableBlock.cs | 22 ++- Runtime/XTable/Core/XTableData.cs | 59 +++++++-- .../XTable/Mapping/XTableCoordinateFormat.cs | 125 ++++++++++++++++++ .../Mapping/XTableCoordinateFormat.cs.meta | 11 ++ .../Rendering/Component/XericUIActionTable.cs | 35 ++--- 6 files changed, 226 insertions(+), 50 deletions(-) create mode 100644 Runtime/XTable/Mapping/XTableCoordinateFormat.cs create mode 100644 Runtime/XTable/Mapping/XTableCoordinateFormat.cs.meta diff --git a/Editor/XTable/XericUIActionTableSceneEditor.cs b/Editor/XTable/XericUIActionTableSceneEditor.cs index 78b6151..2283bdc 100644 --- a/Editor/XTable/XericUIActionTableSceneEditor.cs +++ b/Editor/XTable/XericUIActionTableSceneEditor.cs @@ -153,8 +153,7 @@ namespace XericUIEditor.XTable /// 更新 s_EditText 为当前焦点格的单元格内容 private static void UpdateEditText(XericUIActionTable table, int row, int col) { - var data = table.GetCellData(row, col); - s_EditText = data != null ? data.Text ?? "" : ""; + s_EditText = table.GetCellData(row, col).Text ?? ""; } #endregion @@ -399,30 +398,17 @@ namespace XericUIEditor.XTable alignment = TextAnchor.MiddleCenter }; - // 位置坐标显示当前选中(焦点格) - string cellLabel = XTableCoordinateFormat.CellToText( - table.SelectedRow, table.SelectedCol); - GUI.Label(new Rect(barRect.x + 4, barRect.y, 55, barRect.height), + // 单元格坐标(单选 "A0",多选 "A0:B1") + string cellLabel = table.GetSelectionText(); + GUI.Label(new Rect(barRect.x + 4, barRect.y, 75, barRect.height), cellLabel, labelStyle); - var rangeStyle = new GUIStyle(GUI.skin.label) - { - fontSize = 10, - normal = { textColor = new Color(0.5f, 0.6f, 0.8f) }, - alignment = TextAnchor.MiddleLeft - }; - - // 多选范围文本 - string rangeText = table.GetSelectionText(); - GUI.Label(new Rect(barRect.x + 62, barRect.y, 100, barRect.height), - rangeText, rangeStyle); - var inputStyle = new GUIStyle(GUI.skin.textField) { fontSize = 12, margin = new RectOffset(2, 2, 2, 2) }; - Rect inputRect = new Rect(barRect.x + 165, barRect.y + 2, 200, barRect.height - 4); + Rect inputRect = new Rect(barRect.x + 85, barRect.y + 2, 200, barRect.height - 4); // 回车键提交 if (Event.current.isKey && Event.current.keyCode == KeyCode.Return diff --git a/Runtime/XTable/Core/XTableBlock.cs b/Runtime/XTable/Core/XTableBlock.cs index 8165fdd..13a3ca1 100644 --- a/Runtime/XTable/Core/XTableBlock.cs +++ b/Runtime/XTable/Core/XTableBlock.cs @@ -1,12 +1,16 @@ +using System; using System.Collections.Generic; +using UnityEngine; + namespace XericUI.XTable.Core { /// /// 单个数据块——保存 BlockSizeX × BlockSizeY 个单元格数据。 /// 数据使用线性数组存储,索引 = localRow * BlockSizeX + localCol。 /// - public class XTableBlock + [Serializable] + public class XTableBlock : ISerializationCallbackReceiver { /// 块在全局块坐标系中的行号 public int BlockRow; @@ -86,5 +90,21 @@ namespace XericUI.XTable.Core } return null; } + + #region 序列化回调 + + public void OnBeforeSerialize() { } + + public void OnAfterDeserialize() + { + // 反序列化时确保 BlockSize 有效(构造函数被跳过) + if (BlockSizeX <= 0) BlockSizeX = XTableData.DefaultBlockSize; + if (BlockSizeY <= 0) BlockSizeY = XTableData.DefaultBlockSize; + // 确保数组和列表非空 + if (Cells == null) Cells = new XTableCellData[BlockSizeX * BlockSizeY]; + if (MergeDescriptors == null) MergeDescriptors = new List(); + } + + #endregion } } diff --git a/Runtime/XTable/Core/XTableData.cs b/Runtime/XTable/Core/XTableData.cs index 6658399..b632356 100644 --- a/Runtime/XTable/Core/XTableData.cs +++ b/Runtime/XTable/Core/XTableData.cs @@ -1,5 +1,8 @@ +using System; using System.Collections.Generic; +using UnityEngine; + using XericUI.XTable.Mapping; namespace XericUI.XTable.Core @@ -8,14 +11,18 @@ namespace XericUI.XTable.Core /// 表格数据主类——持有块字典,提供单元格级别的读写接口。 /// 块按需创建(SetCell 时自动创建不存在的块),通过 Z 曲线索引映射。 /// - public class XTableData + [Serializable] + public class XTableData : ISerializationCallbackReceiver { + /// 全系统唯一默认块尺寸——禁止硬编码字面量 + public const int DefaultBlockSize = 32; + #region 属性 - /// 块内列数(默认 32) + /// 块内列数(默认 ) public int BlockSizeX { get; private set; } - /// 块内行数(默认 32) + /// 块内行数(默认 ) public int BlockSizeY { get; private set; } /// @@ -24,6 +31,29 @@ namespace XericUI.XTable.Core /// public Dictionary BlockMap { get; private set; } + /// + /// 获取或创建单元格——如果单元格不存在则自动创建空数据。 + /// 在编辑器和运行时都是安全的,保证不会返回 null。 + /// + public XTableCellData GetOrCreateCell(int row, int col) + { + var data = GetCell(row, col); + if (data == null) + { + data = new XTableCellData(); + SetCell(row, col, data); + } + return data; + } + + /// + /// 判断指定单元格是否有数据 + /// + public bool HasCell(int row, int col) + { + return GetCell(row, col) != null; + } + #endregion #region 构造函数 @@ -31,17 +61,30 @@ namespace XericUI.XTable.Core /// /// 创建表格数据实例 /// - /// 块内列数,默认 32 - /// 块内行数,默认 32 - public XTableData(int blockSizeX = 32, int blockSizeY = 32) + /// 块内列数,默认 + /// 块内行数,默认 + public XTableData(int blockSizeX = DefaultBlockSize, int blockSizeY = DefaultBlockSize) { - BlockSizeX = blockSizeX > 0 ? blockSizeX : 32; - BlockSizeY = blockSizeY > 0 ? blockSizeY : 32; + BlockSizeX = blockSizeX > 0 ? blockSizeX : DefaultBlockSize; + BlockSizeY = blockSizeY > 0 ? blockSizeY : DefaultBlockSize; BlockMap = new Dictionary(); } #endregion + #region 序列化回调 + + public void OnBeforeSerialize() { } + + public void OnAfterDeserialize() + { + if (BlockSizeX <= 0) BlockSizeX = DefaultBlockSize; + if (BlockSizeY <= 0) BlockSizeY = DefaultBlockSize; + if (BlockMap == null) BlockMap = new Dictionary(); + } + + #endregion + #region 核心访问方法 /// diff --git a/Runtime/XTable/Mapping/XTableCoordinateFormat.cs b/Runtime/XTable/Mapping/XTableCoordinateFormat.cs new file mode 100644 index 0000000..7829742 --- /dev/null +++ b/Runtime/XTable/Mapping/XTableCoordinateFormat.cs @@ -0,0 +1,125 @@ +using System.Text; + +namespace XericUI.XTable.Mapping +{ + /// + /// 表格坐标文本格式化工具—— + /// 列索引 ↔ 字母(A..Z, AA..ZZ)、单元格文本 ↔ (row,col)、范围文本 ↔ 矩形选区。 + /// + public static class XTableCoordinateFormat + { + private const string ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + #region 列字母转换 + + /// 列索引转 Excel 风格字母(0→A, 25→Z, 26→AA) + public static string ColumnIndexToLetter(int col) + { + if (col < 0) return string.Empty; + + var sb = new StringBuilder(4); + do + { + sb.Insert(0, ALPHABET[col % 26]); + col = col / 26 - 1; + } while (col >= 0); + return sb.ToString(); + } + + /// Excel 字母转列索引(A→0, Z→25, AA→26) + public static int LetterToColumnIndex(string letter) + { + if (string.IsNullOrEmpty(letter)) return -1; + + int result = 0; + foreach (char c in letter.ToUpperInvariant()) + { + if (c < 'A' || c > 'Z') return -1; + result = result * 26 + (c - 'A' + 1); + } + return result - 1; + } + + #endregion + + #region 单元格文本 ↔ 索引 + + /// 单元格索引 → "A0" 格式文本 + public static string CellToText(int row, int col) + { + return $"{ColumnIndexToLetter(col)}{row}"; + } + + /// "A0" 格式文本 → (row, col),解析失败返回 false + public static bool TryParseCell(string text, out int row, out int col) + { + row = -1; + col = -1; + if (string.IsNullOrEmpty(text)) return false; + + // 分离字母前缀和数字后缀 + int i = 0; + while (i < text.Length && char.IsLetter(text[i])) i++; + if (i == 0 || i == text.Length) return false; + + string colPart = text.Substring(0, i); + string rowPart = text.Substring(i); + + col = LetterToColumnIndex(colPart); + if (col < 0) return false; + if (!int.TryParse(rowPart, out row) || row < 0) return false; + return true; + } + + #endregion + + #region 范围文本 ↔ 矩形选区 + + /// 矩形选区 → "A0:B1" 格式(统一使用左上:右下) + public static string RangeToText(int startRow, int startCol, int endRow, int endCol) + { + NormalizeRange(ref startRow, ref startCol, ref endRow, ref endCol); + return $"{ColumnIndexToLetter(startCol)}{startRow}:{ColumnIndexToLetter(endCol)}{endRow}"; + } + + /// "A0:B1" 格式文本 → 矩形选区参数,解析失败返回 false + public static bool TryParseRange(string text, + out int startRow, out int startCol, + out int endRow, out int endCol) + { + startRow = -1; startCol = -1; endRow = -1; endCol = -1; + if (string.IsNullOrEmpty(text)) return false; + + int colon = text.IndexOf(':'); + if (colon < 0) + { + // 无冒号,尝试作为单个单元格解析 + if (TryParseCell(text, out startRow, out startCol)) + { + endRow = startRow; + endCol = startCol; + return true; + } + return false; + } + + string left = text.Substring(0, colon); + string right = text.Substring(colon + 1); + + if (!TryParseCell(left, out startRow, out startCol)) return false; + if (!TryParseCell(right, out endRow, out endCol)) return false; + + return true; + } + + /// 将命名范围标准化(确保 start ≤ end),返回 (minR, minC, maxR, maxC) + public static void NormalizeRange(ref int startRow, ref int startCol, + ref int endRow, ref int endCol) + { + if (startRow > endRow) (startRow, endRow) = (endRow, startRow); + if (startCol > endCol) (startCol, endCol) = (endCol, startCol); + } + + #endregion + } +} diff --git a/Runtime/XTable/Mapping/XTableCoordinateFormat.cs.meta b/Runtime/XTable/Mapping/XTableCoordinateFormat.cs.meta new file mode 100644 index 0000000..0302610 --- /dev/null +++ b/Runtime/XTable/Mapping/XTableCoordinateFormat.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7ec7f819ca6470d42b7ebf1d054f499d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs index fa09922..b938538 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs @@ -178,12 +178,10 @@ namespace XericUI.XTable.Rendering.Component #region 默认示例数据 - [System.NonSerialized] private bool m_DefaultDataCreated; - private void CreateDefaultSampleData() { - if (m_DefaultDataCreated) return; - m_DefaultDataCreated = true; + // 仅当数据完全为空时才创建示例数据(首次使用或数据损坏后恢复) + if (m_TableData.BlockCount > 0) return; string[,] sample = new string[2, 3] { @@ -193,19 +191,10 @@ namespace XericUI.XTable.Rendering.Component for (int r = 0; r < 2; r++) for (int c = 0; c < 3; c++) - SetCellTextInternal(r, c, sample[r, c], skipDirty: true); - } - - private void SetCellTextInternal(int row, int col, string text, bool skipDirty) - { - var data = TableData.GetCell(row, col); - if (data == null) - { - data = new XTableCellData(); - TableData.SetCell(row, col, data); - } - data.Text = text; - if (!skipDirty) MarkDirty(TableDirtyType.DataChanged); + { + var data = m_TableData.GetOrCreateCell(r, c); + data.Text = sample[r, c]; + } } #endregion @@ -257,11 +246,13 @@ namespace XericUI.XTable.Rendering.Component public void SetCellText(int row, int col, string text) { - SetCellTextInternal(row, col, text, skipDirty: false); + var data = TableData.GetOrCreateCell(row, col); + data.Text = text; + MarkDirty(TableDirtyType.DataChanged); } public XTableCellData GetCellData(int row, int col) - => TableData.GetCell(row, col); + => TableData.GetOrCreateCell(row, col); public void MergeCells(int startRow, int startCol, int rowSpan, int colSpan) { @@ -349,11 +340,12 @@ namespace XericUI.XTable.Rendering.Component return true; } - /// 获取当前选区的文本描述(无多选返回 "A0",有多选返回 "A0:B1") + /// 获取当前选区的文本描述(单格 "A0",多格 "A0:B1") public string GetSelectionText() { if (m_SelectedRow < 0 || m_SelectedCol < 0) return string.Empty; - if (HasMultiSelection) + if (HasMultiSelection && + (m_SelectStartRow != m_SelectEndRow || m_SelectStartCol != m_SelectEndCol)) { return XTableCoordinateFormat.RangeToText( m_SelectStartRow, m_SelectStartCol, @@ -429,7 +421,6 @@ namespace XericUI.XTable.Rendering.Component public void FullRebuild() { m_Assembler?.DestroyAll(); - m_DefaultDataCreated = false; if (m_BackgroundGo != null) {