简介这份rar压缩包收录了Delphi 7时代广受欢迎的TNT Controls 2.3版控件库面向仍在使用Delphi 7或维护老项目的开发者解决原生控件对Unicode支持不足、多语言界面处理困难的问题。包内共159个文件包含46个Pascal源码、45个编译好的DCU单元、12个RES资源文件及11个DCR位图资源另有工程文件、配置文件与示例文档整体仅835KB便于快速分发与部署。目前已有520人学习下载。解压后可通过组件面板安装在设计期拖拽使用也可直接查看源码学习控件实现细节。这套控件以TNTGrid、TNTTreeView等增强组件著称提供丰富的属性、事件与高度可定制能力适合需要深度自定义界面、处理多语言字符集或移植旧版Unicode项目的开发者使用。1. TNT Controls 是什么一份 tnt2.3.rar让 Delphi 7 跳出 ANSI 的围城Delphi 7 的编译器层面只有一个 string 概念默认是 AnsiString一字节一字符。中文字符集 GBK 下一个汉字占两个字节看似处理中文没问题但只要你把数据换到 UTF-8 或 Unicode比如从网页 API 拿回来的文本、跨平台配置文件立刻乱码满天飞。TNT Controls 是 Delphi 5/6/7 时代用得最广的 Unicode 补丁套件tnt2.3.rar 解压出来的就是针对 Delphi 7 的版本。它把 VCL 里常用的 Edit、Memo、TreeView、ListView 甚至 Form、StringList 都做成以 WideString 存储的 TTnt 系列控件让老项目能够在不大改业务逻辑的前提下处理 Unicode。适合哪些人维护 Delphi 7 老旧系统的工程师、想把老程序的中文界面升级到 UTF-8 数据源的团队。我接手的台账管理工具就是用这套迁移思路从 GBK 平滑挪到 UTF-8一千多行业务代码只动了控件类型和几个字符串函数。2. 安装 TNT 2.3 到 Delphi 7解压、编译、上组件面板的完整流程2.1 解压后先确认包文件别双击就装拿到 tnt2.3.rar 之后先别急着解压安装。TNT 的压缩包结构一般分 Source 和 Packages 两个目录Packages 下面是各版本 Delphi 的工程文件。先解压到一个不含中文、不含空格的路径比如 D:\Components\TNT。路径里有中文会让编译器的 Include 和 Library 搜索失效这是很多安装失败的根源。用下面的命令确认包文件齐全# 在 Windows 命令提示符下操作先切到解压目录 cd /d D:\Components\TNT # 列出所有 dpk 工程文件确认存在 D7 对应的包 dir /s /b *.dpk正常应该看到若干 dpk 文件命名里通常带 D7、D6、D5 等版本标记。我们要用的是带 D7 的那两个一个对应运行期包另一个对应设计期包。常见命名是 RunTnt_D7.dpk 和 DclTnt_D7.dpk。如果你的压缩包里命名不同按功能区分运行期包只是运行时使用的代码设计期包负责把控件注册到组件面板二者缺一不可且必须先编译运行期包再编译设计期包。提示解压路径里有中文或空格是 TNT 安装失败的高发原因。我见过同事把包解压到“D:\新建文件夹\TNT 控件”结果怎么编译都报找不到源文件换到纯英文路径后一次通过。2.2 编译顺序先 Run 包再 Dcl 包打开 Delphi 7 主界面菜单 File Open 打开运行期包。项目中只有一个 dpk 文件在 Project Manager 窗口里右键该工程选择 Compile。命令行提示区出现编译成功的信息后再打开设计期包同样的右键 Compile 后右键选择 Install。安装成功后 Delphi 会提示 “Package installed”组件面板上会出现新页面。这一步其实也可以这样操作# 1) 打开 File Open选择 D:\Components\TNT\Packages\RunTnt_D7.dpk # 2) 在项目管理器右键工程选 Compile等待编译完成 # 3) 再打开 DclTnt_D7.dpk右键 Compile编译完成后右键 Install编译期间如果报错找不到某个文件通常是 TNT 源码路径没有加入 Delphi 的 Library 搜索路径。菜单 Tools Environment Options Library Library Path把 D:\Components\TNT\Source 加进去新增的路径放在最前面。报找不到 dcldesign.dcp 时说明设计期包没找到 Delphi 自带的包把 $(DELPHI)\Lib 加进 Library Path 就能解决。编译成功的标志不是“没有弹窗”而是消息框里出现 [Compiling] 和 [Done] 两行。只要出现 [Error] 就要停下来看具体文件常见错是多个包同时定义了同名单元——这时候检查是不是之前装过别的 TNT 版本。我的经验是把旧的 TNT 相关 bpl、dcp 全部从 Delphi 的 BPL 目录清掉再装能省下大量排查时间。2.3 安装成功后的验证与工程选项调整重启 Delphi 7不是必须但强烈建议组件面板上会多出一页名为 TNT 的页面里面除了 TntEdit、TntMemo 这些常用可视控件还有 TntClasses、TntSystem 等非可视单元。这些非可视单元不会出现在面板上但需要在代码里 uses 引用。如果你的面板上只有零星几个控件说明设计期包安装不完整回到 2.2 重来。工程要做的第一处配置把工程的输出目录和 DCU 目录设到独立目录避免运行时找不到 bpl。我一般会在工程目录下建一个 bpl 文件夹把编译好的 TNT 相关 bpl 拷贝进去然后把工程的 Search Path 设置成 TNT 源码路径。这样一来新工程引用 TNT 单元时 IDE 能自动提示。要特别说明的是用 TNT 2.3 的工程编译出的 exe 会比原来大几十 KB那是 WideString 处理代码和运行时包的代价不是异常。这里顺便对比一下选型TNT 2.3 不是唯一选择。市面上还有商业版的 TMS Unicode Component Pack以及从 Delphi 2009 开始 VCL 原生支持 Unicode。但很多老项目绑定第三方组件包升编译器成本远大于装一套 TNT 控件。TNT 2.3 的价值在于只换一层控件不动编译器和第三方库改动面最小。这也是它被长期保留在无数 Delphi 7 项目里的原因。2.4 只有运行期包怎么办纯代码项目也能用 TNT有些精简版的压缩包里没有设计期包只有运行期源文件。这并不意味着方案不可用如果项目里所有界面控件都是动态创建的不需要设计期拖拽那么只要在工程 Search Path 里指向 TNT 源码目录代码里直接 uses TntStdCtrls、TntComCtrls一样能创建 TTntEdit 并正常运行。缺点是组件面板上看不到 TNT 页设计期预览也只能靠代码写死位置。这种模式适合工具类程序不适合窗体繁多的业务系统。# 只使用运行时单元时的最小配置 # 工程 Search Path 添加 TNT 源码目录 # 引用 TntStdCtrls、TntComCtrls、TntClasses 三个核心单元即可3. 把 TEdit 换成 TTntEdit控件替换映射与最小改动方案3.1 映射表从标准 VCL 到 TNT 的对应关系TNT 控件的设计哲学很清楚类名都是在标准控件前面加一个 Tnt。参数、属性、事件名几乎一致少数属性从 string 变成 WideString。常用映射见下表标准 VCL 控件TNT 等价控件所在单元文本属性TEditTTntEditTntStdCtrlsText: WideStringTMemoTTntMemoTntStdCtrlsLines: TntStringsTButtonTTntButtonTntStdCtrlsCaption: WideStringTBitBtnTTntBitBtnTntButtonsCaption: WideStringTLabelTTntLabelTntStdCtrlsCaption: WideStringTListBoxTTntListBoxTntStdCtrlsItems: TntStringsTComboBoxTTntComboBoxTntStdCtrlsItems: TntStringsTListViewTTntListViewTntComCtrlsColumns/Items 均 WideStringTTreeViewTTntTreeViewTntComCtrlsItems: TntTreeNodesTPageControlTTntPageControlTntComCtrlsTabSheet.Caption: WideStringTStatusBarTTntStatusBarTntComCtrlsPanels 文本 WideStringTFormTTntFormTntFormsCaption: WideStringTStringListTTntStringListTntClasses字符串项 WideString这个表是迁移时对照用的。原则上替换控件类型后原有的事件处理代码不用改因为事件参数类型保持一致。所谓“保持一致”是指 TTntEdit 的 OnChange 事件里 Sender 依然是 TObjectOnKeyPress 里的 Key 参数依然是 Char不是 WideChar。这是一个容易被忽略的细节它保证了业务代码改动量最小但同时也限制了某些高级 Unicode 处理——比如要接收超过 BMP 的字符依靠 TNT 2.3 是做不到的那是版本上限认命就好。3.2 批量替换 DFM正则替换与三步检查对于已经有几十个窗体的项目手动拖控件重来不现实。常见做法是直接改 dfm 文件。Delphi 7 的 dfm 默认是文本格式可以用支持正则的编辑器一次性替换。先备份整个工程然后在 Notepad 或 VS Code 里对 *.dfm 做如下替换查找模式(^object\s\w): T(Edit|Memo|Button|BitBtn|Label|ListBox|ComboBox|ListView|TreeView|StatusBar|PageControl|TabSheet|Form)\b 替换模式$1: TTnt$2替换之后逐个打开窗体文件。只替换 dfm 还不够还要在 .pas 文件的 uses 子句里补 TntStdCtrls、TntComCtrls、TntForms、TntClasses 这些同名可见单元。很多人在这步踩坑dfm 里已经写了 TTntEdit但 pas 没有 uses TntStdCtrlsDelphi 打开窗体直接报 “Class TTntEdit not found”。避免办法是替换完成后用文本搜索检查所有 .pas 文件里是否引用了 Tnt 系列单元没有就统一补上。# 在项目根目录里检查哪些 pas 引用了 TEdit 但没引用 TntStdCtrls grep -rl TEdit --include*.pas . | xargs grep -L TntStdCtrls这一步能很直观地暴露遗漏。注意如果在 dfm 中把 TForm 也替换成了 TTntForm那么所有继承自此窗体的子类都要确保 uses TntForms否则编译器报“找不到 TTntForm”。因此我的建议是第一批只替换数据输入类控件Edit、Memo、ComboBox、ListBox、TreeView、ListViewForm 放到第二批单独处理便于定位问题。注意替换 dfm 前一定先备份。我见过有人把 TEdit 替换成 TTntEdit 后又全局替换了一次结果变成 TTTntEdit整个窗体全废只能靠备份回滚。3.3 动态创建控件与运行时类型判断界面控件的替换相对简单动态创建控件稍微麻烦一点。比如在某处代码里 TEdit.Create(Self)前后创建逻辑没变只要把类型换成 TTntEdit 即可。难点在于运行时用 is 判断类型的地方比如遍历窗体给所有 TEdit 设置只读属性。这种代码在替换后需要同步把 is TEdit 改成 is TTntEdit否则逻辑失效一半var i: Integer; Ed: TTntEdit; begin // 遍历窗体上的所有控件把所有 TTntEdit 的只读设为 True for i : 0 to ComponentCount - 1 do begin if Components[i] is TTntEdit then begin Ed : Components[i] as TTntEdit; Ed.ReadOnly : True; end; end; end;这里有个语义上的坑TTntEdit 并不是 TEdit 的子类两者共同祖先只是 TWinControl。因此如果业务代码里有if Components[i] is TEdit的判断替换后条件永远为 False这类判断点必须逐一检查。稳妥做法是在映射表里准备一份“类型判断清单”把所有 is Txxx 都列出来单独过一遍。另一个值得记录的细节是TTntEdit 的 Text 是 WideString 类型ImeName 这些属性存在但行为可能与原生 TEdit 不完全一致。如果项目依赖了输入法切换逻辑迁移后要实测中文输入是否正常。常见表现是输入法没问题但对某些日韩输入法偶发候选框不跟随这属于 TNT 2.3 的固有问题目前没有完美解决方案只能接受。4. 中文不乱码只是起点TNT 的编码转换与文件读写实战4.1 先准备两个编码转换函数UTF-8 与 WideString 互转Unicode 化之后最影响日常开发的其实是宽字符串与普通字符串之间的转换。Delphi 7 没有 TEncodingUTF-8 与 WideString 的互转需要直接调用 Windows API。这两个函数是 TNT 2.3 时代最常用的编码转换工具uses Windows; // 把 UTF-8 字节序列解码为 WideString // S 是 UTF-8 编码的字节串Length(S) 返回字节数 function Utf8ToWideString(const S: UTF8String): WideString; var Len: Integer; begin Len : MultiByteToWideChar(CP_UTF8, 0, PAnsiChar(S), Length(S), nil, 0); SetLength(Result, Len); MultiByteToWideChar(CP_UTF8, 0, PAnsiChar(S), Length(S), PWideChar(Result), Len); end; // 把 WideString 编码为 UTF-8 字节序列 // 返回值是 UTF-8 字节串Length(Result) 返回字节数 function WideStringToUtf8(const W: WideString): UTF8String; var Len: Integer; begin Len : WideCharToMultiByte(CP_UTF8, 0, PWideChar(W), Length(W), nil, 0, nil, nil); SetLength(Result, Len); WideCharToMultiByte(CP_UTF8, 0, PWideChar(W), Length(W), PAnsiChar(Result), Len, nil, nil); end;注意 MultiByteToWideChar 的第二个参数必须是 0如果传了 WC_NO_BEST_FIT_CHARS 这类标志遇到非法字节会直接返回 0 而不是尽量转换。第 7 和第 8 个参数传 nil 和 nil表示不做默认字符替换和标志输出。一旦有非法 UTF-8 字节函数会返回 0调用方需要自己验证 Length(Result) 是否为 0 并决定是报错还是忽略。我通常会在这两个函数外面包一层异常处理碰到损坏数据时记录日志而不是静默返回空串。4.2 用 TTntStringList 和 TTntFileStream 读写 UTF-8 文件Delphi 7 自带的 TStringList 只能按 ANSI 处理文本遇到 UTF-8 文件读进来就是乱码。TNT 提供了 TTntStringList内部每一项都是 WideString而且对带 BOM 的 UTF-8、UTF-16 文件能自动识别编码。很多人第一反应是用它替换 TStringList实测可行但要注意默认行为只对带 BOM 的文件有保证没有 BOM 的 UTF-8 文件会被当成 ANSI 读。写文件时同理如果希望输出 UTF-8建议手动转成字节写入uses TntClasses, Windows; var SL: TTntStringList; W: WideString; Utf8Buf: UTF8String; begin SL : TTntStringList.Create; try // 读取带 BOM 的 UTF-8 文件SL 内部会转成 WideString SL.LoadFromFile(D:\data\config_utf8_bom.txt); W : SL.Text; // 目标文件不带 BOM这里手动转成 UTF-8 字节写入 Utf8Buf : WideStringToUtf8(W); with TTntFileStream.Create(D:\data\out.txt, fmCreate) do begin try Write(Pointer(Utf8Buf)^, Length(Utf8Buf)); finally Free; end; end; finally SL.Free; end; end;这段代码里有两个关键点。第一TTntFileStream 的 fmCreate 表示如果文件存在则覆盖不存在则新建Write 的第二个参数是字节数不是字符数所以用 Length(Utf8Buf) 而不是 Length(W)。第二如果保存时忘了加 BOM某些文本编辑器打开 UTF-8 文件会误判成 ANSI繁体中文系统下显示会乱。解决办法是在文件头部手动写入三个字节的 BOM$EF $BB $BF。写到代码里就是Write(#$EF#$BB#$BF, 3)注意不能写成 Length(W) * SizeOf(WideChar)。4.3 数据库字段与文件名的 Unicode 处理数据库场景里常见的问题分两类。一类是数据库字段本身就是 Unicode 类型比如 SQL Server 的 NVARCHAR通过 ADO 取出来时已经是 WideString赋值给 TTntEdit.Text 没有问题问题反而出在把 WideString 拼进 SQL 语句时。Delphi 7 的 ADO 组件对参数化查询支持不错能用参数就尽量用参数避免把 WideString 拼接进 SQL 文本里被 ANSI 到 Unicode 的隐式转换弄乱。另一类是老库字段是 VARCHAR里面存 UTF-8 字节这种要先取出来按字节流处理再解码。判断字段到底存的是什么编码最简单的方法是直接用 TTntStringList 打开导出的文本文件看结果而不是想当然。文件名方面Windows API 对文件名使用 Unicode但 Delphi 7 的 TFileStream 是 ANSI 版访问含中文的文件名在某些系统区域设置下会失败。TNT 的 TTntFileStream 是 WideString 版本的封装直接使用它能规避大部分问题。真正容易翻车的是遍历目录FindFirstFileW 返回的文件名已经是宽字符后续用 TntFileStream 打开没问题但如果你在中间用隐式转换偷懒很容易引入重复转换的乱码。血泪经验是文件路径这种数据能全程保持 WideString 就绝不要中途转成 AnsiString。5. TNT 2.3 使用避坑五个最容易翻车的故障排查记录5.1 安装设计期包时报 “Cant load package”现象在 IDE 里 Install 设计期包时弹出的错误是 “Cant load package”后面跟着 bpl 文件名。原因这个设计期包依赖的运行期包没有被编译或者运行期包的版本与当前 IDE 不匹配。解决先确保 RunTnt_D7.dpk 编译完成再确认 Dcl 包的 Requires 里引用的运行期包名与编译产物一致。我遇到过一次很隐蔽的情况机器上装了两套 DelphiTNT 源码路径被另一个版本的 .dcu 污染导致编译时优先用了错误的输出文件。解决办法是把 TNT 的 Source 与 Packages 目录分开并检查 Library Path 里是否混入了其他版本的 dcu 目录。5.2 编译通过但窗体上控件显示为空心矩形现象从 TNT 面板拖一个 TTntEdit 到窗体设计时能看到控件轮廓但运行时控件区域空白文本不显示。原因编译时用的是标准 VCL 单元而不是 TNT 的单元。常见原因是工程 uses 里把 Forms 和 TntForms 的顺序写反或者运行时包没有正确加载。解决确认 uses 顺序中 TntForms 在 Forms 之前或者直接删除 uses 里的 Forms让 TNT 单元自动引用。如果用的是运行时包重新编译 Run 包并确认 bpl 已复制到程序目录或系统目录。5.3 TTntTreeView 节点中文显示为问号现象给 TTntTreeView 设置节点文本代码里写中文运行后节点显示 ?。原因赋值语句的左值是 WideString右值是 UTF-8 字节组成的 AnsiString在 Delphi 7 里隐式转换把 UTF-8 字节当 GBK 解码导致字符错乱进而显示为 ?。解决赋值前用 4.1 的 Utf8ToWideString 显式转换。另外检查节点对象的 Text 属性是否真的被 TNT 版本覆盖有部分第三方封装把节点 Text 回退成 string 了这类封装要换掉。5.4 第三方控件与 TNT 混装的边界现象界面里同时用 TComboBox 和 TTntComboBox运行时数据在两者之间拖拽、复制出现乱码。原因标准 VCL 控件内部用 AnsiStringTNT 控件内部是 WideString两者交互必须经过转换一旦有一步用了隐式转换编码假设错了就乱码。解决混装是新老系统过渡期常见的不完美状态尽量用 TTnt 控件统一输入输出通道。特别是剪贴板TNT 控件从剪贴板读文本时用的是宽字符版本 API如果你在中间用标准 VCL 控件读取同样的剪贴板内容可能拿到的就是 ANSI 转换后的结果两者不一致极易出现灵异现象。我的建议是把所有剪贴板读写都收拢到一个公共函数里统一走 WideString。5.5 用正则替换完 DFM 后IDE 打不开窗体现象替换 TEdit 为 TTntEdit 后双击 dfm 或打开窗体IDE 报 “Class TTntEdit not found”。原因dfm 里引用了 TNT 类但对应的 pas 文件 uses 里没加入 TntStdCtrls或者运行时包没安装。解决在报错的 pas 文件 uses 子句补上缺失单元重新编译。如果还报错检查是不是有多个 TNT 副本Delphi 在搜索路径里找到的第一个 TntStdCtrls.pas 不是你要用的那个。这个问题的隐蔽性在于编译信息不会告诉你实际引用了哪个目录的单元只有通过 Build 输出的单元路径才能看到。我的通用排错顺序是先确认运行时包是否干净再确认 uses 子句和 Library Path最后清缓存重启 IDE。TNT 2.3 是十几年前的控件很多“玄学”问题都源于环境叠加而不是控件本身写错了。6. 进阶技巧用统一基类和验证清单收窄迁移面6.1 用 TTntForm 统一基类把字体和遍历逻辑收拢到一起替换完控件后最后一块硬骨头是 TForm。如果所有窗体都继承自同一个基类就把字体统一和控件遍历做成基类方法避免每个窗体各自实现一遍。以下是递归设置字体的写法type TBaseForm class(TTntForm) protected procedure ApplyUnicodeFont(const AWinControl: TWinControl); end; implementation procedure TBaseForm.ApplyUnicodeFont(const AWinControl: TWinControl); var i: Integer; begin for i : 0 to AWinControl.ControlCount - 1 do begin // 给每个子控件设置统一字体防止默认字体在东亚字符下显示为方框 AWinControl.Controls[i].Font.Name : Microsoft YaHei; if AWinControl.Controls[i] is TWinControl then ApplyUnicodeFont(TWinControl(AWinControl.Controls[i])); end; end;调用时从窗体自身开始递归一次即可覆盖所有层级。如果从每个子控件开始再往上遍历会造成大量重复设置运行时开销翻倍。注意 TTntForm 在 FormCreate 时 Handle 尚未创建遍历逻辑放在 OnShow 第一次触发时执行比较稳妥。6.2 终局验证清单按顺序跑一遍再发版迁移完所有控件和基类后按下面五步验证能拦住绝大多数 Unicode 回归。用 TTntStringList 读取一个 UTF-8 带 BOM 的文件显示到 TTntMemo确认无乱码。把 TTntMemo 的内容保存为 UTF-8 无 BOM 的文件再读回来确认内容一致。在 TTntEdit 直接输入中文、日文和韩文确认输入法工作正常。遍历整个窗体确认没有标准 VCL 的文本输入控件残留。用统一基类设置字体后在另一台未安装微软雅黑的机器上测试确认宋体回退正常。这套清单是我在 TNT 迁移项目里固定执行的收尾动作。它不能证明所有编码边界都正确但能在发版前兜住最常见的乱码回归。TNT 2.3 是十几年前的方案你要清醒地知道它的上限超出 BMP 的字符、部分输入法候选框跟随问题它处理不了遇到这类需求别硬嫁接换 Delphi 2009 以上才是正路。但在升级之前它是把 Delphi 7 老项目撑到 Unicode 数据流里最稳的过渡方案。希望这套安装、替换、验证的流程能帮你在 dcu 路径和组件面板之间少踩几个坑。本文还有配套的精品资源点击获取