The IExecuteCommand
interface is a simpler form of
context menu extension which takes care of the annoying parts of
IContextMenu
so you can focus on your area of expertise,
namely, doing the actual thing the user selected,
and leave the shell to doing the grunt work of managing the UI part.
I’ve never needed a scratch shell extension before, so I guess it’s time to create one. This part is completely boring, and those of you who have written COM inproc servers can skip over it.
#include <windows.h> #include <new> LONG g_cObjs; void DllAddRef() { InterlockedIncrement(&g_cObjs); } void DllRelease() { InterlockedDecrement(&g_cObjs); } // 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 DllCanUnloadNow() { return g_cObjs ? S_OK : S_FALSE; }
I’m assuming that the above code is all old hat. Consider it a prerequisite.
Okay, now the good stuff.
The IExecuteCommand
interface is used when you
create a static registration for a shell verb but
specify DelegateExecute
in the command.
Our sample shell extension will be active on text files,
and all it’ll do is print the file names to the debugger.
Since we’re a COM server, we need to register our CLSID. This should also be very familiar to you.
[HKEY_CLASSES_ROOT\CLSID\{guid}\InProcServer32] @="C:\path\to\scratch.dll" "ThreadingModel"="Apartment"
Here’s where we register our object as a verb for text files,
specifying that it should be invoked via DelegateExecute
:
[HKEY_CLASSES_ROOT\txtfile\shell\printnamestodebugger] @="Print names to debugger" [HKEY_CLASSES_ROOT\txtfile\shell\printnamestodebugger\command] "DelegateExecute"="{guid}"
That was the easy part. Now to roll up our sleeves and write the shell extension.
#include <shobjidl.h> CLSID CLSID_ShellExtension = { ...guid... }; 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(); }
I’ve written this all out longhand; I’m trusting that you’re
using some sort of framework (like, say, ATL) which avoids all
this tedium, but since different people may choose different frameworks,
I won’t choose a framework here.
Instead, we just have the boring IUnknown
methods.
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_(ULONG) CShellExtension::AddRef() { return ++m_cRef; } STDMETHODIMP_(ULONG) CShellExtension::Release() { ULONG cRef = --m_cRef; if (cRef == 0) delete this; return cRef; }
Whew.
Up until now, it’s just been boring typing that you have to do
for any shell extension.
Finally we can start doing something interesting.
Windows 7 will initialize your shell extension with information
about the command being executed.
For this particular shell extension, we’ll just print the command
name to the debugger to prove that something happened.
(In real life, you might use the same CShellExtension
to handle multiple commands, and this lets you determine which
command you’re being asked to execute.)
STDMETHODIMP CShellExtension::Initialize( PCWSTR pszCommandName, IPropertyBag *ppb) { OutputDebugStringW(L"Command: "); OutputDebugStringW(pszCommandName); OutputDebugStringW(L"\r\n"); return S_OK; }
The shell will give you the items on which to execute in the form
of an IShellItemArray
:
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; }
The shell will then call a bunch of IExecuteCommand::SetThis
and
IExecuteCommand::SetThat
methods to inform you of the
environment in which you have been asked to execute.
We just ignored them all for simplicity, but in practice,
you may want to pay attention to some of them, particularly
IExecuteCommand::SetPosition
,
IExecuteCommand::SetShowWindow
, and
IExecuteCommand::SetNoShowUI
.
After all the IExecuteCommand::SetXxx
methods have been called,
it’s show time:
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; }
All we do is enumerate the contents of the IShellItemArray
and print their file names (if they have one).
Instead of IEnumShellItems
, you can use
IShellItemArray::GetCount
and
IShellItemArray::GetItemAt
.
Or, if you are porting an existing context menu that uses
IDataObject
,
you can call IShellItemArray::BindToHandler(BHID_DataObject)
to turn your IShellItemArray
into an IDataObject
.
Install this shell extension, right-click on a text file (or
a bunch of text files), and select
Print names to debugger.
If all goes well, the debugger will report
Command: printnamestodebugger
followed by paths of the files you selected.
But wait, there’s more.
The IPropertyBag
passed to
IInitializeCommand::Initialize
contains additional configuration options taken from the
registry.
You can use this to customize the behavior of the shell extension further.
Put the bonus information under the command key
like this:
[HKEY_CLASSES_ROOT\txtfile\shell\printnamestodebugger] "extra"="Special"
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; }
This updated version of CShellExtension
looks for
that registry value extra
we set above
and if found prints its value to the debugger.
Okay, so it looks like a lot of typing, but most of that was
typing you have to do for any shell extension.
The part that is specific to IExecuteCommand
is not
that bad,
and it certainly avoids having to mess with
IContextMenu::QueryContextMenu
and the fifty bajillion variations on
IContextMenu::InvokeCommand
.
Furthermore,
the shell doesn’t even load your IExecuteCommand
handler
until the user selects your command,
so switching to a static registration also gives the system a bit of a
performance boost.
Bonus tip:
You can combine the IExecuteCommand
technique with
Getting Dynamic Behavior for Static Verbs by Using Advanced Query Syntax
and
Using Item Attributes
to specify the conditions under which you want your verb to appear
without having to write a single line of C++ code.
Choosing a Static or Dynamic Shortcut Menu Method
provides additional guidance on choosing among the various methods
for registering verbs.
One nice thing about IExecuteCommand
is that it supports
out-of-proc activation (i.e., local server rather than in-proc server).
This means that it supports cross-bitness shell extensions:
If you don’t have the time to port your 32-bit shell extension to 64-bit,
you can register it as an out-of-proc IExecuteCommand
.
When running on 64-bit Windows,
the 64-bit Explorer will launch your 32-bit server to handle the command.
Conversely, if your IExecuteCommand
is a 64-bit local server,
a 32-bit application can still invoke it.
(We’ll see more about local server shell extensions in a few months. This was just foreshadowing.)
0 comments