Weird Bug with creating multiple image labels, Image Gallery Browser

Viewed 726

So I'm creating an image gallery browser in PYQT5. I can choose a directory and load the image file icons in a scrollable widget like so:

enter image description here

If there is less than 20 or so images in the directory, it works fine. However when there's more than that the image labels for some reason don't show:

enter image description here

If I take a few of those images that aren't showing and put them into a new folder on their own, then try to load only those images, it works, only if the application hasn't already tried to load them before hand and failed, otherwise, the empty square happens again.

So this seems to me to be some sort of framework/memory limitation? Can anyone shed some light on this? Here is my code:

# MainWindow Set-Up : Creating Widgets and Layouts
class ClassUi(object):
    def setup(self, MainW):
        MainW.setObjectName("MainW")
        MainW.resize(400,500)

        self.mainlayout = QtWidgets.QVBoxLayout()

        self.centralwidget = QtWidgets.QWidget(MainW)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth())
        self.centralwidget.setSizePolicy(sizePolicy)
        self.centralwidget.setLayout(self.mainlayout)
        MainW.setCentralWidget(self.centralwidget)

    #direcwidget is a container widget for the top part of the program where a directory is chosen
        self.direcwidget = QtWidgets.QWidget()
        self.direclayout = QtWidgets.QGridLayout()
        self.direcwidget.setLayout(self.direclayout)
        self.mainlayout.addWidget(self.direcwidget)

        self.opdirbut = QtWidgets.QPushButton() #Button that opens directory dialog when pressed
        self.opdirbut.setText("Choose")
        self.opdirbut.setFixedSize(50,50)

        self.linepath = QtWidgets.QLineEdit() #Line Edit that displays current directory
        self.linepath.setText(os.getcwd())

        self.backpath = QtWidgets.QPushButton() #Button that changes directory to parent directory of the current one
        self.backpath.setFixedSize(20,20)
        self.backpath.setText("^")

    #Positioning of widgets inside widget container direcwidget
        self.direclayout.addWidget(self.opdirbut, 0,0, 2, 1)
        self.direclayout.addWidget(self.linepath, 0,2, 1, 3)
        self.direclayout.addWidget(self.backpath, 1,4, 1, 1)


    #Scrollwidget is the area wherein a container widget widgetforscroll holds all image icons in a grid
        self.scrollwidget = QtWidgets.QScrollArea()
        self.mainlayout.addWidget(self.scrollwidget)
        self.scrollwidget.setWidgetResizable(True)
        self.scrollwidget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)

        self.scrollgrid = QtWidgets.QGridLayout()

        self.widgetforscroll = QtWidgets.QWidget()
        self.widgetforscroll.setLayout(self.scrollgrid)

        self.scrollwidget.setWidget(self.widgetforscroll)




#Contains logic of program
class MainWindow(QtWidgets.QMainWindow, ClassUi):
    def __init__(self):
        super().__init__()
        self.setup(self)

    #Counter variables for keeping track of where to layout items
        self.picturerow = 0
        self.picturecolumn = 0
        self.howmany = 0

    #Assigns class methods to directory buttons
        self.opdirbut.clicked.connect(self.opdial)
        self.backpath.clicked.connect(self.uppath)



# Each time this function is called, a new widget called newwidget is created containing "pic" in a pixmap label and a text label and layed out
# on the widgetforscroll widget through its scrollgrid layout. Each time the function is called, picture column increments by one at the end of the function
# when all the columns in a row are filled, picture column is reset to 0 and and picture row is incremented. Picture row and picture column are used in positioning
# the newwidgets in the scrollgrid.
    def addpicture(self, pic):
        if self.picturecolumn == 3:
            self.picturecolumn = 0
            self.picturerow += 1
        self.howmany += 1

#newwidget is object of picwidg class containing pixmap and text label
        newwidget = picwidg(self.howmany, pic)

#This function was not required to be created, it was only created for the purpose of the Qtimer singleshot implementation.
#The newwidget is being positioned on the scrollgrid layout here.
        def addnewone(lyout,nw,rw,cl):
            lyout.addWidget(nw, rw, cl)

        QtCore.QTimer.singleShot(
            self.howmany*500,
            lambda sc=self.scrollgrid, nr = newwidget, ow = self.picturerow, mn=self.picturecolumn : addnewone(sc,nr,ow,mn)
        )
#Incrementing column by 1 for the next time function is called
        self.picturecolumn += 1

#This is the function connected to the choose dialog button. It opens a QFileDialog window which allows you to only choose a directory folder.
#When the folder is chosen:
                        # 1: The linepath text is set the to the new directory
                        # 2: Any previous picwidg objects are cleared from the scrollgrid layout
                        # 3: Picture column and picture row variables are reset for positioning
                        # 4: A for loop scans the new directory for files with .jpg or .png extensions
                        # 5: The addpicture method is called with the filename as the argument
    def opdial(self):
        dialogbox = dialog()

        try:
            os.chdir(dialogbox.getExistingDirectory(options=QtWidgets.QFileDialog.DontUseNativeDialog))
            self.linepath.setText(os.getcwd())

            for i in reversed(range(self.scrollgrid.count())):
                widgetToRemove = self.scrollgrid.itemAt(i).widget()
                # remove it from the layout list
                self.scrollgrid.removeWidget(widgetToRemove)
                # remove it from the gui
                widgetToRemove.setParent(None)

            self.picturecolumn =0
            self.picturerow =0
            self.howmany = 0

            for a, b, c in os.walk(os.getcwd()):
                for i in c:
                    if i[-4:].lower() == ".png" or i[-4:].lower() == ".jpg":
                        self.addpicture(i)


        except:
            pass

#This is the function for reaching the parent directory. It works very similar to the above function, the only difference
#being that instead of grabbing a new directory from a QFileDialog, the directory processed is taken from the current linepath text
#and goes to the parent directory instead, then removes widgets from the scrolllayout and adds new pictures to the scrolllayout
    def uppath(self):
        newpath = os.path.dirname(self.linepath.text())
        os.chdir(newpath)

        self.linepath.setText(newpath)
        for i in reversed(range(self.scrollgrid.count())):
            widgetToRemove = self.scrollgrid.itemAt(i).widget()
            # remove it from the layout list
            self.scrollgrid.removeWidget(widgetToRemove)
            # remove it from the gui
            widgetToRemove.setParent(None)
        self.picturecolumn = 0
        self.picturerow = 0
        self.howmany = 0
        for a, b, c in os.walk(os.getcwd()):
            for i in c:
                # print(i[-4:].lower())
                if i[-4:].lower() == ".png" or i[-4:].lower() == ".jpg":
                    self.addpicture(i)

# This is the class where newwidget instances are created
# Here 2 labels are created, one for the image, one for the text and packed in a vertical layout
class picwidg(QtWidgets.QWidget):
    whoshover = None
    picwidglist =[]

    def __init__(self, numb, pic):
        super().__init__()
        self.setMouseTracking(True)
        self.numb = numb
        self.pic = pic
        picwidg.picwidglist.append(self)

        SizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)

        newwidgetlayout = QtWidgets.QVBoxLayout()
        self.setLayout(newwidgetlayout)
        self.setSizePolicy(SizePolicy)
        self.setMinimumSize(QtCore.QSize(115, 140))
        self.setMaximumSize(QtCore.QSize(115, 140))

        #Pic Label
        self.newpic = QtWidgets.QLabel()
        QtCore.QTimer.singleShot(self.numb*500, self.addingnewpic)
        self.newpic.setScaledContents(True)
        self.newpic.setSizePolicy(SizePolicy)
        self.newpic.setGeometry(0, 0, 100, 100)
        self.newpic.setStyleSheet("border:1px solid gray")

        #Picture text label
        self.newtext = QtWidgets.QLabel()
        font_metrics = QtGui.QFontMetrics(self.font())
        self.newtext.setAlignment(QtCore.Qt.AlignCenter)
        elided_text = font_metrics.elidedText(pic, QtCore.Qt.ElideRight, 100)
        self.newtext.setText(elided_text)

        newwidgetlayout.addWidget(self.newpic)
        newwidgetlayout.addWidget(self.newtext)

    def addingnewpic(self):
        self.newpic.setPixmap(QtGui.QPixmap(self.pic))


#Class for QFileDialog for selecting only directories
class dialog(QtWidgets.QFileDialog):
    def __init__(self):
        super().__init__()
        self.setFileMode(QtWidgets.QFileDialog.DirectoryOnly)
        self.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True)
        self.setOption(QtWidgets.QFileDialog.ShowDirsOnly, False)



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

1 Answers

If you want to show a lot of QPixmap then it is not optimal to use a lot of QLabel, in that case it is better to use a QListView (or QListWidget) since it handles memory better.

In your code you add and remove QLabels but in the case of the model only items are added or removed and the view is repainted avoiding the excessive use of memory.

Considering the above I have implemented the following solution:

import os
from PyQt5 import QtCore, QtGui, QtWidgets

ICON_SIZE = 100


class StyledItemDelegate(QtWidgets.QStyledItemDelegate):
    def initStyleOption(self, option, index):
        super().initStyleOption(option, index)
        option.text = option.fontMetrics.elidedText(
            index.data(), QtCore.Qt.ElideRight, ICON_SIZE
        )


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

        self.choose_btn = QtWidgets.QPushButton(
            self.tr("Choose"), clicked=self.on_choose_btn_clicked
        )
        self.choose_btn.setFixedSize(50, 50)
        self.path_le = QtWidgets.QLineEdit()
        self.back_btn = QtWidgets.QPushButton(
            self.tr("^"), clicked=self.on_back_btn_clicked
        )
        self.back_btn.setFixedSize(20, 20)
        self.pixmap_lw = QtWidgets.QListWidget(
            viewMode=QtWidgets.QListView.IconMode,
            iconSize=ICON_SIZE * QtCore.QSize(1, 1),
            movement=QtWidgets.QListView.Static,
            resizeMode=QtWidgets.QListView.Adjust,
        )
        delegate = StyledItemDelegate(self.pixmap_lw)
        self.pixmap_lw.setItemDelegate(delegate)

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

        grid_layout.addWidget(self.choose_btn, 0, 0, 2, 1)
        grid_layout.addWidget(self.path_le, 0, 1)
        grid_layout.addWidget(self.back_btn, 1, 1, alignment=QtCore.Qt.AlignRight)
        grid_layout.addWidget(self.pixmap_lw, 2, 0, 1, 2)

        self.resize(640, 480)

        self.timer_loading = QtCore.QTimer(interval=50, timeout=self.load_image)
        self.filenames_iterator = None

    @QtCore.pyqtSlot()
    def on_choose_btn_clicked(self):

        directory = QtWidgets.QFileDialog.getExistingDirectory(
            options=QtWidgets.QFileDialog.DontUseNativeDialog
        )
        if directory:
            self.start_loading(directory)

    @QtCore.pyqtSlot()
    def on_back_btn_clicked(self):
        directory = os.path.dirname(self.path_le.text())
        self.start_loading(directory)

    def start_loading(self, directory):
        if self.timer_loading.isActive():
            self.timer_loading.stop()
        self.path_le.setText(directory)
        self.filenames_iterator = self.load_images(directory)
        self.pixmap_lw.clear()
        self.timer_loading.start()

    @QtCore.pyqtSlot()
    def load_image(self):
        try:
            filename = next(self.filenames_iterator)
        except StopIteration:
            self.timer_loading.stop()
        else:
            name = os.path.basename(filename)
            it = QtWidgets.QListWidgetItem(name)
            it.setIcon(QtGui.QIcon(filename))
            self.pixmap_lw.addItem(it)

    def load_images(self, directory):
        it = QtCore.QDirIterator(
            directory,
            ["*.jpg", "*.png"],
            QtCore.QDir.Files,
            QtCore.QDirIterator.Subdirectories,
        )
        while it.hasNext():
            filename = it.next()
            yield filename


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())
Related