Displaying a property sheet for multiple files


Today’s Little Program will show a property sheet that covers multiple files, just like the one you get from Explorer if you multi-select a bunch of files and right-click them all then select Properties.

In fact, that description of how you do the operation interactively maps directly to how you do the operation programmatically!

#define UNICODE
#define _UNICODE
#define STRICT_TYPED_ITEMIDS
#include <windows.h>
#include <ole2.h>
#include <shlobj.h>
#include <atlbase.h>
#include <atlalloc.h>

HRESULT GetUIObjectOf(
    IShellFolder *psf,
    HWND hwndOwner,
    UINT cidl,
    PCUITEMID_CHILD_ARRAY apidl, REFIID riid, void **ppv)
{
 return psf->GetUIObjectOf(hwndOwner, cidl, apidl, riid, nullptr, ppv);
}

The Get­UI­Object­Of helper function merely wraps the IShell­Folder::Get­UI­Object­Of method to insert the pesky nullptr parameter between the riid and ppv. The riid and ppv parameters by convention go right next to each other, and the IID_PPV_ARGS macro assumes that the function you’re calling follows that convention. Unfortunately, the people who designed IShell­Folder::Get­UI­Object­Of didn’t get the memo, and we’ve been stuck with it ever since.

HRESULT InvokeCommandByVerb(
    IContextMenu *pcm,
    HWND hwnd,
    LPCSTR pszVerb)
{
 HMENU hmenu = CreatePopupMenu();
 HRESULT hr = hmenu ? S_OK : E_OUTOFMEMORY;
 if (SUCCEEDED(hr)) {
  hr = pcm->QueryContextMenu(hmenu, 0, 1, 0x7FFF, CMF_NORMAL);
  if (SUCCEEDED(hr)) {
   CMINVOKECOMMANDINFO info = { 0 };
   info.cbSize = sizeof(info);
   info.hwnd = hwnd;
   info.lpVerb = pszVerb;
   hr = pcm->InvokeCommand(&info);
  }
  DestroyMenu(hmenu);
 }
 return hr;
}

The Invoke­Command­By­Verb function merely hosts an IContext­Menu and invokes a single verb.

Okay, those are the only two helper functions we need this week. The rest we can steal from earlier articles.

For the purpose of illustration, the program will display a multi-file property sheet for the first two files in your My Documents folder folder. Remember, Little Programs do little to no error checking.

int __cdecl wmain(int, wchar_t **)
{
 CCoInitialize init;
 ProcessReference ref;
 CComPtr<IShellFolder> spsf;
 BindToCsidl(CSIDL_MYDOCUMENTS, IID_PPV_ARGS(&spsf));
 CComPtr<IEnumIDList> speidl;
 spsf->EnumObjects(nullptr, SHCONTF_NONFOLDERS, &speidl);
 if (!speidl) return 0;
 CComHeapPtr<ITEMID_CHILD> spidl1;
 CComHeapPtr<ITEMID_CHILD> spidl2;
 if (speidl->Next(1, &spidl1, nullptr) != S_OK) return 0;
 if (speidl->Next(1, &spidl2, nullptr) != S_OK) return 0;
 PCUITEMID_CHILD rgpidl[2] = { spidl1, spidl2 };
 CComPtr<IContextMenu> spcm;
 GetUIObjectOf(spsf, nullptr, 2, rgpidl, IID_PPV_ARGS(&spcm));
 if (!spcm) return 0;
 InvokeCommandByVerb(spcm, "properties");
 return 0;
}

Because everybody freaks out if I write code that doesn’t run on Windows XP, I used the Bind­To­CSIDL function instead of one of its more modern equivalents to get access to the My Documents folder.

Once we have My Documents, we ask to enumerate its non-folders. If the enumeration fails or says that there are no items (by returning S_FALSE), then we bail immediately.

Next, we enumerate two items from the folder. If we can’t get both, then we bail.

We then create a two-item array and get the IContext­Menu UI object for the collection.

Finally, we invoke the "properties" verb on the context menu.

And that’s it. If you run this program, you’ll see a context menu for the first two files in your My Documents folder.

Comments (9)
  1. WndSks says:

    This is nice and simple but if you want to do something when the files do not have the same parent folder (Find dialog etc) things become difficult and you have to take a walk into undocumented land :(

  2. I hope the shell verbs are not localized.

  3. Mike Dimmick says:

    @alegr1: See msdn.microsoft.com/…/dd374120(v=vs.85).aspx under "Provide Resources for Shell Verb Action Strings". That doesn't stop someone localizing the verbs themselves, for uncommon verbs, but there are standard mechanisms for localizing the standard verbs.

  4. Nick says:

    Raymond, I don't know if this comes under the "little to no error checking" rule, but I wanted to point out, your call to InvokeCommandByVerb in wmain is wrong – the second parameter is a HWND.

  5. Danny says:

    "Patience, young grasshopper. -Raymond" in reply to skSdnW. You do realize Ray that he is the same person who had "WndSks" as name to which you said to change his name because insulting people is not going to bode well. He simply kept it but make it the other way around.

    [I know. That's why I'm going to make him wait at least two years. -Raymond]
  6. WndSks says:

    Good thing I brought it up in the past then ( blogs.msdn.com/…/10351107.aspx ), and I keep asking because I actually want to know so I can do it correctly.

    You can find some partial information on various forums and news archives but someone on the inside giving us the correct order of ProgId, *, Folder, Directory, AllFilesystemObjects, Unknown, SystemFileAssociations, and Kind? would be handy…

    [It's nowhere near that complicated. -Raymond]
  7. "Because everybody freaks out if I write code that doesn't run on Windows XP"…

    Thank you for still providing XP compatible code examples. I would love to always use the latest/simplest thing available, but I (like I suppose many of us) have to support a large customer base that is still around 50% XP. If something does not run on XP, then I basically am not able to use it (unless I want to write the same things twice and always check the OS version – which I don't). Since we have to support our customers (who are slow to upgrade) at least a year past the end of Microsoft support (a bu$iness decision), XP is what we must target for at least another 2 years.

    It is for this very reason we are targeting .NET 4.0 instead of 4.5 with our "next-gen" product update.

  8. WndSks says:

    @Raymond: If you look at the comment on the MSDN page for CDefFolderMenu_Create2 you see that the order of Folder/Directory matters for the Open verb, the order of other things also matter if you want shell extensions to show in the same way/order as Explorer. If you leave out a key things will be missing and getting the order wrong might break stuff.

    Getting this right on 2000, XP (Added SystemFileAssociations) and NT6 _is_ complicated. Imagine wanting to do this in the 9x days before MS was forced to document CDefFolderMenu_Create2! On top of this you need a root IShellFolder that is able to hand out a working IDataObject and SHGetDesktopFolder will not do since IShellFolder::GetUIObjectOf only wants single level child pidls…

    [You're just trying to get me to respond so you get your answer sooner. Now I'm tempted to push the article out just to spite you. It's not hard, and uses only documented interfaces. -Raymond]
  9. Joshua says:

    [You're just trying to get me to respond so you get your answer sooner. Now I'm tempted to push the article out just to spite you. It's not hard, and uses only documented interfaces. -Raymond]

    Don't tempt me. I abandoned COM to the bit-bucket years ago. Maybe my multi-file properties dialog box will actually work.

    </flamebait>

Comments are closed.