WinForms - Painting On Top Of Child Controls

Skill

WinForms - Painting On Top Of Child Controls

Posted in:

If you have worked with WinForms and ever needed to do some custom painting, I'm sure you have had the following happen. You write up some painting logic, and run the program, only to find that all or part of the painting is not visible. You scratch your head for a moment and then go "Duh! There's another control on top of what I'm trying to paint!", and then go off and try and figure out a different solution. Today I'm going to show you a way of getting around this problem in certain situations.

For those who have not had this problem, here's the dirty little secret - if control A and control B overlap, and A is behind B, anything that is painted on A in the overlapping portion is not visible. This makes sense, but it can get annoying sometimes when a parent control wants to paint something on top of a child control. There are ways to do this, but they are all hacky - like grabbing the child control's graphics object and painting on it directly, or making portions of the child control transparent, or even some other even tackier solutions.

But tacky solutions have no place here :P, so we are going to use a different method. A common reason (or at least the common reason for me) to want to paint on top of child controls is some sort of selection rectangle. You have some sort of view of a bunch of items, and you want the user to be able to drag a selection rectangle around some of them to select them. You could just paint the selection rectangle on the parent container - but then there is a lot of weirdness as the lines disappear underneath the child items. But what if there was a function in .NET that dealt with this specific problem? Guess what - there is, and its called DrawReversibleFrame. Here is what a selection rectangle drawn using DrawReversibleFrame looks like:

Draw Reversible Frame Screnshot

As you may notice, the selection rectangle is drawn on top of all the child controls in the form (in this case, just a random assortment of a label, a text box, a group box, and a button). Not only will the rectangle be drawn on top of child controls - it will draw on top of anything - including areas outside the bounds of the application itself. Its a powerful little method.

So where is this method? It is a static method in the ControlPaint class. The ControlPaint class itself is full of useful goodies - and while we won't be playing with anything besides DrawReversibleFrame today, I encourage you to go play around with some of the other methods in that class.

Without further ado, lets dive into some code. We will be walking through the code it takes to create that the app you saw in the screenshot above.

public partial class DrawReversibleFrameTest : Form
{
    private bool _IsSelecting = false;
    private Point _StartPoint = Point.Empty;
    private Rectangle _FrameRect = Rectangle.Empty;

    public DrawReversibleFrameTest()
    {
      InitializeComponent();
    }
}

Here is our initial form, with three fields. The names of the three fields are pretty self explanatory, but I'll go through them anyway. The first, _IsSelecting, is a boolean that is true if we are currently in selection mode - i.e., the mouse button is down and we are currently drawing a selection rectangle. Next is _StartPoint, which will hold the start point for the selection rectangle, the point at which the user pressed the mouse button. The final field, _FrameRect, holds the current selection rectangle drawn on the screen. You will see in a moment why we need to hang onto the the rectangle representing the selection area.

protected override void OnMouseDown(MouseEventArgs e)
{
  base.OnMouseDown(e);

  if (e.Button != MouseButtons.Left)
    return;

  _IsSelecting = true;
  _StartPoint = e.Location;
  _FrameRect = new Rectangle(PointToScreen(_StartPoint), Size.Empty);
}

Here is the code that executes when the user presses a mouse button while on the form. We only care about the left mouse button, so if it is anything else, we return. If it is the left mouse button, we set _IsSelecting to true, set _StartPoint to the current mouse location, and create the beginnings of the selection rectangle. You may notice that we are translating the initial point for the selection rectangle into screen coordinates - this is because DrawReversibleFrame uses screen coordinates to do its drawing.

Now comes the meat of the code - what to do when the mouse moves:

protected override void OnMouseMove(MouseEventArgs e)
{
  base.OnMouseMove(e);

  if(!_IsSelecting)
    return;

  ControlPaint.DrawReversibleFrame(_FrameRect,
      Color.Black, FrameStyle.Dashed);

  _FrameRect.Width = e.X - _StartPoint.X;
  _FrameRect.Height = e.Y - _StartPoint.Y;

  ControlPaint.DrawReversibleFrame(_FrameRect,
      Color.Black, FrameStyle.Dashed);
}

If we are not selecting, we return. Otherwise, we do our drawing. The reason there are two calls to DrawReversibleFrame here is for an interesting reason. The way to get rid of a rectangle previously drawn by DrawReversibleFrame is to call the method a second time on the exact same coordinates. So the first call here actually erases the previously drawn selection rectangle. Then we calculate where the new selection rectangle is going to be, and draw it.

The arguments that DrawReversibleFrame takes are pretty simple - a rectangle (in screen coordinates), a color, and a FrameStyle (which is either Dashed or Thick). The only oddity is that the color is not the color of the rectangle that you want to paint - it is a value that will be XORed with whatever the background color is where the rectangle gets painted. This is the reason that painting the rectangle twice removes it: A xor B xor B = A. Strange, yes, but useful - because it mean that the rectangle will not always be the same color - it will be a color relative to the background it is painting on, which makes it much more visible to the user in certain circumstances.

Ok, now onto the final function, OnMouseUp:

protected override void OnMouseUp(MouseEventArgs e)
{
  base.OnMouseUp(e);

  if (!_IsSelecting)
    return;

  ControlPaint.DrawReversibleFrame(_FrameRect,
      Color.Black, FrameStyle.Dashed);
  _IsSelecting = false;
  _FrameRect = Rectangle.Empty;
  _StartPoint = Point.Empty;
}

Not really much here - first we make sure we were actually selecting. If we were, then we erase the current selection rectangle (by painting it again), and reset all of our fields. And that's it!

And that is all we have for today. If the thing about the XORing of colors is confusing, don't worry about it. Just use black, and it generally does exactly what you want. If you would like to download the visual studio project, you can grab it here. If you have any questions or comments about DrawReversibleFrame, or anything else in ControlPaint, leave a comment below.

References

MilkBoy
10/28/2009 - 03:47

Thanks, exactly what I was looking for =) Having a custom control that can be resized now used this technique to "preview" the new size

reply

u got it
11/03/2009 - 11:11

How will this work if the child control has its own paint method?
Can we use this logic to draw a control that is on the parent form on top of child control (without making the child transparent)?

reply

Add Comment

Put code snippets inside language tags:
[language] [/language]

Examples:
[javascript] [/javascript]
[actionscript] [/actionscript]
[csharp] [/csharp]

See here for supported languages.

Javascript must be enabled to submit anonymous comments - or you can login.

Sponsors