首页
学习
活动
专区
圈层
工具
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Python Web开发:NiceGUI核心组件-树形组件教程

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

交流讨论:欢迎在评论区留言!

重要提示:本文主要是记录自己的学习与实践过程,所提内容或者观点仅代表个人意见,只是我以为的,不代表完全正确,不喜请勿关注。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OhP37fZ1D_GwprvNdyoox2Dg0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券