资讯动态

TNT Unicode Controls 2.3.0:老Delphi工程中文乱码的补救方案

发布时间:2026/10/13 15:12:04 来源:尧图企业网站定制
简介这套针对 Delphi 的 TNT Unicode Controls 2.3.0 控件集主要解决 VCL 在 Unicode 与多语言环境下支持不足的问题适合需要实现国际化界面、处理中文/日文/韩文等字符的 Delphi 与 CBuilder 开发者。压缩包共 112 个文件容量约 252KB以 pas 源文件、dpk/bdsproj/bpg/bpk 工程配置、res/dcr 资源与图标文件为主附带可导入 IDE 的 reg 注册表项、许可协议和 readme 说明Source 目录提供全部源代码Example 目录有实际应用示例Design 目录则便于在 IDE 中可视化配置。已有 1252 人浏览/学习这套控件集。借助它开发者不仅能获得完整 Unicode 支持还能基于开放的源码深入理解实现机制甚至按项目需求二次定制同时兼顾 CBuilder 环境是一份实用且轻量的国际化开发工具包。1. TNT Unicode Controls 2.3.0老 Delphi 工程处理中文乱码的那颗后悔药TNT Unicode Controls 2.3.0 是一套专门给老 DelphiDelphi 5/6/7 那一代补 Unicode 能力的 VCL 控件集。它在 2009 年之前是处理中文、日文、韩文界面的主流选择标准 TEdit、TMemo、TListBox 全部换成 TTntEdit、TTntMemo、TTntListBox 这类同族控件文本从单一的 ANSI string 升级成 WideStringUTF-16乱码和“半个字”问题基本就消失。你手上如果有一套还没升到 Delphi 2009 的遗留系统界面里中文一保存到文件就花、弹窗标题变问号、日文韩文直接丢字符这套组件就是最省事的补法。它解决的是“老 IDE 干不了新活”的问题而不是给你换架构适合正在维护旧代码的开发者、外包接盘老系统的团队以及对多语言界面有硬需求但暂时动不了 IDE 版本的人。2. 为什么 TEdit 会乱码从 ANSI 消息到 UTF-16 窗口TNT 能补位先要看清标准 VCL 控件在 Delphi 7 里到底把文本丢在哪一步。不是某个控件写得差而是整个 IDE 时代的字符串模型就是 ANSI 的到了接触外部文本的时候就撑不住了。2.1 Delphi 7 的 string 是 ANSI中文字符怎么一步步丢的Delphi 7 里string默认等于AnsiStringTEdit.Text的读写都走这一条路径。控件底层调用的是不带 W 后缀的窗口消息比如WM_SETTEXT、WM_GETTEXT数据在内存里按系统当前代码页解释。中文 Windows 默认是 936GBK所以两个中文字在一个 GBK 环境里自己玩不会出事可一旦文本来源变成 UTF-8 文件、从网页剪贴板粘进来的 UTF-16 内容或者数据库驱动给你的是宽字符结果转换就出问题。var s: AnsiString; w: WideString; begin w : 中文测试; // WideString 是 UTF-16 s : w; // 隐式转换超出当前代码页的字符直接变 ? s : AnsiString(w[1]); // 更危险只取低字节含 0x00 时字符串被截断 end;WideString在内存里是两个字节一个字符AnsiString是一个字节一个字符。把w[1]硬转成AnsiString等于只拿了 UTF-16 的低字节如果高字节是 0字符串就在这里被当作结束了。这就是很多老工程里“界面看着正常一保存就只剩半个词”的来源控件本身没问题是字符串类型在边界上被截了。TNT 的做法是让控件从窗口到属性全部使用宽字符避开这条 ANSI 转换链。2.2 TTntEdit 不是换皮CreateWindowExW 与 DefWindowProcW 的真实改动直接继承 TEdit 然后把 Text 改成 WideString 是行不通的因为窗口句柄背后还是 ANSI 窗口类消息一到系统层面就被转掉了。TNT 组件的实现要更底层基类在CreateParams和CreateWnd阶段动手用CreateWindowExW创建真实句柄并把默认窗口过程替换成DefWindowProcW。这样一来控件收到的WM_SETTEXT、WM_GETTEXT天然走宽字符版本缓冲区按WideChar计算中文字符一个都不会少。// 这是 TNT 基类里最常见的两处改写思路比具体代码更重要 procedure TTntControl.CreateParams(var Params: TCreateParams); begin inherited CreateParams(Params); // 使用 Unicode 窗口类字面量而不是系统的 ANSI 类 Params.WinClassName : WideClassName; end; procedure TTntControl.CreateWnd; begin inherited CreateWnd; // 窗口创建后把默认消息处理改成 DefWindowProcW SetWindowLongW(Handle, GWL_WNDPROC, LongInt(DefWindowProcW)); end;这个改动不是把所有消息都接管而是让系统回调函数变成宽字符版本。像TTntLabel这种不需要窗口句柄的控件走的是另一条路绘制时直接调ExtTextOutW把WideString内容画上去。所以 TNT 不是简单做了一层字符串转换包装而是把控件和 Windows 的交互方式整体换成了 Unicode 生态。这也是为什么用了 TNT 之后剪贴板粘贴中文、从 URL 解码出来的宽字符内容都能在控件里正常显示。2.3 2.3.0 单元地图哪些控件值得换、哪些换了白换2.3.0 这套包的单元划分很有规律基本是一比一复刻 VCL 控件族只是类名前面加 Tnt。使用前先摸清地图替换时才知道该 using 谁。单元文件主要成员替换对象TntClasses.pasTTntStrings、TTntStringList、TTntStringStreamTStringList、TStringsTntSysUtils.pasWideUpperCase、WideLowerCase、WideFormat、WideFileExistsSysUtils 里的同名函数TntStdCtrls.pasTTntEdit、TTntMemo、TTntLabel、TTntComboBox、TTntListBox、TTntButton、TTntCheckBoxTEdit、TMemo、TLabel 等标准控件TntButtons.pasTTntBitBtn、TTntSpeedButtonTBitBtn、TSpeedButtonTntComCtrls.pasTTntPageControl、TTntTreeView、TTntListView、TTntStatusBar、TTntToolBarTPageControl、TTreeView 等TntForms.pasTTntFormTFormTntDialogs.pasWideMessageDlg、TTntOpenDialog、TTntSaveDialogMessageDlg、TOpenDialog、TSaveDialogTntMenus.pasTTntMainMenu、TTntPopupMenu、TTntMenuItemTMainMenu、TPopupMenu优先级上输入框、多行文本、列表框、树、菜单和对话框是乱码重灾区值得第一批换。按钮、标签这类短文本一般从资源文件读取如果资源本身是 GBK 且不涉及外部输入可以先不动。数据库感知控件TTntDBEdit 那一族反而要谨慎很多老工程的数据库乱码不是控件问题是驱动字符集和表字段设置的问题换了 TNT 也可能白换排查时别指望这一层兜底。3. 安装 2.3.0先编译 TntRuntime 再装 TntDesign最小验证工程跑通中文TNT 这类老组件包的安装顺序非常讲究。装反了、路径错了、DCU 混了IDE 启动时就会报包加载失败。安装只有一个原则运行时包只编译不安装设计时包编译后安装。3.1 解压路径与两个包的编译顺序拿到 2.3.0 的源码包后第一件事是解压到一个纯英文路径。Delphi 7 对中文路径的兼容性本身就差组件包编译出来的中间文件带着中文路径后面排错会非常痛苦。目录结构通常是 Runtime 和 Design 两块分别对应运行时包和设计时包。D:\Libs\TNT23 |-- Runtime | |-- TntClasses.pas | |-- TntSysUtils.pas | |-- TntStdCtrls.pas | |-- TntComCtrls.pas | |-- TntDialogs.pas | -- TntRuntime.dpk -- Design |-- TntDsgn.pas -- TntDesign.dpk我一般会先建好这个目录再点开TntRuntime.dpk只点 Compile不点 Install。运行时包是给工程引用的它不是设计期组件不需要出现在组件面板上。设计时包TntDesign.dpk才是要 Install 的那个安装成功后组件面板会出现一个新页。顺序反过来的话设计时包会因为找不到运行时包的 BPL 而直接编译失败。3.2 在 Delphi 7 面板里注册 TNT 组件页打开设计时包后包里包含的单元列表决定了哪些控件被注册。一个典型的TntDesign.dpk内容长这样单元名以你拿到的 2.3.0 实际文件为准// TntDesign.dpk示意 package TntDesign; {$R *.res} requires rtl, vcl, TntRuntime; contains TntDsgn in Design\TntDsgn.pas, TntStdCtrls in Runtime\TntStdCtrls.pas, TntComCtrls in Runtime\TntComCtrls.pas; end.注意requires里写了TntRuntime这就是为什么必须先编译运行时包。接着在包管理器里点 Install 按钮IDE 会弹出一个提示告诉你组件已注册。此时还要补一步打开 Tools - Environment Options - Library - Library Path把D:\Libs\TNT23\Runtime加进搜索路径否则新建工程引用这些单元时 IDE 找不到源文件。BPL 输出目录我习惯指定到 IDE 默认的 Projects\Bpl 下这样开发机和打包机都不用额外配系统 PATH。3.3 最小验证工程一个 Form 验证中文显示、大小写与内存字符串装完先别急着改老工程建一个空白工程拖一个 TTntEdit、一个 TTntMemo、一个 TTntLabel在 FormCreate 里写几行验证代码。这一步确认的是组件真的能用而不是面板上摆了但内部全是 ANSI。uses TntStdCtrls, TntSysUtils; procedure TForm1.FormCreate(Sender: TObject); var ws: WideString; begin TntEdit1.Text : 你好TNT 2.3.0; ws : WideUpperCase(TntEdit1.Text); // 宽字符大小写转换 TntMemo1.Lines.Text : ws; TntLabel1.Caption : 邮箱前缀 WideLowerCase(ABCEXAMPLE); end;TntEdit1.Text直接赋中文字符串说明设计期属性和运行期赋值都走 WideString。WideUpperCase是 TNT 对 SysUtils 的宽字符补位老代码里UpperCase处理英文没区别但处理德文 ß、土耳其文 İ 这类字符时会按系统代码页乱来这里统一换成 Wide 版本最省心。如果编译报找不到 TntStdCtrls检查 Library Path如果组件面板没出现 TNT 页回去重新 Install TntDesign.dpk。4. 迁移老工程批量替换控件名TStrings、对话框和文件读写逐个换组件装好后真正花时间的是把老代码里的控件逐个替换。TNT 的接口设计刻意贴近 VCL 原版所以替换本身不难难在替换得不彻底.dfm 改了、.pas 没改或者字符串容器还停留在 TStringList日文韩文数据一经过就丢。4.1 批量替换 .pas 和 .dfm 控件名的脚本先写一个脚本把工程里所有源文件扫一遍把标准控件类名替换成 TNT 版本。关键点是必须同时处理 .pas 和 .dfm.dfm 里object TEdit1: TEdit这种声明不改IDE 打开窗体就会把 TTntEdit 直接当作未知类处理。import re, pathlib # 类名精确替换表 MAP { TEdit: TTntEdit, TMemo: TTntMemo, TLabel: TTntLabel, TComboBox: TTntComboBox, TListBox: TTntListBox, TPageControl: TTntPageControl, TTreeView: TTntTreeView, TListView: TTntListView, } def replace_in(path): s path.read_text(encodinggbk, errorsignore) for src, dst in MAP.items(): s re.sub(rf\b{src}\b, dst, s) path.write_text(s, encodinggbk) for p in list(pathlib.Path(.).glob(*.pas)) list(pathlib.Path(.).glob(*.dfm)): replace_in(p)用\b词边界是为了避免误伤比如TTreeView会匹配到CustomTreeView里的子串吗不会因为左侧是词字符边界不成立。替换前先备份一份脚本跑完后在 IDE 里全量编译报错集中的地方就是 mapping 表没覆盖到的控件比如 TTntEdit 没有对应的 TTntDateTimePicker那类控件要单独评估。替换完还要手动检查 uses 子句往每个引用了 TNT 控件的单元里补上 TntStdCtrls、TntComCtrls 这类依赖。4.2 字符串容器三件套TTntStringList、WideFormat、WideUpperCase控件换了字符串容器不换等于白换。老代码里到处是TStringList它的Add、Assign、LoadFromFile全是 ANSI 语义只要有一条数据从 TTntMemo 的 Lines 流向 TStringList宽字符立刻被压成窄字符。type TMyForm class(TForm) TntListBox1: TTntListBox; TntMemo1: TTntMemo; private FNames: TTntStringList; // 替代原来的 TStringList end; procedure TMyForm.LoadNames(const AFileName: WideString); var sl: TTntStringList; begin sl : TTntStringList.Create; try sl.LoadFromFile(AFileName); // 系统代码页读取UTF-8 需要辅助函数 TntListBox1.Items.Assign(sl); // Items 本身是 TTntStrings finally sl.Free; end; end;TTntStringList的用法和TStringList几乎一致但内部存储和读写都是 WideString。赋值给TntListBox1.Items时是宽字符到宽字符没有转换损耗。另一个要全局替换的是Format老代码Format(数量%d, [n])在 ANSI 下没问题但一旦格式化串里混入从资源读出来的中文Format的 AnsiString 返回类型会截断。TNT 提供WideFormat返回 WideString签名和 Format 完全一样替换成本最低。4.3 弹窗和文件对话框WideMessageDlg 与 TTntOpenDialog 的换法界面控件换完后运行期最容易漏的是消息框。MessageDlg、ShowMessage底层走 ANSI 资源标题和按钮文字在中文系统上还能看一旦系统区域设置变成非中文、或者消息内容来自数据库里的日文数据弹窗直接变成一排问号。TNT 在 TntDialogs 里给了对应的宽字符版本。uses TntDialogs; // 老代码MessageDlg(确认删除, mtConfirmation, [mbYes, mbNo], 0) // 新代码 if WideMessageDlg(确认删除这条记录, mtConfirmation, [mbYes, mbNo], 0) mrYes then begin // 执行删除 end; // 文件对话框TOpenDialog 的 FileName 是 ANSI换成 TTntOpenDialog with TTntOpenDialog.Create(nil) do begin try Filter : 文本文件 (*.txt)|*.txt|所有文件 (*.*)|*.*; Title : WideUpperCase(请选择要导入的文件); if Execute then TntMemo1.Lines.LoadFromFile(FileName); // FileName 是 WideString finally Free; end; end;WideMessageDlg的前三个参数和原来的MessageDlg完全一致按钮文字用的是系统资源中文系统上显示中文英文系统显示英文。TTntOpenDialog的FileName属性是 WideString选中文文件名不再丢尾巴。这里有个坑要提前说如果你的老工程自己封装了按钮文字比如把 [mbYes] 改成自定义中文按钮那WideMessageDlg帮不了你得用WideCreateMessageDialog拿到对话框对象后逐个按钮改 Caption这是多语言版本常用做法。5. 避坑UTF-8 BOM、WM_GETTEXT 截断和包加载失败三个高频事故TNT 用了两年以上的工程踩来踩去基本就三个坑。这三个坑都有固定套路记录成现象、原因、解决三段式实际排错时可以照着查。5.1 UTF-8 文件带 BOM现象、原因与三段式读取函数现象用TTntMemo.Lines.LoadFromFile读取一个用记事本另存为 UTF-8 的文本文件第一行开头出现一个奇怪的字符中文倒是正常但首字符怎么看怎么不对。原因记事本保存的 UTF-8 文件带 BOM文件开头是EF BB BF三个字节而 2.3.0 的LoadFromFile不做 BOM 识别把它当成内容读进来了。解决不要依赖 LoadFromFile 读 UTF-8改成读字节、剥 BOM、再转 WideString。function LoadUtf8FileToWide(const AFileName: WideString): WideString; var fs: TFileStream; bytes: AnsiString; begin fs : TFileStream.Create(AFileName, fmOpenRead); try SetLength(bytes, fs.Size); fs.Read(bytes[1], fs.Size); finally fs.Free; end; // 剥掉 UTF-8 BOM if (Length(bytes) 3) and (bytes[1] #$EF) and (bytes[2] #$BB) and (bytes[3] #$BF) then Delete(bytes, 1, 3); Result : UTF8Decode(bytes); end;UTF8Decode是 Delphi 7 里就有的函数把 UTF-8 编码的 AnsiString 转成 WideString正好和 TNT 的文本模型接上。写完这个函数凡是工程里要读外部文本文件的地方都走它不要再用 Lines.LoadFromFile。反过来写文件时也要注意TTntMemo.Lines.SaveToFile 保存的是 ANSI想输出 UTF-8 得自己用WideString转UTF8Encode再写流。5.2 WM_GETTEXT 拿到半个字符跨体系句柄消息的正确写法现象工程里有一段老代码拿到某个 TTntEdit 的 Handle用GetWindowText或SendMessage(Handle, WM_GETTEXT, ...)读取内容返回的字符串被截断或者每个中文字符后面跟着一个乱码。原因TTntEdit 的底层窗口是 Unicode 窗口而GetWindowText和SendMessage不带 W 后缀时走 ANSI 转换。调用方按字节数申请了缓冲区系统按宽字符数理解wParam结果缓冲区只填了一半如果文本里有字符无法映射到当前代码页就直接丢字。解决跨系统调用时统一用SendMessageW缓冲区用 WideChar。function GetEditTextW(h: HWND): WideString; var n: Integer; begin n : SendMessageW(h, WM_GETTEXTLENGTH, 0, 0); SetLength(Result, n); SendMessageW(h, WM_GETTEXT, n 1, LPARAM(PWideChar(Result))); end;这里n 1是给结尾的#0留位置LPARAM(PWideChar(Result))必须转成指针类型再传传整型地址在新版编译器会报警告。同样的道理也适用于WM_SETTEXT往 TNT 控件写文本时要么直接改.Text属性要么用SendMessageW不要用第三方 ANSI 缓冲区塞进来。这个坑在调用某些第三方界面库的接口时特别常见因为它们内部拿到句柄后喜欢自己发消息。5.3 包加载失败多版本 TNT 混装导致的 IDE 启动报错现象换了一台机器重装 Delphi 7打开老工程时 IDE 弹窗报错大意是“Cannot load package TntDesign. It contains unit TntStdCtrls, which is also contained in package TntOldRuntime”或者直接提示找不到 TntRuntime.bpl。原因机器上装了不止一份 TNT或者旧工程的搜索路径指向了另一个版本的 Runtime 目录。Delphi 7 的包机制很敏感同一个单元出现在两个包里IDE 直接拒绝加载。解决先把环境里的所有 TNT 痕迹清掉再按顺序重装。# 清理旧 DCU避免不同版本混编 del /s /q D:\Libs\TNT23\*.dcu # 清掉 IDE 默认目录下可能残留的 TNT BPL del /q %BDSCOMMONDIR%\Bpl\Tnt*.bpl del /q %BDSCOMMONDIR%\Bpl\Tnt*.dcp清理完用我前面说的顺序先编译 TntRuntime再安装 TntDesign然后检查项目搜索路径里是不是同时指向了两个 TNT 目录。换 Delphi 版本时尤其要注意DCU 和 BPL 跟编译器版本强绑定把 Delphi 7 编译出来的 DCU 直接拷给 Delphi 2007 用包加载必炸必须在新 IDE 里重新编译两个包。6. 最后的验证技巧一张自测页把 2.3.0 的 Unicode 能力钉死改完老工程后我会在工程里留一个隐藏的验证窗体每次动过界面代码就跑一遍。这个窗体的作用不是功能测试而是快速确认四条最关键的 Unicode 链路没有回退显示、大小写、UTF-8 读写、句柄消息。验证页里放一个 TTntMemo 和一个 TTntButton按钮点击时执行下面的检查。procedure TFormTest.btnVerifyClick(Sender: TObject); var wideIn, wideOut: WideString; fname: WideString; sl: TTntStringList; begin // 1. 显示链路多语言文本直接写入 wideIn : 中文测试 Japanese テスト 한국어; TntMemo1.Lines.Text : wideIn; // 2. 大小写链路确认宽字符函数没有退化 if WideUpperCase(TntMemo1.Lines.Text) then ShowMessage(大小写转换异常); // 3. UTF-8 往返写入再读回内容必须一致 fname : ExtractFilePath(Application.ExeName) tnt_check.txt; sl : TTntStringList.Create; try sl.Add(wideIn); sl.SaveToFile(fname); // 先按系统代码页存 sl.LoadFromFile(fname); // 再读回来 if sl.Text wideIn then ShowMessage(UTF-8 往返不一致); finally sl.Free; end; // 4. 句柄消息用 SendMessageW 取回内容 if GetEditTextW(TntMemo1.Handle) wideIn then ShowMessage(句柄消息读取异常); end;这四条检查一条都不多余。显示链路挂掉说明控件替换不彻底有控件漏换大小写链路挂掉说明WideUpperCase没接上或者还在用 SysUtils 的 ANSI 版本往返不一致说明文件读写里有环节偷偷走了窄字符句柄消息异常则是有人在后面用 ANSI 方式动了控件。检查项做法期望结果显示写入中、日、韩混合文本无问号、无截断大小写WideUpperCase 处理混合文本返回非空且正确文件往返TTntStringList 写再读内容与写入前一致句柄消息SendMessageW 读取内容完整取回宽字符文本这个自测页本身就是最好的回归用例。我现在接手的每一个老工程都会先把这个页面塞进去跑一遍再动手改业务代码——TNT 这套组件能用不代表你的工程里每一处都接对了很多隐藏的 ANSI 调用是编译不报错、运行全乱码的。先把验证页跑通再谈替换顺序反了会在一堆界面里找乱码找到怀疑人生。老 Delphi 工程的 Unicode 改造不是一锤子买卖组件装上只是开始把验证习惯留下才是真正不翻车的办法。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑