Why does tapping the Alt key cause my owner-draw static control to repaint?


A customer had an owner-drawn static control, and they found that when the user pressed the Alt key, their static control redrew. This extra redraw was unwanted, presumably because the control takes a long time to draw, and they didn't want to waste the time redrawing something that hadn't changed.

So why does tapping the Alt key cause the owner-draw static control to repaint?

Because the state of the keyboard accelerators has changed.

The customer noticed that on the initial draw, the itemState had the numeric value of 260, whereas the itemState is 4 on the unwanted redraw. The customer noted that 4 is documented as ODS_DISABLED, but 260 is not documented.

Okay, well, let's note that the value 260 is documented. It breaks down as 4 | 256 which is ODS_DISABLED | ODS_NO­ACCEL.

Let's walk through what's going on here.

When the static control first paints, the window that contains it is in the "hidden accelerators" state, so the draw flags are ODS_DISABLED | ODS_NO­ACCEL. The ODS_DISABLED is a bug, as we saw some time ago. The ODS_NO­ACCEL is telling you to do your owner-draw thing, but don't draw any accelerators.

When the user presses the Alt key, the window changes state to "visible accelerators", so the static control asks you to draw once more, with accelerators, as I discussed some time ago.

Okay, now that we know what's going on, what can we do to stop it?

Once you understand the way the WM_UPDATE­UI­STATE and WM_CHANGE­UI­STATE messages interact, you can see two ways out.

One is to ignore the request to draw the control if it is happening in response to a WM_UPDATE­UI­STATE message.

LRESULT CALLBACK StaticSubclassProc(
    HWND hwnd, UINT wm, WPARAM wParam, LPARAM lParam,
    UINT_PTR id, DWORD_PTR refData)
{
    LRESULT lres;
    ParentClass *parentClass = (ParentClass*)refData;
    switch (wm)
    {
    case WM_UPDATEUISTATE:
        parentClass->ignoreOwnerDraw = true;
        lres = DefSubclassProc(hwnd, wm, wParam, lParam);
        parentClass->ignoreOwnerDraw = false;
        return lres;
    }
    return DefSubclassProc(hwnd, wm, wParam, lParam);
}

class ParentClass
{
 ...
 void OnDrawItem(const DRAWITEMSTRUCT* pdis)
 {
  if (pdis->CtlID == IDC_MYSTATIC && !ignoreOwnerDraw) {
   DoSlowOwnerDraw(...);
  }
 }

 bool ignoreOwnerDraw = false;
}

The idea here is to set a flag if a WM_UPDATE­UI­STATE is in progress, and if the handling of the WM_UPDATE­UI­STATE message results in a request to redraw the control, then ignore it.

I leave as an exercise the code to install and remove the subclass procedure. I do this partly as an actual exercise, and partly to avoid me having to write two versions of the answer, depending on whether the parent is a regular window or a dialog box.

Update: As Adrian notes below, this algorithm fails if the static control chooses merely to invalidate in response to WM_UPDATE­STATE rather than repaint. By the time the WM_PAINT arrives, the flag would already be reset. Fortunately… read on.

Another solution is to prevent the static control from seeing the WM_UPDATE­UI­STATE message at all.

LRESULT CALLBACK IgnoreUIStateChangeSubclassProc(
    HWND hwnd, UINT wm, WPARAM wParam, LPARAM lParam,
    UINT_PTR id, DWORD_PTR /* refData */)
{
    switch (wm) {
    case WM_UPDATEUISTATE:
        return DefWindowProc(hwnd, wm, wParam, lParam);
    case WM_NCDESTROY:
        RemoveWindowSubclass(hwnd, IgnoreUIStateChangeSubclassProc, 0);
        break;
    }
    return DefSubclassProc(hwnd, wm, wParam, lParam);
}

BOOL IgnoreUIStateChange(HWND hwnd)
{
 return SetWindowSubclass(hwnd, IgnoreUIStateChangeSubclassProc,
                          1, 0);
}

I made this subclass procedure self-unregistering because it has no reference to any other objects, so there are no lifetime issues with letting the subclass procedure outlive the parent class. This makes the function self-contained and consequently generally useful. The Ignore­UI­State­Change function registers the subclass procedure on any control, at which point the control will ignore any changes to show or hide accelerators or focus rectangles.

The subclass procedure works by intercepting the WM_UPDATE­UI­STATE message and sending it directly to Def­Window­Proc for default processing, bypassing any custom processing in the control itself. Passing the message to Def­Window­Proc allows the normal message propagation to continue, but bypassing the control's window procedure means that the control is never told that the UI state has changed, which means that it never tries to redraw itself to hide or show accelerators or focus rectangles.

Comments (12)
  1. Medinoc says:

    Masking keyboard shortcuts sounds a lot like a concession to the “computers are scary!” crowd. More damningly, I think instead of making a computer easier to use, it does the exact opposite because it hampers visual discovery of what programs can do.

    1. Viila says:

      Nobody (to a first approximation) these days uses keyboard to navigate their programs. If you’re power user enough to want keyboard shortcuts, you’re power user enough to know to hit alt to see them.

      (Though, I am very glad that Microsoft has stuck with the principle that everything should be keyboard navigable. Last time I upgraded my computer the new motherboard had no USB2 ports, so I had to make do with just a PS/2 keyboard and no mouse to install Windows and get the Intel USB3 drivers onto it. I was almost thwarted at the last hurdle by the motherboard manufacturer’s repackaged custom installer that didn’t support keyboard navigation. Had to turn on the keyboard mouse emulation in Windows.)

      1. Not only is making software accessible simply the right thing to do, accessibility is also a requirement for any software sold to the U.S. government, so there’s that too.

        1. Erkin Alp Güney says:

          Do governments conduct usability tests to check accessibility?

  2. SimonRev says:

    I have always been in the camp of: If the control takes a long time to paint, cache the result of the last paint and just reuse it unless something changes. I suppose that is because I started doing custom drawing stuff back in the days when you got a bunch of WM_PAINTs due to dragging windows around the screen. I guess that with DWM that doesn’t happen quite so much anymore.

    1. parkrrrr says:

      Kids today. Back in the day, you didn’t get WM_PAINTs while dragging windows around the screen, because they were represented by a just a frame, and they didn’t redraw until you dropped them.

      And we liked it!

      1. alegr1 says:

        Same thing with DWM

      2. MarcK4096 says:

        Up hill! Both ways! :)

  3. Adrian says:

    Flipping a flag while processing WM_UPDATEUISTATE doesn’t seem robust. The result of the WM_UPDATEUISTATE could be an invalidation in which case, by the time the WM_PAINT message comes, your flag would be clear.

  4. IanBoyd says:

    They were likely wondering because whenever someone pressed the alt key their control flickered.

    1. SimonRev says:

      I don’t doubt that you may be correct. But if that is true, the correct answer is to double buffer your painting, not try to do some weird hack to avoid painting.

      1. smf says:

        It is an answer and on powerful desktops it may be an acceptable one. On low powered platforms (Windows CE/RT) then it may not be. You may not be able to afford the extra ram for all those controls.

Comments are closed.

Skip to main content