PBIDEA:用 uo_xlsx 不装 Excel 也能读写 xlsx 文件
PBIDEA:用 uo_xlsx 不装 Excel 也能读写 xlsx 文件阅读说明
1. 适用版本:PB 9 及以上(本文按 PB 12.5 + PBIDEA 1.x 验证;涉及 websuite.pbl 一个库)
2. 支持数据库:无需数据库(示例全程读写本地 xlsx 文件,不连接任何 DBMS)
3. 操作系统与环境要求:Windows 7+,已安装 PBIDEA 运行库(PbIdea.dll 等);应用库需包含 websuite.pbl
4. 难度系数:★★☆☆☆
5. 其它阅读说明:前置知识为 PowerScript 基本语法与对象 create/destroy;建议先看 Day 7《PBIDEA:uo_database 连接池与多库事务复用》了解 PBIDEA 库的引入方式
一、uo_xlsx 是什么
很多老系统导出报表还在拼 CSV、或者调用 Excel COM(OLE),机器上没装 Office 就直接报错。PBIDEA 的 websuite.pbl 里封装了 libxl 引擎,提供一组 uo_xl_* 对象,不依赖 Excel、不依赖任何 ActiveX/OCX,就能直接读写 .xlsx(也支持老 .xls)文件。
对象关系一句话:uo_xlsx 继承自 uo_xl_book(工作簿),工作簿里装 uo_xl_sheet(工作表),工作表里是 uo_xl_cell(单元格)。uo_xlsx 的 constructor 会自动执行 xlInitXL() 和 xlCreate(2) 完成引擎初始化(2 表示 xlsx 格式;想要旧版 xls 用 uo_xls 即可,它内部是 xlCreate(1)),所以创建后直接就能用,不需要任何手动初始化。
常用能力一览(均已在本机导出源码核实):
对象常用成员说明
uo_xl_book(uo_xlsx)Open(file) / save(file)打开 / 保存文件,返回 boolean
SheetCount() / GetSheet(索引或名称) / InsertSheet(索引, 名称)工作表的数量、获取、插入
CreateFormat() / CreateFont()新建格式 / 字体对象
GetErrorMessage()取最近一次操作的错误信息
uo_xl_sheetGetCell(行, 列)取单元格(行列都从 1 开始)
SetCol(列, 宽度) / SetRow(行, 高度)设列宽 / 行高
FirstRow() / LastRow() / FirstCol() / LastCol()有数据的区域边界
uo_xl_celltext / num / bool / formula单元格文本 / 数字 / 布尔 / 公式
row / col / format / celltype位置 / 格式 / 类型
celltype 判断单元格类型,常量定义在 uo_xl_base 里:CELLTYPE_EMPTY=0、CELLTYPE_NUMBER=1、CELLTYPE_STRING=2、CELLTYPE_BOOLEAN=3、CELLTYPE_STRICTDATE=6。
二、示例 1:创建工作簿,写入一行报表并保存
先看最核心的流程:新建 → 取/建工作表 → 写单元格 → 保存。写完用 save() 落盘,返回 true 表示成功。
前置:① 应用库列表已加入 websuite.pbl;② 程序运行目录可写。
步骤:1) 打开窗口 w_demo;2) 在按钮 cb_write 的 clicked 事件贴入下方代码;3) 运行并点击按钮,程序目录下生成 report.xlsx,用任意表格软件打开即可看到内容。
// ===== 示例输入:保存路径与工作表名 =====
string ls_file
ls_file = 'D:/report.xlsx' // 目标文件路径(正斜杠即可)
string ls_sheet
ls_sheet = '销售汇总' // 工作表名称
// ===== 创建 xlsx 工作簿(constructor 自动初始化引擎)=====
uo_xlsx book
book = create uo_xlsx
// 默认自带 1 个空工作表,直接取它并改名;SheetCount 从 1 开始
uo_xl_sheet sheet
sheet = book.GetSheet(1)
sheet.name = ls_sheet
// ===== 写表头(第 1 行)=====
uo_xl_cell cell
cell = sheet.GetCell(1, 1)
cell.text = '月份'
cell = sheet.GetCell(1, 2)
cell.text = '销售额'
// ===== 写 3 行数据(第 2~4 行;for 变量先声明,不内联)=====
long ll_r
for ll_r = 1 to 3
cell = sheet.GetCell(ll_r + 1, 1)
cell.text = string(ll_r) + '月' // 文本单元格
cell = sheet.GetCell(ll_r + 1, 2)
cell.num = ll_r * 10000 // 数字单元格
next
// ===== 保存 =====
boolean lb_ok
lb_ok = book.save(ls_file)
if lb_ok then
MessageBox('保存成功', '文件已生成:' + ls_file + '(共 ' + string(book.SheetCount()) + ' 个工作表)')
else
MessageBox('保存失败', '错误:' + book.GetErrorMessage())
end if
destroy book
要点:GetCell(行, 列) 的行列都从 1 起;cell.text 写文本、cell.num 写数字;book.save() 成功后文件立即可用。注意 destroy book 一定要写——uo_xlsx 的 destructor 会调用 xlDestroy() 释放 libxl 资源,不销毁会在多次创建时泄漏。
三、示例 2:打开已有文件,读出单元格内容
读取方向和写完全对称:Open() 打开文件,GetSheet() 取工作表,GetCell() 取单元格,用 text / num 读值,用 celltype 判断类型。
前置:同示例 1;先用示例 1 生成 D:/report.xlsx(或准备任意 xlsx)。
步骤:在按钮 cb_read 的 clicked 事件贴入下方代码;运行后点击按钮,MessageBox 依次显示读到的表头和数据。
// ===== 示例输入:要读取的文件路径 =====
string ls_file
ls_file = 'D:/report.xlsx' // 已存在的 xlsx 文件
uo_xlsx book
book = create uo_xlsx
boolean lb_ok
lb_ok = book.Open(ls_file)
if not lb_ok then
MessageBox('打开失败', '错误:' + book.GetErrorMessage())
destroy book
return
end if
// 取第一个工作表
uo_xl_sheet sheet
sheet = book.GetSheet(1)
// 读第 1 行两个表头(text 属性取文本;取不到时返回空串)
uo_xl_cell cell
cell = sheet.GetCell(1, 1)
string ls_c1
ls_c1 = cell.text
cell = sheet.GetCell(1, 2)
string ls_c2
ls_c2 = cell.text
// 读第 2 行两个数据(num 属性取数字;同时打印 celltype 判断类型)
cell = sheet.GetCell(2, 1)
string ls_r2c1
ls_r2c1 = cell.text
cell = sheet.GetCell(2, 2)
double ld_r2c2
ld_r2c2 = cell.num
int li_type
li_type = cell.celltype
string ls_msg
ls_msg = '表头: ' + ls_c1 + ' / ' + ls_c2 + '~r~n' + '第2行: ' + ls_r2c1 + ' / ' + string(ld_r2c2) + '~r~n' + '第2行第2列类型: ' + string(li_type) + '(1=数字)'
MessageBox('读取结果', ls_msg)
destroy book
补充一句:判断有没有数据,最省事的是 sheet.LastRow() / sheet.LastCol()——只含表头时 LastRow() 返回 1,循环输出报表时常用它做行数上限。
四、示例 3:给表头加粗、设列宽
导出给领导看的报表,表头加粗、列宽合适才像样。这要用到 uo_xl_format(格式)和 uo_xl_font(字体):CreateFormat() 新建格式 → CreateFont() 新建字体 → font.bold = true 加粗 → format.font = font 把字体挂到格式上 → cell.format = format 把格式赋给单元格;列宽用 sheet.SetCol(列, 宽度)。
前置:同示例 1。步骤:在按钮 cb_format 的 clicked 事件贴入下方代码;运行后点击按钮,生成的 report_fmt.xlsx 表头为加粗字体、A 列宽 10、B 列宽 16。
// ===== 示例输入 =====
string ls_file
ls_file = 'D:/report_fmt.xlsx'
uo_xlsx book
book = create uo_xlsx
uo_xl_sheet sheet
sheet = book.GetSheet(1)
// 写表头
uo_xl_cell cell
cell = sheet.GetCell(1, 1)
cell.text = '月份'
cell = sheet.GetCell(1, 2)
cell.text = '销售额'
// ===== 新建格式:字体加粗、水平居中 =====
uo_xl_format fmt
fmt = book.CreateFormat()
uo_xl_font fnt
fnt = book.CreateFont()
fnt.bold = true
fmt.font = fnt
fmt.alignH = 2 // ALIGNH_CENTER=2,居中
// 把格式赋给表头两个单元格
cell = sheet.GetCell(1, 1)
cell.format = fmt
cell = sheet.GetCell(1, 2)
cell.format = fmt
// ===== 设列宽:A 列 10,B 列 16 =====
sheet.SetCol(1, 10.0)
sheet.SetCol(2, 16.0)
boolean lb_ok
lb_ok = book.save(ls_file)
if lb_ok then
MessageBox('保存成功', '已生成带格式的报表:' + ls_file)
else
MessageBox('保存失败', '错误:' + book.GetErrorMessage())
end if
destroy book
uo_xl_base 里还预置了大量常量,比如对齐方式 ALIGNH_LEFT=1 / ALIGNH_CENTER=2 / ALIGNH_RIGHT=3、边框 BORDERSTYLE_THIN=1、填充 FILLPATTERN_SOLID=1 等,写表格时直接引用,不必记数字。
五、常见坑
[*]忘了 destroy:uo_xlsx 释放时要调 xlDestroy(),不 destroy book 会泄漏 libxl 资源,程序里反复创建会越来越慢;
[*]行列从 1 开始:GetCell(1,1) 是 A1,不是 (0,0);
[*]数字 vs 文本:写数字用 cell.num,写文本用 cell.text——把数字当文本写进去,Excel 里会带绿色三角提示、无法求和;
[*]保存失败先看错误:路径不存在、文件被 Excel 占用都会让 save() 返回 false,此时 book.GetErrorMessage() 会给具体原因;
[*]旧版 .xls 文件:uo_xlsx 内部是 xlCreate(2)(xlsx 格式),处理老 .xls 用 uo_xls 对象,用法完全一致。
小结
uo_xlsx 的核心流程只有四步:create 工作簿 → GetSheet 取工作表 → GetCell 写/读单元格 → save/Open 保存或打开;要好看再加一步 CreateFormat 配格式。整个读写过程不依赖本机安装 Excel,服务器上跑批导出也毫无压力。下一篇继续 PBIDEA 的线程组件:用 uo_thread 把耗时操作丢到后台,避免界面卡死。 黄国酬的那个dw2xls,用这个替换掉formone就好了。:lol
或者改写一下 dw2xls 我认同,它确实是 PB 圈最经典的「不装 Excel 也能导出」方案。不过它和 uo_xlsx 是两条不同路线,选谁要看你要什么。
1. dw2xls(黄国酬)= DataWindow 一键导出 .xls
[*]纯 PowerScript 实现,按 BIFF8(Excel 97–2003 的二进制格式)逐字节写文件,不调 COM、不依赖 Office;
[*]把 dw2xls.pbl 加进库列表后,核心就一个全局函数 uf_save_dw_as_excel(dw, filename),返回 1 成功 / -1 失败(另有带参数的 uf_save_dw_as_excel_parm 版本,具体参数以你手里的 pbl 为准);
[*]优点就是你说的省事:一行导出整个数据窗口,表头、分组、合计行跟着一起走,老系统改造成本最低;
[*]边界:产出是老 .xls,扩展名别写成 .xlsx(Excel 会提示格式不匹配);.xls 上限 65536 行 × 256 列;只能写、读不回来,样式和公式控制有限。
2. 你说的 formone 如果是指 Formula One(vcf1.ocx 那个老表格控件)
换掉是对的:它是 32 位 ActiveX,Win10/11 每台机器都得 regsvr32 注册,PB12.5 Classic 又只支持 32 位 OCX,一换机部署就容易掉链子,对 xlsx 的支持也很有限。
3. uo_xlsx(PBIDEA / libxl)= 真 xlsx,可写可读
[*]真 .xlsx(1048576 行 × 16384 列),行列尺寸、字体、边框、数字格式、公式、合并单元格、批注、图片都能精细控制;
[*]能读回来:Open(blob) 可以直接拿数据库里的 blob 打开,save(ref success) 能存成 blob 回库,做模板导入、报表存档很顺手;
[*]代价:没有「一键导出 DataWindow」的现成函数,得自己按行列填。
按这个定位:老项目只想把导出按钮快点换掉 → 用 dw2xls;要 xlsx、要读回来、要精细排版 → 用 uo_xlsx。
4. 最小改动:dw2xls 一行导出
前置:应用库列表加入 dw2xls.pbl。步骤:把原来调 OLE/OCX 的导出代码整段删掉,在按钮 clicked 里贴下面这段,运行后点按钮看 D:/dw_export.xls。
// 适用环境:PB 9 / 11.5 / 12.5 + dw2xls.pbl(黄国酬版),dw_1 已 Retrieve 出数据
// ===== 输入变量(前段声明并直接赋值,逐行说明用途)=====
integer li_ret // 导出结果:1=成功,-1=失败
stringls_file// 目标文件路径;dw2xls 生成的是老版 .xls,扩展名务必写 .xls
ls_file = 'D:/dw_export.xls'
// 一行导出整个数据窗口(表头与分组合计一起带出)
li_ret= uf_save_dw_as_excel(dw_1, ls_file)
if li_ret = 1 then
MessageBox('导出成功', '文件已生成:' + ls_file)
else
MessageBox('导出失败', '请检查路径是否可写,或数据是否超过 65536 行')
end if
5. 要 xlsx 就自己按行列填(uo_xlsx 通用导出片段)
前置:库列表含 websuite.pbl,dw_1 已 Retrieve()。步骤:贴进按钮 clicked,运行后生成 D:/dw_export.xlsx。注意日期列必须用 PackDate 转成 libxl 的日期序列号,否则写进去是一串莫名其妙的数字。
// 适用环境:PB 9+ / PB 12.5 + PBIDEA(websuite.pbl 已在库列表),dw_1 已 Retrieve 出数据
// ===== 输入变量(前段声明并直接赋值,逐行说明用途)=====
string ls_file // 导出的目标 xlsx 路径
string ls_col // 当前列的列名
string ls_coltype// 当前列的数据类型(Describe 取回,如 char(20)/long/decimal(2)/date)
long ll_row // 行循环变量
long ll_col // 列循环变量
long ll_rows // 数据总行数
long ll_cols // 数据总列数
uo_xlsx book // xlsx 工作簿(constructor 自动初始化引擎)
uo_xl_sheet sheet // 工作表
uo_xl_cellcell // 单元格
boolean lb_ok // 保存是否成功
ls_file = 'D:/dw_export.xlsx'
ll_rows = dw_1.RowCount()
ll_cols = long(dw_1.Object.DataWindow.Column.Count)
book= create uo_xlsx
sheet = book.GetSheet(1)
// ===== 写表头:Describe('#N.name') 取列名,再取列名 + '_t.text' 得列标题 =====
for ll_col = 1 to ll_cols
ls_col = dw_1.Describe('#' + string(ll_col) + '.name')
cell = sheet.GetCell(1, ll_col)
cell.text = dw_1.Describe(ls_col + '_t.text')
next
// ===== 写数据:按 ColType 选对应的 GetItemXxx,避免数字被写成文本 =====
for ll_row = 1 to ll_rows
for ll_col = 1 to ll_cols
ls_col = dw_1.Describe('#' + string(ll_col) + '.name')
ls_coltype = dw_1.Describe(ls_col + '.ColType')
cell = sheet.GetCell(ll_row + 1, ll_col) // 第 1 行是表头,数据从第 2 行开始
choose case lower(left(ls_coltype, 3))
case 'cha', 'nch', 'var', 'nva', 'str', 'tex' // 字符类
cell.text = dw_1.GetItemString(ll_row, ll_col)
case 'dat' // date/datetime:libxl 存日期序列号,须 PackDate 转换
cell.num = book.PackDate(dw_1.GetItemDate(ll_row, ll_col))
case else // 数值类:long/int/decimal/number/real/double
cell.num = dw_1.GetItemNumber(ll_row, ll_col)
end choose
next
next
// ===== 保存并输出结果 =====
lb_ok = book.save(ls_file)
if lb_ok then
MessageBox('导出完成', '共 ' + string(ll_rows) + ' 行 ' + string(ll_cols) + ' 列 -> ' + ls_file)
else
MessageBox('导出失败', book.GetErrorMessage())
end if
destroy book // destructor 会调 xlDestroy 释放 libxl 资源,不能漏
几个坑
[*]dw2xls 出的是 .xls,扩展名写成 .xlsx 会被 Excel 警告格式不符;
[*]超 65536 行的老 .xls 会截断,大报表必须走 uo_xlsx;
[*]uo_xlsx 的 GetCell(行, 列) 行列都从 1 开始,不是从 0;
[*]上面片段的 else 分支统一用 GetItemNumber,遇到 blob 列、计算列取不到值,需要单独判断跳过。
pbai
页:
[1]