当前位置: 首页 > news >正文

基于PANDAS的QAbstractTableModel实现高级TableView详细解析(九、在TableView实现多重表头)

一、背景

我们在展示一些数据时需要使表头不能编辑,实现这个效果有两种方式1.在flag中将标题行设定为不可编辑,2.直接使用表头,但默认情况下,TableView会将多重表头压扁显示,形式为:

(LEVEL1,LEVEL2,LEVEL3)

我们要做的就是重写表头信息

二、逻辑实现

1.表头获取

在你的QAbstractTableModel中添加get_header_structure,我在模型中存储数据的变量是_full_data替换成你定义的即可

def get_header_structure(self, orientation): "直接获取总数据的表头信息,默认方式的布局刷新机制可能获取到错误的表头" if orientation == Qt.Horizontal: return self._full_data.columns.to_list() return self._full_data.index.to_list()

2.重写QHeaderView高度计算逻辑

我们需要做一个可以自动调整层级的部件,因此这个是必须的

初始化定义:

self._header_level = 1

self._default_style = {} #可以自定义一些数据,做UI美化

然后定义_max_level(层级获取),_update_height(高度设定)

def _max_level(self): "刷新最大层级" m = self.model() if m is None: self._header_level = 1 return if not hasattr(m, "get_header_structure"): self._header_level = 1 return headers = m.get_header_structure(self.orientation()) if not headers: self._header_level = 1 return levels = 1 for h in headers: if isinstance(h, (tuple, list)): levels = max(levels, len(h)) self._header_level = levels def _update_height(self): "设置最小高度" h = self._total_height() if self.orientation() == Qt.Horizontal: self.setMinimumHeight(h) else: self.setMinimumHeight(h) def _total_height(self): heights = self._base_heights() total = 0 for i in range(self._header_level): total += heights[ min(i, len(heights)-1) ] return total def _base_heights(self): '基础高度' return [30]

3.渲染逻辑(2+3的完整代码)

from PySide6 import QtCore, QtGui, QtWidgets from PySide6.QtCore import Qt, QRect from app.shared.views.models.ShowDataModel import ShowDataModel class MultiHeaderView(QtWidgets.QHeaderView): def __init__(self, orientation=Qt.Horizontal, parent=None): super().__init__(orientation, parent) self.setDefaultAlignment(Qt.AlignCenter) self.setSectionsClickable(True) self.setStretchLastSection(False) self._header_level = 1 def _max_level(self): model = self.model() if model is None: self._header_level = 1 return if not hasattr(model, "get_header_structure"): self._header_level = 1 return headers = model.get_header_structure(self.orientation()) if not headers: self._header_level = 1 return level = 1 for h in headers: if isinstance(h, (tuple, list)): level = max(level, len(h)) self._header_level = level def _base_heights(self): return [30] def _total_height(self): heights = self._base_heights() total = 0 for i in range(self._header_level): total += heights[min(i, len(heights) - 1)] return total def _update_height(self): h = self._total_height() self.setMinimumHeight(h) def sizeHint(self): s = super().sizeHint() s.setHeight(self._total_height()) return s def sectionSizeFromContents(self, logicalIndex): s = super().sectionSizeFromContents(logicalIndex) s.setHeight(self._total_height()) return s def _header_texts(self, section): model: ShowDataModel = self.model() if model is None: return [""] if not hasattr(model, "get_header_structure"): return [""] headers = model.get_header_structure(self.orientation()) if not headers: return [""] if section >= len(headers): return [""] data = headers[section] if isinstance(data, (tuple, list)): return [str(i) for i in data] return [str(data)] def _span_range(self, section, level): model = self.model() if model is None: return section, section count = model.columnCount() texts = self._header_texts(section) current = texts[level] if level < len(texts) else "" start = section while start > 0: left = self._header_texts(start - 1) txt = left[level] if level < len(left) else "" if txt != current: break if left[:level] != texts[:level]: break start -= 1 end = section while end < count - 1: right = self._header_texts(end + 1) txt = right[level] if level < len(right) else "" if txt != current: break if right[:level] != texts[:level]: break end += 1 return start, end def paintEvent(self, event): painter = QtGui.QPainter(self.viewport()) painter.setRenderHint(QtGui.QPainter.TextAntialiasing) painter.fillRect( self.viewport().rect(), QtGui.QColor("#FFFFFF") ) model = self.model() if model is None: return heights = self._base_heights() count = ( model.columnCount() if self.orientation() == Qt.Horizontal else model.rowCount() ) for section in range(count): if self.isSectionHidden(section): continue texts = self._header_texts(section) top = 0 for level in range(self._header_level): h = heights[min(level, len(heights) - 1)] start, end = self._span_range(section, level) # 只有合并起点负责绘制 if section != start: top += h continue x = self.sectionViewportPosition(start) width = 0 for c in range(start, end + 1): if self.isSectionHidden(c): continue width += self.sectionSize(c) rect = QRect( x, top, width, h ) text = texts[level] if level < len(texts) else "" self._draw_cell( painter, rect, text ) top += h def _draw_cell( self, painter, rect, text ): painter.save() # 背景 painter.fillRect( rect, QtGui.QColor("#FFFFFF") ) # 边框 pen = QtGui.QPen( QtGui.QColor("#C9C9C9") ) painter.setPen(pen) painter.drawRect( rect.adjusted(0, 0, -1, -1) ) # 字体 font = painter.font() font.setPointSize(10) font.setBold(True) painter.setFont(font) # 文本颜色 painter.setPen( QtGui.QColor("#000000") ) painter.drawText( rect, Qt.AlignCenter, text ) painter.restore() def _sync_header(self): self._max_level() self._update_height() self.reset() self.updateGeometry() self.viewport().update()

4.渲染加速

若你才用了懒加载逻辑,那缓存就有必要了

初始化缓存:self._span_cache = {}
添加列宽变化计算:self.sectionResized.connect(self._on_section_resized)

替换

def _span_range(self, section, level):
return self._span_cache.get(level, {}).get(
section,
(section, section)
)

新增缓存逻辑

def _rebuild_span_cache(self): self._span_cache.clear() model = self.model() if model is None: return count = ( model.columnCount() if self.orientation() == Qt.Horizontal else model.rowCount() ) for level in range(self._header_level): cache = {} start = 0 while start < count: texts = self._header_texts(start) current = ( texts[level] if level < len(texts) else "" ) end = start while end + 1 < count: right = self._header_texts(end + 1) txt = ( right[level] if level < len(right) else "" ) if txt != current: break # 父节点必须一致 if right[:level] != texts[:level]: break end += 1 # 保存整个区间 for i in range(start, end + 1): cache[i] = (start, end) start = end + 1 self._span_cache[level] = cache

新增:

def _on_section_resized(self, *args): self._rebuild_span_cache() self.viewport().update()

替换

def _sync_header(self): self._max_level() self._update_height() # 新增 self._rebuild_span_cache() self.reset() self.updateGeometry() self.viewport().update()

三、总结

以上多重表头加载就完成了,还可以添加样式、背景色之类的设定,这些我放到资源里面了可以自行下载

http://www.gsyq.cn/news/1605736.html

相关文章:

  • 2026算力避坑实测!主流GPU租赁平台稳定性深度评测,告别宕机与算力虚标
  • Paxos算法:如何解决分布式系统中的共识问题
  • 民意调查真伪辨别!四招看懂靠谱民调标准
  • 快消品新零售商城小程序开发
  • 全球AI可见性基础建设:从“信息发布”到“AI记忆持续性”的重构
  • gt-checksum v4.0.0 新功能解读系列文章(4):SSL 加密连接——数据校验传输安全再升级
  • 基于MCP协议构建AI编程助手持久化代码记忆的实战指南
  • OpenMontage:从文本到视频的AI自动化生成框架实践指南
  • D1117 低压差线性稳压电路
  • 5分钟快速上手OWASP Dependency-Check:命令行实战与CI/CD集成指南
  • LoRA训练实战61:Krea2人物角色LoRA保姆级训练教程,几分钟捏出专属IP!
  • 一款H5播放器,搞定所有流媒体协议?EasyPlayer.js流媒体播放器到底有多强
  • AI Agent沙箱是什么?跟Docker容器和虚拟机有什么区别
  • Skills开源项目:为AI Agent提供标准化技能库,实现代码仓库自动化操作
  • 【车载】轮速-AK协议:从电流信号到车辆控制的解码之旅
  • AI 赋能接口自动化测试系列(二):全场景测试数据智能构造Agent Skill
  • 后端架构演进:微服务与单体应用如何选择
  • 2026 年小程序开发公司推荐,靠谱服务商汇总
  • AI Agent多智能体系统在金融投资分析中的实战应用
  • Postman接口自动化测试:从脚本到可视化报告的完整实践
  • TAS5716数字音频功放:从DSP处理到PWM驱动的完整设计指南
  • 打进内网后一脸懵?内网渗透第一步——信息收集决定了你能走多远
  • 字节开源Deer-Flow:AI工作流编排引擎实战,构建可靠应用管道
  • 赛能saillm 产品全景解析:为中小商家打造的 AI 智能客服与营销平台
  • 微信支付V3平台证书过期故障排查与自动更新方案详解
  • DNS攻击链前置到解析层怎么防?IP离线库三步定位恶意C2服务器IP
  • 小型语言模型在代码代理框架中的能效与性能权衡研究
  • 零知识加密神话破灭:密码管理器27种攻击向量深度解析与安全实践
  • AI 生成 UI 的工程化闭环:从 Prompt 约束到质量门禁的完整实践
  • AI产品经理必看!产业链全解析+求职避坑指南,手把手教你找好岗!