Flowing with Delphi
Flowing With Delphi
Delphi has always been the premiere environment for creating slick user
interfaces. While there are many elements that are required for a good
user interface, the visual layout of controls on the form is one of the
most basic. Previous versions of Delphi introduced the Align, Anchor, and
Constraint properties to ease layout, and now Delphi 2006 introduces the
TFlowPanel and TGridPanel. The sample project for this article can be
downloaded.
TFlowPanel
TFlowPanel is a very simple control. It automatically positions the
controls within itself. The FlowStyle property controls the order of the
controls. By default, it is set to fsLeftRightTopBottom which places the
components from left to right, top to bottom, with the edges touching.
For example:
After resizing the controls are automatically repositioned:
The other layout possibilities are:
Left to Right, Top to Bottom:
|
Left to Right, Bottom to Top:
|
Right to Left, Top to Bottom:
|
Right to Left, Bottom to Top:
|
Top to Bottom, Left to Right:
|
Top to Bottom, Right to Left:
|
Bottom to Top, Left to Right:
|
Bottom to Top, Right to Left:
|
One thing to keep in mind is that the flow panel does not support
scrollbars, so some controls might be chopped off or completely hidden:
Any controls can be used, not just buttons. Here's an example using
buttons, edit boxes, and check boxes:
If the controls have a case of aphephobia then the spacing between
controls can be adjusted through the use of the Margins and
AlignWithMargins properties.
Moving Controls at Design-Time
So what happens if you want to rearrange the controls at design time?
It's not immediately obvious how to do this. Simply dragging the controls
with the mouse doesn't work - the controls just snap back to their
original position. You can cut and paste a control to move it the very
last cell, but this isn't very flexible.
Fortunately there is the magical ControlIndex property, and this is where
things get interesting. If you put a TButton control in the flow panel
there will be a ControlIndex property listed at the very bottom of the
Object Inspector. If, however, you place that very same TButton on the
form, the ControlIndex property will not be present.
So a TButton (or any other control for that matter) might or might not
have a ControlIndex property depending on whether or not it's placed on a
grid panel.
There's no reference to this ControlIndex property anywhere in the VCL
source code. However, TFlowPanel does have two functions called
GetControlIndex and SetControlIndex which returns or changes a control's
current index. Somehow the Delphi IDE is automagically adding the
property when necessary. You'll see this same technique being used for
the TGridPanel as well.
At any rate, changing the button's ControlIndex property will change the
position of the control in the flow panel. Changing it to zero will move
it to the very first spot, changing it to 1 will move it to the second
spot, etc.
TGridPanel
The grid panel is used to arrange controls in a grid. No surprise there.
Each control will be placed in the center of a cell, and each cell will
contain at most one control. As the panel is resized the controls will be
repositioned to remain in the center of the cell; if necessary the
control will shrink.
After resizing:
We again have some magical properties for changing the position of
controls. The Column and Row properties work similarly to the
ControlIndex property did for TFlowPanel. The difference is that
specifying a new value for the Column or Row property will cause the cell
contents to be exchanged. For example, in the screen shot above, if the
Row property of Button 6 was changed from 1 to 3, the button would
exchange places with the edit control. ie:
There are two other magical properties - ColumnSpan and RowSpan. These
work similarly to HTML tables. In the screen shot below button 2 and 5's
ColumnSpan, and button 4 and 11's RowSpan property have all been set to
two. Button 2, 5 and 11 work exactly as you would expect. Button 4,
however, causes some problems. It is positioned correctly, but it causes
Buttons 8 and 12 to be positioned incorrectly.
So long as the cell below Button 4 is empty everything will work fine:
Layout of the Rows & Columns
The grid panel has a ColumnCollection property which allows you to add or
delete columns and to control the width of each column. Columns or rows
will be added automatically if you add more controls than there are
cells. Setting the grid panel's ExpandStyle property to emAddColumns or
emAddRows determines whether columns or rows will be added while setting
it to emFixedSize will prevent additional controls from being added.
There are three options for setting the width of a column. The ssAbsolute
style allows you to specify the width of the column in pixels. The ssAuto
style will scan each row in that column to find the control with the
largest width and will then set the column width to that size.
The ssPercent style allows you to specify the width as a percentage of
the total width. Or so you would think. In fact, it doesn't quite work
that way. For starters, it disregards the width of columns that use the
ssAuto or ssAbsolute styles. Suppose you have a grid panel that is 100
pixels wide, and the first column is set to an absolute width of 30
pixels, and the two remaining columns are set to 25% and 75%. The actual
width of the last two columns will be 17 pixels and 52 pixels (which are
25% and 75% of (100-30) pixels).
The tricky part is setting the percentages. If you have a grid with two
columns which are currently set to 50% each, and then set the first
column to 25%, you'll find that the widths for the two columns are
actually set to 33.33% and 66.67%, rather than 25% and 75% which is
probably what you wanted.
Here's the problem. When you set the width of the first column to 25%,
the TColumnItem object notifies the collection that the item has changed,
and the collection then notifies the grid panel that the collection (not a particular item)
has changed. At this point the grid panel can see that one column is set
to 25% and the other is set to 50% but it doesn't know which one changed.
So how does it arrive at 33.33%? It divides a column's width by the sum
of the percentage for all columns. So for column one this would be
25 / (25+50) = 33.33, and for column two this is 50 / (25+50) = 66.67.
If you keep entering 25% over and over again the end result will
eventually approach 25%. This is rather awkward but it does work.
Fortunately, there is an easier way. Use code to set the widths and wrap
the code in calls to BeginUpdate/EndUpdate:
procedure TfrmMain.btnSetColumnWidthsClick(Sender: TObject);
begin
gridPanel.ColumnCollection.BeginUpdate;
gridPanel.ColumnCollection[0].SizeStyle := ssPercent;
gridPanel.ColumnCollection[0].Value := 25;
gridPanel.ColumnCollection[1].SizeStyle := ssPercent;
gridPanel.ColumnCollection[1].Value := 75;
gridPanel.ColumnCollection.EndUpdate;
end;
As you would expect rows are handled in an identical manner via the
RowCollection property. Here's a screenshot showing rows set to 25%,
25%, and 50%.
Cell Layout
There are many ways of controlling how the control is positioned within
the cell. You can use the Align property of the control in the usual
fashion, either with or without the use of the control's Margins and
AlignWithMargins properties.
You can also use the Anchor property to align the control to the edges of
the cell in a slightly different manner than that accomplished by using
the Align property. Drop a button on a grid panel. You'll notice that the
Anchor property is set to [] - this allows the control to float in the
center of the cell. If you set the Anchor property to [alLeft] it will at
first seem that nothing has changed. However, if you resize the grid
panel, or attempt to move the button, the button will immediately snap to
the left side of the size without the height or width of the button
changing. The Margins property will also be obeyed so long as
AlignWithMargins set.
In the snapshot below button 1 is anchored to the top-left, button 2 is
aligned to the top, button 3 is aligned to the top-right, etc. Notice
that button 9 is using its Margin and AlignWithMargins properties to
distance itself from the edge of the grid panel.
The one thing you can't do to change the layout is to use the Top and
Left properties - these properties are magically hidden from the Object
Inspector when controls are dropped on a grid panel.
More Magic
We've already seen the magical properties ControlIndex, Column,
ColumnSpan, Row, RowSpan, Top and Left. I was just about to finish this
article when I noticed that there is one other magical property. If you
embed one grid panel within another grid panel, the embedded grid panel
will have a ControlCollection property which gives access to a collection
of TControlItems, one for each control that you place on the embedded
grid. The TControlItem instance has Column, ColumnSpan, Row, and RowSpan
properties which provide the same functionality as the same properties on
the embedded controls. The collection editor window does allow you to
re-order the TControlItem instances, but this has no effect on the order
of the controls in the grid.
Reality Sets In...
...and it's good. I'll close this article with a more realistic example
of the grid panel. The two snap shots below are of the same form, but at
different sizes. In each case the controls are appropriately sized - all
without writing a single line of code.