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

Raymond Chen

Raymond

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.

Raymond Chen
Raymond Chen

Follow Raymond