NiceGUI树形组件(ui.tree )是一个强大的可视化工具,用于展示层级数据(如文件系统、组织结构、分类目录等)。其核心功能与特性如下:
层级展示:支持多级嵌套结构,通过父子关系动态展开/折叠节点。
交互性:允许用户点击节点展开/折叠,支持自定义事件(如选中、拖拽)。
动态更新:可通过代码实时修改树的结构或节点内容。
样式定制:可调整图标、颜色、缩进等视觉元素。
2.2.1 树形组件基础
基本静态树实现
from nicegui import ui
tree_data = [
{'id':'root',
'label': '根节点',
'children': [
{
'id': '一级节点1',
'label': '一级节点1',
'children': [
{'label': '二级节点1-1'},
{'label': '二级节点1-2'}
]
},
{
'id': '一级节点2',
'label': '一级节点2',
'children': [
{'label': '二级节点2-1'},
{'label': '二级节点2-2'}
]
}
]
}
]
ui.tree(tree_data, label_key='label', children_key='children')
ui.run()
代码效果:
核心配置参数说明
表2-1 树形组件关键参数
数据绑定
交互控制
注意事项
ID 唯一性:node_key字段值必须在整个树中唯一,否则会导致渲染异常。
性能优化:超过 500 个节点时建议配合虚拟滚动(需前端扩展)。
事件冒泡:若需阻止事件冒泡,可在回调中调用e.stop_propagation()。
关键功能示例
基础树形结构(含复选框)
tree = ui.tree(
nodes=[
{
"id": 1,
"label": "Root",
"children": [
{"id": 2, "label": "Leaf 1"},
{"id": 3, "label": "Leaf 2"}
]
}
],
node_key="id",
tick_strategy="leaf", # 仅叶子节点可勾选
on_tick=lambda e: print("Ticked nodes:", e.args["ticked_nodes"])
)
动态交互示例
def handle_select(e):
ui.notify(f"Selected: {e.args['label']}")
defhandle_expand(e):
print(f"Node expanded: {e.args['expanded']}")
tree = ui.tree(
nodes=...,
on_select=handle_select,
on_expand=handle_expand
)
实时更新节点
#添加子节点
tree.add_node({"id": 4, "label": "New Node"}, parent_id=1)
#修改节点标签
tree.update_node(2, {"label": "Renamed Leaf"})
#删除节点
tree.remove_node(node_id=3)
2.2.2 节点交互功能
右键菜单实现
def show_context_menu(e):
with ui.menu() as menu:
ui.menu_item('新建', on_click=lambda: add_node(e.node))
ui.menu_item('删除', on_click=lambda: delete_node(e.node))
menu.show_at(e.args['event'].clientX, e.args['event'].clientY)
tree.on('node-contextmenu', show_context_menu)
2.2.3 高级功能实现
可编辑节点
def edit_node(node):
input = ui.input(value=node['label'])
input.on('change', lambda e: update_node_label(node, e.value))
def update_node_label(node, new_label):
node['label'] = new_label
tree.update_node(node)
拖拽排序功能
tree = ui.tree(...).props('draggable droppable')
def handle_drop(e):
moved_node = e.args['moved']
target_node = e.args['target']
# 更新数据源结构
tree.update_data(reorganize_tree(tree.data, moved_node, target_node))
tree.on('node-drop', handle_drop)
2.2.4 样式与图标定制
自定义节点样式
def node_class(node):
if node.get('important'):
return 'text-red-500 font-bold'
return ''
ui.tree(..., node_class=node_class)
动态图标设置
def node_icon(node):
if node.get('type') == 'folder':
return 'folder'
return 'description'
ui.tree(..., icon=node_icon)
2.2.5 综合案例:文件浏览
from nicegui import ui
import os
from pathlib import Path
from datetime import datetime
classFileBrowser:
def__init__(self):
"""初始化文件浏览器界面"""
self.current_path = None
self.selected_node = None
with ui.row().classes('w-full items-center'):
self.path_label = ui.label(' 当前路径: /').classes('text-lg')
ui.button(' 返回上级', icon='arrow_upward', on_click=self.navigate_up).tooltip(' 返回上级目录')
ui.button(' 刷新', icon='refresh', on_click=self.refresh).tooltip(' 刷新当前目录')
ui.button(' 新建文件夹', icon='create_new_folder', on_click=self.create_folder_dialog).tooltip(' 新建文件夹')
with ui.row().classes('w-full h-96'):
# 使用新版本的 tree API
self.tree = ui.tree([
{'id': '/', 'label': '根目录', 'icon': 'folder', 'children': []}
], label_key='label', on_select=self.on_node_select)
self.tree.classes('w-96 border-r')
with ui.column().classes('w-96 p-4'):
self.node_info = ui.markdown(' 请选择一个文件或文件夹').classes('text-lg')
ui.button(' 打开文件', icon='open_in_browser', on_click=self.open_file).props('flat')
defon_node_select(self, event):
"""处理节点选择事件"""
# 在 NiceGUI 2.15.0 中,事件参数直接包含选中的节点ID
node_id = event.value ifhasattr(event, 'value') else event
self.handle_node_selected(node_id)
asyncdefhandle_node_selected(self, node_id):
"""处理节点选择事件"""
if node_id == '/':
# 处理根目录
self.current_path = '/'
self.path_label.text = '当前路径: /'
self.node_info.content = "### 根目录\n- 类型: 文件夹"
return
try:
path = Path(node_id)
if path.exists():
self.current_path = str(path)
self.path_label.text = f'当前路径: {self.current_path}'
if path.is_dir():
info = f"""
### {path.name}
- 类型: 文件夹
- 路径: `{path}`
"""
# 如果是目录,加载子节点
self.load_children(path)
else:
stat = path.stat()
info = f"""
### {path.name}
- 类型: 文件
- 路径: `{path}`
- 大小: {self.format_size(stat.st_size)}
- 修改时间: {self.format_time(stat.st_mtime)}
"""
self.node_info.content = info
except Exception as e:
ui.notify(f' 无法访问 {node_id}: {str(e)}', type='negative')
defload_children(self, path):
"""加载子节点"""
try:
children = []
for entry in os.scandir(path):
children.append({
'id': entry.path,
'label': entry.name,
'icon': 'folder'if entry.is_dir() else'insert_drive_file',
'children': [] if entry.is_dir() elseNone
})
# 更新树节点的子节点
self.tree._props['nodes'] = [{
'id': '/',
'label': '根目录',
'icon': 'folder',
'children': self.get_drives() if os.name == 'nt'else [{'id': '/', 'label': '/', 'icon': 'folder'}]
}]
self.tree.update()
except Exception as e:
ui.notify(f' 无法加载子节点: {str(e)}', type='negative')
defget_drives(self):
"""获取驱动器列表"""
drives = []
for d in'CDEFGHIJKLMNOPQRSTUVWXYZ':
path = f'{d}:\\'
if os.path.exists(path):
drives.append({
'id': path,
'label': path,
'icon': 'storage',
'children': []
})
return drives
defnavigate_up(self):
"""导航到上级目录"""
ifself.current_path andself.current_path != '/':
parent = str(Path(self.current_path).parent)
self.tree._props['selected'] = parent
self.tree.update()
self.handle_node_selected(parent)
defrefresh(self):
"""刷新当前目录"""
ifself.current_path:
self.tree._props['selected'] = self.current_path
self.tree.update()
self.handle_node_selected(self.current_path)
else:
self.tree._props['selected'] = '/'
self.tree.update()
self.handle_node_selected('/')
defcreate_folder_dialog(self):
"""创建文件夹对话框"""
ifnotself.current_path:
ui.notify(' 请先选择一个目录', type='warning')
return
with ui.dialog() as dialog, ui.card():
ui.label(' 新建文件夹').classes('text-h6')
folder_name = ui.input(' 文件夹名称').classes('w-full')
with ui.row():
ui.button(' 取消', on_click=dialog.close)
ui.button(' 创建', on_click=lambda: self.create_folder(folder_name.value, dialog))
dialog.open()
defcreate_folder(self, name, dialog):
"""创建新文件夹"""
try:
path = Path(self.current_path) / name
path.mkdir(exist_ok=False)
dialog.close()
ui.notify(f' 文件夹 {name} 创建成功')
self.refresh()
except Exception as e:
ui.notify(f' 创建文件夹失败: {str(e)}', type='negative')
defopen_file(self):
"""打开文件"""
ifnotself.current_path ornot Path(self.current_path).is_file():
ui.notify(' 请选择一个文件', type='warning')
return
ui.notify(f' 打开文件: {self.current_path}')
@staticmethod
defformat_size(size):
"""格式化文件大小"""
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024:
returnf"{size:.2f}{unit}"
size /= 1024
returnf"{size:.2f} TB"
@staticmethod
defformat_time(timestamp):
"""格式化时间戳"""
return datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')
FileBrowser()
ui.run(title=' 文件浏览器', port=8080)
代码效果:
2.2.6 性能优化
虚拟滚动实现
ui.tree(
large_data,
label_key='name',
props='virtual-scroll',
style='height: 500px'
)
懒加载策略
2.2.7 常见问题解决
问题1:节点状态不更新
解决方案:
# 正确更新方式
tree.update_node(updated_node)
# 而不是直接修改原数据
问题2:大数据量卡顿
优化方案:
启用虚拟滚动
分批次加载数据
使用Web Worker处理数据
问题3:自定义样式无效
调试技巧:
ui.tree(...).style('border: 1px solid red') # 添加调试边框
2.2.8 练习
任务:开发组织架构管理系统,要求:
支持部门层级管理
实现员工拖拽调整部门
右键菜单(新增/删除部门)
实时保存到本地存储
部门人数统计显示
提示代码结构:
class OrganizationTree:
def__init__(self):
self.tree = ui.tree(...)
self.tree.on('node-drop', self.handle_drop)
defhandle_drop(self, e):
# 处理拖拽逻辑
pass
defsave_to_local(self):
# 实现本地存储
pass
更新日期:2025-05-27
交流讨论:欢迎在评论区留言!
重要提示:本文主要是记录自己的学习与实践过程,所提内容或者观点仅代表个人意见,只是我以为的,不代表完全正确,不喜请勿关注。
领取专属 10元无门槛券
私享最新 技术干货