Larry Osterman's WebLog

Confessions of an Old Fogey
Blog - Title

Building a flicker free volume control

Building a flicker free volume control

Rate This
  • Comments 30

When we shipped Windows Vista, one of the really annoying UI annoyances with the volume control was that whenever you resized it, it would flicker. 

To be more specific, the right side of the control would flicker – the rest didn’t flicker (which was rather strange).

 

Between the Win7 PDC release (what we called M3 internally) and the Win7 Beta, I decided to bit the bullet and see if I could fix the flicker.  It seems like I tried everything to make the flickering go away but I wasn’t able to do it until I ran into the WM_PRINTCLIENT message which allowed me to direct all of the internal controls on the window to paint themselves.

Basically on a paint call, I’d take the paint DC and send a WM_PRINTCLIENT message to each of the controls in sndvol asking them each to paint themselves to the new DC.  This worked almost perfectly – I was finally able to build a flicker free version of the UI.  The UI wasn’t perfect (for instance the animations that faded in the “flat buttons” didn’t fire) but the UI worked just fine and looked great so I was happy that' I’d finally nailed the problem.  That happiness lasted until I got a bug report in that I simply couldn’t figure out.  It seems that if you launched the volume mixer, set the focus to another application then selected the volume mixer’s title bar and moved the mixer, there were a ton of drawing artifacts left on the screen.

I dug into it a bunch and was stumped.  It appeared that the clipping rectangle sent in the WM_PAINT message to the top level message didn’t include the entire window, thus portions of the window weren’t erased.  I worked on this for a couple of days trying to figure out what was going wrong and I finally asked for help on one of our internal mailing lists.

The first response I got was that I shouldn’t use WM_PRINTCLIENT because it was going to cause me difficulty.  I’d already come to that conclusion – by trying to control every aspect of the drawing experience for my app, I was essentially working against the window manager – that’s why the repaint problem was happening.  By calling WM_PRINTCLIENT I was essentially putting a band-aid on the real problem but I hadn’t solved the real problem, all I’d done is to hide it.

 

So I had to go back to the drawing board.  Eventually (with the help of one of the developers on the User team) I finally tracked down the original root cause of the problem and it turns out that the root cause was somewhere totally unexpected.

Consider the volume UI:

image

The UI is composed of two major areas: The “Devices” group and the “Applications” group.  There’s a group box control wrapped around the two areas.

Now lets look at the group box control.  For reasons that are buried deep in the early history of Windows, a group box is actually a form of the “button” control.  If you look at the window styles for a button in SpyXX, you’ll see:

image

 

Notice the CS_VREDRAW and CS_HREDRAW window class styles.  The MSDN documentation for class styles says:

CS_HREDRAW - Redraws the entire window if a movement or size adjustment changes the width of the client area.
CS_VREDRAW - Redraws the entire window if a movement or size adjustment changes the height of the client area.

In other words every window class with the CS_HREDRAW or CS_VREDRAW style will always be fully repainted whenever the window is resized (including all the controls inside the window).  And ALL buttons have these styles.  That means that whenever you resize any buttons, they’re going to flicker, and so will all of the content that lives below the button.  For most buttons this isn’t a big deal but for group boxes it can be a big issue because group boxes contain other controls.

In the case of sndvol, when you resize the volume control, we resize the applications group box (because it’s visually pinned to the right side of the dialog).  Which causes the group box and all of its contained controls to repaint and thus flicker like crazy.  The only way to fix this is to remove the CS_HREDRAW and CS_VREDRAW buttons from the window style for the control.

The good news is that once I’d identified the root cause, the solution to my problem was relatively simple.  I needed to build my own custom version of the group box which handled its own painting and didn’t have the CS_HREDRAW and CS_VREDRAW class.  Fortunately it’s really easy to draw a group box – if themes are enabled a group box can be drawn with DrawThemeBackground API with the BP_GROUPBOX part and if theming is disabled, you can use the DrawEdge API to draw the group box.  Once I added the new control that and dealt with a number of other clean-up issues (making sure that the right portions of the window were invalidated when the window was resized for example), making sure that my top level control had the WS_CLIPCHILDREN style and that each of the sub windows had the WS_CLIPSIBLINGS style I had a version of sndvol that was flicker free AND which let the window manager handle all the drawing complexity.  There are still some minor visual gotchas in the UI (for example, if you resize the window using the left edge the right side of the group box “shudders” a bit – this is apparently an artifact that’s outside my control – other apps have similar issues when resized on the left edge) but they’re acceptable.

As an added bonus, now that I was no longer painting everything manually, the fade-in animations on the flat buttons started working again!

 

PS: While I was writing this post, I ran into this tutorial on building flicker free applications, I wish I’d run into it while I was trying to deal with the flickering problem because it nicely lays out how to solve the problem.

  • Larry, you should have taken the code from taskmanager, it already has a custom groupbox to deal with flicker: DavesFrameClass

    They question is, why not fix this in the common control? Add a new flag to the groupbox or something.

  • Matthew: the left-side resizing issue can be fixed with clever usuage of the WM_NCCALCSIZE message.

  • WndScks: I suspect that the reason they didn't "fix" this in the groupbox implementation is that it would break applications.  Any time you make any changes to the common controls you run a huge risk of breaking things.

  • Oh and until we realized that the problem was the CS_HREDRAW/CS_VREDRAW flags, we didn't know that the problem was the groupbox.  We thought it was something unrelated.  

  • James: You wouldn't happen to have a sample, would you :)?

  • Haven't got a 'good' example. Here's something though that kind of works. The scenario is, a top-level window calls this funciton (NcCalcSize) as it's WM_NCCALCSIZE handler. This top-level window contains a child-window that has a right-side vertical scrollbar (that is SM_CXVSCROLL wide). And it has a status-bar at the bottom (g_hwndStatusbar). You can see there's some 'magic' numbers in there too - can't remember why they're there - but probably just to fudge things to make it work.

    As you can see its messy and needs work. I had an email conversation way back with the author of winasm.net - pretty sure he had it figured out.

    <pre>

    //

    // WM_NCCALCSIZE handler for top-level windows. Prevents the

    //  flickering observed during a 'top-left' window resize by adjusting

    //  the source+dest BitBlt rectangles

    //

    // hwnd - must have WS_CLIPCHILDREN turned OFF

    //

    UINT NcCalcSize(HWND hwnd, WPARAM wParam, LPARAM lParam)

    {

    ULONG ret = DefWindowProc(hwnd, WM_NCCALCSIZE, wParam, lParam);

    if(wParam == TRUE)

    {

    NCCALCSIZE_PARAMS *nccsp = (NCCALCSIZE_PARAMS *)lParam;

    RECT rc;

    int  sbheight = 0;

    extern HWND g_hwndStatusbar;

    GetWindowRect(g_hwndStatusbar, &rc);

    sbheight = g_fShowStatusbar ? rc.bottom-rc.top : 0;

    nccsp->rgrc[1] = nccsp->rgrc[0];

    nccsp->rgrc[1].right  -= GetSystemMetrics(SM_CXVSCROLL) + 50;

    nccsp->rgrc[1].bottom -= GetSystemMetrics(SM_CYHSCROLL) + sbheight + 50;

    return WVR_VALIDRECTS;

    }

    else

    {

    return ret;

    }

    }</pre>

  • Larry,

    Do you use BeginDeferWindowPos/EndDeferWindow pos? It helps a lot. If you also use WS_CLIPSIBLINGS for your dialog (which is not a default for dialogs) and make sure the group box is lower in Z-order, you could avoid the hassle of writing your own group box

  • Another flickering issues: http://virtualdub.org/blog/pivot/entry.php?id=273#body

  • It's really interesting to see that there's a lot more "shuddering" when using the left window border compared to the right window border. The "shuddering" looks like a legacy issue that's just as clearly visible in Windows 7 on a high end system as on an old system running a legacy Windows version ... :|

    How well do other OS's/window managers handle this? (updating window content when the window is being resized)

    Are there any plans to reduce/fix this issue?

  • Larry: On my Windows Vista laptop, the mixer display has always been corrupted, ever since I first bought the machine. The latest patches are installed, but this doesn't resolve the problem. Do you think my problem was also fixed in Windows 7?

    When I open the mixer, I see this:

    http://seam.cs.umd.edu/volume.png

    Resizing the mixer makes the corruption go away.

    It is also very hard to click on the "Mixer" link from the volume control applet on a tablet PC, because it is overly sensitive to any pen jitter between pressing and releasing the link. I do not have this problem on any other links in Windows -- it seems that someone implemented their own click handling code, instead of using the standard Windows controls which are more tolerant of this jitter.

  • jcs: That kind of problem screams "display driver bug" to me.  Are you sure you've got the most recent display drivers for your laptop?

  • Now I know why the tab control of Task Manager flickers so heavily when resizing the window. It has the CS_VREDRAW and CS_HREDRAW class styles set!

    (BTW, it's really annoying that the Task Manager doesn't use the LVS_EX_LABELTIP style for the Processes list view. Some columns such as "Description" can have long texts, and they get clipped easily. LVS_EX_LABELTIP would make them completely visible on hover.)

    Thanks for your hard work, Larry! Windows 7 is awesome!

  • I believe that I have the latest drivers that my OEM distributes. (neither Windows Update nor my laptop's update utility offer me anything newer). However, it would not surprise me if this was a display driver bug, because this is a low-end laptop graphics chipset.

    Thanks...

  • I was searching for why a groupbox is a button, and instead found this question on stackoverflow http://stackoverflow.com/questions/214553/how-do-you-place-sub-controls-inside-a-group-box which implies that you're not really supposed to put controls inside a group box, but rather on top of it.

  • Random832: Those are two different issues - Windows aren't supposed to be children of buttons (the so issue) and why are groupboxes buttons.  It turns out that the Vista sndvol DID have the app group as a child of the groupbox but fixing it didn't fix the painting problem

Page 2 of 2 (30 items) 12