10
votes

In the Windows Volume Mixer, when your application plays sounds, it adds your application's icon and a custom volume slider to adjust volume specific to that application... nice! However, when you use a large-sized icon for your application (especially important in high-DPI when Windows scales your icons for the Taskbar, etc.), the icon in the Volume Mixer doesn't scale correctly. Specifically, the following code is what I use to set the application's icon:

// set icons the normal way
cWnd.SetIcon( theApp.LoadIcon( res_id ), FALSE );
cWnd.SetIcon( theApp.LoadIcon( res_id ), TRUE );

// set hi-res if available
OSVERSIONINFO osv;
osv.dwOSVersionInfoSize = sizeof( osv );
if ( GetVersionEx( &osv ) ) {
    // if we're Vista or more recent, use hi-def icons
    if ( osv.dwMajorVersion >= 6 ) {
        HICON hIcon = (HICON)::LoadImage( theApp.m_hInstance, MAKEINTRESOURCE( res_id ), IMAGE_ICON, 256, 256, LR_SHARED );
        if ( hIcon ) {
            cWnd.SetIcon( hIcon, TRUE );
        }
    }
}

The culprit is the "hi-res if available" part. If I include that, the Taskbar icon looks great but the Volume Mixer isn't scaled and looks terrible. If I exclude that, the Taskbar icon looks bad (terrible scaling) but the Volume Mixer at least is the right size:

Desktop Scaling 125% with 256x256 icon setDesktop Scaling 125% with regular icons

Has anyone found a solution that makes it so that BOTH icons look good?

EDIT: In my icon file, I have the following resolutions: 256x256, 48x48, 32x32, 24x24, and 16x16, all 32-bit. The 256x256 one is PNG compressed, the others are raw. All of the sizes look great at the resolutions they are in the file (I was trying to put the ICO here or in imgur but apparently neither allow icons). Additionally I have tried including some 8-bit images but that doesn't seem to change things.

EDIT: I'm using GetDeviceCaps( hdc, LOGPIXELSX ) (and Y) to determine the Desktop scaling. Normally desktop scaling is 100% and I get the normal 96 result. But more and more I'm seeing computers default to 125%. This can be changed via right-click Desktop, Personalize, other: Display... there's a slider there (requires log out/in for change).

EDIT: I also want to point out that the Tray ICON suffers a similar scaling issue fate when in high-DPI modes (that is, when using Shell_NotifyIcon). In this case, however, I'm able to use GetDeviceCaps( hdc, LOGPIXELSX ) to determine what Windows wants.. if I have the size, provide it directly, otherwise provide the 256x256 one and Windows does scale it correctly.

EDIT: Sadness ensues. This problem may be a Windows issue. While capturing images for demonstration purposes, I noticed the Volume Mixer icon itself looks poor. For comparison: Volume Mixer Comparison

FINAL EDIT: As described below, the workaround for the issue is to scale the icons. So, the final code that works is to load a pointer to the LoadIconWithScaleDown function from Comctl32.dll (not shown) and use that if it was available, or fall back to the "regular/old" way:

HICON hIcon = 0;
if ( FAILED( comctl32Loader.LoadIconWithScaleDown( theApp.m_hInstance, MAKEINTRESOURCE( res_id ), GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), &hIcon ) ) ) {
    hIcon = theApp.LoadIcon( res_id );
}
cWnd.SetIcon( hIcon, FALSE );
if ( FAILED( comctl32Loader.LoadIconWithScaleDown( theApp.m_hInstance, MAKEINTRESOURCE( res_id ), GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON), &hIcon ) ) ) {
    hIcon = theApp.LoadIcon( res_id );
}
cWnd.SetIcon( hIcon, TRUE );
3

3 Answers

3
votes

Yes, I can reproduce the problem you describe when I use the same code as shown in the question. As we've since determined, the issue only manifests itself at high DPI settings. At 100% scaling (~96 dpi), even on Windows Vista and later, you only need to use the "large" version of the icon (SM_CXICON and SM_CYICON; typically 32x32 pixels) for a window, not the 256x256 pixel version. That's what the apps bundled with Windows do, including the Volume Mixer applet that you're testing with.

The issue comes when you're using high DPI settings, which makes the "large" size go up:

╔════════════╦═════════════════╦═════════════════╗
║    DPI     ║ Large Icon Size ║ Small Icon Size ║
║            ║   (SM_C?ICON)   ║  (SM_C?SMICON)  ║
╠════════════╬═════════════════╬═════════════════╣
║  96 (100%) ║     32x32       ║     16x16       ║
║ 120 (125%) ║     40x40       ║     20x20       ║
║ 144 (150%) ║     48x48       ║     24x24       ║
║ 192 (200%) ║     64x64       ║     32x32       ║
╚════════════╩═════════════════╩═════════════════╝

Things work fine regardless of DPI when you load the 256x256 pixel icon, because Windows is automatically scaling it down to the required size. That generates a much better quality icon (without all the jaggies and other artifacts) than attempting to scale up a 32x32 pixel icon. So your guess is correct, the problem is indeed related to scaling.

I'm going to assume that what you're seeing in the Volume Mixer applet when you use a 256x256 pixel icon is a bug—it should be scaling that large icon down to the size it expects, which is a "large" icon (SM_C?ICON). Presumably, it's calling the DrawIconEx function with the cxWidth and cxHeight parameters both set to 0 and not passing the DI_DEFAULTSIZE flag. That's causing the icon to be drawing using its actual size—huge.

You'll have to work around the problem manually, by scaling the icons yourself. Fortunately, Windows Vista introduces a number of functions that are designed explicitly for this purpose. The easiest one to use in this case is LoadIconWithScaleDown. Like the name suggests, it works similarly to the older LoadIcon/LoadImage functions, but rather than scaling up an icon that is too small, it scales down a larger icon—perfect for when you have a giant, high-quality 256x256 pixel icon in your ICO file.

Unfortunately, these functions are not available on older versions of Windows, which will likely have the same problem when used at higher DPI settings. You'll need to find alternatives there, or just settle for jagged, scaled icons on these older OSes.

Sample code:

#include <CommCtrl.h>                  // include Common Controls header
#pragma comment(lib, "comctl32.lib")   // link to Common Controls library

// Embed a standard manifest to use Common Controls v6
#pragma comment(linker, "/manifestdependency:\"type='win32' "                         \
                        "name='Microsoft.Windows.Common-Controls' version='6.0.0.0' " \
                        "processorArchitecture='*' "                                  \
                        "publicKeyToken='6595b64144ccf1df' "                          \
                        "language='*'\"")
// Load and set "large" icon (typically 32x32 pixels, but not necessarily)
HICON hIconLg;
if (SUCCEEDED(LoadIconWithScaleDown(g_hInstance,
                                    MAKEINTRESOURCE(IDI_ICON),
                                    GetSystemMetrics(SM_CXICON),
                                    GetSystemMetrics(SM_CYICON),
                                    &hIconLg)))
{
   SendMessage(hWnd, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(hIconLg));
}

// Load and set "small" icon (typically 16x16 pixels, but not necessarily)
HICON hIconSm;
if (SUCCEEDED(LoadIconWithScaleDown(g_hInstance,
                                    MAKEINTRESOURCE(IDI_ICON),
                                    GetSystemMetrics(SM_CXSMICON),
                                    GetSystemMetrics(SM_CYSMICON),
                                    &hIconSm)))
{
   SendMessage(hWnd, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(hIconSm));
}

Note that to use these new functions, you will need to link to version 6 of the common controls library. That requires that you instruct the compiler to link in comctl32.lib and embed a manifest in your application. Either can be done using the MSVC-specific #pragmas shown in the sample code above, or configured in your project's properties. If you fail to do either of these things, you'll get a link-time error or an "ordinal not found" error when you first launch the app.

1
votes

I've encountered a similar problem with a C#/WPF program.

In my case, the problem seemed to have caused by missing sizes inside the ico file. The smallest size my app's ico file was 64x64. Once I added smaller sizes inside that file, problem was solved.

0
votes

Thank you guys very much for this. I got this to work in our Wx app, if anyone wants some pre-baked code to drop into their Wx app here is mine below:

#ifdef __WXMSW__
#include <Windows.h>
#include <CommCtrl.h>
#include <wx/msw/private.h>
typedef int (WINAPI *func_LoadIconWithScaleDown)(HINSTANCE, LPCWSTR, int, int, HICON*);
#endif

void MainFrame::BindAppIcon() {
#ifdef __WXMSW__
    wxDynamicLibrary comctl32("comctl32", wxDL_DEFAULT | wxDL_QUIET);
    func_LoadIconWithScaleDown load_icon_scaled = reinterpret_cast<func_LoadIconWithScaleDown>(comctl32.GetSymbol("LoadIconWithScaleDown"));
    int icon_set_count = 0;

    HICON hIconLg;
    if (load_icon_scaled && SUCCEEDED(load_icon_scaled(wxGetInstance(), _T("AAAAA_MAINICON"), ::GetSystemMetrics(SM_CXICON), ::GetSystemMetrics(SM_CYICON), &hIconLg))) {
        ::SendMessage(GetHandle(), WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(hIconLg));
        ++icon_set_count;
    }
    HICON hIconSm;
    if (load_icon_scaled && SUCCEEDED(load_icon_scaled(wxGetInstance(), _T("AAAAA_MAINICON"), ::GetSystemMetrics(SM_CXSMICON), ::GetSystemMetrics(SM_CYSMICON), &hIconSm))) {
        ::SendMessage(GetHandle(), WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(hIconSm));
        ++icon_set_count;
    }

    if (icon_set_count == 2) return;
    // otherwise fall back to Wx method of setting icon
#endif
    wxIcon icon = wxXmlResource::Get()->LoadIcon(wxT("MainIcon"));

    if (!icon.IsOk()) {
        wxLogInfo(_("Main icon not found"));
        icon = wxICON(wxvbam);
    }

    SetIcon(icon);
}

The first line in your app.rc would then be something like this:

AAAAA_MAINICON ICON "icons/app.ico"