How to Use Qscrollarea to Make Scrollbars Appear

How to use QScrollArea to make scrollbars appear

Since many questions wonder how to use a QScrollArea that many widgets have, I will take the license to explain the various forms in detail and use them as a canonical answer for future readers.

QScrollArea only allows placing a container widget so the other widgets must be placed as children of the container widget.

And to place the widgets as children of another there are 2 possibilities:

1. Use a QLayout:

QLayouts allow you to automate the geometry of the widgets based on the QSizePolicy, strecth, sizeHint, etc. So in that case it's simple: Just set the widgetResizable property to True.

import sys
from PyQt5 import QtWidgets

class Window(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(Window, self).__init__(parent)

central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)

scroll_area = QtWidgets.QScrollArea(central_widget)
scroll_area.setGeometry(360, 10, 420, 180)
scroll_area.setWidgetResizable(True)

container = QtWidgets.QWidget()
scroll_area.setWidget(container)

# Set widgets via layout
lay = QtWidgets.QVBoxLayout(container)
lay.setContentsMargins(10, 10, 0, 0)
for letter in "ABCDE":
text = letter * 100
label = QtWidgets.QLabel(text)
lay.addWidget(label)
lay.addStretch()

self.setGeometry(300, 300, 803, 520)

if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())

2. Set the widgets directly without layouts:

In this case you must calculate the minimum geometry that contains the internal widgets and set the size in the container, also you must set the widgetResizable property to False:

import sys
from PyQt5 import QtCore, QtWidgets

class Window(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(Window, self).__init__(parent)

central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)

scroll_area = QtWidgets.QScrollArea(central_widget)
scroll_area.setGeometry(360, 10, 420, 180)
scroll_area.setWidgetResizable(False)

container = QtWidgets.QWidget()
scroll_area.setWidget(container)

# calculate geometry
geometry = QtCore.QRect(10, 10, 0, 0)
for letter in "ABCDE":
text = letter * 100
label = QtWidgets.QLabel(text, container)
label.adjustSize()
label.move(geometry.bottomLeft())
geometry |= label.geometry()

geometry.setTopLeft(QtCore.QPoint(0, 0))
container.resize(geometry.size())

self.setGeometry(300, 300, 803, 520)

if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())

QScrollArea, how do I make my central widget scrollable?

Simple explanation: the scroll area has no relation with the main window, and then it's not shown when the main window is.

Never, ever modify (or imitate) files generated by pyuic

You are making some confusion around the object structure, Qt parent/ownership and how a scroll area is set up.

This is also caused by the fact that you're clearly trying to mimic the structure and behavior of a file generated by pyuic, which should never be done. Those files are intended to be directly used as they are, without any modification, and imported in the main script of your program.

Their peculiar structure is intended only for that purpose, and the way they're built is not to be emulated, as creation of UI by code should only be done by subclassing a QWidget subclass (QMainWindow, QDialog, etc); trying to imitate what they do is not only unnecessary, but most times leads to confusion, bugs and unexpected behavior, exactly like in your case.

The only valid reason to open (and just read) a file generated by pyuic is to learn how widgets are created in the setupUi, keeping in mind that the setupUi argument (usually, MainWindow for QMainWindows, Form for QWidgets, Dialog for QDialogs, unless the name of the top level widget is changed in the object inspector of designer) is the main widget on which the ui is built upon.

Manually editing those files should only be done if one really knows what she/he is doing. There are few, very rare cases for which this is considered necessary, and it's usually to workaround very specific known bugs in the uic module, that normally rise on very specific conditions and situations.

What is happening?

Then, when a QWidget instance is created with an existing widget as argument, the result is that the new widget becomes a child of the existing one.

Widget can be then reparented in various ways: by setting a widget for a scroll area using setWidget(), or by setting the widget as a central widget to a QMainWindow.

What happens with your code is that you created a new QWidget that becomes a child of the QMainWindow with the following line:

self.centralwidget = QtWidgets.QWidget(MainWindow)

Then you create a scroll area (without any parent):

self.scrollArea = QtWidgets.QScrollArea()

Widgets that are not created with a parent are normally considered top level windows, unless they are added to a layout (or manually reparented using setParent(), but that's another story), which causes the parent (the widget on which the layout is set) to take ownership of the new child widget. Otherwise, if you try to show the "parent-less" widget, it will be shown as a top level widget, meaning it will have its own window, and as any top level widget, they can only be shown by manually calling show() or setVisible(True) (and that's your case).

Then you try to set the centralwidget of the scroll area, which will result in reparenting it:

self.scrollArea.setWidget(self.centralwidget)

Finally, you're setting that widget as the central widget, which will reparent it back again to the main window, with the result that the scroll area will not have a widget anymore (QObjects, from which QWidget inherits, can only have a single parent).

MainWindow.setCentralWidget(self.centralwidget)

The correct approach

The solution is to correctly create a QWidget (or QMainWindow in your case) subclass, set the scroll area as the central widget (if you want it as that) and create a new widget for its contents:

class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.scrollArea = QtWidgets.QScrollArea()
self.setCentralWidget(self.scrollArea)
self.scrollArea.setWidgetResizable(True)

self.contents = QtWidgets.QWidget()
self.scrollArea.setWidget(self.contents)

# create a layout for the scroll area contents; using the target widget
# as argument of the layout constructor automatically sets the layout on
# the specified widget
layout = QtWidgets.QVBoxLayout(self.contents)

# the same as:
# layout = QtWidgets.QVBoxLayout()
# self.contents.setLayout(layout)

for row in range(20):
button = QtWidgets.QPushButton('button {}'.format(row + 1))
layout.addWidget(button)

if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
sys.exit(app.exec_())

Adding custom widgets to a QScrollArea in pyqt5 does not make scrollbar appear

In the code that samples ExampleWidget is a child of QScrollArea: self.widget = ExampleWidget(self.scrollArea), that does not imply that the widget is handled with the properties of the QScrollArea, the QScrollArea has a viewport() that is the background widget and the QScrollArea only shows a part of it. So if you want a widget to be inside you have to use the setWidget() method. On the other hand that viewport() has the size of the content and how it calculates that size ?, by default it uses the sizeHint() of the widget, and in your case it does not have it so it will not show what you want.

import sys
from PyQt5 import QtCore, QtGui, QtWidgets

class ExampleWidget(QtWidgets.QWidget):
def paintEvent(self, e):
qp = QtGui.QPainter(self)
self.drawWidget(qp)

def drawWidget(self, qp):
qp.setPen(QtGui.QColor(255, 0, 0))
qp.setBrush(QtGui.QColor(255, 0, 0))
qp.drawRect(0, 0, 100, 100)

def sizeHint(self):
return QtCore.QSize(500, 500)

class Example(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.scrollArea = QtWidgets.QScrollArea()
self.setCentralWidget(self.scrollArea)
self.widget = ExampleWidget()
self.scrollArea.setWidget(self.widget)
self.show()

if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())

Sample Image

Scroll bars with QScrollArea

In Designer, in your QScrollArea uncheck widgetResizable property or use QScrollArea::setWidgetResizable to turn it off programatically.

How to make PyQt5 QScrollArea horizontal scrollbar show up dynamically?

There is no need to add the view to another scroll area, as all Qt item views are scroll areas.

Note that, unlike the headers of QTableView, the header of QTreeView automatically stretches the last section:

Note: The horizontal headers provided by QTreeView are configured with this property set to true, ensuring that the view does not waste any of the space assigned to it for its header. If this value is set to true, this property will override the resize mode set on the last section in the header.

This means that if you are only showing the first column of the tree, the names of files and directories will always be elided if their width exceeds the width of the widget, and the horizontal scroll bar will not be activated even if it's always shown.

treeview.header().setStretchLastSection(False)
treeview.header().setSectionResizeMode(QHeaderView.ResizeToContents)

Setting the scroll bar to the bottom on PyQt5 (using scrollArea and a gridLayout)

The problem is that the scrollbar geometry is updated when:

  • The widget is set to the QScrollArea.
  • The QScrollArea is made visible.
  • The size of the QScrollArea changes.
  • The widget size changes if it is visible.

So when placing the widget first and then the buttons, the QScrollArea is not notified of it, so there are 2 solutions:

  • Move the scrollbar an instant after showing.

        for i in range(100):
    for j in range(100):
    self.gridLayout.addWidget(QtWidgets.QPushButton(), i, j)
    QtCore.QTimer.singleShot(0, self.handle_timeout)

    def handle_timeout(self):
    x = self.scrollArea.verticalScrollBar().maximum()
    self.scrollArea.verticalScrollBar().setValue(x)
  • Set the widget to the QScrollArea after the buttons were placed.

    class IndicSelectWindow(QtWidgets.QDialog):
    def __init__(self, parent=None):
    super(IndicSelectWindow, self).__init__(parent=parent)
    self.resize(500, 400)
    self.layout = QtWidgets.QHBoxLayout(self)
    self.scrollArea = QtWidgets.QScrollArea(self)
    self.scrollArea.setWidgetResizable(True)
    self.scrollAreaWidgetContents = QtWidgets.QWidget()
    self.gridLayout = QtWidgets.QGridLayout(self.scrollAreaWidgetContents)

    self.layout.addWidget(self.scrollArea)
    for i in range(100):
    for j in range(100):
    self.gridLayout.addWidget(QtWidgets.QPushButton(), i, j)

    self.scrollArea.setWidget(self.scrollAreaWidgetContents)
    x = self.scrollArea.verticalScrollBar().maximum()
    self.scrollArea.verticalScrollBar().setValue(x)

How to resize QScrollArea when a scrollbar appears (so only one bar is necessary)?

I had a similar situation, which was solved by overriding QScrollArea::sizeHint() in the descendant.

#include <QScrollBar>
#include <QScrollArea>

class MyScrollArea : public QScrollArea
{
Q_OBJECT

public:
QSize sizeHint() const override
{
auto newSize = QScrollArea::sizeHint();
newSize.setWidth(newSize.width()
+ verticalScrollBar()->sizeHint().width());
return newSize;
}
};

How can i make widgets overflow to make a scrollbar appear in Qt?

QScrollArea is not a container. QScrollArea is a "scrolling view" for another widget. You shouldn't set up layout on QScrollArea. You should create widget, fill it with proper layout and then use QScrollArea::setWidget(QWidget *) to make it scrollable.



Related Topics



Leave a reply



Submit