PB 9.0框架源码实战:数据窗口、事务管理与MDI权限控制

PB 9.0框架源码实战:数据窗口、事务管理与MDI权限控制 简介一个基于PowerBuilder 9.0的完整开发框架附带数据库源码面向需要快速搭建企业级桌面应用的PB开发者也适合希望学习PB数据库交互的入门者。压缩包共157个文件约3.67MB其中107个bmp图片覆盖界面素材18个pbl为PowerBuilder库文件配合ocx、dll等运行库另含mdf/ldf数据库文件及SQL脚本可完整还原框架所依赖的数据环境。框架内部封装了数据库连接、事务处理、错误处理与数据访问对象等通用模块并提供了可直接运行的示例界面方便在此基础上定制业务逻辑。资源在CSDN已有882人学习下载对于想减少重复编码、快速验证PB9.0特性的开发者而言是一份不多见且便于直接参考的源码包。1. 这套 PB 9.0 框架到底解决了什么问题做 PowerBuilder 开发的人十有八九都经历过这种尴尬公司项目紧客户要的是能在 XP 老机器上跑得动的桌面管理程序数据库可能是 Oracle、SQL Server也可能是一台不知道谁维护的 Sybase。你打开 PowerBuilder 9.0先花三天时间搭登录窗口、拼菜单、写数据库连接、把一堆公共函数拷来拷去。等到真正开始写业务数据窗口项目周期已经过去一小半。这个pb9.0 框架源码带数据库.zip压缩包就是在解决这个问题——它把 PB 项目里最重复、最没有技术含量、却又最容易出错的那部分基础工程提前做好了。压缩包里除了框架源码还带了一批界面资源文件Splash.bmp 是启动画面grgallry.bmp 是工具栏按钮位图P_1.bmp、P_2.bmp、P_3.bmp 这些是不同状态下的功能图标。从资源文件组成就能猜出这个框架的定位一个带启动引导、MDI 主窗口、工具栏按钮组的标准 PB 企业应用骨架。它适合两类人一类是刚接手 PB 9.0 维护任务的开发者需要快速搞清楚项目结构另一类是准备从零启动桌面管理项目、不想把时间耗在公共模块上的工程师。框架里的数据窗口封装和事务管理逻辑哪怕你不直接用它拆开看一遍也能学到不少 PB 的老练写法。2. PBL 库结构拆解与环境适配2.1 先把压缩包变成能编译的工程拿到压缩包后第一步不是急着打开各种 .w 窗口对象而是先把目录结构理清楚。常见的 PB 9.0 框架会把对象分散在多个 PBL 库中一般分为应用入口库、窗口对象库、数据窗口库、公用函数库。你解压后应该能看到类似这样的组织方式framework/ ├── app.pbl -- Application 对象与全局函数 ├── dw.pbl -- 数据窗口与用户对象 ├── win.pbl -- 主窗口、登录窗口、子窗口 ├── db.sql -- 数据库初始化脚本 └── bitmaps/ -- bmp 资源文件用 PB 9.0 新建一个 Workspace把这些 PBL 按顺序添加到应用库列表Library List中。库列表的排列顺序很讲究PB 在运行时会按照列表顺序查找对象所以 app.pbl 必须在最上面dw.pbl 次之win.pbl 在后面因为窗口对象依赖数据窗口和公用函数如果顺序反了编译时会报 “Object Not Found”。2.2 SQLCA 初始化与数据库名字源修改框架自带的数据库连接代码通常在 Application 对象的 Open 事件中完成。PB 9.0 连接数据库最常见的方式是在画板里配置 DB Profile但框架代码一般会绕过画板直接写 SQLCA 属性这样交付给客户时只需要改连接字符串不用在每台机器上重新配置 ODBC。// Application Open 事件 SQLCA.DBMS MSS Microsoft SQL Server SQLCA.Database framework_db SQLCA.ServerName 127.0.0.1,1433 SQLCA.LogId sa SQLCA.LogPass your_password SQLCA.AutoCommit False SQLCA.DBParm DisableBind0 CONNECT USING SQLCA; IF SQLCA.SQLCODE 0 THEN MessageBox(数据库连接失败, SQLCA.SQLErrText) RETURN END IF // 打开主窗口 Open(w_main)这段代码的参数说明DBMS指定数据库类型MSS 表示 SQL ServerServerName的写法127.0.0.1,1433能强制走 TCP/IP 端口而不受实例名干扰AutoCommit False非常关键框架的事务控制完全靠手动提交如果你改成 True后面所有事务回滚都会失效。如果要把框架迁移到 Oracle只需要把 DBMS 改为O84 Oracle 8/8iServerName改为 Oracle 的服务名LogId和LogPass换成 Oracle 账号即可。提示修改连接参数后先在 Application 的 Open 事件里加上MessageBox(连接成功, SQLCA.SQLCODE)验证再往下走。这一步排错成本最低。2.3 位图资源的引用路径修正压缩包里的 Splash.bmp、next_butt.bmp、autocad.bmp 等文件在框架源码中通过图片完整路径引用。如果你把工程放在 D 盘而原来的路径是 C 盘启动主窗口时会看到低分辨率图标或者干脆不显示。PB 9.0 处理位图资源不像 .NET 那样会嵌入程序集它编译后 exe 文件虽然包含了位图但开发环境下调试时仍走物理路径。建议在 Workspace 的 Properties 里把Resource File Path指到 bitmaps 目录同时把所有窗口控件的 PictureName 改为相对路径比如bitmaps\next_butt.bmp这样换机器时只需要保证相对结构和压缩包一致。3. 事务管理框架里最不能乱改的部分3.1 框架的事务模型分析这个框架的事务设计遵循了一套很实用的规则每个业务模块通过数据窗口操作数据库但提交和回滚集中在 MessageBox 确认之后执行。拆开它的关键代码你会发现所有更新操作都围绕dw_control.Update()和COMMIT;展开。// 框架中的业务保存模板 long ll_rtn ll_rtn dw_detail.Update() IF ll_rtn 0 THEN COMMIT USING SQLCA; MessageBox(保存成功, 数据已提交) ELSE ROLLBACK USING SQLCA; MessageBox(保存失败, SQLCA.SQLErrText) END IF这段模板看起来普通但有几个容易被忽略的点。Update()返回的是更新影响的行数不是成功与否的布尔值所以判断条件是 0而不是 1因为数据窗口可能同时更新了主表和从表的多行数据。另一方面如果数据窗口带有子数据窗口或者使用了 Grid 风格Update()会逐行拼接 UPDATE 语句一旦某行某字段为空且数据库列不允许 NULL会在执行时报错此时SQLCA.SQLErrText里能看到具体的列名。3.2 显式事务与隔离级别的边界框架没有单独封装事务对象直接用全局的 SQLCA 做事务载体。在单用户或小型局域网应用中这么写没问题但在多人并发场景下需要手动调整隔离级别。PB 9.0 的 SQLCA 支持通过DBParm设置Lock参数// 在 Open 事件中追加 SQLCA.DBParm LockRU, DisableBind0Lock参数的可选值包括RURead Uncommitted、RCRead Committed、RRRepeatable Read、TSSerializable。框架的默认值是空等价于数据库自身的默认隔离级别。SQL Server 下默认是 Read Committed这意味着在长事务中两次读取同一行可能得到不同结果。如果你在框架上做财务类模块建议至少改为RR如果只是查询统计报表RU反而能减少锁等待时间。3.3 数据窗口缓冲区的回滚陷阱PowerBuilder 的数据窗口有四层缓冲区Primary、Filter、Delete、Original。框架里常见的错误是用户修改了几行数据点取消按钮代码直接调用dw_control.Reset()但框架里正确做法应该是// 取消所有未提交的修改 IF dw_control.ModifiedCount() 0 OR dw_control.DeletedCount() 0 THEN ROLLBACK USING SQLCA; dw_control.Retrieve() END IF这里有个细节ROLLBACK只回滚数据库事务数据窗口的 Primary 缓冲区里的值并不会自动回到修改前的状态。必须先ROLLBACK再重新Retrieve()才能把界面上的脏数据刷掉。框架源码里如果某项功能用了Reset()而不是Retrieve()它的数据只是从缓冲区清掉没有触达数据库。理解了这一点你就能解释为什么有时界面显示正确但数据库里还是旧值。4. 数据窗口层框架最有复用价值的部分4.1 框架对数据窗口的二次封装拆开 PBL 后要注意里面自定义用户对象User Object比如u_dw这一类的可视化用户对象它是从 DataWindow 继承来的。框架里所有业务窗口的实际数据窗口控件用的都是这个u_dw类型而不是原生控件。这个继承层干了三件事统一的检索参数入口、分页逻辑、以及打印前处理。// u_dw 用户对象中的统一检索函数 public function integer uf_retrieve(string as_where) // 拼装 where 条件后重新检索 string ls_sql ls_sql this.GetSQLSelect() ls_sql ls_sql WHERE as_where this.SetSQLSelect(ls_sql) return this.Retrieve() end function这个函数的存在意义在于框架内所有窗口的检索不需要各自写dw_1.SetFilter和dw_1.Filter()而是直接传一个 where 子串给uf_retrieve由继承层统一处理 SQL 与过滤的关系。使用时有前提——SetSQLSelect会改变数据窗口与数据库列的绑定关系如果数据窗口里有计算列拼接的 where 条件里不能引用计算列的别名否则报 “Column not found”。4.2 数据窗口翻页不用重新查询的实现Web 系统做分页一般是 limit 参数加排序PB 9.0 桌面应用里对大数据量 DataWindow 的翻页处理框架里给了一个轻量级方案。它利用数据窗口的RetrieveRow事件配合滚动条位置做缓存分页而不是反复访问数据库// 框架中翻页按钮的 Clicked 事件 long ll_cur_row, ll_total ll_cur_row dw_1.GetRow() ll_total dw_1.RowCount() IF dw_1.GetNextModified() 0 THEN MessageBox(提示, 当前页有未保存的修改请先保存或还原) RETURN END IF IF ll_cur_row 20 ll_total THEN dw_1.ScrollToRow(ll_cur_row 20) ELSE dw_1.ScrollToRow(ll_total) END IF这个逻辑的关键是先把数据全部检索到客户端内存翻页只是滚动行的位置不触发数据库往返。适用于数据量在几千行以内的主数据表。如果表有十万行以上这种方案会让首次 Retrieve 卡顿几十秒这时要把数据窗口的Retrieve.AsNeeded选项勾上并配合框架里的 where 条件参数做到按页查询。提示PB 9.0 的ScrollToRow不会触发RetrieveRow事件的分页加载它是纯前端行为。想要真正按需加载必须在u_dw的Retrieve事件里根据当前滚动位置动态修改 SQL 的 ROWCOUNT针对 SQL Server或 ROWNUM针对 Oracle。4.3 数据窗口源码级调优框架里各业务窗口的 DataWindow 对象都有详细的列属性设置这些设置直接决定了数据窗口生成的 SQL 语句质量。几个值得注意的参数Update属性决定哪些列可更新Where子句的生成方式默认为Key Columns即只以主键作为 WHERE 条件如果表没有主键必须改为Key and Updateable Columns否则多用户并发时可能出现覆盖更新框架自带的通用保存函数对这种隐患无能为力。// 运行时可动态调整更新属性 dw_employee.Modify(emp_id.Keyyes) dw_employee.Modify(emp_salary.Updateyes) dw_employee.Modify(emp_salary.Keyno)代码说明第一行把 emp_id 列设为主键列第二行允许 emp_salary 列参与 UPDATE第三行明确阻止 emp_salary 作为 WHERE 条件。这套动态设置适合那些数据窗口对象在画板里没配好、运行时才决定的场景。但要记得Modify的字符串拼写必须和列名大小写完全一致PB 9.0 的 Modify 不区分大小写但列名中带空格或特殊字符时必须用引号括起来。5. MDI 主窗口与菜单权限挂接5.1 框架 MDI 结构下的窗口类型选择这个框架的主界面是典型的 MDI 风格菜单在顶部客户区是 Sheet 子窗口。在 PB 9.0 中 MDI 子窗口的类型必须是main!或mdi!不能是popup!或response!。框架里常见的错误是把弹出窗口类型设为response!然后 Open 在 MDI 客户区这种窗口会强制置顶并且不参与 MDI 的层叠排列。正确的做法是写一个公共函数统一处理子窗口打开// 框架中的窗口打开封装函数 public function integer uf_open_sheet(window aw_window, string as_title) aw_window.WindowType mdi! aw_window.Title as_title OpenSheet(aw_window, w_main, 0, Layered!) return 1 end functionOpenSheet是 PB 专门在 MDI 框架内打开子窗口的函数它的第三个参数表示是否显示在工具栏中0表示不显示Layered!表示层叠排列改成Cascade!则是级联。这套封装的收益是子窗口标题栏上会带上 MDI 父窗口的标题应用内所有子窗口风格一致。注意WindowType必须在 Open 之前设置运行时修改无效。5.2 菜单权限控制的实现方式框架带有菜单权限表数据库中会有类似menu_permission的表结构包含菜单编号、菜单名称、允许的操作级别。主窗口的菜单在初始化时通过遍历菜单项过滤无权限项。这部分代码涉及 PB 的Menu对象操作// w_main 的 open 事件中遍历菜单 integer li_count, li_index string ls_item_name FOR li_index 1 TO MenuId.ItemCount ls_item_name MenuId.Item[li_index].Text IF NOT uf_check_permission(ls_item_name) THEN MenuId.Item[li_index].Visible False END IF NEXT这段代码看似粗糙实际上框架在处理时采用了二级分组策略第一个层级的菜单项区分功能模块第二层级的菜单项才是具体动作。uf_check_permission内部会查询数据库中当前操作员对该菜单项是否有可见权限返回布尔值。要特别注意菜单 Item 的 Text 属性是显示文本如果菜单项带有助记符F之类的字符Text 里也会包含查询数据库时必须去掉了再比对。5.3 菜单与数据窗口联动MDI 框架下菜单要对当前活动子窗口的数据窗口发指令比如“保存”“打印”“查询”。PB 9.0 里可以用w_main.GetActiveSheet()拿到当前活动窗口然后通过窗口的公开函数触发操作// 菜单项“保存”的 clicked 事件 window lw_active lw_active w_main.GetActiveSheet() IF IsValid(lw_active) THEN lw_active.TriggerEvent(ue_save) END IF这里用的是TriggerEvent而非PostEvent区别在于 TriggerEvent 同步执行菜单点击后可以立刻得到保存结果PostEvent 是异步消息如果用户在保存过程中关闭了窗口消息可能投递到无效对象上抛出异常。框架里对对外事件统一命名为ue_前缀这是 PB 社区里约定俗成的自定义事件命名方式ue_save、ue_retrieve、ue_print这种形式在代码检索时一目了然。6. 迁移到 Oracle 与 SQL Server 时的坑和验证方法6.1 数据窗口 SQL 方言的差异处理框架的数据库端文件给的是基础建表和种子数据脚本如果你从 SQL Server 迁移到 Oracle最头痛的不是连接串而是数据窗口的 SQL 语法。PB 9.0 的数据窗口在保存时会检查 SQL 语法但这个检查是在数据库端执行的。处理思路是使用数据窗口的SetSQLSelect在运行前替换方言// Oracle 环境下动态替换分页方言 string ls_sql ls_sql dw_1.GetSQLSelect() ls_sql Replace(ls_sql, TOP 20, ) ls_sql Replace(ls_sql, ORDER BY emp_id, ORDER BY emp_id ROWNUM 20) dw_1.SetSQLSelect(ls_sql)这种字符串替换是最后的手段实际项目里一般直接修改数据窗口对象的 SQL 属性。SQL Server 的分页写法是TOP n加子查询Oracle 是ROWNUM两者不能通用。框架里如果已经建好了若干 DataWindow迁移时要逐个打开数据窗口画板做语法检查。最有效的方式是框架里写一个验证脚本循环遍历指定 PBL 中的所有 DataWindow重新连接数据库并尝试Describe// 批量验证数据窗口 SQL 是否合法 DataWindowChild ldwc string ls_dw_name, ls_syntax integer li_file FOR EACH dw_object IN dw_list ls_syntax dw_object.GetSQLSelect() IF SQLCA.SyntaxFromSQL(ls_syntax, , ) THEN MessageBox(错误, dw_object.Name SQL语法无效) END IF NEXTSyntaxFromSQL是 PB 的静态语法校验函数它不真正执行 SQL只解析词法和语法。这对排查“为什么这个数据窗口打开就报错”很有效因为在画板里复制粘贴的 SQL 往往带着上一个数据库的方言残留。6.2 常见错误的快速定位表现象可能原因检查位置启动时连接失败SQLErrText显示无法打开数据库ServerName端口写法不对或防火墙阻断Application Open 中的 SQLCA 参数数据窗口检索出来全是空行表名或列名大小写问题Oracle 区分大小写DataWindow 画板中 Database 菜单的连接测试中文显示乱码客户端字符集与数据库字符集不一致数据窗口列的Display格式以及DBParm中的CharSet保存时报主键冲突框架的序列生成逻辑未适配目标数据库主键列 Default 值关联 Sequence 或 IdentityRetrieve()返回 -1列绑定失效列被删除或改名后没同步GetSQLSelect与GetColumnName的比对6.3 一个可靠的框架自检方法这个压缩包里的框架代码可以在小范围内自测完整性。写一个无界面的 Service 窗口在它的Open事件里按顺序执行连接数据库、遍历全部数据窗口对象、每个对象做一次Retrieve统计返回行数与错误信息。这样做的好处是能在五分钟内确认框架在你的环境里站不站得住省去挨个窗口点的排查成本。最后值得记住的一点PB 9.0 框架的价值通常不在“能跑起来”而在改造成本。当你把u_dw的事务逻辑吃透、把菜单权限表的数据结构摸清再做新的业务窗口时工作模式会变成“建表 → 用数据窗口画板拖出界面 → 写一行调用 uf_retrieve”大部分时间都花在和业务方核对字段上。本文还有配套的精品资源点击获取