Building a Custom File Preview Handler for Windows Explorer: Complete Developer Tutorial
Updated February 2026 — Targets Windows 11 24H2
The Preview Pane in Windows Explorer is one of the most useful features for power users — select a file and instantly see its contents without opening an application. But what happens when your custom file format shows a blank preview? The answer is to build a Preview Handler — a type of Shell Extension that teaches Explorer how to render your file type.
This comprehensive developer tutorial walks you through implementing the IPreviewHandler COM interface in C++ using ATL, registering and debugging your handler, and handling edge cases that trip up most developers.
Architecture Overview
A Preview Handler is an in-process COM server (DLL) that implements a specific set of COM interfaces. When a user selects a file in Explorer and the Preview Pane is open, Explorer:
- Looks up the file’s extension in the registry to find the associated
PreviewHandlerCLSID. - Creates an instance of the COM object.
- Calls
IInitializeWithFile::Initialize()orIInitializeWithStream::Initialize()to pass the file. - Calls
IPreviewHandler::SetWindow()to provide the target rendering area. - Calls
IPreviewHandler::DoPreview()to render the content.
The handler renders its preview into the provided window using standard Win32 drawing, DirectX, or by hosting a child window.
Required Interfaces
Your Preview Handler must implement:
| Interface | Purpose |
|---|---|
IPreviewHandler | Core interface — SetWindow, DoPreview, Unload |
IInitializeWithStream | Preferred — receive file as IStream (safer, works with virtual files) |
IInitializeWithFile | Alternative — receive file path directly |
IPreviewHandlerVisuals | Optional — respond to background color and font changes |
IOleWindow | Required — provides GetWindow/ContextSensitiveHelp |
IObjectWithSite | Required — receives host site for DPI awareness and theming |
Step 1: Project Setup
Create a new ATL Project in Visual Studio:
- File → New → Project → ATL Project.
- Name it
MyPreviewHandler. - In the wizard, select DLL (Dynamic Link Library).
- Finish the wizard.
Add a new ATL Simple Object:
- Right-click the project → Add → Class → ATL Simple Object.
- Name it
MyFilePreview. - In the Options page, select Apartment threading model.
Project Structure
MyPreviewHandler/
├── MyPreviewHandler.cpp // DLL entry points
├── MyPreviewHandler.def // Module definition
├── MyPreviewHandler.idl // Interface definitions
├── MyPreviewHandler.rgs // Registration script
├── MyFilePreview.h // Preview handler class
├── MyFilePreview.cpp // Implementation
└── resource.h // Resources
Step 2: Implement the Preview Handler Class
Here is the complete implementation of a Preview Handler that renders a custom text-based file format:
Header File (MyFilePreview.h)
#pragma once
#include "resource.h"
#include <ShlObj.h>
#include <ShObjIdl.h>
#include <thumbcache.h> // IPreviewHandler interfaces
class ATL_NO_VTABLE CMyFilePreview :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CMyFilePreview, &CLSID_MyFilePreview>,
public IPreviewHandler,
public IInitializeWithStream,
public IPreviewHandlerVisuals,
public IOleWindow,
public IObjectWithSite
{
public:
CMyFilePreview() : m_hwndParent(NULL), m_hwndPreview(NULL),
m_pStream(NULL), m_pSite(NULL) {
m_rcParent = {0};
m_clrBackground = RGB(255, 255, 255);
m_clrText = RGB(0, 0, 0);
}
DECLARE_REGISTRY_RESOURCEID(IDR_MYFILEPREVIEW)
DECLARE_NOT_AGGREGATABLE(CMyFilePreview)
BEGIN_COM_MAP(CMyFilePreview)
COM_INTERFACE_ENTRY(IPreviewHandler)
COM_INTERFACE_ENTRY(IInitializeWithStream)
COM_INTERFACE_ENTRY(IPreviewHandlerVisuals)
COM_INTERFACE_ENTRY(IOleWindow)
COM_INTERFACE_ENTRY(IObjectWithSite)
END_COM_MAP()
// IInitializeWithStream
STDMETHOD(Initialize)(IStream* pStream, DWORD grfMode);
// IPreviewHandler
STDMETHOD(SetWindow)(HWND hwnd, const RECT* prc);
STDMETHOD(SetRect)(const RECT* prc);
STDMETHOD(DoPreview)();
STDMETHOD(Unload)();
STDMETHOD(SetFocus)();
STDMETHOD(QueryFocus)(HWND* phwnd);
STDMETHOD(TranslateAccelerator)(MSG* pmsg);
// IPreviewHandlerVisuals
STDMETHOD(SetBackgroundColor)(COLORREF color);
STDMETHOD(SetFont)(const LOGFONTW* plf);
STDMETHOD(SetTextColor)(COLORREF color);
// IOleWindow
STDMETHOD(GetWindow)(HWND* phwnd);
STDMETHOD(ContextSensitiveHelp)(BOOL fEnterMode);
// IObjectWithSite
STDMETHOD(SetSite)(IUnknown* pUnkSite);
STDMETHOD(GetSite)(REFIID riid, void** ppvSite);
private:
HWND m_hwndParent;
HWND m_hwndPreview;
RECT m_rcParent;
IStream* m_pStream;
IUnknown* m_pSite;
COLORREF m_clrBackground;
COLORREF m_clrText;
std::wstring m_content;
HRESULT ReadStreamContent();
static LRESULT CALLBACK PreviewWndProc(HWND hwnd, UINT msg,
WPARAM wParam, LPARAM lParam);
};
Implementation (MyFilePreview.cpp)
#include "pch.h"
#include "MyFilePreview.h"
#include <string>
#include <vector>
// Read file content from IStream
HRESULT CMyFilePreview::ReadStreamContent() {
if (!m_pStream) return E_FAIL;
// Get stream size
STATSTG stat;
HRESULT hr = m_pStream->Stat(&stat, STATFLAG_NONAME);
if (FAILED(hr)) return hr;
ULONG size = stat.cbSize.LowPart;
std::vector<char> buffer(size + 1, 0);
// Read from stream
ULONG bytesRead = 0;
LARGE_INTEGER li = {0};
m_pStream->Seek(li, STREAM_SEEK_SET, NULL);
hr = m_pStream->Read(buffer.data(), size, &bytesRead);
if (FAILED(hr)) return hr;
// Convert to wide string (assuming UTF-8 input)
int wideSize = MultiByteToWideChar(CP_UTF8, 0, buffer.data(),
bytesRead, NULL, 0);
m_content.resize(wideSize);
MultiByteToWideChar(CP_UTF8, 0, buffer.data(), bytesRead,
&m_content[0], wideSize);
return S_OK;
}
// IInitializeWithStream
STDMETHODIMP CMyFilePreview::Initialize(IStream* pStream, DWORD grfMode) {
if (m_pStream) m_pStream->Release();
m_pStream = pStream;
m_pStream->AddRef();
return S_OK;
}
// IPreviewHandler::DoPreview - the core rendering method
STDMETHODIMP CMyFilePreview::DoPreview() {
HRESULT hr = ReadStreamContent();
if (FAILED(hr)) return hr;
// Register window class
WNDCLASS wc = {0};
wc.lpfnWndProc = PreviewWndProc;
wc.hInstance = _AtlBaseModule.GetModuleInstance();
wc.lpszClassName = L"MyPreviewHandlerClass";
wc.hbrBackground = CreateSolidBrush(m_clrBackground);
RegisterClass(&wc);
// Create the preview window
m_hwndPreview = CreateWindowEx(0, L"MyPreviewHandlerClass", NULL,
WS_CHILD | WS_VISIBLE,
m_rcParent.left, m_rcParent.top,
m_rcParent.right - m_rcParent.left,
m_rcParent.bottom - m_rcParent.top,
m_hwndParent, NULL,
_AtlBaseModule.GetModuleInstance(), this);
return m_hwndPreview ? S_OK : E_FAIL;
}
// Window procedure for rendering
LRESULT CALLBACK CMyFilePreview::PreviewWndProc(
HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
CMyFilePreview* pThis = reinterpret_cast<CMyFilePreview*>(
GetWindowLongPtr(hwnd, GWLP_USERDATA));
switch (msg) {
case WM_CREATE: {
CREATESTRUCT* pcs = reinterpret_cast<CREATESTRUCT*>(lParam);
pThis = reinterpret_cast<CMyFilePreview*>(pcs->lpCreateParams);
SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pThis);
return 0;
}
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
// Set colors
SetBkColor(hdc, pThis->m_clrBackground);
SetTextColor(hdc, pThis->m_clrText);
// Draw the file content
RECT rc;
GetClientRect(hwnd, &rc);
InflateRect(&rc, -10, -10); // Add padding
DrawTextW(hdc, pThis->m_content.c_str(),
(int)pThis->m_content.length(),
&rc, DT_LEFT | DT_TOP | DT_WORDBREAK);
EndPaint(hwnd, &ps);
return 0;
}
case WM_ERASEBKGND: {
HDC hdc = (HDC)wParam;
RECT rc;
GetClientRect(hwnd, &rc);
HBRUSH hBrush = CreateSolidBrush(pThis->m_clrBackground);
FillRect(hdc, &rc, hBrush);
DeleteObject(hBrush);
return 1;
}
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
// SetWindow / SetRect
STDMETHODIMP CMyFilePreview::SetWindow(HWND hwnd, const RECT* prc) {
m_hwndParent = hwnd;
m_rcParent = *prc;
if (m_hwndPreview) {
SetParent(m_hwndPreview, m_hwndParent);
SetWindowPos(m_hwndPreview, NULL,
m_rcParent.left, m_rcParent.top,
m_rcParent.right - m_rcParent.left,
m_rcParent.bottom - m_rcParent.top,
SWP_NOMOVE | SWP_NOZORDER);
}
return S_OK;
}
STDMETHODIMP CMyFilePreview::SetRect(const RECT* prc) {
m_rcParent = *prc;
if (m_hwndPreview) {
SetWindowPos(m_hwndPreview, NULL,
m_rcParent.left, m_rcParent.top,
m_rcParent.right - m_rcParent.left,
m_rcParent.bottom - m_rcParent.top,
SWP_NOMOVE | SWP_NOZORDER);
}
return S_OK;
}
// Cleanup
STDMETHODIMP CMyFilePreview::Unload() {
if (m_hwndPreview) {
DestroyWindow(m_hwndPreview);
m_hwndPreview = NULL;
}
if (m_pStream) {
m_pStream->Release();
m_pStream = NULL;
}
m_content.clear();
return S_OK;
}
Step 3: Registration
Preview handlers require specific registry entries. Create the following .reg file (replace GUIDs):
Windows Registry Editor Version 5.00
; Register the COM class
[HKEY_CLASSES_ROOT\CLSID\{YOUR-CLSID-GUID}]
@="My Custom File Preview Handler"
[HKEY_CLASSES_ROOT\CLSID\{YOUR-CLSID-GUID}\InprocServer32]
@="C:\\Path\\To\\MyPreviewHandler.dll"
"ThreadingModel"="Apartment"
; Mark it as a Preview Handler
[HKEY_CLASSES_ROOT\CLSID\{YOUR-CLSID-GUID}]
"AppID"="{6d2b5079-2f0b-48dd-ab7f-97cec514d30b}"
"DisplayName"="My File Preview Handler"
; Associate with file extension .myext
[HKEY_CLASSES_ROOT\.myext\shellex\{8895b1c6-b41f-4c1c-a562-0d564250836f}]
@="{YOUR-CLSID-GUID}"
; Add to approved Preview Handlers list
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\PreviewHandlers]
"{YOUR-CLSID-GUID}"="My Custom File Preview Handler"
The GUID {8895b1c6-b41f-4c1c-a562-0d564250836f} is the system-defined GUID for IPreviewHandler — do not change this.
Registration via regsvr32
After building the DLL, register it:
regsvr32 MyPreviewHandler.dll
To unregister:
regsvr32 /u MyPreviewHandler.dll
Step 4: Debugging
Debugging preview handlers requires special techniques because they run inside the Preview Host process (prevhost.exe), not directly in explorer.exe.
Attach to prevhost.exe
- Open your file in Explorer and enable the Preview Pane (View → Preview Pane).
- Select a file with your custom extension.
- In Visual Studio: Debug → Attach to Process → Select
prevhost.exe. - Set breakpoints in your
DoPreview()method. - Select a different file, then select your file again to trigger a new preview.
Use DebugView for Logging
Add OutputDebugString calls to your handler for tracing:
STDMETHODIMP CMyFilePreview::DoPreview() {
OutputDebugStringW(L"[MyPreview] DoPreview called\n");
HRESULT hr = ReadStreamContent();
if (FAILED(hr)) {
OutputDebugStringW(L"[MyPreview] Failed to read stream!\n");
return hr;
}
wchar_t msg[256];
swprintf_s(msg, L"[MyPreview] Content length: %zu chars\n",
m_content.length());
OutputDebugStringW(msg);
// ... rest of implementation
}
Then use DebugView (Sysinternals) to watch the output in real-time.
Common Debugging Issues
| Problem | Cause | Solution |
|---|---|---|
| Preview pane stays blank | COM class not registered correctly | Check CLSID registration with OleView |
| Preview shows for wrong file types | Incorrect extension association | Verify .myext registry path |
| Handler crashes silently | Unhandled exception in DoPreview | Wrap in try/catch, log to DebugView |
| ”This file can’t be previewed” | Handler not in PreviewHandlers list | Add CLSID to HKLM PreviewHandlers key |
| DLL won’t re-register after rebuild | prevhost.exe holds a lock | Kill prevhost.exe, then re-register |
Step 5: Supporting Multiple File Types
To support multiple file extensions, register the same CLSID for each extension:
[HKEY_CLASSES_ROOT\.myext\shellex\{8895b1c6-b41f-4c1c-a562-0d564250836f}]
@="{YOUR-CLSID-GUID}"
[HKEY_CLASSES_ROOT\.mylog\shellex\{8895b1c6-b41f-4c1c-a562-0d564250836f}]
@="{YOUR-CLSID-GUID}"
[HKEY_CLASSES_ROOT\.mydata\shellex\{8895b1c6-b41f-4c1c-a562-0d564250836f}]
@="{YOUR-CLSID-GUID}"
In your DoPreview() method, you can check the file extension to render different formats differently.
Best Practices
-
Prefer
IInitializeWithStreamoverIInitializeWithFile. Streams work with virtual files, cloud files, and ZIP contents. -
Never block the UI thread. If your file parsing takes more than 100ms, do it on a background thread and update the preview window asynchronously.
-
Respect DPI scaling. Use
GetDpiForWindow()and scale your rendering accordingly. On Windows 11, preview panes can have different DPI than the main monitor. -
Handle
SetRectproperly. Explorer resizes the preview pane frequently. Your handler must resize its content to match without leaking resources. -
Free all resources in
Unload(). This is called when the user selects a different file. Any leaked GDI handles, bitmaps, or COM references will accumulate. -
Test with AppContainer. Starting in Windows 10 1803, preview handlers run in a low-privilege AppContainer. Ensure your handler does not require elevated permissions.
Summary
Building a Windows Preview Handler involves implementing a strict set of COM interfaces, careful registration, and DPI-aware rendering. Key takeaways:
- Implement
IPreviewHandler+IInitializeWithStreamas the minimum - Register under the target file extension’s
shellex\{8895b1c6-...}key - Debug by attaching to
prevhost.exe, notexplorer.exe - Never block the UI thread — parse files asynchronously
- Free all resources in
Unload()to prevent memory leaks