How do I create an IShellItemArray from a bunch of file paths?

The IFile­Operation interface accepts bulk operations in the form of an IShell­Item­Array. So how do you take a list of file names and convert them into an IShell­Item­Array?

There is no SHCreate­Shell­Item­Array­From­Paths function, but there is a SHCreate­Shell­Item­Array­From­ID­Lists, and we know how to convert a path to an ID list, namely via SHParse­Display­Name. So lets snap two blocks together.

#define UNICODE
#define _UNICODE
#define STRICT
#include <windows.h>
#include <shlobj.h>
#include <wrl/client.h>

// class CCoInitialize incorporated by reference

template<typename T>
HRESULT CreateShellItemArrayFromPaths(
    UINT ct, T rgt[], IShellItemArray **ppsia)
 *ppsia = nullptr;

 PIDLIST_ABSOLUTE *rgpidl = new(std::nothrow) PIDLIST_ABSOLUTE[ct];
 HRESULT hr = rgpidl ? S_OK : E_OUTOFMEMORY;

 int cpidl;
 for (cpidl = 0; SUCCEEDED(hr) && cpidl < ct; cpidl++)
  hr = SHParseDisplayName(rgt[cpidl], nullptr, &rgpidl[cpidl], 0, nullptr);

 if (SUCCEEDED(hr)) {
  hr = SHCreateShellItemArrayFromIDLists(cpidl, rgpidl, ppsia);

 for (int i = 0; i < cpidl; i++)

 delete[] rgpidl;
 return hr;

The Create­Shell­Item­Array­From­Paths template function takes an array of paths and starts by creating a corresponding array of ID lists. (If you're feeling fancy, you can use a file system bind context to make simple ID lists.) It then pumps this array into the SHCreate­Shell­Item­Array­From­ID­Lists function to get the item array.

Using a template allows you to pass an array of anything as the array of paths, as long as it has a conversion to PCWSTR. So you can pass an array of PCWSTR or an array of PWSTR or an array of BSTR or an array of CCom­Heap­Ptr<wchar_t> or an array of CStringW or whatever else floats your boat.

Let's take this function out for a spin.

int __cdecl wmain(int argc, wchar_t **argv)
 CCoInitialize init;

 Microsoft::WRL::ComPtr<IShellItemArray> spsia;
 Microsoft::WRL::ComPtr<IFileOperation> spfo;

 if (SUCCEEDED(CreateShellItemArrayFromPaths(
                      argc - 1, argv + 1, &spsia)) &&
     SUCCEEDED(CoCreateInstance(__uuidof(FileOperation), nullptr,
                      CLSCTX_ALL, IID_PPV_ARGS(&spfo)))) {

 return 0;

The main program first treats the command line arguments as a list of absolute file paths and uses our new helper function to create a shell item array from them. It then passes the shell item array to the IFile­Operation::Delete­Items method to delete all the items.

No magic here. Just taking the pieces available and combining them in a relatively obvious way.

Comments (7)
  1. WndSks says:

    IFileOperation::DeleteItems is also documented to accept a IPersistIDList and I assume this is more efficient than creating an array that you are just going to throw away?

    [But IPersistIDList represents only one item, which doesn't help if you want to delete multiple. -Raymond]
  2. jschroedl says:

    Very nice – love the shell articles.

  3. T says:

    Looks handy, but may I ask why you're naming variables like this? ppsia, rgt, cpidl, rgpidl… Is it really a matter of getting used to? Because to me, it looks like deliberately obfuscated code.

    [The shell historically uses so-called Hungarian Notation. ppsia = pointer to pointer to shell item array. rgt = range (array) of t. cpidl = count of pointer to id list. rgpidl = range (array) of pointer to id list. Once you get used to it, you spend pretty much zero time thinking about variable names. -Raymond]
  4. John Doe says:

    @Raymond, but Hungarian notation is prefixing names with type indications (mostly worthless for an explicitly statically typed language, anyway).  Where are the actual names?

    [They don't have names. If there's only one item array, you don't need to give it a name. It's just "the item array". A non-Hungarian system would call it shellItemArray. Hungarian calls it sia. -Raymond]
  5. John Doe says:

    @T, that's Hungarian notation (…/aa260976.aspx yes, that's 1999), and Hungarian notation alone.

    ppsia: [p]ointer to [p]ointer to I[S]hell[I]tem[A]rray

    rgt: [r]an[g]e of [T]

    cpidl: [c]ount of [p]ointer to an item [id]entifier [l]ist (Introduction to the Shell Namespace…/cc144090.aspx ; ITEMIDLIST structure…/bb773321.aspx )

    rgpidl: [r]an[g]e of [p]ointer to an item [id]entifier [l]ist

    This is a little program, so I guess you'll have to "just suck it" (…/10424662.aspx ).

    But if it were actually committed/checked-in code, it would certainly ring a bell aound some places where you can't even code an iteration in a slightly different way, much less not have descriptive parameters and variables.

    I guess this is the software parallel of mathematical symbols, but they "solve" their 1 character limit and lack of namespaces with extra alphabets and typesetting.

    Oh wait, there's APL which allows you to do both!

  6. WndSks says:

    @Raymond: I meant in a loop of course:

    for (int i = 1; i < 3; ++i)


    TCHAR path[MAX_PATH+20];

    wsprintf(path+GetTempPath(MAX_PATH, path), _T("file%d.tmp"), i);

    IPersistIDList*ppil = CreateIPersistIDList(path);

    if (ppil) spfo->DeleteItems(ppil), ppil->Release();



    Maybe IFileOperation has a vector of the different operations and in the end this method has more overhead, I don't know…

    [Interesting, hadn't occurred to me to submit multiple operations. Yeah, that should work fine. But then I don't get to show how to create an array of items (sob). -Raymond]
  7. Simon Buchan says:

    I thought it was interesting that you're using C++ features like scope guards (CCoInitialize), template functions and smart pointers, but you're using new[]/delete[] over vector<>. I'm guessing this is for the (std::nothrow): is most of the code you write kernel-side with strict rules about not leaking or even throwing C++ exceptions?

    Heck, if you made it a vector<unique_ptr<IDLIST_ABSOLUTE, CoTaskMemFree>> you wouldn't even have to manually free them, but annoyingly unique_ptr doesn't let you (safely) get a pointer to it's member so you'd have to initialize a raw then assign to the unique_ptr, which sucks.

    [My background is in Win32 platform code, and in Win32, C++ exceptions are not allowed to escape because they are not part of the ABI. I don't know how this code is going to be reused, so I play it safe and write exception-free code. -Raymond]

Comments are closed.