certflow.widgets.enhanced_table 源代码

"""增强表格控件 - 支持排序、列管理、复制等高级功能"""

from __future__ import annotations

from typing import override

from PySide6.QtCore import QPoint, Qt, Signal
from PySide6.QtGui import QAction, QKeyEvent, QKeySequence
from PySide6.QtWidgets import (
    QHeaderView,
    QMenu,
    QTableWidget,
    QWidget,
)


[文档] class EnhancedTable(QTableWidget): """增强表格控件 功能: - 点击列头触发服务端排序(排序全量数据,非仅当前页) - 右键菜单隐藏/显示列 - Ctrl+C 复制选中单元格 - Ctrl+A 全选 - 交替行颜色 - 排序指示器显示 """ column_config_changed = Signal() sort_requested = Signal(str, bool) # 请求服务端排序: field_name, desc def __init__(self, parent: QWidget | None = None) -> None: """初始化增强表格控件 Args: parent: 父级窗口部件 """ super().__init__(parent) self._setup_table() self._column_visible: dict[int, bool] = {} self._current_sort_field: str | None = None self._current_sort_desc: bool = False def _setup_table(self) -> None: """初始化表格基本设置 设置交替行颜色、选择行为、禁用编辑、配置列头右键菜单和排序点击事件。 """ self.setAlternatingRowColors(True) self.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self.setSelectionMode(QTableWidget.SelectionMode.ExtendedSelection) self.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) # 禁用客户端排序,改用服务端排序 # self.setSortingEnabled(True) 注释掉,因为 QTableWidget 排序只排当前页 # 设置列头右键菜单 header = self.horizontalHeader() # 仍开启排序指示器显示,否则点击列头排序后列头不显示升/降序箭头 header.setSortIndicatorShown(True) header.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) header.customContextMenuRequested.connect(self._show_column_menu) # 连接列头点击信号 → 触发服务端排序 header.sectionClicked.connect(self._on_header_clicked) def _show_column_menu(self, pos: QPoint) -> None: """显示列管理右键菜单 提供隐藏当前列、隐藏其他列、显示所有列、重置列宽等操作。 Args: pos: 右键点击的屏幕坐标位置 """ header = self.horizontalHeader() column = header.logicalIndexAt(pos) if column < 0: return menu = QMenu(self) # 列名 col_name = ( self.horizontalHeaderItem(column).text() if self.horizontalHeaderItem(column) else f"列{column + 1}" ) title_action = QAction(f"列: {col_name}", self) title_action.setEnabled(False) menu.addAction(title_action) menu.addSeparator() # 隐藏当前列 hide_action = QAction("隐藏此列", self) hide_action.triggered.connect(lambda: self._hide_column(column)) menu.addAction(hide_action) # 隐藏其他列 hide_others_action = QAction("隐藏其他列", self) hide_others_action.triggered.connect(lambda: self._hide_other_columns(column)) menu.addAction(hide_others_action) menu.addSeparator() # 显示所有列 show_all_action = QAction("显示所有列", self) show_all_action.triggered.connect(self._show_all_columns) menu.addAction(show_all_action) # 重置列宽 reset_width_action = QAction("重置列宽", self) reset_width_action.triggered.connect(self._reset_column_widths) menu.addAction(reset_width_action) menu.exec(header.mapToGlobal(pos)) def _hide_column(self, column: int) -> None: """隐藏指定列 Args: column: 要隐藏的列索引 """ self._column_visible[column] = False self.setColumnHidden(column, True) self.column_config_changed.emit() def _hide_other_columns(self, keep_column: int) -> None: """隐藏除指定列外的所有列 Args: keep_column: 需要保留显示的列索引 """ for col in range(self.columnCount()): if col != keep_column: self._column_visible[col] = False self.setColumnHidden(col, True) else: self._column_visible[col] = True self.setColumnHidden(col, False) self.column_config_changed.emit() def _show_all_columns(self) -> None: """显示所有列""" for col in range(self.columnCount()): self._column_visible[col] = True self.setColumnHidden(col, False) self.column_config_changed.emit() def _reset_column_widths(self) -> None: """重置所有列宽为自适应内容宽度,最小宽度50像素""" header = self.horizontalHeader() header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) for col in range(self.columnCount()): if header.sectionSize(col) < 50: header.resizeSection(col, 50) self.column_config_changed.emit()
[文档] @override def keyPressEvent(self, event: QKeyEvent) -> None: """处理键盘快捷键事件 支持 Ctrl+C 复制选中单元格、Ctrl+A 全选。 Args: event: 键盘事件对象 """ if event.matches(QKeySequence.StandardKey.Copy): self._copy_selected_cells() elif event.matches(QKeySequence.StandardKey.SelectAll): self.selectAll() else: super().keyPressEvent(event)
def _copy_selected_cells(self) -> None: """将选中单元格内容以制表符分隔的格式复制到剪贴板""" selected = self.selectedIndexes() if not selected: return # 按行分组 rows = {} for idx in selected: row = idx.row() col = idx.column() if row not in rows: rows[row] = {} item = self.item(row, col) rows[row][col] = item.text() if item else "" # 构建文本 lines = [] for _row, cols in sorted(rows.items()): sorted_cols = sorted(cols.items()) line = "\t".join(str(val) for _, val in sorted_cols) lines.append(line) # 复制到剪贴板 from PySide6.QtGui import QGuiApplication QGuiApplication.clipboard().setText("\n".join(lines)) def _on_header_clicked(self, logical_index: int) -> None: """列头点击时触发服务端排序 获取该列对应的数据库字段名,切换排序方向(同列反转,不同列默认升序), 然后通过 sort_requested 信号通知外部从数据库重新查询。 Args: logical_index: 被点击列的索引 """ header_item = self.horizontalHeaderItem(logical_index) if header_item is None: return field_name = header_item.data(Qt.ItemDataRole.UserRole) if not field_name: return # 切换排序方向:同列反转,不同列默认升序 desc = not self._current_sort_desc if self._current_sort_field == field_name else False self._current_sort_field = field_name self._current_sort_desc = desc # 更新排序指示器 self.update_sort_indicator(field_name, desc) self.sort_requested.emit(field_name, desc)
[文档] def set_header_field(self, column: int, field: str) -> None: """设置列对应的数据库字段名(用于服务端排序) Args: column: 列索引 field: 对应的数据库字段名 """ item = self.horizontalHeaderItem(column) if item: item.setData(Qt.ItemDataRole.UserRole, field)
[文档] def header_field(self, column: int) -> str: """读取列对应的数据库字段名(与 ``set_header_field`` 对应)。""" item = self.horizontalHeaderItem(column) if item is None: return "" return item.data(Qt.ItemDataRole.UserRole) or ""
[文档] def update_sort_indicator(self, field: str, desc: bool) -> None: """更新排序指示器(列头箭头) Args: field: 数据库字段名 desc: True=降序, False=升序 """ header = self.horizontalHeader() for col in range(self.columnCount()): item = self.horizontalHeaderItem(col) if item and item.data(Qt.ItemDataRole.UserRole) == field: order = Qt.SortOrder.DescendingOrder if desc else Qt.SortOrder.AscendingOrder header.setSortIndicator(col, order) break
[文档] def save_state(self) -> dict: """保存列状态(宽度和可见性) Returns: dict: 包含 widths 和 visible 两个键的状态字典 """ header = self.horizontalHeader() return { "widths": {col: header.sectionSize(col) for col in range(self.columnCount())}, "visible": {col: not self.isColumnHidden(col) for col in range(self.columnCount())}, }
[文档] def restore_state(self, state: dict) -> None: """恢复列状态 Args: state: save_state() 返回的状态字典,为空则跳过 """ if not state: return header = self.horizontalHeader() for col, width in state.get("widths", {}).items(): if col < self.columnCount(): header.resizeSection(col, width) for col, visible in state.get("visible", {}).items(): if col < self.columnCount(): self.setColumnHidden(col, not visible)