Как создать собственные иконки для QFileSystemModel в фоновом потоке

Я делаю файловый браузер в qt для некоторых пользовательских дизайн-файлов. Я хочу загрузить их предварительный просмотр в качестве их эскиза и по этой причине я использую QIconProvider вернуть иконку моему QFileSystemModel,

Проблема в том, что алгоритм, который создает QIcon нужны некоторые ресурсы, и в результате мое приложение не отвечает, пока не завершится загрузка всех миниатюр.

Мне интересно, есть ли способ поставить мой QIconProvider в фоновом потоке, так что мое приложение отзывчиво.

2 ответа

Решение

К сожалению, существует несоответствие импеданса между QFileIconProvider API и модель API: QFileSystemModel предоставляет асинхронные уведомления представлению, когда что-то меняется, но поставщик значков не может асинхронно уведомлять модель, когда значки меняются или становятся известными.

Вы можете установить идентификационный прокси между моделью файловой системы и представлениями. Это прокси data Затем метод будет запрашивать иконки асинхронно. В этом случае синхронный поставщик значков модели не используется и не нужен.

// https://github.com/KubaO/stackrun/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 потоков в этой задаче.
  • Избегайте повторного ввода: необходимо обработать случай, когда значок для одного и того же пути запрашивается несколько раз до завершения создания значка. Текущий код будет порождать несколько потоков, генерирующих один и тот же значок. Простое решение - добавить запись-заполнитель в карту m_icons перед вызовом QConcurrent::run. Я просто позвонил по умолчанию QIdentityProxyModel::data(index, QFileSystemModel::FileIconRole), таким образом, значок получает достойное значение по умолчанию до завершения загрузки
  • Отмена задачи: если вы уничтожаете свою модель (или хотите переключить просмотр папок и т. Д.), Вам понадобится способ отменить активные задачи. К сожалению, нет встроенного способа отменить ожидающий QConcurrent::run задача. Я использовал std::atomic_bool сигнализировать об отмене, которую задачи проверяют перед выполнением. И std::condition_variable ждать, пока все задачи не будут отменены / завершены.

Совет: Мой вариант использования для этого заключался в загрузке миниатюр предварительного просмотра из изображений на диске (вероятно, в общем случае). После некоторых экспериментов я обнаружил, что самый быстрый способ создания превью 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);
Другие вопросы по тегам