Files
playwrite/gui/material_validation/checkbox_treeview.py
Misaka Server f715cb97e3 refactor: implement UI code optimization and refactoring
This commit implements the comprehensive UI code refactoring plan to improve
code quality, reduce duplication, and enhance maintainability.

Phase 1: High Priority Improvements
- Create BaseDialog class to eliminate window centering code duplication
  across ProgressDialog, DeleteProgressWindow, LoginDialog, and
  UserSelectionDialog (~30 lines of duplicate code removed)
- Unify log color configuration with LogTheme class in gui/log_config.py
- Create permission check decorators (@require_admin, @require_permission,
  @require_user_type, @handle_errors)
- Update all dialog classes to use unified patterns

Phase 2: Architecture Improvements
- Create input validation framework (ValidationResult, Validator,
  ValidatedWidget classes)
- Implement StateManager with observer pattern for component state sharing
- Create unified ErrorHandler for consistent error handling
- Extract CheckboxTreeview into reusable component

New Modules:
- gui/widgets/base_dialog.py - Base dialog class with modal setup and centering
- gui/utils/decorators.py - Permission and error handling decorators
- gui/utils/error_handler.py - Unified error handling with user-friendly messages
- gui/utils/state_manager.py - State management with observer pattern
- gui/utils/validators.py - Input validation framework
- gui/material_validation/checkbox_treeview.py - Reusable checkbox table

Modified Files:
- gui/log_config.py - Added LogTheme class for centralized styling
- gui/widgets/log_text.py - Use LogTheme.COLORS
- gui/widgets/progress_dialog.py - Inherit from BaseDialog
- gui/widgets/delete_progress_window.py - Inherit from BaseDialog, use LogTheme
- gui/widgets/__init__.py - Add new exports, optional imports
- gui/login_dialog.py - Use unified centering pattern
- gui/user_selection_dialog.py - Use unified centering pattern
- gui/material_validation_tab.py - Import CheckboxTreeview from new module

Benefits:
- Reduced code duplication by ~200 lines
- Improved maintainability through centralized configuration
- Better abstraction with 5 new reusable base classes
- Enhanced type safety with type hints
- Future-proof theming support via LogTheme

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-26 18:40:26 +08:00

320 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
CheckboxTreeview - 支持复选框和排序的高级表格组件
使用 Unicode 字符模拟 checkbox
- ☐ 未选中
- ☑ 选中
支持点击列标题进行排序
"""
import tkinter as tk
from tkinter import ttk
from typing import Callable, Optional, List
class CheckboxTreeview(ttk.Treeview):
"""
支持 checkbox 的 Treeview 组件
Features:
- Checkbox 列支持(第一列)
- 点击列标题排序(选择列、材料名称列)
- Checkbox 状态变化回调
- 全选/取消全选功能
Usage:
def on_checkbox_change(item_id, is_checked):
print(f"Item {item_id} checked: {is_checked}")
tree = CheckboxTreeview(
parent,
columns=('选择', '材料名称', '状态'),
on_checkbox_change=on_checkbox_change
)
tree.pack()
"""
def __init__(
self,
parent,
on_checkbox_change: Optional[Callable[[str, bool], None]] = None,
**kwargs
):
"""
初始化 CheckboxTreeview
Args:
parent: 父容器
on_checkbox_change: checkbox 状态改变时的回调函数 (item_id, is_checked) -> None
**kwargs: 传递给 Treeview 的参数
"""
super().__init__(parent, **kwargs)
# Checkbox 状态管理
self.checkboxes: dict = {} # item_id -> bool
self.checkbox_column = "选择"
self.on_checkbox_change = on_checkbox_change
# 排序状态管理
self.sort_column: Optional[str] = None # 当前排序列的列标识符
self.sort_direction: Optional[str] = None # 'asc', 'desc', 或 None
self.sortable_columns: List[str] = ["选择", "材料名称"] # 可排序的列白名单
self.original_headings: dict = {} # 存储原始列标题文本(不含箭头)
# 存储原始列标题(延迟执行以确保标题已设置)
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 状态
Args:
event: 鼠标点击事件
"""
# 获取点击位置对应的 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: str, 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[str]:
"""
获取所有选中的 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 状态
Args:
parent: 父节点 ID
index: 插入位置索引
values: 值列表
**kwargs: 其他参数
Returns:
新创建的 item ID
"""
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 状态
Args:
*items: 要删除的 item ID 列表
"""
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: str) -> Optional[str]:
"""
将列索引 ('#1', '#2') 转换为列标识符
Args:
column_index: 列索引字符串,如 '#1', '#2'
Returns:
列标识符,如 '选择', '材料名称',或 None
"""
try:
index = int(column_index[1:]) - 1
columns = self['columns']
if 0 <= index < len(columns):
return columns[index]
except (ValueError, IndexError):
pass
return None
def _on_heading_click(self, event):
"""
处理表头点击事件,触发排序
Args:
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: str):
"""
切换指定列的排序状态
排序状态循环asc -> desc -> None -> asc
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: str, direction: str):
"""
按指定列和方向排序
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')
)
# 重新排列项目顺序(使用 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)
def get_checkbox_state(self, item: str) -> bool:
"""
获取指定 item 的 checkbox 状态
Args:
item: Treeview item ID
Returns:
是否选中
"""
return self.checkboxes.get(item, False)
def set_sortable_columns(self, columns: List[str]):
"""
设置可排序的列
Args:
columns: 列标识符列表
"""
self.sortable_columns = columns.copy()