Странная ошибка с созданием нескольких меток изображений, браузер галереи изображений - PullRequest
2 голосов
/ 26 мая 2020

Итак, я создаю браузер галереи изображений в PYQT5. Я могу выбрать каталог и загрузить значки файлов изображений в виджет с возможностью прокрутки следующим образом:

enter image description here

Если в папке меньше 20 изображений или около того каталог, он работает нормально. Однако, когда их больше, метки изображений по какой-то причине не отображаются:

enter image description here

Если я возьму несколько изображений, которые не показывать и помещать их в новую папку самостоятельно, затем попробуйте загрузить только эти изображения, это работает, только если приложение еще не пыталось загрузить их заранее и не удалось, в противном случае снова появится пустой квадрат.

Так мне кажется, это какое-то ограничение фреймворка / памяти? Может кто-нибудь пролить некоторый свет на это? Вот мой код:

# 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 Ответ

2 голосов
/ 27 мая 2020

Если вы хотите показать много QPixmap, тогда не оптимально использовать много QLabel, в этом случае лучше использовать QListView (или QListWidget), так как он лучше обрабатывает память.

В вашем коде вы добавляете и удаляете QLabels, но в случае модели добавляются или удаляются только элементы, а представление перекрашивается, избегая чрезмерного использования памяти.

Учитывая вышеизложенное, я реализовал следующее решение:

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_())
...