在加载图片缩略图时,Python / PyQt6应用崩溃

前端开发 2026-07-09

我正在用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导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章