c++windowswinapicomwindows-shell

Cannot register IExecuteCommand for context menu


I am trying to register IExecuteCommand for the context menu of a text file. To do this, I created a COM server in a DLL and registered it in Windows. For registration, I used the following .reg file

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\CLSID\{0DF1F277-A2CD-4202-86AA-4C0BF1B8C5E4}\InProcServer32]
@="C:\Users\Username\OneDrive\Desktop\COM\COMIExecuteCommand.dll"
"ThreadingModel"="Apartment"

[HKEY_CLASSES_ROOT\txtfile\shell\printnamestodebugger]
@="Print names to debugger"
[HKEY_CLASSES_ROOT\txtfile\shell\printnamestodebugger\command]
"DelegateExecute"="{0DF1F277-A2CD-4202-86AA-4C0BF1B8C5E4}"

As a result, the "Print names to debugger" menu item appears in the context menu for text files, but when I try to select this menu, I get the following error: "Class not registered" despite the fact that all the necessary instructions for registering the COM component are written in the .reg file.

I am testing on Windows 10 64 bit (DLL also 64 bit)

How can I make the "Print names to debugger" menu work? You can see the COM component code below:


#include "pch.h"

#include <windows.h>
#include <unknwn.h>
#include <new>

#include <shobjidl.h>
CLSID CLSID_ShellExtension = { 0xdf1f277, 0xa2cd, 0x4202, { 0x86, 0xaa, 0x4c, 0xb, 0xf1, 0xb8, 0xc5, 0xe4 } };// {0DF1F277-A2CD-4202-86AA-4C0BF1B8C5E4}
LONG g_cObjs;
void DllAddRef() { InterlockedIncrement(&g_cObjs); }
void DllRelease() { InterlockedDecrement(&g_cObjs); }

class CShellExtension
    : public IExecuteCommand
    , public IInitializeCommand
    , public IObjectWithSelection
{
public:
    CShellExtension();
    // *** IUnknown ***
    STDMETHODIMP QueryInterface(REFIID riid, void** ppv);
    STDMETHODIMP_(ULONG) AddRef();
    STDMETHODIMP_(ULONG) Release();
    // *** IInitializeCommand ***
    STDMETHODIMP Initialize(PCWSTR pszCommandName, IPropertyBag* ppb);
    // *** IObjectWithSelection ***
    STDMETHODIMP SetSelection(IShellItemArray* psia);
    STDMETHODIMP GetSelection(REFIID riid, void** ppv);
    // *** IExecuteCommand ***
    STDMETHODIMP SetKeyState(DWORD grfKeyState) { return S_OK; }
    STDMETHODIMP SetParameters(LPCWSTR pszParameters) { return S_OK; }
    STDMETHODIMP SetPosition(POINT pt) { return S_OK; }
    STDMETHODIMP SetShowWindow(int nShow) { return S_OK; }
    STDMETHODIMP SetNoShowUI(BOOL fNoShowUI) { return S_OK; }
    STDMETHODIMP SetDirectory(LPCWSTR pszDirectory) { return S_OK; }
    STDMETHODIMP Execute();
private:
    ~CShellExtension();
private:
    LONG m_cRef;
    IShellItemArray* m_psia;
};
CShellExtension::CShellExtension()
    : m_cRef(1), m_psia(NULL)
{
    DllAddRef();
}
CShellExtension::~CShellExtension()
{
    if (m_psia) m_psia->Release();
    DllRelease();
}


// guts of shell extension go in here eventually
class CFactory : public IClassFactory
{
public:
    // *** IUnknown ***
    STDMETHODIMP QueryInterface(REFIID riid, void** ppv);
    STDMETHODIMP_(ULONG) AddRef() { return 2; }
    STDMETHODIMP_(ULONG) Release() { return 1; }
    // *** IClassFactory ***
    STDMETHODIMP CreateInstance(IUnknown* punkOuter,
        REFIID riid, void** ppv);
    STDMETHODIMP LockServer(BOOL fLock);
};
CFactory c_Factory;
STDMETHODIMP CFactory::QueryInterface(REFIID riid, void** ppv)
{
    IUnknown* punk = NULL;
    if (riid == IID_IUnknown || riid == IID_IClassFactory) {
        punk = static_cast<IClassFactory*>(this);
    }
    *ppv = punk;
    if (punk) {
        punk->AddRef();
        return S_OK;
    }
    else {
        return E_NOINTERFACE;
    }
}
STDMETHODIMP CFactory::CreateInstance(
    IUnknown* punkOuter, REFIID riid, void** ppv)
{
    *ppv = NULL;
    if (punkOuter) return CLASS_E_NOAGGREGATION;
    CShellExtension* pse = new(std::nothrow) CShellExtension();
    if (!pse) return E_OUTOFMEMORY;
    HRESULT hr = pse->QueryInterface(riid, ppv);
    pse->Release();
    return hr;
}
STDMETHODIMP CFactory::LockServer(BOOL fLock)
{
    if (fLock) DllAddRef();
    else       DllRelease();
    return S_OK;
}

STDAPI DllGetClassObject(REFCLSID rclsid,
    REFIID riid, void** ppv)
{
    if (rclsid == CLSID_ShellExtension) {
        return c_Factory.QueryInterface(riid, ppv);
    }
    *ppv = NULL;
    return CLASS_E_CLASSNOTAVAILABLE;
}

 STDAPI Test(REFCLSID rclsid,
     REFIID riid, void** ppv)
 {
     
     return CLASS_E_CLASSNOTAVAILABLE;
 }

STDAPI DllCanUnloadNow()
{
    return g_cObjs ? S_OK : S_FALSE;
}

STDMETHODIMP CShellExtension::QueryInterface(
    REFIID riid, void** ppv)
{
    IUnknown* punk = NULL;
    if (riid == IID_IUnknown || riid == IID_IExecuteCommand) {
        punk = static_cast<IExecuteCommand*>(this);
    }
    else if (riid == IID_IInitializeCommand) {
        punk = static_cast<IInitializeCommand*>(this);
    }
    else if (riid == IID_IObjectWithSelection) {
        punk = static_cast<IObjectWithSelection*>(this);
    }
    *ppv = punk;
    if (punk) {
        punk->AddRef();
        return S_OK;
    }
    else {
        return E_NOINTERFACE;
    }
}

STDMETHODIMP CShellExtension::SetSelection(IShellItemArray* psia)
{
    if (psia) psia->AddRef();
    if (m_psia) m_psia->Release();
    m_psia = psia;
    return S_OK;
}
STDMETHODIMP CShellExtension::GetSelection(
    REFIID riid, void** ppv)
{
    if (m_psia) return m_psia->QueryInterface(riid, ppv);
    *ppv = NULL;
    return E_NOINTERFACE;
}

STDMETHODIMP CShellExtension::Initialize(
    PCWSTR pszCommandName,
    IPropertyBag* ppb)
{
    OutputDebugStringW(L"Command: ");
    OutputDebugStringW(pszCommandName);
    OutputDebugStringW(L"\r\n");
    if (ppb) {
        VARIANT vt;
        VariantInit(&vt);
        if (SUCCEEDED(ppb->Read(L"extra", &vt, NULL))) {
            if (SUCCEEDED(VariantChangeType(&vt, &vt, 0, VT_BSTR))) {
                OutputDebugStringW(L"extra: ");
                OutputDebugStringW(vt.bstrVal);
                OutputDebugStringW(L"\r\n");
            }
            VariantClear(&vt);
        }
    }
    return S_OK;
}

STDMETHODIMP CShellExtension::Execute()
{
    HRESULT hr;
    if (m_psia) {
        IEnumShellItems* pesi;
        if (SUCCEEDED(hr = m_psia->EnumItems(&pesi))) {
            IShellItem* psi;
            while (pesi->Next(1, &psi, NULL) == S_OK) {
                LPWSTR pszName;
                if (SUCCEEDED(psi->GetDisplayName(SIGDN_FILESYSPATH,
                    &pszName))) {
                    OutputDebugStringW(L"File: ");
                    OutputDebugStringW(pszName);
                    OutputDebugStringW(L"\r\n");
                    CoTaskMemFree(pszName);
                }
                psi->Release();
            }
            pesi->Release();
            hr = S_OK;
        }
    }
    else {
        hr = E_UNEXPECTED;
    }
    return hr;
}

STDMETHODIMP_(ULONG) CShellExtension::AddRef()
{
    return ++m_cRef;
}
STDMETHODIMP_(ULONG) CShellExtension::Release()
{
    ULONG cRef = --m_cRef;
    if (cRef == 0) delete this;
    return cRef;
}

HRESULT __stdcall DllRegisterServer()
{
    return S_OK;
}

HRESULT __stdcall DllUnregisterServer()
{
    return S_OK;
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

DEF file


LIBRARY COMIExecuteCommand
EXPORTS
    DllGetClassObject PRIVATE
    DllCanUnloadNow PRIVATE
    DllRegisterServer PRIVATE
    DllUnregisterServer PRIVATE

Solution

  • The .reg file as displayed here is invalid, the \ character in COM server's path must be escaped, so it should be something like this instead:

    Windows Registry Editor Version 5.00
    
    [HKEY_CLASSES_ROOT\CLSID\{0DF1F277-A2CD-4202-86AA-4C0BF1B8C5E4}\InProcServer32]
    @="C:\\Users\\Username\\OneDrive\\Desktop\\COM\\COMIExecuteCommand.dll"
    "ThreadingModel"="Apartment"
    
    [HKEY_CLASSES_ROOT\txtfile\shell\printnamestodebugger]
    @="Print names to debugger"
    [HKEY_CLASSES_ROOT\txtfile\shell\printnamestodebugger\command]
    "DelegateExecute"="{0DF1F277-A2CD-4202-86AA-4C0BF1B8C5E4}"
    

    As a side note, when running on Windows 11, the association path using the txtfile key will probably won't work as is, but this could be used instead (in the case of .txt files only):

    HKEY_CLASSES_ROOT\SystemFileAssociations\text\shell\printnamestodebugger