一、引言
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 HKCR\CLSID\{<CLSID>} key.
hr = StringCchPrintf(szSubkey, ARRAYSIZE(szSubkey), L"CLSID\\%s", szCLSID);
if (SUCCEEDED(hr))
{
hr = SetHKCRRegistryKeyAndValue(szSubkey, NULL, pszFriendlyName, REG_SZ);
// Create the HKCR\CLSID\{<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_ROOT\Wow6432Node\CLSID下面去,64位的才會注冊到HKEY_CLASSES_ROOT\CLSID下面去。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
