Building a flicker free volume control
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:
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:
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.