c++windowsshell-namespace-extension

Implement IExtractIcon To Get Yellow Folder Icon And Extension Icon Of Virtual File


I am having the exact same problem as the engineer referenced in this link.

I was quite surprised at t he solution given by Raymond (Chen) because Raymond's answer seems (to me) to be circuitous: He very accurately restates the problem that the engineer is having, then immediately proceeds to not show him how to fix it. Raymond's answer essentially says: "In order for you to provide an icon by leveraging the Windows system, you should first write code that provides the icon, then call the Windows system against your code. [If I am wrong, please someone show me.]

I will restate the problem again clearly:

  1. I am implementing IShellFolder::GetUIObjectOf for riid == IExtractIcon.

  2. I have items in my namespace that are virtual and have a 1-1 correspondence between directories and files.

  3. IfGetUIObjectOf is invoked against one of my "directories", I would like to provide an IExtractIcon object that would yield the standard yellow folder icon.

  4. If GetUIObjectOf is invoked against one of my "files", I would like to provide an IExtractIcon object that would yield the same icon that would have been yielded by a real file, according to the extension (.txt, .jpg, etc.) of my virtual item.

I have read the documentation for IExtractIcon no less than 18 times, end to end. My code according to my understanding of the documentation is not working. I have fiddled with it during those 18 times, and each time, it does not do what I expect.

My approach for GetIconLocation was to use whatever lightweight method was available from Windows for "directories", and use AssocQueryString to get filename/index pair for "files". The lightest-weight standard extraction method that I could think-of for "directories" was SHGetFileInfo SHGFI_USEFILEATTRIBUTES

My approach for Extract was to return whatever I need for "directories" to cause the shell to do minimum work, and return S_FALSE for "files" to tell the shell to go ahead and do the extraction for the file path/icon index combo.

I have not tested "files" yet, because I am still stuck on "directories".

In particular, for "directories", I can see from the debugger that the shell is not happy with whatever method I use, either because it cannot find some file that is clearly associated with IExtractIcon invocation, or, if I prevent it from referencing files via one of my return values, it simply refuses to draw the icon, which is the icon on one of the tabs in Windows Explorer 11.

It seems that this should be a straightforward task, especially for the yellow folder icon that denotes my "directories".

What's the right way to do this for "directories" and "files"?

Current Code:

STDMETHODIMP CExtractIcon::GetIconLocation (
    UINT uFlags,
    LPTSTR szIconFile,
    UINT cchMax,
    LPINT piIndex,
    UINT *puFlags)
{
    HRESULT hr = E_FAIL;

    if (descriptor.flags.is_a_directory)
    {
        SHFILEINFO sfi = { 0 };

        if (SHGetFileInfo(
            TEXT("DummyFolderPath"),
            FILE_ATTRIBUTE_DIRECTORY,
            &sfi,
            sizeof(sfi),
            SHGFI_USEFILEATTRIBUTES | SHGFI_SYSICONINDEX | SHGFI_ICON | SHGFI_SMALLICON | (uFlags & GIL_OPENICON ? SHGFI_OPENICON : 0))
            )
            this->hIconSmall = sfi.hIcon;

        if (SHGetFileInfo(
            TEXT("DummyFolderPath"),
            FILE_ATTRIBUTE_DIRECTORY,
            &sfi,
            sizeof(sfi),
            SHGFI_USEFILEATTRIBUTES | SHGFI_SYSICONINDEX | SHGFI_ICON //  (uFlags & GIL_OPENICON ? SHGFI_OPENICON : 0)) DEFECT: Might work for large folders too.
            ))
            this->hIconLarge = sfi.hIcon;

        *piIndex = sfi.iIcon;

        *puFlags = GIL_NOTFILENAME;

        hr = S_FALSE;
    }
    else
    {
        DWORD cchOut = cchMax;

        hr = AssocQueryString(
            ASSOCF_NONE,
            ASSOCSTR_DEFAULTICON,
            descriptor.name.locate_reverse('.') ? descriptor.name.indexed_suffix().dump<TCHAR>() : TEXT(""),
            NULL,
            szIconFile,
            &cchOut);
    }
    return hr;
}

STDMETHODIMP CExtractIcon::Extract(LPCTSTR pszRoot,
    UINT nIconIndex,
    HICON *phiconLarge,
    HICON *phiconSmall,
    UINT nIconSize)
{
    *phiconSmall = this->hIconSmall;
    *phiconLarge = this->hIconLarge;

    return S_OK;
}

Solution

  • AssocQueryString at least gives valid file path/index combinations for "directories" and "files", per the code below. This code also eliminated the exceptions being thrown by the Shell on Windows 11. I return S_FALSE in Extract, per the documentation for IExtractIcon, which is how the Shell is told that it should extract the icon from the path/index combination itself, instead of our doing it.

    Note the TEXT("Folder"). Folder is a special type of ProgID. Since folders do not typically have meaningful extensions like .txt or .jpg, the shell needs a kind of catch-all entry for them, so that when a programmer asks for the same information for a "directory" that it would for a "file", the shell will be able to provide something using the same search algorithm of the registry that it would for a "file".

    STDMETHODIMP CExtractIcon::GetIconLocation (
        UINT uFlags,
        LPTSTR szIconFile,
        UINT cchMax,
        LPINT piIndex,
        UINT *puFlags)
    {
        DWORD cchOut = cchMax;
    
        HRESULT hr = AssocQueryString(
            ASSOCF_NONE,
            ASSOCSTR_DEFAULTICON,
            descriptor.flags.is_a_directory ? TEXT("Folder") : descriptor.name.locate_reverse('.') ? descriptor.name.indexed_suffix().dump<TCHAR>() : TEXT(""),
            NULL,
            szIconFile,
            &cchOut);
    
        if (SUCCEEDED(hr))
        {
            hr = E_FAIL;
    
            auto i = wcslen(szIconFile);
    
            while (i--)
                if (szIconFile[i] == ',')
                {
                    if (1 == swscanf(&szIconFile[i + 1], L"%d", piIndex)) // ... gets icon index into *piIndex.
                    {
                        szIconFile[i] = 0; // ... puts 0 where comma (,) is located to truncate to a valid file path.           
                        hr = S_OK;
                    }
                    break;
                }
        }
    
        return hr;
    }
    
    STDMETHODIMP CExtractIcon::Extract(LPCTSTR pszRoot,
        UINT nIconIndex,
        HICON *phiconLarge,
        HICON *phiconSmall,
        UINT nIconSize)
    {
        return S_FALSE;
    }