How can I get information about the items in the Recycle Bin?


For some reason, a lot of people are interested in programmatic access to the contents of the Recycle Bin. They never explain why they care, so it’s possible that they are looking at their problem the wrong way.

For example, one reason for asking, “How do I purge an item from the Recycle Bin given a path?” is that some operation in their program results in the files going into the Recycle Bin and they want them to be deleted entirely. The correct solution is to clear the FOF_ALLOW­UNDO flag when deleting the items in the first place. Moving to the Recycle Bin and then purging is the wrong solution because your search-and-destroy mission may purge more items than just the ones your program put there.

The Recycle Bin is somewhat strange in that it can have multiple items with the same name. Create a text file called TEST.TXT on your desktop, then delete it into the Recycle Bin. Create another text file called TEST.TXT on your desktop, then delete it into the Recycle Bin. Now open your Recycle Bin. Hey look, you have two TEST.TXT files with the same path!

Now look at that original problem: Suppose the program, as part of some operation, moves the file TEST.TXT from the desktop to the Recycle Bin, and then the second half of the program goes into the Recycle Bin, finds TEST.TXT and purges it. Well, there are actually three copies of TEST.TXT in the Recycle Bin, and only one of them is the one you wanted to purge.

Okay, I got kind of sidetracked there. Back to the issue of getting information about the items in the Recycle Bin.

The Recycle Bin is a shell folder, and the way to enumerate the contents of a shell folder is to bind to it and enumerate its contents. The low-level interface to the shell namespace is via IShell­Folder. There is an easier-to-use medium-level interface based on IShell­Item, and there’s a high-level interface based on Folder designed for scripting.

I’ll start with the low-level interface. As usual, the program starts with a bunch of header files.

#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <shlobj.h>
#include <shlwapi.h>
#include <propkey.h>

The Bind­To­Csidl function binds to a folder specified by a CSIDL. The modern way to do this is via KNOWN­FOLDER, but just to keep you old fogeys happy, I’m doing things the classic way since you refuse to upgrade from Windows XP. (We’ll look at the modern way later.)

HRESULT BindToCsidl(int csidl, REFIID riid, void **ppv)
{
 HRESULT hr;
 PIDLIST_ABSOLUTE pidl;
 hr = SHGetSpecialFolderLocation(NULL, csidl, &pidl);
 if (SUCCEEDED(hr)) {
  IShellFolder *psfDesktop;
  hr = SHGetDesktopFolder(&psfDesktop);
  if (SUCCEEDED(hr)) {
   if (pidl->mkid.cb) {
    hr = psfDesktop->BindToObject(pidl, NULL, riid, ppv);
   } else {
    hr = psfDesktop->QueryInterface(riid, ppv);
   }
   psfDesktop->Release();
  }
  CoTaskMemFree(pidl);
 }
 return hr;
}

The subtlety here is in the test for pidl->mkid.cb. The IShell­Folder::Bind­To­Object method is for binding to child objects (or grandchildren or deeper descendants). If the object you want is the desktop itself, then you can’t use IShell­Folder::Bind­To­Object since the desktop is not a child of itself. In fact, if the object you want is the desktop itself, then you already have the desktop, so we just Query­Interface for it. It’s an annoying special case which usually lurks in your code until somebody tries something like “Save file to desktop” or changes the location of a special folder to the desktop, and then boom you trip over the fact that the desktop is not a child of itself. (See further discussion below.)

Another helper function prints the display name of a shell namespace item. There isn’t much interesting here either.

void PrintDisplayName(IShellFolder *psf,
    PCUITEMID_CHILD pidl, SHGDNF uFlags, PCTSTR pszLabel)
{
 STRRET sr;
 HRESULT hr = psf->GetDisplayNameOf(pidl, uFlags, &sr);
 if (SUCCEEDED(hr)) {
  PTSTR pszName;
  hr = StrRetToStr(&sr, pidl, &pszName);
  if (SUCCEEDED(hr)) {
   _tprintf(TEXT("%s = %s\n"), pszLabel, pszName);
   CoTaskMemFree(pszName);
  }
 }
}

Our last helper function retrieves a property from the shell namespace and prints it. (Obviously, if we wanted to do something other than print it, we could coerce the type to something other than VT_BSTR.)

void PrintDetail(IShellFolder2 *psf, PCUITEMID_CHILD pidl,
    const SHCOLUMNID *pscid, PCTSTR pszLabel)
{
 VARIANT vt;
 HRESULT 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);
 }
}

Okay, now we can get down to business. The properties we will display from each item in the Recycle Bin are the item name and path, the original location (before the item was deleted), the date the item was deleted, and the size of the item.

Getting the name and path are done with various combinations of flags to IShell­Folder::Get­Display­Name­Of, whereas getting the other properties involve talking to the shell property system. (My colleague Ben Karas covers the shell property system on his blog.) The SHCOLUMN­ID documentation says that the displaced property set applies to items which have been moved to the Recycle Bin, so we can define those column IDs based on the values provided in shlguid.h:

const SHCOLUMNID SCID_OriginalLocation =
   { PSGUID_DISPLACED, PID_DISPLACED_FROM };
const SHCOLUMNID SCID_DateDeleted =
   { PSGUID_DISPLACED, PID_DISPLACED_DATE };

The other property we want is System.Size, which the documentation says is defined as PKEY_Size by the propkey.h header file.

Okay, let’s roll!

int __cdecl _tmain(int argc, PTSTR *argv)
{
 HRESULT hr = CoInitialize(NULL);
 if (SUCCEEDED(hr)) {
  IShellFolder2 *psfRecycleBin;
  hr = BindToCsidl(CSIDL_BITBUCKET, IID_PPV_ARGS(&psfRecycleBin));
  if (SUCCEEDED(hr)) {
   IEnumIDList *peidl;
   hr = psfRecycleBin->EnumObjects(NULL,
     SHCONTF_FOLDERS | SHCONTF_NONFOLDERS, &peidl);
   if (hr == S_OK) {
    PITEMID_CHILD pidlItem;
    while (peidl->Next(1, &pidlItem, NULL) == S_OK) {
     _tprintf(TEXT("------------------\n"));

     PrintDisplayName(psfRecycleBin, pidlItem,
                      SHGDN_INFOLDER, TEXT("InFolder"));
     PrintDisplayName(psfRecycleBin, pidlItem,
                      SHGDN_NORMAL, TEXT("Normal"));
     PrintDisplayName(psfRecycleBin, pidlItem,
                      SHGDN_FORPARSING, TEXT("ForParsing"));

     PrintDetail(psfRecycleBin, pidlItem,
                 &SCID_OriginalLocation, TEXT("Original Location"));
     PrintDetail(psfRecycleBin, pidlItem,
                 &SCID_DateDeleted, TEXT("Date deleted"));
     PrintDetail(psfRecycleBin, pidlItem,
                 &PKEY_Size, TEXT("Size"));

     CoTaskMemFree(pidlItem);
    }
   }
   psfRecycleBin->Release();
  }
  CoUninitialize();
 }
 return 0;
}

The only tricky part is the test for whether the call to IShell­Folder::Enum­Objects succeeded, highlighted above. According to the rules for IShell­Folder::Enum­Objects, the method is allowed to return S_FALSE to indicate that there are no children, in which case it sets peidl to NULL.

If you are willing to call functions new to Windows Vista, you can simplify the Bind­To­Csidl function by using the helper function SHBind­To­Object. This does the work of getting the desktop folder and handling the desktop special case.

HRESULT BindToCsidl(int csidl, REFIID riid, void **ppv)
{
 HRESULT hr;
 PIDLIST_ABSOLUTE pidl;
 hr = SHGetSpecialFolderLocation(NULL, csidl, &pidl);
 if (SUCCEEDED(hr)) {
  hr = SHBindToObject(NULL, pidl, NULL, riid, ppv);
  CoTaskMemFree(pidl);
 }
 return hr;
}

But at this point, I’m starting to steal from the topic I scheduled for next time, namely modernizing this program to take advantage of some new helper functions and interfaces. We’ll continue next time.

Comments (12)
  1. Cesar says:

    It is not that we refuse to upgrade from Windows XP; it is that, while people still use Windows XP, and it is still supported by Microsoft, we have to make the software we develop work on it too. After 2014, this will not be an issue anymore.

  2. woodswan says:

    Before reading the post, I was thinking that the Recycle Bin folder is specially managed as the logging file system, so that it can have items with the same name and path. But it seems that I was wrong.

    [That would have been a neat trick, since Windows 95 had a Recycle Bin but FAT doesn't support logging. -Raymond]
  3. David Walker says:

    Yep, that's a lot of trouble.  I just ignore the recycle bin most of the time.  I delete things to the recycle bin, but I clear it out several times a day.  Seeing trash in the recycle bin is mildly annoying.

    As for Windows XP, I know a lot of companies that plan to wait until 2013 or 2014 and then upgrade to Windows 7.  Some of my relatives will likely do the same thing (but most of them are on Windows 7 already).  I have mostly moved to Windows 7 on my 5 main computers, but I keep XP around in a virtual machine for testing things that my customers use.

  4. Adam Rosenfield says:

    Neat trick, but real men http://www.garyshood.com/root/</a“>1 just call DeleteFile().

  5. SimonRev says:

    Actually, it is a refusal to upgrade by our IT administrator, rather than by me.  Not that our IT guy has any sort of coherent reason to ban Windows 7 and Vista (this is the same guy that blocked IMAP access to our corporate email from our PCs inside the building because he thinks the web client is "very nice").

    In the end, Cesar nailed things on the head though — as long as OUR customers use XP, we have to write software than runs under XP.  As nice and cool as the new APIs are, they are irrelevant until our customer base migrates.

    [That's fair. In the past, though, I get flamed for not providing Windows XP ways of doing things as if it no right-thinking person would run anything else. -Raymond]
  6. Anonymous Coward says:

    Alternative solution, which is less efficient but I doubt it matters much for this kind of thing, is to use the Shell32 library object model which does the same thing under the hood, but it makes your code shorter because you don't have to do all the management yourself:


    Private Sub Form_Load()

    Dim S As New Shell32.Shell, I As FolderItem2

    With S.NameSpace(ssfBITBUCKET)

       Caption = .Title

       For Each I In .Items

           List1.AddItem I.Path & " = " & _

               I.ExtendedProperty("{9B174B33-40FF-11D2-A27E-00C04FC30871}2") & _

               "" & I.Name

       Next

    End With

    End Sub


    Visual Basic code because I can't be arsed to write C++ GUI code for a reply to a blog post. To test, draw a widish listbox on the form.

    P.S. Thanks for providing XP compatible code, Raymond, it is appreciated.

  7. dave says:

    so it's possible that they are looking at their problem the wrong way.

    I'd say "probable" !

    I look upon the recycle bin as being something that's there for the user, and is not an application programmer's concern.

    If the file that's being deleted "belongs to the user", then use the API that might put the file in the recycle bin, depending on the user's preferences, and forget about it.   If the file that's being deleted is some internal application thing that shouldn't end up in the recycle bin, then don't use the API that might put the file in the recycle bin.  How hard is that?

    Even if an app can reliably purge its own file from the recycle bin after putting it there, that's not always an operation without side-effects.  The recycle bin is limited in total capacity.

  8. TC says:

    AC's simple method easily demo'd from (gasp) vbscript (thus requiring only notepad):

    option explicit

    const ssfBITBUCKET = 10

    dim sh, item, s

    set sh = createobject ("shell.application")

    s = ""

    with sh.nameSpace (ssfBITBUCKET)

      for each item in .items

         s = s & item.Path & " = " & _

            item.ExtendedProperty("{9B174B33-40FF-11D2-A27E-00C04FC30871}2") & _

            "" & item.Name & vblf

      next

      msgbox s,, .title

    end with

    wscript.quit

  9. TC says:

    Oops, sorry for silly double spacing, not sure why it did that to me.

  10. Anonymous Coward says:

    @TC: That happened because this blog doesn't have a concept of line breaks (as opposed to paragraph breaks). The reason for this is probably that most of the replies are prose, and people press enter a variable number of times when all they really want is a paragraph break. Just be happy our spaces are preserved.

  11. xpclient says:

    And another bug in Windows 7: Custom Recycle Bin icons for full and empty do not refresh automatically, only the default icons do. You have to either refresh manually or tweak the registry to fix it.

  12. TC says:

    @TC: That happened because …

    Thanks AC. 5 seconds after posting, I was thinking "drink less cheap white wine!", and looking for an edit post button!

Comments are closed.