Search code examples
pythonqtpython-3.xpyqt5qscrollarea

Qt.ScrollBarAsNeeded not showing scrollbar when it's actually needed


I'm implementing a python application using PyQt5 and I encountered some problems when making use of a QScrollArea. This is the layout of my application:

enter image description here

It's composed of 2 QScrollArea (left and right pane) and a QMdiArea (center widget) arranged into a QHBoxLayout. When I expand the widgets on the left pane by clicking on the controls, and the height of the QWidget of the QScrollArea is bigger than then height of the QScrollArea itself, the scrollbar appears (as expected), but it's overlapping the content of the QScrollArea. To fix this problem I reimplemented the resizeEvent adding the necessary space for the scrollbar (till this point everything works.

enter image description here

Now, when I manually resize the Main Window, the left Pane gets more space and the scrollbar should disappear (but it doesn't) and it overlaps the widgets of the pane:

enter image description here

I also tried to manually toggle the visibility of the scrollbar (when the resizeEvent is received): when I do this, I can successfully hide the scrollbar but then I can't show it again (not matter if I call setVisible(True) on the scrollbar). This results in the space for the scrollbar being added, but the scrollbar is missing and the content of the pane is not scrollable:

enter image description here

Here is the implementation of the pane widget:

class Pane(QScrollArea):

    MinWidth = 186

    def __init__(self, alignment=0, parent=None):
        super().__init__(parent)
        self.mainWidget = QWidget(self)
        self.mainLayout = QVBoxLayout(self.mainWidget)
        self.mainLayout.setAlignment(alignment)
        self.mainLayout.setContentsMargins(0, 0, 0, 0)
        self.mainLayout.setSpacing(0)
        self.setContentsMargins(0, 0, 0, 0)
        self.setFrameStyle(QFrame.NoFrame)
        self.setFixedWidth(Pane.MinWidth)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Ignored)
        self.setWidgetResizable(True)
        self.setWidget(self.mainWidget)

    def resizeEvent(self, resizeEvent):
        if self.viewport().height() < self.widget().height():
            self.setFixedWidth(Pane.MinWidth + 18)
            # THIS DOESN'T WORK
            #self.verticalScrollBar().show()
        else:
            self.setFixedWidth(Pane.MinWidth)
            #self.verticalScrollBar().hide()

    def addWidget(self, widget):
        self.mainLayout.addWidget(widget)

    def removeWidget(self, widget):
        self.mainLayout.removeWidget(widget)

    def update(self, *__args):
        for item in itemsInLayout(self.mainLayout):
            item.widget().update()
        super().update(*__args)

What I want to achieve is pretty simple (but practically it seems not as simple): I would like to dynamically show the vertical scrollbar on my left/right pane widgets only when it's needed, and add the necessary space for the scrollbar so it doesn't overlap the widgets in the QScrollArea.

Before someone asks, I already tried to do something like this:

def resizeEvent(self, resizeEvent):
    if self.viewport().height() < self.widget().height():
        self.setFixedWidth(Pane.MinWidth + 18) 
        scrollbar = self.verticalScrollbar()
        scrollbar.setVisible(True)
        self.setVerticalScrollBar(scrollbar) ## APP CRASH
    else:
        self.setFixedWidth(Pane.MinWidth)
        #self.verticalScrollBar().hide()

which results in my application to crash. I hope that someone already faced this issue and is able to help me.

EDIT: I'm using PyQt5.5 compiled against Qt5.5 under OSX Yosemite 10.10.4 using clang.


Solution

  • Everything seems to work as expected for me without any need for workarounds. However, I strongly suspect there are additional constraints in your real code that you have not revealed in your question.

    UPDATE

    Below is a simple example that resizes the scrollareas when the scrollbars are shown/hidden:

    import sys
    from PyQt5 import QtCore, QtGui, QtWidgets
    
    class Window(QtWidgets.QMainWindow):
        def __init__(self):
            super(Window, self).__init__()
            widget = QtWidgets.QWidget(self)
            layout = QtWidgets.QHBoxLayout(widget)
            self.mdi = QtWidgets.QMdiArea(self)
            self.leftScroll = Pane(
                QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft, self)
            self.rightScroll = Pane(
                QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft, self)
            layout.addWidget(self.leftScroll)
            layout.addWidget(self.mdi)
            layout.addWidget(self.rightScroll)
            self.setCentralWidget(widget)
            for scroll in self.leftScroll, self.rightScroll:
                for index in range(4):
                    widget = QtWidgets.QTextEdit()
                    widget.setText('one two three four five')
                    scroll.addWidget(widget)
    
    class Pane(QtWidgets.QScrollArea):
        MinWidth = 186
    
        def __init__(self, alignment=0, parent=None):
            super().__init__(parent)
            self.mainWidget = QtWidgets.QWidget(self)
            self.mainLayout = QtWidgets.QVBoxLayout(self.mainWidget)
            self.mainLayout.setAlignment(alignment)
            self.mainLayout.setContentsMargins(0, 0, 0, 0)
            self.mainLayout.setSpacing(0)
            self.setContentsMargins(0, 0, 0, 0)
            self.setFrameStyle(QtWidgets.QFrame.NoFrame)
            self.setFixedWidth(Pane.MinWidth)
            self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
            self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
            self.setSizePolicy(QtWidgets.QSizePolicy.Maximum,
                               QtWidgets.QSizePolicy.Ignored)
            self.setWidgetResizable(True)
            self.setWidget(self.mainWidget)
            self.verticalScrollBar().installEventFilter(self)
    
        def addWidget(self, widget):
            self.mainLayout.addWidget(widget)
    
        def removeWidget(self, widget):
            self.mainLayout.removeWidget(widget)
    
        def eventFilter(self, source, event):
            if isinstance(source, QtWidgets.QScrollBar):
                if event.type() == QtCore.QEvent.Show:
                    self.setFixedWidth(Pane.MinWidth + source.width())
                elif event.type() == QtCore.QEvent.Hide:
                    self.setFixedWidth(Pane.MinWidth)
            return super(Pane, self).eventFilter(source, event)
    
    if __name__ == '__main__':
    
        app = QtWidgets.QApplication(sys.argv)
        window = Window()
        window.setGeometry(500, 300, 800, 300)
        window.show()
        sys.exit(app.exec_())