The mystery of the icon that never appears


A customer reported a problem showing an icon on their dialog box.

We verified that this code does execute during the handling of the WM_INIT­DIALOG message. No assertion fires, yet no icon appears either.

SHFILEINFO sfi = { 0 };
DWORD_PTR dwResult = SHGetFileInfo(m_pszFile, &sfi,
                                   sizeof(sfi), SHGFI_ICON);
assert(dwResult != 0);

m_hico = sfi.hIcon;
assert(m_hico != nullptr);

assert(GetDlgItem(hdlg, IDI_CUSTOMICON) != nullptr);
SendDlgItemMessage(hdlg, IDI_CUSTOMICON,
                   WM_SETICON, ICON_BIG, (LPARAM)m_hico);
assert(SendDlgItemMessage(hdlg, IDI_CUSTOMICON,
                   WM_GETICON, ICON_BIG, 0) == (LPARAM)m_hico);

Our dialog template says

  ICON "", IDI_CUSTOMICON, 10, 10, 0, 0

The customer did some helpful preliminary troubleshooting:

  • Verify that the code does indeed execute. It sounds obvious, but some people forget to check this. They get distracted trying to figure out why a function isn't working, when in fact the root cause is that you forgot to call the function in the first place.

  • Verify that the SHGet­File­Info call succeeded. That rules out the case that the static control is displaying nothing because you didn't give it anything to display.

  • Verify via Get­Dlg­Item that the control you're trying to talk to really does exist. That rules out the case that you are talking to an empty room. (For example, maybe you added the control to the wrong template.)

  • Verify via WM_GET­ICON that the attempt to change the icon really worked.

The problem is that the customer is using the wrong icon-setting message.

The WM_SET­ICON message lets you customize the icon that is displayed in the window's caption bar. For this to have any effect, your window naturally needs to have the WS_CAPTION style. If you don't have a caption, then telling the window manager, "Please display this icon in my caption" is mostly a waste of time. It's like signing up for a lawn-mowing service when you don't have a lawn.

The message to change the icon displayed inside a static control is STM_SET­ICON.

SendDlgItemMessage(hdlg, IDI_CUSTOMICON,
                   STM_SETICON, (LPARAM)m_hico, 0);

Red herring: Some of you may have noticed that the customer set their control size to 0×0. "You aren't seeing an icon because you set the control to zero size!" But since this control was not created with SS_REAL­SIZE­CONTROL or SS_CENTER­IMAGE, the control will resize itself to match the size of the icon.

Here's a sample program to show both types of icons set on the same window, so you can see the difference.

#include <windows.h>
#include <commctrl.h>

LRESULT CALLBACK SubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam,
    LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
{
 switch (uMsg) {
 case WM_NCDESTROY:
  RemoveWindowSubclass(hwnd, SubclassProc, 0);
  PostQuitMessage(0);
  break;
 }
 return DefSubclassProc(hwnd, uMsg, wParam, lParam);
}

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev,
                   PSTR lpCmdLine, int nShowCmd)
{
 HWND hwnd = CreateWindow("static", nullptr,
               WS_OVERLAPPEDWINDOW | WS_VISIBLE |
               SS_ICON | SS_CENTERIMAGE,
               CW_USEDEFAULT, CW_USEDEFAULT,
               CW_USEDEFAULT, CW_USEDEFAULT,
               nullptr, nullptr, hinst, nullptr);
 SetWindowSubclass(hwnd, SubclassProc, 0, 0);

 HICON hicoCaption = LoadIcon(nullptr, IDI_EXCLAMATION)
 SendMessage(hwnd, WM_SETICON, ICON_BIG,
             reinterpret_cast<LPARAM>(hicoCaption));

 HICON hicoClient = LoadIcon(nullptr, IDI_QUESTION);
 SendMessage(hwnd, STM_SETICON,
             reinterpret_cast<LPARAM>(hicoClient), 0);

 MSG msg;
 while (GetMessage(&msg, NULL, 0, 0)) {
  TranslateMessage(&msg);
  DispatchMessage(&msg);
 }

 DestroyIcon(hicoClient);
 DestroyIcon(hicoCaption);

 return 0;
}

We create a top-level static window, which is highly unusual, since static controls are nearly always children of some other window. I'm doing this specifically to show the two different icons. You don't want to do this in a real program.

The static control has the SS_ICON style, because we want it to display an icon, and the SS_CENTER­IMAGE style, because we just want it to center the icon in its client area without resizing. (We will control the size.)

We subclass the window so that we can post a quit message to exit the program when the window is destroyed, which the user can do by pressing Alt+F4. (Hey, this is just a demo program. Catching clicks on the × button is just extra code that will distract from the purpose of the demonstration. Heck, this entire subclass thing is already distracting from the purpose of the demonstration!)

We load up two icons, an exclamation point, which we set as our caption icon, and a question mark, which we put in our client area. (We could have used the Static_Set­Icon macro in windowsx.h to send the STM_SET­ICON message, but I did it manually just to make the message explicit.)

Run the program, and there you can see the two different types of icons: The exclamation point goes in the caption, and the question mark goes in the client area.

Comments (22)
  1. Joshua says:

    Ability to perform basic diagnostics is quite refreshing after all those that couldn't.

  2. Paul Z says:

    Yeah, I'm prepared to forgive the customer for this one. Icon handling is confusing and using the wrong "set the icon" message seems like a pretty simple mistake to make. And their attempts to figure out what went wrong are quite impressive, especially compared to the average customer stories on this blog. If I were debugging that, it would not occur to me to check the actual message; I would just say to myself "hey, it says SETICON, it must mean set the icon. No worries there."

  3. Medinoc says:

    I'm surprised it doesn't react to a click on the ×, while an ordinary windows does. That means the controls goes out of its way not to react, right?

  4. John Doe says:

    In this example, the main difference between using SetWindowSubclass vs SetWindowLongPtr with GWLP_WNDPROC is that with the former you just call DefSubclassProc, while with the latter you'd have to store the previous handler by calling GetWindowLongPtr with GWLP_WNDPROC and call it in the new handler.

    A feature of SetWindowSubclass is that the provided SUBCLASSPROC is called with the provided uIdSubclass and the dwRefData, but those are not being used in this example.  At least uIdSubclass should be used in the call to RemoveWindowSubclass.

    Also, it's very strange to call RemoveWindowSubclass within the same SUBCLASSPROC.  Will DefSubclassProc know how to deal with that, e.g. following a copy of the list of subclasses, made before the top SUBCLASSPROC was called?

  5. Rick C says:

    2 typos:  no semicolon after the first call to LoadIcon, and the second DestroyIcon references hicoCaptionSm (which doesn't exist) instead of hicoCaption.

  6. morlamweb says:

    After years of reading posts where devs make some of the silliest programming mistakes, and not even perform the most basic troubleshooting techniques, it is refreshing to see a post featuring a competent customer.

  7. Mason Wheeler says:

    @morlamweb: IMO that's the first principle of debugging: Always verify the obvious, basic things first.  To err is human, and it's always surprising (and slightly embarrassing) to see how often this finds the problem.

  8. > WinMain

    > CreateWindow("

    We should probably start using UNICODE functions even in scratch programs, at least until the default ANSI code page is UTF-8.</pedant>

  9. Timothy Byrd (ETAP) says:

    Maurits: Can I set the code page to UTF-8? Though it probably wouldn't help my situation. We are stuck supporting an ancient MFC grid control that is used in a hundred paces in our software and though our software is built with UNICODE, I can't put non-code-page text into it. (Someone even figured out a trick to make it usable in our 64-bit build.)

  10. Joshua says:

    I successfully set OEMCP to UTF8 and it worked quite well. The editor can't handle it though so don't try to paste out of 7 bit.

  11. Paul says:

    In order to execute this code from Visual-Studio I created an empty Win32 project, changed the lines as suggested by Rick C and added Comctl32.lib to the linker.

  12. Neil says:

    I used this trick to provide a basic text viewer sample application; it created a readonly edit control as a top-level window. However rather than subclassing the window to notice its destruction I just used IsWindow(hwnd) as my loop test. Note that as @Medinoc suggested I didn't have to add any code to handle closing the window since the existing default syscommand handling took care of all that.

  13. voo says:

    @Paul Z: Yeah I wouldn't even speak of "forgiving". Everybody makes mistakes and they spent a large amount of work trying to figure out the problem and narrowing it down as much as they could before asking for help. That's pretty much model student behavior – what could one ask more for?

    If everybody was like this, support calls would be way rarer – sadly as Ray's usual posts demonstrate almost nobody goes to that effort :(

  14. Alex Cohn says:

    I also don't understand why subclassing is necessary. Would this not be enough?

    MSG msg;

    while (GetMessage(&msg, NULL, 0, 0)) {

     if (msg.message == WM_NCDESTROY) PostQuitMessage(0);

     TranslateMessage(&msg);

     DispatchMessage(&msg);

    }

    [Try it and see. -Raymond]
  15. Dominic says:

    "It sounds obvious, but some people forget to check this."

    Worse, they lock themselves into not checking because they already did and "I didn't change anything." e.g. my own experience at Microsoft at 2007 when an IInternetSecurityManager I had created just stopped working the day after I had tested it and checked it in. After a week of increasingly voodoo-driven investigation, and a desperate outreach to the IE team, I finally noticed that another dev had clobbered my OLE siting code in a merge.

  16. John Doe says:

    @Alex Cohn, the WM_NCDESTROY message is sent, not posted, so GetMessage will not return it.  You have to somehow have a window proc there.

    I was just ranting that SetWindowSubclass and friends are not really necessary in this case (read these 3 words again with care).  By using it, this otherwise little program that just uses a static control as a top-level window now requires comctl32.

    But I guess Raymond actually wishes the best for people that simply copy-paste his code snippets.  Indeed, it's a much better way of subclassing.

    I haven't tested if calling RemoveWindowSubclass is safe within the subclass's proc, but I guess it is.

    Oh look, he already blogged about it a long time ago, how awesome is that? blogs.msdn.com/…/55653.aspx

  17. Alex Cohn says:

    @John Doe, thanks — my Win32 got rusty in the last few years, but I took Raymond's challenge and installed MSVC Express on an ancient Windows laptop running some unsupported version of OS.

    True, WM_NCDESTROY does not happen without subclassing. But I found that even without WM_NCDESTROY, the following code simply works, without comctl32.lib and without SetWindowSubclass(). Am I missing simething?

    MSG msg;

    while (GetMessage(&msg, NULL, 0, 0)) {

     if (msg.message == WM_SYSCOMMAND && msg.wParam == SC_CLOSE)

       PostQuitMessage(0);

     TranslateMessage(&msg);

     DispatchMessage(&msg);

    }

  18. Sam Steele says:

    @Alex Cohn.

    If you call the PostQuitMessage function in response to WM_SYSCOMMAND with wParam == SC_CLOSE,

    then your application will miss out on getting its last few messages.

    These will include WM_CLOSE, WM_DESTROY and WM_NCDESTROY.

    Whether or not this will be a problem will depend upon the design of your application.

    [It also misses the cases where the window is destroyed by some means other than the user selecting "Close" from the System menu. -Raymond]
  19. Alex Cohn says:

    @Sam Steele: you are definitely correct in general case; but in the specific case of an app that wraps around a standard control, there doesn't seem to be a problem with that. At least, my debugger shows that WM_NCDESTROY in SubclassProc() has a direct stack trace to message loop with message == WM_SYSCOMMAND && wParam == SC_CLOSE. And I found no other way to close this tiny app, but for Alt-F4 or Alt-Space + &Close. Am I missing something?

    [DestroyWindow is another way to destroy a window. -Raymond]
  20. Alex Cohn says:

    I could not find a way to DestroyWindow() within this program. But I found a minimal change that shows the beauty of subclassing. For a reason I don't completely understand, when I change the "static" class to "edit", the program does catch clicks on the × button, but nothing except WM_NCLBUTTONDOWN arrives to the message loop. Now you either must check the mouse location and map it to the × button, or subclass and see it work all by itself.

    [Right now, the program doesn't use DestroyWindow, but somebody might incorporate it into a program that does. -Raymond]
  21. Alex Cohn says:

    But why do static and edit windows handle the × button differently? Differently means (if I interpret the evidence correctly): "static" does not handle the caption bar buttons, while "edit" does handle them?

  22. John Doe says:

    @Alex Cohn, I'm conjecturing here: maybe because the edit control was made to be a top-level window by itself, e.g. a very, very simple notepad without a status bar.

    The static control, and probably checkboxes, radio buttons, push buttons and such, probably weren't, IMO following the line of thought that they're only useful as child windows, or rather distasteful as top-level windows.

Comments are closed.

Skip to main content