在加载图片缩略图时,Python / PyQt6应用崩溃
我正在用Python / PyQt6编写一个图片浏览器应用。我已经包含了我代码的一个简化版本。当我选择任意文件夹并点击“打开”时,几乎总是会崩溃——有时是立即崩溃,有时是几秒后崩溃。如果所有缩略图都加载完毕也不崩溃,那么如果你关闭它再重新打开同一个文件夹,它会崩溃。除了“QThread:在线程仍在运行时被销毁”这一输出符合预期之外,我没有看到任何异常、错误信息或其他输出。底部的 try...except 块甚至都没有捕捉到任何东西。
我知道这段代码不完整,当缩略图正在加载时如果我关闭窗口,需要有某种机制来停止线程,但这一步解决后我再来处理。
我知道问题和多线程有关——如果我把 self.load_thumbnail.emit(...) 这一行注释掉就不会崩溃;同样如果我把所有线程相关的东西去掉,把 QImage(...).scaled(...) 的部分放到 for filename in sorted(os.listdir(path)) 循环里也不会崩溃,但正如预期,它会让GUI挂起,直到所有缩略图加载完成。
import sys, os
from PyQt6 import QtWidgets, QtCore, QtGui
class StartWindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self._path = ""
self.resize(300,200)
select_folder_btn = QtWidgets.QPushButton("Select Folder")
select_folder_btn.clicked.connect(self.select_folder)
self.path_lbl = QtWidgets.QLabel()
open_btn = QtWidgets.QPushButton("Open")
open_btn.clicked.connect(self.open_browser)
vbox = QtWidgets.QVBoxLayout(self)
vbox.addStretch()
vbox.addWidget(select_folder_btn)
vbox.addStretch()
vbox.addWidget(self.path_lbl)
vbox.addStretch()
vbox.addWidget(open_btn)
vbox.addStretch()
def select_folder(self):
dialog = QtWidgets.QFileDialog(self, self._path)
dialog.setFileMode(dialog.FileMode.Directory)
if dialog.exec():
self._path = dialog.selectedFiles()[0]
self.path_lbl.setText(self._path)
def open_browser(self):
self.browser_window = ImageBrowserWindow(self._path)
self.browser_window.show()
class ImageBrowserWindow(QtWidgets.QMainWindow):
def __init__(self, path):
super().__init__()
self.resize(1000,800)
self.listview = ImageBrowserList(path)
self.setCentralWidget(self.listview)
class ImageBrowserList(QtWidgets.QListView):
def __init__(self, path):
super().__init__()
self.model = ImageBrowserModel(path)
self.setModel(self.model)
self.setViewMode(self.ViewMode.IconMode)
self.setIconSize(QtCore.QSize(250,250))
self.setGridSize(QtCore.QSize(300,350))
self.setFlow(self.Flow.LeftToRight)
self.setWrapping(True)
self.setTextElideMode(QtCore.Qt.TextElideMode.ElideMiddle)
self.setResizeMode(self.ResizeMode.Adjust)
self.setMovement(self.Movement.Static)
class ImageBrowserModel(QtCore.QAbstractListModel):
load_thumbnail = QtCore.pyqtSignal(QtCore.QModelIndex, str)
def __init__(self, path):
super().__init__()
self._path = path
self._list = []
self.thumbnail_loader_thread = QtCore.QThread(parent=self)
self.thumbnail_loader = ThumbnailLoader()
self.thumbnail_loader.moveToThread(self.thumbnail_loader_thread)
self.thumbnail_loader.loaded.connect(self.thumbnail_loaded)
self.load_thumbnail.connect(self.thumbnail_loader.load)
self.thumbnail_loader_thread.start()
self.placeholder_image = QtGui.QImage(PATH_TO_PLACEHOLDER_IMAGE)
for filename in sorted(os.listdir(path)):
if not os.path.isfile(path + "/" + filename):
continue
if filename.endswith((".jpg", ".jpeg", ".gif", ".png")):
self._list.append({"filename": filename, "thumbnail": None, "thumbnail_loading": False})
def rowCount(self, parent):
return len(self._list)
def data(self, index, role):
if not index.isValid():
return
image = self._list[index.row()]
if role == QtCore.Qt.ItemDataRole.DisplayRole:
return image["filename"]
if role == QtCore.Qt.ItemDataRole.DecorationRole:
if image["thumbnail"] is None:
if not image["thumbnail_loading"]:
image["thumbnail_loading"] = True
self.load_thumbnail.emit(index, self._path + "/" + image["filename"])
return self.placeholder_image
else:
return image["thumbnail"]
@QtCore.pyqtSlot(QtCore.QModelIndex, QtGui.QImage)
def thumbnail_loaded(self, index, image):
self._list[index.row()]["thumbnail"] = image
self._list[index.row()]["thumbnail_loading"] = False
self.dataChanged.emit(index, index)
class ThumbnailLoader(QtCore.QObject):
loaded = QtCore.pyqtSignal(QtCore.QModelIndex, QtGui.QImage)
@QtCore.pyqtSlot(QtCore.QModelIndex, str)
def load(self, index, path):
image = QtGui.QImage(path).scaled(QtCore.QSize(250,250), QtCore.Qt.AspectRatioMode.KeepAspectRatio, QtCore.Qt.TransformationMode.SmoothTransformation)
self.loaded.emit(index, image)
if __name__ == "__main__":
try:
app = QtWidgets.QApplication(sys.argv)
w = StartWindow()
w.show()
print(app.exec())
except Exception as e:
print(e)
解决方案
崩溃很可能是由于在不同线程之间传递 QModelIndex 对象所致。
QModelIndex 并非线程安全,应该只在GUI线程中使用。在你的代码中,索引被发出到 ThumbnailLoader,它在另一个线程中运行:
self.load_thumbnail.emit(index, path)
随后,工作线程再次发出相同的索引:
self.loaded.emit(index, image)
这可能导致未定义的行为,因为在工作线程使用它时,模型/索引可能已经不再有效。
相反,只传递诸如行号之类的原始值,并在主线程中重建索引。
示例:
class ImageBrowserModel(QtCore.QAbstractListModel):
load_thumbnail = QtCore.pyqtSignal(int, str)
def data(self, index, role):
if not index.isValid():
return
image = self._list[index.row()]
if role == QtCore.Qt.ItemDataRole.DecorationRole:
if image["thumbnail"] is None:
if not image["thumbnail_loading"]:
image["thumbnail_loading"] = True
full_path = os.path.join(self._path, image["filename"])
self.load_thumbnail.emit(index.row(), full_path)
return self.placeholder_image
return image["thumbnail"]
@QtCore.pyqtSlot(int, QtGui.QImage)
def thumbnail_loaded(self, row, image):
if 0 <= row < len(self._list):
self._list[row]["thumbnail"] = image
self._list[row]["thumbnail_loading"] = False
idx = self.index(row)
self.dataChanged.emit(idx, idx)
工作线程:
class ThumbnailLoader(QtCore.QObject):
loaded = QtCore.pyqtSignal(int, QtGui.QImage)
@QtCore.pyqtSlot(int, str)
def load(self, row, path):
img = QtGui.QImage(path)
if not img.isNull():
scaled = img.scaled(
QtCore.QSize(250, 250),
QtCore.Qt.AspectRatioMode.KeepAspectRatio,
QtCore.Qt.TransformationMode.SmoothTransformation
)
self.loaded.emit(row, scaled)
你也应该在关闭应用程序之前正确地停止线程,否则 QThread: Destroyed while thread is still running 警告仍可能出现。
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。