Python/PyQt6图片浏览器加载缩略图时频繁崩溃求助
问题描述
使用Python/PyQt6开发图片浏览器,选择文件夹打开后频繁崩溃——有时立即崩溃,有时加载缩略图后崩溃;重新打开同一文件夹也会崩溃。仅出现QThread: Destroyed while thread is still running提示,无其他错误输出。
已确认问题与线程相关:
- 注释
self.load_thumbnail.emit(...)后无崩溃 - 移除线程逻辑,直接在主线程加载缩略图,虽卡顿但不崩溃
问题代码
import sys, os from PyQt6 import QtWidgets, QtCore, QtGui # 需替换为实际占位图路径 PATH_TO_PLACEHOLDER_IMAGE = "placeholder.png" 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)
解决方案及代码修复
核心问题1:跨线程传递无效对象
QModelIndex是与模型状态绑定的临时对象,不能跨线程安全传递。线程中收到的索引可能已失效(如模型数据变化、窗口关闭),直接访问会触发内存错误导致崩溃。
修复:将信号传递参数从QModelIndex改为行号(int),在主线程中重新创建有效索引。
核心问题2:线程未正确终止
关闭窗口时模型对象被销毁,但线程仍在运行,不仅会触发QThread: Destroyed while thread is still running警告,还可能因线程访问已销毁对象引发崩溃。
修复:在模型析构时停止并等待线程结束。
优化:缩略图加载效率
直接用QImage(path).scaled()会先加载完整图片再缩放,对大图片内存占用高。改用QImageReader可直接加载缩放后的图片,降低内存消耗。
修复后的完整代码
import sys, os from PyQt6 import QtWidgets, QtCore, QtGui # 替换为实际占位图路径 PATH_TO_PLACEHOLDER_IMAGE = "placeholder.png" 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(int, 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)): file_path = os.path.join(path, filename) if not os.path.isfile(file_path): 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_data = self._list[index.row()] if role == QtCore.Qt.ItemDataRole.DisplayRole: return image_data["filename"] if role == QtCore.Qt.ItemDataRole.DecorationRole: if image_data["thumbnail"] is None: if not image_data["thumbnail_loading"]: image_data["thumbnail_loading"] = True # 传递行号而非QModelIndex self.load_thumbnail.emit(index.row(), os.path.join(self._path, image_data["filename"])) return self.placeholder_image else: return image_data["thumbnail"] @QtCore.pyqtSlot(int, QtGui.QImage) def thumbnail_loaded(self, row, image): # 重新创建有效索引 index = self.index(row) if index.isValid(): self._list[row]["thumbnail"] = image self._list[row]["thumbnail_loading"] = False self.dataChanged.emit(index, index) # 析构时停止线程 def __del__(self): self.thumbnail_loader_thread.quit() self.thumbnail_loader_thread.wait() class ThumbnailLoader(QtCore.QObject): loaded = QtCore.pyqtSignal(int, QtGui.QImage) # 修改为传递行号和图片 @QtCore.pyqtSlot(int, str) def load(self, row, path): # 用QImageReader高效加载缩略图 reader = QtGui.QImageReader(path) reader.setScaledSize(QtCore.QSize(250,250)) reader.setTransformationMode(QtCore.Qt.TransformationMode.SmoothTransformation) image = reader.read() if not image.isNull(): self.loaded.emit(row, image) if __name__ == "__main__": try: app = QtWidgets.QApplication(sys.argv) w = StartWindow() w.show() sys.exit(app.exec()) except Exception as e: print(e)
内容的提问来源于stack exchange,提问作者Iain
相关产品推荐
相关产品推荐

