pbai 发表于 4 天前

PBIDEA:用 TreeView 与菜单树做导航

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_treeviewpbjson.pbltreeview 派生封装从菜单 JSON 文件加载整棵导航树,带 ue_click 回调
vo_treeview_pbideawebsuite.pbltreeview 派生(增强版)用 loadjson(uo_json) / loadxml(uo_xml) 一键渲染;支持拖拽排序、换图标
vo_menutreepbjson.pblDataWindow 派生(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 列)。适合"菜单即数据"的后台管理系统。



六、三个封装怎么选(对比)


维度原生 TreeViewuo_treeviewvo_treeview_pbideavo_menutree
数据来源手写 InsertItem菜单 JSON 文件JSON / XML 内存对象DataWindow + 库表
加载代码量多少(of_retrieve_ex)最少(loadjson)最少(Retrieve)
点击回调自己写事件ue_click 四参读 tvi.data 解析DataWindow 行事件
拖拽排序自己实现不支持内置(drag/drop)不支持
图标换肤手动固定 four 图标ChangeImage/ChangeIconSizeDataWindow 图标
适用场景完全自定义文件式菜单内存 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 也毫无障碍。记住图标文件要带齐、两个封装回调方式不同这两点,基本就不会踩坑。
页: [1]
查看完整版本: PBIDEA:用 TreeView 与菜单树做导航

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

Mail To:Admin@SybaseBbs.com