Tutorial: Custom Thumbnails (Almost) Everywhere
Table of Contents
Background / Definitions#
I work with the Source engine a lot. I recently made a C++ library for working with Source engine textures, and I’m reasonably proud of it. While I was working on it, a friend of mine said they’d like to use it to make thumbnails for these texture files, so they’d be much easier to work with in a file browser. I said “hey that sounds really fun” and proceeded to steal their idea and make it myself (with permission). This is how that went down.
For simplicity, I use the terms “thumbnail creator”, “thumbnail generator”, “thumbnail provider”, and “thumbnailer” interchangeably to mean a program or library that handles generating thumbnails.
Thumbnailer Types#
Before getting into the process of making thumbnailers, there are two things you should know. There are two main methods of handling thumbnail generation, both with upsides and downsides.
- Run a command (usually calling a custom program) which creates the thumbnail image on disk.
- Pros:
- Easy to program.
- Can easily use shell scripts or other scripting languages to generate the thumbnail.
- Cons:
- Time to generate and display a thumbnail is dependent on how fast your storage medium is.
- The thumbnail must be read by a separate process to be displayed.
- Pros:
- Hook into a library which exposes a function to get the raw thumbnail data.
- Pros:
- More control is given to the calling process over what to do with the data. Should it be written to disk? Perhaps, but maybe for some file types we don’t want to cache the thumbnail because it can be generated very quickly.
- Time to generate and display a thumbnail is not dependent on how fast your storage medium is.
- Cons:
- More difficult to program. The library will likely depend on OS-specific code, and there’s usually some degree of boilerplate that must be added.
- Pros:
Linux thumbnailers use the former method. KDE’s thumbnail plugins, Windows thumbnail generators, and macOS thumbnail plugins use the latter method.
Linux (Generic)#
I started here, at the easiest end of the spectrum. Doing some basic research, I found the tumbler
service, as well as
most Linux file browsers, read thumbnailer entries stored in $PREFIX/share/thumbnailers
(where $PREFIX
is usually
/usr
) to figure out how to generate thumbnails. Thumbnail entries are simple key-value files that look very similar to
desktop entries. They begin with the text [Thumbnailer Entry]
, and support three keys.
TryExec
: optional, the program to check the existence of before executing the command to create the thumbnail.Exec
: required, the command that will result in the thumbnail’s creation if successful.MimeType
: required, a semicolon-separated and terminated list of MIME types to generate thumbnails for.
The command put in for the Exec
key supports certain substitutions.
%u
: the URI pointing to the input file.%i
: the path to the input file.%o
: the path to the output file.%s
: the maximum desired size of the thumbnail for either dimension, in pixels. This will probably be a power of two between 128 and 4096, inclusive.%%
: the escaped percent character.
For the MimeType
field I needed to create a custom MIME type entry, since VTF files are not part of any universally
recognized standard. This would also come in handy for the KDE thumbnailer.
The MIME type entry standard can be found here. Mine was
very simple and looked like this.
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="image/x-vtf">
<comment>Valve Texture Format File</comment>
<acronym>VTF</acronym>
<expanded-acronym>Valve Texture Format</expanded-acronym>
<glob-deleteall/>
<glob pattern="*.vtf"/>
<glob pattern="*.VTF"/>
</mime-type>
</mime-info>
Thus, my complete thumbnailer entry looked like this.
[Thumbnailer Entry]
TryExec=/opt/vtf-thumbnailer/vtf-thumbnailer
Exec=/opt/vtf-thumbnailer/vtf-thumbnailer -i %i -o %o -s %s
MimeType=image/x-vtf;
(Yes, I install my software in /opt
. No particular reason why, it’s just how I learned to do it.)
The program uses these values to create the thumbnail. Thanks to the FreeDesktop thumbnail specification, we can assume the output image from a given thumbnailer will be an 8-bit PNG. The specification also says you should set some extra metadata in the PNG, but this is only required if you’d like the thumbnails to update when their original files’ contents change. These fields are listed in the aforementioned FreeDesktop specification, and are reproduced here for convenience.
Key | Description | Useful? |
---|---|---|
Thumb::URI | The URI of the original file. | Used to find the original file. |
Thumb::MTime | The modification time of the original file. | Used to invalidate cache entries when the original file’s modification time exceeds the cache entry. |
Thumb::Size | The size in bytes of the original file. | Can help in verifying the original file has or hasn’t changed, but mostly pointless. |
Thumb::Mimetype | The file MIME type. | Mostly pointless. |
Description | A description of the thumbnail for accessibility purposes. | Unknown how you’d generate this automatically. |
Software | The name of the thumbnailer that generated the thumbnail. | Pointless. |
I ignored this metadata step because I was using stb_image_write
to save the PNG, and it doesn’t support setting PNG
metadata fields. It didn’t really impact the thumbnailer’s functionality, so I didn’t want to go to the extra trouble.
Three files are written when installing this application. No further installation steps are required beyond writing these files, although a reboot might be necessary to get everything working.
/opt/vtf-thumbnailer/vtf-thumbnailer
/usr/share/mime/packages/vtf-thumbnailer.xml
/usr/share/thumbnailers/vtf-thumbnailer.thumbnailer
Linux’s thumbnailer system is exceedingly simple and very pleasant to work with in general, even if it is a bit slow.
Linux (KDE)#
As I finished up work on the thumbnailer, I noticed it was not working in Dolphin. I installed Thunar, observed it working there (which was cool), and then realized Dolphin (or really KDE in general) has a completely different framework for serving thumbnails. This is where things start to get painful.
Dolphin uses KDE-specific plugins, which in turn depend on Qt. This is the basic C++ boilerplate, extracted from the KDE developers’ sample thumbnailer plugin repository.
#include <KIO/ThumbnailCreator>
#include <QImage>
class VTFThumbCreator : public KIO::ThumbnailCreator {
public:
using ThumbnailCreator::ThumbnailCreator;
KIO::ThumbnailResult create(const KIO::ThumbnailRequest& request) override;
};
#include "kde.h"
#include <KPluginFactory>
K_PLUGIN_CLASS_WITH_JSON(VTFThumbCreator, "plugin.json")
KIO::ThumbnailResult VTFThumbCreator::create(const KIO::ThumbnailRequest& request) {
// return KIO::ThumbnailResult::pass(image) with image being a valid QImage
// instance on success
return KIO::ThumbnailResult::fail();
}
#include <kde.moc>
All KDE library plugins have a plugin.json
file which gets packed into the binary. In this case it holds
important metadata such as whether to cache generated thumbnails, MIME types to generate thumbnails for, and the plugin
name.
{
"CacheThumbnail": false,
"KPlugin": {
"MimeTypes": ["image/x-vtf"],
"Name": "Valve Texture Format Files (VTF Thumbnailer)"
},
"MimeType": "image/x-vtf;"
}
The plugin name will be read by Dolphin and appear as the name of the entry in the previews settings. (And by the way, new plugins must be manually enabled there. If your plugin doesn’t work, make sure it’s both showing up in this menu and that it’s enabled.)
Two more files are necessary for thumbnailer plugins, the XML metadata file and the desktop entry. The metadata file is meant for something like a storefront, where it shows the author, version, description of the plugin, and so on.
<?xml version="1.0" encoding="utf-8"?>
<component type="addon">
<id>info.craftablescience.vtf-thumbnailer</id>
<metadata_license>MIT</metadata_license>
<project_license>MIT</project_license>
<extends>org.kde.dolphin.desktop</extends>
<extends>org.kde.konqueror.desktop</extends>
<extends>org.kde.krusader.desktop</extends>
<extends>org.kde.gwenview.desktop</extends>
<name>VTF Thumbnailer</name>
<summary>Valve Texture Format thumbnail generator</summary>
<description>
<p>This plugin allow KDE software to display thumbnails for Valve Texture Format files.</p>
</description>
<url type="homepage">https://github.com/craftablescience/vtf-thumbnailer</url>
<url type="bugtracker">https://github.com/craftablescience/vtf-thumbnailer/issues</url>
<project_group>craftablescience</project_group>
<categories>
<category>Graphics</category>
</categories>
<icon type="stock">application-postscript</icon>
</component>
The desktop entry is used to identify the plugin as a thumbnailer. One important thing to note is when
ThumbnailerVersion
is incremented after an update, all cached thumbnails created with this thumbnailer will be
regenerated if caching is enabled. (Yes, most of these keys are duplicated from the plugin JSON. I don’t exactly know
why this file needs to exist.)
[Desktop Entry]
Type=Service
Name=Valve Texture Format Files (VTF Thumbnailer)
X-KDE-ServiceTypes=ThumbCreator
MimeType=image/x-vtf;
X-KDE-Library=vtf-thumbnailer
CacheThumbnail=false
ThumbnailerVersion=1
Possibly the most important file that is needed is the CMake buildscript. CMake is essentially required here, as you
need to use the KDE CMake utilities to get the correct installation paths for the various files, find/link to KDE
Framework libraries, and create the plugin target. Unfortunately it’s a lot of CMake. As with pretty much everything
else, I copied most of it from other thumbnailers, making modifications where necessary to allow it to compile for
both KDE v5 and v6 depending on the given value of QT_MAJOR_VERSION
.
set(QT_MIN_VERSION "5.15.2")
set(KF_MIN_VERSION "5.92.0")
set(KDE_COMPILERSETTINGS_LEVEL "5.82")
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
include(ECMOptionalAddSubdirectory)
include(KDEInstallDirs${QT_MAJOR_VERSION})
include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE)
include(FeatureSummary)
include(ECMDeprecationSettings)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Gui)
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} REQUIRED COMPONENTS KIO)
# Plugin
ecm_set_disabled_deprecation_versions(QT 5.15.2 KF 5.100.0)
set(BUILD_SHARED_LIBS ON)
kcoreaddons_add_plugin(vtf-thumbnailer INSTALL_NAMESPACE "kf${QT_MAJOR_VERSION}/vtf-thumbnailer")
target_sources(vtf-thumbnailer PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src/common.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/common.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/kde.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/kde.h")
target_link_libraries(vtf-thumbnailer PUBLIC
KF${QT_MAJOR_VERSION}::KIOGui
Qt::Gui)
install(TARGETS vtf-thumbnailer
DESTINATION ${KDE_INSTALL_PLUGINDIR})
# Desktop
if(QT_MAJOR_VERSION STREQUAL "6")
set(KDE_INSTALL_KSERVICESDIR "${KDE_INSTALL_DATADIR}/kio")
endif()
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/kde/vtf-thumbnailer.desktop"
DESTINATION ${KDE_INSTALL_KSERVICESDIR})
# Metadata
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/kde/info.craftablescience.vtf-thumbnailer.metainfo.xml"
DESTINATION ${KDE_INSTALL_METAINFODIR})
# MIME type info
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/linux/vtf-thumbnailer.xml"
DESTINATION ${KDE_INSTALL_MIMEDIR}
RENAME "vtf-thumbnailer-kde${QT_MAJOR_VERSION}.xml")
The version of Qt that you compile against matters. If you want your thumbnails to work on KDE Plasma v5, compile
against Qt v5. Similarly, for KDE Plasma v6 compile against Qt v6. Qt guarantees compatibility for basic objects like
QImage
for the entire duration of major versions, so use earlier minor versions of Qt to maximize compatibility. It’s
expected that you will ship multiple versions of your thumbnailer built for different major KDE versions (or simply
target the latest version and call it a day, but keep in mind as of 2024 Debian still hasn’t updated to KDE v6).
Unfortunately, I couldn’t figure out how to compile the plugin for KDE v6 in GitHub Actions, so I only created a release
for KDE v5.
Finally, to actually compile this code you need some packages installed. On Debian-based distros the packages are called
extra-cmake-modules
and libkf5kio-dev
, accessible through apt
. This took me far longer than I’d like to admit to
figure out, as most of the official thumbnailers don’t specify what packages they need to build. I found a listing of
KDE’s framework packages on a forum post and slowly pared it down until these two were left.
Five files are written when installing this plugin. For KDE v5, the paths look like this. The computer may need a reboot after installation. I haven’t yet figured out if that’s completely necessary, but it doesn’t hurt.
/usr/lib/plugins/vtf-thumbnailer.so
/usr/lib/plugins/kf5/vtf-thumbnailer/vtf-thumbnailer.so
(I still have not figured out why the library gets installed to two separate locations!)/usr/share/mime/packages/vtf-thumbnailer-kde5.xml
/usr/share/metainfo/info.craftablescience.vtf-thumbnailer.metainfo.xml
/usr/share/kservices5/vtf-thumbnailer.desktop
Windows (Vista onward)#
Hell. It’s hell here. After finishing the KDE thumbnail plugin it took about 18 hours more work to figure out the
Windows thumbnail provider, and that’s while referencing several examples. Maybe I’m stupid, but I prefer to think
Windows is a pile of convoluted garbage that’s been left in the sun too long. I made small tweaks and changes to this
code for ages, and the reason it wasn’t working, for presumably several hours, was that I wasn’t exporting all the
necessary symbols. But the thing is, some symbols here cannot be exported from C++ with a __declspec
declaration,
because it would be redefining symbols from some random Windows header. Making a .def
file to control symbol
visibility is required.
Anyway, Windows is interesting. The thumbnail provider returns an HBITMAP
, very much like the KDE plugin’s QImage
but infinitely worse. Essentially it’s the same style of code as the KDE plugin but with worse types and infinitely
more boilerplate. The metadata files are eschewed for the registry (of course), so registering the thumbnail
provider is fairly trivial at least. This is what the code ended up looking like.
#include <cstddef>
#include <vector>
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <initguid.h>
#include <ShlObj_core.h>
#include <Shlwapi.h>
#include <strsafe.h>
#include <thumbcache.h>
#include <wrl.h>
#define VTF_THUMBNAILER_CLSID_STR L"{8b206795-0606-40ca-9eac-1d049c7ff3be}"
DEFINE_GUID(VTF_THUMBNAILER_CLSID, 0x8b206795, 0x0606, 0x40ca, 0x9e, 0xac, 0x1d, 0x04, 0x9c, 0x7f, 0xf3, 0xbe);
#define GLOBAL(ret) extern "C" [[maybe_unused]] ret __stdcall
To start, we set up the GUID for the thumbnail provider. I used an online generator, since it has to be unique. Next comes the thumbnail provider class.
class VTFThumbnailProvider : public IThumbnailProvider, public IInitializeWithStream {
public:
VTFThumbnailProvider()
: refCount(1)
, stream(nullptr) {}
~VTFThumbnailProvider() {
if (this->stream) {
this->stream->Release();
this->stream = nullptr;
}
}
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) override {
static const QITAB qit[] = {
QITABENT(VTFThumbnailProvider, IThumbnailProvider),
QITABENT(VTFThumbnailProvider, IInitializeWithStream),
{nullptr},
};
return QISearch(this, qit, riid, ppv);
}
STDMETHOD_(ULONG, AddRef)() override {
return InterlockedIncrement(&this->refCount);
}
STDMETHOD_(ULONG, Release)() override {
const ULONG rc = InterlockedDecrement(&this->refCount);
if (rc == 0) {
delete this;
return 0;
}
return rc;
}
STDMETHOD(Initialize)(IStream* stream_, DWORD) override {
if (!stream_) {
return E_POINTER;
}
this->stream = stream_;
this->stream->AddRef();
return S_OK;
}
STDMETHOD(GetThumbnail)(UINT cx, HBITMAP* phbmp, WTS_ALPHATYPE* pdwAlpha) override {
if (!phbmp || !pdwAlpha) {
return E_POINTER;
}
if (!this->stream) {
return E_UNEXPECTED;
}
STATSTG stat;
HRESULT hr = this->stream->Stat(&stat, STATFLAG_NONAME);
if (FAILED(hr)) {
return hr;
}
ULONG bytesRead = 0;
std::vector<std::byte> data(stat.cbSize.QuadPart);
hr = this->stream->Read(data.data(), static_cast<ULONG>(stat.cbSize.QuadPart), &bytesRead);
if (FAILED(hr) || bytesRead != stat.cbSize.QuadPart) {
return E_FAIL;
}
// The file's data has now been read to the data vector, process it
// Return E_UNEXPECTED if the file is broken
// Past this point data is now storing BGRA8888 (yes, BGRA8888)
// thumbnail image data
BITMAPINFO bmi = {};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = static_cast<LONG>(width);
bmi.bmiHeader.biHeight = -static_cast<LONG>(height);
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
void* pBits = nullptr;
HDC hdc = GetDC(nullptr);
HBITMAP hBitmap = CreateDIBSection(hdc, &bmi, DIB_RGB_COLORS, &pBits, nullptr, 0);
ReleaseDC(nullptr, hdc);
if (!hBitmap) {
return E_OUTOFMEMORY;
}
std::memcpy(pBits, data.data(), data.size());
*phbmp = hBitmap;
*pdwAlpha = WTSAT_ARGB;
return S_OK;
}
private:
ULONG refCount;
IStream* stream;
};
GLOBAL(HRESULT) VTFThumbnailProvider_CreateInstance(REFIID riid, void** ppv) {
auto* pNew = new(std::nothrow) VTFThumbnailProvider;
HRESULT hr = pNew ? S_OK : E_OUTOFMEMORY;
if (SUCCEEDED(hr)) {
hr = pNew->QueryInterface(riid, ppv);
pNew->Release();
}
return hr;
}
typedef HRESULT(*PFNCREATEINSTANCE)(REFIID, void**);
struct CLASS_OBJECT_INIT {
const CLSID* pClsid;
PFNCREATEINSTANCE pfnCreate;
};
const CLASS_OBJECT_INIT c_rgClassObjectInit[] = {{&VTF_THUMBNAILER_CLSID, VTFThumbnailProvider_CreateInstance}};
The boilerplate isn’t nasty yet, but working with HBITMAP
is frustrating. For some reason it wants to read the pixel
order in reverse, so BGRA8888
gets turned into ARGB8888
. There are a few formats the bitmap can store pixel data in,
but for the sake of simplicity I used BGRA8888
everywhere, even for opaque textures. There’s not too much interesting
going on here so let’s move on to the factory.
HINSTANCE g_hInst = nullptr;
ULONG g_cRefModule = 0;
GLOBAL(BOOL) DllMain(HINSTANCE hInstance, DWORD dwReason, void*) {
if (dwReason == DLL_PROCESS_ATTACH) {
g_hInst = hInstance;
DisableThreadLibraryCalls(hInstance);
}
return TRUE;
}
GLOBAL(void) DllAddRef() {
InterlockedIncrement(&g_cRefModule);
}
GLOBAL(void) DllRelease() {
InterlockedDecrement(&g_cRefModule);
}
GLOBAL(HRESULT) DllCanUnloadNow() {
return !g_cRefModule ? S_OK : S_FALSE;
}
class VTFThumbnailProviderFactory : public IClassFactory {
public:
static HRESULT CreateInstance(REFCLSID clsid, const CLASS_OBJECT_INIT* pClassObjectInits, size_t cClassObjectInits, REFIID riid, void** ppv) {
*ppv = nullptr;
HRESULT hr = CLASS_E_CLASSNOTAVAILABLE;
for (size_t i = 0; i < cClassObjectInits; i++) {
if (clsid == *pClassObjectInits[i].pClsid) {
IClassFactory *pClassFactory = new(std::nothrow) VTFThumbnailProviderFactory(pClassObjectInits[i].pfnCreate);
hr = pClassFactory ? S_OK : E_OUTOFMEMORY;
if (SUCCEEDED(hr)) {
hr = pClassFactory->QueryInterface(riid, ppv);
pClassFactory->Release();
}
break;
}
}
return hr;
}
explicit VTFThumbnailProviderFactory(PFNCREATEINSTANCE pfnCreate_)
: refCount(1)
, pfnCreate(pfnCreate_) {
::DllAddRef();
}
IFACEMETHODIMP QueryInterface(REFIID riid, void** ppv) override {
static const QITAB qit[] = {
QITABENT(VTFThumbnailProviderFactory, IClassFactory),
{nullptr},
};
return QISearch(this, qit, riid, ppv);
}
IFACEMETHODIMP_(ULONG) AddRef() override {
return InterlockedIncrement(&this->refCount);
}
IFACEMETHODIMP_(ULONG) Release() override {
const ULONG rc = InterlockedDecrement(&this->refCount);
if (!rc) {
delete this;
}
return rc;
}
IFACEMETHODIMP CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppv) override {
return pUnkOuter ? CLASS_E_NOAGGREGATION : this->pfnCreate(riid, ppv);
}
IFACEMETHODIMP LockServer(BOOL fLock) override {
if (fLock) {
::DllAddRef();
} else {
::DllRelease();
}
return S_OK;
}
private:
~VTFThumbnailProviderFactory() {
::DllRelease();
}
ULONG refCount;
PFNCREATEINSTANCE pfnCreate;
};
GLOBAL(HRESULT) DllGetClassObject(REFCLSID clsid, REFIID riid, void** ppv) {
return VTFThumbnailProviderFactory::CreateInstance(clsid, c_rgClassObjectInit, ARRAYSIZE(c_rgClassObjectInit), riid, ppv);
}
All of that code is boilerplate to create an instance of the thumbnail provider. The following code is more interesting.
namespace {
HRESULT SetHKCRRegistryKey(LPCWSTR subKey, LPCWSTR valueName, LPCWSTR data) {
HKEY hKey;
LONG result = RegCreateKeyExW(HKEY_CLASSES_ROOT, subKey, 0, nullptr, REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &hKey, nullptr);
if (result != ERROR_SUCCESS) {
return HRESULT_FROM_WIN32(result);
}
result = RegSetValueExW(hKey, valueName, 0, REG_SZ, (const BYTE*) data, (lstrlenW(data) + 1) * sizeof(WCHAR));
RegCloseKey(hKey);
return HRESULT_FROM_WIN32(result);
}
HRESULT DeleteHKCRRegistryKey(LPCWSTR subKey) {
LONG result = SHDeleteKeyW(HKEY_CLASSES_ROOT, subKey);
return HRESULT_FROM_WIN32(result == ERROR_FILE_NOT_FOUND ? ERROR_SUCCESS : result);
}
} // namespace
GLOBAL(HRESULT) DllNotifyShell() {
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
return S_OK;
}
GLOBAL(HRESULT) DllRegisterServer() {
wchar_t modulePath[MAX_PATH];
if (!GetModuleFileNameW(g_hInst, modulePath, MAX_PATH)) {
return HRESULT_FROM_WIN32(GetLastError());
}
HRESULT hr;
hr = ::SetHKCRRegistryKey(L"CLSID\\" VTF_THUMBNAILER_CLSID_STR, nullptr, L"Valve Texture Format Files (VTF Thumbnailer)");
if (FAILED(hr)) {
return hr;
}
hr = ::SetHKCRRegistryKey(L"CLSID\\" VTF_THUMBNAILER_CLSID_STR "\\InProcServer32", nullptr, modulePath);
if (FAILED(hr)) {
return hr;
}
hr = ::SetHKCRRegistryKey(L"CLSID\\" VTF_THUMBNAILER_CLSID_STR "\\InProcServer32", L"ThreadingModel", L"Apartment");
if (FAILED(hr)) {
return hr;
}
hr = ::SetHKCRRegistryKey(L".vtf\\ShellEx\\{e357fccd-a995-4576-b01f-234630154e96}", nullptr, VTF_THUMBNAILER_CLSID_STR);
if (SUCCEEDED(hr)) {
::DllNotifyShell();
}
return hr;
}
GLOBAL(HRESULT) DllUnregisterServer() {
HRESULT hr;
hr = ::DeleteHKCRRegistryKey(L"CLSID\\" VTF_THUMBNAILER_CLSID_STR);
if (FAILED(hr)) {
return hr;
}
return ::DeleteHKCRRegistryKey(L".vtf\\ShellEx\\{e357fccd-a995-4576-b01f-234630154e96}");
}
The DllRegisterServer
and DllUnregisterServer
functions are called in the installer after the files are copied into
place through regsvr32.exe
. Passing a path to a DLL to this program will call the DllRegisterServer
function in the
DLL, which in this case modifies the necessary registry values, lets the file browser know there’s a new thumbnail
provider, and exits. Running the same command with the /u
flag will call the DllUnregisterServer
function in the DLL
instead.
As for the specific registry keys, each one resides under HKEY_CLASSES_ROOT
. The CLSID\<GUID>
value is the thumbnail
provider name. The value for CLSID\<GUID>\InProcServer32
is the path to the DLL. The value for
CLSID\<GUID>\InProcServer32\ThreadingModel
should always be Apartment
according to the scant Windows docs on
thumbnail providers and every example of one I’ve found. Finally, the value for
<file extension>\ShellEx\{e357fccd-a995-4576-b01f-234630154e96}
(yes, you need that exact GUID) should be your own
thumbnail provider’s GUID. These are all Windows needs to find the DLL, load it correctly, and use it on the files you
want thumbnails for.
Oh right and here’s the stupid .def
file, because the Windows API is just the worst.
LIBRARY "vtf-thumbnailer"
EXPORTS
VTFThumbnailProvider_CreateInstance PRIVATE
DllMain PRIVATE
DllAddRef PRIVATE
DllRelease PRIVATE
DllCanUnloadNow PRIVATE
DllGetClassObject PRIVATE
DllNotifyShell PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
One DLL is written when installing this application. It should start working immediately without rebooting.
C:\Program Files\vtf-thumbnailer\vtf-thumbnailer.dll
Wrap-Up#
At this point I was tired of thumbnailers. I packaged the product and shipped a release, built from the code hosted on this GitHub repository, and haven’t looked back until now. I hope this post helps and/or inspires you to make a thumbnailer of your own, it’s really not that difficult if you have a decent reference and quite rewarding in the end!