c++windowswinapiwindows-shell

Display menu items that are directly retrieved from the shell extension


To display the menu I use the TrackPopupMenuEx function. The menu I want to display I get directly from the shell extension. This is the menu for the item "New" (HKEY_CLASSES_ROOT\Directory\Background\shellex\ContextMenuHandlers\New) which is displayed in the background context menu of the folder.

I was able to get the menu itself, but I can't display it correctly. When trying to display the menu, the following problems appear:

  1. If I directly pass hNewMenu (hNewMenu is HMENU obtained via QueryContextMenu) to TrackPopupMenuEx, then the item itself called "New" is not displayed, but its child elements are shown directly
  2. The menu items that are displayed do not work (Cannot be executed via InvokeCommand). However, they will work if you pass the menu to TrackPopupMenuEx as GetSubMenu(hNewMenu, 0) but in this case of course "New" should not be visible

I tried to create a new menu hRootMenu with a root item "New" where the items from GetSubMenu(hNewMenu, 0) could be placed as children. However, it did not work, the manually added parent item "New" is still not displayed and instead the submenus from "New" are displayed directly.

My question is, how can I display the item "New" with its children inside and so that the menu items can be executed via IContextMenu::InvokeCommand?

You can see my code below. To run this code and display the menu, you need to do the following:

  1. Replace the path variable value with the path to the any folder
  2. Press the Context menu button to display the context menu

#include <Windows.h>
#include <ShlObj.h>
#include <sstream>

HINSTANCE hInst;

IContextMenu2* cm2;
IContextMenu3* cm3;
const wchar_t* path = L"C:\\Users\\Username\\Desktop\\folder";

bool UpdateContextMenu(LPCONTEXTMENU icm1, void** ppContextMenu)
{
    *ppContextMenu = NULL;

    if (icm1)
    {    // since we got an IContextMenu interface we can 
        // now obtain the higher version interfaces via that
        if (SUCCEEDED(icm1->QueryInterface(IID_IContextMenu3, ppContextMenu))) {
            cm3 = (LPCONTEXTMENU3)*ppContextMenu;
        }
        else if (SUCCEEDED(icm1->QueryInterface(IID_IContextMenu2, ppContextMenu))) {
            cm2 = (LPCONTEXTMENU2)*ppContextMenu;
        }

        if (*ppContextMenu) {
            icm1->Release();     // we can now release version 1 interface, 
            // cause we got a higher one
        }
        else {
            *ppContextMenu = icm1;    // since no higher versions were found
        }  // redirect ppContextMenu to version 1 interface
    }
    else
        return false;    // something went wrong

    return true; // success
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {

    if (cm3) {
        LRESULT res;

        if (SUCCEEDED(cm3->HandleMenuMsg2(message, wParam, lParam, &res))) {
            return res;
        }
    }
    else if (cm2) {
        if (SUCCEEDED(cm2->HandleMenuMsg(message, wParam, lParam))) {
            return 0;
        }
    }


    switch (message) {

    case WM_CREATE: {
        CreateWindowA(
            "BUTTON", "Context menu",
            WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON,
            100, 100, 100, 30,
            hWnd, (HMENU)1, hInst, nullptr);
        break;
    }
    case WM_COMMAND: {

        if (LOWORD(wParam) == 1) {
            LPITEMIDLIST pidl;

            SFGAOF sfgao;
            if (SUCCEEDED(SHParseDisplayName(path, NULL, &pidl, 0, &sfgao))) {
                IShellFolder* psf;
                LPCITEMIDLIST pidlChild;

                if (SUCCEEDED(SHBindToParent(pidl, IID_IShellFolder, (void**)&psf, &pidlChild))) {
                    psf->Release();
                }
            }

            IContextMenu* outPcm = nullptr;

            CLSID clsid2;
            CLSIDFromString(L"{D969A300-E7FF-11d0-A93B-00A0C90F2719}", &clsid2); // CLSID_NewMenu

            const int SCRATCH_QCM_FIRST = 1;
            const int SCRATCH_QCM_LAST = 0x7FFF;

            IShellExtInit* shExtInit;
            if (SUCCEEDED(CoCreateInstance(clsid2, NULL, CLSCTX_INPROC_SERVER, IID_IShellExtInit, (LPVOID*)&shExtInit))) {
                if (SUCCEEDED(shExtInit->Initialize(pidl, NULL, NULL))) {
                    IContextMenu* cm = NULL;
                    if (SUCCEEDED(shExtInit->QueryInterface(IID_IContextMenu, (void**)&cm))) {
                        UpdateContextMenu(cm, (void**)&outPcm);
                        HMENU hNewMenu = CreatePopupMenu();
                        HMENU hRootMenu = CreatePopupMenu();

                        if (SUCCEEDED(outPcm->QueryContextMenu(hNewMenu, 0, SCRATCH_QCM_FIRST, SCRATCH_QCM_LAST, CMF_NORMAL))) {
                            AppendMenu(hRootMenu, MF_STRING | MF_POPUP, (UINT_PTR)GetSubMenu(hNewMenu, 0), L"New");

                            int idCmd = TrackPopupMenuEx(hRootMenu, TPM_RETURNCMD, 100, 100, (HWND)hWnd, NULL);
                            CMINVOKECOMMANDINFO cmi = { 0 };
                            cmi.cbSize = sizeof(CMINVOKECOMMANDINFO);
                            cmi.lpVerb = (LPSTR)MAKEINTRESOURCE(idCmd - SCRATCH_QCM_FIRST);
                            cmi.nShow = SW_SHOWNORMAL;
                            HRESULT hr = outPcm->InvokeCommand(&cmi);
                            if (!SUCCEEDED(hr)) {
                                std::stringstream ss;
                                ss << "GetLastError() = " << GetLastError() << " hr = " << hr;
                                MessageBoxA(NULL, ss.str().c_str(), "", 0);
                            }
                            outPcm->Release();
                            cm2 = nullptr;
                            cm3 = nullptr;
                        }
                    }
                }
            }


        }

        break;
    }

    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
    hInst = hInstance;

    WNDCLASSA wc = {};
    wc.lpfnWndProc = WndProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = "MYWINDOWCLASS";

    RegisterClassA(&wc);

    HWND hWnd = CreateWindowExA(
        0, "MYWINDOWCLASS", "Test", WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
        nullptr, nullptr, hInstance, nullptr
    );

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

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

    return msg.wParam;
}

Solution

  • For whatever reason, the implementation expects that the WM_INITMENUPOPUP message passed to HandleMenuMsg2 has the menu handle to the submenu it created in QueryContextMenu ([New >] [New]). I assume the menu provided by IShellFolder checks the menu item id so it knows which shell extension to forward the message to and the New menu is never the first menu item there so this issue does not happen in Explorer.

    CreateNewMenuInstance(&g_cm); // Initialize with pidl
    g_hmenu = CreatePopupMenu();
    g_cm->QueryContextMenu(g_hmenu, 0, SCRATCH_QCM_FIRST, SCRATCH_QCM_LAST, 0);
    int cmd = TrackPopupMenuEx(g_hmenu, ...);
    // if (cmd) ... InvokeCommand(...);
    g_cm->Release(), g_cm = nullptr;
    DestroyMenu(g_hmenu), g_hmenu = nullptr;
    
    ...
    
    // LRESULT CALLBACK WndProc(...)
    if (g_cm)
    {
      LRESULT res = 0;
      if (message != WM_INITMENUPOPUP || (WPARAM)g_hmenu != wParam)
      {
        if (SUCCEEDED(SHForwardContextMenuMsg(g_cm, message, wParam, lParam, &res, true)))
          return res;
      }
    }
    ...
    return DefWindowProc(hWnd, message, wParam, lParam);
    

    Screen shot

    This workaround also fixes InvokeCommand but the implementation expects you to provide the IContextMenu with a site that implements IShellView to get the full rename and shortcut wizard features.

    I don't feel great about this hack, using the real IContextMenu from IShellFolder is much better if you want the full background menu.