马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PBIDEA:用 TreeView 与菜单树做导航
阅读说明
1. 适用版本:PB 10.5 及以上(含 PBIDEA;本文对象方法以本机 PBIDEA 导出源码核实,PBIDEA 1.x 通用)
2. 支持数据库:不涉及数据库操作(导航树数据来自 JSON/XML 或内存结构,按需自行对接);若用 vo_menutree 的 DataWindow 树则从你的库取菜单, DBMS 不限
3. 操作系统与环境要求:Windows 7+,已安装 PBIDEA 运行库(PbIdea.dll 等);菜单图标文件需放在程序目录的 tmp\ 下(folder.ico / folder_open.ico / folder_item.ico / vip.ico),缺失会运行时报错
4. 难度系数:★★★☆☆(需要熟悉 PB 控件事件与 JSON 基本结构,照做可跟)
5. 其它阅读说明:本文示例把 PBIDEA 的 vo_treeview_pbidea / uo_treeview 直接拖到窗口使用;JSON 节点字段约定为 label(或 text)=显示文本、page(或 id)=目标模块、tip=悬浮提示、items=子节点数组
做管理类软件,左侧一棵导航树几乎是标配:点开"系统管理→用户管理"就跳到对应窗口。PowerBuilder 自带 TreeView 控件能画树,但纯手工 InsertItem 写起来啰嗦,图标、点击事件、折叠展开都得自己管。PBIDEA 封装了三个现成对象,把"JSON/XML 一键变导航树 + 点击回调"这件事做透了。本文把它们的来龙去脉、用法、边界坑一次讲清。
一、四个对象是什么关系
| 对象 | 所在库 | 本质 | 典型用途 | | 原生 TreeView 控件 | PB 内置 | PowerBuilder 标准 treeview | 一切树的基础,所有封装都建立在它之上 | | uo_treeview | pbjson.pbl | treeview 派生封装 | 从菜单 JSON 文件加载整棵导航树,带 ue_click 回调 | | vo_treeview_pbidea | websuite.pbl | treeview 派生(增强版) | 用 loadjson(uo_json) / loadxml(uo_xml) 一键渲染;支持拖拽排序、换图标 | | vo_menutree | pbjson.pbl | DataWindow 派生(d_menutree) | 菜单数据在数据库里时,用树形 DataWindow 直接 Retrieve 出树 |
一句话:想最省事就上 vo_treeview_pbidea(JSON 一行加载);菜单来自文件用 uo_treeview;菜单来自库表用 vo_menutree;要完全自定义再回退原生 TreeView。
二、原生 TreeView 控件基础(必须懂的地基)
不论用哪个封装,底层都是标准 TreeView 方法。掌握这几个,任何树你都能手写:
- InsertItemFirst(handleParent, label, pictureIndex) / InsertItemLast(handleParent, label, pictureIndex):挂到某父节点的最前/最后,返回新节点 handle;handleParent=0 表示树根。
- InsertItem(handleParent, handleAfter, label, pictureIndex):插到指定节点之后。
- FindItem(RootTreeItem!, 0) / FindItem(CurrentTreeItem!, 0) / FindItem(ChildTreeItem!, h) / FindItem(NextTreeItem!, h):按类型定位节点。
- GetItem(handle, ref tvi) / SetItem(handle, tvi):读/写节点(TreeViewItem 结构,含 label、pictureindex、data 等)。
- DeleteItem(handle)、ExpandItem(h)、CollapseItem(h)、ExpandAll(h)、SelectItem(h)。
- SetRedraw(false/true):批量插入前关重绘,性能更好。
下面用一个完整可运行示例,在窗口的 TreeView 控件 tv_1 上建一棵两层树:
前置:窗口 w_nav 上放置 TreeView 控件 tv_1(Picture Mask Color 设好,准备 2 个图标索引)。
步骤:在按钮 cb_build 的 clicked 事件贴入下方代码,运行窗口点按钮即可看到树与 MessageBox。 - // 关闭重绘,批量插入更高效
- tv_1.SetRedraw(false)
- // 示例输入:根节点与两个子系统名称
- string ls_root, ls_sys1, ls_sys2
- ls_root = '系统总览'
- ls_sys1 = '用户管理'
- ls_sys2 = '权限管理'
- // 示例输入:子功能名称
- string ls_sub1, ls_sub2
- ls_sub1 = '新增用户'
- ls_sub2 = '角色分配'
- // 根节点(0 表示挂到树根;pictureindex=1 用第 1 个图标)
- long ll_root
- ll_root = tv_1.InsertItemLast(0, ls_root, 1)
- // 第一级:用户管理
- long ll_sys
- ll_sys = tv_1.InsertItemLast(ll_root, ls_sys1, 1)
- // 第二级:用户管理下的子功能(pictureindex=2 用第 2 个图标)
- long ll_child
- ll_child = tv_1.InsertItemLast(ll_sys, ls_sub1, 2)
- // 第一级:权限管理
- ll_sys = tv_1.InsertItemLast(ll_root, ls_sys2, 1)
- // 第二级:权限管理下的子功能
- ll_child = tv_1.InsertItemLast(ll_sys, ls_sub2, 2)
- // 展开根节点
- tv_1.ExpandItem(ll_root)
- tv_1.SetRedraw(true)
- MessageBox('导航树', '已构建节点:根=' + ls_root + ',子项=' + ls_sub1 + '/' + ls_sub2)
复制代码
读取用户点的是哪个节点,在标准 TreeView 的 doubleclicked(或 selectionchanged)事件里取当前节点:
- // 在 tv_1 的 doubleclicked 事件
- long ll_handle
- TreeViewItem tvi
- ll_handle = this.FindItem(CurrentTreeItem!, 0)
- if ll_handle <= 0 then return
- this.GetItem(ll_handle, tvi)
- MessageBox('你点击了', tvi.label)
复制代码
原生写法的缺点很明显:节点数据、图标、点击事件全要手写,菜单一多就疲于 InsertItem。下面看 PBIDEA 怎么一行解决。
三、vo_treeview_pbidea:JSON 一行变导航树
vo_treeview_pbidea 是增强版 treeview 封装,核心是两个方法:
- loadjson(uo_json json):把一棵 JSON 树渲染成 TreeView,内部自动 reset()、递归插入、再 ExpandAll。
- loadxml(uo_xml xml):同上,但来源是 XML 节点树(用 uo_xml 解析后的根节点)。
JSON 节点约定(源码 _of_loadjson 实际读取的字段):label 或 text=显示文本;page 或 id=目标模块;tip=悬浮提示;obj/param=附带参数;items=子节点数组。还会按节点类型自动选图标:有子节点用 folder 图标(idx 1),叶子用 folder_item(idx 3),标签含 <VIP> 用 vip 图标(idx 4)。
前置:窗口上放置 vo_treeview_pbidea 控件,命名为 tv_nav(它是 treeview 派生可视对象,可直接拖入窗口;tmp\ 下放好四个图标文件)。
步骤:在窗口 open 事件或按钮 clicked 中贴入下面代码,运行即可看到整棵树。 - // 示例输入:导航树的 JSON 结构(label 显示文本;page 目标窗口/模块;tip 悬浮提示;items 子节点数组)
- string ls_json
- ls_json = '{"label":"系统总览","items":[' + &
- '{"label":"用户管理","page":"w_user","tip":"维护系统账号","items":[' + &
- '{"label":"新增用户","page":"w_user_add"},' + &
- '{"label":"用户列表","page":"w_user_list"}]},' + &
- '{"label":"权限管理","page":"w_role","items":[' + &
- '{"label":"角色分配","page":"w_role_assign"}]}' + &
- ']}'
- uo_json lj
- lj = create uo_json
- // 用 Parse 把字符串解析成 JSON 对象(返回 boolean,失败可 GetError() 看原因)
- if not lj.Parse(ls_json) then
- MessageBox('错误', 'JSON 解析失败:' + lj.GetError())
- destroy lj
- return
- end if
- // 一键把 JSON 渲染成树(loadjson 内部会 reset、递归插入、ExpandAll)
- tv_nav.loadjson(lj)
- destroy lj
- MessageBox('导航树', '已加载 JSON 导航树,共 2 个一级菜单')
复制代码
关键差异:vo_treeview_pbidea 没有 of_opentask 自动回调,它不会自己触发 ue_click。要拿到点击的菜单数据,在 doubleclicked(或 selectionchanged)事件里读 TreeViewItem.data——注意源码把节点信息存进了 tvi.data,是一段 JSON 字符串,键是 text / href / obj / param / tip(不是 page)。解析出来即可导航:
- // 在 tv_nav 的 doubleclicked 事件
- long ll_handle
- TreeViewItem tvi
- uo_json lj
- string ls_text, ls_href
- ll_handle = this.FindItem(CurrentTreeItem!, 0)
- if ll_handle <= 0 then return
- this.GetItem(ll_handle, tvi)
- if isnull(tvi.data) then return
- lj = create uo_json
- if not lj.Parse(tvi.data) then
- destroy lj
- return
- end if
- lj.get("text", ls_text)
- lj.get("href", ls_href)
- destroy lj
- // 实际项目里按 href 打开对应窗口;这里用 MessageBox 演示取到的值
- MessageBox('导航跳转', '菜单:' + ls_text + '~n目标模块:' + ls_href)
复制代码
如果你更喜欢"点击直接给四个参数"的回调风格,看下一节的 uo_treeview。
四、uo_treeview:菜单文件 + ue_click 回调
uo_treeview 与 vo_treeview_pbidea 同源思路,但多了从文件加载和ue_click 事件回调:
- of_retrieve_ex():读取内部变量 is_menu(默认 ..\demores\menu_cn.json)指向的菜单 JSON 文件,解析并建树。
- of_parse_json(parentItem, uo_json):把一段 uo_json 递归插成树(公开方法,可自己拼数据后调用)。
- of_reset():清空整棵树。
- of_collapse_all() / of_collapse(handle):折叠。
- of_opentask(handle):根据节点触发 ue_click(text, href, tip, param) 事件——这是它和 vo_treeview_pbidea 最大的不同。
前置:窗口放置 uo_treeview 控件,命名为 tv_menu;程序目录放好菜单 JSON 文件(字段同第三节)。
步骤:窗口 open 事件贴入下面代码。 - // 示例输入:菜单 JSON 文件路径(默认是 ..\demores\menu_cn.json,可改成你自己的)
- string ls_file
- ls_file = '..\demores\menu_cn.json'
- // 赋给控件内部变量 is_menu,再调用 of_retrieve_ex 加载
- tv_menu.is_menu = ls_file
- tv_menu.of_retrieve_ex()
- MessageBox('菜单树', '已从文件加载菜单:' + ls_file)
复制代码
要拿到点击结果,在 doubleclicked 事件调用 of_opentask(handle),它会把节点信息拆好传进 ue_click:
- // 在 tv_menu 的 doubleclicked 事件:触发 ue_click 回调
- this.of_opentask(handle)
复制代码- // 在 tv_menu 的 ue_click(text, href, tip, param) 事件:写导航逻辑
- MessageBox('菜单点击', 'text=' + text + '~nhref=' + href + '~ntip=' + tip)
复制代码注意:of_opentask 是 uo_treeview 独有,vo_treeview_pbidea 没有,后者请用第三节的"读 tvi.data"写法。
五、vo_menutree:菜单在数据库里就用它
vo_menutree 不走 TreeView 手动插入,而是DataWindow 派生(dataobject = d_menutree,一个树形 DataWindow)。当你的菜单本来就存在库表里(id / 父id / 名称),直接用 DataWindow 的树形呈现最自然:
前置:窗口放置 vo_menutree 控件(DataWindow 派生,dataobject 已设为树形 d_menutree);已连接 SQLCA。
步骤:窗口 open 事件贴入下面代码。 - // 示例输入:已连接好的事务对象 SQLCA
- // 树形 DataWindow 按 id / parent 层级自动展开
- vo_menutree_1.SetTransObject(SQLCA)
- vo_menutree_1.Retrieve()
- // 开启鼠标悬浮提示(需要 PBIDEA 的 uo_tooltip 运行库支持)
- vo_menutree_1.of_capture_mouse()
- MessageBox('菜单树', 'DataWindow 树形菜单已加载')
复制代码
它自带 ue_mouseover(row, column, ...) 事件,悬浮到某行会用 uo_tooltip 弹出 tip 提示(tip 取自数据窗的 tip 列)。适合"菜单即数据"的后台管理系统。
六、三个封装怎么选(对比)
| 维度 | 原生 TreeView | uo_treeview | vo_treeview_pbidea | vo_menutree | | 数据来源 | 手写 InsertItem | 菜单 JSON 文件 | JSON / XML 内存对象 | DataWindow + 库表 | | 加载代码量 | 多 | 少(of_retrieve_ex) | 最少(loadjson) | 最少(Retrieve) | | 点击回调 | 自己写事件 | ue_click 四参 | 读 tvi.data 解析 | DataWindow 行事件 | | 拖拽排序 | 自己实现 | 不支持 | 内置(drag/drop) | 不支持 | | 图标换肤 | 手动 | 固定 four 图标 | ChangeImage/ChangeIconSize | DataWindow 图标 | | 适用场景 | 完全自定义 | 文件式菜单 | 内存 JSON/XML 导航 | 数据库菜单 |
七、边界情况与常见坑
- 图标文件必须存在:uo_treeview / vo_treeview_pbidea 默认 picturename[] = {tmp\folder.ico, tmp\folder_open.ico, tmp\folder_item.ico, tmp\vip.ico}。程序目录没有 tmp\folder.ico 等会运行时报错。发布程序时记得带上这四个图标,或改用 ChangeImage(index, icon) 换成你自己的。
- JSON 字段名要看清:vo_treeview_pbidea 把节点信息存进 tvi.data 时用的是 text/href/obj/param/tip;你用 Parse(tvi.data) 后应 get("text",...) / get("href",...),别写成 get("page",...)(page 只在你原始 JSON 里,存进 data 后键名已换成 href)。
- 两个封装的回调方式不同:uo_treeview 用 of_opentask→ue_click;vo_treeview_pbidea 没有 of_opentask,必须自己读 tvi.data。混用会"点了没反应"。
- items 必须是数组:_of_loadjson 对数组走 "/0"、"/1" 索引遍历;如果你把子节点写成对象而非数组,只有第一个会被识别。
- 中文与 ToString:若你用 uo_json 的 Set 动态拼 JSON 再 ToString() 保存,中文默认可能被转义;需要保留中文时用 ToString(true, true)(第二个参数 bEncodeChinese=true 不编码中文)。本文用 Parse 读字符串不受影响。
- 拖拽排序要清理高亮:vo_treeview_pbidea 的 dragdrop 里会 SetDropHighlight(0) 收尾;你自己写拖拽逻辑时别忘了复位,否则高亮残留。
- vo_menutree 依赖树形 DataWindow:d_menutree 必须是 TreeView 风格的数据窗(带层级),且 SQL 返回 id/父id/名称/tip 等列;普通 Grid 数据窗放进去不会成树。
八、进阶扩展
- XML 导航树:vo_treeview_pbidea.loadxml(uo_xml) 直接吃 uo_xml 解析后的根节点,适合拿现成配置文件渲染树(节点名+值+属性全展开)。
- 动态换图标:vo_treeview_pbidea.ChangeImage(3, 'tmp\myicon.ico') 把第 3 个图标换成自己的;ChangeIconSize(20) 统一调图标大小(均通过 pbidea.dll 生效)。
- 拖拽排序:vo_treeview_pbidea 已内置 begindrag/dragdrop/_of_moveitem,把节点拖到另一节点下即成为其子节点,松手后原节点删除——做可配置菜单顺序时直接可用。
- 悬浮提示:of_showtip(msg) 配合 uo_tooltip 可给任意节点加 tooltip;vo_menutree 的 ue_mouseover 已自动接好。
- 和权限结合:在 ue_click / doubleclicked 里先判断当前用户是否有 href 对应模块的权限,无权限 return 不打开,导航树天然就是权限入口。
九、小结
PBIDEA 把"导航树"这件烦心事拆成了三档:最省事用 vo_treeview_pbidea.loadjson(或 loadxml)一行渲染,点击读 tvi.data 解析;菜单在文件里用 uo_treeview.of_retrieve_ex + ue_click 四参回调;菜单在库表里用 vo_menutree 的树形 DataWindow 直接 Retrieve。底层都是标准 TreeView 方法,需要完全掌控时回退手写 InsertItem 也毫无障碍。记住图标文件要带齐、两个封装回调方式不同这两点,基本就不会踩坑。 |