ShellEx.info

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:

  1. Looks up the file’s extension in the registry to find the associated PreviewHandler CLSID.
  2. Creates an instance of the COM object.
  3. Calls IInitializeWithFile::Initialize() or IInitializeWithStream::Initialize() to pass the file.
  4. Calls IPreviewHandler::SetWindow() to provide the target rendering area.
  5. 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:

InterfacePurpose
IPreviewHandlerCore interface — SetWindow, DoPreview, Unload
IInitializeWithStreamPreferred — receive file as IStream (safer, works with virtual files)
IInitializeWithFileAlternative — receive file path directly
IPreviewHandlerVisualsOptional — respond to background color and font changes
IOleWindowRequired — provides GetWindow/ContextSensitiveHelp
IObjectWithSiteRequired — receives host site for DPI awareness and theming

Step 1: Project Setup

Create a new ATL Project in Visual Studio:

  1. File → New → Project → ATL Project.
  2. Name it MyPreviewHandler.
  3. In the wizard, select DLL (Dynamic Link Library).
  4. Finish the wizard.

Add a new ATL Simple Object:

  1. Right-click the project → Add → Class → ATL Simple Object.
  2. Name it MyFilePreview.
  3. 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

  1. Open your file in Explorer and enable the Preview Pane (View → Preview Pane).
  2. Select a file with your custom extension.
  3. In Visual Studio: DebugAttach to Process → Select prevhost.exe.
  4. Set breakpoints in your DoPreview() method.
  5. 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

ProblemCauseSolution
Preview pane stays blankCOM class not registered correctlyCheck CLSID registration with OleView
Preview shows for wrong file typesIncorrect extension associationVerify .myext registry path
Handler crashes silentlyUnhandled exception in DoPreviewWrap in try/catch, log to DebugView
”This file can’t be previewed”Handler not in PreviewHandlers listAdd CLSID to HKLM PreviewHandlers key
DLL won’t re-register after rebuildprevhost.exe holds a lockKill 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

  1. Prefer IInitializeWithStream over IInitializeWithFile. Streams work with virtual files, cloud files, and ZIP contents.

  2. 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.

  3. Respect DPI scaling. Use GetDpiForWindow() and scale your rendering accordingly. On Windows 11, preview panes can have different DPI than the main monitor.

  4. Handle SetRect properly. Explorer resizes the preview pane frequently. Your handler must resize its content to match without leaking resources.

  5. 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.

  6. 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:

  1. Implement IPreviewHandler + IInitializeWithStream as the minimum
  2. Register under the target file extension’s shellex\{8895b1c6-...} key
  3. Debug by attaching to prevhost.exe, not explorer.exe
  4. Never block the UI thread — parse files asynchronously
  5. Free all resources in Unload() to prevent memory leaks
Need a starting template? Check out the Developer Guide for a broader overview of building shell extensions in modern C++.