Search code examples
c++windowswinapiwindows-shell

IContextMenu::QueryContextMenu returns incorrect menu


I create a context menu by calling IContextMenu::QueryContextMenu.

Everything is fine except for two things and both of these problems occur when displaying the context menu for the admin folder, which in my case is the current user.

Assuming the admin username is "Admin", then the problem occurs when trying to display the context menu for the C:\Users\Admin folder, namely the following problems:

  1. Missing "Properties" menu (Why? If you check the context menu via Explorer, then the "Properties" item is for this folder, but QueryContextMenu does not return it)

  2. Failed to run "Restore previous versions" menu item (When you try to click on this item, an error occurs. MessageBox shows "Unspecified error", but this menu works when selected via "Explorer") and GetLastError() returns 1223 error code ("The operation was canceled by the user": but I am not cancelling anything)

  3. "Share" menu does not work when filePath points to file

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

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

#include <windows.h>
#include <shlobj.h>
#include <string>
#include <sstream>
#include <iomanip>

HINSTANCE hInst;
LPCSTR szTitle = "WinAPI";
LPCSTR szWindowClass = "MYWINDOWCLASS";
const wchar_t* filePath = L"C:\\Users\\Admin";

class ContextMenu
{
public:
    IContextMenu2* getIContextMenu2()
    {
        return m_contextMenu2;
    }
    IContextMenu3* getIContextMenu3()
    {
        return m_contextMenu3;
    }

    void showContextMenu(const std::wstring& path, HWND hwnd, UINT xPos, UINT yPos)
    {
        IContextMenu* pcm = nullptr;
        IContextMenu* outPcm = nullptr;

        if (SUCCEEDED(GetUIObjectOfFile(hwnd, path.c_str(), IID_IContextMenu, (void**)&pcm))) {

            if (GetContextMenu(pcm, (void**)&outPcm)) {

                HMENU hmenu = CreatePopupMenu();

                if (hmenu) {

                    if (SUCCEEDED(outPcm->QueryContextMenu(hmenu, 0, SCRATCH_QCM_FIRST, SCRATCH_QCM_LAST, CMF_CANRENAME))) {
                        int idCmd = TrackPopupMenuEx(hmenu, TPM_RETURNCMD, xPos, yPos, (HWND)hwnd, NULL);
                        InvokeCommand(outPcm, idCmd);

                        if (m_contextMenu2) {
                            m_contextMenu2 = nullptr;
                        }

                        if (m_contextMenu3) {
                            m_contextMenu3 = nullptr;
                        }

                        outPcm->Release();
                    }
                }
            }
        }
    }

    void showFolderBackgroundContextMenu(const std::wstring& path, HWND hwnd, UINT xPos, UINT yPos)
    {
        IContextMenu* pcm = nullptr;
        IContextMenu* outPcm = nullptr;

        if (SUCCEEDED(GetUIObjectOfFolder(hwnd, path.c_str(), IID_IContextMenu, (void**)&pcm))) {

            if (GetContextMenu(pcm, (void**)&outPcm)) {

                HMENU hmenu = CreatePopupMenu();

                if (hmenu) {

                    if (SUCCEEDED(outPcm->QueryContextMenu(hmenu, 0, SCRATCH_QCM_FIRST, SCRATCH_QCM_LAST, CMF_NORMAL))) {
                        int idCmd = TrackPopupMenuEx(hmenu, TPM_RETURNCMD, xPos, yPos, (HWND)hwnd, NULL);


                        if (m_contextMenu2) {
                            m_contextMenu2 = nullptr;
                        }

                        if (m_contextMenu3) {
                            m_contextMenu3 = nullptr;
                        }
                    }
                }

                outPcm->Release();
            }
        }
    }

private:
    const int SCRATCH_QCM_FIRST = 1;
    const int SCRATCH_QCM_LAST = 0x7FFF;
    IContextMenu2* m_contextMenu2 = nullptr;
    IContextMenu3* m_contextMenu3 = nullptr;

    HRESULT GetUIObjectOfFile(HWND hwnd, LPCWSTR pszPath, REFIID riid, void** ppv)
    {
        *ppv = NULL;
        HRESULT hr;
        LPITEMIDLIST pidl;
        SFGAOF sfgao;
        if (SUCCEEDED(hr = SHParseDisplayName(pszPath, NULL, &pidl, 0, &sfgao))) {
            IShellFolder* psf;
            LPCITEMIDLIST pidlChild;
            if (SUCCEEDED(hr = SHBindToParent(pidl, IID_IShellFolder, (void**)&psf, &pidlChild))) {
                hr = psf->GetUIObjectOf(hwnd, 1, &pidlChild, riid, NULL, ppv);
                psf->Release();
            }
            CoTaskMemFree(pidl);
        }

        return hr;
    }

    HRESULT GetUIObjectOfFolder(HWND hwnd, LPCWSTR pszPath, REFIID riid, void** ppv)
    {
        *ppv = NULL;
        HRESULT hr;
        LPITEMIDLIST pidl;
        SFGAOF sfgao;
        if (SUCCEEDED(hr = SHParseDisplayName(pszPath, NULL, &pidl, 0, &sfgao))) {
            IShellFolder2* psf;
            LPCITEMIDLIST pidlChild;
            if (SUCCEEDED(hr = SHBindToParent(pidl, IID_IShellFolder2, (void**)&psf, &pidlChild))) {
                hr = psf->CreateViewObject(hwnd, riid, ppv);
                psf->Release();
            }
            CoTaskMemFree(pidl);
        }

        return hr;
    }

    void InvokeCommand(IContextMenu* pContextMenu, UINT idCommand)
    {
        CMINVOKECOMMANDINFO cmi = { 0 };
        cmi.cbSize = sizeof(CMINVOKECOMMANDINFO);
        cmi.lpVerb = (LPSTR)MAKEINTRESOURCE(idCommand - 1);
        cmi.nShow = SW_SHOWNORMAL;

        if (!SUCCEEDED(pContextMenu->InvokeCommand(&cmi))) {

            std::stringstream ss;
            ss << "Last error: " << GetLastError();
            std::string report = ss.str();

            MessageBoxA(NULL, report.c_str(), report.c_str(), 0);
        }
    }

    BOOL GetContextMenu(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))) {
                m_contextMenu3 = (LPCONTEXTMENU3)*ppContextMenu;
            }
            else if (SUCCEEDED(icm1->QueryInterface(IID_IContextMenu2, ppContextMenu))) {
                m_contextMenu2 = (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
    }

};

ContextMenu contextMenu;

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    HMODULE hmod = LoadLibraryW(L"shell32.dll");
    BOOL(WINAPI * FileIconInit)(_In_ BOOL fRestoreCache);
    FileIconInit = (BOOL(WINAPI*)(BOOL)) GetProcAddress(hmod, MAKEINTRESOURCEA(660));
    if (FileIconInit) {
        FileIconInit(TRUE);
    }

    hInst = hInstance;

    WNDCLASSA wc = {};
    wc.lpfnWndProc = WndProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = szWindowClass;

    RegisterClassA(&wc);

    HWND hWnd = CreateWindowExA(
        0, szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
        nullptr, nullptr, hInstance, nullptr
    );

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

    //SHELLEXECUTEINFO shellInfo;
    //ZeroMemory(&shellInfo, sizeof(SHELLEXECUTEINFO));
    //shellInfo.cbSize = sizeof(SHELLEXECUTEINFO);
    //shellInfo.fMask = SEE_MASK_INVOKEIDLIST;
    //shellInfo.hwnd = hWnd;
    //shellInfo.lpVerb = L"PreviousVersions";
    //shellInfo.lpFile = filePath;

    //ShellExecuteEx(&shellInfo);

    //std::stringstream ss;
    //ss << "Last error: " << GetLastError();
    //std::string report = ss.str();
    
    //MessageBoxA(hWnd, report.c_str(), report.c_str(), 0);


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

    return msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
    IContextMenu2* cm2 = contextMenu.getIContextMenu2();
    IContextMenu3* cm3 = contextMenu.getIContextMenu3();

    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) {
            SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL);
            contextMenu.showContextMenu(filePath, hWnd, 100, 100);
        }
        break;
    }

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

Unspecified error screenshot:

enter image description here

Missing "Properties" menu item screenshot

enter image description here


Solution

  • The problem is SHParseDisplayName is based on IShellFolder::ParseDisplayName. The doc says this:

    Some Shell folders may not implement IShellFolder::ParseDisplayName. Each folder that does will define its own parsing syntax.

    PS: this is true for all functions based on this: SHParseDisplayName, SHCreateItemFromParsingName, etc.

    In fact this is what happens, the Shell itself implements it using some magic incantations that are not documented, and it happens that for some reason that the "c:\users<currentuser>" string is parsed by the shell in a special way and does not returns the file system IShellFolder folder (clsid f3364ba0-65b9-11ce-a9ba-00aa004ae837 aka ShellFSFolder), but another IShellFolder that's not exactly the same (clsid 59031A47-3F72-44A7-89C5-5595FE6B30EE aka CLSID_UsersFilesFolder, which is undocumented).

    For some obscure reason this "CLSID_UsersFilesFolder" folder doesn't have a "Properties" menu item. Since this is missing, the "Restore previous versions" menu item doesn't work either because it's based internally on the existence of the "Properties" menu item (it calls ShellExecute withe the "properties" verb on this folder).

    One solution to fix that is to pass a non null IBindCtx reference to the call, so the shell will stop doing it's magic, and the proper IShellFolder implementation will be returned.

    As for the not working "Share" menu item, you can fix it if you pass a COM site implementing IOleWindow to the context menu implementation (through context menu's IObjectWithSite implementation).

    Here is a minimal example that demonstrates all this:

    class Site : public IServiceProvider, public IOleWindow
    {
        HWND _hwnd;
    
    public:
        Site(HWND hwnd) :_hwnd(hwnd) {}
    
        // IUnknown
        ULONG AddRef() { return 1; }
        ULONG Release() { return 1; }
        HRESULT QueryInterface(REFIID riid, void** ppvObject)
        {
            if (riid == IID_IUnknown || riid == IID_IServiceProvider)
            {
                *ppvObject = static_cast<IServiceProvider*>(this);
                return S_OK;
            }
    
            if (riid == IID_IOleWindow)
            {
                *ppvObject = static_cast<IOleWindow*>(this);
                return S_OK;
            }
            return E_NOINTERFACE;
        }
    
        // IServiceProvider, defer services calls to this QI
        HRESULT QueryService(REFGUID guidService, REFIID riid, void** ppvObject) { return QueryInterface(riid, ppvObject); }
    
        // IOleWindow
        HRESULT GetWindow(HWND* phwnd) { *phwnd = _hwnd; return S_OK; }
        HRESULT ContextSensitiveHelp(BOOL fEnterMode) { return E_NOTIMPL; }
    };
    
    LPCWSTR path = L"c:\\users\\currentuser";
    //LPCWSTR path = L"c:\\temp\\test.txt"; // file sample for "share" test
    
    LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
        const int buttonId = 1;
        const int buttonX = 10;
        const int buttonY = 10;
        switch (message)
        {
        case WM_CREATE:
            CreateWindow(L"BUTTON", L"Show Context menu", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON, buttonX, buttonY, 200, 30, hwnd, (HMENU)(long)buttonId, nullptr, nullptr);
            break;
    
        case WM_COMMAND:
            if (LOWORD(wParam) == buttonId)
            {
                IShellItem* item;
                IBindCtx* ctx;
                CreateBindCtx(0, &ctx); // use a binding context to avoid the shell to be too smart and get back wrong items
                SHCreateItemFromParsingName(path, ctx, IID_PPV_ARGS(&item));
                ctx->Release();
                if (item)
                {
                    IContextMenu* cm;
                    item->BindToHandler(nullptr, BHID_SFUIObject, IID_PPV_ARGS(&cm));
                    if (cm)
                    {
                        // pass site to enable some menu items properly like "share"
                        Site site{ hwnd };
                        IObjectWithSite* ows;
                        cm->QueryInterface(&ows);
                        if (ows)
                        {
                            ows->SetSite(static_cast<IServiceProvider*>(&site));
                        }
    
                        auto menu = CreatePopupMenu();
                        const int firstId = 1;
                        cm->QueryContextMenu(menu, 0, firstId, 0x7FFF, CMF_EXPLORE | CMF_CANRENAME | CMF_EXTENDEDVERBS); // remove some flags if not needed
                        POINT pt{ buttonX, buttonY };
                        ClientToScreen(hwnd, &pt);
                        auto cmd = TrackPopupMenu(menu, TPM_RETURNCMD, pt.x, pt.y, 0, hwnd, nullptr);
                        if (cmd)
                        {
                            CMINVOKECOMMANDINFO cmi{};
                            cmi.cbSize = sizeof(CMINVOKECOMMANDINFO);
                            cmi.lpVerb = (LPSTR)MAKEINTRESOURCE(cmd - firstId);
                            cmi.nShow = SW_SHOWNORMAL;
                            cm->InvokeCommand(&cmi);
                        }
    
                        if (ows)
                        {
                            ows->SetSite(nullptr);
                        }
    
                        cm->Release();
                        DestroyMenu(menu);
                    }
                    item->Release();
                }
            }
            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)
    {
        auto hmod = LoadLibrary(L"shell32.dll");
        auto fileIconInit = (BOOL(WINAPI*)(BOOL))GetProcAddress(hmod, MAKEINTRESOURCEA(660));
        if (fileIconInit)
        {
            fileIconInit(TRUE);
        }
    
        WNDCLASS wc = {};
        wc.lpfnWndProc = WndProc;
        wc.hInstance = hInstance;
        wc.lpszClassName = L"MyWindowClass";
        RegisterClass(&wc);
    
        auto hwnd = CreateWindow(wc.lpszClassName, L"Context Menu", 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 0;
    }