Introduction
I had read every article on flicker-free drawing, yet I could not make it work for my resizable custom control. There are a couple of known issues, but there is also a rarely mentioned subtlety involved. In this article, I will discuss all the issues involved and how to approach them.
The sample included in this article is an implementation of an elliptical button control. The drawing is done using GDI+. The round button changes outer color as the mouse cursor rolls over. The inner color changes as the mouse is clicked on the button. The control stretches and shrinks as the dialog box is dragged.
Background
Many articles on the subject suggest to:
- use double buffering
- override
OnEraseBkgnd
and return TRUE
.
Well, I implemented both and it still flickered. The problem most visibly occurred during resizing, when the whole dialog was repainted. The issue was that the dialog box would erase my custom control as it erased its own background. Therefore, every time I resized the dialog, I would get a flicker due to the dialog box painting grey over my control before the control repainted itself.
Using the code
The clipping solution: tell the dialog to erase everything but the region occupied by the round control. The easiest way to accomplish this is to set the Windows style to WS_CLIPCHILDREN
. You can do this programmatically in "OnInitDialog
" method (or you can set this dialog property in Visual Studio's design view):
BOOL CNoFlickerDlg::OnInitDialog()
{
....
ModifyStyle(0, WS_CLIPCHILDREN);
return TRUE;
}
Since my control has a custom (round) region, it must be re-computed each time the control changes shape or size. In order for this to work correctly, the best place to call the ReCalculateDimensions
function is from the dialog's OnEraseBkgnd
, like so:
BOOL CNoFlickerDlg::OnEraseBkgnd(CDC* pDC)
{
m_cButtonControl.ReCalculateDimensions();
return CDialog::OnEraseBkgnd(pDC);
}
The reason you have to make the call here is important. The dialog will exclude the control region from its surface (that is being erased) based on what the control region is set to be. Therefore, by calling it right before CDialog::OnEraseBkgnd
, you ensure that the control region is up to date right before it is clipped.
Double Buffering: Since all drawing is done in GDI+, I used a CachedBitmap
class to help me with double buffering. The code comes down to the following few lines:
void CButtonControl::OnPaint()
{
CPaintDC dc(this);
Graphics graphics(dc.m_hDC);
if (m_bDirtyBuffer)
{
DrawButton(graphics);
m_bDirtyBuffer = FALSE;
}
graphics.DrawCachedBitmap(m_pCachedBitmap, 0, 0);
}
As you can tell from the code above, the actual drawing is only done when m_bDirtyBuffer
flag is set to true. Every other time, the painting is done using a cached bitmap. You set the flag to true whenever the control is resized (i.e., in OnSize
method).
Finally, make sure that the control itself has OnEraseBkgnd
method overridden and returns TRUE
. This will make your controls flicker free!
Points of Interest
Download the demo and drag it by the corner. See the button change shape without any flicker.