• Unity多语言本地化改进版


    简介

    之前捣鼓过一个通过csv配置游戏多语言支持的小工具,但是发现使用过程中,通过notepad++去进行转码很不方便,并且直接将配置的csv不加密的放在游戏中心里感觉不是很踏实
    于是乎~~

    新的方案

    1.在PC/MAC平台上解析多语言配置,也就是editor运行环境中解析csv或者excel
    2.通过在Editor运行过程中生成多个语言对象,然后序列化并加密存盘
    3.在使用端(移动端)通过resources加载加密之后的文件
    4.读取对应的语言序列化文件并实例化加载到游戏中进行使用

    使用方法

    由于不想让csv文件被打包入游戏工程中,所以选择与Assets文件夹并列的路径文件:
    PS:当然路径可以自己指定
    这里写图片描述
    然后里面存放多语言csv
    这里写图片描述
    然后代码中是调用是这样的:
    这里写图片描述

    代码实现

    序列化反序列化工具

    
    using Newtonsoft.Json;
    using System;
    using System.IO;
    using System.Security.Cryptography;
    using System.Text;
    
    public static class SaveHelper
    {
    
        private const string M_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
    
        public static bool IsFileExist(string filePath)
        {
            return File.Exists(filePath);
        }
    
        public static bool IsDirectoryExists(string filePath)
        {
            return Directory.Exists(filePath);
        }
    
        public static void CreateFile(string fileName, string content)
        {
            StreamWriter streamWriter = File.CreateText(fileName);
            streamWriter.Write(content);
            streamWriter.Close();
        }
    
        public static void CreateDirectory(string filePath)
        {
            if (IsDirectoryExists(filePath))
            {
                return;
            }
            Directory.CreateDirectory(filePath);
        }
    
        private static string SerializeObject(object pObject)
        {
            string serializedString = string.Empty;
            serializedString = JsonConvert.SerializeObject(pObject);
            return serializedString;
        }
    
        private static object DeserializeObject(string pString, Type pType)
        {
            object deserializedObject = null;
            deserializedObject = JsonConvert.DeserializeObject(pString, pType);
            return deserializedObject;
        }
    
        private static string RijndaelEncrypt(string pString, string pKey)
        {
            byte[] keyArray = UTF8Encoding.UTF8.GetBytes(pKey);
            byte[] toEncryptArray = UTF8Encoding.UTF8.GetBytes(pString);
            RijndaelManaged rDel = new RijndaelManaged();
            rDel.Key = keyArray;
            rDel.Mode = CipherMode.ECB;
            rDel.Padding = PaddingMode.PKCS7;
            ICryptoTransform cTransform = rDel.CreateEncryptor();
            byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);
            return Convert.ToBase64String(resultArray, 0, resultArray.Length);
        }
    
        private static String RijndaelDecrypt(string pString, string pKey)
        {
            byte[] keyArray = UTF8Encoding.UTF8.GetBytes(pKey);
            byte[] toEncryptArray = Convert.FromBase64String(pString);
            RijndaelManaged rDel = new RijndaelManaged();
            rDel.Key = keyArray;
            rDel.Mode = CipherMode.ECB;
            rDel.Padding = PaddingMode.PKCS7;
            ICryptoTransform cTransform = rDel.CreateDecryptor();
            byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);
            return UTF8Encoding.UTF8.GetString(resultArray);
        }
    
        public static void SaveData(string fileName, object pObject)
        {
            // 如果文件已存在,则删除
            if (File.Exists(fileName))
            {
                File.Delete(fileName);
            }
            string toSave = SerializeObject(pObject);
            toSave = RijndaelEncrypt(toSave, M_KEY);
            StreamWriter streamWriter = File.CreateText(fileName);
            streamWriter.Write(toSave);
            streamWriter.Close();
        }
    
        public static object ReadData(string str, Type pType, bool isFile = true)
        {
            string data;
            if (isFile)
            {
                // 如果文件不存在,则返回空
                if (!File.Exists(str))
                {
                    return null;
                }
                StreamReader streamReader = File.OpenText(str);
                data = streamReader.ReadToEnd();
                streamReader.Close();
            }
            else
            {
                data = str;
            }
    
            data = RijndaelDecrypt(data, M_KEY);
            return DeserializeObject(data, pType);
        }
    }
    

    CSV解析器

    using System.Collections.Generic;
    using System.IO;
    using System.Text;
    
    // col是竖行,row是横排,防止我忘了
    public class LTCSVLoader
    {
    
        private TextReader inStream = null;
    
        private List<string> vContent;
    
        private List<List<string>> table;
    
        /// <summary>
        /// 只支持GBK2312的编码(WPS直接保存的编码支持,仅提供给Windows使用)
        /// </summary>
        /// <param name="fileName"></param>
        public void ReadFile(string fileName)
        {
            inStream = new StreamReader(fileName, Encoding.GetEncoding("GBK"));
            table = new List<List<string>>();
            List<string> temp = this.getLineContentVector();
            while (null != temp)
            {
                List<string> tempList = new List<string>();
                for (int i = 0; i < temp.Count; ++i)
                {
                    tempList.Add(temp[i]);
                }
                table.Add(tempList);
                temp = this.getLineContentVector();
            }
        }
    
        /// <summary>
        /// 目前只支持UTF-8的编码(WPS直接保存的编码不支持)
        /// </summary>
        /// <param name="str"></param>
        public void ReadMultiLine(string str)
        {
            inStream = new StringReader(str);
    
            table = new List<List<string>>();
            List<string> temp = this.getLineContentVector();
            while (null != temp)
            {
                List<string> tempList = new List<string>();
                for (int i = 0; i < temp.Count; ++i)
                {
                    tempList.Add(temp[i]);
                }
                table.Add(tempList);
                temp = this.getLineContentVector();
            }
        }
    
        private int containsNumber(string parentStr, string parameter)
        {
            int containNumber = 0;
            if (parentStr == null || parentStr.Equals(""))
            {
                return 0;
            }
            if (parameter == null || parameter.Equals(""))
            {
                return 0;
            }
            for (int i = 0; i < parentStr.Length; i++)
            {
                i = parentStr.IndexOf(parameter, i);
                if (i > -1)
                {
                    i = i + parameter.Length;
                    i--;
                    containNumber = containNumber + 1;
                }
                else
                {
                    break;
                }
            }
            return containNumber;
        }
    
        private bool isQuoteAdjacent(string p_String)
        {
            bool ret = false;
            string temp = p_String;
            temp = temp.Replace("""", "");
            if (temp.IndexOf(""") == -1)
            {
                ret = true;
            }
            return ret;
        }
    
        private bool isQuoteContained(string p_String)
        {
            bool ret = false;
            if (p_String == null || p_String.Equals(""))
            {
                return false;
            }
            if (p_String.IndexOf(""") > -1)
            {
                ret = true;
            }
            return ret;
        }
    
        private string[] readAtomString(string lineStr)
        {
            string atomString = "";// 要读取的原子字符串
            string orgString = "";// 保存第一次读取下一个逗号时的未经任何处理的字符串
            string[] ret = new string[2];// 要返回到外面的数组
            bool isAtom = false;// 是否是原子字符串的标志
            string[] commaStr = lineStr.Split(new char[] { ',' });
            while (!isAtom)
            {
                foreach (string str in commaStr)
                {
                    if (!atomString.Equals(""))
                    {
                        atomString = atomString + ",";
                    }
                    atomString = atomString + str;
                    orgString = atomString;
                    if (!isQuoteContained(atomString))
                    {
                        // 如果字符串中不包含引号,则为正常,返回
                        isAtom = true;
                        break;
                    }
                    else
                    {
                        if (!atomString.StartsWith("""))
                        {
                            // 如果字符串不是以引号开始,则表示不转义,返回
                            isAtom = true;
                            break;
                        }
                        else if (atomString.StartsWith("""))
                        {
                            // 如果字符串以引号开始,则表示转义
                            if (containsNumber(atomString, """) % 2 == 0)
                            {
                                // 如果含有偶数个引号
                                string temp = atomString;
                                if (temp.EndsWith("""))
                                {
                                    temp = temp.Replace("""", "");
                                    if (temp.Equals(""))
                                    {
                                        // 如果temp为空
                                        atomString = "";
                                        isAtom = true;
                                        break;
                                    }
                                    else
                                    {
                                        // 如果temp不为空,则去掉前后引号
                                        temp = temp.Substring(1, temp.LastIndexOf("""));
                                        if (temp.IndexOf(""") > -1)
                                        {
                                            // 去掉前后引号和相邻引号之后,若temp还包含有引号
                                            // 说明这些引号是单个单个出现的
                                            temp = atomString;
                                            temp = temp.Substring(1);
                                            temp = temp.Substring(0, temp.IndexOf("""))
                                                    + temp.Substring(temp.IndexOf(""") + 1);
                                            atomString = temp;
                                            isAtom = true;
                                            break;
                                        }
                                        else
                                        {
                                            // 正常的csv文件
                                            temp = atomString;
                                            temp = temp.Substring(1, temp.LastIndexOf("""));
                                            temp = temp.Replace("""", """);
                                            atomString = temp;
                                            isAtom = true;
                                            break;
                                        }
                                    }
                                }
                                else
                                {
                                    // 如果不是以引号结束,则去掉前两个引号
                                    temp = temp.Substring(1, temp.IndexOf('"', 1))
                                            + temp.Substring(temp.IndexOf('"', 1) + 1);
                                    atomString = temp;
                                    isAtom = true;
                                    break;
                                }
                            }
                            else
                            {
                                // 如果含有奇数个引号
                                if (!atomString.Equals("""))
                                {
                                    string tempAtomStr = atomString.Substring(1);
                                    if (!isQuoteAdjacent(tempAtomStr))
                                    {
                                        // 这里做的原因是,如果判断前面的字符串不是原子字符串的时候就读取第一个取到的字符串
                                        // 后面取到的字符串不计入该原子字符串
                                        tempAtomStr = atomString.Substring(1);
                                        int tempQutoIndex = tempAtomStr.IndexOf(""");
                                        // 这里既然有奇数个quto,所以第二个quto肯定不是最后一个
                                        tempAtomStr = tempAtomStr.Substring(0, tempQutoIndex)
                                                + tempAtomStr.Substring(tempQutoIndex + 1);
                                        atomString = tempAtomStr;
                                        isAtom = true;
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
            }
            // 先去掉之前读取的原字符串的母字符串
            if (lineStr.Length > orgString.Length)
            {
                lineStr = lineStr.Substring(orgString.Length);
            }
            else
            {
                lineStr = "";
            }
            // 去掉之后,判断是否以逗号开始,如果以逗号开始则去掉逗号
            if (lineStr.StartsWith(","))
            {
                if (lineStr.Length > 1)
                {
                    lineStr = lineStr.Substring(1);
                }
                else
                {
                    lineStr = "";
                }
            }
            ret[0] = atomString;
            ret[1] = lineStr;
            return ret;
        }
    
        private bool readCSVNextRecord()
        {
            // 如果流未被初始化则返回false
            if (inStream == null)
            {
                return false;
            }
            // 如果结果向量未被初始化,则初始化
            if (vContent == null)
            {
                vContent = new List<string>();
            }
            // 移除向量中以前的元素
            vContent.Clear();
            // 声明逻辑行
            string logicLineStr = "";
            // 用于存放读到的行
            StringBuilder strb = new StringBuilder();
            // 声明是否为逻辑行的标志,初始化为false
            bool isLogicLine = false;
            while (!isLogicLine)
            {
                string newLineStr = inStream.ReadLine();
                if (newLineStr == null)
                {
                    strb = null;
                    vContent = null;
                    isLogicLine = true;
                    break;
                }
                if (newLineStr.StartsWith("#"))
                {
                    // 去掉注释
                    continue;
                }
                if (!strb.ToString().Equals(""))
                {
                    strb.Append("
    ");
                }
                strb.Append(newLineStr);
                string oldLineStr = strb.ToString();
                if (oldLineStr.IndexOf(",") == -1)
                {
                    // 如果该行未包含逗号
                    if (containsNumber(oldLineStr, """) % 2 == 0)
                    {
                        // 如果包含偶数个引号
                        isLogicLine = true;
                        break;
                    }
                    else
                    {
                        if (oldLineStr.StartsWith("""))
                        {
                            if (oldLineStr.Equals("""))
                            {
                                continue;
                            }
                            else
                            {
                                string tempOldStr = oldLineStr.Substring(1);
                                if (isQuoteAdjacent(tempOldStr))
                                {
                                    // 如果剩下的引号两两相邻,则不是一行
                                    continue;
                                }
                                else
                                {
                                    // 否则就是一行
                                    isLogicLine = true;
                                    break;
                                }
                            }
                        }
                    }
                }
                else
                {
                    // quotes表示复数的quote
                    string tempOldLineStr = oldLineStr.Replace("""", "");
                    int lastQuoteIndex = tempOldLineStr.LastIndexOf(""");
                    if (lastQuoteIndex == 0)
                    {
                        continue;
                    }
                    else if (lastQuoteIndex == -1)
                    {
                        isLogicLine = true;
                        break;
                    }
                    else
                    {
                        tempOldLineStr = tempOldLineStr.Replace("","", "");
                        lastQuoteIndex = tempOldLineStr.LastIndexOf(""");
                        if (lastQuoteIndex == 0)
                        {
                            continue;
                        }
                        if (tempOldLineStr[lastQuoteIndex - 1] == ',')
                        {
                            continue;
                        }
                        else
                        {
                            isLogicLine = true;
                            break;
                        }
                    }
                }
            }
            if (strb == null)
            {
                // 读到行尾时为返回
                return false;
            }
            // 提取逻辑行
            logicLineStr = strb.ToString();
            if (logicLineStr != null)
            {
                // 拆分逻辑行,把分离出来的原子字符串放入向量中
                while (!logicLineStr.Equals(""))
                {
                    string[] ret = readAtomString(logicLineStr);
                    string atomString = ret[0];
                    logicLineStr = ret[1];
                    vContent.Add(atomString);
                }
            }
            return true;
        }
    
        private List<string> getLineContentVector()
        {
            if (this.readCSVNextRecord())
            {
                return this.vContent;
            }
            return null;
        }
    
        private List<string> getVContent()
        {
            return this.vContent;
        }
    
        public int GetRow()
        {
            if (null == table)
            {
                throw new System.Exception("table尚未初始化,请检查是否成功读取");
            }
            return table.Count;
        }
    
        public int GetCol()
        {
            if (null == table)
            {
                throw new System.Exception("table尚未初始化,请检查是否成功读取");
            }
            if (table.Count == 0)
            {
                throw new System.Exception("table内容为空");
            }
            return table[0].Count;
        }
    
        public int GetFirstIndexAtCol(string str, int col)
        {
            if (null == table)
            {
                throw new System.Exception("table尚未初始化,请检查是否成功读取");
            }
            if (table.Count == 0)
            {
                throw new System.Exception("table内容为空");
            }
            if (col >= table[0].Count)
            {
                throw new System.Exception("参数错误:col大于最大行");
            }
            for (int i = 0; i < table.Count; ++i)
            {
                if (table[i][col].Equals(str))
                {
                    return i;
                }
            }
            return -1;
        }
    
        public int GetFirstIndexAtRow(string str, int row)
        {
            if (null == table)
            {
                throw new System.Exception("table尚未初始化,请检查是否成功读取");
            }
            if (table.Count == 0)
            {
                throw new System.Exception("table内容为空");
            }
            if (row >= table.Count)
            {
                throw new System.Exception("参数错误:cow大于最大列");
            }
            int tempCount = table[0].Count;
            for (int i = 0; i < tempCount; ++i)
            {
                if (table[row][i].Equals(str))
                {
                    return i;
                }
            }
            return -1;
        }
    
        public int[] GetIndexsAtCol(string str, int col)
        {
            if (null == table)
            {
                throw new System.Exception("table尚未初始化,请检查是否成功读取");
            }
            if (table.Count == 0)
            {
                throw new System.Exception("table内容为空");
            }
            if (col >= table[0].Count)
            {
                throw new System.Exception("参数错误:col大于最大行");
            }
            List<int> tempList = new List<int>();
            for (int i = 0; i < table.Count; ++i)
            {
                if (table[i][col].Equals(str))
                {
                    // 增加
                    tempList.Add(i);
                }
            }
            return tempList.ToArray();
        }
    
        public int[] GetIndexsAtRow(string str, int row)
        {
            if (null == table)
            {
                throw new System.Exception("table尚未初始化,请检查是否成功读取");
            }
            if (table.Count == 0)
            {
                throw new System.Exception("table内容为空");
            }
            if (row >= table.Count)
            {
                throw new System.Exception("参数错误:cow大于最大列");
            }
            int tempCount = table[0].Count;
            List<int> tempList = new List<int>();
            for (int i = 0; i < tempCount; ++i)
            {
                if (table[row][i].Equals(str))
                {
                    tempList.Add(i);
                }
            }
            return tempList.ToArray();
        }
    
        public string GetValueAt(int col, int row)
        {
            if (null == table)
            {
                throw new System.Exception("table尚未初始化,请检查是否成功读取");
            }
            if (table.Count == 0)
            {
                throw new System.Exception("table内容为空");
            }
            if (row >= table.Count)
            {
                throw new System.Exception("参数错误:row大于最大列");
            }
            if (col >= table[0].Count)
            {
                throw new System.Exception("参数错误:col大于最大行");
            }
            return table[row][col];
        }
    
    }
    
    

    多语言实现类

    using UnityEngine;
    using System.Collections.Generic;
    
    public class LTLocalization
    {
    
        public const string LANGUAGE_ENGLISH = "EN";
        public const string LANGUAGE_CHINESE = "CN";
        public const string LANGUAGE_JAPANESE = "JP";
        public const string LANGUAGE_FRENCH = "FR";
        public const string LANGUAGE_GERMAN = "GE";
        public const string LANGUAGE_ITALY = "IT";
        public const string LANGUAGE_KOREA = "KR";
        public const string LANGUAGE_RUSSIA = "RU";
        public const string LANGUAGE_SPANISH = "SP";
    
        private const string KEY_CODE = "KEY";
        private const string FILE_PATH = "LTLocalization/localization";
    
        private SystemLanguage language = SystemLanguage.Chinese;
        private Dictionary<string, string> textData = new Dictionary<string, string>();
    
        private static LTLocalization mInstance;
    
        private LTLocalization()
        {
        }
    
        private static string GetLanguageAB(SystemLanguage language)
        {
            switch (language)
            {
                case SystemLanguage.Afrikaans:
                case SystemLanguage.Arabic:
                case SystemLanguage.Basque:
                case SystemLanguage.Belarusian:
                case SystemLanguage.Bulgarian:
                case SystemLanguage.Catalan:
                    return LANGUAGE_ENGLISH;
                case SystemLanguage.Chinese:
                case SystemLanguage.ChineseTraditional:
                case SystemLanguage.ChineseSimplified:
                    return LANGUAGE_CHINESE;
                case SystemLanguage.Czech:
                case SystemLanguage.Danish:
                case SystemLanguage.Dutch:
                case SystemLanguage.English:
                case SystemLanguage.Estonian:
                case SystemLanguage.Faroese:
                case SystemLanguage.Finnish:
                    return LANGUAGE_ENGLISH;
                case SystemLanguage.French:
                    return LANGUAGE_FRENCH;
                case SystemLanguage.German:
                    return LANGUAGE_GERMAN;
                case SystemLanguage.Greek:
                case SystemLanguage.Hebrew:
                case SystemLanguage.Icelandic:
                case SystemLanguage.Indonesian:
                    return LANGUAGE_ENGLISH;
                case SystemLanguage.Italian:
                    return LANGUAGE_ITALY;
                case SystemLanguage.Japanese:
                    return LANGUAGE_JAPANESE;
                case SystemLanguage.Korean:
                    return LANGUAGE_KOREA;
                case SystemLanguage.Latvian:
                case SystemLanguage.Lithuanian:
                case SystemLanguage.Norwegian:
                case SystemLanguage.Polish:
                case SystemLanguage.Portuguese:
                case SystemLanguage.Romanian:
                    return LANGUAGE_ENGLISH;
                case SystemLanguage.Russian:
                    return LANGUAGE_RUSSIA;
                case SystemLanguage.SerboCroatian:
                case SystemLanguage.Slovak:
                case SystemLanguage.Slovenian:
                    return LANGUAGE_ENGLISH;
                case SystemLanguage.Spanish:
                    return LANGUAGE_SPANISH;
                case SystemLanguage.Swedish:
                case SystemLanguage.Thai:
                case SystemLanguage.Turkish:
                case SystemLanguage.Ukrainian:
                case SystemLanguage.Vietnamese:
                case SystemLanguage.Unknown:
                    return LANGUAGE_ENGLISH;
            }
            return LANGUAGE_CHINESE;
        }
    
        private static string GetWinReadPath(string fileName)
        {
            return Application.dataPath + "/../" + fileName + ".csv";
        }
    
        private static string GetWinSavePath(string fileName)
        {
            return Application.dataPath + "/Resources/LTLocalization/" + fileName + ".txt";
        }
    
        private void ReadData()
        {
    #if UNITY_EDITOR
            // 在Windows平台下读取语言配置文件
            string CSVFilePath = GetWinReadPath(FILE_PATH);
            LTCSVLoader loader = new LTCSVLoader();
            loader.ReadFile(CSVFilePath);
            // 将配置文件序列化为多个语言类
            int csvRow = loader.GetRow();
            int csvCol = loader.GetCol();
            Debug.Log("row:" + csvRow + "col:" + csvCol);
            for (int tempCol = 1; tempCol < csvCol; ++tempCol)
            {
                LTLocalizationData languageData = new LTLocalizationData();
                // 获取第一行数据(语言类型)
                languageData.LanguageType = loader.GetValueAt(tempCol, 0);
                // 遍历生成变量
                languageData.LanguageData = new Dictionary<string, string>();
                for (int tempRow = 1; tempRow < csvRow; ++tempRow)
                {
                    languageData.LanguageData.Add(loader.GetValueAt(0, tempRow), loader.GetValueAt(tempCol, tempRow));
                }
                // 将语言对象序列化存档
                SaveHelper.SaveData(GetWinSavePath(languageData.LanguageType), languageData);
    
                if (GetLanguageAB(language).Equals(languageData.LanguageType))
                {
                    textData = languageData.LanguageData;
                }
            }
    #else
            // 读取对应的语言对象
            TextAsset tempAsset = (TextAsset)Resources.Load("LTLocalization/" + GetLanguageAB(language), typeof(TextAsset));
            if (null == tempAsset)
            {
                tempAsset = (TextAsset)Resources.Load("LTLocalization/" + "EN", typeof(TextAsset));
            }
            if (null == tempAsset)
            {
                Debug.LogError("未检测到语言配置文件");
            }
            else
            {
                string saveData = tempAsset.text;
                LTLocalizationData currentLanguageData = (LTLocalizationData)SaveHelper.ReadData(saveData, typeof(LTLocalizationData), false);
                textData = currentLanguageData.LanguageData;
            }
    #endif
        }
    
        private void SetLanguage(SystemLanguage language)
        {
            this.language = language;
        }
    
        public static void Init()
        {
            mInstance = new LTLocalization();
            mInstance.SetLanguage(Application.systemLanguage);
            mInstance.ReadData();
        }
    
        public static void ManualSetLanguage(SystemLanguage setLanguage)
        {
            if (null == mInstance)
            {
                mInstance = new LTLocalization();
            }
            mInstance.SetLanguage(setLanguage);
            mInstance.ReadData();
        }
    
        public static string GetText(string key)
        {
            if (null == mInstance)
            {
                Init();
            }
            if (mInstance.textData.ContainsKey(key))
            {
                return mInstance.textData[key];
            }
            return "[NoDefine]" + key;
        }
    
    }
    
    

    多语言数据对象

    using UnityEngine;
    using System.Collections.Generic;
    
    public class LTLocalizationData
    {
    
        public string LanguageType;
    
        public Dictionary<string, string> LanguageData;
    
        public override string ToString()
        {
            string result = "LanguageType:" + LanguageType;
            List<string> tempKeys = new List<string>(LanguageData.Keys);
            for (int i = 0; i < tempKeys.Count; ++i)
            {
                result += "
    Key:[" + tempKeys[i] + "]|Value:[" + LanguageData[tempKeys[i]] + "]";
            }
            return result;
        }
    
    }
    
    

    总结

    感觉比上一个版本好了很多
    1.不用考虑csv编码的问题了
    2.反序列化速度比读取csv更快
    3.加了密更可靠
    4.不同语言分开读取,占用内存更小
    缺点嘛,暂时觉得还不错~~继续先用着,有问题再改

  • 相关阅读:
    DevExpress9.3 汉化(winform)
    关于XtraGrid的CustomUnboundColumnData事件的触发条件 (收藏)
    解决DBConCurrencyException并发冲突异常(收藏)
    Devexpress控件使用总结版本9.3
    DBConcurrencyException 极端解决方案 (收藏)
    S8500 与电脑端无法正常连接
    Devexpress 10.1.6 源代码重新编译成功(DXperience 10.1.6 重新编译)附所有需要用到的资源下载地址 (收藏)
    BugTracker
    DevExpress控件学习XtraGrid控件
    LINQ:创建IQueryable Provider<1>
  • 原文地址:https://www.cnblogs.com/coldcode/p/5243062.html
Copyright © 2020-2023  润新知