一、引言
Windows Thumbnail Handler是Windows平台下用来为关联的文件类型提供内容预览图的一套COM接口。通过实现Thumbnail相关的COM接口,就可以为为自定义的文件格式提供内容预览图。如下图所示:
Thumbnail handler以COM组件的形式注册使用。因此,如果我们想给自己的文件格式开发一个Thumbnail Handler以提供内容预览图,要以COM组件的开发方式进行开发。本人在之前并没有相关的COM开发经验,对于COM组件相关的概念、线程模型及原理也知之甚少。幸好微软为我们提供了一个样板工程(CppShellExtThumbnailHandler)。在此工程的基础上,我们可以进行修改以完成我们自己的功能。
二、实现
在动手修改代码之前,我们不妨先编译运行一下这个工程。这个工程通过读取.recipe格式的文件中的图片内容,来为其生成预览图。这个倒在其次,关键的关键是:工程中的RecipeThumbnailProvider继承自IInitializeWithStream。这个类有一个纯虚函数Initialize,其函数原型为:
IInitializeWithStream : public IUnknown { public: virtual /* [local] */ HRESULT STDMETHODCALLTYPE Initialize( /* [annotation][in] */ _In_ IStream *pstream, /* [annotation][in] */ _In_ DWORD grfMode) = 0; };
其中唯一一个对我们有用的参数是pstream还是IStream*类型的。通过这个接口我们只能获取到关联文件的字节流。这对于小文件而言问题不大,直接把字节流读到内存中来操作也无妨;但如果自定义文件达到数百MB或者数个GB时,这么做肯定是不现实的。这时候我们更希望得到文件的绝对路径。第一想法是看看有没有传递文件路径的接口呢?MSDN中赫然列出了另外一个接口:IInitializeWithFile。这个接口也有一个纯虚函数,其原型为:
IInitializeWithFile : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE Initialize( /* [string][in] */ __RPC__in_string LPCWSTR pszFilePath, /* [in] */ DWORD grfMode) = 0; };
喜出望外,pszFilePath不正是我们梦寐以求的么!那好啊,基本上来讲,只要把跟IInitializeWithStream相关的部分全部替换掉不就OK了么。 该修改的地方涉及如下:
class RecipeThumbnailProvider : public IInitializeWithFile, public IThumbnailProvider { public: // IUnknown IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv); IFACEMETHODIMP_(ULONG) AddRef(); IFACEMETHODIMP_(ULONG) Release(); // IInitializeWithFile IFACEMETHODIMP Initialize(LPCWSTR pfilePath, DWORD grfMode); // IThumbnailProvider IFACEMETHODIMP GetThumbnail(UINT cx, HBITMAP *phbmp, WTS_ALPHATYPE *pdwAlpha); ... ... }
// Query to the interface the component supported. IFACEMETHODIMP RecipeThumbnailProvider::QueryInterface(REFIID riid, void **ppv) { static const QITAB qit[] = { QITABENT(RecipeThumbnailProvider, IThumbnailProvider), QITABENT(RecipeThumbnailProvider, IInitializeWithFile), { 0 }, }; return QISearch(this, qit, riid, ppv); }
// Initializes the thumbnail handler with a stream. IFACEMETHODIMP RecipeThumbnailProvider::Initialize(LPCWSTR pfilePath, DWORD grfMode) { LOGINFO(pfilePath); return 1; }
其他的文件都不需要动,编译后注册使用。满以为可以看到日志文件中有文件路径的输出,哪知道什么反应都没有。显然,我们修改之后的Initialize()方法并没有得到调用。网上一搜,不少人也有类似的需求,也有着一样的遭遇,却并没有找到有效的解决方案。怎么解决呢?根据MSDN的解释是,需要在注册表中注册DissableProcessIsolation=1这个项。根据StackOverflow上面的解释是:旧的Windows是将Shell Extension加载到Explorer.exe中运行的,然而这样并不十分安全。于是新的Windows系统将这部分功能独立出来,用Dllhost.exe来加载Shell Extension,脱离与Explorer.exe的关联。这在一定程度降低了Explorer.exe崩溃的概率。相比于IInitializeWithFile, MSDN上也更推崇IInitializeWithStream,以保障系统的安全。
既然如此,还得再修改下程序中操作注册表部分的代码:
HRESULT SetHKCRRegistryKeyAndValue(PCWSTR pszSubKey, PCWSTR pszValueName, PCWSTR pszData, UINT type) { HRESULT hr; HKEY hKey = NULL; // Creates the specified registry key. If the key already exists, the // function opens it. hr = HRESULT_FROM_WIN32(RegCreateKeyEx(HKEY_CLASSES_ROOT, pszSubKey, 0,NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL)); if (SUCCEEDED(hr)) { if (pszData != NULL) { // Set the specified value of the key. DWORD cbData = lstrlen(pszData) * sizeof(*pszData); if (type == REG_DWORD) { cbData = 4; } hr = HRESULT_FROM_WIN32(RegSetValueEx(hKey, pszValueName, 0, type, reinterpret_cast<const BYTE *>(pszData), cbData)); } RegCloseKey(hKey); } return hr; }
HRESULT RegisterInprocServer(PCWSTR pszModule, const CLSID& clsid, PCWSTR pszFriendlyName, PCWSTR pszThreadModel) { if (pszModule == NULL || pszThreadModel == NULL) { return E_INVALIDARG; } HRESULT hr; wchar_t szCLSID[MAX_PATH]; StringFromGUID2(clsid, szCLSID, ARRAYSIZE(szCLSID)); wchar_t szSubkey[MAX_PATH]; // Create the HKCRCLSID{<CLSID>} key. hr = StringCchPrintf(szSubkey, ARRAYSIZE(szSubkey), L"CLSID\%s", szCLSID); if (SUCCEEDED(hr)) { hr = SetHKCRRegistryKeyAndValue(szSubkey, NULL, pszFriendlyName, REG_SZ); // Create the HKCRCLSID{<CLSID>}InprocServer32 key. if (SUCCEEDED(hr)) { WCHAR data[4] = { 0x01, 0x00, 0x00, 0x00 }; SetHKCRRegistryKeyAndValue(szSubkey, L"DisableProcessIsolation", data, REG_DWORD); hr = StringCchPrintf(szSubkey, ARRAYSIZE(szSubkey), L"CLSID\%s\InprocServer32", szCLSID); if (SUCCEEDED(hr)) { // Set the default value of the InprocServer32 key to the // path of the COM module. hr = SetHKCRRegistryKeyAndValue(szSubkey, NULL, pszModule, REG_SZ); if (SUCCEEDED(hr)) { // Set the threading model of the component. hr = SetHKCRRegistryKeyAndValue(szSubkey, L"ThreadingModel", pszThreadModel, REG_SZ); } } } } return hr; }
注册看看结果:
注册表上是没什么问题了。而我们的文件路径也顺利在日志文件中出现了:
而我们也可以看到自定义文件也能获取到内容预览图了:
三、小结
整个摸索过程中,最痛苦的就是调试方法的盲目性。因为网上没有具体的指导教程,根本不知道这样改是因为原理上不通还是因为操作上的错误,而导致Shell Extension不起作用的。此外,Shell Extension的调试也很困难,只能通过日志文件的输出来判定大致的出错范围。编译出来的COM服务只能通过RegSvr32.exe注册使用:
$ RegSvr32 CppShellExtThumbnailHandler.dll
虽然RegSvr32.exe中带了一个32,但其实32位和64位的都叫这个名字。在64位系统上,32位的RegSvr32.exe会把服务注册到HKEY_CLASSES_ROOTWow6432NodeCLSID下面去,64位的才会注册到HKEY_CLASSES_ROOTCLSID下面去。RegSvr32.exe会根据编译出来的dll的位数来调用对应版本的RegSvr32.exe
另外,在使用RegSvr32.exe进行注册服务时,如果当前的DLL还依赖其他的DLL,那么会出现注册失败的情况:
这时候要做的就是,把所有依赖的DLL都放到一起,或者放到System32目录下面去。这样就可以正常的注册了。
详细的样例工程已经上传到我的github: https://github.com/csuft/WindowsThumbnail。
四、参考链接
- http://slion.net/view/Dev/MakingOfMs3dThumbnailProvider#Code_Samples
- https://social.msdn.microsoft.com/Forums/en-US/80617ead-f9c4-422a-a405-06fd3837f7be/problem-about-iinitializewithfile-ithumbnailprovider?forum=windowssearch
- http://stackoverflow.com/questions/24232451/debugging-shell-extensions-in-win-7-and-8-1
- https://code.msdn.microsoft.com/windowsapps/CppShellExtThumbnailHandler-32399b35
- http://stackoverflow.com/questions/4508012/unable-to-register-dll-using-regsvr32