Custom Controls in Windows Forms - Part 7 Advanced Size Management (SetBoundsCore)

Recap

We've been building a very basic windows forms control together through the month of December. So far we've learned how to do simple painting, handle mouse movement, and create properties that codegen correctly. Just before the holidays, we were at an exciting cliffhanger: if we change the height of our square control, can we automatically update the width to match?

 

 

[square1.cs]

 

Making Sure the Square Control Stays Plumb

 

The problem with the square control is that we haven’t protected it against resizing out of shape in any way shape or form. If someone drops the control onto the form, and changes the width property or the height property, we want it to still remain a square.

 

To solve this, we could sync the Resize/SizeChanged events and change the size back to whatever we want, but that would be trapping it after the size has changed, it would be better if we could trap it before it has changed and tweak the height and width as necessary.

 

The first thought is to override the numerous Height/Width properties and call base with a different value, but, as it turns out, these are not virtual (overridable). Fortunately, all calls to change the Size/Location/Bounds/Left/Right/Top/Bottom properties eventually go through the SetBoundsCore function. This is protected virtual, which means that we can override it.

 

(For the Win32 junkies out there, you can think of SetBoundsCore as a wrapper around SetWindowPos.)

 

Option 1: Make the square a fixed height and width – the easy way out

If you just want to make the control always remain the same height or width, override the SetBoundsCore function and call the base with a fixed value.

 

       protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified) {

            // always make the square 100x100 pixels

            base.SetBoundsCore (x, y, 100, 100, specified);

        }

 

If this suits your needs, you can stop right here.

 

Option 2: If the width is changed, update the height – if the height is changed, update the width

Most controls that have certain sizing requirements can get away with some form of option 1. In the case of the Square, it would be nice if it was clever enough to go with the flow – if we updated the Width, we surely meant to update the Height as well. In order to understand how to accomplish this we must understand the last parameter to SetBoundsCore – BoundsSpecified.

 

If you were to look at the code for the Height property setter, it would look something like this:

 public int Height {
   get;
   set {
       this.SetBounds(this.x, this.y, this.width, value, BoundsSpecified.Height);
   }
}
 All of the other Size/Location properties are similar:

   Width -> BoundsSpecified.Width

   Size -> BoundsSpecified.Size

   Location -> BoundsSpecified.Location

   Bounds -> BoundsSpecified.All

 

By looking at the setter for these properties, we can clearly see that the BoundsSpecified variable tells us what aspects of x,y,width and height have programmatically changed.

 

Now that we know what BoundsSpecified means, we can use it to our advantage:

 

protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified) {

           

            switch(specified) {

                case BoundsSpecified.Height:

                    width = height;

                    break;

                case BoundsSpecified.Width:

                case BoundsSpecified.Size:

                default:

                    // if both the width and the height have changed

                    // or just the width save the width into the height

                    height = width;

                    break;

            }

            base.SetBoundsCore (x, y, width, height, specified);

        }

 

Can SetBoundsCore ever be called with BoundsSpecified.None?

Believe it or not, yes. The BoundsSpecifed is there to remember what programmatic changes have occurred to the Bounds of the control, so that at a moments notice, it can be restored. When a change has to be made to the size of a control as a side effect of the Dock/Anchor properties (in Whidbey: LayoutEngine), the call to SetBoundsCore will be made with BoundsSpecified.None.

 

E.g. You set a control to be 10x10 pixels in size, then you set the Dock property to be Fill. The control blows up to be the size of the parent (lets say it’s 300x300). You then change the Dock property to Left – and somehow the control is now 10 pixels wide and 300 pixels tall. If you’ve ever wondered the magic of this, it’s BoundsSpecified under the covers remembering storing off the last programmatic size/location change.

 

If you try this out with Dock/Anchor you’ll notice that it doesn’t handle docking very well. In the interest of keeping things simple I’ve omitted this handling. If you care about this case, you would need to explicitly handle BoundsSpecifed.None, determine the current DockStyle and set the height or width appropriately.

 

If I’ve made your brain hurt with talk of Dock and Anchor, I recommend this article.

 

(Note: Overriding SetBoundsCore only prevents programmatic changes of the control's Bounds – which is not a complete solution for a toplevel control such as a Form. A user can resize using the non-client edge or resize grip – to stop this kind of sizing you would need to either use FormBorderStyle=FixedDialog, or set MaximumSize and MinimumSize. For toplevel controls that don’t derive from form, see WM_GETMINMAXINFO).