python设计通用表格类 带右键菜单

这个通用的表格类 功能强大 右键菜单 这类你可以在其他窗体使用 右键自动就来 使用类的效率就来

python 复制代码
import tkinter as tk
from tkinter import ttk, messagebox
from typing import List, Dict, Union
import csv
import traceback
from collections import Counter
class RightClickTreeview(ttk.Treeview):
    """
    支持右键菜单的增强型Treeview控件

    参数:
        parent: 父控件
        show_context_menu: 是否显示右键菜单
        **kwargs: 传递给ttk.Treeview的其他参数
    """


    def __init__(self, parent, show_context_menu=True, **kwargs):
        super().__init__(parent, **kwargs)
        # 存储回调函数
        self.custom_actions = {}


        # 配置项
        self.show_context_menu = show_context_menu
        self.copy_with_headers = True  # 复制时是否包含表头
        self.encoding = "utf-8"  # 导出编码

        # 求和列配置(通过API控制)
        self.sum_columns = []  # 存储需要求和的列名列表
        self.sum_row_id = None  # 求和行的ID

        # 存储点击位置信息
        self.current_row = None
        self.current_column = None
        self.current_column_index = None
        self.current_region = None
        self.current_column_name = None

        # 存储打开的编辑窗口
        self.edit_window = None

        # 初始化右键菜单
        if show_context_menu:
            self._init_context_menus()
            self._bind_events()

    """初始化右键菜单 - 扁平化设计"""
    def _init_context_menus(self):

        # 创建单元格右键菜单
        self.cell_menu = tk.Menu(self, tearoff=0, font=("微软雅黑", 10))
        self._create_cell_menu()

        # 创建列标题右键菜单
        self.column_menu = tk.Menu(self, tearoff=0, font=("微软雅黑", 10))
        self._create_column_menu()



    """创建单元格菜单 - 扁平化"""
    def _create_cell_menu(self):

        # 复制功能
        self.cell_menu.add_command(
            label="复制单元格",
            command=self._copy_cell,
            accelerator="Ctrl+C"
        )
        self.cell_menu.add_command(
            label="复制整个表格",
            command=self._copy_table_with_headers
        )
        self.cell_menu.add_separator()


        # 统计功能
        self.cell_menu.add_command(
            label="列统计",
            command=self._show_column_statistics
        )
        self.cell_menu.add_command(
            label="表格统计",
            command=self._show_table_statistics
        )
        self.cell_menu.add_command(
            label="导出为CSV",
            command=lambda: self.export_data("csv")
        )

        self.cell_menu.add_command(
            label="导出为Excel",
            command=lambda: self.export_data("excel")
        )
    """创建列标题菜单"""
    def _create_column_menu(self):

        self.column_menu.add_command(
            label="复制列标题",
            command=self._copy_column_header
        )
        self.column_menu.add_command(
            label="复制列(带表头)",
            command=self._copy_column_with_header
        )
        self.column_menu.add_command(
            label="复制列(不重复内容)",
            command=self._copy_column_unique
        )
        self.column_menu.add_separator()

        # 排序功能
        self.column_menu.add_command(
            label="升序排序",
            command=lambda: self._sort_by_column(False)
        )
        self.column_menu.add_command(
            label="降序排序",
            command=lambda: self._sort_by_column(True)
        )
        self.column_menu.add_separator()

        # 统计功能
        self.column_menu.add_command(
            label="列统计",
            command=self._show_column_statistics
        )



    """绑定事件"""
    def _bind_events(self):

        # 右键事件
        self.bind("<Button-3>", self._on_right_click)

        # 快捷键绑定
        self.bind("<Control-c>", lambda e: self._copy_cell())
        self.bind("<Control-C>", lambda e: self._copy_table_with_headers())


        #self.bind("<F2>", lambda e: self._edit_cell())

        # 双击编辑
        self.bind("<Double-Button-1>", self._on_double_click)

        # 选择事件
        self.bind("<<TreeviewSelect>>", self._on_selection_changed)
#处理右键点击事件
    def _on_right_click(self, event):
        """
        处理右键点击事件
        参数:
            event: 鼠标事件
        """
        # 确定点击位置
        self.current_region = self.identify_region(event.x, event.y)
        self.current_row = self.identify_row(event.y)
        self.current_column = self.identify_column(event.x)

        # 获取列索引和名称
        if self.current_column:
            self.current_column_index = int(self.current_column.replace("#", "")) - 1
            if self.current_column_index < len(self["columns"]):
                self.current_column_name = self["columns"][self.current_column_index]
            else:
                self.current_column_name = None
        else:
            self.current_column_index = None
            self.current_column_name = None

        # 显示对应的菜单
        if self.current_region == "heading":  # 点击列标题
            self._show_menu_at_position(event, self.column_menu)
        elif self.current_region == "cell":  # 点击单元格
            self._show_menu_at_position(event, self.cell_menu)


    """在指定位置显示菜单"""
    def _show_menu_at_position(self, event, menu):

        try:
            menu.tk_popup(event.x_root, event.y_root)
        finally:
            menu.grab_release()



    def _on_selection_changed(self, event):
        """处理选择变化事件"""
        pass

    # ========== 复制功能 ==========
    """复制选中单元格的内容"""
    def _copy_cell(self):

        if not self.current_row or self.current_column_index is None:
            return

        try:
            # 获取单元格值
            values = self.item(self.current_row).get("values", [])

            if self.current_column_index < len(values):
                cell_value = str(values[self.current_column_index])
                self._copy_to_clipboard(cell_value)
        except Exception as e:
            self._show_error_message("复制失败", str(e))

    """复制整个表格(带表头)"""
    def _copy_table_with_headers(self):

        try:
            # 获取表头
            headers = list(self["columns"])

            # 获取所有数据
            all_data = []
            for item_id in self.get_children():
                if item_id == self.sum_row_id:  # 跳过求和行
                    continue
                row_data = self._get_row_data(item_id)
                all_data.append(row_data)

            # 转换为文本
            text_lines = []
            if headers:
                text_lines.append("\t".join(headers))

            for row in all_data:
                line = "\t".join(str(item) for item in row)
                text_lines.append(line)

            result = "\n".join(text_lines)
            self._copy_to_clipboard(result)
        except Exception as e:
            self._show_error_message("复制失败", str(e))
#"""复制整列数据(带表头)"""
    def _copy_column_with_header(self):

        if self.current_column_index is None:
            return

        try:
            column_data = []

            # 获取列标题
            if self.current_column_index < len(self["columns"]):
                column_name = self["columns"][self.current_column_index]
                column_data.append(column_name)

            # 获取所有行的该列数据(跳过求和行)
            for item_id in self.get_children():
                if item_id == self.sum_row_id:  # 跳过求和行
                    continue
                values = self.item(item_id).get("values", [])
                if self.current_column_index < len(values):
                    column_data.append(str(values[self.current_column_index]))

            result = "\n".join(column_data)
            self._copy_to_clipboard(result)
        except Exception as e:
            self._show_error_message("复制失败", str(e))
#"""复制列中不重复的内容"""
    def _copy_column_unique(self):

        if self.current_column_index is None:
            return

        try:
            unique_values = set()

            # 获取所有行的该列数据(跳过求和行)
            for item_id in self.get_children():
                if item_id == self.sum_row_id:  # 跳过求和行
                    continue
                values = self.item(item_id).get("values", [])
                if self.current_column_index < len(values):
                    value = str(values[self.current_column_index]).strip()
                    if value:  # 只添加非空值
                        unique_values.add(value)

            # 转换为列表并排序
            sorted_values = sorted(list(unique_values))
            result = "\n".join(sorted_values)

            if result:
                self._copy_to_clipboard(result)
                self._show_info_message("复制成功", f"已复制 {len(sorted_values)} 个不重复的值")
            else:
                self._show_info_message("提示", "该列没有有效数据")

        except Exception as e:
            self._show_error_message("复制失败", str(e))
     #"""复制列标题"""
    def _copy_column_header(self):

        if self.current_column_name:
            try:
                self._copy_to_clipboard(self.current_column_name)
            except Exception as e:
                self._show_error_message("复制失败", str(e))
   #"""复制文本到剪贴板"""
    def _copy_to_clipboard(self, text):

        try:
            self.clipboard_clear()
            self.clipboard_append(text)
        except Exception as e:
            self._show_error_message("复制失败", str(e))
    # ========== 编辑功能 ==========
    def _edit_cell(self):
        """编辑选中单元格"""
        if not self.current_row or self.current_column_index is None:
            return

        # 如果点击的是求和行,不允许编辑
        if self.current_row == self.sum_row_id:
            self._show_info_message("提示", "求和行不允许编辑")
            return

        # 如果已有编辑窗口,则关闭它
        if self.edit_window and self.edit_window.winfo_exists():
            try:
                self.edit_window.destroy()
            except:
                pass

        # 获取当前值
        values = self.item(self.current_row).get("values", [])
        current_value = ""
        if self.current_column_index < len(values):
            current_value = str(values[self.current_column_index])

        # 创建编辑对话框
        self._create_edit_dialog(self.current_row, self.current_column_index, current_value)
    #"""创建编辑对话框 - 居中显示"""
    def _create_edit_dialog(self, item_id, column_index, current_value):
        """创建编辑对话框 - 居中显示"""
        # 创建编辑窗口
        self.edit_window = tk.Toplevel(self)
        self.edit_window.title("编辑单元格")
        self.edit_window.geometry("400x200")

        # 获取窗口信息
        column_name = ""
        if column_index < len(self["columns"]):
            column_name = self["columns"][column_index]

        # 使窗口居中
        self._center_window(self.edit_window)
        self.edit_window.transient(self)
        self.edit_window.grab_set()

        # 创建内容框架
        content_frame = ttk.Frame(self.edit_window, padding="20")
        content_frame.pack(fill=tk.BOTH, expand=True)

        # 标题
        title_label = ttk.Label(
            content_frame,
            text=f"编辑单元格 - 列: {column_name}",
            font=("微软雅黑", 10, "bold")
        )
        title_label.pack(anchor=tk.W, pady=(0, 10))

        # 当前值标签
        display_value = current_value[:50] + "..." if len(current_value) > 50 else current_value
        current_label = ttk.Label(
            content_frame,
            text=f"当前值: {display_value}"
        )
        current_label.pack(anchor=tk.W, pady=(0, 10))

        # 输入框
        entry_frame = ttk.Frame(content_frame)
        entry_frame.pack(fill=tk.X, pady=(0, 10))

        ttk.Label(entry_frame, text="新值:").pack(side=tk.LEFT)

        self.edit_entry = tk.Text(entry_frame, height=4, width=40)
        self.edit_entry.pack(side=tk.LEFT, padx=(10, 0), fill=tk.BOTH, expand=True)
        self.edit_entry.insert("1.0", current_value)
        self.edit_entry.focus_set()

        # 按钮框架
        button_frame = ttk.Frame(content_frame)
        button_frame.pack(fill=tk.X, pady=20)

        def save_edit():
            new_value = self.edit_entry.get("1.0", tk.END).strip()

            # 更新单元格值
            values = list(self.item(item_id).get("values", []))
            if column_index < len(values):
                values[column_index] = new_value
                self.item(item_id, values=values)

            # 如果该列设置了求和,重新计算
            if column_index < len(self["columns"]):
                column_name = self["columns"][column_index]
                if column_name in self.sum_columns:
                    self.update_sum()

            self.edit_window.destroy()
            self.edit_window = None

        def cancel_edit():
            self.edit_window.destroy()
            self.edit_window = None

        # 按钮
        ttk.Button(
            button_frame,
            text="保存",
            command=save_edit,
            width=10
        ).pack(side=tk.LEFT, padx=(0, 10))

        ttk.Button(
            button_frame,
            text="取消",
            command=cancel_edit,
            width=10
        ).pack(side=tk.LEFT)

        # 绑定快捷键
        self.edit_window.bind("<Return>", lambda e: save_edit())
        self.edit_window.bind("<Escape>", lambda e: cancel_edit())

        # 窗口关闭事件
        self.edit_window.protocol("WM_DELETE_WINDOW", cancel_edit)
    # ========== 列统计功能 ==========
    def _show_column_statistics(self):
        """显示列统计信息 - 增强版,区分数字列和文本列"""
        if self.current_column_index is None:
            return

        column_name = self.current_column_name
        values = []

        # 获取该列所有值(跳过求和行)
        for item_id in self.get_children():
            if item_id == self.sum_row_id:
                continue
            cell_value = self.get_cell_value(item_id, self.current_column_index)
            if cell_value is not None:
                values.append(cell_value)

        total = len(values)
        if total == 0:
            self._show_info_message("列统计", f"列 '{column_name}' 没有数据")
            return

        # 统计空值
        empty_count = sum(1 for v in values if v == "" or v is None or str(v).strip() == "")
        non_empty = total - empty_count

        # 尝试识别列类型
        numeric_values = []
        text_values = []

        for v in values:
            if v == "" or v is None or str(v).strip() == "":
                continue

            # 尝试转换为数字
            try:
                # 处理可能的千位分隔符
                v_str = str(v).replace(',', '')
                if v_str.replace('.', '', 1).isdigit():  # 检查是否为数字
                    num = float(v_str)
                    numeric_values.append(num)
                else:
                    text_values.append(str(v))
            except:
                text_values.append(str(v))

        # 构建统计信息
        stats_text = f"📊 列统计: {column_name}\n\n"
        stats_text += f"总数: {total}\n"
        stats_text += f"非空值: {non_empty}\n"
        stats_text += f"空值: {empty_count}\n"
        stats_text += f"填充率: {(non_empty/total*100):.1f}%\n\n"

        # 数字列统计
        if numeric_values:
            stats_text += "【数字统计】\n"
            stats_text += f"有效数值: {len(numeric_values)}\n"
            stats_text += f"平均值: {sum(numeric_values)/len(numeric_values):.2f}\n"
            stats_text += f"中位数: {self._calculate_median(numeric_values):.2f}\n"
            stats_text += f"最小值: {min(numeric_values):.2f}\n"
            stats_text += f"最大值: {max(numeric_values):.2f}\n"
            stats_text += f"总和: {sum(numeric_values):.2f}\n"
            stats_text += f"标准差: {self._calculate_std(numeric_values):.2f}\n\n"

            # 数值分布(简单的四分位数)
            if len(numeric_values) >= 4:
                sorted_nums = sorted(numeric_values)
                q1 = sorted_nums[len(sorted_nums)//4]
                q3 = sorted_nums[3*len(sorted_nums)//4]
                stats_text += f"Q1 (25%分位): {q1:.2f}\n"
                stats_text += f"Q3 (75%分位): {q3:.2f}\n"

        # 文本列统计
        if text_values:
            stats_text += "【文本统计】\n"
            stats_text += f"文本值数量: {len(text_values)}\n"

            # 统计出现频率
            if text_values:
                counter = Counter(text_values)
                stats_text += f"唯一值数量: {len(counter)}\n"

                # 显示最常见的5个值
                most_common = counter.most_common(5)
                stats_text += "\n最常见值:\n"
                for value, count in most_common:
                    percentage = (count / len(text_values)) * 100
                    stats_text += f"  {value}: {count}次 ({percentage:.1f}%)\n"

        # 创建统计对话框
        self._create_statistics_dialog(column_name, stats_text)
     #"""创建统计对话框 - 居中显示"""
    def _create_statistics_dialog(self, title, content):

        stats_win = tk.Toplevel(self)
        stats_win.title(f"列统计 - {title}")
        stats_win.geometry("500x600")

        # 使窗口居中
        self._center_window(stats_win)
        stats_win.transient(self)

        # 创建文本框显示统计信息
        text_frame = ttk.Frame(stats_win)
        text_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        text_widget = tk.Text(text_frame, wrap=tk.WORD, font=("Consolas", 10))
        text_widget.pack(fill=tk.BOTH, expand=True)

        text_widget.insert("1.0", content)
        text_widget.config(state=tk.DISABLED)  # 设置为只读

        # 滚动条
        scrollbar = ttk.Scrollbar(text_frame, command=text_widget.yview)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        text_widget.config(yscrollcommand=scrollbar.set)

        # 关闭按钮
        button_frame = ttk.Frame(stats_win)
        button_frame.pack(fill=tk.X, padx=10, pady=10)

        ttk.Button(
            button_frame,
            text="关闭",
            command=stats_win.destroy,
            width=15
        ).pack()

        stats_win.bind("<Escape>", lambda e: stats_win.destroy())
        stats_win.bind("<Return>", lambda e: stats_win.destroy())

    # ========== API控制的求和功能 ==========
    def set_sum_columns(self, columns: Union[str, List[str]]):
        """
        设置需要求和的列

        参数:
            columns: 列名或列名列表,例如:"工资" 或 ["工资", "年龄"]
        """
        if isinstance(columns, str):
            columns = [columns]

        # 验证列名是否存在
        valid_columns = []
        all_columns = list(self["columns"])

        for col in columns:
            if col in all_columns:
                valid_columns.append(col)
            else:
                print(f"警告: 列 '{col}' 不存在,已忽略")

        self.sum_columns = valid_columns
        return valid_columns
   #添加一个需要求和的列
    def add_sum_column(self, column: str):
        """
        添加一个需要求和的列

        参数:
            column: 列名
        """
        if column not in self.sum_columns:
            if column in self["columns"]:
                self.sum_columns.append(column)
                return True
            else:
                print(f"警告: 列 '{column}' 不存在")
                return False
        return True

    def remove_sum_column(self, column: str):
        """
        移除一个求和的列

        参数:
            column: 列名
        """
        if column in self.sum_columns:
            self.sum_columns.remove(column)
            return True
        return False

    def clear_sum_columns(self):
        """清除所有求和列设置"""
        self.sum_columns = []
        if self.sum_row_id:
            self.delete(self.sum_row_id)
            self.sum_row_id = None

    def update_sum(self):
        """
        更新求和行

        调用此方法计算并显示求和行
        """
        if not self.sum_columns:
            # 如果没有任何求和列,删除求和行
            if self.sum_row_id:
                self.delete(self.sum_row_id)
                self.sum_row_id = None
            return

        # 删除旧的求和行
        if self.sum_row_id:
            self.delete(self.sum_row_id)

        # 创建新的求和行数据
        row_values = []
        for i, col in enumerate(self["columns"]):
            if col in self.sum_columns:
                # 计算该列的和
                total = 0
                count = 0
                for item_id in self.get_children():
                    if item_id == self.sum_row_id:  # 跳过求和行
                        continue
                    cell_value = self.get_cell_value(item_id, i)
                    if cell_value is not None:
                        try:
                            # 处理数字
                            v_str = str(cell_value).replace(',', '')
                            if v_str.replace('.', '', 1).isdigit():
                                total += float(v_str)
                                count += 1
                        except:
                            pass
                row_values.append(f"∑: {total:.2f} ({count}项)")
            else:
                row_values.append("")

        # 添加求和行
        self.sum_row_id = self.insert("", tk.END, text="合计", values=row_values, tags=("sum",))

        # 设置求和行样式
        self.tag_configure("sum", background="#E8F4FD", font=("微软雅黑", 9, "bold"))
     #获取求和结果
    def get_sum_result(self, column: str = None) -> Union[float, Dict[str, float]]:
        """
        获取求和结果

        参数:
            column: 列名,如果为None则返回所有求和列的结果

        返回:
            如果指定列名,返回该列的求和值
            如果未指定列名,返回字典 {列名: 求和值}
        """
        results = {}
        all_columns = list(self["columns"])

        for col in self.sum_columns:
            col_index = all_columns.index(col) if col in all_columns else -1
            if col_index >= 0:
                total = 0
                for item_id in self.get_children():
                    if item_id == self.sum_row_id:  # 跳过求和行
                        continue
                    cell_value = self.get_cell_value(item_id, col_index)
                    if cell_value is not None:
                        try:
                            v_str = str(cell_value).replace(',', '')
                            if v_str.replace('.', '', 1).isdigit():
                                total += float(v_str)
                        except:
                            pass
                results[col] = total

        if column:
            return results.get(column, 0.0)
        else:
            return results

    # ========== 表格统计功能 ==========
    def _show_table_statistics(self):
        """显示表格统计信息"""
        total_rows = len(self.get_children())
        if self.sum_row_id:
            total_rows -= 1  # 减去求和行

        total_columns = len(self["columns"])
        selected_rows = len(self.selection())

        # 统计非空单元格
        non_empty_cells = 0
        total_cells = total_rows * total_columns

        for item_id in self.get_children():
            if item_id == self.sum_row_id:  # 跳过求和行
                continue
            values = self.item(item_id).get("values", [])
            for value in values:
                if value and str(value).strip():
                    non_empty_cells += 1

        empty_cells = total_cells - non_empty_cells

        stats_text = f"""
        📊 表格统计信息:
        
        总行数: {total_rows}
        总列数: {total_columns}
        选中行数: {selected_rows}
        
        单元格统计:
        总计: {total_cells}
        非空: {non_empty_cells}
        空值: {empty_cells}
        填充率: {(non_empty_cells/total_cells*100):.1f}%
        """

        self._show_info_message("表格统计", stats_text.strip())

    # ========== 排序功能 ==========
    def _sort_by_column(self, reverse=False):
        """按列排序"""
        if self.current_column_index is None:
            return

        # 获取所有数据(排除求和行)
        data = []
        for item in self.get_children():
            if item == self.sum_row_id:  # 跳过求和行
                continue
            values = self.item(item).get("values", [])
            if self.current_column_index < len(values):
                cell_value = values[self.current_column_index]
                # 尝试转换为数字
                try:
                    v_str = str(cell_value).replace(',', '')
                    if v_str.replace('.', '', 1).isdigit():
                        sort_key = float(v_str)
                    else:
                        sort_key = str(cell_value).lower()
                except:
                    sort_key = str(cell_value).lower()
            else:
                sort_key = ""

            data.append((sort_key, item))

        # 排序
        data.sort(key=lambda x: x[0], reverse=reverse)

        # 重新排列(将求和行放在最后)
        for index, (_, item) in enumerate(data):
            self.move(item, "", index)

        # 如果有求和行,移到最底部
        if self.sum_row_id:
            self.move(self.sum_row_id, "", tk.END)

        # 更新列标题排序指示器
        column_name = self["columns"][self.current_column_index]
        indicator = " ↓" if reverse else " ↑"
        self.heading(column_name, text=f"{column_name}{indicator}")

    # ========== 其他功能 ==========
    """删除选中行"""
    def _delete_row(self):

        selected_items = self.selection()
        if not selected_items:
            return

        # 过滤掉求和行
        valid_items = [item for item in selected_items if item != self.sum_row_id]
        if not valid_items:
            return

        # 确认删除
        confirm = self._ask_yes_no(
            "确认删除",
            f"确定要删除选中的 {len(valid_items)} 行吗?\n此操作不可撤销!"
        )

        if confirm:
            for item_id in valid_items:
                self.delete(item_id)

            # 如果有求和列,重新计算
            if self.sum_columns:
                self.update_sum()

    """全选所有行(排除求和行)"""
    def _select_all(self):
        all_items = [item for item in self.get_children() if item != self.sum_row_id]
        self.selection_set(all_items)


    # ========== 数学计算辅助函数 ==========
    def _calculate_median(self, numbers):
        """计算中位数"""
        if not numbers:
            return 0
        sorted_nums = sorted(numbers)
        n = len(sorted_nums)
        if n % 2 == 0:
            return (sorted_nums[n//2 - 1] + sorted_nums[n//2]) / 2
        else:
            return sorted_nums[n//2]

    """计算标准差"""
    def _calculate_std(self, numbers):

        if len(numbers) <= 1:
            return 0
        mean = sum(numbers) / len(numbers)
        variance = sum((x - mean) ** 2 for x in numbers) / (len(numbers) - 1)
        return variance ** 0.5
    # ========== 窗口工具函数 ==========
    def _center_window(self, window):
        """使窗口居中显示"""
        window.update_idletasks()
        width = window.winfo_width()
        height = window.winfo_height()
        x = (window.winfo_screenwidth() // 2) - (width // 2)
        y = (window.winfo_screenheight() // 2) - (height // 2)
        window.geometry(f'{width}x{height}+{x}+{y}')
    # ========== 公共API:获取选定单元格信息 ==========
    def get_selected_cell_info(self):
        """
        获取选定单元格的详细信息
        增强版:返回更多行信息
        """
        selected_items = self.selection()
        if not selected_items or self.current_column_index is None:
            return None

        item_id = selected_items[0]

        # 获取行信息
        item_data = self.item(item_id)
        values = item_data.get("values", [])
        row_text = item_data.get("text", "")

        # 构建行的字典格式数据
        row_dict = {}
        columns = self["columns"]
        for i, col in enumerate(columns):
            if i < len(values):
                row_dict[col] = values[i]
            else:
                row_dict[col] = ""

        # 获取当前单元格内容
        content = ""
        if self.current_column_index < len(values):
            content = values[self.current_column_index]

        # 获取行索引
        all_items = self.get_children()
        row_index = all_items.index(item_id) if item_id in all_items else -1

        # 检查是否为求和行
        is_sum_row = (item_id == self.sum_row_id)

        # 获取列名称
        column_name = ""
        if self.current_column_index < len(columns):
            column_name = columns[self.current_column_index]

        return {
            "content": content,
            "row_index": row_index,
            "column_index": self.current_column_index,
            "column_name": column_name,
            "item_id": item_id,
            "is_sum_row": is_sum_row,
            "row_text": row_text,
            "first_column_content": values[0] if values else "",
            "all_values": values.copy(),
            "row_dict": row_dict,  # 新增:整行数据字典
            "record_id": values[0] if values else row_text  # 推荐的主键
        }

    """
     获取选定单元格的详细信息

     返回:字典包含以下键值(如果没有选中单元格则返回None):
         - content: 单元格内容
         - row_index: 行索引(从0开始)
         - column_index: 列索引(从0开始)
         - column_name: 列字段名称
         - item_id: Treeview中的item ID
         - is_sum_row: 是否为求和行
         - first_column_content: 该行第一列的内容(通常是ID或关键字段)
         - row_text: 行的显示文本(#0列的内容,通常是序号)
     """


    # ========== 其他公共方法 ==========
    """获取行数据"""
    def _get_row_data(self, item_id):

        item_data = self.item(item_id)
        values = item_data.get("values", [])

        # 如果有显示列,则只获取显示列的数据
        if self["displaycolumns"] and self["displaycolumns"] != ('#all',):
            visible_columns = self["displaycolumns"]
            visible_indices = [self["columns"].index(col) for col in visible_columns if col in self["columns"]]
            visible_indices.sort()
            values = [values[i] for i in visible_indices if i < len(values)]

        return values
   #"""获取指定单元格的值"""
    def get_cell_value(self, item_id, column_index):

        values = self.item(item_id).get("values", [])
        if column_index < len(values):
            return values[column_index]
        return None
    #"""设置指定单元格的值"""
    def set_cell_value(self, item_id, column_index, value):

        values = list(self.item(item_id).get("values", []))
        if column_index < len(values):
            values[column_index] = value
            self.item(item_id, values=values)

            # 如果该列设置了求和,重新计算
            if column_index < len(self["columns"]):
                column_name = self["columns"][column_index]
                if column_name in self.sum_columns:
                    self.update_sum()
            return True
        return False
   #"""搜索数据"""
    def search_data(self, search_text, column_index=None):

        results = []
        search_text = search_text.lower()

        for item_id in self.get_children():
            if item_id == self.sum_row_id:  # 跳过求和行
                continue
            values = self.item(item_id).get("values", [])

            if column_index is not None:
                # 搜索指定列
                if column_index < len(values):
                    cell_value = str(values[column_index]).lower()
                    if search_text in cell_value:
                        results.append(item_id)
            else:
                # 搜索所有列
                for value in values:
                    if search_text in str(value).lower():
                        results.append(item_id)
                        break

        return results
    #"""高亮显示指定行"""
    def highlight_rows(self, item_ids, color="yellow"):

        # 清除之前的高亮
        for item in self.get_children():
            self.tag_configure(item, background="")

        # 设置新的高亮
        for item_id in item_ids:
            if item_id != self.sum_row_id:  # 不修改求和行样式
                self.tag_configure(item_id, background=color)
                self.see(item_id)
    # ========== 对话框工具函数 ==========
    def _show_info_message(self, title, message):
        """显示信息对话框"""
        messagebox.showinfo(title, message)
    def _show_error_message(self, title, message):
        """显示错误对话框"""
        messagebox.showerror(title, message)
    #"""显示确认对话框"""
    def _ask_yes_no(self, title, message):

        return messagebox.askyesno(title, message)

    # ========== 新增:显示记录集功能 ==========
    def display_recordset(self, recordset):
            """
            显示记录集(数据库查询结果)

            参数:
                recordset: 可以是以下格式:
                    1. 字典列表:如 [{"编码": "001", "姓名": "张三"}, ...]
                    2. 二维列表:如 [["001", "张三"], ["002", "李四"]]
                    3. DatabaseQuery对象:需有 get_result() 方法

            使用示例:
                # 在其他.py文件中查询数据库
                result = database.query("SELECT * FROM employees")

                # 在RightClickTreeview中显示
                tree = RightClickTreeview(parent)
                tree.display_recordset(result)
            """
            try:
                # 清空现有数据
                for item in self.get_children():
                    self.delete(item)

                # 清除求和行
                self.sum_columns = []
                if self.sum_row_id:
                    self.delete(self.sum_row_id)
                    self.sum_row_id = None

                # 处理不同格式的数据
                if hasattr(recordset, 'get_result'):
                    # DatabaseQuery对象
                    data = recordset.get_result()
                else:
                    data = recordset

                if not data:
                    return 0

                # 判断数据类型
                if isinstance(data, list) and len(data) > 0:
                    if isinstance(data[0], dict):
                        # 字典列表格式
                        return self._display_dict_list(data)
                    elif isinstance(data[0], (list, tuple)):
                        # 二维列表格式
                        return self._display_2d_list(data)
                    else:
                        # 其他格式,尝试直接插入
                        return self._display_other_format(data)
                else:
                    print("警告: 记录集格式不支持")
                    return 0

            except Exception as e:
                print(f"显示记录集时出错: {e}")
                import traceback
                traceback.print_exc()
                return 0
    # """显示字典列表"""
    def _display_dict_list(self, dict_list):

            if not dict_list:
                return 0

            # 获取列名(使用第一个字典的键)
            columns = list(dict_list[0].keys())

            # 设置列
            self["columns"] = columns

            # 配置列
            self.column("#0", width=50, stretch=False)
            self.heading("#0", text="序号")

            for i, col in enumerate(columns):
                width = max(80, len(col) * 10 + 20)
                self.column(f"#{i + 1}", width=width, minwidth=50)
                self.heading(f"#{i + 1}", text=col)

            # 插入数据
            for i, row_dict in enumerate(dict_list):
                values = []
                for col in columns:
                    values.append(row_dict.get(col, ""))
                self.insert("", tk.END, text=str(i + 1), values=values)

            return len(dict_list)
    #"""显示二维列表"""
    def _display_2d_list(self, data_2d):

            if not data_2d:
                return 0

            # 第一行作为表头,其余作为数据
            if len(data_2d) >= 2:
                # 第一行是表头
                headers = data_2d[0]
                data_rows = data_2d[1:]
            else:
                # 只有一行,没有表头,生成默认列名
                headers = [f"列{i + 1}" for i in range(len(data_2d[0]))]
                data_rows = data_2d

            # 设置列
            self["columns"] = headers

            # 配置列
            self.column("#0", width=50, stretch=False)
            self.heading("#0", text="序号")

            for i, col in enumerate(headers):
                width = max(80, len(str(col)) * 10 + 20)
                self.column(f"#{i + 1}", width=width, minwidth=50)
                self.heading(f"#{i + 1}", text=str(col))

            # 插入数据
            for i, row in enumerate(data_rows):
                self.insert("", tk.END, text=str(i + 1), values=row)

            return len(data_rows)
    #"""显示其他格式的数据"""
    def _display_other_format(self, data):

            # 尝试将数据转换为列表
            if hasattr(data, '__iter__'):
                data_list = list(data)
            else:
                data_list = [data]

            if not data_list:
                return 0

            # 尝试推断列数
            first_item = data_list[0]

            # 如果是一维数据,显示为单列
            if isinstance(first_item, (str, int, float)):
                headers = ["数据"]
                rows = [[item] for item in data_list]
            else:
                # 尝试转换为列表
                try:
                    first_row = list(first_item)
                    headers = [f"列{i + 1}" for i in range(len(first_row))]
                    rows = [list(item) for item in data_list]
                except:
                    # 转换失败,显示为单列
                    headers = ["数据"]
                    rows = [[str(item)] for item in data_list]

            # 设置列
            self["columns"] = headers

            # 配置列
            self.column("#0", width=50, stretch=False)
            self.heading("#0", text="序号")

            for i, col in enumerate(headers):
                width = max(80, len(str(col)) * 10 + 20)
                self.column(f"#{i + 1}", width=width, minwidth=50)
                self.heading(f"#{i + 1}", text=str(col))

            # 插入数据
            for i, row in enumerate(rows):
                self.insert("", tk.END, text=str(i + 1), values=row)

            return len(rows)

    #清空并显示记录集(常用操作的快捷方法)
    def clear_and_display(self, recordset):
            """

            """
            # 清空现有数据
            for item in self.get_children():
                self.delete(item)

            # 显示新数据
            return self.display_recordset(recordset)
    # 导出功能
    def get_all_data(self):
        """获取所有表格数据"""
        data = []
        # 添加表头
        headers = list(self["columns"])
        data.append(headers)
        # 添加数据行
        for item_id in self.get_children():
            row_data = self._get_row_data(item_id)
            data.append(row_data)
        return data
    def export_data(self, format_type, filename=None):
        """
        导出表格数据
        参数:
            format_type: 导出格式 (csv, excel)
            filename: 导出文件名(可选)
        """
        try:
            # 获取数据
            data = self.get_all_data()
            if not data or len(data) <= 1:
                self._show_info_message("提示", "没有数据可以导出")
                return
            # 如果没有指定文件名,弹出保存对话框
            if not filename:
               filename = self._get_save_filename(format_type, prefix="selected_")
               if not filename:
                  return

            # 根据格式导出
            success = False
            if format_type == "csv":
                print("获取保存文件名csv22")

                success = self._export_to_csv(data, filename)

            elif format_type == "excel":
                success = self._export_to_excel(data, filename)

            else:
                self._show_error_message("错误", f"不支持的导出格式: {format_type}")
                return

            if success:
                self._show_info_message("成功", f"数据已导出到:\n{filename}")

        except Exception as e:
            self._show_error_message("导出失败", str(e))
            print(f"导出错误详情: {traceback.format_exc()}")
    def _get_save_filename(self, format_type, prefix=""):
        """获取保存文件名"""


        import datetime
        timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        default_name = f"{prefix}table_export_{timestamp}.{format_type}"
        print("获取保存文件名csv3")
        print(default_name)
        # 尝试使用文件保存对话框
        try:
            from tkinter import filedialog
            filetypes = []

            if format_type == "csv":
                print("获取保存文件名csv2444csv")
                filetypes = [("CSV文件", "*.csv"), ("所有文件", "*.*")]


            if format_type == "excel":
                filetypes = [("Excel文件", "*.xlsx"), ("所有文件", "*.*")]


            filename = filedialog.asksaveasfilename(
              defaultextension=f".{format_type}",
              filetypes=filetypes,
              initialfile=default_name
              )

            return filename if filename else None

        except:
            # 如果对话框失败,使用默认文件名
            return default_name
    def _export_to_csv(self, data, filename):
        """导出为CSV"""
        try:

            with open(filename, 'w', newline='', encoding=self.encoding) as csvfile:
                writer = csv.writer(csvfile)
                writer.writerows(data)
            return True
        except Exception as e:
            self._show_error_message("CSV导出失败", str(e))
            return False
    def _export_to_excel(self, data, filename):
        """导出为Excel"""
        try:
            # 检查pandas是否安装
            try:
                import pandas as pd
            except ImportError:
                self._show_error_message(
                    "Excel导出失败",
                    "需要安装pandas和openpyxl库\n\n"
                    "请运行以下命令安装:\n"
                    "pip install pandas openpyxl"
                )
                return False

            headers = data[0]
            rows = data[1:]

            # 确保所有行的长度一致
            max_len = max(len(headers), max((len(row) for row in rows), default=0))

            # 填充缺失的值
            padded_rows = []
            for row in rows:
                padded_row = list(row)
                if len(padded_row) < max_len:
                    padded_row.extend([''] * (max_len - len(padded_row)))
                padded_rows.append(padded_row)

            df = pd.DataFrame(padded_rows, columns=headers)
            df.to_excel(filename, index=False)
            return True

        except Exception as e:
            self._show_error_message("Excel导出失败", str(e))
            return False

##动态添加右键菜单 在其他 .py 文件中轻松使用 RightClickTreeview 类,并通过右键动态添加菜单 关联到你自定义的函数了。
    def add_custom_menu_item(self, menu_label, user_function):
        """
        添加自定义菜单项到右键菜单
        参数:
            menu_label: 菜单项显示的文字,如"保存到数据库"
            user_function: 用户自己写的函数,表格会调用它
        """
        # 创建包装函数
        def wrapped_function():
            # 1. 获取用户当前选中的信息
            selected_info = self.get_selected_cell_info()
            # 2. 调用用户提供的函数,并传递选中信息
            user_function(selected_info)
        # 添加到菜单最后
        self.cell_menu.add_command(
            label=menu_label,
            command=wrapped_function  # 点击时调用包装函数
        )
        # 保存起来(可选)
        self.custom_actions[menu_label] = user_function

    """处理双击事件 出现表格编辑框
    def _on_double_click(self, event):

        self.current_region = self.identify_region(event.x, event.y)
        if self.current_region == "cell":
            self._edit_cell()
    """


#表格数据双击事件  在其他 .py 文件中轻松使用 RightClickTreeview 类,并通过双击关联到你自定义的函数了。
    def _on_double_click(self, event):
        """
        处理双击事件
        """
        # 确定点击位置
        self.current_region = self.identify_region(event.x, event.y)
        self.current_row = self.identify_row(event.y)
        self.current_column = self.identify_column(event.x)

        # 获取列索引和名称
        if self.current_column:
            self.current_column_index = int(self.current_column.replace("#", "")) - 1
            if self.current_column_index < len(self["columns"]):
                self.current_column_name = self["columns"][self.current_column_index]
            else:
                self.current_column_name = None
        else:
            self.current_column_index = None
            self.current_column_name = None

        # 如果是双击单元格,调用用户自定义的双击处理函数
        if self.current_region == "cell":
            # 调用用户设置的双击处理函数
            if hasattr(self, 'double_click_handler') and self.double_click_handler:
                cell_info = self.get_selected_cell_info()
                if cell_info:
                    self.double_click_handler(cell_info)
    def set_double_click_handler(self, handler):
        """
        设置双击事件处理函数
        参数:
            handler: 用户定义的函数,接收一个参数(单元格信息字典)
        """
        self.double_click_handler = handler
    def add_custom_menu_item(self, menu_label, user_function):
        """
        添加自定义菜单项到右键菜单
        参数:
            menu_label: 菜单项显示的文字,如"保存到数据库"
            user_function: 用户自己写的函数,表格会调用它
        """

        # 创建包装函数
        def wrapped_function():
            # 1. 获取用户当前选中的信息
            selected_info = self.get_selected_cell_info()
            # 2. 调用用户提供的函数,并传递选中信息
            user_function(selected_info)

        # 添加到菜单最后
        self.cell_menu.add_command(
            label=menu_label,
            command=wrapped_function  # 点击时调用包装函数
        )
        # 保存起来(可选)
        self.custom_actions[menu_label] = user_func
相关推荐
计算机毕业编程指导师1 小时前
【计算机毕设选题】基于Spark的拉勾网招聘数据分析系统源码,Python+Django全流程
大数据·hadoop·python·spark·django·招聘·拉勾网
Remember_9931 小时前
深入理解 Java String 类:从基础原理到高级应用
java·开发语言·spring·spring cloud·eclipse·tomcat
duyinbi75171 小时前
TOOD_R50_FPN_Anchor-Based_1x_COCO_列车悬挂部件检测分类实战
python
—Qeyser1 小时前
Flutter组件 - BottomNavigationBar 底部导航栏
开发语言·javascript·flutter
学习3人组1 小时前
大模型轻量化调优(昇腾平台方向)岗位技术名词拆解
人工智能·python
666HZ6661 小时前
数据结构3.0 栈、队列和数组
开发语言·数据结构·算法
bing.shao1 小时前
Golang 在OPC领域的应用
开发语言·后端·golang
知乎的哥廷根数学学派1 小时前
基于物理引导和不确定性量化的轻量化神经网络机械退化预测算法(Python)
人工智能·pytorch·python·深度学习·神经网络·算法·机器学习
xj7573065332 小时前
Django 面试常见问题
python·面试·django