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.

  • Nice post.

    I would love to read the explanation why group boxes are buttons though.

  • Billy: If I knew, I'd have included it in the post.

  • This post highlights why writing Windows applications can be frustrating for us mere mortals. Try doing that all over again WITHOUT access to "internal mailing lists" and "one of the developers from on the User team." You're only allowed to use newsgroups, blog entries, and the MSDN documentation. I'm timing you... go!

  • I wish I could get back all the days of my life I've spent trying to get Windows common controls to be flicker free. It's unbelievable how hard it is :(

  • That last (unfixed) problem you mentioned -- resizing the left edge of the window causing the right side to flicker -- is driving me nuts.  

    I've run into it in any number of apps I've written and I've never successfully found a fix to it.  It appears that when you resize the left edge the window manager moves the whole contents of the window over to the left and only then gives you a chance to redraw.  

    If someone here knows a fix for this, I would be *incredibly* appreciative.  At least buy you a beer the next time you're in town appreciative. :)

  • Did you try to subclass the group box and override its WM_ERASEBKGRND message in an attempt to retain native Groupbox drawing?

    My theory on BUTTON and BS_GROUPBOX is probably the obvious one: it seemed like a good idea at the time. It would have been less hassle to shoe-horn it into BUTTON rather than create a new window class. And possibly more importantly a couple less 16 bit handles consumed. I'd call it a design feature rather than a bug.

    It's often hard to explain to non-programmers "yes, that bug fix took 5 days. And no, I'm not just out of university."

  • steveg: That was one of my early ideas, but subclassing the group box retains the CS_VREDRAW and CS_HREDRAW class styles.  I had to built my own subclassed control and try it.

    Matthew: I don't know how to fix it unfortunately and neither did the window manager developer who was helping me.

    Mike: I know how hard it is.  I only fall back on the internal DLs when I run out of ideas. Having said that, the information *is* out there (if I'd seen the link I included in this post when I was doing this, I'd have saved about 2 weeks of time).

  • steveg: Btw, this bug collectively took about 4 weeks.  2 weeks for the first implementation and 2 weeks for the 2nd implementation.  It turned out that there were a huge number of things wrong that led to the flickering, however this issue was the only one that was blogworthy (for instance at one point the volume sliders were child controls of the group box which is a hideously bad idea and confuses the heck out of the window manager)

  • Mike: I don't believe I see where Larry used "Internal mailing lists" in the process of running down his issue.

    LarryOsterman: Thanks for the interesting article :) Found a typo for ya:

    "I decided to bit the bullet" -> "I decided to bite the bullet"

    Hope that helps :)

  • Interesting problem and solution. However, you still mised something (or maybe someone else did): When I move the mouse over the "Speakers" drop-down list it fades in—but not completely, the left and right sides of the faded-in list are missing until it's completely opaque and then "pop" into view. Similarly for fading out: The left and right sides (each around 5 pixels wide) stay visible until the rest has completely faded.

    Interestingly, they tend to fade in a little when doing a screenshot, which makes this pretty hard to capture but you can see the opacity differences:

    http://hypftier.de/dump/volume_speakers_fadein.png

    http://hypftier.de/dump/volume_speakers_fadeout.png

    Also I notice that the D in "Devices" gets underlined a second time when I press Alt.

  • Johannes: I didn't miss it.  It's an artifact of how the fade in logic works in the toolbar control - there isn't a multiline drop down toolbar split button conrol so I had to build my own using custom draw messages (which was really easy to do).  Unfortunately the custom draw logic doesn't handle the fade in so there's a period of about .5 second where the styling for the top and bottom margins of the toolbar button style are incorrect.

  • Rather than a sub-class, you could always super-class the button class and remove the redraw styles. Might work...

  • The flicker free drawing link you added in the postscript is a good description of double buffering.  One idea I've learned from others is to make a reusable class that encapsulates the memory device and bitmap and have the destructor of the class do the final BitBlt to the screen like shown here http://www.codeproject.com/KB/GDI/flickerfree.aspx.  I've used that class a number of times to double-buffer GDI draw code.

  • I knew there had to be an underlying bug.

    http://blogs.msdn.com/larryosterman/archive/2008/08/28/gotchas-associated-with-using-wm-printclient.aspx#8906475

    The Windows painting model isn't *totally* broken. ;-)

    Adrian.

  • Notepad and pretty much anything that uses the edit control have needless flicker. I know it's needless because I built a custom child window class that doesn't flicker.

    But of course, *I* in my usage did not need the flicker and maybe at some point Larry, you could blog or get someone to blog on flicker in the edit control.

    BTW I didn't remove the flicker by double-buffering the render.

Page 1 of 2 (30 items) 12