You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.02 00:24:52