python - 创建多个图像标签的奇怪错误,图像库浏览器
问题描述
所以我在 PYQT5 中创建了一个图库浏览器。我可以选择一个目录并将图像文件图标加载到一个可滚动的小部件中,如下所示:
如果目录中的图像少于 20 个左右,则可以正常工作。但是,由于某种原因,图像标签不止于此:
如果我将其中一些未显示的图像单独放入一个新文件夹中,然后尝试仅加载这些图像,则它可以工作,前提是应用程序尚未事先尝试加载它们并且失败,否则,空方格再次出现。
所以在我看来,这似乎是某种框架/内存限制?任何人都可以对此有所了解吗?这是我的代码:
# 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_())
解决方案
如果要显示大量 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_())
推荐阅读
- azure-data-explorer - 日期时间与字符串连接
- mysql - 将 NOW() 转换为 dmY
- python - 类最佳实践 - 没有参数的方法与属性覆盖本身
- mongodb - MongoDB 中 db.currentOp() 输出中的奇怪操作
- javascript - 错误:尝试 setState 时重新渲染过多
- android - 如何在 NavigationDrawer 中正确地将项目标记为选中?
- google-data-studio - 在谷歌数据工作室编辑数据透视表
- mongodb - Mongo 如何使用 $addToSet 进行条件更新?
- java - .jdbc4.MySQLIntegrityConstraintViolationException:Spring JPA 中针对@Column 的重复条目(unique = true)
- django - Django dumpdata CommandError:无法序列化数据库:int() 的无效文字,基数为 10