Invoking commands on items in the Recycle Bin


Once you've found the items you want in the Recycle Bin, you may want to perform some operation on them. This brings us back to our old friend, IContextMenu. At this point, you're just snapping two blocks together. You have one block called Retrieving properties from items in the Recycle Bin and you have another block called Invoking verbs on items.

For the first block, let's assume you've written a function called WantToRestoreThisItem which studies the properties of a Recycle Bin item and determines whether you want to restore it. I leave this for you to implement, since I don't know what your criteria are. Maybe you want to restore files only if they were deleted from a particular directory. Maybe you want to restore files that were deleted while you were drunk. (This assumes you have some other computer program that tracks when you're drunk.)¹ Whatever. It's your function.

For the second block, we have a helper function which should look awfully familiar.

void InvokeVerb(IContextMenu *pcm, PCSTR pszVerb)
{
 HMENU hmenu = CreatePopupMenu();
 if (hmenu) {
  HRESULT hr = pcm->QueryContextMenu(hmenu, 0, 1, 0x7FFF, CMF_NORMAL);
  if(SUCCEEDED(hr)) {
   CMINVOKECOMMANDINFO info = { 0 };
   info.cbSize = sizeof(info);
   info.lpVerb = pszVerb;
   pcm->InvokeCommand(&info);
  }
  DestroyMenu(hmenu);
 }
}

And now we snap the two blocks together.

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) {
     if (WantToRestoreThisItem(psi)) {
      IContextMenu *pcm;
      hr = psi->BindToHandler(NULL, BHID_SFUIObject,
                              IID_PPV_ARGS(&pcm));
      if (SUCCEEDED(hr)) {
       InvokeVerb(pcm, "undelete");
       pcm->Release();
      }
     }
     psi->Release();
    }
   }
   psiRecycleBin->Release();
  }
  CoUninitialize();
 }
 return 0;
}

One annoyance of the Recycle Bin is that, at least up until Windows 7, it ignores the CMIC_MASK_FLAG_NO_UI flag. It always displays a confirmation dialog if something dangerous is about to happen (like overwriting an existing file). To mitigate this problem, we can at least reduce the number of confirmations from one-per-file to just one by batching up all the objects we want to operate on into a single context menu. For this, it's easier to go back to the classical version of the program.

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) {
    // in a real program you wouldn't hard-code a fixed limit
    PITEMID_CHILD rgpidlItems[100];
    UINT cpidlItems = 0;
    PITEMID_CHILD pidlItem;
    while (peidl->Next(1, &pidlItem, NULL) == S_OK) {
     if (WantToRestoreThisItem(psfRecycleBin, pidlItem) &&
         cpidlItems < ARRAYSIZE(rgpidlItems)) {
      rgpidlItems[cpidlItems++] = pidlItem;
     } else {
      CoTaskMemFree(pidlItem);
     }
    }
    // restore the items we collected
    if (cpidlItems) {
     IContextMenu *pcm;
     hr = psfRecycleBin->GetUIObjectOf(NULL, cpidlItems,
                     (PCUITEMID_CHILD_ARRAY)rgpidlItems,
                     IID_IContextMenu, NULL, (void**)&pcm);
     if (SUCCEEDED(hr)) {
      InvokeVerb(pcm, "undelete");
      pcm->Release();
     }
     for (UINT i = 0; i < cpidlItems; i++) {
      CoTaskMemFree(rgpidlItems[i]);
     }
    }
   }
   psfRecycleBin->Release();
  }
  CoUninitialize();
 }
 return 0;
}

In the course of the enumeration, we save the ITEMIDLISTs of all the items we want to restore, then create one giant context menu for all of them. This is the programmatic equivalent of multi-selecting the items from the Recycle Bin and then right-clicking. We then invoke the undelete verb on the entire group.

Okay, so now suppose you want to restore the files, but instead of restoring them to their original locations, you want to restore them to a special folder. Like, say, C:\Files I deleted while I was drunk.¹ No problem. We just need a different block to snap into: The drag/drop block.

void DropOnRestoreFolder(IDataObject *pdto)
{
 IDropTarget *pdt;
 if (SUCCEEDED(GetUIObjectOfFile(NULL,
        L"C:\\Files I deleted while I was drunk",
        IID_PPV_ARGS(&pdt)))) {
  POINTL pt = { 0, 0 };
  DWORD dwEffect = DROPEFFECT_MOVE;
  if (SUCCEEDED(pdt->DragEnter(pdto, MK_LBUTTON,
                               pt, &dwEffect))) {
   dwEffect &= DROPEFFECT_MOVE;
   if (dwEffect) {
    pdt->Drop(pdto, MK_LBUTTON, pt, &dwEffect);
   } else {
    pdt->DragLeave();
   }
  }
  pdt->Release();
 }
}

And now it's just a matter of snapping out the undelete block and snapping in the drag/drop block.

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) {
    // in a real program you wouldn't hard-code a fixed limit
    PITEMID_CHILD rgpidlItems[100];
    UINT cpidlItems = 0;
    PITEMID_CHILD pidlItem;
    while (peidl->Next(1, &pidlItem, NULL) == S_OK) {
     if (WantToRestoreThisItem(psfRecycleBin, pidlItem) &&
         cpidlItems < ARRAYSIZE(rgpidlItems)) {
      rgpidlItems[cpidlItems++] = pidlItem;
     } else {
      CoTaskMemFree(pidlItem);
     }
    }
    // restore the items we collected
    if (cpidlItems) {
     IDataObject *pdto;
     hr = psfRecycleBin->GetUIObjectOf(NULL, cpidlItems,
                     (PCUITEMID_CHILD_ARRAY)rgpidlItems,
                     IID_IDataObject, NULL, (void**)&pdto);
     if (SUCCEEDED(hr)) {
      DropOnRestoreFolder(pdto);
      pdto->Release();
     }
     for (UINT i = 0; i < cpidlItems; i++) {
      CoTaskMemFree(rgpidlItems[i]);
     }
    }
   }
   psfRecycleBin->Release();
  }
  CoUninitialize();
 }
 return 0;
}

Footnotes

¹ If being drunk isn't your thing, then substitute some other form of impaired judgment.

Comments (14)
  1. Joshua says:

    I think I'm staying away from this technique. The only thing that would drive me to use it is being in a service context or some other reason why the UI is not directly displayable.

  2. kinokijuf says:

    Request for Windows 8: Add feature that allows only guest account login when the user at the console is drunk.

  3. kinokijuf says:

    Nitpicker's Corner: You should create folders under user's profile instead of under C:

    [That would have complicated the example, and it's unrelated to the point of the exercise. I bet you also tell people who write sample code that "foo" is a bad name for a variable. -Raymond]
  4. KS says:

    I can't remember exactly, but weren't you supposed to create a message pump in the thread that uses apartment threaded objects (CoInitialize(NULL))? This thing has always confused me.

    [If you are apartment-threaded, then you must pump messages while waiting. But we never wait. (The methods we call may wait, and since they are apartment-threaded, they are responsible for pumping messages while they wait.) -Raymond]
  5. D-Coder says:

    "(This assumes you have some other computer program that tracks when you're drunk.)¹"

    Doesn't everyone?

    I would post the code but apparently I deleted the source while I was drunk.

  6. Adam Rosenfield says:

    Where can I get a computer program that tracks when I am drunk?

  7. Dan Bugglin says:

    Check out Gmail Labs, there is a "Google Goggles" or somesuch lab that requires you to answer simple math problems before sending e-mail at certain pre-defined times.  If you cannot you are not permitted to send the e-mail.

  8. voo says:

    @The MAZZTer "simple math problems"? ;) You don't happen to have a PhD in math, do you? Sure the first few levels are trivial, but I doubt that more than some geniuses can solve the level10 problems in the allocated time – at least not without help (and if I use mathematica I assume I'm cheating?).

    eg: imgs.xkcd.com/…/googlecopy.jpg – goodness how the hell would I solve that (mathematica needs half a minute or so to solve the indefinite integral~). Though maybe I'm missing some clever simplification ;)

  9. Stuart says:

    I was looking to see if anyone would post the source code to the method that would determine if they were drunk…

    public static bool AmIDrunk() {

     return true;

    }

    (code is in C#, translation to your language of choice left as an exercise for the reader)

  10. IK says:

    […] we’ll be saying a big hello to all intelligent life forms everywhere… and to everyone else out there, the secret is to bang the rocks together, guys.

    • The Hitchhiker’s Guide to the Galaxy
  11. Cheong says:

    @Stuart: You may want to back-track whether you were drunk at some time, so:

    public static bool AmIDrunk(DataTime timePointToCheck) {

    return true;

    }

    Btw, this function works even if you track future timepoint as well… (I must be drunk)

  12. @voo: "Sure the first few levels are trivial, but I doubt that more than some geniuses can solve the level10 problems in the allocated time – at least not without help (and if I use mathematica I assume I'm cheating?)."

    So don't set it to level 10?

  13. JM says:

    @cheong00: you were drunk before you were born? I think I read a Zen koan about that once. You'll also be drunk after you're dead, apparently, but that's slightly more plausible (I assume you've made arrangements in Valhalla).

  14. constb says:

    What if drop target is not a folder but rather a window with 'DragAcceptFiles -> TRUE' flavor? How to get an IDropTarget for that?

    [This question is not specific to Recycle Bin data objects. -Raymond]

Comments are closed.