Modernizing our simple program that retrieves information about the items in the Recycle Bin


Last time, we wrote a simple program to print various properties of the items in the Recycle Bin, and we did so in the classical style, using item ID lists and IShell­Folders. One thing you may have noticed is that a lot of functions take the combination of an IShell­Folder and a PCUITEMID_CHILD. In the shell namespace, operations on items usually happen by means of the pair (folder, child), and one of the common mistakes made by beginners is failing to keep track of the pairing and passing child pidls to the wrong parent folder.

Even if you're not a beginner and are good at keeping track of which child pidls correspond to which parent folders, it's still extra work you have to do, and it means that a lot of functions take two parameters in order to describe one thing.

Enter IShell­Item.

The IShell­Item encapsulates the pair (folder, child). This solves two problems:

  1. You only have to pass one thing around (the IShell­Item) instead of two (the IShell­Folder and the PCUITEMID_CHILD).

  2. By keeping track of the two items as a single unit, it reduces the risk that you'll accidentally use a child pidl with the wrong parent folder.

Another complexity of the classic shell interface is that there are a bunch of ways of obtaining COM objects from a shell folder:

  • IShell­Folder::Bind­To­Object
  • IShell­Folder::Bind­To­Storage
  • IShell­Folder::Create­View­Object
  • IShell­Folder::Get­UI­Object­Of
  • IUnknown::Query­Interface (thanks to the desktop special case we saw last time).

The IShell­Item::Bind­To­Handler interface hides these special-cases by dealing with them under the covers so you don't have to. You just call IShell­Item::Bind­To­Handler and it figures out where to get the object and what weird special cases apply. (It also takes care of the weird S_FALSE return value from IShell­Folder::Enum­Objects.)

And then there's the annoyance of IShell­Folder::Get­Display­Name­Of using the kooky STRRET structure. The IShell­Item::Get­Display­Name function encapsulates that away for you by doing the work to convert that STRRET into a boring string pointer.

First up in modernizing our sample program is to change Bind­To­Csidl to return a shell item instead of a shell folder.

HRESULT BindToCsidlItem(int csidl, IShellItem ** ppsi)
{
 *ppsi = NULL;
 HRESULT hr;
 PIDLIST_ABSOLUTE pidl;
 hr = SHGetSpecialFolderLocation(NULL, csidl, &pidl);
 if (SUCCEEDED(hr)) {
  hr = SHCreateShellItem(NULL, NULL, pidl, ppsi);
  CoTaskMemFree(pidl);
 }
 return hr;
}

But wait, since we're modernizing, we may as well upgrade to SHGet­Known­Folder­ID­List:

HRESULT BindToKnownFolderItem(REFKNOWNFOLDER rfid, IShellItem ** ppsi)
{
 *ppsi = NULL;
 HRESULT hr;
 PIDLIST_ABSOLUTE pidl;
 hr = SHGetKnownFolderIDList(rfid, 0, NULL, &pidl);
 if (SUCCEEDED(hr)) {
  hr = SHCreateShellItem(NULL, NULL, pidl, ppsi);
  CoTaskMemFree(pidl);
 }
 return hr;
}

Hey wait, there's a function for this already in Windows 7! It's called SHGet­Known­Folder­Item. Yay, now we can delete the function entirely.

Next, we convert Print­Display­Name to use IShell­Item and the item-based display name flags SIGDN.

void PrintDisplayName(IShellItem *psi, SIGDN sigdn, PCTSTR pszLabel)
{
 LPWSTR pszName;
 HRESULT hr = psi->GetDisplayName(sigdn, &pszName);
 if (SUCCEEDED(hr)) {
  _tprintf(TEXT("%s = %ws\n"), pszLabel, pszName);
  CoTaskMemFree(pszName);
 }
}

And then we convert Print­Detail to use IShell­Item. Oh wait, now we've hit a snag: The IShell­Item interface doesn't have a helper method that wraps IShell­Folder2::Get­Details­Ex. Fortunately, there is a way to ask IShell­Item to regurgitate the IShell­Folder and PITEMID_CHILD that it is wrapping: You use the IParent­And­Item::Get­Parent­And­Item method.

void PrintDetail(IShellItem *psi,
    const SHCOLUMNID *pscid, PCTSTR pszLabel)
{
 IParentAndItem *ppni;
 HRESULT hr = psi->QueryInterface(IID_PPV_ARGS(&ppni));
 if (SUCCEEDED(hr)) {
  IShellFolder *psf;
  PITEMID_CHILD pidl;
  hr = ppni->GetParentAndItem(NULL, &psf, &pidl);
  if (SUCCEEDED(hr)) {
   VARIANT vt;
   hr = psf->GetDetailsEx(pidl, pscid, &vt);
   if (SUCCEEDED(hr)) {
    hr = VariantChangeType(&vt, &vt, 0, VT_BSTR);
    if (SUCCEEDED(hr)) {
     _tprintf(TEXT("%s: %ws\n"), pszLabel, V_BSTR(&vt));
    }
    VariantClear(&vt);
   }
   psf->Release();
   CoTaskMemFree(pidl);
  }
 }
}

Wow, it looks like we lost ground there. Ah, but Windows Vista extends IShell­Item with the IShell­Item2 interface, and that has a bunch of new methods for retrieving properties.

void PrintDetail(IShellItem2 *psi,
    const SHCOLUMNID *pscid, PCTSTR pszLabel)
{
  PROPVARIANT vt;
  HRESULT hr = psi->GetProperty(*pscid, &vt);
  if (SUCCEEDED(hr)) {
   hr = VariantChangeType(&vt, &vt, 0, VT_BSTR);
   if (SUCCEEDED(hr)) {
    _tprintf(TEXT("%s: %ws\n"), pszLabel, V_BSTR(&vt));
   }
   PropVariantClear(&vt);
  }
 }
}

But wait, there's more. There's a special accessor just for retrieving properties as strings!

void PrintDetail(IShellItem2 *psi2,
    const SHCOLUMNID *pscid, PCTSTR pszLabel)
{
 LPWSTR pszValue;
 HRESULT hr = psi2->GetString(*pscid, &pszValue);
 if (SUCCEEDED(hr)) {
  _tprintf(TEXT("%s: %ws\n"), pszLabel, pszValue);
  CoTaskMemFree(pszValue);
 }
}

Okay, that's more like it. Now let's update the main program.

int __cdecl _tmain(int argc, PTSTR *argv)
{
 HRESULT hr = CoInitialize(NULL);
 if (SUCCEEDED(hr)) {
  IShellItem *psiRecycleBin;
  hr = SHGetKnownFolderItem(FOLDERID_RecycleBinFolder, KF_FLAG_DEFAULT,
                            NULL, IID_PPV_ARGS(&psiRecycleBin));
  if (SUCCEEDED(hr)) {
   IEnumShellItems *pesi;
   hr = psiRecycleBin->BindToHandler(NULL, BHID_EnumItems,
                                     IID_PPV_ARGS(&pesi));
   if (hr == S_OK) {
    IShellItem *psi;
    while (pesi->Next(1, &psi, NULL) == S_OK) {
     IShellItem2 *psi2;
     if (SUCCEEDED(psi->QueryInterface(IID_PPV_ARGS(&psi2)))) {
      _tprintf(TEXT("------------------\n"));

      PrintDisplayName(psi2, SIGDN_PARENTRELATIVE,
                             TEXT("ParentRelative"));
      PrintDisplayName(psi2, SIGDN_NORMALDISPLAY, TEXT("Normal"));
      PrintDisplayName(psi2, SIGDN_FILESYSPATH, TEXT("FileSys"));

      PrintDetail(psi2, &SCID_OriginalLocation, TEXT("Original Location"));
      PrintDetail(psi2, &SCID_DateDeleted, TEXT("Date deleted"));
      PrintDetail(psi2, &PKEY_Size, TEXT("Size"));
      psi2->Release();
     }
     psi->Release();
    }
   }
   psiRecycleBin->Release();
  }
  CoUninitialize();
 }
 return 0;
}

Okay, so now we know how to enumerate the contents of the Recycle Bin and obtain properties of the items in it. How do we purge or restore items? We'll look at that next time.

Comments (18)
  1. Mordachai says:

    This article is a "Exhibit A" as to why COM is not a friendly technology to program in, IMO.

    Trying to come up with the above interfaces, and choose between multiple APIs and ways to accomplish something, without violating one's market requirements (i.e. "can this run on XP?"), is a major PITA.  Knowing what "alternate routes to take" to get there in shorter time is a serious time-sink in itself.

    Maybe I'm just showing my age, but to me the Win32 API – crufty as it is in many places, has always been easier to navigate and get the thing I'm looking for (and be reasonably certain as to what versions of the OS it will run under).

    As a long-term strategy for creating a programmer's toolbox API, COM is just not as good as a straight C API.

  2. @Steve says:

    I agree with you there.  I cringe every time I have to delve into COM as it just feels unfriendly in every possible way.  Yet at the same time, the C API is ancient and not conducive at all to modern, safe C++ programming. (and COM isn't really much better, even with ATL and other such wrappers).

    With the "C++ Renaissance" that Microsoft keeps touting, I am hopeful that they realize this as well and start releasing coherent C++ APIs instead — there aren't all that many of us that actually need the flat C API anymore.

  3. A. Skrobov says:

    @Steve, you forget how many Windows developers there are that use languages other than C++. Anything from Delphi to Tcl.

  4. Adrian says:

    As long as we're modernizing, why not do away with the TEXT macros and TCHAR versions of things and just use wide strings everywhere?

  5. Billy O'Neal says:

    @@Steve,

    Windows cannot expose C++ APIs for two reasons: 1. C++ does not work cleanly across DLL boundaries. 2. C++ cannot be called by non C++ code cleanly.

  6. Tyler Reddon says:

    @Steve if you want to see why you don't make a C++ API go try to program Be, that should shake the idea.

  7. Mason Wheeler says:

    @Steve Wolf: Try it in Delphi sometime.  It makes COM much, much easier.  It's not that COM is a bad technology, it's that C++ is a bad language to try to implement it in!

  8. voo says:

    Mini nitpick: In your second code example you've highlighted hr = SHCreateShellItem(NULL, NULL, pidl, ppsi); as in the first instead of the correct  hr = SHGetKnownFolderIDList(rfid, 0, NULL, &pidl);

  9. WndSks says:

    How is someone outside of MS supposed to be able to write code like this on their own?

    MSDN does not document how to free the string returned from IShellItem2::GetString! The shell usually uses IMalloc but some functions require LocalFree etc.

  10. Neil says:

    Is using smart pointers going to make the code more or less clear than explicit calls to Release?

    [It depends on whether you think smart pointers are clear. Sometimes they can be quite subtle. -Raymond]
  11. Cesar says:

    @WndSks: since it is COM, absent documentation saying otherwise, I would assume the memory was allocated by CoTaskMemAlloc and should be released by CoTaskMemFree.

  12. Rick C says:

    @WndSks:  There's rules for it.  IIRC if it doesn't say otherwise, callers always use CoTaskMemFree.

  13. 640k says:

    New low level apis should be implemented in C, high level apis should be implemented in .net.

    COM is ready for Recycle Bin.

  14. Mordachai says:

    The vast majority of languages support some mechanism for linking up C APIs.  Java, C#, VB, C++, C, Objective-C, TCL (with or without TK), Python, Smalltalk, you-name-it, all have ways of interfacing with C APIs and DLLs which export such APIs.

    C is crufty, but it doesn't lead to Thingie1::, replaced by Thingie2::, then Thingie3:: – but wait, can Thingie3:: interface with OtherThingie1:: or do I need OtherThingie2::, and are those both supported on my customer's OS requirements, and is this the right way to go about doing this at all, etc.

    Those issues don't ever disappear, but they're – IMO – easier to document and convey with C APIs.  Let .NET have comprehensive wrappers for everything and present a higher level OS-API.  COM is just painful, IMO.  (It takes the best ideas from C++ and marries them to an incredibly inelegant implementation that is less flexible and harder to use [especially to get right] than is/are C APIs for no tangible [real-world] benefit that I am aware of).

    [Things get hairy with a C interface when there are multiple providers. You end up with a structure of callback functions. (See: FCI.) And if you want to make a v2, then one of those callback functions would be "If you support v2, please return the callback function table for v2." And then you've reinvented QueryInterface and vtables. -Raymond]
  15. WndSks says:

    @Raymond: The IParentAndItem::GetParentAndItem parameters are marked out and not out_opt (on MSDN) but I guess you know the API better than the MSDN doc folks…

    @Cesar,@Rick C: Do you have an official link to back up your claims? (IShellItem::GetDisplayName for example is specific about using CoTaskMemFree on the string, but if CoTaskMemFree is implied, why list it on only some interfaces/methods)

    [Doc error. They are annotated correctly in the header file. As for CoTaskMemAlloc, the Memory Management Rules COM documentation clearly states that "Out-parameters […] are freed by the caller using the standard COM task memory allocator." This requirement is mentioned in other places too, like here (And you may want to change your handle. My patience for it is running out.) -Raymond]
  16. WndSks says:

    Thanks Raymond, you rock!

  17. Pete says:

    If you had written this entry a year ago, it would have saved me significant time on my current project. Please engage your time machine and fix this.

  18. Rick C says:

    @WndSks, you don't see documentation on every little function how to allocate it, because, as Raymond pointed out, the overall docs clearly state you use CoTaskMemFree, IMalloc, or the default OLE allocator, all of which ultimately wind up being the same thing, unless otherwise declared.  Anybody who does COM programming should know that, the same way any C programmer should know operator precedence.

Comments are closed.