In FireMonkey, the usual way to group controls is to use a ‘layout’ of some sort. Conceptually, a layout is just a container control with no visual appearance of its own, at least by default. This contrasts to something like a panel or group box, which is also a container but one that the user sees as such.
A few layout classes are provided with Delphi. The simplest is TLayout, which does nothing more than implement the basic defintion of a layout control; beyond it stands TScrollBox, TScaleLayout, TGridLayout and TFlowLayout. You can read about them in the help, though in a nutshell, they do the following -
- TScrollBox shows scroll bars as its contents extend beyond its own visible client area (the scroll bars are auto-hidden if not needed)
- TScaleLayout resizes its controls as it itself is resized
- TGridLayout positions and sizes its constituent controls in fixed-sized cells
- TFlowLayout positions controls like the words in a paragraph, first left to right (or right to left via a property setting), then top to bottom. Controls are then moved accordingly when the layout is resized.
While it isn’t a layout control strictly speaking, an honorary mention also goes to the FireMonkey TListBox, whose fixed-width items can host child controls.
That said, I was recently wanting a layout object to organise controls top aligned from from top to bottom. Unfortunately, none of the standard layout classes met my requirements, which were as thus:
- Each control should fill the parent’s client width, unless the standard Paddings and Margins properties indicate otherwise.
- However, a control’s height should be specific to the control.
- The first control would be located at the top of the parent, the second immediately below the first (perhaps with a standard gap), the third immediately below the second and so on.
- It should be easy to change the order of controls.
- Ideally a vertical scroll bar should show if the controls cannot fit.
Requirement (1) ruled out TFlowLayout, number (2) ruled out TListBox and TGridLayout, none of the requirements made TScaleLayout relevant, and only (5) could be serviced by TScrollBox. Writing a custom layout class for the task proved pretty easy however:
- Create a new package project, and set its ‘description’ to something appropriate under Project|Options, Description (e.g., ‘List Layout Control’).
- Add a new unit to the package, and add System.SysUtils, System.Classes and FMX.Types to the unit’s interface section uses clause.
- Following TFlowLayout and its peers, declare a class descending from TControl. Publish the usual FMX control properties inherited from (but not published by) the base class.
- Following TFlowLayout again, add a new published property called VerticalSpacing, typed to Single, and add overrides for the DoAddObject, DoInsertObject and DoRealign protected methods.
- Declare the usual Register procedure needed for registering a custom component with the IDE.
Following this, the unit’s interface section should look like this. I’ve also added a ComponentPlatforms attribute to say the class supports Windows and OS X (‘any’ target would be more exact to be honest):
uses System.SysUtils, System.Classes, FMX.Types; type [ComponentPlatforms(pidWin32 or pidWin64 or pidOSX32)] TListLayout = class(TControl) strict private FVerticalGap: Single; procedure SetVerticalGap(const Value: Single); protected procedure DoRealign; override; procedure DoAddObject(AObject: TFmxObject); override; procedure DoRemoveObject(AObject: TFmxObject); override; published property Align; property Anchors; property ClipChildren; property ClipParent; property Cursor; property DesignVisible; property DragMode; property EnableDragHighlight; property Enabled; property Locked; property Height; property HitTest; property Margins; property Opacity; property Padding; property PopupMenu; property Position; property RotationAngle; property RotationCenter; property Scale; property TouchTargetExpansion; property VerticalGap: Single read FVerticalGap write SetVerticalGap; property Visible; property Width; property OnApplyStyleLookup; property OnDragEnter; property OnDragLeave; property OnDragOver; property OnDragDrop; property OnDragEnd; property OnClick; property OnDblClick; property OnMouseDown; property OnMouseMove; property OnMouseUp; property OnMouseWheel; property OnMouseEnter; property OnMouseLeave; property OnPainting; property OnPaint; property OnResize; end; procedure Register;
The implementation of Register is as you would expect if you’ve ever written a custom VCL control, namely a simple call to RegisterComponents. DoAddObject and DoInsertObject then just call the inherited implementation before requesting the control realigns its children. Lastly, the VerticalGap property setter assigns the backing field before requesting a realignment too:
procedure Register; begin RegisterComponents('Samples', [TListLayout]); end; { TListLayout } procedure TListLayout.DoAddObject(AObject: TFmxObject); begin inherited; Realign; end; procedure TListLayout.DoRemoveObject(AObject: TFmxObject); begin inherited; Realign; end; procedure TListLayout.SetVerticalGap(const Value: Single); begin if Value = FVerticalGap then Exit; FVerticalGap := Value; Realign; end;
The final thing to implement is the DoRealign override. As a bit of an aside, DoRealign itself embodies the XE2 to XE3 FireMonkey transition (a lot of good work done, but a lot still to complete) in microcosm: in XE2, there was just Realign, which was a public, virtual method. As implemented in TControl, it did a whole load of checks to see whether controls should be realigned before finally doing the actual realigning. This was bad design, since if you wished to customise how realignment is performed in a descendant of TControl, you had to duplicate all those initial checks in your Realign override. In XE3, in contrast, Realign has been devirtualised and instead paired with a virtual, protected DoRealign method. In the new scheme, Realign still performs all the initial checks it did before, however it then delegates to DoRealign to do the actual repositioning and resizing. All well and good, but the refactoring wasn’t quite finished – to prevent the possibility of recursive calls to Realign/DoRealign, DoRealign still needs to set a protected FDisableRealign field to True, then reset it to False once it has finished. Really Realign should do that for you though, wrapping the FDisableAlign assignments in a try/finally block – if that were done, FDisableAlign could then be withdrawn into strict private scope. Nonetheless, it’s not a big issue.
Anyhow, here’s what my DoRealign implementation looks like:
procedure TListLayout.DoRealign; var Control: TControl; NextY, StdWidth: Single; begin if ControlsCount = 0 then Exit; FDisableAlign := True; try NextY := Margins.Top; StdWidth := Width - Margins.Left - Margins.Right; for Control in Controls do if Control.Visible then begin NextY := NextY + Control.Padding.Top; Control.SetBounds(Margins.Left + Control.Padding.Left, NextY, StdWidth - Control.Padding.Right - Control.Padding.Left, Control.Height); NextY := NextY + Control.Height + Control.Padding.Bottom + VerticalGap; end; finally FDisableAlign := False; end; end;
If you’re following along, save everything, switch to the Release build configuration before adding and compiling for the Win64 and OS X target platforms (if you only have the Starter edition, that’s fine, however there won’t be any platforms beyond Win32 to compile for). The first time you compile the package there will be a prompt for adding fmx to the package’s requires clause – accept it. Then, toggle back to the default Win32 target, right click on the BPL’s node in the Project Manager, and choose Install. Finally, for each target platform, add the DCU output folder to the IDE’s search path (Tools|Options, Environment Options -> Delphi Options -> Library, Library Path); if desired, also add the .pas folder to the IDE’s browse path. E.g., if I were to save custom control units under E:\Delphi\Lib, this would give a default DCU output folder for 32 bit Windows of E:\Delphi\Lib\Win32\Release. If you toggle the project’s build configuration for Debug and recompile, you can also add the ..\Debug folders to the debug DCU search path as well. [In case it weren't obvious, these instructions are in case you aren't familiar with how to manually install a custom control - FMX or VCL - in the IDE. If you are, then there's nothing particular to my example control, or shouldn't be.] If all goes well, TListLayout should now be available in the Tool Palette when designing a form.
Now, the DoRealign implementation explicitly fulfils requirements (1) to (3) in my original list. Requirement (4) is also implicitly met, since our DoRealign lays out controls in the order they appear in the Controls array property, and that order can be changed by setting a sub-control’s Index as desired. So, if MyPanel is at the bottom of the layout control, setting its Index to 0 will move it to the top. Requirement (5) can then be met simply by nesting the TListLayout inside a TScrollBox, and setting its Align property to alTop:
Here, the form has a top-aligned TToolbar (StyleLookup set to ‘HeaderItemStyle’, as the default toolbar style looks pretty ugly IMO!), followed by a client-aligned TScrollbox with its Padding set to (2, 2, 2, 2). This then contains a top-aligned TListLayout with VerticalSpacing set to 4, with the layout itself containing three top-aligned panels, each of which has its Margins set to (8, 8, 8, 8), a left-aligned TLabel added and a client-aligned TEdit too.