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 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)