添加表格单元转换工具。
修复表格数据结构保存。
This commit is contained in:
@@ -153,8 +153,7 @@ namespace XericUIEditor.XTable
|
||||
/// <summary>更新 s_EditText 为当前焦点格的单元格内容</summary>
|
||||
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
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.XTable.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个数据块——保存 BlockSizeX × BlockSizeY 个单元格数据。
|
||||
/// 数据使用线性数组存储,索引 = localRow * BlockSizeX + localCol。
|
||||
/// </summary>
|
||||
public class XTableBlock
|
||||
[Serializable]
|
||||
public class XTableBlock : ISerializationCallbackReceiver
|
||||
{
|
||||
/// <summary>块在全局块坐标系中的行号</summary>
|
||||
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<XTableMergeDescriptor>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 曲线索引映射。
|
||||
/// </summary>
|
||||
public class XTableData
|
||||
[Serializable]
|
||||
public class XTableData : ISerializationCallbackReceiver
|
||||
{
|
||||
/// <summary>全系统唯一默认块尺寸——禁止硬编码字面量</summary>
|
||||
public const int DefaultBlockSize = 32;
|
||||
|
||||
#region 属性
|
||||
|
||||
/// <summary>块内列数(默认 32)</summary>
|
||||
/// <summary>块内列数(默认 <see cref="DefaultBlockSize"/>)</summary>
|
||||
public int BlockSizeX { get; private set; }
|
||||
|
||||
/// <summary>块内行数(默认 32)</summary>
|
||||
/// <summary>块内行数(默认 <see cref="DefaultBlockSize"/>)</summary>
|
||||
public int BlockSizeY { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -24,6 +31,29 @@ namespace XericUI.XTable.Core
|
||||
/// </summary>
|
||||
public Dictionary<ulong, XTableBlock> BlockMap { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或创建单元格——如果单元格不存在则自动创建空数据。
|
||||
/// 在编辑器和运行时都是安全的,保证不会返回 null。
|
||||
/// </summary>
|
||||
public XTableCellData GetOrCreateCell(int row, int col)
|
||||
{
|
||||
var data = GetCell(row, col);
|
||||
if (data == null)
|
||||
{
|
||||
data = new XTableCellData();
|
||||
SetCell(row, col, data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断指定单元格是否有数据
|
||||
/// </summary>
|
||||
public bool HasCell(int row, int col)
|
||||
{
|
||||
return GetCell(row, col) != null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 构造函数
|
||||
@@ -31,17 +61,30 @@ namespace XericUI.XTable.Core
|
||||
/// <summary>
|
||||
/// 创建表格数据实例
|
||||
/// </summary>
|
||||
/// <param name="blockSizeX">块内列数,默认 32</param>
|
||||
/// <param name="blockSizeY">块内行数,默认 32</param>
|
||||
public XTableData(int blockSizeX = 32, int blockSizeY = 32)
|
||||
/// <param name="blockSizeX">块内列数,默认 <see cref="DefaultBlockSize"/></param>
|
||||
/// <param name="blockSizeY">块内行数,默认 <see cref="DefaultBlockSize"/></param>
|
||||
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<ulong, XTableBlock>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 序列化回调
|
||||
|
||||
public void OnBeforeSerialize() { }
|
||||
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
if (BlockSizeX <= 0) BlockSizeX = DefaultBlockSize;
|
||||
if (BlockSizeY <= 0) BlockSizeY = DefaultBlockSize;
|
||||
if (BlockMap == null) BlockMap = new Dictionary<ulong, XTableBlock>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 核心访问方法
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
using System.Text;
|
||||
|
||||
namespace XericUI.XTable.Mapping
|
||||
{
|
||||
/// <summary>
|
||||
/// 表格坐标文本格式化工具——
|
||||
/// 列索引 ↔ 字母(A..Z, AA..ZZ)、单元格文本 ↔ (row,col)、范围文本 ↔ 矩形选区。
|
||||
/// </summary>
|
||||
public static class XTableCoordinateFormat
|
||||
{
|
||||
private const string ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
|
||||
#region 列字母转换
|
||||
|
||||
/// <summary>列索引转 Excel 风格字母(0→A, 25→Z, 26→AA)</summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>Excel 字母转列索引(A→0, Z→25, AA→26)</summary>
|
||||
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 单元格文本 ↔ 索引
|
||||
|
||||
/// <summary>单元格索引 → "A0" 格式文本</summary>
|
||||
public static string CellToText(int row, int col)
|
||||
{
|
||||
return $"{ColumnIndexToLetter(col)}{row}";
|
||||
}
|
||||
|
||||
/// <summary>"A0" 格式文本 → (row, col),解析失败返回 false</summary>
|
||||
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 范围文本 ↔ 矩形选区
|
||||
|
||||
/// <summary>矩形选区 → "A0:B1" 格式(统一使用左上:右下)</summary>
|
||||
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}";
|
||||
}
|
||||
|
||||
/// <summary>"A0:B1" 格式文本 → 矩形选区参数,解析失败返回 false</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>将命名范围标准化(确保 start ≤ end),返回 (minR, minC, maxR, maxC)</summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ec7f819ca6470d42b7ebf1d054f499d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>获取当前选区的文本描述(无多选返回 "A0",有多选返回 "A0:B1")</summary>
|
||||
/// <summary>获取当前选区的文本描述(单格 "A0",多格 "A0:B1")</summary>
|
||||
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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user