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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=注册账号,然后进入控制台的 API Keys 页面https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite创建一个 Key。创建时注意选择权限范围,测试用途选默认的读写权限即可。Key 只显示一次,复制后存到安全的地方。
拿到 Key 之后,你可以在模型对话页面https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite先手动测试一下,输入一段中文,确认返回正常。这一步是为了排除 Key 本身的问题,如果模型对话页面都返回不了中文,那后面 Delphi 测试程序肯定也跑不通。
对于需要长期跑自动化测试的场景,可以考虑 Coding Planhttps://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite,它的额度更适合频繁请求。不过对于 TAdvMemo 修正验证这种一次性任务,按量付费的 API Key 就够了。
接入文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,里面有完整的请求格式和返回示例。如果你用 Claude Code 做辅助开发,可以参考https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude-code-anthropic&utm_campaign=rewrite的配置说明,把 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 到 CurX+1 的宽字符长度没有增加,说明 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_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite,输入不同的提示词就能得到不同风格的中文内容。
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_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite检查 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-mini,TaoToken 会返回一个包含 error 字段的 JSON。另外,如果请求体里的 JSON 格式有误(比如少了引号),也会导致解析失败。建议用TJSONObject.ParseJSONValue之前先检查返回值是否为 nil。
OAuth 相关报错:如果你在 Claude Code 或 Cline 里配置 TaoToken,可能会遇到 OAuth 认证失败。这时候检查配置文件里的 Base URL 是否写成了https://taotoken.net/api,Key 是否填在正确的位置。对于 Claude Code,参考https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude-code-anthropic&utm_campaign=rewrite的配置说明,确保三件套齐全: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。如果还是char,C := Copy(S, CurX + 1, 2)会编译报错或者截断。另外,TDeleteCharUndo的构造函数参数类型也要对应修改,否则撤销操作会出错。
6. 长期编码与 Agent 场景的 CTA
TAdvMemo 的中文修正只是 Delphi 项目里多字节处理的一个缩影。类似的字节索引问题还会出现在字符串截断、正则匹配、JSON 解析等场景。如果你经常需要处理这类编码问题,建议把 TaoToken 作为统一的验证通道,配合 Coding Plan 做长期测试。
对于需要频繁调用 API 做中文文本生成和验证的场景,Coding Planhttps://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite提供更稳定的额度和更低的单次成本。你可以用它跑自动化回归测试,每次修改 TAdvMemo 源码后自动拉取中文样本验证。
如果你在配置过程中遇到 Key 或权限问题,直接去 API Keys 页面https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite重新生成一个。接入文档https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite里有完整的错误码说明和请求示例,遇到 401 或 JSON 解析失败时可以先对照文档排查。
最后提醒一点:修改 TMS 套件源码后,记得把补丁单独存一份,因为 TMS 升级时可能会覆盖你的修改。可以在项目里建一个patches目录,把修改前后的AdvMemo.pas都存进去,方便对比和回滚。