基于PANDAS的QAbstractTableModel实现高级TableView详细解析(八、在TableView实现冻结窗口)

📅 2026/6/30 22:57:47
基于PANDAS的QAbstractTableModel实现高级TableView详细解析(八、在TableView实现冻结窗口)
一、原理与EXCEL的冻结窗口原理一致在主窗口上面创建一个遮罩层遮罩层的尺寸恰好为冻结范围的尺寸二、需求分析1.基础冻结窗口至少要满足以下几点共享模型可以设置冻结区域调整列宽时冻结区同步变化窗口大小变化时遮罩层自动调整from PySide6 import QtCore, QtWidgets class FreezeTableWidget(QtWidgets.QWidget): 遮罩式冻结列表格控件 def __init__(self, parentNone): super().__init__(parent) self._freeze_cols 0 self.mainView QtWidgets.QTableView(self) self.frozenView QtWidgets.QTableView(self) self._init_views() self._layout_main() self._connect_headers() # ------------------------------------------------------------------ # 初始化 # ------------------------------------------------------------------ def _init_views(self): for view in (self.mainView, self.frozenView): view.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) view.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) header view.horizontalHeader() header.setSectionResizeMode(QtWidgets.QHeaderView.Interactive) header.setStretchLastSection(False) # frozenView 专属配置 self.frozenView.verticalHeader().hide() self.frozenView.setHorizontalScrollBarPolicy( QtCore.Qt.ScrollBarAlwaysOff ) self.frozenView.setVerticalScrollBarPolicy( QtCore.Qt.ScrollBarAlwaysOff ) def _layout_main(self): layout QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self.mainView) # frozenView 作为覆盖层 self.frozenView.setParent(self) self.frozenView.raise_() self.frozenView.hide() def _connect_headers(self): self.mainView.horizontalHeader().sectionResized.connect( self._on_main_column_resized ) # ------------------------------------------------------------------ # model # ------------------------------------------------------------------ def setModel(self, model: QtCore.QAbstractItemModel): self.mainView.setModel(model) self.frozenView.setModel(model) def model(self): return self.mainView.model() def setItemDelegate(self, delegate): self.mainView.setItemDelegate(delegate) self.frozenView.setItemDelegate(delegate) # ------------------------------------------------------------------ # 冻结列 # ------------------------------------------------------------------ def set_frozen(self, count: int): self._freeze_cols max(0, count) self._apply_frozen() def _apply_frozen(self): model self.mainView.model() if not model or self._freeze_cols 0: self.frozenView.hide() return self.frozenView.show() self._sync_all_column_widths() self._update_frozen_geometry() def _sync_all_column_widths(self): for col in range(self._freeze_cols): self.frozenView.setColumnWidth( col, self.mainView.columnWidth(col) ) def _on_main_column_resized(self, logical, old, new): if logical self._freeze_cols: self.frozenView.setColumnWidth(logical, new) self._update_frozen_geometry() # ------------------------------------------------------------------ # 遮罩层几何 # ------------------------------------------------------------------ def _update_frozen_geometry(self): if self._freeze_cols 0: self.frozenView.hide() return width sum( self.mainView.columnWidth(i) for i in range(self._freeze_cols) ) header self.mainView.horizontalHeader() header_height header.height() self.frozenView.setGeometry( self.mainView.viewport().x() - 2, header.geometry().bottom() - header_height, width, self.mainView.viewport().height() header_height 2 ) self.frozenView.raise_() def resizeEvent(self, event): super().resizeEvent(event) self._update_frozen_geometry()2.滚动跟随 行高同步1阶段完成后我们的代码就实现基础的遮罩功能了但是当我们使用滚动加载时会发现冻结的部分不会跟着移动第二阶段我们要实现mainView ↔ frozenView 垂直滚动同步行高同步保证行完全对齐a.在__init__里新增调用位置self._connect_headers()后面self._connect_scroll()def _connect_scroll(self): self.mainView.verticalScrollBar().valueChanged.connect( self.frozenView.verticalScrollBar().setValue ) self.frozenView.verticalScrollBar().valueChanged.connect( self.mainView.verticalScrollBar().setValue )b.同步行高在_connect_headers()中追加self.mainView.verticalHeader().sectionResized.connect(self._on_row_resized)def _on_row_resized(self, row, old, new): self.frozenView.setRowHeight(row, new)3.同步选中完成2阶段后作为展示模型就算是合格了但你会发现冻结区选中会断开3阶段要完成下面的目标两个 view 使用同一个 selectionModel将setModel替换def setModel(self, model): self.mainView.setModel(model) self.frozenView.setModel(model) self.frozenView.setSelectionModel( self.mainView.selectionModel() )重写函数def selectionModel(self): return self.mainView.selectionModel() def selectedIndexes(self): return self.mainView.selectedIndexes() def setSelectionMode(self, mode): self.mainView.setSelectionMode(mode) self.frozenView.setSelectionMode(mode) def setSelectionBehavior(self, behavior): self.mainView.setSelectionBehavior(behavior) self.frozenView.setSelectionBehavior(behavior)4.事件穿透完成3阶段后冻结区域依旧是没法复制的现在我们就需要将冻结的事件和主视图绑定a.安装事件管理器在init的最后添加self.frozenView.viewport().installEventFilter(self)b.逻辑实现def eventFilter(self, obj, event): if obj is self.frozenView.viewport(): if event.type() in ( QtCore.QEvent.MouseButtonPress, QtCore.QEvent.MouseButtonRelease, QtCore.QEvent.MouseMove, ): # 坐标映射到 mainView pos self.mainView.viewport().mapFromGlobal( self.frozenView.viewport().mapToGlobal(event.pos()) ) proxy QtGui.QMouseEvent( event.type(), pos, event.globalPosition(), event.button(), event.buttons(), event.modifiers(), ) QtWidgets.QApplication.sendEvent( self.mainView.viewport(), proxy ) return True return super().eventFilter(obj, event)5.增强支持a,setColumnHiddendef setColumnHidden(self, col, hide): if col self._freeze_cols: self.frozenView.setColumnHidden(col, hide) else: self.mainView.setColumnHidden(col, hide)b,model刷新保护def _bind_model_signals(self, model): model.modelReset.connect(self._schedule_apply) model.layoutChanged.connect(self._schedule_apply) def _schedule_apply(self): QtCore.QTimer.singleShot(0, self._apply_frozen)二.关联自定义模型的整行选择这个是给看前面章节的同学准备的需要在事件管理器的最前面添加下面的代码即可if event.type() QtCore.QEvent.Type.MouseButtonPress: # 判断是不是点击在任意 view 上 if isinstance(obj, QtWidgets.QWidget) and obj is self.mainView.viewport(): pos event.pos() index self.mainView.indexAt(pos) if not index.isValid(): return False model self.mainView.model() source_model model source_index index if isinstance(model, QtCore.QSortFilterProxyModel): source_model model.sourceModel() source_index model.mapToSource(index) if getattr(source_model, checkbox_status, False) and getattr(source_model, row_select_enable, False): source_model.toggle_row_check(source_index) return True # 拦截不走默认三、下期我在资源上传了这部分代码可以自行下载下期介绍多重表头支持