'PyQt QtreeView scrollbar overlapping header

I've been trying to use QTreeView (instead of QListView) because I needed an horizontal header. Furthermore, I wanted to use stylesheet to customize header colors and scrollbars.

By default, the scrollbar overlaps the header. It's a problem that have been discussed already, and the workaround seemed fine EXCEPT that even if the scrollbar is implemented to start below the header, the scrollbar background still overlaps the header.

In my case, I would like the header to be totally adjusted to the widget, and the scrollbar to start just below

Below is some code example that reproduce my issue:

import sys
from PyQt6 import QtCore, QtGui, QtWidgets


class custom_treeview(QtWidgets.QTreeView):

    def __init__(self, parent, name, color):
        super().__init__(parent)
        self.setAcceptDrops(True)
        self.setRootIsDecorated(False)
        self.setDragEnabled(True)
        self.setAlternatingRowColors(True)
        self.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)

        # set model
        model = QtGui.QStandardItemModel()
        model.setRowCount(0)
        model.setColumnCount(1)
        model.setHeaderData(0, QtCore.Qt.Orientation.Horizontal, name)
        self.setModel(model)

        self.setStyleSheet(f"""QTreeView {{ border: 2px solid {color}; }}
        ::section {{ background-color: {color} ;border: none;font: bold 12px;}}
QScrollBar::handle:vertical {{ background: {color}; }}
QScrollBar::handle:horizontal {{ background: {color} }}
""")


    def updateVertScrollBar(self):
        sb = self.verticalScrollBar()
        rect = sb.geometry()
        rect.setTop(self.header().height())
        sb.setGeometry(rect)

    def resizeEvent(self, e: QtGui.QResizeEvent) -> None:
        super().resizeEvent(e)
        self.updateVertScrollBar()


class MyApp(QtWidgets.QMainWindow):

    def __init__(self, parent=None):
        QtWidgets.QMainWindow.__init__(self, parent)
        self.setObjectName("main_win")
        self.resize(800, 600)
        self.centralwidget = QtWidgets.QWidget(self)
        self.centralwidget.setObjectName("centralwidget")
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.splitter = QtWidgets.QSplitter(self.centralwidget)
        self.splitter.setOrientation(QtCore.Qt.Orientation.Horizontal)
        self.splitter.setObjectName("splitter")
        self.horizontalLayout.addWidget(self.splitter)
        self.setCentralWidget(self.centralwidget)
        self.centralwidget.setStyleSheet("""QScrollBar:vertical { background: #FFFFFF;width: 10px; } 
        QScrollBar::add-line:vertical { height: 0px; } 
        QScrollBar::sub-line:vertical { height: 0px; } 
        QScrollBar::add-line:horizontal { width: 0px; } 
        QScrollBar::sub-line:horizontal { width: 0px; } 
        QScrollBar:horizontal { background: #FFFFFF;height: 10px } 
        QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { height: 0px; background: none; } 
        QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { width: 0px; background: none; }""")

        self.left_tree = custom_treeview(self.splitter, "left tree", "#AB1234")

        self.right_tree = custom_treeview(self.splitter, "right tree", "#4331BA")

        a = [str(i) for i in range(500)]

        for i in a:

            item = QtGui.QStandardItem()
            item.setText(i)
            self.left_tree.model().appendRow(item)

        for i in a:

            item = QtGui.QStandardItem()
            item.setText(i)
            self.right_tree.model().appendRow(item)




sys.argv = ['']
app = QtWidgets.QApplication(sys.argv)
main_app = MyApp()
main_app.show()
app.exec()

The problem is:

header overlapped by scrollbar

I also want the widget to be resizable (it's the reason why there is a splitter in my example).

What should I do to get a clean behaviour?



Solution 1:[1]

The problem is that the scroll bar is a direct child of the scroll area, while the contents of the view (including the header) are part of its viewport. You cannot "extend" the header, because it would be hidden anyway by the geometry of the viewport itself.

A possible solution is to add a basic QWidget styled with the same color as a scroll bar widget, and ensure that it always has the header height. To do so, you don't need to override resizeEvent(), but updateGeometries(), which is called whenever the view requires to update its internal widgets.

class custom_treeview(QtWidgets.QTreeView):
    def __init__(self, parent, name, color):
        # ...
        self.setStyleSheet(f"""
            QTreeView {{ 
                border: 2px solid {color}; 
            }}
            QHeaderView::section {{ 
                background-color: {color}; 
                border: none; 
                font: bold 12px; 
            }}
            QScrollBar::handle:vertical {{ 
                background: {color}; 
            }}
            QScrollBar::handle:horizontal {{ 
                background: {color}; 
            }}
            QWidget#fakeHeader {{
                background: {color}; 
            }}
            """)
        self.fakeHeader = QtWidgets.QWidget(objectName='fakeHeader')
        self.addScrollBarWidget(self.fakeHeader, QtCore.Qt.AlignTop)

    def updateGeometries(self):
        super().updateGeometries()
        self.fakeHeader.setFixedHeight(
            self.header().height() + self.frameWidth()
        )

Note that this is obviously a workaround, and is not a "correct" solution: for instance, if the contents are wider than the view width, the header labels will still be restricted by the viewport margins.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1