Flowing with Delphi

By: a z

Abstract: Explore two new controls in Delphi for managing the layout of your user interface.

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.



Server Response from: ETNASC04