winapiregistrywindows-11shell-extensionsdroptarget

IDropTarget registration not working on Windows 11


I followed this article from Raymond Chen:

How do I accept files to be opened via IDropTarget instead of on the command line?

This article shows how to implement a drop target server which may be used to associate a file type with an application, without having to use the command-line to achieve that.

On my previous Windows 10 computer, all worked fine. However, on my current computer installed with Windows 11, nothing is working.

Obviously, there are important changes between Windows 10 and 11. Can someone explain to me what I should do to make the above demo work correctly on Windows 11? Or point me to a document showing the differences between the different versions of Windows, and what I should do to make my code compatible with them?

UPDATE on 10.08.2022

Since a reproducible example has been requested many times, below is my implementation of the Raymond Chen's example mentioned above, with which the issue occurs.

ImageVerb.cpp

// std
#include <string>
#include <sstream>

// classes
#include "ProcessReference.h"
#include "SimpleDropTarget.h"
#include "SimpleDropTargetFactory.h"

// windows
#include <windows.h>
#include <shlobj.h>
#include <shellapi.h>

/**
* Based on the following Raymond Chen article: https://devblogs.microsoft.com/oldnewthing/20100503-00/?p=14183
*
* IMPORTANT NOTE
* In order to put this application to work as expected, the following keys should be created and/or modified in the registry:
* - Computer\HKEY_CURRENT_USER\SOFTWARE\Classes\CLSID\{C4A3B129-FD6D-43EB-8880-6C32E5495ACD}\LocalServer32
*   => In the LocalServer32 key, set path to this exe in the Default value (e.g W:\Labo\__ImageVerb\x64\Debug\ImageVerb.exe)
* - Computer\HKEY_CURRENT_USER\SOFTWARE\Classes\heicfile\Shell\ImageVerbVerb\DropTarget
*   => In the ImageVerbVerb key, set the name to show in the Shell Explorer popup menu in the Default value
*   => Optionally add a new (empty) NeverDefault string value in this key to avoid that the item takes the first/Default position in the menus
*   => In the DropTarget key, add a new CLSID string value, and set the {C4A3B129-FD6D-43EB-8880-6C32E5495ACD} parameter in it
* - Computer\HKEY_CURRENT_USER\SOFTWARE\Classes\.heic
*   => In the .heic key, set heicfile in the Default value
*
* NOTE All the keys seems to propagate themselves through the registry (in the HKCR keys, ...)
*/

//------------------------------------------------------------------------------
#define WM_OPENFILES (WM_USER + 1)
//------------------------------------------------------------------------------
HWND                    g_hWnd = NULL;
SimpleDropTargetFactory g_SimpleFropTargetFactory;
//------------------------------------------------------------------------------
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
        case WM_CLOSE:
            ::PostQuitMessage(0);
            break;

        case WM_DESTROY:
            return 0;

        case WM_OPENFILES:
        {
            IDataObject* pDataObj = reinterpret_cast<IDataObject*>(lParam);

            FORMATETC fmte = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
            STGMEDIUM stgm;

            if (SUCCEEDED(pDataObj->GetData(&fmte, &stgm)))
            {
                HDROP hdrop  = reinterpret_cast<HDROP>(stgm.hGlobal);
                UINT  cFiles = ::DragQueryFile(hdrop, 0xFFFFFFFF, NULL, 0);

                for (UINT i = 0; i < cFiles; ++i)
                {
                    TCHAR szFile[MAX_PATH];
                    UINT  cch = ::DragQueryFile(hdrop, i, szFile, MAX_PATH);

                    if (cch > 0 && cch < MAX_PATH)
                    {
                        // get the window client rect
                        RECT clientRect;
                        ::GetClientRect(g_hWnd, &clientRect);

                        // get the window device context
                        HDC hDC = ::GetDC(g_hWnd);

                        ::SetBkMode(hDC, TRANSPARENT);
                        ::SetBkColor(hDC, 0x000000);
                        ::SetTextColor(hDC, 0xffffff);

                        ::DrawText(hDC, szFile, ::wcslen(szFile), &clientRect, DT_SINGLELINE | DT_CENTER | DT_BOTTOM);
                        ::ReleaseDC(g_hWnd, hDC);
                    }
                }

                ::ReleaseStgMedium(&stgm);
            }

            pDataObj->Release();
            break;
        }

        default:
            return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }

    return 0;
}
//------------------------------------------------------------------------------
void OpenFilesFromDataObject(IDataObject* pDataObj)
{
    pDataObj->AddRef();

    ::PostMessage(g_hWnd, WM_OPENFILES, 0, reinterpret_cast<LPARAM>(pDataObj));
}
//------------------------------------------------------------------------------
int APIENTRY wWinMain(_In_     HINSTANCE hInstance,
                      _In_opt_ HINSTANCE hPrevInstance,
                      _In_     LPWSTR    lpCmdLine,
                      _In_     int       nCmdShow)
{
    if (FAILED(::CoInitialize(NULL)))
        return -1;

    g_SimpleFropTargetFactory.Set_OnOpenFilesFromDataObject(OpenFilesFromDataObject);

    // in case we use COM
    HRESULT hrRegister;
    DWORD   dwRegisterCookie;
    MSG     msg;

    {
        // lock the Windows Explorer and other Shell objects to prevent their host process from closing prematurely
        ProcessReference ref;
        g_pProcRef = &ref;

        // register the drop target interface with OLE so other applications can connect to it
        hrRegister = ::CoRegisterClassObject(g_CLSID_DropTarget, &g_SimpleFropTargetFactory,
                CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &dwRegisterCookie);

        WNDCLASSEX wcex  = {0};
        BOOL       bQuit = FALSE;

        // register window class
        wcex.cbSize        = sizeof(WNDCLASSEX);
        wcex.style         = CS_OWNDC;
        wcex.lpfnWndProc   = WindowProc;
        wcex.cbClsExtra    = 0;
        wcex.cbWndExtra    = 0;
        wcex.hInstance     = hInstance;
        wcex.hIcon         = ::LoadIcon(nullptr, IDI_APPLICATION);
        wcex.hCursor       = ::LoadCursor(nullptr, IDC_ARROW);
        wcex.hbrBackground = (HBRUSH)::GetStockObject(BLACK_BRUSH);
        wcex.lpszMenuName  = nullptr;
        wcex.lpszClassName = L"ImageVerb";
        wcex.hIconSm       = ::LoadIcon(nullptr, IDI_APPLICATION);

        if (!RegisterClassEx(&wcex))
            return 0;

        // create main window
        g_hWnd = ::CreateWindowEx(0,
                                  L"ImageVerb",
                                  L"Image verb",
                                  WS_DLGFRAME | WS_CAPTION | WS_SYSMENU,
                                  CW_USEDEFAULT,
                                  CW_USEDEFAULT,
                                  800,
                                  650,
                                  nullptr,
                                  nullptr,
                                  hInstance,
                                  nullptr);

        ::ShowWindow(g_hWnd, nCmdShow);

        // get the window client rect
        RECT clientRect;
        ::GetClientRect(g_hWnd, &clientRect);

        // get the window device context
        HDC hDC = ::GetDC(g_hWnd);

        // please wait text background
        HBRUSH hBrush = ::CreateSolidBrush(RGB(20, 30, 43));
        ::FillRect(hDC, &clientRect, hBrush);
        ::DeleteObject(hBrush);

        ::SetBkMode(hDC, TRANSPARENT);
        ::SetBkColor(hDC, 0x000000);
        ::SetTextColor(hDC, 0xffffff);

        std::wostringstream sstr;

        // is the command line containing the COM magic command indicating that the app was launched as a server?
        if (::CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, lpCmdLine, -1, L"-Embedding", -1) != CSTR_EQUAL &&
            ::CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, lpCmdLine, -1, L"/Embedding", -1) != CSTR_EQUAL)
        {
            // no, process the command line normally
            if (!::wcslen(lpCmdLine))
                sstr << L"Run as normal process";
            else
                sstr << L"Cmd line: " << lpCmdLine;
        }
        else
            sstr << L"Run as local server";

        ::DrawText(hDC, sstr.str().c_str(), (int)sstr.str().length(), &clientRect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
        ::ReleaseDC(g_hWnd, hDC);

        // program main loop
        while (!bQuit)
        {
            // check for messages
            if (::PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
            {
                // handle or dispatch messages
                if (msg.message == WM_QUIT)
                    bQuit = TRUE;
                else
                {
                    ::TranslateMessage(&msg);
                    ::DispatchMessage(&msg);
                }
            }
            else
            {}
        }

        // destroy the window explicitly
        ::DestroyWindow(g_hWnd);
    }

    // release the Shell object lock
    g_pProcRef = nullptr;

    if (SUCCEEDED(hrRegister))
    {
        ::CoRevokeClassObject(dwRegisterCookie);
    }

    ::CoUninitialize();

    return (int)msg.wParam;
}
//------------------------------------------------------------------------------

ProcessReference.h

#pragma once

// windows
#ifndef UNICODE
    #define UNICODE
#endif
#ifndef _UNICODE
    #define _UNICODE
#endif
#include <windows.h>

/**
* Process reference interface, used to keep track of outstanding objects and locks
*/
class ProcessReference : public IUnknown
{
    public:
        ProcessReference();
        virtual ~ProcessReference();

        /**
        * Add a reference
        *@returns new reference count after add
        */
        STDMETHODIMP_(ULONG) AddRef();

        /**
        * Remove a reference
        *@returns new reference count after remove
        */
        STDMETHODIMP_(ULONG) Release();

        /**
        * Query a COM object for a pointer to its interface
        *@param riid - reference to the queried interface identifier (IID)
        *@param ppv - the queried interface pointer
        *@returns error or success code
        */
        STDMETHODIMP QueryInterface(REFIID riid, void** ppv);

    private:
        LONG  m_Ref    = 1;
        DWORD m_Thread = 0;
};

ProcessReference.cpp

#include "ProcessReference.h"

// windows
#include <Shlobj.h>
//------------------------------------------------------------------------------
ProcessReference::ProcessReference() :
    m_Thread(::GetCurrentThreadId())
{
    // lock hosted Shell extensions and other components (in particular Windows Explorer)
    // to prevent their host process from closing prematurely
    ::SHSetInstanceExplorer(this);
}
//------------------------------------------------------------------------------
ProcessReference::~ProcessReference()
{
    // release the hosted Shell extensions lock
    ::SHSetInstanceExplorer(NULL);

    Release();

    MSG msg;

    // process the remaining messages handled by the references to release
    while (m_Ref && ::GetMessage(&msg, NULL, 0, 0))
    {
        ::TranslateMessage(&msg);
        ::DispatchMessage(&msg);
    }
}
//------------------------------------------------------------------------------
STDMETHODIMP_(ULONG) ProcessReference::AddRef()
{
    return ::InterlockedIncrement(&m_Ref);
}
//------------------------------------------------------------------------------
STDMETHODIMP_(ULONG) ProcessReference::Release()
{
    const LONG ref = ::InterlockedDecrement(&m_Ref);

    if (!ref)
        ::PostThreadMessage(m_Thread, WM_NULL, 0, 0);

    return ref;
}
//------------------------------------------------------------------------------
STDMETHODIMP ProcessReference::QueryInterface(REFIID riid, void** ppv)
{
    if (riid == IID_IUnknown)
    {
        *ppv = static_cast<IUnknown*>(this);
        AddRef();
        return S_OK;
    }

    *ppv = NULL;
    return E_NOINTERFACE;
}
//------------------------------------------------------------------------------

SimpleDropTarget.h

#pragma once

// classes
#include "ProcessReference.h"

// windows
#include <oleidl.h>

//------------------------------------------------------------------------------
// process reference, used to prevent Windows Explorer instances from closing prematurely
extern ProcessReference* g_pProcRef;

// drop target component class identifier {C4A3B129-FD6D-43EB-8880-6C32E5495ACD}
// NOTE a new GUID should be regenerated every time this class is used in a new project. To do that (in VS),
// open Tools->Create GUID menu and select option nb. 3, then press Copy
const CLSID g_CLSID_DropTarget = {0xc4a3b129, 0xfd6d, 0x43eb, {0x88, 0x80, 0x6c, 0x32, 0xe5, 0x49, 0x5a, 0xcd}};
//------------------------------------------------------------------------------

/**
* Simple drop target, receives the open file events from the Shell
*/
class SimpleDropTarget : public IDropTarget
{
    public:
        /**
        * Called when files are opened from data object
        *@param pDataObj - data object containing the files info
        */
        typedef void (*ITfOnOpenFilesFromDataObject)(IDataObject* pDataObj);

        SimpleDropTarget();
        virtual ~SimpleDropTarget();

        /**
        * Add a reference
        *@returns new reference count after add
        */
        STDMETHODIMP_(ULONG) AddRef();

        /**
        * Remove a reference
        *@returns new reference count after remove
        */
        STDMETHODIMP_(ULONG) Release();

        /**
        * Query a COM object for a pointer to its interface
        *@param riid - reference to the queried interface identifier (IID)
        *@param ppv - the queried interface pointer
        *@returns error or success code
        */
        STDMETHODIMP QueryInterface(REFIID riid, void** ppv);

        /**
        * Called when a drag operation should be accepted
        *@param pDataObj - data object containing the drag info
        *@param grfKeyState - modifier keys state
        *@param ptl - current cursor position, in screen coordinates
        *@param pdwEffect - DoDragDrop pdwEffect function parameter, drag operation result on function ends
        *@returns error or success code
        */
        STDMETHODIMP DragEnter(IDataObject* pDataObj, DWORD grfKeyState, POINTL ptl, DWORD* pdwEffect);

        /**
        * Called when an object is dragged over a valid target
        *@param grfKeyState - modifier keys state
        *@param ptl - current cursor position, in screen coordinates
        *@param pdwEffect - DoDragDrop pdwEffect function parameter, drag operation result on function ends
        *@returns error or success code
        */
        STDMETHODIMP DragOver(DWORD grfKeyState, POINTL ptl, DWORD* pdwEffect);

        /**
        * Called when a drag target is leaved
        *@returns error or success code
        */
        STDMETHODIMP DragLeave();

        /**
        * Called when an object is dropped on the target
        *@param pDataObj - data object containing the drag info
        *@param grfKeyState - modifier keys state
        *@param ptl - current cursor position, in screen coordinates
        *@param pdwEffect - DoDragDrop pdwEffect function parameter, drop operation result on function ends
        *@returns error or success code
        */
        STDMETHODIMP Drop(IDataObject* pDataObj, DWORD grfKeyState, POINTL ptl, DWORD* pdwEffect);

        /**
        * Set the OnOpenFilesFromDataObject callback
        *@param hCallback - callback function handler
        */
        void Set_OnOpenFilesFromDataObject(ITfOnOpenFilesFromDataObject hCallback);

    private:
        LONG                         m_Ref                        = 1;
        ITfOnOpenFilesFromDataObject m_fOnOpenFilesFromDataObject = nullptr;
};

SimpleDropTarget.cpp

#include "SimpleDropTarget.h"

//------------------------------------------------------------------------------
ProcessReference* g_pProcRef = nullptr;
//------------------------------------------------------------------------------
SimpleDropTarget::SimpleDropTarget() :
    IDropTarget()
{
    g_pProcRef->AddRef();
}
//------------------------------------------------------------------------------
SimpleDropTarget::~SimpleDropTarget()
{
    g_pProcRef->Release();
}
//------------------------------------------------------------------------------
STDMETHODIMP_(ULONG) SimpleDropTarget::AddRef()
{
    return ::InterlockedIncrement(&m_Ref);
}
//------------------------------------------------------------------------------
STDMETHODIMP_(ULONG) SimpleDropTarget::Release()
{
    const LONG ref = ::InterlockedDecrement(&m_Ref);

    if (!ref)
        delete this;

    return ref;
}
//------------------------------------------------------------------------------
STDMETHODIMP SimpleDropTarget::QueryInterface(REFIID riid, void** ppv)
{
    if (riid == IID_IUnknown || riid == IID_IDropTarget)
    {
        *ppv = static_cast<IUnknown*>(this);
        AddRef();
        return S_OK;
    }

    *ppv = NULL;
    return E_NOINTERFACE;
}
//------------------------------------------------------------------------------
STDMETHODIMP SimpleDropTarget::DragEnter(IDataObject* pdto, DWORD grfKeyState, POINTL ptl, DWORD* pdwEffect)
{
    *pdwEffect &= DROPEFFECT_COPY;
    return S_OK;
}
//------------------------------------------------------------------------------
STDMETHODIMP SimpleDropTarget::DragOver(DWORD grfKeyState, POINTL ptl, DWORD* pdwEffect)
{
    *pdwEffect &= DROPEFFECT_COPY;
    return S_OK;
}
//------------------------------------------------------------------------------
STDMETHODIMP SimpleDropTarget::DragLeave()
{
    return S_OK;
}
//------------------------------------------------------------------------------
STDMETHODIMP SimpleDropTarget::Drop(IDataObject* pdto, DWORD grfKeyState, POINTL ptl, DWORD* pdwEffect)
{
    if (m_fOnOpenFilesFromDataObject)
        m_fOnOpenFilesFromDataObject(pdto);

    *pdwEffect &= DROPEFFECT_COPY;
    return S_OK;
}
//------------------------------------------------------------------------------
void SimpleDropTarget::Set_OnOpenFilesFromDataObject(ITfOnOpenFilesFromDataObject hCallback)
{
    m_fOnOpenFilesFromDataObject = hCallback;
}
//------------------------------------------------------------------------------

SimpleDropTargetFactory.h

#pragma once

// classes
#include "SimpleDropTarget.h"

// windows
#include <Unknwnbase.h>

/**
* Simple drop target factory
*/
class SimpleDropTargetFactory : public IClassFactory
{
    public:
        // SimpleDropTarget::OnOpenFilesFromDataObject alias
        typedef SimpleDropTarget::ITfOnOpenFilesFromDataObject ITfOnOpenFilesFromDataObject;

        SimpleDropTargetFactory();
        virtual ~SimpleDropTargetFactory();

        /**
        * Add a reference
        *@returns new reference count after add
        */
        STDMETHODIMP_(ULONG) AddRef();

        /**
        * Remove a reference
        *@returns new reference count after remove
        */
        STDMETHODIMP_(ULONG) Release();

        /**
        * Query a COM object for a pointer to its interface
        *@param riid - reference to the queried interface identifier (IID)
        *@param ppv - the queried interface pointer
        *@returns error or success code
        */
        STDMETHODIMP QueryInterface(REFIID riid, void** ppv);

        /**
        * Create an uninitialized object
        *@param pUnkOuter - pointer to the controlling IUnknown aggregate interface if being created as part of an aggregate, otherwise nullptr
        *@param riid - reference to interface identifier to be used to communicate with the newly created object
        *@param ppv - pointer that receives the interface requested in riid
        *@returns error or success code
        */
        STDMETHODIMP CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppv);

        /**
        * Lock an object application open in memory
        *@param fLock - If TRUE increment lock count, otherwise decrement lock count
        *@returns error or success code
        *@note This function enable instances to be created more quickly
        */
        STDMETHODIMP LockServer(BOOL fLock);

        /**
        * Set the OnOpenFilesFromDataObject callback
        *@param hCallback - callback function handler
        */
        void Set_OnOpenFilesFromDataObject(ITfOnOpenFilesFromDataObject hCallback);

    private:
        ITfOnOpenFilesFromDataObject m_fOnOpenFilesFromDataObject = nullptr;
};

SimpleDropTargetFactory.cpp

#include "SimpleDropTargetFactory.h"

// std
#include <new>

// classes
#include "SimpleDropTarget.h"

//------------------------------------------------------------------------------
SimpleDropTargetFactory::SimpleDropTargetFactory()
{}
//------------------------------------------------------------------------------
SimpleDropTargetFactory::~SimpleDropTargetFactory()
{}
//------------------------------------------------------------------------------
STDMETHODIMP_(ULONG) SimpleDropTargetFactory::AddRef()
{
    return 2;
}
//------------------------------------------------------------------------------
STDMETHODIMP_(ULONG) SimpleDropTargetFactory::Release()
{
    return 1;
}
//------------------------------------------------------------------------------
STDMETHODIMP SimpleDropTargetFactory::QueryInterface(REFIID riid, void** ppv)
{
    if (riid == IID_IUnknown || riid == IID_IClassFactory)
    {
        *ppv = static_cast<IUnknown*>(this);
        AddRef();
        return S_OK;
    }

    *ppv = NULL;
    return E_NOINTERFACE;
}
//------------------------------------------------------------------------------
STDMETHODIMP SimpleDropTargetFactory::CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppv)
{
    *ppv = NULL;

    if (pUnkOuter)
        return CLASS_E_NOAGGREGATION;

    SimpleDropTarget* pDropTarget = new(std::nothrow)SimpleDropTarget();

    if (!pDropTarget)
        return E_OUTOFMEMORY;

    pDropTarget->Set_OnOpenFilesFromDataObject(m_fOnOpenFilesFromDataObject);

    HRESULT hr = pDropTarget->QueryInterface(riid, ppv);
    pDropTarget->Release();

    return hr;
}
//------------------------------------------------------------------------------
STDMETHODIMP SimpleDropTargetFactory::LockServer(BOOL fLock)
{
    // server shutting down
    if (!g_pProcRef)
        return E_FAIL;

    if (fLock)
        g_pProcRef->AddRef();
    else
        g_pProcRef->Release();

    return S_OK;
}
//------------------------------------------------------------------------------
void SimpleDropTargetFactory::Set_OnOpenFilesFromDataObject(ITfOnOpenFilesFromDataObject hCallback)
{
    m_fOnOpenFilesFromDataObject = hCallback;
}
//------------------------------------------------------------------------------

Solution

  • So after many searches and headache, I found what was the issue.

    In fact, the applications associated by default in Windows 11 may override the keys and values for the file type you write in the registry.

    In my case I tried to associate my application with the .heic file type. On Windows 10, there was no association with this type by default. On Windows 11, the following key:

    HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts.heic\UserChoice

    was showing AppX43hnxtbyyps62jhe9sqpdzxn1790zetc in its ProgId value. This ID points to the Windows Photo Viewer application. When I replaced the default app for the .heic files by my own application, the ProgId value changed to heicfile, and my file association was working as expected.

    Thank to @SimonMourier to helped me to find out what was the issue.