In Windows Forms, Controls are Components which are visible. This is how I undersand it, at least. That is, they are pasties.
If a fridge was your form, magnets would be your controls. Only, some of them would glow, and talk, and change their words and size and slide around. And some would have magnets stuck to them. (I'm surprised how well the analogy is still holding..)
Each control has a
Bounds property, and this property returns a rectangle, which defines a location and size of a rectangle in Cartesian coordinates, with the top-left corner being 0,0.
The bounds of a control are defined in
MSDN thusly:
"Gets or sets the size and location of the control including its nonclient elements, in pixels, relative to the parent control."
Nonclient elements- things that have been generated for you and on which you may not (probably) draw. For instance, automatic scroll bars triggered inside a panel when its contents are put outside of its bounds.
Did you catch that?
So say a form has a panel with Bounds of 50,50,100×100. That means that on the form, there is a panel 100×100, at 50,50.
Now say that our panel has a button at 0,0,20×20. This means that at the top left corner of the panel (not the form) there is a button 20×20.
This is all sensible.
Now let's say we add a button dynamically, at run time, to the panel. The code for this is:
Button b1 = new Button();
b1.Text="Clicky!";
ThePanel.Controls.Add(b1);
So we construct a button, are kind enough to give it text, throw it on the panel. Add() will automatically set b1's parent, and b1 enters he form's message loop appropriately. Windows Forms is lovely like that.
But we didn't set coordinates for the button. Size, Location, Left, Top, Width, and Height are all synonyms for components of the Bounds property, previously mentioned. The difference is that because Bounds returns a rectangle, which is a value, its members cannot be modified in place. These properties can.
b1.Size = new Size(10,10);
b1.Left = 0; b1.Top = 0;
This is better. But remember, it's in a panel. If the panel moves, they will too. There are functions for calculating a controls absolute position relative to its form, or even to the screen the form is being shown on, but this should probably not matter. If you have a control inside another, it shouldn't ever need to know about the outside.
Controls which can hold other controls are called containers. The thing about containers is that they don't necessarily do bound checking the way you would think. In other words, if you place a subcontrol outside of the bounds of its parent control, it will simply clip, showing only the part of the subcontrol that has not strayed, if any. This, also, is sensible.
The thing about panels is they have an AutoScroll property you can turn on. And when this property is turned on, panels (and any other control descended from ScrollableControl, apparently) will do something quite handy- they will automaticaly draw scroll bars, and expand their content without expanding their on-screen dimensions.
This is incredibly handy, but the coordinates involved become absurd.
This is what I think
SHOULD happen but
DOES NOT:
The coordinates of a control within a scrolling container are relative to the top-left corner of the parent's surface- that is, the virtual space which is only exposed to the client through scrolling.
Here is what does happen:
The coordinates of a control within a scrolling container are relative to whatever the top-left corner of the visible portion of the parent's surface happens to be at the time you check.
I cannot begin to guess why this design choice was made.
So let's, I suppose, start with an example of how this is insane. First, we take our panel and button from before:
Panel 100×100
Button 0,0, 10×10
We move the button so that part of it is off the panel's dimensions:
b1.Top = 100;
When this finishes, several things happen. First, the Panel, having discovered that one of its controls is no longer within its bounds, will sort of split.
Its bounds will not change- the panel is still positioned on its parent control (the form) at the same coordinates and with the same size. However, its virtual size has enlarged so that it is larger- 110x100, because the button has 'stretched' it out. At this point, some of the other properties of the panel will change.
DisplayRectangle will now have a size of 110x100.
Bounds will be unchanged, at 100x100.
ClientRectangle will
shrink, becoming less than 100x100 (about 100x90). This is because
ClientRectangle represents the size of the box that is visible to the user, and now that scroll bars have been drawn inside the panel, there is less of the panel's contents exposed to the user!
This is all done automatically for you as long as the panel's
AutoScroll attribute is set to
true. The scroll bars will function without any additional code- the user can pan around the virtual surface of the panel.
But when you go to add another button to the panel, you may discover things operating a little strangely. Let's say that you want to add another button at the bottom of the panel, extending it outward some more. We add the function:
void AddButton() {
Button nb = new Button();
nb.Width = 10;
nb.Height = 10;
Panel1.Controls.Add(nb);
nb.Left = 0;
nb.Top = Panel1.DisplayRectangle.Height;
}
So this code should create a new button, size it to 10x10, add it to the panel, then position it at the bottom of the Panel's DisplayRectangle, causing the panel to stretch itself downward again, adding more virtual space. And this is exactly what it does- sometimes.
The problem is this. If you happen to have already scrolled the panel down, the button will end up being out of place- too far down the panel. This is because
a child control's bounds are not absolute when in a scrolling container.
The MSDN documentation for Control.Bounds specifies that it represents the control's size and position relative to the top-left corner of its container. What it does not directly state is that it is relative, in fact, to the
top-left corner of whatever portion of the container is currently visible.
So say you're scrolled 10 pixels down on the panel, then add a button at 100,0. Relative to the virtual surface of the panel, it is actually at 110,0. Say you're scrolled 1000 pixels down. That same button is now at (and Bounds/Top will return) -890,0.
Any time the user scrolls the panel, all its child control's positions are changing to reflect their position relative to the top-left corner of ClientRectangle, rather than DisplayRectangle. To manage positions relative to the DisplayRectangle, you will have to subtract Panel1.VScrollPosition from the result.
So to use the previous example, the button is located, relative to the virtual surface of the panel, at (button.Top - button.parent.VScrollPosition, button.Left - button.Parent.HScrollPosition). To move it to 200,0, regardless of where the panel is scrolled to, you must set:
button.Top = 200 - button.Parent.VScrollPosition;
button.Left = 0 - button.Parent.HScrollPosition;
There does not appear to be any way to toggle this strange behavior in windows forms. At least, none that I have found. I'm still struggling with a few aspects of this behavior myself- there appear to be certain cases where the panel does not resize itself accurately to reflect the new positions/sizes of its child controls on the first try- but later attempts correct this. I'll write another post on that when I figure it out- or get stuck.
I hope this helps people trying to work with dynamic forms using scrolling containers, and finding their buttons being placed strangely while debugging!