尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

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

基于PANDAS的QAbstractTableModel实现高级TableView详细解析(八、在TableView实现冻结窗口)
📅 发布时间:2026/6/30 23:08:53

一、原理

与EXCEL的冻结窗口原理一致,在主窗口上面创建一个遮罩层,遮罩层的尺寸恰好为冻结范围的尺寸;

二、需求分析

1.基础

冻结窗口至少要满足以下几点:

  • 共享模型
  • 可以设置冻结区域
  • 调整列宽时冻结区同步变化
  • 窗口大小变化时遮罩层自动调整
from PySide6 import QtCore, QtWidgets class FreezeTableWidget(QtWidgets.QWidget): """ 遮罩式冻结列表格控件 """ def __init__(self, parent=None): 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,setColumnHidden

def 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 # 拦截,不走默认

三、下期

我在资源上传了这部分代码可以自行下载,下期介绍多重表头支持

相关新闻

  • Confluence高危漏洞CVE-2022-26134应急响应与安全加固实战指南
  • 把 Enterprise Services Repository 配成一座稳定的集成设计中枢
  • *如何使用* *bc* 进行高级*数学*计算?

最新新闻

  • 这份大厂Java高频面试题(2026最新版),建议直接收藏
  • Meta Quest 播放软件《下一代视频播放器》NEXt-Gen Video Player 下载和使用教程
  • 【论文复现】存在测距误差的WSN无锚点分布式自定位,《WSN中存在测距误差的无锚点分布式自定位方法》
  • 抖音监控助手:实时追踪博主动态与直播推送的终极指南
  • VisualGGPK2完整指南:快速掌握《流放之路》游戏资源管理技巧
  • Spark SQL 优化:从 Catalyst 优化器到数据倾斜治理,大数据查询的性能调优路径

日新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

周新闻

  • Windows字体自定义终极方案:No!! MeiryoUI完全指南
  • Deepin Boot Maker:告别命令行,3分钟制作Linux启动盘的智能解决方案
  • Plain Craft Launcher 2:重新定义你的Minecraft游戏体验

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号