0

I have a custom button control where I need to do some drawing. Nothing fancy or complex, yet I notice quite a difference in performance when drawing within WndProc WM_PAINT and drawing within the OnPaint event.

When I draw in OnPaint I do not get any flickering at all. When I draw in WM_PAINT I do get flickering, though only when entering and leaving the button. So the flickering occurs when the button receives or looses highlight (BN_HILITE / BN_UNHILITE notification).

As far as I know the OnPaint event is simply a event based wrapper for the WM_PAINT message. So in theory the OnPaint event should be less efficent since it adds a layer of abstraction to the painting process.

I am unsure if it is my code producing the flickering or something else.

Here is the Code of the overridden OnPaint event of my custom button:

protected override void OnPaint(PaintEventArgs pevent)
{
    base.OnPaint(pevent);

    if (Day <= 0) return;
    if (string.IsNullOrEmpty(Text)) return;

    // Adjust font size so all text will fit.
    AdjustFont(pevent.ClipRectangle);
    // Check which ForeColor to use.
    Color fc = Month == ExpectedMonth
        ? ForeColor
        : UnexpectedMonthForeColor;

    using (var brush = new SolidBrush(fc))
    {
        // Use StringFormat to center string in control.
        StringFormat sf = new StringFormat {
            LineAlignment = StringAlignment.Center,
            Alignment = StringAlignment.Center
        };
        pevent.Graphics.DrawString(Text, Font, brush, pevent.ClipRectangle, sf);
    }
}

Here is the WndProc implementation which produces flickering:

const int WM_PAINT = 0x000f;

protected override void WndProc(ref Message m)
{
    base.WndProc(ref m);
    if (m.Msg == WM_PAINT)
    {
        if (Day <= 0)                   return;
        if (string.IsNullOrEmpty(Text)) return;

        using (var gr = Graphics.FromHwnd(Handle))
        {
            //Adjust font size so all text will fit.
            AdjustFont(ClientRectangle);
            //Check which ForeColor to use.
            Color fc = Month == ExpectedMonth
                ? ForeColor
                : UnexpectedMonthForeColor;

            using (var brush = new SolidBrush(fc))
            {
                // Use StringFormat to center string in control.
                StringFormat sf = new StringFormat {
                    LineAlignment = StringAlignment.Center,
                    Alignment = StringAlignment.Center
                };
                gr.DrawString(Text, Font, brush, ClientRectangle, sf);
            }
        }
    }
}

Here the AdjustFont method:

private void AdjustFont(Rectangle rcBounds)
{
    // Calculate string size and check if it fits into the current bounds.
    var szText = TextRenderer.MeasureText(Text, Font);
    if (szText.Width > rcBounds.Width || szText.Height > rcBounds.Height)
    {
        // Reduce font size by 0.25 until the text fits into the bounds.
        while (Font.Size > 0.25f 
            && (szText.Width > rcBounds.Width 
            || szText.Height > rcBounds.Height))
        {
            Font = new Font(
                Font.FontFamily, 
                Font.Size - 0.25f, 
                FontStyle.Regular, 
                Font.Unit, 
                Font.GdiCharSet, 
                Font.GdiVerticalFont);

            szText = TextRenderer.MeasureText(Text, Font); 
        }
    }               
}
  • Consider adding a counter to determine the number of invocations. Are both implementations called the same number of times? _Please don't guess._ – mjwills Jun 25 '20 at 07:54
  • @mjwills I already testet that. Both implementations produce the exact same amount of invocations. –  Jun 25 '20 at 08:10
  • Given the two code blocks aren't _exactly_ the same the most simple explanation is that one of the different bits is slower in one version than the other. (e.g. `gr.Dispose` is in one version but not the other). – mjwills Jun 25 '20 at 08:11
  • Is `AdjustFont`'s `while` loop (the logic in it) executed the same number of times in both versions? – mjwills Jun 25 '20 at 08:13
  • @mjwills Measured it just now and the loop is executed the exact same number of times in both versions. I also measured how many ms and ticks are produced and the outcome shows that the WndProc implementation is much slower. I guess due to the additional IF clause and the creation and disposing of the graphics element. –  Jun 25 '20 at 08:27
  • Results for OnPaint (11 invocations): Total ms: 5,2317 Avg ms: 0,4756 Min ms: 0,2263 Max ms: 0,9561 –  Jun 25 '20 at 08:34
  • Results for WndProc WM_PAINT (11 invocations): Total ms: 25,5222 Avg ms: 2,3202 Min ms: 1,36 Max ms: 3,8019 –  Jun 25 '20 at 08:35
  • Given one way works, why not just use that way? Why is this an issue for you? – mjwills Jun 25 '20 at 12:26
  • "OnPaint event is simply a event based wrapper for the WM_PAINT message" - Why make such an assumption? In the `OnPaint` method create a StackTrace and inspect its frames to see the call hierarchy. Then inspect the [source code](https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Control.cs,9884211b7ff61817) for those methods to see what is really going on. – TnTinMn Jun 25 '20 at 14:13
  • `base.WndProc(ref m)` ends up calling `OnPaint`, so just use `OnPaint`. These base calls are already optimized. If your program responds slowly then the problem is elsewhere. – Barmak Shemirani Jul 05 '20 at 17:41

1 Answers1

0

I chose to use the OnPaint() version since it's performance is better and I do not need immediate updates in the designer.

I will take a deeper look at the WM_PAINT implementation as suggested by @TnTinMn. Any new result will be added to this answer.