Apply Black formatter to the entire codebase for consistent code style. Co-Authored-By: Claude (glm-5) <noreply@anthropic.com>
241 lines
8.0 KiB
Python
241 lines
8.0 KiB
Python
#!/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("<Button-1>", self._on_click)
|
||
# 绑定表头点击事件
|
||
self.bind("<ButtonRelease-1>", 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)
|