2009-02-11

What are you, Picasso(.Net)?

There is a form in our application that draws very, very slowly. I'm trying to fix that.

In .Net, you have very little control over a border. In many cases (in particular, in this instance, with a Panel), you only get to set whether or not the control has a border, and whether that border is solid or rendered with a 3D effect. If you want more control over the border (such as setting its thickness or color), you have to draw one yourself. Since WinForms.Net has no "Line" control, this means you have to draw the line on the form at runtime by subscribing to the form's Paint event and inserting your line-drawing code there.

You have slightly more control over a background, being able to set the color in most cases; but if you want anything fancy like a gradient, it again requires custom paint code.

It is, therefore, not surprising that we have quite a few Paint event handlers on this particular form.

The layout of our form is as follows:

  • There is data in a couple panels at the top of the form.
  • There are submit and cancel buttons in a panel at the bottom of the form, along with an information panel.
  • In the center of the form is a large area. This area (which is itself a panel) contains one to many user controls.
  • Each user control is designed first as a panel.
  • The panel contains five "header-style" panels and three user controls.
  • The top-most and bottom-most header panels contain summary information — the top being the name of this whole grouping of data, and the bottom being a subtotal.
  • The middle three header panels contain a summary of the data on each of the "sub-user controls", plus a button that hides or shows that user control.
  • On load, the sub-user controls are hidden, so a "collapsed" view of all header panels is shown.
  • The header panels are filled with a gradient fill. This gradient creates a much more visually-appealing division between header rows than solid colors with lines between them would do. (There are lines between them as well, but even with 3pt black dividing lines, solid colors just blend together across those lines in a way gradients don't.)
  • There are drawn borders around each header panel, and around the entire user control itself. There are also separator lines between most of the panels on the form elsewhere.

I thought maybe the gradient fill was just causing too much overhead, but when I replaced it with a solid fill, it had no effect on the overall speed of the form. So, I put debug code in all the Paint event handlers, to see how often the paint events were firing, as I figured this might be the source of my problem. The paint events for each of the panels on the main form fired 2-10 times each (the one panel containing the user controls firing the most often). The paint events for the innermost user controls (the "sub-user controls") fired three times each, which made sense given there were three sets of them loading — except for the first of the group, which fired six times (twice per control).

The containing user control is where things go insane. There are three of them, remember, containing five header panels and three (hidden, at startup) user controls inside. The header panels' paint events fired 5 (for the header itself) to 25 (for each of the sub-controls' headers) times each (which, I suppose, you can divide by 3 to get approximate firings per control). The user control's large panel containing all child controls (header panels and sub-user controls), its paint event fired 651 times, and the paint event for the user control itself fired 747 times!

And that's just when the form is created. I added a button that resizes the form by increasing the width by about 10 pixels, just to see the effect, and I got 153 panel paint events and 168 control paint events.

The maddening thing is, the user control's paint event handler is empty. It does nothing (except for the debug code that notes the firing). Oh, it did do something — it did draw a gradient pattern under the entire control — but since the drawing it was doing was completely covered by all the controls that were placed on top of it, I removed it.

The panel's paint method actually does something — it draws a heavy border around all the subordinate controls. And, aesthetically-speaking, it's really needed. What I can't understand is why exactly it's needed over 200 times per control, especially when the containing and contained controls combined aren't getting painted as often (by a factor of about 7).

So, what's my solution? Well, aside from making sure the paint events did as little work as possible (why is e.Graphics.SetClip(e.ClipRectangle) not automatic in a Paint event?), drawing gradients in Rectangle rect only if (e.ClipRectangle.Contains(rect) || e.ClipRectangle.IntersectsWith(rect)), and then only in the part rect.Intersect(e.ClipRectangle); but I also completely unbound the Paint event handlers in the user control, and instead of drawing a thick border on the panel, just used the default BorderStyle.FixedSingle.

It's still slow, although not as slow as it was (I'm sure those events are still getting fired, just with no custom code to process); and it doesn't look the same with the change in border, but there's enough padding between user control instances that I think I can get away with it.

In short, I didn't solve the problem; I still can't find the cause, and I've only been marginally successful in treating the symptom.

2 comments:

Spencer said...

Congratulations. So far, it sounds like failure. OH SNAP

Yakko Warner said...

Yes, I do believe that was more or less what I stated in my conclusion, thank you.