I implemented a context menu handler, based on Microsoft's example here which uses IExploreCommand
.
The code displays a context menu, with submenus, just fine with the Windows 11 modern and classic context menu. So, there is no problem with my MSIX package or my AppxManifest.xml file.
The problem is that IShellItemArray
is empty when the user right-clicks on a directory background when using the classic context menu (Windows 10 or Windows 11 Shift-Right-Click). It works correctly when using the Windows 11 modern context menu.
When I make a context menu that has no submenus at all, then IShellItemArray contains the folder path with a directory background click regardless of whether it's the classic or modern context menu. It's only empty when the context menu has one or more submenus in the classic context menu. Also note that right-click on files and folders still populates IShellItemArray, as expected, whether there are submenus or not.
Here's a minimal example that demonstrates the issue:
#include "pch.h"
#include <wrl/module.h>
#include <shobjidl_core.h>
#include <wil/resource.h>
#include <vector>
using namespace Microsoft::WRL;
HMODULE g_hModule = nullptr;
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
g_hModule = hModule;
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
// If user right-clicks a file or folder, the selected item is in the array
// If user right-clicks the background, the array is empty
// Issue only occurs when using classic context menu
class SubCommand : public RuntimeClass<RuntimeClassFlags<ClassicCom>, IExplorerCommand>
{
public:
SubCommand(PCWSTR title, PCWSTR icon, PCWSTR command) :
m_title(title), m_icon(icon), m_command(command) {
}
IFACEMETHODIMP GetTitle(_In_opt_ IShellItemArray* items, _Outptr_result_nullonfailure_ PWSTR* name)
{
*name = nullptr;
auto title = wil::make_cotaskmem_string_nothrow(m_title);
RETURN_IF_NULL_ALLOC(title);
*name = title.release();
return S_OK;
}
IFACEMETHODIMP GetIcon(_In_opt_ IShellItemArray* items, _Outptr_result_nullonfailure_ PWSTR* iconPath)
{
*iconPath = nullptr;
return E_NOTIMPL;
}
IFACEMETHODIMP GetToolTip(_In_opt_ IShellItemArray*, _Outptr_result_nullonfailure_ PWSTR* infoTip) { *infoTip = nullptr; return E_NOTIMPL; }
IFACEMETHODIMP GetCanonicalName(_Out_ GUID* guidCommandName) { *guidCommandName = GUID_NULL; return S_OK; }
IFACEMETHODIMP GetState(_In_opt_ IShellItemArray* selection, _In_ BOOL okToBeSlow, _Out_ EXPCMDSTATE* cmdState)
{
*cmdState = ECS_ENABLED;
return S_OK;
}
IFACEMETHODIMP Invoke(_In_opt_ IShellItemArray* selection, _In_opt_ IBindCtx*) noexcept
{
try
{
DWORD count = 0;
if (selection)
{
RETURN_IF_FAILED(selection->GetCount(&count));
}
if (count > 0)
{
ComPtr<IShellItem> item;
RETURN_IF_FAILED(selection->GetItemAt(0, &item));
PWSTR filePath;
RETURN_IF_FAILED(item->GetDisplayName(SIGDN_FILESYSPATH, &filePath));
wil::unique_cotaskmem_string filePathCleanup(filePath);
WCHAR message[100];
StringCchPrintfW(message, ARRAYSIZE(message), L"First item: %s", filePath);
MessageBox(nullptr, message, L"Debug", MB_OK);
}
else
{
MessageBox(nullptr, L"No items selected", L"Debug", MB_OK);
}
return S_OK;
}
catch (...)
{
return E_FAIL;
}
}
IFACEMETHODIMP GetFlags(_Out_ EXPCMDFLAGS* flags) { *flags = ECF_DEFAULT; return S_OK; }
IFACEMETHODIMP EnumSubCommands(_COM_Outptr_ IEnumExplorerCommand** enumCommands) { *enumCommands = nullptr; return E_NOTIMPL; }
private:
PCWSTR m_title;
PCWSTR m_icon;
PCWSTR m_command;
};
class SubCommandEnum : public RuntimeClass<RuntimeClassFlags<ClassicCom>, IEnumExplorerCommand>
{
public:
SubCommandEnum()
{
m_commands.push_back(Make<SubCommand>(L"SubmenuTest", L"SubmenuTest.ico", L"SubmenuTest"));
}
IFACEMETHODIMP Next(ULONG celt, IExplorerCommand** pUICommand, ULONG* pceltFetched)
{
ULONG fetched = 0;
while (fetched < celt && m_index < m_commands.size())
{
pUICommand[fetched] = m_commands[m_index].Get();
pUICommand[fetched]->AddRef();
++fetched;
++m_index;
}
if (pceltFetched)
{
*pceltFetched = fetched;
}
return (fetched == celt) ? S_OK : S_FALSE;
}
IFACEMETHODIMP Skip(ULONG celt)
{
m_index += celt;
return (m_index < m_commands.size()) ? S_OK : S_FALSE;
}
IFACEMETHODIMP Reset()
{
m_index = 0;
return S_OK;
}
IFACEMETHODIMP Clone(IEnumExplorerCommand** ppenum)
{
auto clone = Make<SubCommandEnum>();
clone->m_index = m_index;
*ppenum = clone.Detach();
return S_OK;
}
private:
std::vector<ComPtr<IExplorerCommand>> m_commands;
size_t m_index = 0;
};
class __declspec(uuid("8A0E292E-E857-424B-9457-EC5377D0DCFD")) ContextMenuHandler : public RuntimeClass<RuntimeClassFlags<ClassicCom>, IExplorerCommand, IObjectWithSite>
{
public:
IFACEMETHODIMP GetTitle(_In_opt_ IShellItemArray* items, _Outptr_result_nullonfailure_ PWSTR* name)
{
*name = nullptr;
auto title = wil::make_cotaskmem_string_nothrow(L"ExplorerHelper");
RETURN_IF_NULL_ALLOC(title);
*name = title.release();
return S_OK;
}
IFACEMETHODIMP GetIcon(_In_opt_ IShellItemArray* items, _Outptr_result_nullonfailure_ PWSTR* iconPath)
{
*iconPath = nullptr;
return E_NOTIMPL;
}
IFACEMETHODIMP GetToolTip(_In_opt_ IShellItemArray*, _Outptr_result_nullonfailure_ PWSTR* infoTip) { *infoTip = nullptr; return E_NOTIMPL; }
IFACEMETHODIMP GetCanonicalName(_Out_ GUID* guidCommandName) { *guidCommandName = GUID_NULL; return S_OK; }
IFACEMETHODIMP GetState(_In_opt_ IShellItemArray* selection, _In_ BOOL okToBeSlow, _Out_ EXPCMDSTATE* cmdState)
{
*cmdState = ECS_ENABLED;
return S_OK;
}
IFACEMETHODIMP Invoke(_In_opt_ IShellItemArray* selection, _In_opt_ IBindCtx*) noexcept
{
// Any code added here no longer runs now that we have a submenu
return S_OK;
}
IFACEMETHODIMP GetFlags(_Out_ EXPCMDFLAGS* flags) { *flags = ECF_HASSUBCOMMANDS; return S_OK; }
IFACEMETHODIMP EnumSubCommands(_COM_Outptr_ IEnumExplorerCommand** enumCommands)
{
*enumCommands = Make<SubCommandEnum>().Detach();
return S_OK;
}
IFACEMETHODIMP SetSite(_In_ IUnknown* site) noexcept { m_site = site; return S_OK; }
IFACEMETHODIMP GetSite(_In_ REFIID riid, _COM_Outptr_ void** site) noexcept { return m_site.CopyTo(riid, site); }
protected:
ComPtr<IUnknown> m_site;
};
CoCreatableClass(ContextMenuHandler)
STDAPI DllGetActivationFactory(_In_ HSTRING activatableClassId, _COM_Outptr_ IActivationFactory** factory)
{
return Module<ModuleType::InProc>::GetModule().GetActivationFactory(activatableClassId, factory);
}
_Use_decl_annotations_
STDAPI DllCanUnloadNow(void)
{
return Module<InProc>::GetModule().GetObjectCount() == 0 ? S_OK : S_FALSE;
}
_Use_decl_annotations_
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** instance)
{
return Module<InProc>::GetModule().GetClassObject(rclsid, riid, instance);
}
Depending on various contexts (method being called, packaging, version of Windows, etc.), Windows may not always provide information about the current folder.
One way to get it is to implement IObjectWithSite in the COM object that implements IExplorerCommand, and use the passed-in site to determine what's the current folder, something like this:
IFACEMETHODIMP Invoke(_In_opt_ IShellItemArray* selection, _In_opt_ IBindCtx*) noexcept
{
try
{
ComPtr<IServiceProvider> sp;
m_site->QueryInterface(sp.GetAddressOf());
if (sp)
{
ComPtr<IShellBrowser> browser;
sp->QueryService(SID_STopLevelBrowser, browser.GetAddressOf());
if (browser)
{
ComPtr<IShellView> view;
browser->QueryActiveShellView(&view);
if (view)
{
ComPtr<IFolderView> fview;
view->QueryInterface(fview.GetAddressOf());
if (fview)
{
ComPtr<IShellItem> folder;
fview->GetFolder(IID_PPV_ARGS(folder.GetAddressOf()));
if (folder)
{
wil::unique_cotaskmem_string path;
// SIGDN_FILESYSPATH will give the path for file system folders
// here we use this SIGDN_DESKTOPABSOLUTEPARSING enum value
// as not all folders are file system folders
folder->GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, &path);
MessageBox(nullptr, path.get(), L"Debug", MB_OK);
}
}
}
}
}
return S_OK;
}
catch (...)
{
return E_FAIL;
}
}
Note: this also work in other IExplorerCommand methods such as GetState, etc.