马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
本帖最后由 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」。列定义从哪来?可以从一段语法串来:
- public function string of_syn ();
- // 返回一个"零依赖"的 DataWindow 语法串:2 列(id / name)
- // retrieve 用 PBSELECT 占位,所以 Create 之后不需要连接数据库
- string ls_syn
- ls_syn = 'release 10;~r~n'
- ls_syn = ls_syn + 'datawindow(units=0 processing=1)~r~n'
- ls_syn = ls_syn + 'header(height=68)~r~n'
- ls_syn = ls_syn + 'detail(height=80)~r~n'
- ls_syn = ls_syn + 'table(column=(type=long name=id dbname=~"t.id~" updatewhereclause=yes)'
- ls_syn = ls_syn + ' column=(type=char(10) name=name dbname=~"t.name~" updatewhereclause=yes)'
- ls_syn = ls_syn + ' retrieve=~"PBSELECT( VERSION(400) )~" update=~"t~" updatewhere=1)~r~n'
- 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'
- 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'
- 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'
- ls_syn = ls_syn + 'text(band=header name=name_t text=~"Name~" x=~"302~" y=~"8~" height=~"52~" width=~"251~" border=~"0~" alignment=~"2~")'
- return ls_syn
- 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 上。 - public function long of_open ();
- // 创建内部 DataStore。返回 1 成功,-1 失败(失败原因见实例变量 is_err)
- ids_data = create datastore
- if ids_data.Create(of_syn(), is_err) <> 1 then
- destroy ids_data
- return -1
- end if
- return 1
- end function
- public function long of_close ();
- // 释放内部 DataStore
- if not IsValid(ids_data) then return -9
- destroy ids_data
- return 1
- 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!。
对应的三个封装方法:
- public function long of_import (string as_data);
- // 按默认制表符分隔导入文本,返回值即 ImportString 的返回值(正数=导入行数,负数=失败码)
- if not IsValid(ids_data) then return -9
- return ids_data.ImportString(as_data)
- end function
- public function long of_import_csv (string as_data);
- // 显式按 CSV(逗号)格式导入;格式常量必须放在第一个参数位
- if not IsValid(ids_data) then return -9
- return ids_data.ImportString(CSV!, as_data)
- end function
- public function long of_import_range (string as_data, long al_startrow, long al_endrow);
- // 只导入 al_startrow 到 al_endrow 行(1 基,闭区间)
- if not IsValid(ids_data) then return -9
- return ids_data.ImportString(as_data, al_startrow, al_endrow)
- end function
- public function long of_import_col (string as_data, long al_startrow, long al_endrow, long al_startcol, long al_endcol, long al_dwstartcol);
- // 行区间 + 列区间 + 落位列:取文本第 al_startcol 到 al_endcol 个字段,
- // 从 DataWindow 第 al_dwstartcol 列开始按顺序填
- if not IsValid(ids_data) then return -9
- return ids_data.ImportString(as_data, al_startrow, al_endrow, al_startcol, al_endcol, al_dwstartcol)
- 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~tJerry | 2 | 2 | r1=1/Tom | | 再导一次 | 3~tKate | 1 | 3 | r3=Kate,追加不覆盖 | | 逗号数据走默认制表符 | 1,Tom | -4 | 1 | name 为 NULL | | 逗号数据走 CSV! 前缀 | 1,Tom~r~n2,Jerry | 2 | 2 | r2=2/Jerry | | 只导第 2 到第 3 行 | (数据, 2, 3) | 2 | 2 | 首行是第 2 行 | | 列区间与落位列 | (数据, 2, 3, 2, 2, 2) | 2 | 2 | 只填 name 列,id 为 NULL | | 行区间越界 | (数据, 9, 9) | -1 | 0 | 无 | | 中间行缺字段 | 1~tTom~r~n2~r~n3~tKate | 3 | 3 | r2 的 id=2、name 为 NULL | | 多出的字段 | 9~tTom~tEXTRA~tMORE | 1 | 1 | id=9、name=Tom,多出的字段被丢弃 | | 空字段 | 7~t | 1 | 1 | name 为 NULL(注意:是 NULL,不是空串) | | 超长字段 | name 列给 20 个 X | -4 | 1 | name 为 NULL | | 空串 | '' | -1 | 0 | 无 | | 起止行都写 0 | (数据, 0, 0) | 1 | 1 | 0 表示「从第一行到最后一行」 | | 五千行 | 5000 行制表符文本 | 5000 | 5000 | 第 5000 行 = 5000/N5000 |
两条最容易被忽略的:空字段导入后是 NULL 而不是空串(判断时用 IsNull(),别用 Len() = 0);数字列上写非数字,整行仍然会进来,只是那一列变 NULL。
五、ImportFile:扩展名和文件内容必须对得上
ImportFile 的格式判定规则实测如下(同一份内容换扩展名、换调用形态):
| 文件内容 | 扩展名 | 调用形态 | 返回 | 行数 | | 制表符 | .txt | ImportFile(文件) | 2 | 2 | | 逗号 | .csv | ImportFile(文件) | 2 | 2 | | 制表符 | .csv | ImportFile(文件) | -4 | 1 | | 逗号 | .txt | ImportFile(文件) | -4 | 1 | | 制表符 | .csv | ImportFile(Text!, 文件) | -4 | 1 | | 逗号 | .txt | ImportFile(CSV!, 文件) | -4 | 1 | | 逗号 | .csv | ImportFile(CSV!, 文件) | 2 | 2 |
结论两条:不带格式前缀时,PB 按扩展名定格式,同时要求内容与之相符——.txt 必须是制表符、.csv 必须是逗号,不符就 -4 并塞进 1 行残数据;带格式前缀时前缀说了算,但内容仍要相符(CSV! 配制表符内容照样 -4)。
所以工程上只有一种稳妥写法:永远显式写格式前缀,并且不要相信扩展名。
- public function long of_import_file (string as_file, boolean ab_csv);
- // 从文件导入;ab_csv = true 时强制按 CSV 解析(不看扩展名)
- if not IsValid(ids_data) then return -9
- if ab_csv then
- return ids_data.ImportFile(CSV!, as_file)
- end if
- return ids_data.ImportFile(as_file)
- 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.csv | UTF-8 带 BOM,内容 1,中文 | 返回 1,读到的 name 就是「中文」 | | fx_gbk.csv | ANSI/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) | 1 | 25 | 制表符分隔、ANSI 编码,首行 id 与 name | | Text! 不带表头 | SaveAs(f, Text!, false) | 1 | 16 | 比带表头少 9 字节 | | CSV! 带表头 | SaveAs(f, CSV!, true) | 1 | 25 | 逗号分隔,首行 id,name | | CSV! 不带表头 | SaveAs(f, CSV!, false) | 1 | 16 | | | Excel! | SaveAs(f, Excel!, false) | 1 | 110 | BIFF 格式,Excel 可直接打开 | | Excel5! | SaveAs(f, Excel5!, false) | 1 | 3072 | Excel 5.0 工作簿,骨架固定,2 行与 3 行都是 3072 字节 | | XML! | SaveAs(f, XML!, false) | 1 | 316 | UTF-16LE,见第八节 | | HTMLTable! | SaveAs(f, HTMLTable!, false) | 1 | 278 | 生成 <TABLE BORDER WIDTH=110> 的网页表格 | | PSReport! | SaveAs(f, PSReport!, false) | 1 | 11706 | 报表二进制;PB10 上是 9796 字节 | | PDF! | SaveAs(f, PDF!, false) | -1 | 无文件 | 两版实测都失败,见第六节说明 | | dBase3! | SaveAs(f, dBase3!, false) | 1 | 152 | | | WKS! | SaveAs(f, WKS!, false) | 1 | 86 | | | WK1! | SaveAs(f, WK1!, false) | 1 | 86 | | | WMF! | SaveAs(f, WMF!, false) | 1 | 4136 | PB10 上是 5140 字节 | | SaveAsAscii | SaveAsAscii(f) | 1 | 40 | 每个字段都加双引号、制表符分隔;PB10 下只写出表头 14 字节就崩,见第十五节 | | SaveAsFormattedText | SaveAsFormattedText(f) | 1 | 82 | UTF-16LE,每行行尾多一个制表符;PB10 下只写出表头 30 字节就崩,见第十五节 |
三点补充:
PDF! 两次实测都返回 -1、不生成任何文件(两个版本一致)。它属于「枚举存在、运行时导出器不可用」的情况——返回 -1 就是明确的失败信号,不要以为只是没配置好输出路径。
SaveAsFormattedText 写出来的东西和 CSV 完全不是一回事:实测原文是 "Id" + 制表符 + "Name" + 制表符 + 换行,每个字段带双引号、行尾还多一个制表符。它的定位是「给别的报表工具读的定长字段文本」,不是 CSV。
空数据源也要留意:CSV! 空数据源导出 9 字节(只有表头 id,name),Text! 不带表头导出 0 字节(真空文件)。所以拿文件大小判断「导出成功」之前,先确认数据源不是空的。
八、编码与落盘格式的三个意外
第一,默认编码是 ANSI,要 UTF-8 得写第四个参数。
- // 默认:ANSI(GBK),文件 17 字节,用 ANSI 能命中中文、用 UTF-8 命中不了
- ll_rc = ids_data.SaveAs('o17.csv', CSV!, true, EncodingANSI!)
- // 指定 UTF-8:文件 22 字节,而且是**带 BOM** 的 UTF-8
- 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 的引号是「全字段加」,不只是含特殊字符时加。 实测原文:
- "Id" "Name"
- "1" "Tom"
- "2" "Jerry"
复制代码
每个字段都套双引号、字段间是制表符、行尾还有一个制表符。它的用途是给外部工具做「引号包裹 + 制表符分隔」的交换格式,不是给人看的 CSV。
PB10 下 SaveAsAscii 比 SaveAsFormattedText 更糟:它连完整的表头都写不出来,只落了 14 字节("Id" + 制表符 + "Name" + 制表符 + 换行)就崩。这两个「便利导出」函数在 PB10 上都不能用,见第十五节。
九、CSV 的引号与回读:一个能往返、一个会翻车
导出 CSV 时 PB 只做一半的转义,实测三组:
| 数据 | 导出原文 | 回读结果 | | 1 + a,b | 1,"a,b" | 返回 2,第一行 name 读回 a,b,能还原 | | 1 + x"y | 1,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。
所以封装里必须带一个「量文件长度」的方法,并且在实际项目里用它来判成败:
- public function long of_fsize (string as_file);
- // 文件字节数;文件不存在返回 -1
- long ll_f, ll_n
- ll_f = FileOpen(as_file, StreamMode!, Read!, LockRead!)
- if ll_f < 1 then return -1
- ll_n = FileSeek(ll_f, 0, FromEnd!)
- FileClose(ll_f)
- return ll_n
- 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 参):
- public function long of_export (string as_file, string as_ext, boolean ab_heading, boolean ab_utf8);
- // 按扩展名关键字导出。返回 1 成功,-1 该格式不可用,-8 关键字不认识,-9 未打开
- // PB10 慎用 html / ascii / formatted 三个关键字:这三个导出让 PB10 运行时崩溃(见第十五节)
- string ls_ext
- if not IsValid(ids_data) then return -9
- ls_ext = Lower(as_ext)
- choose case ls_ext
- case 'text'
- if ab_utf8 then
- return ids_data.SaveAs(as_file, Text!, ab_heading, EncodingUTF8!)
- end if
- return ids_data.SaveAs(as_file, Text!, ab_heading, EncodingANSI!)
- case 'csv'
- if ab_utf8 then
- return ids_data.SaveAs(as_file, CSV!, ab_heading, EncodingUTF8!)
- end if
- return ids_data.SaveAs(as_file, CSV!, ab_heading, EncodingANSI!)
- case 'excel'
- return ids_data.SaveAs(as_file, Excel!, ab_heading)
- case 'excel5'
- return ids_data.SaveAs(as_file, Excel5!, ab_heading)
- case 'xml'
- return ids_data.SaveAs(as_file, XML!, ab_heading)
- case 'html'
- return ids_data.SaveAs(as_file, HTMLTable!, ab_heading)
- case 'psr'
- return ids_data.SaveAs(as_file, PSReport!, ab_heading)
- case 'pdf'
- return ids_data.SaveAs(as_file, PDF!, ab_heading)
- case 'dbf'
- return ids_data.SaveAs(as_file, dBase3!, ab_heading)
- case 'wks'
- return ids_data.SaveAs(as_file, WKS!, ab_heading)
- case 'wk1'
- return ids_data.SaveAs(as_file, WK1!, ab_heading)
- case 'wmf'
- return ids_data.SaveAs(as_file, WMF!, ab_heading)
- case 'ascii'
- return ids_data.SaveAsAscii(as_file)
- case 'formatted'
- if ab_utf8 then
- return ids_data.SaveAsFormattedText(as_file, EncodingUTF8!)
- end if
- return ids_data.SaveAsFormattedText(as_file)
- case else
- return -8
- end choose
- return -8
- end function
复制代码
of_get 里做了一件小事,但读代码时很省事:把 NULL 显式转成字符串 <NULL>,这样断言里一眼能区分「空串」和「NULL」:
- public function string of_get (long al_row, string as_col);
- // 取第 al_row 行 as_col 列的文本;值为 NULL 时返回 '<NULL>'
- if not IsValid(ids_data) then return '<INVALID>'
- if Lower(as_col) = 'id' then
- if IsNull(ids_data.GetItemNumber(al_row, 'id')) then return '<NULL>'
- return String(ids_data.GetItemNumber(al_row, 'id'))
- end if
- if IsNull(ids_data.GetItemString(al_row, as_col)) then return '<NULL>'
- return ids_data.GetItemString(al_row, as_col)
- 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 会直接给出结果。 - // 输入值:前段定义并注释
- nvo_dwio lio // 导入导出封装对象
- long ll_rows // 导入返回的行数
- string ls_msg // 汇总提示文本
- lio = create nvo_dwio
- if lio.of_open() <> 1 then
- MessageBox('打开失败', 'DataStore 创建失败')
- return
- end if
- // 制表符数据:连续导入两次,第二次是追加
- ll_rows = lio.of_import('1~tTom~r~n2~tJerry')
- ls_msg = '制表符导入=' + String(ll_rows) + ' 当前行数=' + String(lio.of_rows())
- // 逗号数据:必须走 CSV! 前缀,裸两参形态会返回 -4
- ls_msg = ls_msg + '~r~nCSV! 导入=' + String(lio.of_import_csv('3,Kate')) + ' 当前行数=' + String(lio.of_rows())
- // 导出 UTF-8 CSV,再量一次文件长度确认真的落地
- if lio.of_export('dwio_demo.csv', 'csv', true, true) = 1 then
- ls_msg = ls_msg + '~r~n已导出 UTF-8 CSV,字节数=' + String(lio.of_fsize('dwio_demo.csv'))
- end if
- MessageBox('导入导出结果', ls_msg)
- lio.of_close()
- 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 差异:同一调用、不同读数
其余行为两版逐字一致,实测到三条数值差异:
| 项目 | PB10 | PB12.5 | | 空文件 ImportFile | 返回 -1 | 返回 -2 | | PSReport! 导出字节数 | 9796 | 11706 | | WMF! 导出字节数 | 5140 | 4136 |
这三个是语义相同的实现差异,写代码时不要拿它们做判据:空文件该判「小于 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(独立进程):
| 分组 | 内容 | PB10 | PB12.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 名字,而是返回值语义、格式判定规则、编码默认值这三样。把这三样按本文的表抄一遍,剩下的就是选自己需要的格式。
如果只记一句话:导出之后永远自己量一次文件长度,导入之后永远自己查一次行数与值。 返回值会骗人,文件不会。
|