You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 06:28:16