Composing Layouts
FMXER layouts are made of a few containers nested into each other. This page shows the sizing rules and the combinations used most often; every container is described in detail in Containers.
Sizing: top-down
FMXER does not run a measure pass the way Flutter does: each container gives its children a size by a simple rule, and you either pass the size or let the slot take the element's own size (its design size in the .fmx, or the one the configuration procedure sets).
| Container | Rule |
|---|---|
TColumnForm | each AddFrame<T>(AHeight)/AddForm<T>(AHeight) adds a slot of AHeight (default 200), aligned to the top, as wide as the column; with TColumnForm.AUTO_HEIGHT the slot takes the element's height. The column's own Height grows with each slot |
TRowForm | each AddFrame<T>(AWidth) adds a slot of AWidth (default 200), aligned to the left (ElementAlign); with TRowForm.AUTO_WIDTH the slot takes the element's width. The row's Width grows with each slot |
TVertScrollFrame / TVertScrollForm | the content is aligned to the top and keeps its own Height: put a column in it and the scroll box scrolls the column |
THorzScrollFrame | the content is aligned to the left and keeps its own Width: put a row in it |
TCenterForm | the content gets the size you pass (SetContentAsFrame<T>(AWidth, AHeight)), centered |
| everything else | the child fills the slot (Client) |
A slot is a TLayout: the element inside is aligned to it by its own Align (most elements are Client by design), so margins and paddings set on the element apply inside the slot.
Automatic sizes
Pass AUTO_HEIGHT (column) or AUTO_WIDTH (row) instead of a size, and the slot takes the size of the element: the one designed in its .fmx, or the one set in the configuration procedure — margins included. For a form element (a nested column, for example) it is the form's Height/Width after the configuration procedure: a nested column measures itself.
Col
.AddFrame<TTextFrame>(TColumnForm.AUTO_HEIGHT) // 80: the design height of TTextFrame
.AddFrame<TEditFrame>(TColumnForm.AUTO_HEIGHT
, procedure (Edit: TEditFrame)
begin
Edit.SetCaption('Name').SetHeight(90); // 90: set here
end
)
.AddForm<TRowForm>(TColumnForm.AUTO_HEIGHT
, procedure (Row: TRowForm)
begin
Row.SetHeight(60); // a form element: its Height
Row
.AddFrame<TButtonFrame>(TRowForm.AUTO_WIDTH
, procedure (B: TButtonFrame) begin B.SetText('OK').SetWidth(80); end);
end
);The size is taken once, when the element is added: an element that changes size later does not resize its slot. The scroll boxes follow the same rule for a frame content: it keeps its design height (TVertScrollFrame) or width (THorzScrollFrame), or the one set by the configuration procedure.
Since v.1.3
AUTO_HEIGHT/AUTO_WIDTH are new in v.1.3. Before, a frame put in a scroll box got a height (width) of 50 unless the configuration procedure set one.
Column
Col
.AddFrame<TTextFrame>(40
, procedure (Text: TTextFrame)
begin
Text.SetContent('Settings').SetFontSize(22);
end
)
.AddFrame<THorzDividerFrame>(1)
.AddFrame<TEditFrame>(90
, procedure (Edit: TEditFrame)
begin
Edit.SetCaption('Server').SetText('https://example.com');
end
)
.AddFrame<TButtonFrame>(60
, procedure (Button: TButtonFrame)
begin
Button.SetText('Save');
end
)
.SetPadding(10); // padding of the whole column (form helper)Each element fades in, because the default ElementStand of a column is fadeIn.
A scrolling column
Columns do not scroll by themselves. Put the column in a TVertScrollFrame: its height is the sum of the element heights, so the scroll box knows how far to scroll.
Scaffold.SetContentAsFrame<TVertScrollFrame>(
procedure (Scroll: TVertScrollFrame)
begin
Scroll
.SetTouchWidth(30)
.SetContentAsForm<TColumnForm>(
procedure (Col: TColumnForm)
begin
for var I := 1 to 20 do
Col.AddFrame<TTextFrame>(50
, procedure (Text: TTextFrame)
begin
Text.SetContent('Row ' + I.ToString);
end
);
end
);
end
);TouchWidth is a padding on the right side of the scroll box (50 by default): a strip where a finger can drag the content without hitting an element. Set it to 0 on desktop-only apps.
Capturing loop variables
In the example above every closure captures the same I, but each closure runs immediately (inside AddFrame), so each text gets the right value. A closure that runs later (a click handler) would see the last value: copy the loop variable into a local declared inside the loop, or build the handler in a separate function.
Row
Row
.AddFrame<TButtonFrame>(100
, procedure (B: TButtonFrame) begin B.SetText('One'); end)
.AddFrame<TButtonFrame>(100
, procedure (B: TButtonFrame) begin B.SetText('Two'); end)
.AddFrame<TButtonFrame>(100
, procedure (B: TButtonFrame) begin B.SetText('Three'); end);A row inside a column is the usual way to place elements side by side: Col.AddForm<TRowForm>(75, procedure (Row: TRowForm) ...). Call Row.SetElementAlign(TAlignLayout.Right) before adding elements to fill the row from the right. For a horizontally scrolling row, put it in a THorzScrollFrame.
Layers: stack
TStackFrame puts its elements one on top of the other, each filling the frame. The home screens of the HelloWorld and QRCode demos use it for a background, an animation and the content:
Scaffold.SetContentAsFrame<TStackFrame>(
procedure (Stack: TStackFrame)
begin
Stack
.AddFrame<TBackgroundFrame>(
procedure (Back: TBackgroundFrame)
begin
Back.SetFillColor(TAlphaColorRec.White);
end
)
.AddFrame<TAnimatedImageFrame>(
procedure (Anim: TAnimatedImageFrame)
begin
Anim
.LoadFromFile(LocalFile('85570-background-animation-for-a-simple-project.json'))
.SetWrapMode(TSkAnimatedImageWrapMode.FitCrop)
.SetOpacity(0.33);
end
)
.AddFrame<TVertScrollFrame>(
procedure (Scroll: TVertScrollFrame)
begin
Scroll.SetContentAsForm<TColumnForm>(
procedure (Col: TColumnForm)
begin
// the screen content
end
);
end
);
end
);Layers added later are on top. Each layer can be closed by name: AddFrame<T>(AConfigProc, 'name') and CloseFrame('name') (layers without a name are called layer0, layer1...).
Side by side: accessory and pair
TAccessoryFrame has a content area with optional fixed-width elements on the left and on the right — a list row with an icon, a field with a button:
Col.AddFrame<TAccessoryFrame>(70
, procedure (Accessory: TAccessoryFrame)
begin
Accessory
.SetLeftAsFrame<TIconFontsGlyphFrame>(50
, procedure (Glyph: TIconFontsGlyphFrame)
begin
Glyph.SetIcon(IconFonts.MD.qrcode_scan, TAppColors.PrimaryColor);
end
)
.SetContentAsFrame<TButtonFrame>(
procedure (Button: TButtonFrame)
begin
Button.SetText('Read a QR code');
end
)
.SetPadding(10);
end
);THorzPairFrame splits its area in two halves that scale with it:
Col.AddFrame<THorzPairFrame>(60
, procedure (Pair: THorzPairFrame)
begin
Pair.SetContentAsFrame<TButtonFrame, TButtonFrame>(
procedure (Left: TButtonFrame) begin Left.SetText('Cancel'); end
, procedure (Right: TButtonFrame) begin Right.SetText('OK'); end
);
end
);Centered, framed, spaced
| Need | Use |
|---|---|
| an element of fixed size in the middle of the screen | TCenterForm.SetContentAsFrame<T>(AWidth, AHeight, ...) |
| a colored or bordered background behind an element | TBackgroundFrame / TBackgroundForm (SetFillColor, SetStrokeColor, SetStrokeThickness) |
| a titled box | TCardFrame (title bar, optional glyphs, content) |
| space around or inside an element | TMarginFrame / TPaddingFrame / TContainerFrame + SetMargin/SetPadding (see the MarginAndPadding demo) |
| a thin horizontal line | THorzDividerFrame in a 1-pixel slot |
| any FMX control as an element | TContainerFrame.SetContentAs<T: TFmxObject> creates a T inside an empty frame |
Col.AddFrame<TContainerFrame>(60
, procedure (Container: TContainerFrame)
begin
Container.SetContentAs<TSwitch>(
procedure (Switch: TSwitch)
begin
Switch.Align := TAlignLayout.Right;
Switch.IsChecked := True;
end
);
end
);Performance: frames, forms and many elements
Measured on Delphi 13, Windows, release build (times per element):
| Element | Created in |
|---|---|
a frame element (TTextFrame) | 0.2 ms |
a frame container with its two stands (TContainerFrame) | 0.1 ms |
a form element (TColumnForm, TRowForm, TContainerForm) | 2.3–2.5 ms — on Windows each form creates a native window |
a column slot with a text (AddFrame<TTextFrame>), fade-in included | 1.0–1.7 ms |
Two rules follow:
- Prefer frames for elements created many times.
TColumnFrameandTRowFramehave the API ofTColumnFormandTRowFormas frames: a row of three texts in a column costs 2.5 ms withTRowFrame, 6 ms withTRowForm. - Add many elements in a batch. Each slot added to a column realigns the slots already there, so adding N elements one by one costs O(N²): 400 texts take 1.5–2 seconds.
BatchUpdaterealigns once at the end — 0.34 seconds for the same 400:
Col.BatchUpdate(
procedure (C: TColumnFrame)
begin
for var LItem in Items do
C.AddFrame<TRowFrame>(48
, procedure (Row: TRowFrame)
begin
Row
.AddFrame<TTextFrame>(TRowFrame.AUTO_WIDTH)
.AddFrame<TButtonFrame>(80);
end
);
end
);For hundreds of uniform rows, TListViewFrame is still the right choice: it virtualizes its items.
Responsive tips
- Compute sizes from the container when the configuration procedure runs:
Col.AddFrame<TLogoFrame>(Col.Width, ...)makes a square logo (MarginAndPadding demo),Scaffold.Widthgives the screen width (QRCode demo). - Sizes are computed once. For layouts that must react to rotation or resizing, handle
TVertScrollFrame.OnResizedProc, or rebuild the route (CloseRoute+RouteTo). - For long or data-driven lists use TListViewFrame: it virtualizes its items, a column does not.
