如何在PyQt5的QTreeView中为主条目前置行号(不修改数据模型)
问题描述
现有基于PyQt5实现的树视图代码,生成的GUI中所有条目都没有行号。希望仅在主条目(如示例中的a、b、c)前添加行号,效果为展开/折叠图标前显示行号,且不修改原数据模型,优先考虑插入虚拟列的方案,也可以接受在展开图标前添加标签的方案。
原实现代码如下:
# https://gist.github.com/nbassler/342fc56c42df27239fa5276b79fca8e6 """ Reworked code based on http://trevorius.com/scrapbook/uncategorized/pyqt-custom-abstractitemmodel/ Adapted to Qt5 and fixed column/row bug. TODO: handle changing data. """ import sys from PyQt5 import QtCore, QtWidgets class CustomNode(object): def __init__(self, data): self._data = data if type(data) == tuple: self._data = list(data) if type(data) is str or not hasattr(data, '__getitem__'): self._data = [data] self._columncount = len(self._data) self._children = [] self._parent = None self._row = 0 def data(self, column): if column >= 0 and column < len(self._data): return self._data[column] def columnCount(self): return self._columncount def childCount(self): return len(self._children) def child(self, row): if row >= 0 and row < self.childCount(): return self._children[row] def parent(self): return self._parent def row(self): return self._row def addChild(self, child): child._parent = self child._row = len(self._children) self._children.append(child) self._columncount = max(child.columnCount(), self._columncount) class CustomModel(QtCore.QAbstractItemModel): def __init__(self, nodes): QtCore.QAbstractItemModel.__init__(self) self._root = CustomNode(None) for node in nodes: self._root.addChild(node) def rowCount(self, index): if index.isValid(): return index.internalPointer().childCount() return self._root.childCount() def addChild(self, node, _parent): if not _parent or not _parent.isValid(): parent = self._root else: parent = _parent.internalPointer() parent.addChild(node) def index(self, row, column, _parent=None): if not _parent or not _parent.isValid(): parent = self._root else: parent = _parent.internalPointer() if not QtCore.QAbstractItemModel.hasIndex(self, row, column, _parent): return QtCore.QModelIndex() child = parent.child(row) if child: return QtCore.QAbstractItemModel.createIndex(self, row, column, child) else: return QtCore.QModelIndex() def parent(self, index): if index.isValid(): p = index.internalPointer().parent() if p: return QtCore.QAbstractItemModel.createIndex(self, p.row(), 0, p) return QtCore.QModelIndex() def columnCount(self, index): if index.isValid(): return index.internalPointer().columnCount() return self._root.columnCount() def data(self, index, role): if not index.isValid(): return None node = index.internalPointer() if role == QtCore.Qt.DisplayRole: return node.data(index.column()) return None class MyTree(): """ """ def __init__(self): self.items = [] # Set some random data: for i in 'abc': self.items.append(CustomNode(i)) self.items[-1].addChild(CustomNode(['d', 'e', 'f'])) self.items[-1].addChild(CustomNode(['g', 'h', 'i'])) self.tw = QtWidgets.QTreeView() self.tw.setModel(CustomModel(self.items)) def add_data(self, data): """ TODO: how to insert data, and update tree. """ # self.items[-1].addChild(CustomNode(['1', '2', '3'])) # self.tw.setModel(CustomModel(self.items)) if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) mytree = MyTree() mytree.tw.show() sys.exit(app.exec_())
解决方案
方案一:代理模型添加虚拟列(推荐)
通过实现QAbstractProxyModel子类,在原模型前插入一列作为行号列,仅为主条目(根节点的直接子节点)显示行号,完全不修改原数据模型。
代码实现
在原代码基础上添加代理模型类,并修改MyTree初始化逻辑:
# 原CustomNode、CustomModel代码保留不变 class RowNumberProxyModel(QtCore.QAbstractProxyModel): def __init__(self, parent=None): super().__init__(parent) def mapFromSource(self, sourceIndex): if sourceIndex.isValid(): return self.createIndex(sourceIndex.row(), sourceIndex.column() + 1, sourceIndex.internalPointer()) return QtCore.QModelIndex() def mapToSource(self, proxyIndex): if proxyIndex.isValid(): return self.sourceModel().index(proxyIndex.row(), proxyIndex.column() - 1, proxyIndex.parent()) return QtCore.QModelIndex() def index(self, row, column, parent=QtCore.QModelIndex()): if column == 0: return self.createIndex(row, column, parent.internalPointer()) sourceParent = self.mapToSource(parent) sourceIndex = self.sourceModel().index(row, column - 1, sourceParent) return self.mapFromSource(sourceIndex) def parent(self, index): if index.column() == 0: sourceIndex = self.sourceModel().index(index.row(), 0, self.mapToSource(index.parent())) return self.mapFromSource(sourceIndex.parent()) return self.mapFromSource(self.sourceModel().parent(self.mapToSource(index))) def rowCount(self, parent=QtCore.QModelIndex()): return self.sourceModel().rowCount(self.mapToSource(parent)) def columnCount(self, parent=QtCore.QModelIndex()): return self.sourceModel().columnCount(self.mapToSource(parent)) + 1 def data(self, index, role=QtCore.Qt.DisplayRole): if index.column() == 0 and role == QtCore.Qt.DisplayRole: parent = self.mapToSource(index.parent()) if not parent.isValid(): return str(index.row() + 1) return "" return self.sourceModel().data(self.mapToSource(index), role) def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole): if orientation == QtCore.Qt.Horizontal and section == 0 and role == QtCore.Qt.DisplayRole: return "行号" return self.sourceModel().headerData(section - 1, orientation, role) # 修改MyTree类 class MyTree(): def __init__(self): self.items = [] for i in 'abc': self.items.append(CustomNode(i)) self.items[-1].addChild(CustomNode(['d', 'e', 'f'])) self.items[-1].addChild(CustomNode(['g', 'h', 'i'])) self.tw = QtWidgets.QTreeView() self.source_model = CustomModel(self.items) self.proxy_model = RowNumberProxyModel() self.proxy_model.setSourceModel(self.source_model) self.tw.setModel(self.proxy_model) self.tw.setColumnWidth(0, 40) # 调整行号列宽度
方案二:自定义TreeView绘制行号
直接重写QTreeView的drawRow方法,在展开/折叠图标前绘制行号,无需修改模型,代码更简洁。
代码实现
在原代码基础上添加自定义树视图类,并修改MyTree初始化逻辑:
# 原CustomNode、CustomModel代码保留不变 class NumberedTreeView(QtWidgets.QTreeView): def drawRow(self, painter, option, index): super().drawRow(painter, option, index) # 判断是否是主条目(根节点的直接子节点) parent = index.parent() if not parent.isValid(): row_num = str(index.row() + 1) # 计算绘制位置 indent = self.indentation() * self.depth(index) text_rect = QtCore.QRect(option.rect.left() + indent - 30, option.rect.top(), 25, option.rect.height()) # 绘制行号 painter.save() painter.setFont(self.font()) painter.setPen(QtCore.Qt.black) painter.drawText(text_rect, QtCore.Qt.AlignCenter, row_num) painter.restore() # 修改MyTree类 class MyTree(): def __init__(self): self.items = [] for i in 'abc': self.items.append(CustomNode(i)) self.items[-1].addChild(CustomNode(['d', 'e', 'f'])) self.items[-1].addChild(CustomNode(['g', 'h', 'i'])) self.tw = NumberedTreeView() self.tw.setModel(CustomModel(self.items)) self.tw.setIndentation(40) # 调整缩进给行号留出空间
方案对比
- 代理模型方案:遵循MVC设计,行号作为独立列存在,支持列宽调整、排序跟随,扩展性更强。
- 自定义绘制方案:无需修改模型,代码更轻量化,但行号是绘制的静态内容,无法进行列交互,适合简单场景。
内容的提问来源于stack exchange,提问作者sdbbs
相关产品推荐
相关产品推荐

