QTreeView column header width is too large

Question:

I am trying to set up a simple QTreeView with nice columns. The first two columns are set to fit the contents, the last column is set to stretch.

For some reason, the minimum width of the last column ("Balance") is way higher than it needs to be, which produces unwanted behaviour. I captured this behavior on this video (YouTube link). The last column takes way too much space and does not shrink as much as it should when the QWidget is shrunk.

The complete code producing this example is below. I am aware the QAbstractItemModel is implemented incorrectly but it is a minimal implementation that showcases the issue.

I tried playing around with QTreeView.header().setMinimumWidth(0) but that did not work either.

I don’t understand where is the issue because I think the MRE below is already very barebones and nearly all settings are left to default values. Why would the column which has ResizeMode.Stretch need to take up so much minimum width? Shouldn’t it take at maximum the width of the header text, i.e. "Balance"? It seems to take up roughly twice as much.

I am running Python 3.11.2 and PyQt 6.4.2.

MRE:

import sys
from typing import Any

from PyQt6 import QtWidgets
from PyQt6.QtCore import QAbstractItemModel, QModelIndex, Qt
from PyQt6.QtWidgets import QApplication, QHeaderView, QTreeView, QWidget


class Form(QWidget):
    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent=parent)
        self.resize(323, 454)

        self.horizontalLayout = QtWidgets.QHBoxLayout(self)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")
        self.horizontalLayout.addLayout(self.verticalLayout)

        self.treeView = QTreeView(self)
        self.verticalLayout.addWidget(self.treeView)
        self.treeView.setModel(CategoryTreeModel(self.treeView))

        self.treeView.header().setSectionResizeMode(
            0,
            QHeaderView.ResizeMode.ResizeToContents,
        )
        self.treeView.header().setSectionResizeMode(
            1,
            QHeaderView.ResizeMode.ResizeToContents,
        )
        self.treeView.header().setSectionResizeMode(
            2,
            QHeaderView.ResizeMode.Stretch,
        )


class CategoryTreeModel(QAbstractItemModel):
    COLUMN_HEADERS = {
        0: "Name",
        1: "Transactions",
        2: "Balance",
    }

    def __init__(
        self,
        tree_view: QTreeView,
    ) -> None:
        super().__init__()
        self._tree_view = tree_view

    def rowCount(self, index: QModelIndex = ...) -> int:
        return 5

    def columnCount(self, index: QModelIndex = ...) -> int:  # noqa: U100
        return 3

    def index(self, row: int, column: int, _parent: QModelIndex = ...) -> QModelIndex:
        return QAbstractItemModel.createIndex(self, row, column, "item")

    def parent(self, index: QModelIndex = ...) -> QModelIndex:
        return QModelIndex()

    def data(self, index: QModelIndex, role: Qt.ItemDataRole = ...) -> Any:
        if not index.isValid():
            return None
        column = index.column()
        if role == Qt.ItemDataRole.DisplayRole:
            if column == 0:
                return "some very long name"
            if column == 1:
                return "0"
            if column == 2:
                return "0"
        return None

    def headerData(
        self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole = ...
    ) -> str | int | None:
        if role == Qt.ItemDataRole.TextAlignmentRole:
            if section == 2:
                return Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
        if role == Qt.ItemDataRole.DisplayRole:
            if orientation == Qt.Orientation.Horizontal:
                return self.COLUMN_HEADERS[section]
            return str(section)
        return None


app = QApplication(sys.argv)
form = Form()
form.show()
app.exec()
Asked By: Jakub Franek

||

Answers:

The documentation is a bit obscure on the point, and it requires considering multiple aspects:

  • the stretchLastSection() tells if the last header section takes all available space, and if it’s set to True (which is the default value for QTreeView) it overrides the section resize mode of the last section;
  • when disabled, it uses the default value of sectionResizeMode() (which is Interactive and by default uses the defaultSectionSize() or that of the section;
  • the default sectionResizeMode() can only be changed using setSectionResizeMode(mode) (without any section reference);

This means that, if stretchLastSection() is True:

  1. calling setSectionResizeMode(section, mode) is pointless for the last section;
  2. it will always use the minimum size of the defaultSectionSize(), no matter if it could theoretically stretch the size of the section;

The basic solution, then, is to ensure that you call setStretchLastSection(False).

This will allow resizing the section to a point where the section text won’t be readable, so you could also add the following:

    style = self.style()
    lastSectionText = self.treeView.model().headerData(
        2, Qt.Orientation.Horizontal, Qt.ItemDataRole.DisplayRole) or 'XXXX'
    self.treeView.header().setMinimumSectionSize(
        style.pixelMetric(style.PixelMetric.PM_HeaderMarkSize)
        + style.pixelMetric(style.PixelMetric.PM_HeaderGripMargin) * 2
        + self.fontMetrics().horizontalAdvance(lastSectionText)
    )

For obvious reasons, the code right above might not work fine if you use setSectionsMovable(True), or if the resize modes or section text change at runtime. In that case, you may need to create your own QHeaderView subclass.

Note: the ifs in headerData() are not optimal, since they will all be performed whenever the orientation is vertical: use elif instead for the main indentation (the if role); also, return None is implicit for return and when the function exits normally, so just use return when you want to return None and skip the last one.

Answered By: musicamante
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.