马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
本帖最后由 pbai 于 2026-10-27 05:53 编辑
PBIDEA:用 uo_charset 做字符集转换与乱码排查全解(「gbk」的真实含义、AutoConvert 的盲区与 BOM 的三种下场,PB10 基准 · PB12.5 实测)
阅读说明
- 适用版本:PowerBuilder 10 及以上(本文以 PB10 为写作与验证基准,PB12.5 双轨实测)。所用组件 uo_charset 属 PBIDEA 运行库(websuite.pbl + 运行时 PbIdea.dll),需与工程所用版本一致。
- 支持数据库:本文不涉及数据库,无建表脚本、无连接串。
- 操作系统与环境:Windows 7 及以上;32 位工程配 32 位运行库、64 位工程配 64 位运行库;PbIdea.dll 与 Pbidea_cs.dll 必须与可执行文件同目录或位于系统搜索路径。
- 难度系数:★★★☆☆(需要一点「字节」意识:PB 字符串内部是 UTF-16LE,blob 才是字节序列)。
- 其它:文中示例所需的测试文件全部由代码自己生成,不依赖任何外部数据;建议先读第八节「写文件」再回头看第五节,顺序会顺。
一、这篇讲什么
PowerBuilder 里的中文乱码,九成不是「PB 不支持中文」,而是三件事没对齐:字节是什么编码、字符串是什么编码、落盘又写成什么编码。PB 内部字符串固定是 UTF-16LE(PB10 起),而文件、网络、数据库里躺着的往往是 GBK 或 UTF-8 的字节流。两者之间的桥,就是 PBIDEA 的 uo_charset。
它看起来只有几个方法,实际用起来全是暗礁:
- 你以为传 big5 就能转繁体,它给你一个长度 0 的空 blob,不报错;
- 你以为 gbk 就是 GBK,换个代码页它悄悄写出 Big5 字节;
- 你以为 AutoConvert 能自动认编码,它把 GBK 文件解成一半字数的乱码;
- 你以为读文件不用管 BOM,UTF-16LE 带 BOM 的文件读回来开头多一个问号。
这篇把 uo_charset 的 10 个方法、8 个包装函数、以及 20 组实机探针的读数一次讲透:哪些名字真能用、失败长什么样、BOM 到底被吃还是被留、哪种情况根本判不出编码。所有读数都是 PB10 与 PB12.5 双轨跑出来的,两版逐字一致。
二、先给结论:能力边界一张表
| 场景 | 结论 | 依据(实测) | | 目标字符集 | 只认 gbk / utf-8 / utf-16le 三个名字,第四个别名 gb18030 默认等价 GBK | 24 个候选名逐个试,其余全部返回 0 长度 | | 大写与代码页号 | GBK、CP936、936、big5、utf-16、utf-16be、cs-2、ascii、latin1、gb 全失败 | 同上,且不报错、不为 NULL | | 同名互转 | gbk→gbk、utf-8→utf-8、utf-16le→utf-16le 一律返回空 blob | 想做「复制/清洗」不能用它,会得到空串 | | 失败形态 | 返回长度 0 的 blob,IsNull() 为 false,无错误码 | 只能靠 Len() 判成败 | | gbk 的真实含义 | 是「当前 ANSI 代码页」的影子,SetCodePage() 能把它改成 UTF-8 甚至 Big5 | 默认 D6D0CEC4、CP65001 下 E4B8ADE69687、CP950 下 A4A4A4E5 | | GB18030 四字节汉字 | 默认被压成 3F 3F(两个问号);SetGB18030(true) 后才输出 95 32 82 36 | U+20000 实测 | | IsUtf8Text | 是真校验器(能识别被截断的 UTF-8) | GBK 字节判 N、截断 UTF-8 判 N | | IsUtf16Text | 是「含 0 字节 / 带 BOM」的启发式,纯中文无 BOM 的 UTF-16LE 判 N | 8 字节纯中文 UTF-16LE → N | | AutoConvert | 只对 UTF-8(含 BOM)与 ASCII 可靠;GBK 与 UTF-16 会解错 | 4 字 GBK 解成 2 字、4 字 UTF-16LE 解成 5 字 | | 唯一有「失败信号」的方法 | SetCodePage()(非法代码页返回 false) | 其余方法失败一律静默 |
三、API 全貌
uo_charset 全部能力由 PbIdea.dll 导出,PB 侧只是薄封装。按用途分四组:
| 分组 | 方法 | 说明 | | 核心转换 | CharsetConvert(blob src, string from, string to) → blob | 唯一的真转换入口,字节进字节出 | | 自动识别 | AutoConvert(blob src) → string | 一次性把字节解成字符串,不认识 GBK | | 嗅探 | IsUtf8Text(blob) / IsUtf16Text(blob) | 布尔判定,见第六节 | | 定向嗅探 | HaveAnsiText(blob, string) / HaveUtf8Text(blob, string) / HaveUtf16Text(blob, string) | 判断 blob 里是否含某个词的某种编码字节 | | 环境 | SetCodePage(long) / SetGB18030(boolean) | 改默认代码页、开关 GB18030 表 | | 字体 | SetDefaultFont(...) 4 个重载 / GetDefaultFont(ref string, ref int) | 影响的是 PBIDEA 各控件默认字体,与转码无关 | | 清单与查字 | GetCharsetList() → uo_json / GetCharByUnicode(long) → string | 138 项代码页清单、按码点取字 | | 8 个包装 | gbk2utf8 / gbk2utf16 / utf162utf8 / utf162gbk / utf82utf16 / utf82gbk / gbk2big5 / big52gbk | 只是 CharsetConvert 的语法糖,big5 那两个同样返回空 |
要注意包装函数的参数语义:gbk2utf8(blob) 的第一个参数是 GBK 字节,不是 PB 字符串。传 Blob(字符串) 进去(那是 UTF-16LE 字节)会得到 5 字节垃圾——实测 gbk2utf8(Blob('中文')) 返回 5 字节,而不是 6 字节的正确 UTF-8。
四、最小可运行示例
下面这个 NVO 是本文的完整示例对象 nvo_charset_io,五个方法分别负责:判定编码、字节转字符串、字符串转字节、读文件、写文件。直接把整段存成 nvo_charset_io.sru 导入即可,无需任何额外依赖(只用 uo_charset)。
使用演示与操作步骤:①新建 NVO,命名 nvo_charset_io,粘贴代码;②在任意窗口按钮里 nvo_charset_io lnv / lnv = create nvo_charset_io / lnv.of_demo();③of_demo() 会在当前目录写出 GBK / UTF-8 / UTF-16LE 三个文件再逐个读回,最后用 MessageBox 报告每步结果。
- forward
- global type nvo_charset_io from nonvisualobject
- end type
- end forward
- global type nvo_charset_io from nonvisualobject
- end type
- global nvo_charset_io nvo_charset_io
- type variables
- uo_charset ic_charset
- end variables
- forward prototypes
- public function string of_detect (blob ab_data)
- public function string of_blob_to_text (blob ab_data, string as_enc)
- public function blob of_text_to_blob (string as_text, string as_enc, boolean ab_bom)
- public function string of_load_text (string as_path, string as_enc)
- public function boolean of_save_text (string as_path, string as_text, string as_enc, boolean ab_bom)
- public subroutine of_demo ()
- end prototypes
- // 判定编码:返回 utf-8 / utf-16le / gbk,空数据返回空串
- public function string of_detect (blob ab_data);
- if not IsValid(ic_charset) then
- ic_charset = create uo_charset
- end if
- if IsNull(ab_data) then
- return ''
- end if
- if Len(ab_data) = 0 then
- return ''
- end if
- if ic_charset.IsUtf8Text(ab_data) then
- return 'utf-8'
- end if
- if ic_charset.IsUtf16Text(ab_data) then
- return 'utf-16le'
- end if
- return 'gbk'
- end function
- // 字节转字符串:as_enc 传空则自动判定;utf-16le 分支负责剥 BOM
- public function string of_blob_to_text (blob ab_data, string as_enc);
- string ls_text
- string ls_enc
- if not IsValid(ic_charset) then
- ic_charset = create uo_charset
- end if
- ls_text = ''
- ls_enc = Lower(Trim(as_enc))
- choose case ls_enc
- case 'utf-8'
- ls_text = String(ic_charset.CharsetConvert(ab_data, 'utf-8', 'utf-16le'))
- case 'utf-16le'
- ls_text = String(ab_data)
- if Len(ls_text) > 0 then
- if Left(ls_text, 1) = Char(65279) then
- ls_text = Mid(ls_text, 2)
- end if
- end if
- case else
- ls_text = String(ic_charset.CharsetConvert(ab_data, 'gbk', 'utf-16le'))
- end choose
- return ls_text
- end function
- // 字符串转字节:ab_bom 只在 utf-8 / utf-16le 下有效
- public function blob of_text_to_blob (string as_text, string as_enc, boolean ab_bom);
- blob lb_data
- blob lb_bom
- string ls_enc
- if not IsValid(ic_charset) then
- ic_charset = create uo_charset
- end if
- lb_data = Blob('')
- ls_enc = Lower(Trim(as_enc))
- choose case ls_enc
- case 'utf-8'
- lb_data = ic_charset.CharsetConvert(Blob(as_text), 'utf-16le', 'utf-8')
- if ab_bom then
- lb_bom = ic_charset.CharsetConvert(Blob(Char(65279)), 'utf-16le', 'utf-8')
- lb_data = lb_bom + lb_data
- end if
- case 'utf-16le'
- lb_data = Blob(as_text)
- if ab_bom then
- lb_data = Blob(Char(65279)) + lb_data
- end if
- case else
- lb_data = ic_charset.CharsetConvert(Blob(as_text), 'utf-16le', 'gbk')
- end choose
- return lb_data
- end function
- // 读文本文件:as_enc 传空则自动判定编码
- public function string of_load_text (string as_path, string as_enc);
- long ll_f
- long ll_n
- blob lb_data
- string ls_enc
- ll_f = FileOpen(as_path, StreamMode!, Read!)
- if ll_f < 0 then
- return ''
- end if
- ll_n = FileRead(ll_f, lb_data)
- FileClose(ll_f)
- if ll_n <= 0 then
- return ''
- end if
- ls_enc = as_enc
- if ls_enc = '' then
- ls_enc = of_detect(lb_data)
- end if
- return of_blob_to_text(lb_data, ls_enc)
- end function
- // 写文本文件:成功返回 true,落盘字节数与预期不符返回 false
- public function boolean of_save_text (string as_path, string as_text, string as_enc, boolean ab_bom);
- long ll_f
- long ll_n
- blob lb_data
- lb_data = of_text_to_blob(as_text, as_enc, ab_bom)
- ll_f = FileOpen(as_path, StreamMode!, Write!, LockWrite!, Replace!)
- if ll_f < 0 then
- return false
- end if
- ll_n = FileWrite(ll_f, lb_data)
- FileClose(ll_f)
- return (ll_n = Len(lb_data))
- end function
- // 自检入口:写三种编码的文件再逐个读回,最后弹窗报告
- public subroutine of_demo ();
- string ls_dir
- string ls_text
- string ls_back
- string ls_path
- string ls_rpt
- ls_dir = GetCurrentDirectory()
- ls_text = 'A中文,。'
- ls_rpt = '原文 ' + String(Len(ls_text)) + ' 字' + Char(13) + Char(10)
- ls_path = ls_dir + '\cs_gbk.txt'
- of_save_text(ls_path, ls_text, 'gbk', false)
- ls_back = of_load_text(ls_path, '')
- ls_rpt += 'GBK 往返一致:' + String(ls_back = ls_text) + Char(13) + Char(10)
- ls_path = ls_dir + '\cs_u8.txt'
- of_save_text(ls_path, ls_text, 'utf-8', false)
- ls_back = of_load_text(ls_path, '')
- ls_rpt += 'UTF-8 往返一致:' + String(ls_back = ls_text) + Char(13) + Char(10)
- ls_path = ls_dir + '\cs_u16.txt'
- of_save_text(ls_path, ls_text, 'utf-16le', true)
- ls_back = of_load_text(ls_path, '')
- ls_rpt += 'UTF-16LE+BOM 往返一致:' + String(ls_back = ls_text) + Char(13) + Char(10)
- ls_rpt += '判定结果:' + of_detect(of_text_to_blob(ls_text, 'gbk', false)) + ' / ' + of_detect(of_text_to_blob(ls_text, 'utf-8', false))
- MessageBox('字符集自检', ls_rpt)
- end subroutine
- on nvo_charset_io.create
- call super::create
- TriggerEvent( this, "constructor" )
- end on
- on nvo_charset_io.destroy
- call super::destroy
- TriggerEvent( this, "destructor" )
- end on
复制代码说明:示例用 GetCurrentDirectory() 拼出三个测试文件路径。实测时该对象以完全相同的逻辑运行,唯一差别是末尾用 FileWrite 把报告写进文件而不是弹 MessageBox——因为无界面测试环境无法弹窗。
五、CharsetConvert 的四个真相
5.1 只有三个名字真能用
拿 UTF-16LE 的「中文」逐名试 24 个候选目标字符集,读数分成两堆:
| 名字 | 输出长度 | 结论 | | gbk | 4 | 可用 | | gb18030 | 4 | 可用,但字节与 gbk 完全相同 | | utf-8 / UTF-8 / utf8 | 6 | 可用(大小写与连字符都宽容) | | GBK | 0 | 失败(大小写不宽容,与 utf-8 相反) | | gb2312 / GB2312 | 0 | 失败 | | cp936 / CP936 / 936 | 0 | 失败(代码页号不能当名字用) | | big5 / big-5 / BIG5 / BIG5-2003 / BIG5-HKSCS / cp950 | 0 | 失败(繁体中文无路可走) | | utf-16 / utf-16be / ucs-2 / utf-32le | 0 | 失败(只有小端 UTF-16 可用) | | ascii / latin1 / iso-8859-1 / gb / 空串 / nosuch-cs | 0 | 失败 |
注意这张表的方向性:utf-16le 作为源是有效的(utf-16le→gbk 实测 4 字节正确),作为目标也有效(gbk→utf-16le 实测 4 字节、String() 还原正确);但写成 utf-16(不带 le)两个方向都失败。
GetCharsetList() 列出的 138 项看着很像「可用字符集清单」,实际是 Windows 代码页清单:936 是「简体中文 GBK」、54936 是「GB18030 简体中文」、950 是「繁体中文 Big5」、65001 是「UTF-8」。这些 code 与 name 都不能直接传给 CharsetConvert——传 936 实测返回 0。清单的用途是给界面做下拉框,不是给转换做参数。
5.2 同名转换返回空
CharsetConvert(x, 'gbk', 'gbk')、('utf-8','utf-8')、('utf-16le','utf-16le') 三种写法实测全部返回 0 长度。这意味着它不能当「规范化/清洗」工具用:想过滤一遍非法字节再原样写回,必须换别的路子(先转成字符串再转回字节)。
5.3 失败长什么样:0 长度 + 不报错 + 不为 NULL
这是最要命的一点。名字写错、方向写错、编码不匹配,得到的都是同一个结果:Len() 为 0 的 blob,IsNull() 为 false,没有任何错误码或异常。所以判断成败只有一条路:
- blob lb_out
- string ls_err
- lb_out = lc_charset.CharsetConvert(lb_src, 'utf-8', 'gbk')
- if Len(lb_out) = 0 then
- ls_err = '转换失败:检查字符集名字是否在 gbk / utf-8 / utf-16le 之内'
- end if
复制代码
另一类「静默失败」更隐蔽:方向搞错。把 GBK 字节当 UTF-8 转(CharsetConvert(gbk_blob, 'utf-8', 'utf-16le')),实测返回 8 字节、Asc() 得到 -3 的乱码字符串,既不报错也不失败,只是内容全错。所以「转出来有内容」不等于「转对了」,长度和内容都要验。
5.4 「gbk」会随 SetCodePage 漂移
同一个进程里按顺序改代码页,每步都转一次「中文」到 gbk:
| 步骤 | 调用 | 输出字节 | 实际编码 | | 0 | 默认 | D6 D0 CE C4 | GBK | | 1 | SetCodePage(65001) | E4 B8 AD E6 96 87 | UTF-8 | | 2 | SetCodePage(950) | A4 A4 A4 E5 | Big5 | | 3 | SetCodePage(936) | D6 D0 CE C4 | GBK(恢复) | | 4 | SetCodePage(0) | D6 D0 CE C4 | GBK(系统默认) |
结论很清楚:'gbk' 这个名字实际代表「当前 ANSI 代码页」,而不是硬编码的 936。谁在程序某处调过 SetCodePage,后面所有 'gbk' 转换就跟着变。多线程/多模块的大型工程里,这条足以制造「同一份代码在不同机器写出不同字节」的灵异现象。稳的做法:启动时显式 SetCodePage(936) 一次,或者干脆只用 utf-8 落盘、只在读旧数据时用 gbk。
顺便记下:SetCodePage 是这一族方法里唯一会返回 false 的——传 99999 返回 false;而 936 / 65001 / 0 / 950 都返回 true。
六、判定编码:三个嗅探器各管什么
八个典型 blob 同时喂给 IsUtf8Text 与 IsUtf16Text:
| 数据 | 字节数 | IsUtf8Text | IsUtf16Text | | UTF-16LE 的「中文」 | 4 | N | N | | GBK 的「中文」 | 4 | N | N | | UTF-8 的「中文」 | 6 | Y | N | | ASCII abcd | 4 | Y | Y | | 空 blob | 0 | N | N | | 含 0 字节的数据 | 4 | Y | Y | | 被截断的 UTF-8(6 字节取前 4) | 4 | N | N | | UTF-16LE 带 BOM 的「中文」 | 6 | N | Y |
三条可直接落地的判断:
- IsUtf8Text 是校验器,不是「有没有高位字节」的猜测——被截断的 UTF-8 它判 N,说明它真的在验续字节合法性。可以信。
- IsUtf16Text 是启发式:只要数据里出现 0 字节、或带 BOM,它就判 Y。所以 abcd(UTF-16LE 下是 61 00 62 00)判 Y,纯中文的 UTF-16LE(2D 4E 87 65,一个 0 字节都没有)判 N。
- 因此顺序很重要:先问 IsUtf8Text,再问 IsUtf16Text,都不是就当 GBK。反过来的话,ASCII 内容会被误判成 UTF-16。
6.1 AutoConvert 的盲区
AutoConvert 看着像「一把梭」,实测只对 UTF-8 可靠。四个文件逐个读进 blob 再 AutoConvert:
| 文件 | 字节数 | IsUtf8Text | IsUtf16Text | AutoConvert 结果 | | UTF-8 无 BOM(4 字) | 12 | Y | N | 4 字,正确 | | UTF-8 带 BOM(4 字) | 15 | Y | N | 4 字,正确(BOM 被吃掉) | | GBK(4 字) | 8 | N | N | 2 字,错 | | UTF-16LE 带 BOM(4 字) | 10 | N | Y | 5 字,错(BOM 变成一个字符) | | UTF-16BE 带 BOM(4 字) | 10 | N | N | 4 字但首字是 ?,错 |
GBK 那行尤其坑:8 字节解出 2 个字符,Asc() 还是 20013(看着像「中」),肉眼不查长度很难发现。结论:AutoConvert 只能用在「确定是 UTF-8」的场景,做通用解码器必翻车。
6.2 有一种 UTF-16LE 根本判不出来
纯中文、无 BOM、且没有 ASCII 字符的 UTF-16LE 数据(如「中文测试」8 字节:2D 4E 87 65 79 6B D5 8B),IsUtf8Text 判 N、IsUtf16Text 也判 N——它会掉进「当 GBK 处理」的分支,转出 6 个字符的乱码,Asc() 首字是 45。
这不是缺陷,而是信息不足:4 字节的 2D 4E 87 65 在 GBK 与 UTF-16LE 下都「合法」,没有任何字节特征能区分。工程上的正解只有一条:编码由上下文决定,不由数据决定。读自己写出的文件就记下编码;读外部文件就在配置文件/界面上让用户选;实在要猜,就只在「有 BOM」或「含 ASCII 字符」时猜 UTF-16,其余按 GBK。
6.3 Have\*Text:用已知词反查字节形态
HaveAnsiText / HaveUtf8Text / HaveUtf16Text 的语义是「这段 blob 里,是否存在以 ANSI / UTF-8 / UTF-16 编码的某个词」。三个探针分别喂 GBK、UTF-8、UTF-16LE 三种 blob:
| blob | HaveAnsiText(「中文」) | HaveUtf8Text(「中文」) | HaveUtf16Text(「中文」) | | GBK 字节 | Y | N | N | | UTF-8 字节 | N | Y | N | | UTF-16LE 字节 | N | N | Y |
三列互斥、判定干净。实战价值:当你知道文件里必然出现某个固定词(表头、<?xml、「Config」之类),用它比 IsUtf8Text 更精准——能直接分清「ANSI 还是 UTF-8 还是 UTF-16」,不受字节合法性启发式影响。这是本文给出的第二个嗅探器,两者配合基本能覆盖全部实际场景。
七、BOM 的三种下场
同一个 BOM(U+FEFF)出现在源数据里,往不同方向转,结果完全不同:
| 源 | 目标 | 结果字节 | BOM 去哪了 | | UTF-8 带 BOM | gbk | D6 D0 CE C4 B2 E2 CA D4(8 字节) | 被丢弃,正文干净 | | UTF-8 带 BOM | utf-16le | 4 字符,首字符就是「中」 | 被丢弃 | | UTF-16LE 带 BOM | gbk | 3F D6 D0 CE C4 B2 E2 CA D4(9 字节) | 变成一个问号 ?(0x3F) | | UTF-16LE 带 BOM | utf-8 | EF BB BF + 正文(15 字节) | 保留成 UTF-8 BOM |
也就是说:从 utf-8 源转出时 BOM 会被吃掉,从 utf-16le 源转出时 BOM 会被当成普通字符。凡是读 UTF-16LE 文件,转码后必须自己判一次首字符:
- string ls_text
- ls_text = String(lb_data)
- if Len(ls_text) > 0 then
- if Left(ls_text, 1) = Char(65279) then
- ls_text = Mid(ls_text, 2)
- end if
- end if
复制代码
Char(65279) 就是 U+FEFF,实测可用(Len() 返回 1)。同理,想生成 BOM 也可以用它:CharsetConvert(Blob(Char(65279)), 'utf-16le', 'utf-8') 实测得到正好 3 字节 EF BB BF,Blob(Char(65279)) 得到 2 字节 FF FE。这比自己拼字节常量干净得多。
八、写文件的正确姿势
四种目标编码,一个函数 of_text_to_blob() 全覆盖,落盘字节实测如下(内容 A中文,。,5 字符):
| 目标 | BOM | 字节数 | 首字节 | 说明 | | gbk | 无 | 9 | 41 | 41 D6 D0 CE C4 A3 AC A1 A3,最省空间 | | utf-8 | 无 | 13 | 41 | 41 E4 B8 AD ... | | utf-8 | 有 | 16 | EF BB BF | 13 + 3 字节 BOM,Excel/记事本友好 | | utf-16le | 有 | 12 | FF FE | 10 + 2 字节 BOM,等宽但体积翻倍 |
写文件时牢记两点:①FileOpen 必须用 StreamMode! + Write! + Replace!,LineMode! 会自动加行尾并把内容按行切断,二进制就毁了;②FileWrite(handle, blob) 返回实际写入字节数,拿它跟 Len(blob) 比对才是真正的「写成功」判据,FileOpen 成功不代表写成功。
读文件同理用 StreamMode!;FileRead(handle, blob) 直接把整个文件读进 blob(局部 blob 变量即可,不需要 ref),实测 12 / 15 / 8 / 10 字节的文件 FileRead 返回值与 Len() 完全一致。
九、截断与坏字节:两个替换符
网络包不完整、文件被截断时,转换器不会抛错,而是塞替换符:
| 输入 | 转换 | 输出 | 说明 | | UTF-8 6 字节取前 4(半个「文」) | → utf-16le | 2D 4E FD FF = 「中」+ U+FFFD | 半截序列变替换字符 | | GBK 4 字节取前 3(半个「文」) | → utf-8 | E4 B8 AD 3F = 「中?」 | 半截字节变 ? | | UTF-16LE 5 字节(多出 1 字节) | → gbk | 3F D6 D0 = 「?中」 | 残缺的首单元变 ? | | 空 blob | → utf-8 | 0 字节,IsNull() = false | 空进空出,不算失败 |
所以「长度对不对」是第一道校验,「有没有 ? 或 U+FFFD」是第二道。对不允许丢字的场景(比如导入财务数据),必须两刀都切。
十、GB18030 四字节汉字
GB18030 相对 GBK 的核心增量是四字节区(Unicode 扩展 B 及以后)。实测 U+20000(UTF-8 下 F0 A0 80 80,UTF-16LE 下是代理对 40 D8 00 DC):
| 目标 | 是否调 SetGB18030(true) | 输出 | 结论 | | gbk | 否 | 3F 3F(两个问号) | 字符丢失 | | gb18030 | 否 | 3F 3F | 同样丢失——名字没起作用 | | gbk | 是 | 95 32 82 36 | 正确的 GB18030 四字节 | | gb18030(回读) | 是 | 4 字节 UTF-8,可还原 | 往返无损 |
两条结论:①gb18030 这个名字在默认状态下等价于 gbk,别指望改个名字就支持四字节;②必须先 SetGB18030(true)(无返回值,是 subroutine),之后的 gbk 目标才真的走 GB18030 表。这个开关是进程级全局状态,和 SetCodePage 一样要在程序入口统一设置,不要在函数里随手改。
十一、清单、查字与字体
- GetCharsetList() 返回 uo_json 根对象,ToString() 长度 5456 字符(UTF-8 落盘 6492 字节),138 项,形如 {"0":{"code":"10000","name":"罗马"}, ...}。注意 ArrCount() 返回 0、IsArray() 为 false——它是对象不是数组,遍历要用键名 0..137,不能按数组走。
- GetCharByUnicode(long):20013 → 「中」(Len() = 1,Asc() = 20013);65 → A;0 → 空串(Len() = 0);-1 → 一个字符且 Asc() 返回 -1;65535 → 一个字符,Asc() 返回 -1(因为 Asc() 按有符号 16 位返回,65535 溢出成 -1,这一点必须记住,判断大于 32767 的码点别用 Asc());非 BMP 的 128512(U+1F600)返回 1 个字符但 Asc() 是乱值;冗余代理 55296 也返回 1 个字符。
- 字体三件套:GetDefaultFont(ref string, ref int) 实测取到 Microsoft YaHei UI / 9;SetDefaultFont('SimSun') 返回 true 且 GetDefaultFont 随即反映 SimSun。它影响 PBIDEA 控件默认字体,与字符集转换无关,别混在同一个初始化块里。
十二、和相近方案怎么分工
| 需求 | 用哪个 | 理由 | | PB 字符串 → UTF-8/ANSI 字节 | uo_string.ToUTF8() / ToAnsi() | 一步到位,不用先 Blob() | | UTF-8 字节 → PB 字符串 | uo_string.FromUTF8() | 返回 string(不是 blob),语义最省事 | | 任意字符集互转、读文件判编码 | uo_charset | 只有它能做 GBK ↔ UTF-16LE 双向 | | UTF-16LE 字节 → 字符串 | String(blob)(PB 原生) | 不需要组件参与,等价于按 UTF-16LE 解释 | | 字符串 → UTF-16LE 字节 | Blob(string)(PB 原生) | 同上 | | DataWindow 导出时指定编码 | SaveAsFormattedText(..., EncodingUTF8!) | 那是导出通道自己的参数,与 uo_charset 无关 |
一句话记法:PB 原生只管 UTF-16LE 这一端,uo_string 管 UTF-8/ANSI 的便捷方法,uo_charset 管「任意两种字符集之间」。
十三、坑清单(16 条)
- CharsetConvert 只认 gbk / utf-8 / utf-16le(+ 默认等价的 gb18030),其余名字一律返回 0 长度,不报错。
- 大小写不统一:utf-8/UTF-8/utf8 都行,GBK 不行、UTF8 不行。别问规律,照抄小写最稳。
- 代码页号(936 / 54936 / 950 / 65001)不能当字符集名传进去。
- 同名互转返回空 blob;想做「复制」请自己 Blob()。
- 判断成败只能看 Len(),没有错误码;IsNull() 恒为 false。
- 方向写错不会报错,只会得到乱码(GBK 当 UTF-8 转实测 8 字节、Asc() = -3)。
- 'gbk' 是「当前 ANSI 代码页」的影子,SetCodePage(65001) 会让它输出 UTF-8 字节、SetCodePage(950) 让它输出 Big5 字节。
- SetCodePage 必须在程序入口定一次,别在业务函数里改。
- AutoConvert 不识 GBK(4 字解成 2 字),也不识 UTF-16LE(BOM 变成一个字符)。
- IsUtf16Text 只认「含 0 字节 / 带 BOM」,纯中文无 BOM 的 UTF-16LE 判 N。
- 判定顺序必须 IsUtf8Text → IsUtf16Text → 否则 GBK,反过来 ASCII 会被误判成 UTF-16。
- UTF-16LE 源里的 BOM 不会被吃掉:转 GBK 变 ?、转 UTF-8 变 EF BB BF,必须自己剥。
- Char(65279) 可用来剥/造 BOM,但 Asc() 对大于 32767 的码点返回负数(Asc(Char(65279)) = -257)。
- gb18030 名字默认等价 GBK,四字节汉字要先 SetGB18030(true),否则变 3F 3F。
- 截断数据被静默替换为 ? 或 U+FFFD,不抛异常;导入类业务必须查 ?。
- FileOpen 用 LineMode! 会把二进制内容按行切断;读写字节一律 StreamMode!。
十四、PB12.5 差异
与 PB10 行为一致。 本文全部 20 组探针在 PB10 与 PB12.5 上分别 import、全量编译、运行,两侧诊断输出逐字相同(20 个对象 × 2 个版本,共 40 次运行,零差异):转换字节、BOM 处理、代码页漂移、截断替换符、GB18030 开关、GetCharsetList 条目数全部一致。
需要留意的只有环境侧两点:①PbIdea.dll 要配对应位数的 PB 运行时(32 位 PB 用 32 位 DLL);②uo_charset 属 websuite.pbl,编译时该库必须在库列表里,否则报 Unresolved external 之类的外部引用错误。
十五、实机验证记录
| 项 | PB10 基准 | PB12.5 实测 | | 源码导入 | 20 个对象,0 错误 | 20 个对象,0 错误 | | 全量编译(full rebuild) | 0 错误,21 个对象齐全 | 0 错误,21 个对象齐全 | | 运行验证 | 20/20 PASS | 20/20 PASS | | 双轨读数比对 | 诊断输出逐字一致(0 处差异) | 同左 | | 编码检查 | 全 GBK / CRLF / 无 BOM / 带导出头,0 异常 | 同左 | | 原型一致性 | 声明与实现逐对象对齐,0 错 0 提醒 | 同左 |
探针清单(对象名 → 覆盖内容):p01 转换主干与字节形态 · p02 两个嗅探器矩阵 · p03 Have\*Text 三列 · p04 字符集清单 · p05 GetCharByUnicode 九组码点 · p06 AutoConvert 盲区 · p07 八个包装函数与 big5/utf-16be · p08 24 个别名与失败形态 · p09 代码页与字体 · p10 五个真实文件读取 · p11 3×10 方向矩阵 · p12 GB18030 四字节 · p13 清单 UTF-8 落盘 · p14/p17 BOM 构造与三种下场 · p15/p19 代码页数字名与漂移顺序 · p16 截断与坏字节 · p18 无 BOM 歧义 · 演示对象 nvo_charset_io 五项往返。
本文所有代码均为实机编译 + 运行通过;MessageBox 版本与实测版本仅输出语句不同(无界面环境用 FileWrite 落盘读数),逻辑完全一致。 |