祝愿大家身体健康!

 站点注册  找回密码
 站点注册

QQ登录

只需一步,快速开始

查看: 68|回复: 1

[学习笔记] PowerBuilder DataWindow 导入导出全解:ImportString / ImportFile / SaveAs 全格式实测(PB10 基准 · PB12.5 实测)

[复制链接]

[学习笔记] PowerBuilder DataWindow 导入导出全解:ImportString / ImportFile / SaveAs 全格式实测(PB10 基准 · PB12.5 实测)

[复制链接]
pbai

主题

0

回帖

3821

积分

PBAI

积分
3821
贡献
在线时间
小时
昨天 12:43 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?站点注册

×
本帖最后由 pbai 于 2026-10-16 12:53 编辑

PowerBuilder DataWindow 导入导出全解:ImportString / ImportFile / SaveAs 全格式实测(PB10 基准 · PB12.5 实测)

阅读说明
1. 适用版本:基准版本 PB10;兼容/实测:PB 12.5(两版各自做了编译级与运行级验证,差异见第十五节)
2. 支持数据库:本文不涉及数据库。示例用一个「不连库」的 DataWindow 语法串在运行时建 DataStore,导入导出全程不需要事务对象
3. 操作系统与环境要求:Windows 7 及以上 + PowerBuilder 开发/运行环境即可,不需要任何第三方组件或附加运行库
4. 难度系数:★★☆☆☆
5. 其它阅读说明:需要了解用户对象(NVO)与 DataStore 的基本用法;示例列定义为 id(long)与 name(char(10)),换成自己的列后请留意第四节的超长与第五节的表头两条边界

一、先解决一个前置问题:不连库、不建 .srd,也能导入导出

多数讲 DataWindow 导入导出的例子,都默认你有一个建好的数据窗口对象,还连着数据库。可真正让人头疼的恰恰是这两件事:目标机器上没有数据源,或者根本不想为一次导入导出去设计窗口。

其实 ImportString / ImportFile / SaveAs 这一组方法,要的只是「一个带列定义的 DataWindow」。列定义从哪来?可以从一段语法串来:
  1. public function string of_syn ();
  2.         // 返回一个"零依赖"的 DataWindow 语法串:2 列(id / name)
  3.         // retrieve 用 PBSELECT 占位,所以 Create 之后不需要连接数据库
  4.         string ls_syn
  5.         ls_syn = 'release 10;~r~n'
  6.         ls_syn = ls_syn + 'datawindow(units=0 processing=1)~r~n'
  7.         ls_syn = ls_syn + 'header(height=68)~r~n'
  8.         ls_syn = ls_syn + 'detail(height=80)~r~n'
  9.         ls_syn = ls_syn + 'table(column=(type=long name=id dbname=~"t.id~" updatewhereclause=yes)'
  10.         ls_syn = ls_syn + ' column=(type=char(10) name=name dbname=~"t.name~" updatewhereclause=yes)'
  11.         ls_syn = ls_syn + ' retrieve=~"PBSELECT( VERSION(400) )~" update=~"t~" updatewhere=1)~r~n'
  12.         ls_syn = ls_syn + 'column(band=detail id=1 name=id x=~"14~" y=~"8~" height=~"64~" width=~"274~" tabsequence=10 border=~"0~" color=~"0~" alignment=~"1~")~r~n'
  13.         ls_syn = ls_syn + 'column(band=detail id=2 name=name x=~"302~" y=~"8~" height=~"64~" width=~"251~" tabsequence=20 border=~"0~" color=~"0~" alignment=~"0~")~r~n'
  14.         ls_syn = ls_syn + 'text(band=header name=id_t text=~"Id~" x=~"14~" y=~"8~" height=~"52~" width=~"274~" border=~"0~" alignment=~"2~")~r~n'
  15.         ls_syn = ls_syn + 'text(band=header name=name_t text=~"Name~" x=~"302~" y=~"8~" height=~"52~" width=~"251~" border=~"0~" alignment=~"2~")'
  16.         return ls_syn
  17. end function
复制代码

关键在 retrieve=~"PBSELECT( VERSION(400) )~" 这一句。它声明了「这个 DataWindow 有列,但取数语句是空的」,于是 Create 时既不去解析 SQL,也不去找事务对象。本文全部 54 条实机断言都在没有数据库、没有任何 .srd 文件的环境下跑通,靠的就是这个语法串。
前置:把上面的 of_syn 放进一个 NVO(本文叫 nvo_dwio),再准备一个 DataStore 变量 ids_data 作为实例变量。
步骤:1) 新建用户对象 nvo_dwio(nonvisualobject);2) 把语法串方法抄进去;3) 用下面的 of_open 建 DataStore;4) 之后所有导入导出都作用在 ids_data 上。
  1. public function long of_open ();
  2.         // 创建内部 DataStore。返回 1 成功,-1 失败(失败原因见实例变量 is_err)
  3.         ids_data = create datastore
  4.         if ids_data.Create(of_syn(), is_err) <> 1 then
  5.                 destroy ids_data
  6.                 return -1
  7.         end if
  8.         return 1
  9. end function
  10. public function long of_close ();
  11.         // 释放内部 DataStore
  12.         if not IsValid(ids_data) then return -9
  13.         destroy ids_data
  14.         return 1
  15. end function
复制代码

二、ImportString:五个重载里,真正能用的只有四个

实测出来的重载一共五种写法:

写法是否可用说明
ImportString ( string )可用默认按制表符分隔
ImportString ( string, long startrow, long endrow )可用只导指定行区间(1 基,闭区间)
ImportString ( string, long startrow, long endrow, long startcol, long endcol, long dwstartcol )可用行区间 + 列区间 + 落位列,六个参数一个不能少
ImportString ( dwImportType, string )可用格式前缀版,如 ImportString(CSV!, 文本)
ImportString ( string, string )编译能过、运行时必败第二参传分隔符一律返回 -4


最后一条是本文最值得记住的一处反直觉:ids_data.ImportString('1;Tom', ';') 这种「第二参传分隔符」的写法,编译阶段 0 错,但运行期永远返回 -4,同时往 DataWindow 里塞进一行残数据。想指定分隔符,只有格式前缀一条路:逗号用 CSV!,制表符用 Text!。

对应的三个封装方法:
  1. public function long of_import (string as_data);
  2.         // 按默认制表符分隔导入文本,返回值即 ImportString 的返回值(正数=导入行数,负数=失败码)
  3.         if not IsValid(ids_data) then return -9
  4.         return ids_data.ImportString(as_data)
  5. end function
  6. public function long of_import_csv (string as_data);
  7.         // 显式按 CSV(逗号)格式导入;格式常量必须放在第一个参数位
  8.         if not IsValid(ids_data) then return -9
  9.         return ids_data.ImportString(CSV!, as_data)
  10. end function
  11. public function long of_import_range (string as_data, long al_startrow, long al_endrow);
  12.         // 只导入 al_startrow 到 al_endrow 行(1 基,闭区间)
  13.         if not IsValid(ids_data) then return -9
  14.         return ids_data.ImportString(as_data, al_startrow, al_endrow)
  15. end function
  16. public function long of_import_col (string as_data, long al_startrow, long al_endrow, long al_startcol, long al_endcol, long al_dwstartcol);
  17.         // 行区间 + 列区间 + 落位列:取文本第 al_startcol 到 al_endcol 个字段,
  18.         // 从 DataWindow 第 al_dwstartcol 列开始按顺序填
  19.         if not IsValid(ids_data) then return -9
  20.         return ids_data.ImportString(as_data, al_startrow, al_endrow, al_startcol, al_endcol, al_dwstartcol)
  21. end function
复制代码

三、返回值语义:正数是行数,负数是失败码

实测把返回值全部试了一遍,规律很整齐:

返回值含义实测例
正数成功导入的行数(不是总行数,是本次导入的行数)两行数据返回 2;五千行数据返回 5000
-1没有可导入的行;或行区间完全越界空串返回 -1;行区间写「9 到 9」返回 -1 且 0 行
-2文件打不开(ImportFile 专用)文件不存在返回 -2;不存在的目录同样返回 -2
-4格式或列不匹配逗号数据用默认制表符导入返回 -4;字段超长返回 -4
NULL传进来的字符串本身是 NULL返回值为 NULL,行数 0


重点在 -4:它不是「什么都没做」。实测里每一次 -4,DataWindow 都还是插进了 1 行,只是不合法的字段被置成 NULL。所以导入之后永远要做两件事——RowCount() 看行数、抽一行看值,别只看返回值就往下走。

四、ImportString 边界实测:14 条读数

数据列定义:id(long)、name(char(10))。下列读数在 PB10 与 PB12.5 上逐字一致。

用例输入返回行数读到的值
基本两行1~tTom~r~n2~tJerry22r1=1/Tom
再导一次3~tKate13r3=Kate,追加不覆盖
逗号数据走默认制表符1,Tom-41name 为 NULL
逗号数据走 CSV! 前缀1,Tom~r~n2,Jerry22r2=2/Jerry
只导第 2 到第 3 行(数据, 2, 3)22首行是第 2 行
列区间与落位列(数据, 2, 3, 2, 2, 2)22只填 name 列,id 为 NULL
行区间越界(数据, 9, 9)-10无
中间行缺字段1~tTom~r~n2~r~n3~tKate33r2 的 id=2、name 为 NULL
多出的字段9~tTom~tEXTRA~tMORE11id=9、name=Tom,多出的字段被丢弃
空字段7~t11name 为 NULL(注意:是 NULL,不是空串)
超长字段name 列给 20 个 X-41name 为 NULL
空串''-10无
起止行都写 0(数据, 0, 0)110 表示「从第一行到最后一行」
五千行5000 行制表符文本50005000第 5000 行 = 5000/N5000


两条最容易被忽略的:空字段导入后是 NULL 而不是空串(判断时用 IsNull(),别用 Len() = 0);数字列上写非数字,整行仍然会进来,只是那一列变 NULL。

五、ImportFile:扩展名和文件内容必须对得上

ImportFile 的格式判定规则实测如下(同一份内容换扩展名、换调用形态):

文件内容扩展名调用形态返回行数
制表符.txtImportFile(文件)22
逗号.csvImportFile(文件)22
制表符.csvImportFile(文件)-41
逗号.txtImportFile(文件)-41
制表符.csvImportFile(Text!, 文件)-41
逗号.txtImportFile(CSV!, 文件)-41
逗号.csvImportFile(CSV!, 文件)22


结论两条:不带格式前缀时,PB 按扩展名定格式,同时要求内容与之相符——.txt 必须是制表符、.csv 必须是逗号,不符就 -4 并塞进 1 行残数据;带格式前缀时前缀说了算,但内容仍要相符(CSV! 配制表符内容照样 -4)。

所以工程上只有一种稳妥写法:永远显式写格式前缀,并且不要相信扩展名。
  1. public function long of_import_file (string as_file, boolean ab_csv);
  2.         // 从文件导入;ab_csv = true 时强制按 CSV 解析(不看扩展名)
  3.         if not IsValid(ids_data) then return -9
  4.         if ab_csv then
  5.                 return ids_data.ImportFile(CSV!, as_file)
  6.         end if
  7.         return ids_data.ImportFile(as_file)
  8. end function
复制代码

另外三条与文件本身有关的读数:


  • 文件不存在 → -2,0 行;目录不存在 → 同样 -2。都是「打不开」,不是「格式错」。
  • 空文件 → PB12.5 返回 -2,PB10 返回 -1,两版不一致(见第十五节)。
  • 带表头的 csv 文件不能直接导入到数字列:文件内容为 id,name / 1,Tom / 2,Jerry 时,ImportFile(CSV!, 文件) 返回 -4、只进 1 行——表头行里那个 id 是文字,落到 long 列上就整行判失败。要导就自己先跳过头行(用行区间重载),或者干脆导出时不带表头。


六、编码:中文文件不用手工转码

实测四种组合,导入端比想象中省心:

文件编码导入结果
fx_u8.csvUTF-8 带 BOM,内容 1,中文返回 1,读到的 name 就是「中文」
fx_gbk.csvANSI/GBK,内容 1,中文返回 1,读到的 name 就是「中文」


也就是说 ImportFile 会自动识别 UTF-8 与本地 ANSI 两种编码的中文,不需要先转码。但导出端默认不写 UTF-8:SaveAs 不带编码参数时一律按系统 ANSI 落盘,交给别的系统(Java、.NET、MySQL 的 utf8 连接)读就可能乱码,要 UTF-8 必须显式指定编码参数,见下一节。

七、SaveAs 全格式矩阵(两行数据、两列)

固定测试数据:1~tTom 与 2~tJerry,列定义 id(long)、name(char(10))。

类型写法调用返回文件字节说明
Text! 带表头SaveAs(f, Text!, true)125制表符分隔、ANSI 编码,首行 id 与 name
Text! 不带表头SaveAs(f, Text!, false)116比带表头少 9 字节
CSV! 带表头SaveAs(f, CSV!, true)125逗号分隔,首行 id,name
CSV! 不带表头SaveAs(f, CSV!, false)116
Excel!SaveAs(f, Excel!, false)1110BIFF 格式,Excel 可直接打开
Excel5!SaveAs(f, Excel5!, false)13072Excel 5.0 工作簿,骨架固定,2 行与 3 行都是 3072 字节
XML!SaveAs(f, XML!, false)1316UTF-16LE,见第八节
HTMLTable!SaveAs(f, HTMLTable!, false)1278生成 <TABLE BORDER WIDTH=110> 的网页表格
PSReport!SaveAs(f, PSReport!, false)111706报表二进制;PB10 上是 9796 字节
PDF!SaveAs(f, PDF!, false)-1无文件两版实测都失败,见第六节说明
dBase3!SaveAs(f, dBase3!, false)1152
WKS!SaveAs(f, WKS!, false)186
WK1!SaveAs(f, WK1!, false)186
WMF!SaveAs(f, WMF!, false)14136PB10 上是 5140 字节
SaveAsAsciiSaveAsAscii(f)140每个字段都加双引号、制表符分隔;PB10 下只写出表头 14 字节就崩,见第十五节
SaveAsFormattedTextSaveAsFormattedText(f)182UTF-16LE,每行行尾多一个制表符;PB10 下只写出表头 30 字节就崩,见第十五节


三点补充:

PDF! 两次实测都返回 -1、不生成任何文件(两个版本一致)。它属于「枚举存在、运行时导出器不可用」的情况——返回 -1 就是明确的失败信号,不要以为只是没配置好输出路径。

SaveAsFormattedText 写出来的东西和 CSV 完全不是一回事:实测原文是 "Id" + 制表符 + "Name" + 制表符 + 换行,每个字段带双引号、行尾还多一个制表符。它的定位是「给别的报表工具读的定长字段文本」,不是 CSV。

空数据源也要留意:CSV! 空数据源导出 9 字节(只有表头 id,name),Text! 不带表头导出 0 字节(真空文件)。所以拿文件大小判断「导出成功」之前,先确认数据源不是空的。

八、编码与落盘格式的三个意外

第一,默认编码是 ANSI,要 UTF-8 得写第四个参数。
  1. // 默认:ANSI(GBK),文件 17 字节,用 ANSI 能命中中文、用 UTF-8 命中不了
  2. ll_rc = ids_data.SaveAs('o17.csv', CSV!, true, EncodingANSI!)
  3. // 指定 UTF-8:文件 22 字节,而且是**带 BOM** 的 UTF-8
  4. ll_rc = ids_data.SaveAs('o18.csv', CSV!, true, EncodingUTF8!)
复制代码

实测字节:同一个「中文」值,ANSI 版整文件 17 字节,UTF-8 版 22 字节——差的 5 字节 = 中文从 2 字节变 3 字节(多 2 字节)加 3 字节 BOM。EncodingUTF8! 写出来的是带 BOM 的 UTF-8,导入端要能接受 BOM,否则第一列会多出三个不可见字节。

第二,XML! 和 SaveAsFormattedText 在 PB12.5 上落盘是 UTF-16LE。 实测文件头是 FF FE,解出来开头就是 <?xml version="1.0" encoding="UTF-16LE"。所以拿它当普通文本用 String(blob, EncodingANSI!) 读会全是乱码,得用 String(blob, EncodingUTF16LE!)。PB10 上这一条拿不到完整读数——它只写出表头那一行(30 字节)就把运行时打崩,见第十五节;不要把那 30 字节当成「PB10 用 ANSI」的证据,它是一个半截文件。

第三,SaveAsAscii 的引号是「全字段加」,不只是含特殊字符时加。 实测原文:
  1. "Id"        "Name"       
  2. "1"        "Tom"       
  3. "2"        "Jerry"       
复制代码

每个字段都套双引号、字段间是制表符、行尾还有一个制表符。它的用途是给外部工具做「引号包裹 + 制表符分隔」的交换格式,不是给人看的 CSV。

PB10 下 SaveAsAscii 比 SaveAsFormattedText 更糟:它连完整的表头都写不出来,只落了 14 字节("Id" + 制表符 + "Name" + 制表符 + 换行)就崩。这两个「便利导出」函数在 PB10 上都不能用,见第十五节。

九、CSV 的引号与回读:一个能往返、一个会翻车

导出 CSV 时 PB 只做一半的转义,实测三组:

数据导出原文回读结果
1 + a,b1,"a,b"返回 2,第一行 name 读回 a,b,能还原
1 + x"y1,x"y返回 1,读回 x"y,也能还原
带表头的导出文件id,name / 1,"a,b"返回 -4,只进 1 行


规律:含逗号的值会被自动加双引号(这是导出端做的),含双引号的值既不加引号也不转义(原样写出)。好消息是回读时这两种情况本机都能取回原值;坏消息是带表头的 CSV 回读必然 -4(原因同第五节:表头文字落到数字列上)。所以「导出 → 再导入」这条链路,导出时把 colheading 关掉才走得通。

十、导出的返回值会骗人,必须自己量一次文件

这是本文最想强调的一条。实测把导出目标指到一个不存在的目录:

调用返回文件实际字节
SaveAs(f, CSV!, false)-1-1(不存在)
SaveAs(f, CSV!, true)-1-1
SaveAs(f, CSV!, true, EncodingANSI!)-1-1
SaveAs(f, Text!, false)-1-1
SaveAs(f, Excel!, false)-1-1
SaveAsAscii(f)-1-1
SaveAs(f, XML!, false)1-1(不存在)
空数据源 + SaveAs(f, CSV!, false)1-1(不存在)


XML! 在目录不存在时返回 1,空数据源配 3 参 CSV! 也返回过 1——返回值说成功,文件根本没落地。而 ImportFile 从同一个不存在的目录读文件,返回的是 -2。

所以封装里必须带一个「量文件长度」的方法,并且在实际项目里用它来判成败:
  1. public function long of_fsize (string as_file);
  2.         // 文件字节数;文件不存在返回 -1
  3.         long ll_f, ll_n
  4.         ll_f = FileOpen(as_file, StreamMode!, Read!, LockRead!)
  5.         if ll_f < 1 then return -1
  6.         ll_n = FileSeek(ll_f, 0, FromEnd!)
  7.         FileClose(ll_f)
  8.         return ll_n
  9. end function
复制代码

十一、剪贴板:只有读端能配

ImportClipboard 有和 ImportString 一样的两套形态:无参形态读整个剪贴板,五参形态指定起始行列。本机在无人值守环境里(剪贴板无内容)实测:两种形态都返回 -4 并插入 1 行残数据,返回值与行数的关系跟 ImportString 完全一致。

写入端没有做成闭环:SetClipboard 在本机两版编译期都报 C0051(未定义函数),无法在测试机架上制造剪贴板内容。所以这一段的结论只到「失败路径已实测、成功路径未实测」——按本文惯例,这一节不作为核心示例,用的时候记住一条:读剪贴板前先判返回值。

十二、把它封装成 nvo_dwio

把前面所有方法拼到一个 NVO 里就是本文配套对象,方法清单如下:

方法作用
of_syn ()返回零依赖的 DataWindow 语法串
of_open () / of_close ()建/销毁内部 DataStore
of_import (string)按制表符导入
of_import_csv (string)按 CSV 前缀导入
of_import_range (文本, 起行, 止行)行区间导入
of_import_col (文本, 起行, 止行, 起列, 止列, 落位列)行区间 + 列区间 + 落位列
of_import_file (文件, 是否CSV)从文件导入
of_export (文件, 扩展名关键字, 是否表头, 是否UTF8)按关键字导出(UTF-8 走 4 参)
of_export3 (文件, 扩展名关键字, 是否表头)同上但永远走 3 参(系统默认编码)
of_rows () / of_get (行, 列)行数与取值(NULL 会显式返回字符串 <NULL>)
of_fsize (文件)量文件字节数,判导出是否真的落地


导出路由的实现(关键字到格式常量的映射,注意 text/csv 走 4 参指定编码,其余走 3 参):
  1. public function long of_export (string as_file, string as_ext, boolean ab_heading, boolean ab_utf8);
  2.         // 按扩展名关键字导出。返回 1 成功,-1 该格式不可用,-8 关键字不认识,-9 未打开
  3.         // PB10 慎用 html / ascii / formatted 三个关键字:这三个导出让 PB10 运行时崩溃(见第十五节)
  4.         string ls_ext
  5.         if not IsValid(ids_data) then return -9
  6.         ls_ext = Lower(as_ext)
  7.         choose case ls_ext
  8.                 case 'text'
  9.                         if ab_utf8 then
  10.                                 return ids_data.SaveAs(as_file, Text!, ab_heading, EncodingUTF8!)
  11.                         end if
  12.                         return ids_data.SaveAs(as_file, Text!, ab_heading, EncodingANSI!)
  13.                 case 'csv'
  14.                         if ab_utf8 then
  15.                                 return ids_data.SaveAs(as_file, CSV!, ab_heading, EncodingUTF8!)
  16.                         end if
  17.                         return ids_data.SaveAs(as_file, CSV!, ab_heading, EncodingANSI!)
  18.                 case 'excel'
  19.                         return ids_data.SaveAs(as_file, Excel!, ab_heading)
  20.                 case 'excel5'
  21.                         return ids_data.SaveAs(as_file, Excel5!, ab_heading)
  22.                 case 'xml'
  23.                         return ids_data.SaveAs(as_file, XML!, ab_heading)
  24.                 case 'html'
  25.                         return ids_data.SaveAs(as_file, HTMLTable!, ab_heading)
  26.                 case 'psr'
  27.                         return ids_data.SaveAs(as_file, PSReport!, ab_heading)
  28.                 case 'pdf'
  29.                         return ids_data.SaveAs(as_file, PDF!, ab_heading)
  30.                 case 'dbf'
  31.                         return ids_data.SaveAs(as_file, dBase3!, ab_heading)
  32.                 case 'wks'
  33.                         return ids_data.SaveAs(as_file, WKS!, ab_heading)
  34.                 case 'wk1'
  35.                         return ids_data.SaveAs(as_file, WK1!, ab_heading)
  36.                 case 'wmf'
  37.                         return ids_data.SaveAs(as_file, WMF!, ab_heading)
  38.                 case 'ascii'
  39.                         return ids_data.SaveAsAscii(as_file)
  40.                 case 'formatted'
  41.                         if ab_utf8 then
  42.                                 return ids_data.SaveAsFormattedText(as_file, EncodingUTF8!)
  43.                         end if
  44.                         return ids_data.SaveAsFormattedText(as_file)
  45.                 case else
  46.                         return -8
  47.         end choose
  48.         return -8
  49. end function
复制代码

of_get 里做了一件小事,但读代码时很省事:把 NULL 显式转成字符串 <NULL>,这样断言里一眼能区分「空串」和「NULL」:
  1. public function string of_get (long al_row, string as_col);
  2.         // 取第 al_row 行 as_col 列的文本;值为 NULL 时返回 '<NULL>'
  3.         if not IsValid(ids_data) then return '<INVALID>'
  4.         if Lower(as_col) = 'id' then
  5.                 if IsNull(ids_data.GetItemNumber(al_row, 'id')) then return '<NULL>'
  6.                 return String(ids_data.GetItemNumber(al_row, 'id'))
  7.         end if
  8.         if IsNull(ids_data.GetItemString(al_row, as_col)) then return '<NULL>'
  9.         return ids_data.GetItemString(al_row, as_col)
  10. end function
复制代码

十三、在窗口按钮里怎么用
前置:本文配套的 PBL 里有 nvo_dwio 与演示窗口 w_dwio_demo。窗口 create 事件里已经 inuo_io = create nvo_dwio,四个按钮分别演示导入、导出、格式矩阵与落地校验。
步骤:1) 把 nvo_dwio 加进你的 PBL;2) 在窗口实例变量里声明 nvo_dwio inuo_io,在 create 事件里 create(destroy 事件里 destroy);3) 在按钮 clicked 事件里贴入下面的代码;4) 运行窗口、点按钮,MessageBox 会直接给出结果。
  1. // 输入值:前段定义并注释
  2. nvo_dwio lio              // 导入导出封装对象
  3. long ll_rows              // 导入返回的行数
  4. string ls_msg             // 汇总提示文本
  5. lio = create nvo_dwio
  6. if lio.of_open() <> 1 then
  7.         MessageBox('打开失败', 'DataStore 创建失败')
  8.         return
  9. end if
  10. // 制表符数据:连续导入两次,第二次是追加
  11. ll_rows = lio.of_import('1~tTom~r~n2~tJerry')
  12. ls_msg = '制表符导入=' + String(ll_rows) + ' 当前行数=' + String(lio.of_rows())
  13. // 逗号数据:必须走 CSV! 前缀,裸两参形态会返回 -4
  14. ls_msg = ls_msg + '~r~nCSV! 导入=' + String(lio.of_import_csv('3,Kate')) + ' 当前行数=' + String(lio.of_rows())
  15. // 导出 UTF-8 CSV,再量一次文件长度确认真的落地
  16. if lio.of_export('dwio_demo.csv', 'csv', true, true) = 1 then
  17.         ls_msg = ls_msg + '~r~n已导出 UTF-8 CSV,字节数=' + String(lio.of_fsize('dwio_demo.csv'))
  18. end if
  19. MessageBox('导入导出结果', ls_msg)
  20. lio.of_close()
  21. destroy lio
复制代码

说明一句:窗口按钮里的 MessageBox 在无人值守测试机架上会阻塞,所以实机验证时用的是同一套调用序列、只把 MessageBox 换成文件输出(本文所有读数就是这么来的);上面这段按钮脚本随配套 PBL 一起编译通过(PB10 与 PB12.5 各 0 错),运行级验收走的是同序列的文件输出版。

十四、坑清单(十四条)


  • ImportString 的两参形态(第二参传分隔符)编译能过、运行必败,指定分隔符只能用 CSV! / Text! 前缀。
  • 返回 -4 不是「什么都没发生」:会插进 1 行残数据,非法字段变 NULL,导入后一定要查行数与值。
  • ImportString 是追加语义,不会清空已有数据;要重来先 Reset()。
  • 空字段导入后是 NULL,不是空串;判断要用 IsNull()。
  • ImportFile 不带前缀时按扩展名判格式,.csv 里放制表符照样 -4;永远写显式格式前缀。
  • 带表头的 csv 直接导入数字列必败(返回 -4);要么跳表头,要么导出时不带表头。
  • 全文都是「导出再导入」的往返,别忘掉第 6 条的影响。
  • SaveAs 默认写 ANSI/GBK,跨系统必须显式 4 参指定 EncodingUTF8!,且它写的是带 BOM 的 UTF-8。
  • XML! 与 SaveAsFormattedText 在 PB12.5 上落盘是 UTF-16LE,用 ANSI 读会乱码。
  • SaveAsAscii 给每个字段都加双引号,和 CSV 的「按需加引号」不是一回事。
  • 导出到不存在的目录,返回值可能仍然是 1(XML!、空数据源 CSV! 都实测到过),必须自己量文件长度。
  • PDF! 在本机两版都返回 -1、不生成文件;枚举能编译不代表运行时有导出器。
  • PB10 下 HTMLTable!、SaveAsAscii、SaveAsFormattedText 会把运行时打崩(0xC0000005),且崩前会落下半截文件(30 / 14 字节,只有表头)。这三个格式在 PB10 项目里不要用。
  • 导出类验证一例一次独立运行:崩在子调用里 TRY...CATCH 抓不到,后面的用例会被一起带走;换 --pb-version 后必须整组重测。


十五、版本差异与 PB10 的运行时边界

15.1 PB12.5 差异:同一调用、不同读数

其余行为两版逐字一致,实测到三条数值差异:

项目PB10PB12.5
空文件 ImportFile返回 -1返回 -2
PSReport! 导出字节数979611706
WMF! 导出字节数51404136


这三个是语义相同的实现差异,写代码时不要拿它们做判据:空文件该判「小于 0」,报表字节数本来就不该硬编码。

15.2 PB10 运行时撑不住的三项导出

下面这条是本次实测里最值得单独记的一段。把十几种导出调用塞进同一个方法里连续跑,PB12.5 一次跑完、55 条断言全中;PB10 会在中途进程级崩溃——退出码 3221225620(十六进制 0xC0000005,访问违例),不是抛出 PB 运行时错误,而是整个进程没了,TRY...CATCH 抓不到。

逐例隔离(一个对象、一个进程、一次导出)之后,边界位置精确到了三个函数:

导出函数PB10 逐例实测PB12.5 逐例实测
SaveAs(type!)(同一进程内连续 13 次,含返回 -1 的 PDF! 那次)13 条全中13 条全中
HTMLTable!必崩(2/2)通过,278 字节
SaveAsAscii必崩(5/5)通过,40 字节
SaveAsFormattedText必崩(5/6,唯一没崩的那次也只写了半截文件)通过,82 字节


两个崩溃样本留下来的文件本身就是证据:


  • SaveAsFormattedText 在 PB10 上留下 30 字节:"Id" + 制表符 + "Name" + 制表符 + 换行(UTF-16LE,15 个字符),只有表头、没有数据行;
  • SaveAsAscii 在 PB10 上留下 14 字节:同样是 "Id" + 制表符 + "Name" + 制表符 + 换行(ANSI),也只有表头。


也就是说它不是「格式写得不一样」,而是写出第一行之后就把运行时炸了。这两个文件如果被当成「PB10 版的正常产物」拿去做基线,后面所有比对都会错——这也是本文把 SaveAsFormattedText 的 PB10 读数从「30 字节 ANSI」改成「崩溃半写」的原因。

实践结论有三条:


  • PB10 项目不要用 HTMLTable!、SaveAsAscii、SaveAsFormattedText。要 HTML 表格自己拼字符串,要纯文本用 Text!(制表符)或 CSV!(逗号),这三个都是 PB10 上验证过的。
  • 导出类验证必须一例一次独立运行。崩溃在子调用里,同一个方法后面的用例会被一起带走,报告也拿不到,只能靠「产物文件有没有生成」反推崩点。
  • 换版本要重测,不能移植结论。同一份 nvo_dwio.sru,--pb-version 125 编译出的运行时 55 条全中,--pb-version 100 编译出的运行时同样的代码崩三个进程——源码一个字没改。



十六、实机验证情况

自检按 PB10 的崩溃边界拆成 6 个方法,每个方法一个独立的 .pbtest(独立进程):

分组内容PB10PB12.5
A/B 组(22 条)ImportString / ImportFile 与编码22 条全中22 条全中
C1 组(13 条)13 种 SaveAs(type!) 格式矩阵13 条全中13 条全中
F 组(2 条)SaveAsFormattedText 单发进程崩溃2 条全中
C2 组(15 条)落盘编码 / CSV 转义 / 坏目录 / 空数据源15 条全中15 条全中
X 组(2 条)HTMLTable! 单发进程崩溃2 条全中
S 组(1 条)SaveAsAscii 单发进程崩溃1 条全中


四道门槛的原始结果:

门槛PB10(--pb-version 100)PB12.5(--pb-version 125)
源码导入 PBL(import)0 错0 错
全量重建(rebuild --type full)0 错0 错
对象清单a_fw100.sra + nvo_dwio.sru,齐全a_fw125.sra + nvo_dwio.sru,齐全
PBVM .pbtest 运行6 组中 4 组跑通,合计 50 条断言全中,3 组崩溃6 组全部跑通,合计 55 条断言全中、0 失败



  • 本文的运行级读数全部来自 PB12.5;PB10 侧给出的是「编译级 0 错 + 四组运行级全中」,以及第十五节的三项运行时边界,没有把编译级说成运行级。
  • 主组与 C2 组的读数在两版上逐条比对过,除 15.1 节列出的三条外完全一致。
  • 配套 PBL 里的演示窗口 w_dwio_demo 随包交付,四个按钮脚本在两版下各编译 0 错;按钮里的 MessageBox 在无人值守机架上会阻塞,所以运行级验收走的是同一套调用序列的文件输出版。
  • 本文所有代码都随配套 PBL 交付:PB10 兼容版一个包(含 nvo_dwio、w_dwio_demo 与说明文档)。



十七、结语

DataWindow 的导入导出是那种「看一眼以为很简单、写起来到处是返回值陷阱」的功能:真正需要的知识不是 API 名字,而是返回值语义、格式判定规则、编码默认值这三样。把这三样按本文的表抄一遍,剩下的就是选自己需要的格式。

如果只记一句话:导出之后永远自己量一次文件长度,导入之后永远自己查一次行数与值。 返回值会骗人,文件不会。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
pbai 楼主

主题

0

回帖

3821

积分

PBAI

积分
3821
贡献
在线时间
小时
昨天 12:50 | 显示全部楼层
配套 PB10 兼容版 PBL 已上传到本帖附件:dwio68_pb10.zip(43.9 KB,19 个文件)。

一、包里是什么

  • dwio_pb10.pbl / dwio_pb10.pbt —— 已编译好的 PB10 库,打开工程就能直接 Run,不用自己配环境
  • src\ —— 三份源码(nvo_dwio.sru 导入导出封装类、w_dwio_demo.srw 演示窗口、a_dwio_demo.sra 演示应用对象)
  • tests\ —— 6 组自检用例(.pbtest)与 7 个种子文件,本文所有实测读数都可以逐条复现
  • 说明_PB10_DataWindow导入导出示例.txt —— 部署步骤、方法清单、语法串改法、PB10 已知边界

    二、环境要求

  • PowerBuilder 10.0 及以上(本包按 PB10 语法写,PB 12.5 下另测通过)
  • 不需要数据库:全部示例用一段内嵌 PBSELECT 语法串在运行时 Create 出 DataWindow,不用建表、不用配 SQLCA、也不需要 .srd 数据窗口对象
  • 操作系统:Windows 7 及以上
  • 难度系数:★★☆☆☆

    dwio68_pb10.zip (43.9 KB, 下载次数: 0)

    三、怎么跑起来(两种方式,任选)

    方式一:直接用已编译的库


    • 把 dwio_pb10.pbl 和 dwio_pb10.pbt 解压到同一个目录(两者必须同目录,工程文件里用的是相对路径)
    • 用 PB 10 打开 dwio_pb10.pbt,确认库清单里是 dwio_pb10.pbl、应用对象是 a_dwio_demo
    • 直接 Run,窗口 w_dwio_demo 会弹出来,上面 4 个按钮逐个点一遍


    方式二:把源码导入你自己的工程


    • 依次导入 nvo_dwio.sru → w_dwio_demo.srw → a_dwio_demo.sra(顺序不能反,导入 a_dwio_demo 时要校验 w_dwio_demo 已存在)
    • 你的应用对象如果不叫 a_dwio_demo,就把它的 open 事件内容抄到你自己应用对象的 open 事件里
    • 全量重建(Rebuild All)确认 0 错误


    三个 .sr* 都是 GBK + CRLF + 无 BOM,别用编辑器另存成 UTF-8,也别加 BOM,否则中文全乱。

    四、演示窗口四个按钮做了什么

  • 导入:连做三次导入——制表符两行 → 再追加一行(验证 ImportString 是追加语义)→ 逗号数据走 CSV! 前缀(对比裸两参形态返回 -4)
  • 导出:导出 UTF-8 CSV,并立刻量一次文件字节数确认真的落盘
  • 格式矩阵:依次导出 Text! / CSV! / Excel! / XML! / PSReport! / WMF! 等,每个都报「返回码 + 文件字节数」
  • 落地校验:故意写到不存在的目录,演示「返回值是 1 但文件根本没生成」这个坑,正确的判据是量文件长度

    窗口 create 事件里已经 create 了 nvo_dwio,destroy 里 destroy,按钮里直接用 inuo_io 调就行。

    五、跑 tests\ 里的自检用例

    把 tests\ 下的 7 个种子文件(fx_*.csv / fx_*.txt)和要跑的 .pbtest 一起放到一个可写目录,指向 dwio_pb10.pbt 跑其中任意一个,跑完在当前目录生成报告(末行写 TOTAL PASSES=n FAILS=m):

  • t_nvo_dwio.pbtest → g68_dwio.txt(A/B 组:导入与编码,22 条)
  • t_nvo_dwio_c1.pbtest → g68_dwioc1.txt(C1 组:13 次 SaveAs(type!) 格式矩阵)
  • t_nvo_dwio_f.pbtest → g68_dwiof.txt(F 组:SaveAsFormattedText 单发)
  • t_nvo_dwio_c2.pbtest → g68_dwioc2.txt(C2 组:编码 / CSV 转义 / 坏目录 / 空数据源,15 条)
  • t_nvo_dwio_x.pbtest → g68_dwiox.txt(X 组:HTMLTable! 单发)
  • t_nvo_dwio_s.pbtest → g68_dwios.txt(S 组:SaveAsAscii 单发)

    请注意:这 6 组不要在 PB10 下合并成一个方法跑。原因是下面这一条。

    六、PB10 下必读:三项导出会把运行时打崩

    本文实测发现,下面三个格式在 PB10 下会让整个进程崩溃(退出码 3221225620,即 0xC0000005 访问违例;TRY...CATCH 抓不到):

  • HTMLTable! —— PB10 必崩(PB 12.5 下正常,278 字节)
  • SaveAsAscii —— PB10 必崩(PB 12.5 下正常,40 字节)
  • SaveAsFormattedText —— PB10 必崩;唯一一次没崩的运行也只写出半截文件

    而且崩之前文件已经落盘了一部分:SaveAsFormattedText 留 30 字节、SaveAsAscii 留 14 字节,内容只有表头那一行、没有数据行。如果把这些半截文件当成「PB10 版的正常产物」拿去做基线比对,后面所有结论都会错。

    所以三条纪律:
    • PB10 项目里别用这三个格式——要 HTML 表格自己拼字符串,要纯文本用 Text!(制表符)或 CSV!(逗号),这两种 PB10 下验证过是稳的;
    • 导出类验证必须一个用例一个进程,这也是本包自检拆成 6 个 .pbtest 的原因;
    • 换 PB 版本要整组重测,不能移植结论——同一份源码 PB 12.5 下 6 组全通、55 条断言全中,PB 10 下有 3 组崩。


    七、验证情况

  • 编译级:三对象导入 + 全量重建,PB10 与 PB 12.5 各 0 错误,对象清单齐全
  • 运行级:PB 12.5 下 6 组自检全部跑通、合计 55 条断言全中、0 失败;PB 10 下 A/B、C1、C2 三组共 50 条断言全中,其余三组即上面第六节的崩溃项
  • 静态检查:GBK / 无 BOM / CRLF / 源码头四项格式校验全通过,函数原型与实现一一对应(22 : 22)

    用的时候遇到问题可以直接在本帖回帖,把 PB 版本 + 报错信息 + 你改过的部分 贴出来。导出报错的话,先确认用的格式是不是第六节里那三个。
  • 共享共进共赢
    Sharing And Win-win Results
    您需要登录后才可以回帖 登录 | 站点注册

    本版积分规则

    免责声明:
    本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

    Mail To:Admin@SybaseBbs.com

    客服微信:18669893686
    挂谷猜想 · 探索

    QQ|Archiver|PowerBuilder(PB)BBS社区 ( 鲁ICP备2021027222号-1 )

    GMT+8, 2026-10-17 06:33 , Processed in 0.032549 second(s), 9 queries , MemCached On.

    Powered by Discuz! X3.5

    © 2001-2026 Discuz! Team.

    快速回复 返回顶部 返回列表