简介TNT Unicode Controls 2.3.0 是一套专为 Delphi 开发环境设计的 VCL 控件集用于弥补 VCL 在 Unicode 字符处理上的不足帮助开发者轻松构建支持中文、日文、韩文等多语言的国际化应用程序。资源共包含 112 个文件压缩包约 252KB其中既有 pas 源代码文件、res 资源文件、dcr 组件图标也包含 dpk/cfg/bdsproj 等工程与配置类文件包内目录按 Delphi、Example、CBuilder 划分方便不同工具链用户直接取用。Source 目录提供了完整源码便于深入理解控件机制或二次定制Design 目录存放设计期资源配合 Reset Tnt Palette.reg 可调整 IDE 元件面板与组件配色License.rtf 与 Readme.txt 则说明授权条款和基本安装指南。Example 目录中的示例代码能帮助快速上手各类工程文件与配置项也让控件的编译、安装和调试过程更加清晰整体结构清晰兼顾学习与实用。目前已有 1252 人学习下载适合需要增强 Delphi 程序 Unicode 支持、或希望从源码层面掌握控件实现的中高级开发者使用。1. TNT Unicode Controls 2.3.0老 Delphi 工程在 Unicode 时代的后悔药这套控件库发布到现在已经过去了很多年放在今天的内存和编辑器环境里看起来有点跟不上时代。但如果你手上压着一个十年前用旧版 Delphi 写的老工程里面还有一堆中文界面、UTF-8 文本和数据库读出来的多语言数据2.3.0 反而比任何新框架都更值得一试。TNT Unicode Controls 是一套把常见 VCL 控件替换成宽字符版本的组件包2.3.0 是其中一个广泛使用的稳定发布版。装上之后TEdit 变成 TTntEditTMemo 变成 TTntMemo底层全部走 WideString老代码里那些乱码、截断和字符越界问题多数能直接消失而不是靠补丁式的转码来凑合。这篇文章写给两类人一是接手历史工程、被中文乱码和运行时数组越界折腾的开发者二是在纠结要不要把老界面整体重写一遍的项目负责人。读完你能知道这套东西到底能干什么、怎么装、替换时哪些坑必须绕开以及验证结果要走到哪一步才算真正落地。2. 为什么是 2.3.0这套控件库的原理、适用边界与安装落地2.1 先看原理TNT Unicode 控件与原生 VCL 控件的本质差异TNT 系列控件和原生 VCL 控件放在一起对比最核心的差异不是控件名多了一个 TTnt 前缀而是底层的字符数据类型完全不同。在 Delphi 7 及更早的 IDE 里原生 VCL 的 String 默认是 AnsiString按单字节或 MBCS 编码处理字符。TTntEdit 这类组件把内部属性全部改成 WideString也就是 UTF-16 编码的宽字符。文本框的容量、光标移动、选中范围、字符串比较全部按宽字符语义执行。字符计数的差别很直观同样是“”三个全角字母AnsiString 在中文系统上占 6 个字节WideString 只占 3 个字符单元。这一点直接影响后面所有字符串处理代码。TTnt 控件处理 Windows 消息时也有意绕开了一批 ANSI 版本的消息入口。常见做法是直接处理 WM_SETTEXT 和 WM_GETTEXT 的宽字符版本同时把控件内部用于绘制文本的 API 调用切到宽字符函数。所以界面上显示中文本就不需要再关心代码页的临时切换也不容易出现某个对话框按钮文字变成问号的情况。还有个容易被忽略的细节TNT 系列连公共对话框组件也做了宽字符适配。工程里如果用了 TTntOpenDialog、TTntSaveDialog文件对话框里的文件名和目录路径也会按宽字符处理选一个带中文名的文件时返回的 FileName 属性不会再被截断或乱码。从版本策略上讲2.3.0 之后的 TNT 控件更新节奏明显放缓。因为新版 Delphi 从 2009 开始原生把 String 默认类型改成了 UnicodeStringIDE 自带的 VCL 控件就天然支持宽字符。所以 2.3.0 的适用边界很清楚它服务于那些不能直接升级 IDE、必须继续留在旧版 Delphi 上的历史工程。如果你的项目已经在新版 IDE 上就不需要再引入这套控件。2.2 规划安装路径2.3.0 支持哪些 IDE 版本与怎么装安装之前先想清楚两个问题当前工程用的是哪个 IDE 版本以及工程里是否已经引入了其他第三方界面库。TNT Unicode Controls 2.3.0 的常见使用场景是 Delphi 7也有部分工程在 Delphi 2007 上跑。这两类环境的包编译方式略有差别下面把安装路径拆开讲。第一步是准备组件包源文件。把整个控件库目录放到一个独立路径比如D:\Components\TNT不要直接扔到 IDE 的安装目录里。原因很简单IDE 升级或重装时第三方组件目录如果混在系统目录里容易被一起清理掉而且 IDE 的 Library 路径也不好区分哪些是自己加的。第二步是在 IDE 中打开组件包源文件。Delphi 7 下常见的做法是打开TNT7.dpkDelphi 2007 下则对应TNT2007.dpk。打开后先确认一下包的项目选项Output Dir 和 DCP 输出目录不要指向临时目录统一放到D:\Components\TNT\output这类固定位置方便后续排查。然后用 IDE 的 Compile 按钮先编译一次再点 Install 按钮安装到 IDE 中。如果是命令行编译可以这样做dcc32 -B -DDELPHI7 -ID:\Components\TNT -UND:\Components\TNT\output TNT7.dpk参数含义说明-B表示重新编译整个包适合第一次安装或代码有改动后强制重建。-DDELPHI7是条件编译符号TNT 源码里用它来切换不同 IDE 版本下的兼容代码。-I指定 include 路径这里指向 TNT 源码根目录。-UN指定单元输出目录编译好的 DCU 文件统一放到一个文件夹里。编译时如果报“找不到某个 .dcu 文件”先不要急着改代码优先检查 IDE 的 Library 路径里是否包含了TNT\source目录。九成以上的情况是路径没加全而不是源码有问题。第三步是配置 IDE 的 Library 路径。打开 IDE 的 Environment Options在 Library 标签页里把D:\Components\TNT\source和D:\Components\TNT\output加进去。加完路径后用 IDE 的 Tools - Build 或直接新建工程来验证。2.3 验证安装是否成功按两个步骤确认控件已可用安装完成后不要立刻打开老工程做替换。先新建一个空工程按照下面的步骤验证控件能正常拖放、编译、运行。第一步在组件面板上找到 TNT 相关的组件页。安装成功的话面板上会多出一组以 TTnt 开头的控件包括 TTntEdit、TTntMemo、TTntComboBox、TTntButton、TTntLabel 等。如果找不到这个页面说明包没有安装成功回到上一步检查 Install 操作是否真正执行完。第二步在窗体上拖一个 TTntEdit 和 TTntMemo写入以下代码运行看看procedure TForm1.FormCreate(Sender: TObject); var s: WideString; begin s : 日本語テスト 中文测试; TTntEdit1.Text : s; TTntMemo1.Lines.Add(s); TTntMemo1.Lines.Add(IntToStr(Length(s))); end;逻辑说明这段代码把一个同时包含日文和中文的字符串赋给 TTntEdit 和 TTntMemo。Length(s)在 WideString 上返回的是字符数量不是字节数这里能直观看出字符串长度计算是否正常。运行后如果编辑框和备忘录都完整显示这些字符长度显示为 10说明控件的宽字符路径已经正常工作。如果显示问号或长度变成 20说明安装的组件没有真正启用宽字符处理需要回到 2.2 确认条件编译符号是否设置正确。验证到这一步控件本身已经可用。接下来才进入老工程的实际替换环节这也是工作量最大的部分。3. 把工程切到 TNT 控件TEdit 到 TTntEdit 的三个核心替换动作3.1 从 TEdit 到 TTntEdit三步完成替换与属性继承老工程里的输入框替换不是简单地改一下控件类名就完事。TNT 控件在设计上尽量兼容原生 VCL 的属性名和事件签名但代码里如果直接使用了 AnsiString 类型的中间变量替换后编译会立刻报错。我一般按三步走每一步都有明确的验证点。第一步替换窗体上的控件类型。打开 .dfm 文件把TEdit改成TTntEdit同时把对象类型声明同步改掉。如果窗体上的控件是代码动态创建的就把TEdit.Create改成TTntEdit.Create。第二步替换引用该控件的变量类型。代码里所有var Edit1: TEdit;这样的声明要改成var Edit1: TTntEdit;。这一步最容易被遗漏的是函数参数和局部变量比如function GetInputValue(AEdit: TEdit): string; var tmp: string; begin tmp : AEdit.Text; Result : tmp; end;替换后应当改成function GetInputValue(AEdit: TTntEdit): string; var tmp: WideString; begin tmp : AEdit.Text; Result : UTF8Encode(tmp); end;逻辑说明AEdit.Text现在返回的是 WideString 类型赋值给 AnsiString 时会触发隐式转换但 D7 里这种隐式转换经常产生乱码所以显式用UTF8Encode转成 UTF-8 编码的 AnsiString。Result是 AnsiString在 D7 下默认编码是本地代码页。如果调用方最终要把这个字符串显示在 TTntEdit 上Result还得再转回 WideString。代码改完后运行前先检查工程里有没有对 Text 属性做指针操作的代码比如PChar(Edit1.Text)或StrCopy(PChar(...), ...)。这类代码在 AnsiString 时代很常见替换成 WideString 后 PChar 指向的是 UTF-16 数据直接当 AnsiString 用会截断或产生错误数据必须改成PWideChar。第三步处理 .dfm 文件里的文本资源。窗体上原本写死在属性里的 Caption、Text 等字符串编辑器保存时按 ANSI 编码存储。替换控件类型后IDE 会尝试按宽字符格式读取这些字符串。如果 .dfm 里的中文是用旧代码页保存的打开窗体时可能出现乱码提示。常见做法是先把 .dfm 文件另存为 UTF-8 编码再让 IDE 重新加载。这一步操作要小心IDE 对 .dfm 编码格式的识别不一致必须先备份原文件。3.2 处理 TTntComboBox 和 TTntListBox 这类易漏控件输入框和备忘录是替换的重点但很多工程里实际踩坑的是下拉框和列表框。TComboBox 和 TListBox 的 Items 属性在原生 VCL 中是一个 TStringList每一行都是 AnsiString。工程里如果从外部文本文件加载选项或者把数据库字段直接赋给 Items替换成 TTntComboBox 后数据来源也必须切换成宽字符。一个典型的替换动作是把外部 UTF-8 文本文件的下拉选项载入 TTntComboBoxprocedure LoadComboItems(ACombo: TTntComboBox; const AFileName: string); var List: TTntStringList; i: Integer; begin List : TTntStringList.Create; try if FileExists(AFileName) then begin List.LoadFromFile(AFileName, TEncoding.UTF8); ACombo.Items.Assign(List); end; finally List.Free; end; end;逻辑说明TTntStringList是 TNT 控件库提供的宽字符版本字符串列表行数据以 WideString 保存。LoadFromFile的第二个参数TEncoding.UTF8指定按 UTF-8 解码文件内容。不同 TNT 版本对第二个参数的支持有差异如果你的版本不支持可以先手动读文件再用UTF8Decode转换后逐行赋值。参数说明AFileName传完整路径避免相对路径导致的工作目录漂移问题。加载完成后用Assign赋给 ComboBox 的 Items避免循环赋值产生的多余事件触发。替换完成后列表项里的中文在界面上会正常显示但代码里凡是直接取Items[i]的地方现在拿到的都是 WideString。如果后续逻辑要把选项值传给数据库查询记得在查询语句构造前显式转成 UTF-8 或按数据库接口要求的编码类型。3.3 批量搜索确认找出所有漏掉的旧控件声明一次替换几百个控件的工程靠肉眼检查不现实。我的习惯是分两层做批量确认第一层搜控件类型声明第二层搜 Text 属性的赋值点。第一层用 IDE 的搜索功能搜索目标设为整个工程目录查找字符串TEdit替换完成后再搜索这些字符串确认剩余数量TTntEdit凡是还剩下TEdit且不在 TNT 源码目录里的代码都是没有替换到的位置。这里要注意搜索结果里可能有注释里提到的TEdit那不是漏网之鱼需要逐个人工确认。第二层用文件搜索工具扫描所有.pas文件里对.Text属性的赋值grep -rn \.Text *: --include*.pas D:\Project\Source这个命令的作用是列出工程里所有对 Text 属性赋值的代码行。拿到结果后逐条人工判断赋值语句的左侧如果是 TTnt 控件变量右侧的字符串类型必须是 WideString 或宽字符常量右侧是 AnsiString 变量的需要补显式转换。批量替换做完编译通过只是第一步。把工程跑起来把涉及中文输入、中文显示的窗口一个个点开肉眼确认没有乱码才能算这一步真正完成。4. 字符串、列表与编码Unicode 控件背后的数据类型切换4.1 TTntStringList 与 TStringList 在索引和排序上的差异界面控件替换到 TTnt 之后紧跟着就不得不处理字符串列表的问题。因为 TTntEdit 的 Text 是 WideString而原生TStringList的每一行都是 AnsiString两者混用时就到了转换地狱。TNT 控件库自带TTntStringList这是一个以 WideString 存行的字符串列表和 TTnt 控件配合最顺畅。TTntStringList和TStringList的差异不只是类型名。字符串比较时TStringList在 D7 下按字节序比较TTntStringList按 UTF-16 字符序比较。一个直接后果是排序结果可能不一样中文在字节序下按区位码排在字符序下按 Unicode 码点排。如果工程里的下拉列表排序是用户能感知的替换后排序顺序变了需要评估是否符合业务预期。索引行为也有边界。TTntStringList 的索引仍然是整数下标但字符串长度取的是字符数。以下代码展示了这个差异procedure ShowListInfo(const AList: TTntStringList); var i: Integer; begin for i : 0 to AList.Count - 1 do begin // 这里显示的是字符数不是字节数 OutputDebugString(PChar(Line IntToStr(i) Length IntToStr(Length(AList[i])))); end; end;逻辑说明AList[i]返回 WideStringLength返回字符数量。如果调用方原本是按“每行 40 字节”做的截断或格式化替换后必须重算阈值通常需要把单字符按 2 字节预算中文按 2 字符算。4.2 宽字符与 ANSI 的互转统一用 UTF-8 作为中间格式在一个工程里强制把所有字符串都改成 WideString 不现实。第三方 DLL 接口、数据库驱动、老的自绘代码都在等 AnsiString 输入。那么工程里就需要一条统一的转换策略字符串在进入 TTnt 控件前转成 WideString从 TTnt 控件取出后统一编码成 UTF-8 再进入其他模块。我一般会在工程里封装两个全局函数统一入口function AppAnsiToWide(const AValue: AnsiString): WideString; begin Result : UTF8Decode(AValue); end; function AppWideToAnsi(const AValue: WideString): AnsiString; begin Result : UTF8Encode(AValue); end;参数说明UTF8Decode把 UTF-8 编码的 AnsiString 转成 WideString。这里要求传入的 AnsiString 必须确实是 UTF-8 数据否则转换后会出现替换字符。UTF8Encode把 WideString 转成 UTF-8 编码的 AnsiString结果字符串中每个中文字符占 3 个字节。注意这两个函数的命名是刻意带App前缀的。目的是让代码审查的人一眼看出这是工程级约定而不是 Delphi 标准库函数。有了统一入口后老代码里散落的WideString(...)强制转换可以逐步替换掉。4.3 文件读写与代码页避免写入文件后再次乱码界面显示正常并不代表数据流全程健康。很多工程在文件读写这一步把中文再次变成问号原因出在默认编码不一致上。TTntMemo 里能正常显示的中文用原生TFileStream.WriteBuffer写文件时默认按单字节写出UTF-16 的高字节被丢弃文件里只剩下一堆不完整编码。正确做法是写文件前明确指定编码。下面的代码展示了如何把 TTntMemo 的内容按 UTF-8 写入文件并写入 BOMprocedure SaveMemoAsUtf8(const AMemo: TTntMemo; const AFileName: string); var fs: TFileStream; bom: TBytes; utf8Bytes: TBytes; s: WideString; begin s : AMemo.Text; utf8Bytes : TEncoding.UTF8.GetBytes(s); bom : TEncoding.UTF8.GetPreamble(); fs : TFileStream.Create(AFileName, fmCreate); try if Length(bom) 0 then fs.WriteBuffer(bom[0], Length(bom)); if Length(utf8Bytes) 0 then fs.WriteBuffer(utf8Bytes[0], Length(utf8Bytes)); finally fs.Free; end; end;逻辑说明先用TEncoding.UTF8.GetBytes把 WideString 转成 UTF-8 字节数组。GetPreamble获取 UTF-8 的 BOM字节序标记内容是 EF BB BF。写文件时先写 BOM 再写正文。带 BOM 的 UTF-8 文件在记事本和大部分编辑器里能正确识别编码。参数说明fmCreate表示如果文件存在会直接覆盖。如果需要追加内容改成fmOpenWrite or fmShareDenyWrite并先移动文件指针到末尾。如果工程里的其他模块读文件时不期望有 BOM可以在写文件时去掉GetPreamble那段逻辑。但取舍要清晰不带 BOM 的文件读取时必须有人显式按 UTF-8 解码否则在中文 Windows 上容易被默认按 ANSI 读。读文件时也要对称处理。用TEncoding.UTF8.GetString把字节数组还原成 WideString再赋给 TTntMemo.Text。不要在 TTntMemo 和文件之间直接使用Lines.LoadFromFile因为原生 VCL 的LoadFromFile在老版本里没有编码参数会按系统代码页读取。5. 避坑手册TNT Unicode Controls 2.3.0 的 5 个高频翻车点5.1 安装后组件面板空白找不到 TNT 组件页现象按照安装步骤操作后组件面板上多了一个页面但页面里没有控件图标或者整个 TNT 页面根本没出现。原因组件包编译成功了但安装过程没有真正把控件注册到 IDE 的组件列表里。常见诱因是RegisterComponents过程没有执行或者组件包源文件里条件编译符号不对导致注册代码被跳过了。解决重新打开 .dpk 文件确认requires列表里依赖的包都已存在。然后用 IDE 的 Install 按钮重新安装注意观察 IDE 状态栏是否有“Package installed successfully”的提示。如果装完还是没有图标打开组件包源码搜索RegisterComponents确认注册的组件类名和单元名一致。5.2 替换 TComboBox 后下拉框中文全部变成问号现象窗体上原来的 TComboBox 改成 TTntComboBox 后编译运行下拉列表里的中文选项变成了若干个问号但编辑框和标签上的中文正常。原因下拉框的 Items 是在窗体设计时写入 .dfm 文件的。替换控件类型后IDE 读取 .dfm 里的字符串时按 ANSI 解码但 .dfm 文件本身的存储编码还是旧格式重新保存时发生了二次编码。编辑框的 Text 是运行时赋值走的是新的宽字符路径所以没出问题。解决把 .dfm 文件备份后用文本编辑器打开将文件编码转换成 UTF-8再让 IDE 重新加载窗体。加载后手动检查每个 TTntComboBox 的 Items 预览确认中文正常后再保存。不要在这个问题上依赖版本控制工具的自动转换它会自作主张。5.3 编译报错 “Incompatible types: AnsiString and WideString”现象替换 TTntEdit 后代码里原本合法的赋值语句全部编译失败报错指向 Text 属性赋值和 OnChange 事件里的字符串变量。原因TTntEdit.Text 是 WideString赋值给 AnsiString 变量需要隐式转换但 D7 在这种场景下默认不允许隐式转。旧代码里大量存在s : Edit1.Text这样的写法Edit1 改成 TTntEdit 后左侧变量类型跟不上了。解决分两类处理。第一类左侧变量本应声明为 WideString 的直接改声明第二类左侧变量必须保持 AnsiString 的比如要传给某个第三方 DLL主动用UTF8Encode显式转换。不建议图省事把所有 AnsiString 都改成 WideString这会波及业务逻辑里大量依赖单字节语义的代码。5.4 TTntMemo 显示正常但保存成文件后再次打开是乱码现象TTntMemo 里中文完全正常写入文件后打开也正常但工程重启后再读入这个文件显示成乱码。或者写出的文件在另一台电脑上打开乱码。原因写入文件时没有固定编码。Write 出来的字节按当前系统代码页生成另一台机器的代码页不同读出来自然对不上。也可能是写文件时用了原生 TStringListTTntMemo 的 WideString 内容赋给 TStringList 时被截断或转换错了编码。解决统一使用带编码控制的读写函数写入用TEncoding.UTF8读取也用同一个。文件里加上 BOM 能减少不同环境下的识别歧义。工程内所有读写 TTntMemo 内容的路径统一收敛到 4.3 里封装的那个函数不要散落多处各自写。5.5 Font.Charset 设置成 GB2312 后个别汉字显示成方框现象TTntLabel 和 TTntButton 的 Font.Charset 在旧工程里被设置为 DEFAULT_CHARSET 以外的值比如 GB2312_CHARSET。替换成 TNT 控件后界面上一部分汉字变成方框但另一部分正常。原因Charset 属性本身是给 ANSI 时代的字体映射用的。TTnt 控件绘制宽字符文本时字体选择逻辑走的是 Unicode 字体匹配强行指定 GB2312 反而限制了字体回退。不同字符比如生僻字和日文假名需要不同字体回退固定 Charset 会让字体无法切换。解决把 TNT 控件相关字体的 Charset 统一改为DEFAULT_CHARSET。Font.Name 保留原先的中文字体名比如宋体或微软雅黑。如果改动后界面字体和设计稿有轻微差异调整 Font.Size 补偿而不是重新指定 Charset。这个问题的排查线索通常指向“显示不规律”——如果你发现方框只在某些字符上出现优先检查 Charset 而不是字体名称。6. 进阶验证用文件比对和字节审查确认 Unicode 替换到位组件替换完成后光靠界面肉眼扫一遍不够扎实。最后一步我会做两件额外确认花不了多长时间但能避免上线后才发现数据流里藏着编码坑。第一件是字节级文件审查。用一个固定的中文测试串比如“中文Unicode测试123”分别经过 TTntEdit、TTntMemo、TTntComboBox 三条路径写入三个 UTF-8 文件。然后用十六进制编辑器打开文件检查中文字符的字节是否按 UTF-8 规则编码。以“中”字为例UTF-8 编码是 E4 B8 AD如果文件里出现的字节是 D6 D0说明写入时被转成了 GBK。这个检查能直接定位是哪一段代码把 WideString 路线改回了 ANSI 路线。第二件是字符串内存宽度检查。在一个调试窗口里临时加一段代码读取字符串变量的元素大小var ws: WideString; an: AnsiString; begin ws : 测试; an : 测试; ShowMessage(IntToStr(SizeOf(ws[1]))); // 正常应显示 2 ShowMessage(IntToStr(SizeOf(an[1]))); // 正常应显示 1 end;逻辑说明SizeOf(ws[1])返回 2 说明 WideString 的每个字符占 2 字节即 UTF-16 语义生效。SizeOf(an[1])返回 1 说明 AnsiString 还是单字节语义。工程里凡是同时出现这两类赋值的地方都要检查是否有隐式转换。用这个技巧能快速扫出一批“名义上是 Unicode 控件但数据还在走 ANSI 处理”的代码。常见例子是把 TTntEdit.Text 取出来赋给一个 AnsiString 变量后参与拼接再传回另一个 TTntEdit。我最近一次处理老工程时保留了一个对照用的小工程里面只有两个 TTntEdit 和一个按钮专门用来验证新写的字符串处理函数有没有破坏宽字符语义。每次改动编码相关代码先在这个小工程里跑一遍UTF8Encode / UTF8Decode的往返测试再回到大工程里替换。这套习惯帮我挡住了不少临时抱佛脚的麻烦。2.3.0 版本确实老但它解决的是历史工程里最真实的一类痛点界面能显示、文件不乱码、数据不截断。如果你手上正好有这类工程按上面这几步走替换完再跑一轮字节级验证这个方向就值得投入。希望帮到你。本文还有配套的精品资源点击获取