Wippo
Wippo

Reputation: 963

PyQt5: Qstyle.CC_ScrollBar rendered in the wrong place

I am working with a listView and a custom delegate. Through the paint function I draw a set of control elements so that each row of the list acts as if it was a widget, without actually being one. This is crucial for performance since the list is composed of hundreds of thousands of elements, as pointed here and here. The only problem is with QStyleOptionSlider complex control: if I ask for a CC.ScrollBar the control is rendered in the top left corner of the view and not where i want. If in QApplication.style().drawComplexControl(QStyle.CC_ScrollBar, self.scrollOptions, painter) i ask for a CC_Slider (instead of CC_ScrollBar) the control is rendered where expected. I also tried to initialise the style from a real scroll widget but nothing changed.

I would like to know if I'm doing something wrong or if it's a problem with the library, since all the other controls i have painted work perfectly. The only difference I've noticed is that other elements (e.g. frame, label, pushbutton) have their own QStyleOption class while the scrollbar is merged with the slider class, but to quote the docs:

QStyleOptionSlider contains all the information that QStyle functions need to draw QSlider and QScrollBar.

Debug Info: Python 3.8.6 / PyQt 5.15.1 / Pyqt-tools 5.15.1.2 / Windows 10

Minimal example

from PyQt5.QtCore import QSize, Qt, QRect
from PyQt5.QtGui import QColor
from PyQt5.QtGui import QStandardItem
from PyQt5.QtGui import QStandardItemModel
from PyQt5.QtWidgets import QStyle
from PyQt5.QtWidgets import QStyledItemDelegate, QApplication, QStyleOptionFrame, \
    QStyleOptionSlider
from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(800, 600)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
        self.gridLayout.setObjectName("gridLayout")
        self.inferenceListView = QtWidgets.QListView(self.centralwidget)
        self.inferenceListView.setGridSize(QtCore.QSize(0, 200))
        self.inferenceListView.setObjectName("inferenceListView")
        self.gridLayout.addWidget(self.inferenceListView, 0, 1, 1, 1)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 21))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)
        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)
        self.setupProposals()

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))

    def setupProposals(self):
        self.delegate = MyDelegate()
        # self.delegate.initScroll(self.horizontalScrollBar)
        model = QStandardItemModel(0, 0)
        for index in range(0, 5000):
            model.appendRow(QStandardItem(str(index)))
            self.inferenceListView.setItemDelegateForRow(index, self.delegate)
        self.inferenceListView.setModel(model)


class MyDelegate(QStyledItemDelegate):
    def __init__(self, parent=None):
        QStyledItemDelegate.__init__(self, parent)
        self.frame = QStyleOptionFrame()
        # ---------------

        self.scrollOptions = QStyleOptionSlider()
        self.scrollOptions.orientation = Qt.Vertical
        self.scrollOptions.LayoutDirectionAuto = Qt.LayoutDirectionAuto
        self.scrollOptions.orientation = Qt.Vertical
        self.scrollOptions.state = QStyle.State_Enabled
        self.scrollOptions.maximum = 10
        self.scrollOptions.minimum = 0
        self.scrollOptions.sliderValue = 0

    def initScroll(self, scroll):
        self.scrollOptions.initFrom(scroll)

    def sizeHint(self, option, index):
        return QSize(150, 200)

    def paint(self, painter, option, index):
        optX = option.rect.x()
        optY = option.rect.y()
        optH = option.rect.height()
        optW = option.rect.width()

        painter.fillRect(option.rect, QColor(100, 100, 100, 100))
        painter.drawLine(optX, optY + optH, optX + optW, optY + optH)
        QApplication.style().drawControl(QStyle.CE_ShapedFrame, self.frame, painter)

        self.scrollOptions.rect = QRect(optX + 100, optY + 100, 50, 80)

        # OK WITH CC_SLIDER
        #QApplication.style().drawComplexControl(QStyle.CC_Slider, self.scrollOptions, painter)

        # WRONG PLACE WITH CC_SCROLLBAR
        QApplication.style().drawComplexControl(QStyle.CC_ScrollBar, self.scrollOptions, painter)

    def editorEvent(self, event, model, option, index):
        return False


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

EDIT

Seems to be a Windows-related problem at the moment. I had a colleague with a Mac run the code above and the scrollbar is drawn in the correct place. I attach an image of what happens on Windows in the two cases: enter image description here

Upvotes: 2

Views: 187

Answers (1)

musicamante
musicamante

Reputation: 48424

It seems like a possibly incomplete implementation of subControlRect in QCommonStyle, as the rectangle returned for CC_Slider is always positioned on the top left (see source).

A possible solution can be to use a proxy style and return a translated rectangle if the subrect is not contained in the option rect:

class ProxyStyle(QtWidgets.QProxyStyle):
    def subControlRect(self, cc, opt, sc, widget=None):
        r = super().subControlRect(cc, opt, sc, widget)
        if cc == self.CC_ScrollBar and not opt.rect.contains(r):
            r.translate(opt.rect.topLeft())
        return r

# ...
app = QtWidgets.QApplication(sys.argv)
app.setStyle(ProxyStyle())

Upvotes: 2

Related Questions