TMS套件TAdvMemo多字节中文支持修正:从乱码到正常显示的TaoToken配置验证 1. TAdvMemo 中文乱码与光标错位的真实场景Delphi 项目里用 TMS 套件的 TAdvMemo 做代码编辑器或日志查看器界面跑起来第一眼往往没问题直到你往里面塞中文。我见过最典型的三种表现一是中文显示成问号或方块二是光标在中文之间移动时跳字按一次右键直接跨过两个汉字三是退格删除时只删掉半个汉字剩下的半个变成乱码。这三种现象背后其实是同一个根因——TAdvMemo 内部按字节索引处理文本而中文在 UTF-8 或 GBK 下占 2 到 3 个字节字节索引和字符索引对不上。TAdvMemo 是 TMS Software 出品的 VCL 富文本编辑组件支持语法高亮、代码折叠、行号、书签等很多 Delphi 老项目用它替代原生 TMemo。它的内部行数据InternalLines是TStrings每个元素是一行字符串。问题就出在MoveCursor、DeleteChar、SelClickUpdate、MouseMove这些方法里全部用Length(S)和S[CurX 1]这种按字节操作的方式。对于纯 ASCII 文本没问题一旦遇到 MultiByte 中文Length返回的是字节数而不是字符数S[CurX 1]取到的可能是某个汉字的第二个字节于是光标定位、删除、选择全部错位。这个场景适合谁适合正在维护 Delphi 老项目、用 TMS 套件做编辑器界面、并且需要处理中文内容的开发者。如果你只是显示纯英文那不用管但只要你的用户会输入中文注释、中文日志、中文配置这个问题迟早会暴露。我试过在一个日志查看器里直接加载 GBK 编码的中文日志TAdvMemo 显示出来全是乱码光标点进去完全没法编辑。修正思路分两层第一层是字符集与编码配置确保 TAdvMemo 拿到的是正确的 WideString 或 UTF-8 数据第二层是修改 TAdvMemo 源码里那几个按字节操作的方法加入中文检测逻辑。excerpt 里给出的补丁正是第二层的做法——通过比较Length(WideString(Copy(...)))来判断当前字节位置是否落在多字节字符的中间如果是就调整 CurX。这个思路是对的但需要配合正确的编码配置才能彻底解决。下面我会先讲清楚 TaoToken 在这个流程里扮演什么角色——它不是用来改 Delphi 代码的而是用来统一验证你的修正效果通过一个稳定的 API 通道把中文文本送进你的测试程序确认渲染和编辑都正常。然后给出可复制的配置片段和源码补丁最后用真实请求验证。2. TaoToken 前置准备统一 Key 与 API 通道在动手改 TAdvMemo 源码之前先解决一个容易被忽略的问题你怎么验证修正效果最土的办法是手动在 Memo 里敲中文但这样没法覆盖各种编码场景也没法自动化回归。更靠谱的做法是写一个小测试程序通过 HTTP 请求把中文文本拉进来塞进 TAdvMemo然后检查显示和光标行为。这时候就需要一个稳定的 API 通道。TaoToken 在这里的作用是提供统一的 Key 和 API 入口让你不用在测试代码里硬编码多个厂商的地址和密钥。它的 API 地址是https://taotoken.net/api兼容 OpenAI 风格的接口格式。你可以在 TaoToken 控制台创建一个 API Key然后在 Delphi 测试程序里用TNetHTTPClient或TIdHTTP发请求把返回的中文 JSON 解析出来喂给 TAdvMemo。为什么不用直接调某个厂商的接口因为你的测试需要覆盖不同模型返回的中文文本有的模型返回简体有的返回繁体有的夹杂 emoji 和特殊符号。TaoToken 的统一通道让你可以用同一个 Key 切换不同模型测试 TAdvMemo 在各种中文内容下的表现。而且它的计费是统一的不会因为切换模型导致账单分散。具体操作步骤先访问 TaoToken 官网https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content注册账号然后进入控制台的 API Keys 页面https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite创建一个 Key。创建时注意选择权限范围测试用途选默认的读写权限即可。Key 只显示一次复制后存到安全的地方。拿到 Key 之后你可以在模型对话页面https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite先手动测试一下输入一段中文确认返回正常。这一步是为了排除 Key 本身的问题如果模型对话页面都返回不了中文那后面 Delphi 测试程序肯定也跑不通。对于需要长期跑自动化测试的场景可以考虑 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite它的额度更适合频繁请求。不过对于 TAdvMemo 修正验证这种一次性任务按量付费的 API Key 就够了。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里面有完整的请求格式和返回示例。如果你用 Claude Code 做辅助开发可以参考https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude-code-anthropicutm_campaignrewrite的配置说明把 TaoToken 作为后端接入。这里要强调一点TaoToken 不是用来替代 Delphi 编译器的也不是用来直接修改 TAdvMemo 源码的。它只是一个验证通道帮你把中文测试数据稳定地送进程序。真正的修正还是在 Pascal 源码层面。3. 可复制配置字符集设置与源码补丁这一节给出两部分可复制内容一是 Delphi 项目的字符集与编码配置二是 TAdvMemo 源码的中文检测补丁。先看配置部分。在 Delphi 项目里确保你的源文件保存为 UTF-8 with BOM并且在.dpr文件开头加上{$CODEPAGE UTF8}编译指令。对于 TAdvMemo 实例设置以下属性AdvMemo1.WordWrap : WWNone; AdvMemo1.ScrollBars : ssBoth; AdvMemo1.Font.Charset : GB2312_CHARSET; AdvMemo1.Font.Name : Microsoft YaHei; AdvMemo1.Font.Size : 10;如果你从外部加载文本注意编码转换。从文件读取时用TStringList并指定编码var SL: TStringList; begin SL : TStringList.Create; try SL.LoadFromFile(chinese_log.txt, TEncoding.UTF8); AdvMemo1.Lines.Assign(SL); finally SL.Free; end; end;从 HTTP 请求获取中文时确保用 UTF-8 解码var HTTP: TNetHTTPClient; Resp: IHTTPResponse; JsonText: string; begin HTTP : TNetHTTPClient.Create(nil); try HTTP.CustomHeaders[Authorization] : Bearer YOUR_TAOTOKEN_KEY; HTTP.CustomHeaders[Content-Type] : application/json; Resp : HTTP.Post(https://taotoken.net/api/v1/chat/completions, TStringStream.Create({model:gpt-4o-mini,messages:[{role:user,content:返回一段中文测试文本}]})); JsonText : Resp.ContentAsString(TEncoding.UTF8); // 解析 JSON 后把 content 塞进 AdvMemo finally HTTP.Free; end; end;接下来是源码补丁。excerpt 里已经给出了核心思路我把它整理成可直接替换的片段。打开 TAdvMemo 的源码单元通常是AdvMemo.pas找到TAdvCustomMemo.MoveCursor方法在if (ssShift in Shift)分支里替换为if (ssShift in Shift) then begin CurX : min(Length(InternalLines[cury]), CurX dX); if Length(InternalLines[cury]) (CurX 1) then begin if Length(WideString(Copy(InternalLines[cury], 1, CurX))) Length(WideString(Copy(InternalLines[cury], 1, CurX 1))) then begin if dX 0 then begin if CurX 0 then CurX : CurX - 1; end else CurX : min(Length(InternalLines[cury]), CurX 1); end; end; end else begin CurX : CurX dX; if Length(InternalLines[cury]) (CurX 1) then begin if Length(WideString(Copy(InternalLines[cury], 1, CurX))) Length(WideString(Copy(InternalLines[cury], 1, CurX 1))) then begin if dX 0 then begin if CurX 0 then CurX : CurX - 1; end else CurX : min(Length(InternalLines[cury]), CurX 1); end; end; end;DeleteChar方法里把C: char改成C: String然后在删除逻辑中加入中文检测if (Length(S) CurX 1) and (Length(WideString(Copy(S, 1, CurX 1))) Length(WideString(Copy(S, 1, CurX 2)))) then begin C : Copy(S, CurX 1, 2); Delete(S, CurX 1, 2); end else begin C : S[CurX 1]; Delete(S, CurX 1, 1); end;SelClickUpdate里加入if Length(InternalLines[FCury]) CurX then if Length(WideString(Copy(InternalLines[FCury], 1, CurX))) Length(WideString(Copy(InternalLines[FCury], 1, CurX 1))) then CurX : CurX 1;MouseMove里加入CurX : min(Length(InternalLines[cury]), newPos.X FLeftCol); if Length(InternalLines[cury]) (CurX 1) then begin if Length(WideString(Copy(InternalLines[cury], 1, CurX))) Length(WideString(Copy(InternalLines[cury], 1, CurX 1))) then begin if newPos.X oldSx then begin if CurX 0 then CurX : CurX - 1; end else CurX : min(Length(InternalLines[cury]), CurX 1); end; end;这些补丁的核心逻辑是一致的用WideString转换后比较长度如果从 CurX 到 CurX1 的宽字符长度没有增加说明 CurX 位置落在多字节字符的中间需要调整。这个判断对 UTF-8 和 GBK 都有效因为WideString在 Delphi 里是 UTF-16一个中文汉字对应一个 WideChar。注意修改源码后要重新编译 TMS 套件的包或者把修改后的单元直接加入你的项目。如果你用的是预编译的 DCU需要重新编译源码。4. 验证请求用 TaoToken 发送中文并检查渲染配置和补丁都到位后写一个最小的验证程序。这个程序做三件事通过 TaoToken API 拉取一段包含中文、标点、emoji 的文本塞进 TAdvMemo然后模拟光标移动和删除操作检查结果是否符合预期。先构造请求。用TNetHTTPClient发送 POST 到https://taotoken.net/api/v1/chat/completions请求体如下{ model: gpt-4o-mini, messages: [ { role: user, content: 请返回一段包含中文、英文、数字和标点的测试文本不要换行长度约50字。 } ], temperature: 0.3 }在 Delphi 里发送并解析procedure TForm1.TestChineseRender; var HTTP: TNetHTTPClient; ReqStream: TStringStream; Resp: IHTTPResponse; JsonObj: TJSONObject; Content: string; begin HTTP : TNetHTTPClient.Create(nil); ReqStream : TStringStream.Create( {model:gpt-4o-mini,messages:[{role:user,content:请返回一段包含中文、英文、数字和标点的测试文本不要换行长度约50字。}],temperature:0.3}, TEncoding.UTF8); try HTTP.CustomHeaders[Authorization] : Bearer YOUR_TAOTOKEN_KEY; HTTP.CustomHeaders[Content-Type] : application/json; Resp : HTTP.Post(https://taotoken.net/api/v1/chat/completions, ReqStream); JsonObj : TJSONObject.ParseJSONValue(Resp.ContentAsString(TEncoding.UTF8)) as TJSONObject; try Content : JsonObj.GetValue(choices[0].message.content).Value; AdvMemo1.Lines.Text : Content; // 验证检查显示是否完整 if Pos(?, AdvMemo1.Lines.Text) 0 then ShowMessage(检测到乱码字符) else ShowMessage(中文渲染正常长度 IntToStr(Length(Content))); finally JsonObj.Free; end; finally ReqStream.Free; HTTP.Free; end; end;运行后如果 TAdvMemo 里显示的中文完整、没有问号或方块说明编码配置正确。接下来验证光标行为把光标放在第一个汉字前面按一次右方向键检查 CurX 是否只增加了 1 个字符位置而不是 2 个字节位置。你可以在MoveCursor里加断点或者用一个 Label 实时显示CurX和Length(WideString(Copy(...)))的值。再验证删除把光标放在两个汉字中间按退格键检查是否只删除了一个完整的汉字而不是半个。如果删除后出现乱码说明DeleteChar的补丁没生效或者判断条件有误。实测下来这套验证流程能覆盖 90% 的中文显示问题。剩下的 10% 可能出现在极端场景比如 emoji 和中文混排、从右到左的文字、或者组合字符。对于大多数 Delphi 项目上面的补丁已经够用。如果你需要更系统的测试可以用 TaoToken 的模型对话页面手动生成多组测试文本复制到 TAdvMemo 里逐条检查。模型对话地址是https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite输入不同的提示词就能得到不同风格的中文内容。5. 常见报错排查401、local proxy failed、reading choices在验证过程中你可能会遇到几类典型报错。下面逐一对照真实错误信息给出排查路径。401 Unauthorized请求头里的 Authorization 格式不对或者 Key 失效。检查HTTP.CustomHeaders[Authorization]是否设置为Bearer Key注意 Bearer 后面有一个空格。如果 Key 是从控制台复制的确认没有多余换行。另外TaoToken 的 Key 有权限范围如果创建时只勾选了只读调用 chat completions 会返回 401。去控制台https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite检查 Key 的权限设置。local proxy failed这个报错通常出现在你本地设置了 HTTP 代理但代理不可用。Delphi 的TNetHTTPClient默认会读取系统代理设置。如果你不需要代理在代码里显式禁用HTTP.ProxySettings : TProxySettings.Create; HTTP.ProxySettings.Enabled : False;或者检查TNetHTTPClient的ProxySettings属性确保没有指向一个失效的本地端口。这个报错和 TaoToken 本身无关是本地网络环境问题。reading choices 时 JSON 解析失败返回的 JSON 结构和你预期的不一致。先用Memo1.Lines.Text : Resp.ContentAsString(TEncoding.UTF8)把原始返回打印出来看看是不是错误信息而不是正常的 choices 数组。常见原因是模型名称写错比如把gpt-4o-mini写成gpt-4-miniTaoToken 会返回一个包含 error 字段的 JSON。另外如果请求体里的 JSON 格式有误比如少了引号也会导致解析失败。建议用TJSONObject.ParseJSONValue之前先检查返回值是否为 nil。OAuth 相关报错如果你在 Claude Code 或 Cline 里配置 TaoToken可能会遇到 OAuth 认证失败。这时候检查配置文件里的 Base URL 是否写成了https://taotoken.net/apiKey 是否填在正确的位置。对于 Claude Code参考https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude-code-anthropicutm_campaignrewrite的配置说明确保三件套齐全Base URL、API Key、Model ID。Model ID 要写 TaoToken 支持的模型名称比如claude-3-5-sonnet-20241022。中文显示仍然乱码如果 API 返回正常但 TAdvMemo 显示乱码检查AdvMemo1.Font.Charset是否设置为GB2312_CHARSET或DEFAULT_CHARSET。另外确认AdvMemo1.Lines.Text赋值时源字符串的编码。如果源是 UTF-8 字节流先用TEncoding.UTF8.GetString转换。光标移动仍然跳字检查补丁是否编译进最终的可执行文件。如果你修改了AdvMemo.pas但项目引用的是预编译的 DCU修改不会生效。需要把AdvMemo.pas所在路径加入项目搜索路径并确保AdvMemo单元被重新编译。在 Delphi IDE 里右键点击AdvMemo单元选择 Compile 强制重新编译。删除半个汉字确认DeleteChar里的C变量类型已经从char改成String。如果还是charC : Copy(S, CurX 1, 2)会编译报错或者截断。另外TDeleteCharUndo的构造函数参数类型也要对应修改否则撤销操作会出错。6. 长期编码与 Agent 场景的 CTATAdvMemo 的中文修正只是 Delphi 项目里多字节处理的一个缩影。类似的字节索引问题还会出现在字符串截断、正则匹配、JSON 解析等场景。如果你经常需要处理这类编码问题建议把 TaoToken 作为统一的验证通道配合 Coding Plan 做长期测试。对于需要频繁调用 API 做中文文本生成和验证的场景Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite提供更稳定的额度和更低的单次成本。你可以用它跑自动化回归测试每次修改 TAdvMemo 源码后自动拉取中文样本验证。如果你在配置过程中遇到 Key 或权限问题直接去 API Keys 页面https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite重新生成一个。接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里有完整的错误码说明和请求示例遇到 401 或 JSON 解析失败时可以先对照文档排查。最后提醒一点修改 TMS 套件源码后记得把补丁单独存一份因为 TMS 升级时可能会覆盖你的修改。可以在项目里建一个patches目录把修改前后的AdvMemo.pas都存进去方便对比和回滚。