Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Widgets are shrinked when added to scroll area



  • I have a QScrollArea and QWidget assigned to it:

    scrollArea = QScrollArea()
    widget = QWidget()
    scrollArea.setWidget(widget)
    
    layout = QVBoxLayout()
    widget.setLayout(layout)
    

    And then I create labels dynamically at run time, assign to them QPixmap with images and add these labels to the layout:

    for img in images:
        pixmap = QPixmap(img).scaled(200, 200, 1)
        label = QLabel()
        label.setPixmap(pixmap)
    
        layout.addWidget(label)
    

    And what I get is when a new label is added to the layout all the labels begin to shrink and then get the proper size. Then again shrink and so on... You can see this behaviour here (https://gfycat.com/windingwhisperedgrison).
    I tried changing 'sizePolicy' to 'Minimum', setting 'minimumHeight', calling 'updateGeometry()' - nothing. What can the problem be?

    P.S. I noticed that the labels get proper sizes again when the scroll bar (on the right) gets smaller (widget change its size and gets bigger?). Maybe the problem is that, for some reason, widget doesn't change its size fast enough but new labels are added so they shrink to fit? If it's the case how can we resize widget faster (before a new label is added)?


  • Lifetime Qt Champion

    Hi
    Since you scale the pixmap to 200x200 then the easy fix is simply to set minimum size on the labels to that.
    label = QLabel()
    label.setMinimumSize(QSize(200,200));



  • It doesn't work. I tried it already. The labels shrink anyway.


  • Lifetime Qt Champion

    @kraken1564

    Hi
    thats odd. works fine here in c++
    It wont make them less than min size.
    alt text

    can you show your exact code ? Maybe we can spot something.



  • The code above is basically my code. One thing I forgot to say that I do some "computations" in a working thread and then use signals/slots to emit intermediate results. So the code where I create labels with images is my slot updating the GUI.

    I recorded the screen one more time. This what I see with the code above: https://gfycat.com/lighthearteddentalcrustacean . If I add

    widget.updateGeometry()
    

    after adding a new label, these jumps/flickers almost disappear but sometimes happen (way less frequently).

    If I add

    time.sleep(1)
    

    after emitting a new signal (so slot creating my labels is called less often), these jumps/flickers don't happen at all: https://gfycat.com/calculatinggenuinekoodoo .

    Can it be some problem with processing too many signals in a short period of time so the widgets cannot be repainted/updated fast enough to avoid these jumps/flickers?


  • Lifetime Qt Champion

    Hi,

    How many signals are you sending per seconds for your image updates ?



  • About 5-10. It's not too many I think. But I have quite old CPU - Athlon 64 x2 3800+. I do not have any fancy hardware with me right now to test it.

    But, indeed, the rarer the GUI needs to be updated, the less frequent these glitch are.


  • Lifetime Qt Champion

    The number seems fine but is it per image ?
    It could also come from your graphics stack.



  • No, It's one signal per image.


  • Lifetime Qt Champion

    Can you provide a minimal buildable example that shows that behaviour ?



  • I think the "too many signals" hypothesis is right. I added a label and update it hundreds times per second. The glitches gets way worse. If I do not do it, they almost disappear. Also I tested it on another hardware and got the same behaviour ( less signals - no glitches, more signals - glitches).

    Here is the code (but it is use PyQt, is it ok?):

    import sys
    from multiprocessing import Pool
    from pathlib import Path
    
    from PyQt5 import QtCore, QtGui, QtWidgets
    
    # !!! Change to your folder with images
    FOLDER = '/home/user/images'
    
    def find_images(folder):
        '''Return paths of the images in the folder'''
    
        images = []
        for filename in Path(folder).rglob('*.jpg'):
            if filename.is_file():
                images.append(str(filename))
        return images
    
    def scale_image_BA(image_path):
        '''To generate more signals, concurrent processing is
        used (via lib "multiprocessing"). QPixmap or QImage cannot
        be serialized so converting to QByteArray is used'''
    
        reader = QtGui.QImageReader(image_path)
        reader.setScaledSize(QtCore.QSize(200, 200))
        image = reader.read()
    
        ba = QtCore.QByteArray()
        buf = QtCore.QBuffer(ba)
        buf.open(QtCore.QIODevice.WriteOnly)
        image.save(buf, 'JPG', 100)
        buf.close()
        return ba
    
    def QByteArray_to_QPixmap(ba_image):
        '''Convert from QByteArray back to QPixmap'''
    
        pixmap = QtGui.QPixmap()
        pixmap.loadFromData(ba_image)
        return pixmap
    
    
    class Signals(QtCore.QObject):
        image = QtCore.pyqtSignal(QtCore.QByteArray)
        label = QtCore.pyqtSignal(str)
    
    
    class Worker(QtCore.QRunnable):
        def __init__(self, image_paths):
            super().__init__()
            self.image_paths = image_paths
            self.signals = Signals()
    
        @QtCore.pyqtSlot()
        def run(self):
            '''Run image scaling on all available cores to emit more signals'''
    
            with Pool() as p:
                for baimg in p.imap_unordered(scale_image_BA, self.image_paths):
                    # Glitches happen if emit many signals per second???
                    # Without updating 'label' many times, everything's fine
                    for i in range(1000):
                        self.signals.label.emit(str(i))
                    self.signals.image.emit(baimg)
    
    
    class MyMainWindow(QtWidgets.QMainWindow):
    
        def __init__(self):
            super().__init__()
    
            self.setMinimumSize(QtCore.QSize(800, 600))
    
            centralWidget = QtWidgets.QWidget()
            self.setCentralWidget(centralWidget)
    
            scrollArea = QtWidgets.QScrollArea()
            scrollArea.setWidgetResizable(True)
    
            imagesWidget = QtWidgets.QWidget()
            scrollArea.setWidget(imagesWidget)
    
            self.label = QtWidgets.QLabel('Counter')
    
            startButton = QtWidgets.QPushButton('Start')
            startButton.clicked.connect(self.run_worker)
    
            verticalLayout = QtWidgets.QVBoxLayout()
            verticalLayout.addWidget(scrollArea)
            verticalLayout.addWidget(self.label)
            verticalLayout.addWidget(startButton)
            centralWidget.setLayout(verticalLayout)
    
            self.imagesLayout = QtWidgets.QVBoxLayout()
            imagesWidget.setLayout(self.imagesLayout)
    
        def run_worker(self):
            images = find_images(FOLDER)
            threadpool = QtCore.QThreadPool.globalInstance()
            w = Worker(images)
            w.signals.image.connect(self.update_gui)
            w.signals.label.connect(self.label.setText)
            threadpool.start(w)
    
        def update_gui(self, image):
            label = QtWidgets.QLabel()
            pixmap = QByteArray_to_QPixmap(image)
            label.setPixmap(pixmap)
            self.imagesLayout.addWidget(label)
    
    
    if __name__ == '__main__':
    
        app = QtWidgets.QApplication(sys.argv)
        mw = MyMainWindow()
        mw.show()
        app.exec()
    

  • Lifetime Qt Champion

    What version of PyQt5 are you using ?



  • 5.12.2, but tested also on 5.14.2 and 5.9 - got the same behaviour.


  • Lifetime Qt Champion

    What graphics stack do you have on your machine ?



  • One PC has Geforce GT 730, driver v. 435.21, Linux Mint with Cinnamon DE (last one);
    the other one has Intel UHD 630, Manjaro with KDE (use Qt, right?) - again get the same thing,



  • I've made the peace of code showing this behaviour a little simpler:

    import sys
    from PyQt5 import QtCore, QtGui, QtWidgets
    
    # Maybe you need to play with these constants
    PIXMAP_NUM = 1000
    EVENTLOOP_RUN_NUM = 100
    
    
    class Signals(QtCore.QObject):
        pixmaps = QtCore.pyqtSignal(list)
    
    
    class Worker(QtCore.QRunnable):
        def __init__(self):
            super().__init__()
            self.signals = Signals()
    
        @QtCore.pyqtSlot()
        def run(self):
            '''Make QPixmap object with different colours, add them to a list
            and emit the list'''
    
            c = QtGui.QColor
            colors = [c('red'), c('green'), c('blue'), c('cyan'),
                      c('magenta'), c('yellow')]
            i = 0
    
            pms = []
            for _ in range(PIXMAP_NUM):
                pixmap = QtGui.QPixmap(200, 200)
                pixmap.fill(colors[i])
                i += 1
                if i == len(colors):
                    i = 0
                pms.append(pixmap)
    
            self.signals.pixmaps.emit(pms)
    
    
    class MyMainWindow(QtWidgets.QMainWindow):
    
        def __init__(self):
            super().__init__()
    
            self.setMinimumSize(QtCore.QSize(800, 600))
    
            centralWidget = QtWidgets.QWidget()
            self.setCentralWidget(centralWidget)
    
            scrollArea = QtWidgets.QScrollArea()
            scrollArea.setWidgetResizable(True)
    
            imagesWidget = QtWidgets.QWidget()
            scrollArea.setWidget(imagesWidget)
    
            startButton = QtWidgets.QPushButton('Start')
            startButton.clicked.connect(self.run_worker)
    
            verticalLayout = QtWidgets.QVBoxLayout()
            verticalLayout.addWidget(scrollArea)
            verticalLayout.addWidget(startButton)
            centralWidget.setLayout(verticalLayout)
    
            self.imagesLayout = QtWidgets.QVBoxLayout()
            imagesWidget.setLayout(self.imagesLayout)
    
        def run_worker(self):
            threadpool = QtCore.QThreadPool.globalInstance()
            w = Worker()
            w.signals.pixmaps.connect(self.update_gui)
            threadpool.start(w)
    
        def update_gui(self, pixmaps):
            for i, img in enumerate(pixmaps):
                label = QtWidgets.QLabel()
                # The next line doesn't change the behaviour
                label.setMinimumSize(QtCore.QSize(200, 200))
                label.setPixmap(img)
                self.imagesLayout.addWidget(label)
                # The next line doesn't change the behaviour
                self.updateGeometry()
    
                if i % EVENTLOOP_RUN_NUM == 0:
                    QtCore.QCoreApplication.processEvents()
    
    
    if __name__ == '__main__':
    
        app = QtWidgets.QApplication(sys.argv)
        mw = MyMainWindow()
        mw.show()
        app.exec()
    

    This code reproduce the glitches on both the machines.

    I've noticed one thing. If we do not set the minimum size,

    label.setMinimumSize(QtCore.QSize(200, 200))
    

    labels shrink. But if we do, they overlap (so, indeed, the minimum size is respected but imagesWidget is not big enough - cannot change its own size fast enough??? - to set the labels without overlapping???)


Log in to reply