本文最后更新于:2024年8月1日 晚上
前情提要(迫真) 书接上回,我们已经介绍了如何设置文件夹图标,想必大家都意犹未尽
那么根据有Set
必有Get
定理,本节我们来讲讲图标的获取(on Windows
)
众所周知,Qt
是一个Convenient
的C++
库
So,长话短说,两行代码秒了
1 2 QFileIconProvider iconPro; QIcon icon = iconPro.icon (dirPath);
好的,请大家有序退场👋
新吃席观众还在懵逼,老吃席观众已经开始排排坐了:都知道笔者出手必有大坑
缓存(Cache) 大家看看这是什么☝,老朋友了
缓存是个好东西啊,空间换时间,现代CPU都上到三级缓存了
可是没有什么是永恒的,是非好坏、真假对错,甚至是这句话本身 —— MrBeanC
和文件系统打交道,时效性确实不太能保证,无论是HDD
亦或是SSD
,在Memory
面前就是弟弟
那么Qt
给QFileIconProvider
上缓存也不难理解了
文件夹图标缓存 没错,在程序生命周期内,使用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); CoInitialize (NULL ); 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); 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 ()) 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 (); } bool result = false ; QShGetFileInfoParams params (fileName, attributes, info, flags, &result) ; if (!getFileInfoThread->runWithParams (¶ms, timeOutMSecs)) { getFileInfoThread->cancel (); getFileInfoThread = nullptr ; qWarning ().noquote () << "SHGetFileInfo() timed out for " << fileName; return false ; } return result; }
首先这个命名非常有意思,为什么是Background 呢,难道是多线程?(第一行代码就结束了比赛
好吧,关注点难道不应该是shGetFileInfoBackground
把SHGetFileInfo
API写脸上了吗
这里有一点比较疑惑:为什么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