Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use SHCreateItemFromParsingName with names from the shell namespace?

I am using SHCreateItemFromParsingName to turn a path into a IShellItem:

IShellItem ParseName(String path)
{
    IShellItem shellItem;

    HRESULT hr = SHCreateItemFromParsingName(path, null, IShellItem, out shellItem);
    if (Failed(hr)) 
        throw new ECOMException(hr);
    return shellItem;
}

Note: A IShellItem was introduced around 2006 to provide a handy wrapper around the Windows 95-era IShellFolder+pidl constructs. You can even ask a IShellItem to cough up it's underlying IShellFolder and pidl with the IParentAndItem.GetParentAndItem interface and method.

Different things have different display names

I can get ahold of some well-known locations in the shell namespace, and see their absolute parsing (SIGDN_DESKTOPABSOLUTEPARSING) and editing (SIGDN_DESKTOPABSOLUTEEDITING) display names:

Path Editing Parsing
C:\ "C:" "C:"
C:\Windows "C:\Windows" "C:\Windows"
Desktop "Desktop" "C:\Users\Ian\Desktop"
Computer "This PC" "::{20D04FE0-3AEA-1069-A2D8-08002B30309D}"
Recycle Bin "Recycle Bin" "::{645FF040-5081-101B-9F08-00AA002F954E}"
Documents Library "Libraries\Documents" "::{031E4825-7B94-4DC3-B131-E946B44C8DD5}\Documents.library-ms" "
Startup "C:\Users\Ian\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup" "C:\Users\Ian\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup"

How to parse them when the user types in them in?

I can use IFileOpenDialog to let the user select one of these folders. But i'd really like the user to be able to type

  • "C:\Users"
  • "C:\Windows\Fonts"
  • "This PC"
  • "Recycle Bin"
  • "Libraries"
  • "Startup"
  • "Fonts"

and be able to parse it into an IShellItem.

The problem is that some of the paths are not parsed by SHCreateItemFromParsingName:

  • SHCreateItemFromParsingName("C:\"): Parses
  • SHCreateItemFromParsingName("C:\Windows"): Parses
  • SHCreateItemFromParsingName(""): Parses (but becomes "This PC")
  • SHCreateItemFromParsingName("This PC"): Fails
  • SHCreateItemFromParsingName("Recycle Bin"): Fails
  • SHCreateItemFromParsingName("Libraries"): Fails
  • SHCreateItemFromParsingName("OneDrive"): Fails
  • SHCreateItemFromParsingName("Libraries\Documents"): Fails
  • SHCreateItemFromParsingName("Network"): Fails
  • SHCreateItemFromParsingName("Startup"): Fails

Meanwhile, the IFileOpenDialog control that my program uses can parse them fine:

enter image description here

How can i parse the various special shell name places that a user might type in (that Windows Explorer and the IFileOpen dialog can parse) into an IShellItem for that folder?

The real question is that i want the user to be able to have a recent MRU list that contains things like:

  • C:\Windows
  • Recycle Bin
  • This PC

and be able to parse them later.

like image 201
Ian Boyd Avatar asked Mar 23 '17 03:03

Ian Boyd


2 Answers

It would be interesting to debug Explorer and see how it does it.

My suggestion is; if the initial parse fails, prepend shell: to the path string and try parsing it again with SHParseDisplayName. If you set STR_PARSE_SHELL_PROTOCOL_TO_FILE_OBJECTS in the bind context you can also bind to special files. The shell: protocol is able to parse the internal/canonical name of special/known folders but I don't know if it also checks the display name.

Edit:

I had a chance to play around a bit now and the shell: prefix is not a huge improvement because it only checks the known folder canonical names:

PCWSTR paths[] = {
    TEXT("C:\\"),
    TEXT("C:\\Windows"),
    TEXT(""),
    TEXT("This PC"),
    TEXT("MyComputerFolder"), // Canonical KF name
    TEXT("Recycle Bin"),
    TEXT("RecycleBinFolder"), // Canonical KF name
    TEXT("Libraries"),
    TEXT("OneDrive"),
    TEXT("Libraries\\Documents"),
    TEXT("Network"),
    TEXT("NetworkPlacesFolder"), // Canonical KF name
    TEXT("Startup"),
};

OleInitialize(0);
INT pad = 0, fill, i;
for (i = 0; i < ARRAYSIZE(paths); ++i) pad = max(pad, lstrlen(paths[i]));
for (i = 1, fill = printf("%-*s | Original | shell:   |\n", pad, ""); i < fill; ++i) printf("-"); printf("\n");
for (i = 0; i < ARRAYSIZE(paths); ++i)
{
    WCHAR buf[MAX_PATH], *p1 = NULL, *p2 = NULL;
    IShellItem*pSI;
    HRESULT hr = SHCreateItemFromParsingName(paths[i], NULL, IID_IShellItem, (void**) &pSI);
    if (SUCCEEDED(hr)) pSI->GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, &p1), pSI->Release();
    wsprintf(buf, L"shell:%s", paths[i]);
    HRESULT hr2 = SHCreateItemFromParsingName(buf, NULL, IID_IShellItem, (void**) &pSI);
    if (SUCCEEDED(hr2)) pSI->GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING, &p2), pSI->Release();
    wprintf(L"%-*s | %.8x | %.8x | %s\n", pad, paths[i], hr, hr2, p2 && *p2 ? p2 : p1 ? p1 : L"");
    CoTaskMemFree(p1), CoTaskMemFree(p2);
}

gives me this output:

                    | Original | shell:   |
-------------------------------------------
C:\                 | 00000000 | 80070003 | C:\
C:\Windows          | 00000000 | 80070003 | C:\Windows
                    | 00000000 | 80070003 | ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}
This PC             | 80070002 | 80070003 | 
MyComputerFolder    | 80070002 | 00000000 | ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}
Recycle Bin         | 80070002 | 80070003 | 
RecycleBinFolder    | 80070002 | 00000000 | ::{645FF040-5081-101B-9F08-00AA002F954E}
Libraries           | 80070002 | 00000000 | ::{031E4825-7B94-4DC3-B131-E946B44C8DD5}
OneDrive            | 80070002 | 80070003 | 
Libraries\Documents | 80070002 | 80070002 | 
Network             | 80070002 | 80070003 | 
NetworkPlacesFolder | 80070002 | 00000000 | ::{F02C1A0D-BE21-4350-88B0-7367FC96EF3C}
Startup             | 80070002 | 00000000 | C:\Users\Anders\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup

On Windows 8 SHCreateItemFromParsingName calls SHParseDisplayName (with STR_PARSE_AND_CREATE_ITEM and STR_PARSE_TRANSLATE_ALIASES) so even Microsoft have trouble separating parsing and display names in their API.

If you want to stay away from undocumented interfaces then you would have to add a third pass where you check the known folder display names. Or alternatively as Raymond Chen suggests in the comments; parse every path component manually against item display names in that IShellFolder.

like image 123
Anders Avatar answered Nov 20 '22 18:11

Anders


It would be interesting to debug Explorer and see how it does it.

begin from windows 7 shell use next undocumented interface (until it unchanged from win 7 up to latest win 10)

MIDL_INTERFACE("88DF9332-6ADB-4604-8218-508673EF7F8A") IShellUrl : public IUnknown
{
    virtual HRESULT STDMETHODCALLTYPE ParseFromOutsideSource(PCWSTR,DWORD);
    virtual HRESULT STDMETHODCALLTYPE GetUrl(PWSTR,DWORD);
    virtual HRESULT STDMETHODCALLTYPE SetUrl(PCWSTR,DWORD);
    virtual HRESULT STDMETHODCALLTYPE GetDisplayName(PWSTR,DWORD);
    virtual HRESULT STDMETHODCALLTYPE GetPidl(ITEMIDLIST_ABSOLUTE * *);
    virtual HRESULT STDMETHODCALLTYPE SetPidl(ITEMIDLIST_ABSOLUTE const *);
    virtual HRESULT STDMETHODCALLTYPE SetPidlAndArgs(ITEMIDLIST_ABSOLUTE const *,PCWSTR);
    virtual PWSTR STDMETHODCALLTYPE GetArgs();
    virtual HRESULT STDMETHODCALLTYPE AddPath(ITEMIDLIST_ABSOLUTE const *);
    virtual void STDMETHODCALLTYPE SetCancelObject(ICancelMethodCalls *);
    virtual HRESULT STDMETHODCALLTYPE StartAsyncPathParse(HWND,PCWSTR,DWORD,ICancelMethodCalls *);
    virtual HRESULT STDMETHODCALLTYPE GetParseResult();
    virtual HRESULT STDMETHODCALLTYPE SetRequestID(int);
    virtual HRESULT STDMETHODCALLTYPE GetRequestID(int *);
    virtual HRESULT STDMETHODCALLTYPE SetNavFlags(int,int);
    virtual HRESULT STDMETHODCALLTYPE GetNavFlags(long *);
    virtual HRESULT STDMETHODCALLTYPE Execute(struct IShellNavigationTarget *,int *,DWORD);
    virtual HRESULT STDMETHODCALLTYPE SetCurrentWorkingDir(ITEMIDLIST_ABSOLUTE const *);
    virtual void STDMETHODCALLTYPE SetMessageBoxParent(HWND);
    virtual HRESULT STDMETHODCALLTYPE GetPidlNoGenerate(ITEMIDLIST_ABSOLUTE * *);
    virtual DWORD STDMETHODCALLTYPE GetStandardParsingFlags(BOOL);
};

class DECLSPEC_UUID("4BEC2015-BFA1-42FA-9C0C-59431BBE880E") ShellUrl;

we can use it for parse display names like Recycle Bin, This PC, etc.. (IFileOpenDialog dialog use it)

we can use it synchronous or asynchronous. for synchronous need call

ParseFromOutsideSource(L"your name", flags = GetStandardParsingFlags(0))

if this call is ok, we can get and use ITEMIDLIST_ABSOLUTE* by call GetPidl (when no longer need free it by ILFree) also if file system path exist can get it by GetUrl otherwise original name returned.

also possible use asynchronous parsing - you need call StartAsyncPathParse - pass own hwnd and optional ICancelMethodCalls interface. when operation finished shell post RegisterWindowMessage(L"AC_ParseComplete") (wParam == IShellUrl*, lParam == 0) to your window. you can get final status by call GetParseResult() and if it ok - use GetPidl

code example for synchronous parsing

HRESULT ParsePath(PCWSTR path, IShellItem **ppsi)
{
    IShellUrl* pShUrl;

    HRESULT hr = CoCreateInstance(__uuidof(ShellUrl), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pShUrl));

    if (hr == S_OK)
    {
        if (SUCCEEDED(hr = pShUrl->ParseFromOutsideSource(path, pShUrl->GetStandardParsingFlags(TRUE))))
        {
            ITEMIDLIST_ABSOLUTE *pidl;

            if (SUCCEEDED(hr = pShUrl->GetPidl(&pidl)))
            {
                hr = SHCreateItemFromIDList(pidl, IID_PPV_ARGS(ppsi));

                //WCHAR sz[MAX_PATH];
                //if (SUCCEEDED(pShUrl->GetUrl(sz, RTL_NUMBER_OF(sz)))) DbgPrint(">%S\n", sz);

                ILFree(pidl);
            }
        }

        pShUrl->Release();
    }

    return hr;
}

void tt(PCWSTR path)
{
    IShellItem *psi;

    if (0 <= ParsePath(path, &psi))
    {
        PWSTR szName;

        if (S_OK == psi->GetDisplayName(SIGDN_NORMALDISPLAY, &szName))
        {
            DbgPrint("NORMALDISPLAY>%S\n", szName);
            CoTaskMemFree(szName);
        }

        if (S_OK == psi->GetDisplayName(SIGDN_FILESYSPATH, &szName))
        {
            DbgPrint("FILESYSPATH>%S\n", szName);
            CoTaskMemFree(szName);
        }

        psi->Release();
    }
}

void tt()
{
    if (0 <= CoInitialize(0))
    {
        tt(L"Recycle Bin");
        tt(L"Startup");
        CoUninitialize();
    }
}
like image 36
RbMm Avatar answered Nov 20 '22 19:11

RbMm