因为花了好几个星期做的功能,不可能做到面面俱到,所以重点说明很难理解的部分,最后贴上项目地址,可自行查看细节
读取Excel文件的数据
个人感觉不需要去理解这部分代码,拿来主义就好,因为这种功能不太需要额外扩展
先下载所需要的Dll文件
解压并放到项目中,最好和下面文件同目录
引入命名空间
每次看教程,找不到命名空间就很无奈。。。
using Excel;
using System.Data;
using System.IO;
基础的数据结构
public class ExcelTool
{
public int col = 0;
public int row = 0;
//读取的表格数据集合
DataRowCollection collect;
//表格数据转到二维数组中,感觉这写的有点捞,谜之操作
string[][] data = null;
public string[][] Data
{
get
{
if (data == null)
Swap();
return data;
}
}
//构造函数
public ExcelTool(string path,int sheetIndex = 0)
{
ReadExcel(path, sheetIndex);
}
}
读取Excel的核心代码
//表格路径,表格索引(一个excel文件中,存在好几张表)
void ReadExcel(string path, int sheetIndex)
{
//打开文件流 ,路径是从磁盘的根目录开始的,到具体的excel文件如:C:\Users\Ai\Desktop\text.xlsx
FileStream stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
//读取excel文件
IExcelDataReader excleRead = ExcelReaderFactory.CreateOpenXmlReader(stream);
//获取excel文件的读取结果
DataSet result = excleRead.AsDataSet();
//
collect = result.Tables[sheetIndex].Rows;
col = result.Tables[sheetIndex].Columns.Count;
row = result.Tables[sheetIndex].Rows.Count;
//因为excel设置内容格式会导致多读行,列的内容,这里是为了去除多读的内容
while (row > 0)
{
if (collect[row - 1][0].ToString() == "")
{
row--;
continue;
}
break;
}
while (col > 0)
{
if (collect[0][col - 1].ToString() == "")
{
col--;
continue;
}
break;
}
}
格式转换
没啥好说的,基本操作
private void Swap()
{
data = new string[row][];
for (int i = 0; i < row; i++)
{
string[] str = new string[col];
for (int j = 0; j < col; j++)
{
str[j] = collect[i][j].ToString();
}
data[i] = str;
}
}
实际运用
//返回表格的第sheetIndex(索引从0开始)张表格所有数据
ExcelTool excelData = new ExcelTool(excelPath,sheetIndex);
生成ScriptableObject文件
因为表格的数据格式是非常明确地,所有可以定义一个继承ScriptableObject类的基类
封装ScriptableObject基类
/// <summary>
/// 为了窗口可视化,增加一个类ExcelToolRowData
/// </summary>
public class ExcelToolBaseSO : ScriptableObject
{
//默认表格的第一行数据为key
public string[] keys;
public List<ExcelToolRowData> data = new List<ExcelToolRowData>();
public void AddNode(string[] values)
{
data.Add(new ExcelToolRowData(values));
}
}
//加上此属性,可以在inspect面板上显示类里面的数据
//该类可以存储表格中,某一行的所有数据
[System.Serializable]
public class ExcelToolRowData
{
public string[] rowData;
}
读取Excel文件是,生成ScriptableObject文件,并写入相关数据
//表格路径
//ScriptableObject资源生成的路径
//表格的第几张表
//忽略的行数 ,因为第一行是key,第二行可能是备注信息等等
public void ReadExcelToSO<T>(string excelPath, string assetPath,int sheetIndex, int ignoreRowNum = 2) where T : ExcelToolBaseSO
{
//路径处理,当相对路径是,修改为绝对路径
if (excelPath.StartsWith("Assets"))
{
excelPath = Application.dataPath + excelPath.Substring(6);
}
//读取表格,并获取表格中所有数据
ExcelTool excelData = new ExcelTool(excelPath,sheetIndex);
//读取ScriptableObject资源文件
var excelAsset = AssetDatabase.LoadAssetAtPath<T>(assetPath);
//如果没有资源文件,就创建资源文件
if (excelAsset == null)
{
excelAsset = ScriptableObject.CreateInstance<T>();
AssetDatabase.CreateAsset(excelAsset, assetPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
//资源文件清空并赋值
excelAsset.data.Clear();
excelAsset.keys=excelData.Data[0];
excelAsset.valueTypes = excelData.Data[1];
foreach (var rowData in excelData.Data)
{
if (ignoreRowNum > 0)
{
ignoreRowNum--;
continue;
}
excelAsset.AddNode(rowData);
}
}
读取Excel功能总结
- excel文件数据是不可直接使用的,读取生成ScriptableObject资源文件后,可以直接在程序中使用。
而这个过程有两个必须的参数,excel文件路径,以及后续生成的ScriptableObject文件的路径 - 并且这个过程都是需要在编辑模式下进行的,制定一个自定义窗口,填写两个文件路径,就可以根据excel资源路径,获取ScriptableObject资源文件。
写入Excel功能
其实我不太希望在Unity编辑ScriptableObject文件,进而影响程序的,但是考虑到,直接修改效率会高一点,属于是可以不用,但是功能必须给到。
唯一坑的地方就是写入的时候数组下标从1开始(简直了)
/// <summary>
/// 使用SO的内容,并覆写Excel文件
/// LookAt ExcelPackage 里面的数组从1开始:坑啊
/// </summary>
/// <typeparam name="T">SO的文件类型</typeparam>
/// <param name="excelPath">excel资源路径</param>
/// <param name="assetPath">输出到SO的路径</param>
/// <param name="ignoreRowNum">忽略的首行数,默认为2(第一行Keys,第二行中文备注)</param>
public void WriteSOToExcel<T>(string excelPath, string assetPath, int ignoreRowNum = 2) where T : ExcelToolBaseSO
{
if (excelPath.StartsWith("Assets"))
{
excelPath = Application.dataPath + excelPath.Substring(6);
}
FileInfo excelFile = new FileInfo(excelPath);
ExcelPackage package = new ExcelPackage(excelFile);
ExcelWorksheet worksheet = package.Workbook.Worksheets[1];
T excelAsset = AssetDatabase.LoadAssetAtPath<T>(assetPath);
for (int i = 1; i <= excelAsset.data.Count; i++)
{
string[] rowData = excelAsset.data[i-1].rowData;
for (int j = 1; j <= rowData.Length; j++)
{
worksheet.SetValue(i + ignoreRowNum, j, rowData[j-1]);
}
}
//修改某一行的数据
//worksheet.Cells[4, 3].Value = "女";
//worksheet.SetValue(4, 3, "女");
//保存excel
package.Save();
}
Unity自定义窗口
这部分就没什么技巧,都是体力活,可能相关自定义窗口语法不清楚,我在代码中给到注释,以及给出我参考他人的案例
自定义语法参考
最后的结果
我个人还是很满意写的这个工具窗口,希望阅读的你也喜欢,要是你能用上我的工具,那就是这篇文章存在的意义了。
自定义窗口总结
- 虽然有好几个类都是实现自定义窗口,一套写下来,感觉GUILayout自带的布局功能不错
public class ExcelToolExtension : EditorWindow
{
string excelPath = @"";
string outputPath = @"";
string notePath = @"Assets/FrameWork/ExcelTool/ExcelToolLoadNoteAsset.asset";
bool firstState = true;
int toolGirdId = 0;
Vector2 scrollPos = Vector2.zero;
ExcelToolLoadNoteSO note;
public ExcelToolLoadNoteSO Note
{
get
{
if (note == null)
{
note = AssetDatabase.LoadAssetAtPath<ExcelToolLoadNoteSO>(notePath);
}
return note;
}
}
[MenuItem("Framework/Window/ExcalTool")]
public static void Init()
{
ExcelToolExtension exWindow = GetWindow<ExcelToolExtension>();
exWindow.Show();
}
public void OnGUI()
{
EditorGUILayout.LabelField("Excel数据与Asset资源处理");
toolGirdId = GUILayout.Toolbar(toolGirdId, new[] { "重新导入Excel", "导入新Excel","移除Excel的Asset文件","Asset文件写入Excel" },GUILayout.Height(50));
if (firstState)
{
outputPath = Note.outputPath;
}
switch (toolGirdId)
{
case 0:
if (GUILayout.Button("重新导入所有Excel!!!",GUILayout.Height(50)))
{
LoadAllExcel();
};
GUILayout.BeginHorizontal();
GUILayout.Space(10);
scrollPos = GUILayout.BeginScrollView(scrollPos);
ShowAllLoadedExcel(LoadExcel,"重新导入");
GUILayout.EndScrollView();
GUILayout.EndHorizontal();
break;
case 1:
GUILayout.Space(5);
EditorGUILayout.LabelField("Excel资源路径案例: " + @"Assets/FrameWork/ExcelTool/ExcelSample.xlsx");
EditorGUILayout.LabelField("Asset输出目录案例: " + @"Assets/FrameWork/ExcelTool");
GUILayout.Label("输出路径与Excel同目录: 生成相应的Asset文件,文件在Excel同目录下,名称为Excel文件名+表名");
GUILayout.Box("", GUILayout.Height(5), GUILayout.ExpandWidth(true));
excelPath = EditorGUILayout.TextField("Excel资源路径", excelPath);
outputPath = EditorGUILayout.TextField("Asset输出目录", outputPath);
GUILayout.Space(10);
GUILayout.BeginHorizontal();
if (GUILayout.Button("输出路径与表格同目录",GUILayout.Height(50)))
{
outputPath = GetDefaultPathByExcelPath(excelPath);
}
if (GUILayout.Button("导入Excel数据", GUILayout.Height(50)))
{
LoadExcel(excelPath, outputPath);
}
GUILayout.EndHorizontal();
break;
case 2:
if (GUILayout.Button("移除所有Excel的Asset文件,并删除导入记录!!!", GUILayout.Height(50)))
{
RemoveAllExcelSO();
};
GUILayout.BeginHorizontal();
GUILayout.Space(10);
scrollPos = GUILayout.BeginScrollView(scrollPos);
ShowAllLoadedExcel(RemoveExcelSO,"移除Asset文件");
GUILayout.EndScrollView();
GUILayout.EndHorizontal();
break;
case 3:
if (GUILayout.Button("所有Asset文件写入Excel!!!", GUILayout.Height(50)))
{
WriteAllExcel();
};
GUILayout.BeginHorizontal();
GUILayout.Space(10);
scrollPos = GUILayout.BeginScrollView(scrollPos);
ShowAllLoadedExcel(WriteExcel, "写入Excel文件");
GUILayout.EndScrollView();
GUILayout.EndHorizontal();
break;
default:
break;
}
firstState = false;
}
public void LoadExcel(string excelPath,string outputPath)
{
if(string.IsNullOrEmpty(excelPath) || string.IsNullOrEmpty(outputPath))
{
Debug.Log("Excel路径和资源输出路径不能为空");
return;
}
List<string> sheetNames = AssetUtil.Ins.GetExcelTableNames(excelPath);
string excelName = AssetUtil.Ins.GetFileNameByPath(excelPath);
List<string> paths = new List<string>();
for (int i = 0;i < sheetNames.Count; i++)
{
string path = outputPath+"/" + excelName + sheetNames[i] + ".asset";
paths.Add(path);
AssetUtil.Ins.ReadExcelToSO<ExcelToolBaseSO>(excelPath, path,i);
}
Note.AddNode(excelPath,outputPath,paths);
Debug.Log("导入" + excelPath + "表格");
}
public void LoadAllExcel()
{
foreach (var item in Note.Notes)
{
LoadExcel(item.excelPath, outputPath);
}
Debug.Log("重新导入所有表格");
}
public void RemoveExcelSO(string excelPath)
{
Note.Remove(excelPath);
Debug.Log("移除"+excelPath+"的Asset文件");
}
public void RemoveAllExcelSO()
{
foreach (var item in Note.Notes)
{
RemoveExcelSO(item.excelPath);
}
Note.Notes.Clear();
Debug.Log("移除所有Excel的Asset文件");
}
public void WriteExcel(string excelPath, string outputPath)
{
AssetUtil.Ins.WriteSOToExcel<ExcelToolBaseSO>(excelPath, outputPath);
Debug.Log("覆写" + excelPath + "文件");
}
public void WriteAllExcel()
{
foreach (var item in Note.Notes)
{
WriteExcel(item.excelPath, item.outputPath);
}
Debug.Log("所有Asset文件覆写Excel!!!");
}
public void ShowAllLoadedExcel(Action<string,string> action,string btnText)
{
for (int i = 0; i < Note.Notes.Count; i++)
{
string excelPath = Note.Notes[i].excelPath;
string outputPath = Note.Notes[i].outputPath;
List<string> paths = Note.Notes[i].paths;
GUILayout.BeginHorizontal(GUILayout.MinHeight(50));
GUILayout.BeginVertical(GUILayout.Width(500));
GUILayout.Space(5);
EditorGUILayout.LabelField("Excel表格资源路径: " + excelPath);
for (int j = 0; j < paths.Count; j++)
{
GUILayout.Space(5);
EditorGUILayout.LabelField("Asset路径: " + paths[j]);
}
GUILayout.EndVertical();
GUILayout.FlexibleSpace();
if (GUILayout.Button(btnText, GUILayout.Width(100), GUILayout.ExpandHeight(true)))
{
action(excelPath, outputPath);
};
GUILayout.Space(15);
GUILayout.EndHorizontal();
GUILayout.Box("", GUILayout.Height(5), GUILayout.ExpandWidth(true));
}
}
public void ShowAllLoadedExcel(Action<string> action, string btnText)
{
for (int i = 0; i < Note.Notes.Count; i++)
{
string excelPath = Note.Notes[i].excelPath;
string outputPath = Note.Notes[i].outputPath;
List<string> paths = Note.Notes[i].paths;
GUILayout.BeginHorizontal(GUILayout.MinHeight(50));
GUILayout.BeginVertical(GUILayout.Width(500));
GUILayout.Space(5);
EditorGUILayout.LabelField("Excel表格资源路径: " + excelPath);
for (int j = 0; j < paths.Count; j++)
{
GUILayout.Space(5);
EditorGUILayout.LabelField("Asset路径: " + paths[j]);
}
GUILayout.EndVertical();
GUILayout.FlexibleSpace();
if (GUILayout.Button(btnText, GUILayout.Width(100),GUILayout.ExpandHeight(true)))
{
action(excelPath);
};
GUILayout.Space(15);
GUILayout.EndHorizontal();
GUILayout.Box("", GUILayout.Height(5), GUILayout.ExpandWidth(true));
}
}
public string GetDefaultPathByExcelPath(string excelPath)
{
if (string.IsNullOrEmpty(excelPath))
{
Debug.Log("Excel路径不能为空");
return null;
}
string path = AssetUtil.Ins.GetDirectoryPathByPath(excelPath);
Note.outputPath = path;
return path;
}
}
用到的几个自定义函数
/// <summary>
/// 获取Excel所有表的名称
/// </summary>
/// <param name="path">Excel的文件路径</param>
/// <returns></returns>
public List<string> GetExcelTableNames(string path)
{
if (path.StartsWith("Assets"))
{
path = Application.dataPath + path.Substring(6);
}
FileStream stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
IExcelDataReader excleRead = ExcelReaderFactory.CreateOpenXmlReader(stream);
DataSet result = excleRead.AsDataSet();
List<string> names = new List<string>();
for (int i = 0; i < result.Tables.Count; i++)
{
names.Add(result.Tables[i].TableName);
}
return names;
}
public string GetFileNameByPath(string path)
{
string[] subPath = path.Split(".xlsx");
string[] subStr = subPath[0].Split("/");
return subStr[subStr.Length - 1];
}
public string GetDirectoryPathByPath(string path)
{
string[] subPath = path.Split("/");
string directoryPath = "";
for (int i = 0; i < subPath.Length-1; i++)
{
directoryPath += subPath[i];
directoryPath += "/";
}
return directoryPath.Substring(0,directoryPath.Length - 1);
}
项目地址
我自己写的一些工具,系统,里面库里面的ExcelTool下包含此文章的所有内容,希望可以被建议或者帮组到读者。
最后的总结
- 工具本身没什么问题
- 最后得到的ScriptableObject资源文件的成员都是string类型,会不利于后续的其他类型的数据访问,频繁的装箱,插箱会影响性能。
- 设想通过表格的第二行存储当前列的数据格式,然后动态生成相应的类(这个功能尝试很久无果),后续的设计也就没法进行
- 希望有志人士可以精进一下
- 如果你真的看到这里,点个赞,让我知道,我也曾被欣赏。