Search code examples
windowswinapicomipreviewhandlerpreview-handler

Windows preview handlers don't respect drawing area set with SetRect


I've been trying to use Windows preview handlers in my application and noticed that some of the preview handlers do not respect the drawing rectangle set with SetRect(&rect).

For example, with the Edge PDF preview handler (CLSID {3A84F9C2-6164-485C-A7D9-4B27F8AC009E}), calling SetRect only works properly the first time it is called. On consecutive calls, only the rectangle dimensions are updated, but not the position:

RECT rect{0,0,500,500}; // 500x500 rectangle at position (0,0)
preview_handler->SetWindow(hwnd, &rect); // Sets parent window and initial drawing rectangle correctly
rect = {100, 100, 700, 700}; // 600x600 rect at position (100,100)
preview_handler->SetRect(&rect); // Updates rectangle size to 600x600, but stays at position (0,0)

The Powerpoint preview handler (CLSID {65235197-874B-4A07-BDC5-E65EA825B718}) behaves similarly, except it doesnt respect the position at all, it will always be at (0,0).

The Word preview handler (CLSID {84F66100-FF7C-4fb4-B0C0-02CD7FB668FE}) automatically extends the drawing rect to the entire client area of the parent Window the first time it receives focus (e.g. when you click on the "Word" area, once the preview loaded). Later SetRect calls behave properly.

According to MSDN, SetRect

Directs the preview handler to change the area within the parent hwnd that it draws into

and

...only renders in the area described by this method's prc...

I have checked what the file Explorer does. It seems like it creates a child window at the intended size and position and then puts the preview in that window. In that case, the issues listed above are irrelevant since the preview fills the entire window. My question now is if the preview handlers just don't behave properly or if there is something i'm missing.

The following is a sample application to demonstrate the issue, it expects a file path as first argument, creates a new window and previews the file. It first sets the drawing rectangle to 500x500 at position (0,0), then 600x600 at (100,100). Most error handling omitted.

Compile with cl /std:c++20 /EHsc main.cpp

#include <Windows.h>
#include <shlwapi.h>
#include <objbase.h>
#include <Shobjidl.h>
#include <thread>
#include <string>
#include <array>
#include <filesystem>
#include <iostream>

#pragma comment(lib, "Shlwapi.lib")
#pragma comment(lib, "User32.lib")
#pragma comment(lib, "Ole32.lib")

void WindowThreadProc(bool& ready, HWND& hwnd) {
    SetProcessDPIAware();
    HINSTANCE hinst = GetModuleHandleW(nullptr);

    auto wnd_proc = [](HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) ->LRESULT {
        switch (message) {
        case WM_DESTROY:
            PostQuitMessage(0);
            break;
        }
        return DefWindowProc(hWnd, message, wParam, lParam);
    };

    WNDCLASS wc{};
    wc.lpfnWndProc = wnd_proc;
    wc.hInstance = hinst;
    wc.lpszClassName = "Test Window";

    RegisterClass(&wc);

    HWND handle = CreateWindowExW(
        0, L"Test Window", L"Test Window", WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
        nullptr, nullptr, hinst, nullptr);

    ShowWindow(handle, true);

    hwnd = handle;
    ready = true;

    MSG msg{};
    while (GetMessage(&msg, nullptr, 0, 0) > 0) {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }
}

//return clsid of preview handler for the passed extension, throw if no suitable preview handler is installed
CLSID getShellExClsidForType(
    const std::wstring& extension,
    const GUID& interfaceClsid) {
    std::array<wchar_t, 39> interfaceClsidWstr;
    StringFromGUID2(
        interfaceClsid,
        interfaceClsidWstr.data(),
        static_cast<DWORD>(interfaceClsidWstr.size()));

    std::array<wchar_t, 39> extensionClsidWstr;
    DWORD extensionClsidWstrSize = static_cast<DWORD>(extensionClsidWstr.size());
    HRESULT res;
    res = AssocQueryStringW(
        ASSOCF_INIT_DEFAULTTOSTAR,
        ASSOCSTR_SHELLEXTENSION,
        extension.c_str(),
        interfaceClsidWstr.data(),
        extensionClsidWstr.data(),
        &extensionClsidWstrSize);
    if (res != S_OK) {
        throw "no preview handler found";
    };

    CLSID extensionClsid;
    IIDFromString(extensionClsidWstr.data(), &extensionClsid);

    std::wcout << L"Extension: " << extension << L" - Preview Handler CLSID: " << extensionClsidWstr.data() << std::endl;

    return(extensionClsid);
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        return 0;
    }

    //initialize as STA
    CoInitialize(nullptr);

    bool ready = false;
    HWND hwnd;
    //create and run message pump in different thread
    std::thread window_thread(WindowThreadProc, std::ref(ready), std::ref(hwnd));

    //wait for window to be ready
    while (!ready) {}

    //create preview handler, use first argument as path
    std::filesystem::path path(argv[1]);
    CLSID clsid = getShellExClsidForType(path.extension(), __uuidof(IPreviewHandler));
    IPreviewHandler *preview_handler;
    CoCreateInstance(
        clsid, 
        nullptr, 
        CLSCTX_LOCAL_SERVER, 
        __uuidof(IPreviewHandler), 
        reinterpret_cast<void**>(&preview_handler));

    IInitializeWithStream* init_with_stream;
    HRESULT res;
    //initialize previewhandler with stream or with file path
    IInitializeWithFile* init_with_file;
    res = preview_handler->QueryInterface(&init_with_file);
    if (res == S_OK) {
        init_with_file->Initialize(path.c_str(), STGM_READ);
        init_with_file->Release();
    }
    else {
        IInitializeWithStream* init_with_stream;
        res = preview_handler->QueryInterface(&init_with_stream);
        if (res == S_OK) {
            IStream* stream;
            SHCreateStreamOnFileEx(
                path.c_str(),
                STGM_READ | STGM_SHARE_DENY_WRITE,
                0, false, nullptr,
                &stream);
            init_with_stream->Initialize(stream, STGM_READ);
            stream->Release();
            init_with_stream->Release();
        }
        else {
            throw "neither InitializeWithFile nor InitializeWithStream supported";
        }
    }

    auto print_rect = [](RECT& rect) {
        std::wcout << L"Setting Rect to: (" << rect.left << L", " << rect.top << ", " << rect.right << ", " << rect.bottom << ")" << std::endl;
    };

    //initial rect
    RECT rect{ 0,0,500,500 };
    print_rect(rect);
    preview_handler->SetWindow(hwnd, &rect);
    preview_handler->DoPreview();
    preview_handler->SetRect(&rect);

    //new rect
    rect = { 200, 200, 800, 800 };
    print_rect(rect);
    preview_handler->SetRect(&rect);


    window_thread.join();

    preview_handler->Release();
    
    return(0);
}

Solution

  • Most developers probably only test in Explorer and if Explorer uses a child window to host the handler, these bugs go unnoticed in the preview handlers.

    Your best option is to do the same with your own application.

    Hosting shell extensions is known to be a hard task, some extensions even manage to mess up QueryInterface so Explorer has to check both the HRESULT and the pointer!