add file icon overlay for jumbo icons with WINAPI

Viewed 273

I need to get large "jumbo" file icons (256x256 px) with overlay (for example link icon for shortcuts), basically the same as can be seen in Windows File Explorer when you set to view items in extra large icons.

For example I have a shortcut "a.txt.lnk" to a text file, which has this icon in File Explorer. This is what I want to get (yes, the arrow in the bottom left corner is what I mean by overlay):

enter image description here

I googled really hard to find out how to do this.

To experiment with the code I used iconextractor example in QtWinExtras library (in sources of Qt framework). Unfortunately it has some issues and does not work well. All I managed to do was to obtain either small icons (32x32 px) with shortcut overlay or "jumbo" icons without overlay. This:

enter image description here

or this:

enter image description here

I simplified the code from the Qt example to this main.cpp:

#include <QtWin>

#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QGuiApplication>
#include <QImage>
#include <QPixmap>

#include <comdef.h>
#include <CommCtrl.h>
#include <commoncontrols.h>

struct PixmapEntry {
    QString name;
    QPixmap pixmap;
};

using PixmapEntryList = QVector<PixmapEntry>;

static QString formatSize(const QSize &size)
{
    return QString::number(size.width()) + u'x' + QString::number(size.height());
}

static QPixmap pixmapFromShellImageList(int iImageList, const SHFILEINFO &info)
{
    QPixmap result;
    static const IID iID_IImageList = {0x46eb5926, 0x582e, 0x4017, {0x9f, 0xdf, 0xe8, 0x99, 0x8d, 0xaa, 0x9, 0x50}};

    IImageList *imageList = nullptr;
    if (FAILED(SHGetImageList(iImageList, iID_IImageList, reinterpret_cast<void **>(&imageList))))
        return result;

    HICON hIcon = nullptr;

    // NOTE: the following was missing in original Qt code 
    // and therefore returned no jumbo icons at all when 
    // overlays were requested. 
    // Highest 8 bits are reverved for overlay index.
    int iconIndex = info.iIcon & 0xFFFFFF; 

    if (SUCCEEDED(imageList->GetIcon(iconIndex, ILD_TRANSPARENT, &hIcon))) {
        result = QtWin::fromHICON(hIcon);
        DestroyIcon(hIcon);
    }
    return result;
}

static PixmapEntryList extractShellIcons(const QString &sourceFile, bool addOverlays)
{
    enum { // Shell image list ids
        sHIL_EXTRALARGE = 0x2, // 48x48 or user-defined
        sHIL_JUMBO = 0x4 // 256x256 (Vista or later)
    };

    struct FlagEntry {
        QString name;
        unsigned flags;
    };

    const QString nativeName = QDir::toNativeSeparators(sourceFile);
    const auto *sourceFileC = reinterpret_cast<const wchar_t *>(nativeName.utf16());

    SHFILEINFO info;
    unsigned int flags = SHGFI_ICON | SHGFI_SYSICONINDEX | SHGFI_SHELLICONSIZE;
    if (addOverlays)
        flags |= SHGFI_ADDOVERLAYS | SHGFI_OVERLAYINDEX; // What do these flags do? How to utilize the overlay index?

    PixmapEntryList result;

    const QString prefix = "sh_";
    ZeroMemory(&info, sizeof(SHFILEINFO));
    SHGetFileInfo(sourceFileC, 0, &info, sizeof(SHFILEINFO), flags);

    // extract standard icon
    PixmapEntry entry;
    entry.pixmap = QtWin::fromHICON(info.hIcon);
    DestroyIcon(info.hIcon);
    if (entry.pixmap.isNull()) {
        qWarning() << "Error converting icons.";
        return PixmapEntryList();
    }
    entry.name = prefix + formatSize(entry.pixmap.size());

    const int iconIndex = info.iIcon & 0xFFFFFF;
    const int overlayIconIndex = info.iIcon >> 24; // Note: some resources suggest there should be -1 to adjust for 0-based index. Really?

    qDebug() << "Obtained icon #" << iconIndex << "overlay #" << overlayIconIndex;

    result.append(entry);

    // extract jumbo icon
    const QPixmap jumbo = pixmapFromShellImageList(sHIL_JUMBO, info);
    if (!jumbo.isNull()) {
        PixmapEntry entry;
        entry.pixmap = jumbo;
        entry.name = QLatin1String("jumbo_") + formatSize(jumbo.size());
        result.append(entry);
    }

    return result;
}

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    const QString &sourceFile = "C:/my/path/to/some/link/a.txt.lnk";
    QString imageFileRoot = QDir::currentPath() + u'/';

    // Note: Settings true or false in the following line has no effect of overlays.
    // Small icons have always overlays, jumbo icons have never overlays.
    const PixmapEntryList pixmaps = extractShellIcons(sourceFile, true);

    for (const auto &entry : pixmaps) {
        const QString fileName = imageFileRoot + entry.name + QLatin1String(".png");
        entry.pixmap.save(fileName);
        qDebug() << "Wrote " << QDir::toNativeSeparators(fileName);
    }
    return 0;
}

and iconextractor.pro:

TEMPLATE = app
TARGET = iconextractor
CONFIG += console
QT = core gui winextras
LIBS += -lshell32 -luser32
SOURCES += main.cpp

My problem is that I do not understand what is this line flags |= SHGFI_ADDOVERLAYS | SHGFI_OVERLAYINDEX; good for. It has no effect on standard icons (they contain overlay even without this line) and has no effect on jumbo icons (they do not contain overlay anyway). ObviouslyI can get icon overlay index but I have no idea what I should do with it. How to get the jumbo sized icon with overlay?

1 Answers

I think I have made a breakthrough. The key is to transform the highest 8 bits from the icon index to overlay icon index using IImageList::GetOverlayImage and with the actual index retrieve it as icon (the same way as the base icon). So this is the changed function which loads base icon, overlay icon and draws the overlay icon into base icon.

static QPixmap pixmapFromShellImageList(int iImageList, const SHFILEINFO &info)
{
    QPixmap result;
    static const IID iID_IImageList = {0x46eb5926, 0x582e, 0x4017, {0x9f, 0xdf, 0xe8, 0x99, 0x8d, 0xaa, 0x9, 0x50}};

    IImageList *imageList = nullptr;
    if (FAILED(SHGetImageList(iImageList, iID_IImageList, reinterpret_cast<void **>(&imageList))))
        return result;

    HICON hIcon = nullptr;
    int iconIndex = info.iIcon & 0xFFFFFF;

    if (FAILED(imageList->GetIcon(iconIndex, ILD_TRANSPARENT, &hIcon))) {
        return result;
    }

    result = QtWin::fromHICON(hIcon);
    DestroyIcon(hIcon);

    int overlayIndex;
    if (FAILED(imageList->GetOverlayImage(info.iIcon >> 24, &overlayIndex)))
    {
        return result;
    }

    if (FAILED(imageList->GetIcon(overlayIndex, ILD_TRANSPARENT, &hIcon)))
    {
        return result;
    }

    QPixmap overlay = QtWin::fromHICON(hIcon);
    DestroyIcon(hIcon);

    // TODO: better placement and resizing of overlay within the icon
    QPainter painter(&result);
    painter.setRenderHint(QPainter::SmoothPixmapTransform);
    int w = overlay.width() / 2;
    int h = overlay.height() / 2;
    painter.drawPixmap(0, result.height() - h, w, h, overlay);

    return result;
}

The overlay is a bit too big, but this is OK, I can tweak the code to make it smaller and less arbitrarily placed. (maybe with IImageList::GetImageRect) This is my current result which I am quite happy with:

enter image description here

Related