如何获取文件(夹)图标 without Cache

本文最后更新于:2024年8月1日 晚上

前情提要(迫真)

书接上回,我们已经介绍了如何设置文件夹图标,想必大家都意犹未尽

那么根据有Set必有Get定理,本节我们来讲讲图标的获取(on Windows

众所周知,Qt是一个ConvenientC++

So,长话短说,两行代码秒了

1
2
QFileIconProvider iconPro;
QIcon icon = iconPro.icon(dirPath);

好的,请大家有序退场👋

新吃席观众还在懵逼,老吃席观众已经开始排排坐了:都知道笔者出手必有大坑

缓存(Cache)

大家看看这是什么☝,老朋友了

缓存是个好东西啊,空间换时间,现代CPU都上到三级缓存了

可是没有什么是永恒的,是非好坏、真假对错,甚至是这句话本身 —— MrBeanC

和文件系统打交道,时效性确实不太能保证,无论是HDD亦或是SSD,在Memory面前就是弟弟

那么QtQFileIconProvider上缓存也不难理解了

文件夹图标缓存

没错,在程序生命周期内,使用QFileIconProvider::icon获取文件夹图标,会在全局范围内进行缓存

无论后续文件夹图标是否改变,都只会返回第一次缓存的结果

基操分割线

到目前为止,都是基操(基本操作)

但是捏,对于缓存这件事,QFileIconProvider文档里只字未提,甚至查不到Cache这个词

而且哈,这个类,也没有提供任何方法(method)来清除缓存或选择不缓存

No, Nothing

这或许就是无形的手吧.🤌

😠😾🤬💬🗯︎

So What

可能(不可能)有人觉得这没什么

万一,我是说万一哈,有这么一款软件,它的功能是修改文件夹图标

// 源码级广告植入:Dr.Folder

修改完成后,软件会重新获取并展示该图标(或者是不重启的情况下刷新)

然后假如获取的还是旧图标,你说这不完犊子了😑

返璞归真

那没办法了,你仁我义(的反义词)

还是看看远方的Windows API吧,家人们

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
QIcon getFileIcon(QString filePath) {
filePath = QDir::toNativeSeparators(filePath); // IMPORTANT: 否则会找不到文件

CoInitialize(NULL); // important for SHGetFileInfo,否则失败
SHFILEINFO sfi;
memset(&sfi, 0, sizeof(SHFILEINFO));

QIcon icon;
if (SHGetFileInfo(filePath.toStdWString().c_str(), 0, &sfi, sizeof(SHFILEINFO), SHGFI_ICON)) {
icon = QtWin::fromHICON(sfi.hIcon); // 貌似Qt6删除了这个模块 哭
DestroyIcon(sfi.hIcon);
}
CoUninitialize();

return icon;
}

Okay,完美,兄弟们,可以获取到原汁原味的图标了

小丑

好的,缓存是没缓存了,但是卡炸了

这波啊,这波是时间换时效

系统级缓存(Attention)

注意,除了Qt内部的图标缓存外,Windows文件系统也会对进行一定的缓存

参考我的上一篇文章:更改文件夹图标 & 使其立即生效 - MrBeanC-Blog (cls.ink)

这么说吧,在一定时间内连续调用SHGetFileInfo,其速度会越来越快

但是第一次的调用时长可达数秒(最多)或数百毫秒(常见),这是不可接受的

姜还是老的辣

但是QFileIconProvider人家就不卡,这可和什么内部缓存无关啊

人家第一次获取就不卡的

而且比SHGetFileInfo快了一个数量级(WTF?)

这不科学吧,用了什么黑科技

QFileIconProvider源码剖析

都把自己的心肺肠子翻出来,晒一晒,洗一洗,拾掇拾掇 —— 康熙

1
2
3
4
5
6
7
8
9
10
QIcon QFileIconProviderPrivate::getIcon(const QFileInfo &fi) const {
return QGuiApplicationPrivate::platformTheme()->fileIcon(fi, toThemeIconOptions(options));
}
QIcon QFileIconProvider::icon(const QFileInfo &info) const {
...
QIcon retIcon = d->getIcon(info);
if (!retIcon.isNull())
return retIcon;
...
}

我们可以看到实现细节隐藏在了platformTheme()->fileIcon()

这也是Qt实现跨平台的秘诀:每个平台写一份代码

这里我们来重点关注一下Windows的实现(qtbase/src/plugins/platforms/windows/qwindowstheme.cpp

1
2
3
QIcon QWindowsTheme::fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOptions iconOptions) const {
return QIcon(new QWindowsFileIconEngine(fileInfo, iconOptions));
}

看到这里不禁感叹:妙哉妙哉

原来我们通过QFileIconProvider::icon获取的图标里并没有真正的图像数据,而是封装了一个Engine(用于获取图标的中介)

1
2
3
4
5
6
7
8
9
10
11
12
13
class QWindowsFileIconEngine : public QAbstractFileIconEngine
{
public:
explicit QWindowsFileIconEngine(const QFileInfo &info, QPlatformTheme::IconOptions opts) :
QAbstractFileIconEngine(info, opts) {}

QList<QSize> availableSizes(QIcon::Mode = QIcon::Normal, QIcon::State = QIcon::Off) const override
{ return QWindowsTheme::instance()->availableFileIconSizes(); }

protected:
QString cacheKey() const override;
QPixmap filePixmap(const QSize &size, QIcon::Mode mode, QIcon::State) override;
};

怪不得.icon()获取图标那么快呢,敢情是没有真数据啊

好的,盯帧一下我们就能发现filePixmap这个函数才是真正的重点

QWindowsFileIconEngine::filePixmap

文件夹缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool cacheableDirIcon = fileInfo().isDir() && !fileInfo().isRoot();
if (cacheableDirIcon) {
QMutexLocker locker(&mx);
int iIcon = (useDefaultFolderIcon && defaultFolderIIcon >= 0) ? defaultFolderIIcon
: **dirIconEntryCache.object(filePath);
if (iIcon) {
QPixmapCache::find(dirIconPixmapCacheKey(iIcon, iconSize, requestedImageListSize), &pixmap);
if (pixmap.isNull()) // Let's keep both caches in sync
dirIconEntryCache.remove(filePath);
else
return pixmap;
}
}
...
...
if (cacheableDirIcon) {
QMutexLocker locker(&mx);
QPixmapCache::insert(key, pixmap);
dirIconEntryCache.insert(filePath, FakePointer<int>::create(info.iIcon));
}

从这里我们可以发现:

  • 只对文件夹(Directory)进行了缓存(cacheableDirIcon),没有缓存文件(file); // Why?
  • 使用了QPixmapCache进行缓存,而且是::insert静态方法

静态方法说明是全局状态

[现在可以公开的情报]

!!因此,可以使用QPixmapCache::clear()来清除QFileIconProvider的缓存

😑还得是源码

深入虎穴

Okay,都到这里了,让我们看看Qt到底用了什么方法来获取图标吧

filePixmap()中调用了以下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static bool shGetFileInfoBackground(const QString &fileName, DWORD attributes,
SHFILEINFO *info, UINT flags,
qint64 timeOutMSecs = 5000)
{
static QShGetFileInfoThread *getFileInfoThread = nullptr;
if (!getFileInfoThread) {
getFileInfoThread = new QShGetFileInfoThread;
getFileInfoThread->start(); //启动线程,调用run()函数
}

bool result = false;
QShGetFileInfoParams params(fileName, attributes, info, flags, &result);
if (!getFileInfoThread->runWithParams(&params, timeOutMSecs)) {
// Cancel and reset getFileInfoThread. It'll
// be reinitialized the next time we get called.
getFileInfoThread->cancel();
getFileInfoThread = nullptr;
qWarning().noquote() << "SHGetFileInfo() timed out for " << fileName;
return false;
}
return result;
}

首先这个命名非常有意思,为什么是Background呢,难道是多线程?(第一行代码就结束了比赛

好吧,关注点难道不应该是shGetFileInfoBackgroundSHGetFileInfoAPI写脸上了吗

这里有一点比较疑惑:为什么Qt在这里要使用多线程呢?

首先,这是一个filePixmap()是一个同步函数,也没法异步返回

其次,在主线程中调用了m_doneCondition.wait进行等待,等待被子线程唤醒或超时(5s),意味着还是会阻塞主线程,所以子线程有什么意义

其实最主要的意义就是这个超时机制,由于前面说的SHGetFileInfo可能非常耗时,我们不能无限期等待下去(GUI会卡死)

所以利用多线程 + 条件变量的方式实现超时机制

仅单线程的话,控制权会陷入API调用SHGetFileInfo,无法退出

下面附上QShGetFileInfoThread的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class QShGetFileInfoThread : public QThread {
...
void run() override {
m_init = CoInitializeEx(nullptr, COINIT_MULTITHREADED);

QMutexLocker readyLocker(&m_readyMutex);
while (!m_cancelled.loadRelaxed()) {
if (!m_params && !m_cancelled.loadRelaxed()
&& !m_readyCondition.wait(&m_readyMutex, QDeadlineTimer(1000ll)))
continue;

if (m_params) {
const QString fileName = m_params->fileName;
SHFILEINFO info;
const bool result = SHGetFileInfo(reinterpret_cast<const wchar_t *>(fileName.utf16()),
m_params->attributes, &info, sizeof(SHFILEINFO),
m_params->flags);
m_doneMutex.lock();
if (!m_cancelled.loadRelaxed()) {
*m_params->result = result;
memcpy(m_params->info, &info, sizeof(SHFILEINFO));
}
m_params = nullptr;

m_doneCondition.wakeAll();
m_doneMutex.unlock();
}
}

if (m_init != S_FALSE)
CoUninitialize();
}

bool runWithParams(QShGetFileInfoParams *params, qint64 timeOutMSecs) {
QMutexLocker doneLocker(&m_doneMutex);

m_readyMutex.lock();
m_params = params;
m_readyCondition.wakeAll();
m_readyMutex.unlock();

return m_doneCondition.wait(&m_doneMutex, QDeadlineTimer(timeOutMSecs));
}
...
};

Why QFileIconProvider Fast

好的,接下来还剩下一个问题,为什么用QFileIconProvider 比直接用SHGetFileInfo更快(在加载大量图标的时候)

首先就是内部缓存机制,其次是延迟获取机制(先返回QIcon + Engine,在合适的时候再调用filePixmap

这样可以结合Qt的绘制机制进行一些奇怪的优化(多线程、局部重绘之类的)

以上,我猜的

好的,Peace

Ref

MrBeanCpp/Dr-Folder: Select icons for your software folders automatically. (github.com)

更改文件夹图标 & 使其立即生效 - MrBeanC-Blog


如何获取文件(夹)图标 without Cache
https://mrbeancpp.github.io/2024/08/01/How-to-get-file-folder-icon/
作者
MrBeanC
发布于
2024年8月1日
许可协议