Prepend a main row counter as first column/text to QTreeView in PyQt5?

The example code below generates a GUI like this:

What I would like, is to prepend a row count before the main entries only (a, b and c in the image), as shown on the manually edited image:

How can I do that – without changing the data / data model? (I guess I’d prefer a whole new “virtual column” being inserted, but I guess some hack that just prepends a label to the expand/collapse icon/handle might work just as well …)

# 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_())

OK, I think I’ve got it solved, based on the solution in Add additional information to items in a QTreeView/QFileSystemModel :

  • Have the CustomModel return a “hacked” columnCount() value, increased by 1 from the actual column count of the data
  • In CustomModel.data(), check if we’re in the last “hacked”/added column, if so, determine whether the node is “first level” (could not find another way for this, other than passing a reference to the holder of the data, here MyTree, to CustomModel – so that we can retrieve the data MyTree.items inside it), if it is, output row number (else output nothing)
  • In CustomModel.data(), also check via section if we’re in the last “hacked”/added column, if so, output #
  • Wherever the .setModel() is run on the table view, after that, add .moveSection to visually move the “hacked”/added column elsewhere – in this case, at the beginning (leftmost position), which is column index 0.

With that, I get code that generates this GUI:

… which is exactly what I wanted (the new first column is a bit too wide now, but I guess that is not too hard to fix).

It was tricky to apply the original solution from the cited answer, because they seemingly call just columnCount(), for which I was getting the error “CustomModel.columnCount() missing 1 required positional argument: ‘index'”.

# 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, dataparent, nodes):
    QtCore.QAbstractItemModel.__init__(self)
    self._root = CustomNode(None)
    self._dataparent = dataparent
    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):
    hack = 1 # /q/46835109
    if index.isValid():
      return index.internalPointer().columnCount() +hack
    return self._root.columnCount() +hack

  def data(self, index, role):
    extra = False
    if not index.isValid():
      return None
    extra = index.column() == self.columnCount(index.parent()) - 1 # /q/46835109 ; no +hack here, else no match!
    node = index.internalPointer()
    is_first_level = (node in self._dataparent.items)
    if role == QtCore.Qt.DisplayRole:
      if extra and is_first_level:
        return "????{}".format(index.row()+1) # 1-based count
      else:
        return node.data(index.column())
    return None

  def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole): # /q/64287713
    if (orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole):
      if section == self.columnCount(QtCore.QModelIndex()) - 1:
        return '#'
    return super().headerData(section, orientation, role)

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, self.items))
    last_column_idx = self.tw.model().columnCount(QtCore.QModelIndex()) - 1
    self.tw.header().moveSection(last_column_idx, 0) # /q/46835109 "Moves the section at visual index `from` to occupy visual index `to`"

  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_())

1

While the currently proposed solution may work for the OP, it has substantial issues. Most importantly, it alters the model structure only for display purposes, which is generally discouraged:

  • the virtual column doesn’t really contain useful data;
  • access to actual indexes becomes inconsistent (the real first column is not the first visible one);
  • it may present issues for further implementation (such as proxy models); consider checking it with QAbstractItemModelTester;
  • could present issues for selections or whenever the view should eventually span columns for parent items having less columns;

A more ideal approach would be to leave the model untouched, and imitate what QTableView does, which is adding a vertical header.

In order to achieve this, we need to:

  • create a vertical QHeaderView as child of the tree view;
  • ensure that the header is properly scrolled along with the viewport;
  • override the view’s updateGeometries(), in which we:
    • call the base implementation;
    • add a left margin for the header;
    • set its geometry appropriately;
    • resize each header section considering expanded rows;

Since the text has to be properly aligned with the top level item, and using Qt.AlignTop would show the text slightly above that, we probably need a custom QHeaderView so that we can override its paintSection(); in the following example I provide 3 alternatives for painting: the first one just tries to shift the section rect, but that may not be ideal as the section separator may be shown off the actual section start; the second is extremely basic and just paints the section text; the third mimics the default behavior as close as possible using QStyle features.

class VerticalTreeHeader(QHeaderView):
    def __init__(self, parent):
        super().__init__(Qt.Vertical, parent)
        self.setSectionResizeMode(QHeaderView.Fixed)
        # in case the default minimum is too big
        self.setMinimumSectionSize(1)
        # only necessary for the "translatePaintSection"
        self.setDefaultAlignment(Qt.AlignHCenter | Qt.AlignTop)

        # change the following with the alternative methods to test them
        self.paintSection = self.stylePaintSection

    def translatePaintSection(self, qp, rect, index):
        baseHeight = self.parent().rowHeight(
            self.model().index(index, 0, self.rootIndex()))
        fmHeight = qp.fontMetrics().height()
        dy = (baseHeight - fmHeight) // 2
        rect.translate(0, dy)
        super().paintSection(qp, rect, index)

    def textPaintSection(self, qp, rect, index):
        baseHeight = self.parent().rowHeight(
            self.model().index(index, 0, self.rootIndex()))
        rect.setHeight(baseHeight)
        qp.drawText(rect, Qt.AlignCenter, str(index + 1))

    def stylePaintSection(self, qp, rect, index):
        opt = QStyleOptionHeader()
        self.initStyleOption(opt)
        opt.rect = rect
        opt.section = index

        # this doesn't check for hidden rows, consider implementing it
        isFirst = index == 0
        isLast = index == self.model().rowCount(self.rootIndex()) - 1

        if isFirst and isLast:
            opt.position = opt.OnlyOneSection
        elif isFirst:
            opt.position = opt.Beginning
        elif isLast:
            opt.position = opt.End
        else:
            opt.position = opt.Middle

        # draw the header with *no* text
        self.style().drawControl(QStyle.CE_Header, opt, qp, self)

        rowHeight = self.parent().rowHeight(
            self.model().index(index, 0, self.rootIndex()))
        qp.drawText(
            rect.x(), rect.y(), rect.width(), rowHeight, 
            Qt.AlignCenter, str(index + 1)
        )


class VerticalHeaderTreeView(QTreeView):
    def __init__(self):
        super().__init__()
        self.dummyColumnHeader = QLabel('#', self, alignment=Qt.AlignCenter)
        self.vheader = VerticalTreeHeader(self)
        self.verticalScrollBar().valueChanged.connect(self.scrollVHeader)

    def scrollVHeader(self):
        self.vheader.setOffset(self.verticalOffset())

    def setRootIndex(self, index):
        super().setRootIndex(index)
        self.vheader.setRootIndex(index)

    def setModel(self, model):
        super().setModel(model)
        self.vheader.setModel(model)

    def updateGeometries(self):
        super().updateGeometries()

        width = min(
            self.vheader.maximumWidth(), 
            max(
                self.vheader.minimumWidth(), 
                self.vheader.sizeHint().width(), 
                # an arbitrary width based on the font
                self.fontMetrics().horizontalAdvance(' 000 ')
            )
        )

        margins = self.viewportMargins()
        margins.setLeft(width)
        self.setViewportMargins(margins)

        vg = self.viewport().geometry()
        headerGeo = QRect(
            vg.left() - width, vg.top(), width, vg.height()
        )
        self.vheader.setGeometry(headerGeo)

        self.dummyColumnHeader.setGeometry(QRect(
            QPoint(headerGeo.x(), self.header().y()), 
            QPoint(headerGeo.right() + 1, headerGeo.y())
        ))

        # get the first visible index (we could assume it's 0, but 
        # better safe than sorry
        root = self.rootIndex()
        for row in range(self.model().rowCount(root)):
            if not self.isRowHidden(row, root):
                index = self.model().index(row, 0, root)
                break
        else:
            return

        # navigate down the whole visible tree, item by item
        row = index.row()
        height = self.rowHeight(index)
        while True:
            index = self.indexBelow(index)
            if index.parent() == root or not index.isValid():
                # resize the *previous* top level row
                self.vheader.resizeSection(row, height)
                if not index.isValid():
                    break
                height = self.rowHeight(index)
                row = index.row()
            else:
                # a child, add its height
                height += self.rowHeight(index)

Note that your model implementation has a bug in parent(): if the parent is the root, you should not use createIndex(), but return a QModelIndex() instead:

def parent(self, index):
    if index.isValid():
        p = index.internalPointer().parent()
        if p and p != self._root:
            return super().createIndex(p.row(), 0, p)
    return QModelIndex()

I suggest you to fix it in your question too.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật