#!/usr/bin/env python # -*- coding: utf-8 -*- """ CheckboxTreeview 组件 支持 checkbox 的 Treeview 组件,使用 Unicode 字符模拟 checkbox: - ☐ 未选中 - ☑ 选中 """ import tkinter as tk from tkinter import ttk class CheckboxTreeview(ttk.Treeview): """支持 checkbox 的 Treeview 组件 使用 Unicode 字符模拟 checkbox: - ☐ 未选中 - ☑ 选中 Features: - Checkbox 点击切换 - 排序功能(支持按选择状态和材料名称排序) - 全选/取消全选 """ def __init__(self, parent, on_checkbox_change=None, **kwargs): """初始化 CheckboxTreeview Args: parent: 父容器 on_checkbox_change: checkbox 状态改变时的回调函数 **kwargs: 传递给 Treeview 的参数 """ super().__init__(parent, **kwargs) self.checkboxes = {} # item_id -> bool self.checkbox_column = "选择" self.on_checkbox_change = on_checkbox_change # checkbox 状态改变回调 # 排序状态 self.sort_column = None # 当前排序列的列标识符 self.sort_direction = None # 'asc', 'desc', 或 None self.sortable_columns = ["选择", "材料名称"] # 可排序的列白名单 self.original_headings = {} # 存储原始列标题文本(不含箭头) # 存储原始列标题(延迟执行以确保标题已设置) self.after(100, self._store_original_headings) # 绑定点击事件 self.bind("", self._on_click) # 绑定表头点击事件 self.bind("", self._on_heading_click) def _on_click(self, event): """处理点击事件,切换 checkbox 状态""" # 获取点击位置对应的 item 和 column region = self.identify_region(event.x, event.y) # 仅处理单元格点击,不处理表头点击 if region == "cell": column = self.identify_column(event.x) item = self.identify_row(event.y) # 检查是否点击了 checkbox 列(第一列) if column == "#1" and item: # 切换 checkbox 状态 current_state = self.checkboxes.get(item, False) new_state = not current_state self.set_checked(item, new_state) # 通知父组件 checkbox 状态已改变 if self.on_checkbox_change: self.on_checkbox_change(item, new_state) return "break" # 阻止默认行为 def set_checked(self, item, checked: bool): """设置指定 item 的 checkbox 状态 Args: item: Treeview item ID checked: 是否选中 """ self.checkboxes[item] = checked # 更新显示 checkbox_char = "☑" if checked else "☐" values = list(self.item(item, "values")) if values: values[0] = checkbox_char self.item(item, values=values) def get_checked_items(self) -> list: """获取所有选中的 item Returns: List of item IDs """ return [item for item, checked in self.checkboxes.items() if checked] def check_all(self, checked: bool = True): """全选或取消全选 Args: checked: True 为全选,False 为取消全选 """ for item in self.get_children(): self.set_checked(item, checked) def insert(self, parent, index, values=None, **kwargs): """重写 insert 方法,初始化 checkbox 状态""" if values is None: values = [] # 确保第一个值是 checkbox if not values or values[0] not in ["☐", "☑"]: values = ["☐"] + list(values) item = super().insert(parent, index, values=values, **kwargs) # 初始化 checkbox 状态为未选中 checkbox_char = values[0] if values else "☐" self.checkboxes[item] = (checkbox_char == "☑") return item def delete(self, *items): """重写 delete 方法,清理 checkbox 状态""" for item in items: if item in self.checkboxes: del self.checkboxes[item] super().delete(*items) def _store_original_headings(self): """存储原始列标题文本(不含箭头)""" for col in self['columns']: self.original_headings[col] = self.heading(col, 'text') def _get_column_id_from_column_index(self, column_index): """将列索引 ('#1', '#2') 转换为列标识符 Args: column_index: 列索引字符串,如 '#1', '#2' Returns: 列标识符,如 '选择', '材料名称' """ index = int(column_index[1:]) - 1 columns = self['columns'] if 0 <= index < len(columns): return columns[index] return None def _on_heading_click(self, event): """处理表头点击事件,触发排序""" region = self.identify_region(event.x, event.y) if region == "heading": column = self.identify_column(event.x) column_id = self._get_column_id_from_column_index(column) # 仅对可排序列进行排序 if column_id in self.sortable_columns: self._toggle_sort(column_id) def _toggle_sort(self, column_id): """切换指定列的排序状态 Args: column_id: 列标识符(如 '选择', '材料名称') """ # 确定新的排序方向 if self.sort_column == column_id: # 同一列:asc -> desc -> None if self.sort_direction == 'asc': new_direction = 'desc' elif self.sort_direction == 'desc': new_direction = None else: new_direction = 'asc' else: # 不同列:从升序开始 new_direction = 'asc' # 应用排序 if new_direction: self._sort_by_column(column_id, new_direction) self.sort_column = column_id self.sort_direction = new_direction else: # 清除排序状态 self.sort_column = None self.sort_direction = None # 更新表头显示 self._update_heading_display() def _sort_by_column(self, column_id, direction): """按指定列和方向排序 Args: column_id: 列标识符 direction: 'asc' 或 'desc' """ # 收集所有项目及其数据和复选框状态 items_data = [] for item in self.get_children(): values = self.item(item, "values") checkbox_state = self.checkboxes.get(item, False) items_data.append({ 'item_id': item, 'values': values, 'checked': checkbox_state }) # 根据列和方向排序 if column_id == "选择": # 按复选框状态排序(选中在前,未选中在后) items_data.sort(key=lambda x: x['checked'], reverse=(direction == 'desc')) elif column_id == "材料名称": # 按材料名称排序 items_data.sort( key=lambda x: str(x['values'][1]) if len(x['values']) > 1 else "", reverse=(direction == 'desc') ) # 重新排列项目顺序(使用 detach 和 move 保留项目ID和状态) for item_data in items_data: self.move(item_data['item_id'], '', 'end') def _update_heading_display(self): """更新列标题显示(添加/移除排序箭头)""" for col in self['columns']: original = self.original_headings.get(col, col) if col == self.sort_column: # 添加排序箭头 arrow = " ↑" if self.sort_direction == 'asc' else " ↓" self.heading(col, text=original + arrow) else: # 移除箭头,显示原始标题 self.heading(col, text=original)