Jim
Jim

Reputation: 2064

Scroll to the bottom when adding an element to a QScrollArea

I have a QScrollArea and I would like when a I push my "Add" button that adds Widgets to the widgets contained in the QScrollArea for the scroll to scroll all the way to the bottom

I made several attempts to Scroll to the bottomw ith code like

   scrollWidget.update()
   bar = scrollWidget.verticalScrollBar()
   bar.setValue(bar.maximum())

or even using ensureWidgetVisible But what appears to be happening is it scrolls to the bottom of the scroll "Before" the resize occurs, then it resizes so I am not quite at the bottom.

I verfied this by writing code that checks the bar size, and the maximum bar size and the child count This shows there are new children but the bar size has not yet been updated.

I then tried to give Qt time to "recaluclate" sizes first by calling:

   QApplication.processEvents()
   scrollWidget.update()

I do not want the scroll area to ALWAYS be on the bottom but only after pushing my button

Upvotes: 2

Views: 1830

Answers (2)

Starter Gear
Starter Gear

Reputation: 11

I did some experiments, changed the order of method calls. But I still didn't understand when exactly it's worth calling the scroll change. I made bad code, but it works.

def scroll_to_bottom(self):
    for i in range(2): # range(3) or range(4), range(2) works for me
        QApplication.processEvents()
        self.scrollArea_messages.update()
        cur_max = self.scrollArea_messages.verticalScrollBar().maximum()
        self.scrollArea_messages.verticalScrollBar().setValue(cur_max)

Upvotes: 1

NeatNit
NeatNit

Reputation: 632

I just had to deal with the same issue, I figured out a solution that I think is good, although I'm a Qt newbie so take it with a grain of salt:

class MyMainWindow(QWidget):
    def __init__(self, parent=None):
        super(MyMainWindow, self).__init__(parent)
        
        self.scrollarea = QScrollArea()

        # [...]

        self.vscrollbar = self.scrollarea.verticalScrollBar()
        self.vscrollbar.rangeChanged.connect(self.scrollToBottom)

    @Slot(int, int)
    def scrollToBottom(self, minimum, maximum):
        self.vscrollbar.setValue(maximum)

During construction we connect the rangeChanged Signal to our custom Slot scrollToBottom that sets the scrolling value to the maximum value, thereby scrolling down to the bottom every time the contents grow vertically.

I went a step further and made it only scroll to the bottom if the view was scrolled all the way to the bottom before the contents grew:

class MyMainWindow(QWidget):
    def __init__(self, parent=None):
        super(MyMainWindow, self).__init__(parent)
        
        self.scrollarea = QScrollArea()

        # [...]

        self.vscrollbar = self.scrollarea.verticalScrollBar()
        self.vscrollbar.rangeChanged.connect(self.scrollToBottomIfNeeded)
        self.vscrollbar.valueChanged.connect(self.storeAtBottomState)
        self.atbottom = True

    @Slot(int)
    def storeAtBottomState(self, value):
        self.atbottom = value == self.vscrollbar.maximum()

    @Slot(int, int)
    def scrollToBottomIfNeeded(self, minimum, maximum):
        if self.atbottom:
            self.vscrollbar.setValue(maximum)

In the context of my application, this is the preferred behaviour, as the contents can grow while the user is looking at something in the ScrollArea, so autoscroll would prevent them from staying where they are. If your application only grows the contents after a user action, use the approach in the first snippet.

In response to your comment, this is how to only scroll down when adding an element:

class MyMainWindow(QWidget):
    def __init__(self, parent=None):
        super(MyMainWindow, self).__init__(parent)
        
        self.scrollarea = QScrollArea()
        self.addbutton = QPushButton()

        self.addbutton.clicked.connect(self.addElement)

        # [...]

        self.vscrollbar = self.scrollarea.verticalScrollBar()
        self.vscrollbar.rangeChanged.connect(self.scrollToBottomIfNeeded)
        self.adding = False

    @Slot()
    def addElement(self):
        self.adding = true
        # ... actually add an element ...

    @Slot(int, int)
    def scrollToBottomIfNeeded(self, minimum, maximum):
        if self.adding:
            self.vscrollbar.setValue(maximum)
            self.adding = False

Upvotes: 1

Related Questions