如何在后台线程中为 QFileSystemModel 创建自定义图标

How to create custom icons for QFileSystemModel in a background thread

我正在为一些自定义设计文件在 qt 中制作一个文件浏览器。我想加载他们的预览作为他们的缩略图,因此我使用 QIconProvider 到 return 图标到我的 QFileSystemModel

问题是创建 QIcon 的算法需要一些资源,因此我的应用程序在加载所有缩略图之前没有响应。

我想知道是否有任何方法可以将我的 QIconProvider 置于后台线程中,以便我的应用程序响应。

不幸的是,QFileIconProvider API 和模型 api 之间存在阻抗不匹配:QFileSystemModel 在事情发生变化时向视图提供异步通知,但是当图标更改或已知时,图标提供程序无法异步通知模型。

您可以在文件系统模型和视图之间安装身份代理。该代理的 data 方法将异步查询图标。模型的同步图标提供程序然后未使用且不必要。

// https://github.com/KubaO/Whosebugn/tree/master/questions/icon-proxy-39144638
#include <QtWidgets>
#include <QtConcurrent>

/// A thread-safe function that returns an icon for an item with a given path.
/// If the icon is not known, a null icon is returned.
QIcon getIcon(const QString & path);

class IconProxy : public QIdentityProxyModel {
    Q_OBJECT
    QMap<QString, QIcon> m_icons;
    Q_SIGNAL void hasIcon(const QString&, const QIcon&, const QPersistentModelIndex& index) const;
    void onIcon(const QString& path, const QIcon& icon, const QPersistentModelIndex& index) {
        m_icons.insert(path, icon);
        emit dataChanged(index, index, QVector<int>{QFileSystemModel::FileIconRole});
    }
public:
    QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override {
        if (role == QFileSystemModel::FileIconRole) {
            auto path = index.data(QFileSystemModel::FilePathRole).toString();
            auto it = m_icons.find(path);
            if (it != m_icons.end()) {
                if (! it->isNull()) return *it;
                return QIdentityProxyModel::data(index, role);
            }
            QPersistentModelIndex pIndex{index};
            QtConcurrent::run([this,path,pIndex]{
                emit hasIcon(path, getIcon(path), pIndex);
            });
            return QVariant{};
        }
        return QIdentityProxyModel::data(index, role);
    }
    IconProxy(QObject * parent = nullptr) : QIdentityProxyModel{parent} {
        connect(this, &IconProxy::hasIcon, this, &IconProxy::onIcon);
    }
};

接受的答案很棒 - 向我介绍了一些更高级的 Qt 概念。

对于将来尝试此操作的任何人,我必须进行一些更改才能使其顺利运行:

  • 限制线程:QThreadPool 传递给 QConcurrent::run,并将最大线程设置为 1 或 2。使用默认值会终止应用程序,因为所有线程被烧毁构建图像预览。瓶颈将是磁盘,所以不会 在这个任务上有超过 1 个或 2 个线程。
  • 避免重入:需要处理在图标生成完成前多次查询同一路径的图标的情况。当前代码会生成多个线程来生成相同的图标。简单的解决方案是在 QConcurrent::run 调用之前向 m_icons 映射添加一个占位符条目。我刚刚调用了默认值 QIdentityProxyModel::data(index, QFileSystemModel::FileIconRole),因此图标在加载完成之前获得了不错的默认值
  • 任务取消: 如果您破坏模型(或想要切换视图文件夹等),您将需要一种方法来取消活动任务。不幸的是,没有内置的方法来取消挂起的 QConcurrent::run 任务。我使用 std::atomic_bool 来表示取消,任务在执行前检查。还有一个 std::condition_variable 等待,直到所有任务都完成 cancelled/complete.

提示:我的用例是从磁盘上的图像加载缩略图预览(可能是常见用例)。经过一些实验,我发现生成预览的最快方法是使用 QImageReader,将缩略图大小传递给 setScaledSize。请注意,如果您有非方形图像,您需要传递具有适当宽高比的尺寸,如下所示:

    const QSize originalSize = reader.size(); // Note: Doesn't load the file contents
    QSize scaledSize = originalSize;
    scaledSize.scale(MaximumIconSize, Qt::KeepAspectRatio);
    reader.setScaledSize(scaledSize);