马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
本帖最后由 pbai 于 2026-10-3 06:57 编辑
PBIDEA:用 uo_clipboard 补齐 PowerBuilder 的剪贴板能力(文件列表、格式信息与变化监听,PB10 基准 · PB12.5 实测)
阅读说明
1. 适用版本:基准 PB10,兼容 PB 12.5(PBIDEA 支持 PB 8 及以上)
2. 支持数据库:本文不涉及数据库
3. 操作系统与环境要求:Windows XP 及以上(本文实测环境 Windows 10),工程中需引用 websuite.pbl 并分发 PbIdea.dll(32 位)
4. 难度系数:★★☆☆☆
5. 其它阅读说明:会用到 PBIDEA 的 uo_blob 做 hex 对照,如不熟悉可先读《PBIDEA:用 uo_blob 补齐 PowerBuilder 的二进制处理短板》;文中所有读数均为本机实机双轨(PB10 + PB12.5)实测值 【版本声明】
- 本文以 PBIDEA 当前最新版本为准(版本源由维护方统一核验),并对照随版发布的源码整理。
- 核验日期:2026-10-03;当次核验的 PBIDEA 核心运行库:18,105,344 字节 / 2544 个导出,md5 4f49718e4460…。
- 版本识别口径:逐项解析 PE 导出表(文件体积、目录名都不能作为判据)。
- 适用平台:Windows XP 及以上(XP / 7 / 10 / 11 均可);PowerBuilder 8 及以上(PB 8/9/10/10.5/11.x/12.x 及以上通用)。本文的实机验证环境为 Windows 10 + PB 10 与 PB 12.5。
- PBIDEA 版本会持续更新,本文只对上述核验时点的最新版本负责;后续若行为有变,以新版为准。
一、原生 Clipboard() 只能做一半
PowerBuilder 自带一个全局函数 Clipboard(),但它只能干两件事:把一段文本放进剪贴板,或者把剪贴板里的文本读出来。你要是想让用户在资源管理器里直接"粘贴文件",或者想知道剪贴板里现在到底有哪些格式、有没有图片,原生函数就无能为力了。
PBIDEA 的 uo_clipboard 把这半边天补齐了:文本、文件列表(CF_HDROP)、格式清单、指定格式的裸数据、剪贴板变化监听,一共 10 个方法加 1 个 change 事件,全部走 PbIdea.dll。这篇文章把每个成员的实测行为过一遍——包括好几个源码注释里没写、只有跑起来才能发现的坑。
先给一个全貌表,后文逐个展开:
| 成员 | 签名 | 作用 | | SetClipboard | SetClipboard(text) 返回 long | 放文本(成功返 1) | | SetClipboard | SetClipboard(textOrFilename, isFilename) 返回 long | 放文本或文件名 | | SetClipboard | SetClipboard(data) 返回 long | 放 blob(按 ANSI 文本解释,见坑 8) | | FetchClipboard | FetchClipboard(ref blob data) 返回 long | 取回内容:1=文本 2=图片 3=文件列表 | | GetClipBoardFileList | GetClipBoardFileList() 返回 string | 文件列表(分号分隔) | | GetClipBoardFileList | GetClipBoardFileList(ref string files[]) 返回 int | 文件列表装进数组,返回条数 | | SetClipBoardFileList | SetClipBoardFileList(files[]) 返回 boolean | 把文件列表放上剪贴板(返回值不可信,见坑 1) | | GetClipboardInfo | GetClipboardInfo() 返回 string | JSON 格式清单 | | GetClipboardData | GetClipboardData(fmt) 返回 blob | 指定格式的裸数据 | | StartClipboardWatch | StartClipboardWatch() 返回 boolean | 开始监听剪贴板变化 | | StopClipboardWatch | StopClipboardWatch() 返回 boolean | 停止监听 | | change 事件 | change() | 监听期间剪贴板有变化时触发 |
二、文本往返:SetClipboard 与 FetchClipboard
最常用的路径。写入用 SetClipboard(string),读出用 FetchClipboard(ref blob)——注意它返回的不是字符串而是 blob,返回值本身表示类型:1 是文本,2 是图片,3 是文件列表(第 3 个值源码注释里没有,是实测出来的)。
- // 演示:文本写入剪贴板再取回(u_clip_demo 为按钮 clicked 事件内的局部流程)
- long ll_r, ll_type
- blob lb_data
- string ls_text
- uo_clipboard lcb
- // 输入:要放进剪贴板的文本
- ls_text = 'PBIA2026剪贴板测试' // 待写入的文本内容
- lcb = create uo_clipboard
- // 写入剪贴板,成功返回 1
- ll_r = lcb.SetClipboard(ls_text)
- // 取回:返回值 1=文本(2=图片,3=文件列表),内容装进 lb_data
- ll_type = lcb.FetchClipboard(lb_data)
- // blob 直接转 string 即可还原原文,中文不会乱
- MessageBox('文本往返', 'SetClipboard 返回=' + String(ll_r) + &
- '~n类型=' + String(ll_type) + &
- '~n取回长度=' + String(Len(lb_data)) + ' 字节' + &
- '~n内容=[' + String(lb_data) + ']')
- destroy lcb
复制代码
实测读数(PB10 与 PB12.5 完全一致):SetClipboard 返回 1;FetchClipboard 返回 1;blob 长度 26 字节;String(lb_data) 直接得到原文,中文不乱。
26 这个数字值得多说一句。'PBIA2026剪贴板测试' 是 13 个字符,13 × 2 = 26——FetchClipboard 给出的文本 blob 是 UTF-16LE 编码、不带 NUL 结尾。我用 uo_blob 的 hex() 逐字节看过:
- 500042004900410032003000320036006a52348d7f674b6dd58b
复制代码
每两个字节一个字符,'PBIA2026' 的 ASCII 后面跟着 6a52(剪)348d(贴)7f67(板)4b6d(测)58d5+6d58(试,U+8BD5 的小端是 d58b)……精确对应。PB10 和 PB12.5 的 string 内部都是 UTF-16,所以 String(blob) 一步到位还原,不需要 FromUTF8 之类的转码函数。
顺手记一个反面教材:如果你拿这个 blob 去 FromUTF8,只会得到一个字符 'U'——因为 FromUTF8 按字节读,读到第二个字符的低字节 0x00 就停了。编码判断错了,工具越准死得越快。
三、格式清单 GetClipboardInfo 与裸数据 GetClipboardData
想知道剪贴板里现在有什么,用 GetClipboardInfo(),它返回一段 JSON:
- // 演示:查看当前剪贴板的格式清单
- string ls_info
- uo_clipboard lcb
- lcb = create uo_clipboard
- // 先放一段文本,让剪贴板有内容
- lcb.SetClipboard('PBIA2026剪贴板测试')
- // 取格式清单 JSON
- ls_info = lcb.GetClipboardInfo()
- MessageBox('格式清单', ls_info)
- destroy lcb
复制代码
实测输出(放文本之后):
- {"count":4,"fmts":[
- {"format":16,"name":"CF_LOCALE","handle":7956904,"size":4},
- {"format":13,"name":"CF_UNICODETEXT","handle":7863384,"size":66},
- {"format":7,"name":"CF_OEMTEXT","handle":7856208,"size":38},
- {"format":1,"name":"CF_TEXT","handle":7856160,"size":38}],
- "priority":16}
复制代码
Windows 放文本时会同时挂好几个格式,程序按优先级取用。这里的 format 数字就是 Win32 的 CF_ 常量,常用的几个列一下,用 GetClipboardData 取裸数据时按号取:
| 常量 | 值 | 含义 | | CF_TEXT | 1 | ANSI 文本 | | CF_OEMTEXT | 7 | OEM 字符文本 | | CF_DIB | 8 | 设备无关位图 | | CF_UNICODETEXT | 13 | UTF-16 文本 | | CF_ENHMETAFILE | 14 | 增强图元文件 | | CF_HDROP | 15 | 文件列表 |
GetClipboardData(13) 拿 UTF-16 文本的裸数据时,实测长度是 66 字节,而 FetchClipboard 只有 26——GetClipboardData 返回的是整个 GlobalAlloc 分配块,尾部带着一串 0 填充;CF_TEXT 拿到 38 字节,内容是 GBK 编码再加填充。要精确内容用 FetchClipboard,要看原始格式(比如判断文件列表的结构)才用 GetClipboardData。还有一点:向一个不存在的格式要数据,比如剪贴板里只有文本时 GetClipboardData(15),返回一个长度为 0 的空 blob,不报错——这个行为可以放心用。
四、文件列表:让用户能"粘贴文件"
文件列表走的是 CF_HDROP 格式,也就是资源管理器"复制文件"用的那个。写用 SetClipBoardFileList,读用 GetClipBoardFileList(两个重载)。
- // 演示:把一个文件放上剪贴板,再读回验证
- string ls_files[], ls_readback[]
- int li_n
- boolean lb_ok
- uo_clipboard lcb
- // 输入:要复制的文件全路径(单文件最可靠,原因见坑 2)
- ls_files[1] = 'C:\Windows\win.ini'
- lcb = create uo_clipboard
- // 写入文件列表(实测返回值恒为 false,但写入是成功的,别用它判断)
- lb_ok = lcb.SetClipBoardFileList(ls_files)
- // 读回:数组版,返回条数
- li_n = lcb.GetClipBoardFileList(ls_readback)
- // 字符串版,多文件时用分号分隔
- MessageBox('文件列表', 'SetClipBoardFileList 返回=' + String(lb_ok) + &
- '~n读回条数=' + String(li_n) + &
- '~n读回字符串=[' + lcb.GetClipBoardFileList() + ']')
- destroy lcb
复制代码
PB12.5 实测:lb_ok=false 但写入成功,读回条数 1,字符串正好是 'C:\Windows\win.ini'。此时 FetchClipboard 返回 3(文件列表),不是 1 也不是 2。
这里集中说一下文件列表的三个坑,都是实机撞出来的:
坑 1:返回值不可信。 SetClipBoardFileList 声明返回 boolean,但两版实测写入成功也返回 false。要判断成败,写完读回来对一下首项,别信返回值。
坑 2:多文件只有第一项可靠。 放两个文件进去读回,PB12.5 只回来 1 条;换三个文件、固定数组和变长数组各试一遍,读回条目数对不上、路径内容错乱。想给用户一批文件,要么逐次单文件,要么打个 zip 再放一个文件,别指望多元素列表。
坑 3:PB10 读回会拖垃圾项。 同样的单文件代码,PB10 下读回条数是 2 到 3,首项正确,后面跟着的是进程内存里的垃圾(实测拖出过 'LL' 和加载模块名 FONTSUB.DLL 这种东西)。PB12.5 下读回是干净的 1 条。所以读回之后只信你写入的那一项,或者按已知路径做白名单校验,条数本身不要当真。
裸数据层面我也看过 CF_HDROP:标准 DROPFILES 头 20 字节 + 宽字符路径 + 双 NUL 结束。不过它头部 fWide 标志与数据形态不一致,跨程序读这个格式是否正常没验证过,实际业务建议只用 GetClipBoardFileList 读回,别自己去啃裸块。
五、变化监听:StartClipboardWatch 与 change 事件
uo_clipboard 内置监听:StartClipboardWatch() 之后,剪贴板一有变化就触发 change 事件。change 定义在 uo_clipboard 上,业务逻辑写在子类里最干净:
- // 前置说明:监听逻辑写在子类的 change 事件里。
- // 先建一个标准可视/非可视类用户对象 nvo_clip_watch,祖先选 uo_clipboard,
- // 在它的 change 事件里写触发后的处理逻辑(此处计数并记录时间)。
复制代码- // 对象:nvo_clip_watch(继承 uo_clipboard)
- // 实例变量
- long ii_cnt = 0 // change 事件触发次数
- // change 事件脚本
- ii_cnt++
- // 这里写你的业务:记录日志、刷新界面、分析内容等
复制代码- // 演示:启动监听,连续写 3 次看事件触发
- nvo_clip_watch lw
- long ll_i, ll_r
- lw = create nvo_clip_watch
- // 开始监听,成功返回 true
- IF lw.StartClipboardWatch() THEN
- // 连续写 3 次,每次写完泵一下消息循环给事件派发留机会
- FOR ll_i = 1 TO 3
- ll_r = lw.SetClipboard('监听第' + String(ll_i) + '次')
- FOR ll_r = 1 TO 100
- Yield()
- NEXT
- NEXT
- MessageBox('监听结果', '3 次写入共触发 change 次数=' + String(lw.ii_cnt))
- lw.StopClipboardWatch()
- ELSE
- MessageBox('监听结果', 'StartClipboardWatch 失败')
- END IF
- destroy lw
复制代码
实测两版一致:StartClipboardWatch 返回 true,写 3 次触发 3 次 change,1:1 不丢不并;StopClipboardWatch 返回 true。两点说明:一是事件派发依赖消息循环被泵,如果你的长任务里不 Yield,事件会攒着等到循环空下来才来;二是 uo_clipboard 的 destructor 会自动 StopClipboardWatch,忘了手动停也不会漏钩子,但监听期间对象必须活着,destroy 了监听就停了。
至于源码注释里说的"返回 2 时 data 里是一张图的内容,可以 p_1.SetPicture(data)"这条图片通路——需要真实图片进剪贴板(比如别的程序里复制截图),headless 测试环境构造不出来,本段未实测、仅静态核对,读者用到时以 SetPicture 实际效果为准。
六、边界行为:空串、blob 重载与"没有清空"
两个边界实测:
空串写入是失败而不是清空。 SetClipboard('') 返回 0,且剪贴板保持原内容不动(实测写空串前是 '监听第3次',写后取回还是它)。PBIDEA 没有暴露清空剪贴板的方法,要"清"只能放一个占位内容盖掉。
blob 重载不是二进制通道。 SetClipboard(blob) 这个重载会把 blob 按 ANSI 文本解释、遇到 0x00 截断。实测放一个 'BIN数据' 的 blob 进去(首字节 0x42 即 'B',第二字节是 0x00),取回来只剩一个字符 'B'。真想放二进制数据,先转 hex 或 base64 字符串再放,两边约定好解码。
七、与原生方案对比
| 能力 | 原生 Clipboard() | uo_clipboard | | 写/读文本 | 支持(只此一项) | 支持,且 blob 形态带类型标记 | | 文件列表(可粘贴文件) | 不支持 | 支持(单文件可靠) | | 格式清单 | 不支持 | GetClipboardInfo JSON | | 指定格式裸数据 | 不支持 | GetClipboardData | | 变化监听 | 不支持 | Start/Stop + change 事件 | | 依赖 | 无 | PbIdea.dll |
简单说:只倒腾文本,原生函数最省事;一旦涉及文件、格式、监听,就得上 uo_clipboard。
八、坑清单(全部实测)
- SetClipBoardFileList 返回值恒 false 但写入成功——判断成败只能读回验证。
- 多文件列表只有第一项可靠,读回条目数与内容错乱(固定数组、变长数组、2 个与 3 个文件都复现)。
- PB10 下文件列表读回拖垃圾项(实测 N=3、N=2),PB12.5 干净(N=1)——读回后只信首项或做白名单校验。
- FetchClipboard 返回 3 = 文件列表,源码注释只写了 1 和 2。
- 文本 blob 是 UTF-16LE 无 NUL,26 字节 = 13 字符;别用 FromUTF8 解它(会在 0x00 处截断)。
- GetClipboardData 返回整块分配含 0 填充(66 字节),FetchClipboard 才是精确内容。
- SetClipboard('') 返回 0 且不清空剪贴板,旧内容原样保留;组件没有清空 API。
- SetClipboard(blob) 按 ANSI 文本解释遇 0x00 截断(7 字节 blob 只出 'B'),不是二进制通道。
- change 事件派发依赖消息循环被泵,长任务中要留 Yield;监听对象 destroy 前最好手动 Stop(destructor 会兜底)。
- 向未占用的格式要数据返回空 blob 不报错,可以放心探测。
九、实机验证情况
文中代码在本机做了 PB10 与 PB12.5 双轨实机验证,测试对象 nvo_clip_demo + 子类 nvo_clip_watch + uo_blob 对照,共 22 项断言:
- PB10:--pb-version 100 全量编译 0 错误;PBVM 运行 PASS,22/22 断言全中。
- PB12.5:--pb-version 125 全量编译 0 错误;PBVM 运行 PASS,22/22 断言全中。
- 两版输出串逐项比对:除文件列表读回条数(T4_N:125=1,100=3;T5_N:125=1,100=2)外,其余含文本 blob 的 hex 逐字节完全一致。
- check_pb125.py 0 错误(1 条提醒为帮助知识库未收录 uo_clipboard 的 change 事件,已对照 PBIDEA 导出源码核实为真实事件);check_enc.py 编码全 OK,bad=0。
- 图片通路(FetchClipboard 返 2 + SetPicture)未实测,见第五节标注。
十、PB12.5 差异
逐项对照后,两版行为差异只有一处:
- 文件列表读回条数:PB12.5 读回干净(写入 N 项……实测单文件读回 1 条);PB10 读回拖垃圾项(单文件读回 2~3 条,尾部是内存垃圾)。写入侧(HDROP 格式确实在板上、首项正确)两版一致。
其余成员——SetClipboard 返回值、FetchClipboard 类型码与 blob 编码、GetClipboardInfo JSON 结构、GetClipboardData 填充行为、监听事件计数、空串与 blob 重载边界——PB12.5 与 PB10 行为一致,未发现差异。
十一、小结
uo_clipboard 是那种"平时想不起来,用时发现原生根本不行"的组件。文本往返、格式探测、文件列表、变化监听四条通路,本机双轨都跑通了,读数也稳。真正要留神的是文件列表:返回值别信、多文件别堆、PB10 读回要过滤——记住这三条,剩下的按表用就行。
|