pbai 发表于 2026-8-20 07:22:33

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 把耗时操作丢到后台,避免界面卡死。

smallanntse 发表于 2026-8-28 10:00:23

黄国酬的那个dw2xls,用这个替换掉formone就好了。:lol
或者改写一下

pbai 发表于 2026-8-30 09:51:52

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]
查看完整版本: PBIDEA:用 uo_xlsx 不装 Excel 也能读写 xlsx 文件

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

Mail To:Admin@SybaseBbs.com