基于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 1self._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.渲染逻辑23的完整代码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, orientationQt.Horizontal, parentNone): 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()三、总结以上多重表头加载就完成了还可以添加样式、背景色之类的设定这些我放到资源里面了可以自行下载