# FMXER > FMXER: a declarative, Flutter-inspired visual framework for Delphi FireMonkey (FMX). Build screens by composing frames and forms in code, with named routes, a scaffold, layout containers and ready-made elements. FMXER is an open source (MPL 2.0) framework for Embarcadero Delphi FireMonkey, built on TFrameStand and TFormStand. Source code, releases and demos: https://github.com/andrea-magni/FMXER --- Source: https://andrea-magni.github.io/FMXER/guide/introduction # Introduction **FMXER** is a declarative visual framework for Delphi FireMonkey (FMX), inspired by the way [Google Flutter](https://flutter.dev) builds user interfaces. It was introduced at EKON 23 and [CodeRage 2019](https://youtu.be/RiK2re19Kyk), and has been used since then to build mobile and desktop apps from Delphi. ## The idea In a classic FMX application every screen is a form designed in the IDE: you drop controls, set properties in the Object Inspector and write event handlers. That works, but screens tend to repeat the same pieces (a title bar, a list, a button with an icon, a card) and every variation becomes another form. Flutter takes another road: a screen is a **tree of small widgets**, each configured in code and nested into the next. FMXER brings that approach to FMX, with the tools Delphi already has: - an **element** is a plain `TFrame` or `TForm` descendant, designed once in the IDE (or written in code); - elements are **nested in code**: a container exposes methods like `SetContentAsFrame` or `AddFrame`, which create the child element, configure it through an anonymous method and show it; - most elements have **fluent setters** (`SetTitle`, `SetText`, `SetMargin`...) returning the element itself, so a configuration reads top to bottom; - **screens are routes**: `TNavigator` creates a screen when you route to it and frees it when you close it. ```pascal Scaffold .SetTitle('Library Store') .SetContentAsFrame( procedure (List: TListViewFrame) begin List.AddItem('Book list', '', -1 , procedure (const AItem: TListViewItem) begin Navigator.RouteTo('books'); end ); end ); ``` ## Built on TFrameStand FMXER does not reinvent the plumbing: it is built on [TFrameStand and TFormStand](https://andrea-magni.github.io/TFrameStand/), two components by the same author that show a frame (or the content of a form) inside any control, through a **stand** — a visual layer designed in a `TStyleBook`, with backgrounds, effects and animations. Every FMXER container has its own `TFrameStand` and `TFormStand`: when you call `SetContentAsFrame`, the container asks its `TFrameStand` to create a `T`, place it in the right layout, run the configuration procedure and show it. Closing a route hides and frees the whole tree. What you know about TFrameStand (stands, `OnShow*` animations, `[BeforeShow]` and `[Hide]` methods, `TSubjectInfo`) applies to FMXER elements too. ## What you get | Area | What FMXER provides | |---|---| | Navigation | `TNavigator`: named routes, a stack of open screens, transient routes, delayed routing, create/close events | | Screen structure | `TScaffoldForm`: title bar with an optional detail area, content, floating action buttons, snackbar | | Layout | columns, rows, stacks, vertical and horizontal scroll boxes, centered content, cards, left/right accessories, 50/50 pairs, margins and paddings | | Elements | text, buttons, edits, track bars, color pickers, chips, images, SVG, Lottie animations, Skia paint boxes, QR codes, web views, lists | | Icons | Material Design Icons (5,900+ glyphs) through an icon font and a shared image list | | Styling | `TAppColors` (primary, text, background, divider colors), stands, fluent margin/padding/align helpers | ## Status and requirements FMXER is an open source project (MPL 2.0) at version **1.1**, with more work on `master` since then (new elements and the QRCode demo). It targets **Delphi 12** and **13**, with the Skia built into RAD Studio (Delphi 11 is supported up to v.1.1) — see [Installation](./installation). ::: tip Who is FMXER for? FMXER suits apps made of many similar screens — lists, details, forms, settings, dashboards — where composing in code is faster than designing each screen. You can mix it freely with classic forms: an FMXER element is just a frame, and a classic form can be a route. ::: ## Next steps - [Install FMXER](./installation) and its dependencies. - Build [your first app](./getting-started). - Read the [core concepts](./core-concepts) to understand elements, stands and ownership. --- Source: https://andrea-magni.github.io/FMXER/guide/installation # Installation FMXER has no design-time components: its design-time package only adds FMXER to the splash screen and the About box of the IDE. Your application compiles FMXER's units, so installing FMXER means making its `source` folder (or the compiled units of its runtime packages) and its dependencies reachable by the compiler. Pick one of these ways: | | What it does | Platforms | |---|---|---| | [TMS Smart Setup](#tms-smart-setup) | downloads FMXER, TFrameStand and DelphiZXingQRCode, builds the runtime packages, sets the library path | Win32, Win64 (other platforms: add the source folder) | | [Setup](#setup) | copies FMXER, builds the runtime packages, sets the library path of every platform | all | | [Source](#getting-the-source) | clone the repository, add the folders to the search path | all | ## Requirements | | | |---|---| | Delphi | **12 Athens** and **13 Florence**. Delphi 11 is supported up to FMXER v.1.1, see [below](#delphi-11). | | Platforms | Windows (32/64 bit), macOS (Intel and Apple silicon), iOS (device and Apple silicon simulator), Android (32/64 bit) | | Framework | FireMonkey only | ## Dependencies | Library | Used for | Where to get it | |---|---|---| | [TFrameStand / TFormStand](https://andrea-magni.github.io/TFrameStand/) | everything: every container shows its children through a `TFrameStand`/`TFormStand` | submodule `lib/TFrameStand`, [GetIt](https://getitnow.embarcadero.com/?q=TFrameStand) or the [setup](https://github.com/andrea-magni/TFrameStand/releases/latest) | | Skia | text (`TSkLabel`), SVG, Lottie animations, paint boxes, QR code rendering | built into RAD Studio (units `System.Skia`, `FMX.Skia`) | | [IconFontsImageList](https://github.com/EtheaDev/IconFontsImageList) | the Material Design icon font (`FMXER.IconFontsData`, glyph elements, action buttons with icons) | submodule `lib/IconFontsImageList`, GetIt | | [DelphiZXingQRCode](https://github.com/foxitsoftware/DelphiZXingQRCode) | QR code encoding (`TQRCodeFrame`) | submodule `lib/DelphiZXingQRCode` | Demo-only dependencies: [ZXing.Delphi](https://github.com/Spelt/ZXing.Delphi) 4.0 or later (QR code *reading* in the QRCode demo, submodule `lib/ZXing.Delphi`, branch `4.0`), the [MARS-Curiosity](https://github.com/andrea-magni/MARS) REST client (LibraryStore demo) and FireDAC (DataSet demo). ::: tip TFrameStand version The submodule points to **TFrameStand 2.4**. 2.3 fixed several lifecycle defects of delayed actions and nested stands; 2.4 registers the interactive gestures (zoom, double tap…) of the controls of a frame with the hosting form, so they work on Android. FMXER also compiles with TFrameStand 1.6 (the version of FMXER 1.1), but 2.4 is recommended. ::: ## TMS Smart Setup [TMS Smart Setup](https://doc.tmssoftware.com/smartsetup/) installs FMXER (id `andreamagni.fmxer`) with its dependencies TFrameStand and DelphiZXingQRCode: ```bash tms install andreamagni.fmxer ``` ::: warning Smart Setup registry `tms install andreamagni.fmxer` works once FMXER is listed in the [Smart Setup registry](https://github.com/tmssoftware/smartsetup-registry); until then, use the setup or the source. ::: IconFontsImageList is not a Smart Setup product: install it **before**, from GetIt (*IconFontsImageList*), and Smart Setup finds it in the library path of the IDE. Smart Setup builds the packages for Win32 and Win64 (the platforms of the IconFontsImageList FMX package): for the other platforms add `\source` to the search path of the project, as in [Search path](#search-path). ## Setup The setup (`FMXER__Setup.exe`, see the [releases](https://github.com/andrea-magni/FMXER/releases)) copies the library, builds the runtime packages for Win32 and Win64 and adds the source folder to the library path of every platform, for Delphi 12 and 13. It uses the `$(FMXERDIR)` environment variable of the IDE for the paths, like the TFrameStand setup does with `$(TFRAMESTANDDIR)`. Install the dependencies first: TFrameStand (its [setup](https://github.com/andrea-magni/TFrameStand/releases/latest), GetIt or Smart Setup) and IconFontsImageList (GetIt). The setup checks that their packages are there (`FrameStandPackage.dcp` and `IconFontsImageListFMX.dcp`, in the Dcp folder of the IDE or in the library path) and, if one is missing, stops before installing anything and says what to install. The QR code package is built only when a DelphiZXingQRCode package is installed (Smart Setup builds one); otherwise the QR code units compile from source, with `DelphiZXingQRCode\Source` in the search path. The setup also copies the libraries the QRCode demo needs, in `lib\DelphiZXingQRCode` and `lib\ZXing.Delphi` (the demos find them through their search path). The LibraryStore demo needs the [MARS-Curiosity](https://github.com/andrea-magni/MARS) client: install MARS with its setup. The setup installs the icon font (*Material Design Icons Desktop*) for the current user, see [The icon font](#the-icon-font). Silent install, for every installed Delphi version: `FMXER__Setup.exe /SILENT /RADStudioVersions=all`. ## Packages The runtime packages are in `packages\12Athens` and `packages\13Florence` (`FMXER.groupproj`); the setup and Smart Setup build them for you. | Package | Contains | Requires | |---|---|---| | `FMXERPackage` | every unit of `source` but the QR code ones | `rtl`, `fmx`, `Skia.Package.RTL`, `Skia.Package.FMX`, `FrameStandPackage`, `IconFontsImageListFMX` | | `FMXERQRCodePackage` | `FMXER.QRCodeFrame`, `QRCode.Utils`, `QRCode.Render` | `FMXERPackage`, `DelphiZXingQRCode` | | `dclFMXERPackage` | `FMXER.Registration` (in `packages\`): FMXER in the splash screen and the About box of the IDE | `rtl`, `vcl`, `vclimg`, `designide` | `FMXERPackage` and `FMXERQRCodePackage` are runtime only: build them, don't install them in the IDE. `dclFMXERPackage` is design-time only (the setup installs it). The units are compiled to `dcu\\\`: add that folder to the library path to use the compiled units instead of the source. To build an application *with runtime packages*, add `FMXERPackage` (and `FMXERQRCodePackage`) to the runtime packages list of the project and deploy the `.bpl` files. ## Getting the source Clone the repository with its submodules: ```bash git clone --recursive https://github.com/andrea-magni/FMXER.git ``` If you already cloned it without `--recursive`: ```bash git submodule update --init ``` If TFrameStand and IconFontsImageList are already installed (GetIt, setup, Smart Setup), you only need FMXER's own `source` folder. The demos use the submodules: each `.dproj` has `..\..\source` and the `lib\` folders in its search path. With empty submodule folders the IDE library path is used, so the demos also build against installed libraries. ## Search path Add these folders to **Project > Options > Building > Delphi Compiler > Search path** (all platforms), or to **Tools > Options > Language > Delphi > Library > Library path**: ```text \source \lib\TFrameStand\source \lib\IconFontsImageList\Source \lib\DelphiZXingQRCode\Source ``` Skip the folders of the libraries you have installed in another way. ## Enable Skia FMXER elements use Skia controls. Enable Skia in the project source, before `Application.Initialize`, as all the demos do: ```pascal program MyApp; uses System.StartUpCopy, FMX.Forms, FMX.Skia, Forms.Main in 'Forms.Main.pas' {MainForm}; {$R *.res} begin GlobalUseSkia := True; Application.Initialize; Application.CreateForm(TMainForm, MainForm); Application.Run; end. ``` ## Delphi 11 Since v.1.2 FMXER uses the Skia units of RAD Studio (`System.Skia`, `FMX.Skia`, `FMX.Skia.Canvas`), available since Delphi 12. Up to v.1.1 it used [Skia4Delphi](https://github.com/skia4delphi/skia4delphi) for Delphi 11, whose units are named `Skia` and `Skia.FMX`: on Delphi 11 use FMXER v.1.1. To move a project from FMXER v.1.1 on Delphi 11 to v.1.2 on Delphi 12+, replace `Skia` with `System.Skia`, `Skia.FMX` with `FMX.Skia` and `Skia.FMX.Graphics` with `FMX.Skia.Canvas` in the `uses` clauses: the component classes have the same names, so the `.fmx` files load as they are. ::: details Command-line builds ZXing.Delphi (QRCode demo) needs the `FRAMEWORK_FMX` define, which MSBuild sets for FMX projects and plain `dcc32` does not. `build.cmd` passes it (see [Building & Testing](./building)). ::: ## The icon font Elements with icons (`TIconFontsGlyphFrame`, `TScaffoldForm.AddActionButton` with an icon, `TListViewFrame` images) draw glyphs of the **Material Design Icons Desktop** font through a shared `TIconFontsImageList` (see [Icons](/elements/icons)). The font file is in `media\Material Design Icons Desktop.ttf`, and it must be available at runtime: | Platform | What to do | |---|---| | Windows | install the font (double-click the `.ttf`, *Install*; the setup installs it for you), or put the `.ttf` next to the executable: FMXER registers it with Skia at startup | | macOS | deploy the `.ttf` to `Contents\Resources\Fonts` and add `ATSApplicationFontsPath=Fonts` to the version info keys (as the NewProjectTemplate demo does) | | iOS | deploy the `.ttf` to the application bundle (remote path `.\`), as the NewProjectTemplate demo does | | Android | deploy the `.ttf` to `.\assets\internal` for both **Android and Android64** (two deployment entries): FMXER registers it with Skia at startup (`initialization` of `FMXER.IconFontsData`); without it the icons are missing | Use **Project > Deployment** to add the file; the demos are a working reference. ## Check the installation Open `demos\AllDemos.groupproj` and run **NewProjectTemplate**: a scaffold with the title *New Project*, the FMXER logo and a floating close button. If it runs, everything is in place — move on to [your first app](./getting-started). --- Source: https://andrea-magni.github.io/FMXER/guide/getting-started # Your First App This walkthrough builds a two-screen app: a home screen where you type your name, and a second screen that greets you. Along the way you meet the pieces you will use in every FMXER app: the navigator, the scaffold, a column and a few elements. The finished code is close to the **NewProjectTemplate** and **HelloWorld** [demos](/demos/). ## 1. Create the project 1. **File > New > Multi-Device Application**, *Blank Application*. 2. Save the main form as `Forms.Main.pas` (form name `MainForm`) and the project as `MyFirstApp`. 3. Add the FMXER search path (see [Installation](./installation#search-path)). 4. Drop a **TFormStand** on the main form (it is named `FormStand1`). This is the only component you need at design time: the navigator uses it to show every screen of the app inside the main form. 5. Enable Skia in the project source: ```pascal begin GlobalUseSkia := True; Application.Initialize; Application.CreateForm(TMainForm, MainForm); Application.Run; end. ``` ## 2. Initialize the navigator and define a route A **route** is a named screen. `DefineRoute` records how to build it — a form class and a configuration procedure — but creates nothing yet: the screen is created the first time you route to it. Add an `OnCreate` handler to the main form: ```pascal implementation {$R *.fmx} uses FMXER.Navigator, FMXER.ScaffoldForm; procedure TMainForm.FormCreate(Sender: TObject); begin // the navigator shows the routes through FormStand1, inside this form Navigator(FormStand1); Navigator.DefineRoute('home' , procedure (Home: TScaffoldForm) begin Home.SetTitle('My first FMXER app'); end ); Navigator.RouteTo('home'); end; ``` Run it: the main form shows a **scaffold** — a title bar in the primary color and an empty content area. `Navigator(FormStand1)` initializes the singleton navigator the first time; afterwards `Navigator` (no arguments) returns it from anywhere in the app. ## 3. Compose the content The scaffold content can be any frame or form. A **column** stacks elements vertically, each one with a fixed height: ```pascal uses FMXER.Navigator, FMXER.UI.Misc, FMXER.ScaffoldForm, FMXER.ColumnForm, FMXER.TextFrame, FMXER.EditFrame, FMXER.ButtonFrame; var UserName: string; procedure TMainForm.FormCreate(Sender: TObject); begin Navigator(FormStand1); Navigator.DefineRoute('home' , procedure (Home: TScaffoldForm) begin Home .SetTitle('My first FMXER app') .SetContentAsForm( procedure (Col: TColumnForm) begin Col .AddFrame(60 , procedure (Text: TTextFrame) begin Text .SetContent('Welcome! What is your name?') .SetFontColor(TAlphaColorRec.Black) .SetFontSize(20); end ) .AddFrame(90 , procedure (Edit: TEditFrame) begin Edit .SetCaption('Name') .SetTextPrompt('Type your name here') .SetFocusOnShow(True) .OnChangeProc := procedure (ATracking: Boolean) begin UserName := Edit.GetText; end; end ) .AddFrame(60 , procedure (Button: TButtonFrame) begin Button .SetText('Continue') .SetOnClickHandler( procedure begin Navigator.RouteTo('greeting'); end ); end ) .ContentLayout.SetPadding(10); end ); end ); Navigator.RouteTo('home'); end; ``` Things to notice: - every `Add*`/`Set*` call takes the **element class** as a generic parameter and a **configuration procedure** that receives the new instance, already created and parented; - the configuration procedures are **closures**: the button's handler can use `Navigator`, the edit's handler can write `UserName`, and so on; - `TColumnForm.AddFrame(Height, ...)` returns the column itself, so the elements chain. Sizes are explicit: see [Composing Layouts](./layouts). `TAlphaColorRec` comes from `System.UITypes`, already in the uses clause of a new FMX form. ## 4. A second route Define the greeting screen next to the home route. Its content is built **when you route to it**, so it reads the current value of `UserName`: ```pascal Navigator.DefineRoute('greeting' , procedure (Greeting: TScaffoldForm) begin Greeting .SetTitle('Hello') .SetContentAsFrame( procedure (Text: TTextFrame) begin Text .SetContent('Hello, ' + UserName + '!') .SetFontColor(TAlphaColorRec.Black) .SetFontSize(28); end ) .AddActionButton('X' , procedure begin Navigator.CloseRoute('greeting'); end ); end ); ``` `AddActionButton` adds a round floating button at the bottom right of the scaffold. Closing the route hides the screen (with the stand's animation) and frees it, with everything it contains: the next `RouteTo('greeting')` builds a fresh one. ## 5. Back button and closing the app The navigator keeps a **stack** of the routes you opened. Two lines complete the app: ```pascal procedure TMainForm.FormCreate(Sender: TObject); begin // Android back button, and Esc on desktop: go back one screen Navigator(FormStand1).SetCatchBackKey(True); // ... route definitions as above ... // closing the home route closes the application Navigator.OnCloseRoute := procedure (ARouteName: string) begin if ARouteName = 'home' then Close; end; Navigator.RouteTo('home'); end; ``` With `SetCatchBackKey(True)` Back and Esc close the screen on top; on the home screen they are left to the form (on Android, the app closes). See [The stack and the Back button](./navigation#the-stack-and-the-back-button). ## 6. One unit per route As an app grows, keep each route in its own unit, with a `Define...Route` procedure — the layout used by all the demos: ```pascal unit Routes.home; interface procedure DefineHomeRoute(const ARouteName: string = 'home'); implementation uses FMXER.Navigator, FMXER.ScaffoldForm, FMXER.LogoFrame; procedure DefineHomeRoute(const ARouteName: string); begin Navigator.DefineRoute(ARouteName , procedure (Scaffold: TScaffoldForm) begin Scaffold .SetTitle('New Project') .SetContentAsFrame; end ); end; end. ``` The main form then only initializes the navigator, calls the `Define...Route` procedures and routes to the first screen. ## Next - [Core Concepts](./core-concepts): elements, stands, ownership and the configuration procedures. - [Navigation](./navigation): transient routes, delayed routing, the stack and its events. - The [element catalog](/elements/), to see what you can put on screen. --- Source: https://andrea-magni.github.io/FMXER/guide/core-concepts # Core Concepts An FMXER screen is a **tree of elements**. This page explains what an element is, how a container creates its children, and who owns what. ## Elements An **element** is a `TFrame` or a `TForm` descendant that does one thing: show a text (`TTextFrame`), a button (`TButtonFrame`), a list (`TListViewFrame`), or arrange other elements (`TColumnForm`, `TStackFrame`, `TScaffoldForm`...). Elements are designed in the IDE like any frame or form — the `.fmx` holds controls, styles and animations — and expose a small public API to configure them from code. | Kind | Base class | Shown through | Examples | |---|---|---|---| | frame element | `TFrame` | the container's `TFrameStand` | `TTextFrame`, `TButtonFrame`, `TStackFrame`, `TVertScrollFrame` | | form element | `TForm` | the container's `TFormStand` (the form's controls are moved into the container) | `TScaffoldForm`, `TColumnForm`, `TRowForm`, `TBackgroundForm` | Form elements are useful when an element is a whole screen or a large arrangement: the form designer has style previews and Fire UI views. At runtime there is no visible difference: [TFormStand](https://andrea-magni.github.io/TFrameStand/features/formstand) moves the form's content into the target control. ## Containers and slots A **container** is an element with one or more **slots** where other elements go. For each slot it has a pair of generic methods, one for frames and one for forms: | Container | Slot methods | |---|---| | `TScaffoldForm` | `SetContentAsFrame`, `SetContentAsForm`, `SetTitleDetailContentAsFrame`/`...AsForm`, `AddActionButton...` | | `TColumnForm` / `TRowForm` | `AddFrame(Height or Width, ...)`, `AddForm(...)` | | `TStackFrame` | `AddFrame` (layers, one on top of the other) | | `TAccessoryFrame` | `SetLeftAsFrame`, `SetContentAsFrame`, `SetRightAsFrame` (and `...AsForm`) | | `THorzPairFrame` | `SetLeftContentAsFrame`, `SetRightContentAsFrame`, `SetContentAsFrame` | | `TContainerFrame`, `TBackgroundFrame`, `TVertScrollFrame`, `TCardFrame`, ... | `SetContentAsFrame`, `SetContentAsForm` | Every slot method does the same four things: 1. asks the container's `TFrameStand` (or `TFormStand`) to **create** a `T`, placed in the slot's layout, inside the slot's stand; 2. calls your **configuration procedure** with the new instance; 3. **shows** it (running the stand's `OnShow*` animations); 4. keeps the returned `TSubjectInfo` (the element's handle in TFrameStand), when the container needs it later. ```pascal Card.SetContentAsFrame( procedure (Text: TTextFrame) // 2. called with the new TTextFrame begin Text.SetContent('Inside a card'); end ); ``` The configuration procedure is optional: `Scaffold.SetContentAsFrame;` shows a logo with its design-time settings. ## Configuration procedures are closures Configuration procedures are anonymous methods, so they **capture** the variables around them. This is how elements talk to each other without fields or events wired at design time: ```pascal Home .SetTitle('Hello') .AddActionButton('A' , procedure begin Home.ShowSnackBar('Button A pressed', 2000); // Home is captured end ); ``` Two rules keep this safe: - a closure keeps the **variable**, not the object alive: if it can run after its element was freed (a timer, a message subscription), stop it first — see [State & Events](./state#subscriptions-and-the-lifetime-of-elements); - the configuration procedure runs **once**, when the element is created. To change an element later, keep a reference (a captured variable) or rebuild it. ## Fluent setters Elements expose `Set*` methods that return the element itself, so a configuration reads as a sentence. Since v.1.3 the API follows three rules: - every slot method (`SetContentAsFrame`, `AddFrame`, `SetGlyphAsFrame`, `SetOverlayAsText`...) returns its container; - every clickable element has `SetOnClickProc(TProc)`, and the image elements `SetOnTapHandler`; - the properties you set when building a screen have a setter: `SetTitle`, `SetText`, `SetURL`, `SetContentStand`, `SetElementStand`, `SetTouchWidth`... ```pascal Text .SetContent('Total') .SetFontSize(18) .SetFontColor(TAppColors.PrimaryColor) .SetHorzAlign(TSkTextHorzAlign.Trailing); ``` On top of the elements' own setters, `FMXER.UI.Misc` adds helpers to every `TControl`: `SetMargin`, `SetPadding`, `SetAlignClient`/`Top`/`Bottom`/`Left`/`Right`/`Contents`, `SetHitTest`, `SetWidth`, `SetHeight` (see the [reference](/reference/ui-misc#control-helpers)). ::: warning Keep the type when chaining helpers The helpers return `TControl`, so after `.SetMargin(5)` the element's own setters are not visible any more. Use the generic overload to keep the type, or call the helpers last: ```pascal Button .SetMargin(5) // returns TButtonFrame .SetText('OK') .SetBackgroundVisible(False); Chip .SetText('Login') .SetMargin(0, 5, 0, 0); // fine: nothing after it ``` ::: ## Stands Every slot has a **stand name**: the name of the stand, in the container's style book, that wraps the child. Stands are how FMXER animates elements without code — a stand can fade the element in, slide it, add a background or a shadow. A stand is a style item with a `container` inside; animations named `OnShow*`/`OnHide*` run on show and hide. See [Designing Stands](https://andrea-magni.github.io/TFrameStand/guide/stands) in the TFrameStand documentation. | Container | Stand property | Stands in its style book | |---|---|---| | `TColumnForm` | `ElementStand` | `fadeIn`, also the default: every element of a column fades in | | `TRowForm` | `ElementStand` | `fadeIn`, not the default: call `SetElementStand('fadeIn')` to use it | | `TScaffoldForm` | `ContentStand`, `TitleDetailStand`, `ActionButtonStand` | `snackbar` (used by `ShowSnackBar`) | | other containers | `ContentStand` (and `LeftStand`, `RightStand`, `GlyphStand`...) | none | An empty stand name means the `DefaultStandName` of the container's `TFrameStand`/`TFormStand` (`fadeIn` in `TColumnForm`, `framestand`/`formstand` elsewhere). When the name is not found in the style book, the element goes into a plain `TLayout`: no visual effect, everything else works. Set a stand property **before** the slot method that uses it: ```pascal Row.SetElementStand('fadeIn').AddFrame(120); ``` ## Ownership and lifetime | Object | Created by | Freed when | |---|---|---| | a route's screen (e.g. a `TScaffoldForm`) | the navigator, through its `TFormStand`, at the first `RouteTo` | the route is closed (`CloseRoute`, `StackPop`, `CloseAllRoutes`) | | an element in a slot | the container's `TFrameStand`/`TFormStand` | its container is freed (or its `TSubjectInfo` is closed) | | `TIconFonts`, `TNavigator` | lazily, on first use | at application shutdown | So closing a route frees the whole tree of elements built for it, and the next `RouteTo` builds a new one from the route definition. Nothing in the tree should be freed by hand. ## Where the work is done ```text TMainForm └─ FormStand1 (TFormStand) ← Navigator(FormStand1) └─ route 'home': TScaffoldForm ← DefineRoute ├─ TitleLabel ├─ ContentLayout │ └─ TStackFrame ← SetContentAsFrame │ ├─ TBackgroundFrame ← AddFrame │ └─ TVertScrollFrame ← AddFrame │ └─ TColumnForm ← SetContentAsForm │ ├─ TEditFrame (height 100) ← AddFrame(100, ...) │ └─ TButtonFrame (height 60) ← AddFrame(60, ...) └─ ActionButtonsLayout └─ TActionButtonFrame ← AddActionButton('X', ...) ``` This is, in short, the home screen of the HelloWorld demo. ## TElementDef: definitions as values Columns and rows also accept a `TElementDef`: the configuration procedure plus named parameters, in one value you can build elsewhere and pass around. ```pascal Col.AddFrame( TElementDef.Create(nil, [ Param('Height', 100) ]) ); ``` The parameters understood today are `Height` (`TColumnForm`), `Width` (`TRowForm`) and `OnClickProc` (`TScaffoldForm.AddActionButton`). A `TProc` converts implicitly to a `TElementDef` with no parameters. See [FMXER.UI.Misc](/reference/ui-misc#telementdef). --- Source: https://andrea-magni.github.io/FMXER/guide/layouts # 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](/elements/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(AHeight)`/`AddForm(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(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(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. ```pascal Col .AddFrame(TColumnForm.AUTO_HEIGHT) // 80: the design height of TTextFrame .AddFrame(TColumnForm.AUTO_HEIGHT , procedure (Edit: TEditFrame) begin Edit.SetCaption('Name').SetHeight(90); // 90: set here end ) .AddForm(TColumnForm.AUTO_HEIGHT , procedure (Row: TRowForm) begin Row.SetHeight(60); // a form element: its Height Row .AddFrame(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. ::: info 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 ```pascal Col .AddFrame(40 , procedure (Text: TTextFrame) begin Text.SetContent('Settings').SetFontSize(22); end ) .AddFrame(1) .AddFrame(90 , procedure (Edit: TEditFrame) begin Edit.SetCaption('Server').SetText('https://example.com'); end ) .AddFrame(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. ```pascal Scaffold.SetContentAsFrame( procedure (Scroll: TVertScrollFrame) begin Scroll .SetTouchWidth(30) .SetContentAsForm( procedure (Col: TColumnForm) begin for var I := 1 to 20 do Col.AddFrame(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. ::: warning 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 ```pascal Row .AddFrame(100 , procedure (B: TButtonFrame) begin B.SetText('One'); end) .AddFrame(100 , procedure (B: TButtonFrame) begin B.SetText('Two'); end) .AddFrame(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(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: ```pascal Scaffold.SetContentAsFrame( procedure (Stack: TStackFrame) begin Stack .AddFrame( procedure (Back: TBackgroundFrame) begin Back.SetFillColor(TAlphaColorRec.White); end ) .AddFrame( procedure (Anim: TAnimatedImageFrame) begin Anim .LoadFromFile(LocalFile('85570-background-animation-for-a-simple-project.json')) .SetWrapMode(TSkAnimatedImageWrapMode.FitCrop) .SetOpacity(0.33); end ) .AddFrame( procedure (Scroll: TVertScrollFrame) begin Scroll.SetContentAsForm( procedure (Col: TColumnForm) begin // the screen content end ); end ); end ); ``` Layers added later are on top. Each layer can be closed by name: `AddFrame(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: ```pascal Col.AddFrame(70 , procedure (Accessory: TAccessoryFrame) begin Accessory .SetLeftAsFrame(50 , procedure (Glyph: TIconFontsGlyphFrame) begin Glyph.SetIcon(IconFonts.MD.qrcode_scan, TAppColors.PrimaryColor); end ) .SetContentAsFrame( 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: ```pascal Col.AddFrame(60 , procedure (Pair: THorzPairFrame) begin Pair.SetContentAsFrame( 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(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` creates a `T` inside an empty frame | ```pascal Col.AddFrame(60 , procedure (Container: TContainerFrame) begin Container.SetContentAs( 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`), fade-in included | 1.0–1.7 ms | Two rules follow: - **Prefer frames for elements created many times.** `TColumnFrame` and `TRowFrame` have the API of `TColumnForm` and `TRowForm` as frames: a row of three texts in a column costs 2.5 ms with `TRowFrame`, 6 ms with `TRowForm`. - **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. `BatchUpdate` realigns once at the end — 0.34 seconds for the same 400: ```pascal Col.BatchUpdate( procedure (C: TColumnFrame) begin for var LItem in Items do C.AddFrame(48 , procedure (Row: TRowFrame) begin Row .AddFrame(TRowFrame.AUTO_WIDTH) .AddFrame(80); end ); end ); ``` For hundreds of uniform rows, [TListViewFrame](/elements/lists) is still the right choice: it virtualizes its items. ## Responsive tips - Compute sizes from the container when the configuration procedure runs: `Col.AddFrame(Col.Width, ...)` makes a square logo (MarginAndPadding demo), `Scaffold.Width` gives 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](/elements/lists): it virtualizes its items, a column does not. --- Source: https://andrea-magni.github.io/FMXER/guide/navigation # Navigation `TNavigator` (unit `FMXER.Navigator`) turns the screens of an app into **named routes**. A route is defined once, created on demand when you route to it, and freed when you close it. The navigator keeps a stack of the open routes for the Back button. ## Initialization The navigator is a singleton bound to one `TFormStand`, usually dropped on the main form: ```pascal procedure TMainForm.FormCreate(Sender: TObject); begin Navigator(FormStand1); // first call: creates the navigator on FormStand1 // ... end; ``` After that, `Navigator` (no arguments) returns the same instance from any unit. Calling `Navigator` before it is initialized raises *Navigator has not been initialized*; calling `TNavigator.Init` twice raises *Navigator has been already initialized*, and so does `Navigator(AFormStand)` with a form stand other than the first one (since v.1.4: it used to return the first navigator silently). `TNavigator.Initialized` tells whether it is ready (useful in code that can run during shutdown); `TNavigator.ReleaseInstance` frees it, so that it can be initialized again. ### More than one navigator `Navigator` is a convenience for the app's main navigation, not a limit: a `TNavigator` can be created on any `TFormStand` with `TNavigator.Create`, and has its own routes and stack. Use one for an area with its own navigation — a tab, a side panel — and free it with its owner: ```pascal FTabNavigator := TNavigator.Create(TabFormStand); // a TFormStand on the tab's form or frame FTabNavigator .DefineRoute('list', ...) .DefineRoute('detail', ...); FTabNavigator.RouteTo('list'); // ... FTabNavigator.Free; ``` ## Defining routes ### With a form class ```pascal Navigator.DefineRoute('home' , procedure (Home: TScaffoldForm) begin Home.SetTitle('Home'); end ); ``` `DefineRoute(ARouteName, AConfigProc, AParent, AStandName)` records a route whose screen is a new `T`, created by the navigator's `TFormStand` in `AParent` (default: the `DefaultParent` of the form stand, or its owner — the main form) inside the stand `AStandName`, then configured by `AConfigProc`. ### With a function For full control, define a route with a function that returns the `TSubjectInfo` of the new screen — for example to use a different `TFrameStand`/`TFormStand`, or a frame as a screen: ```pascal Navigator.DefineRoute('about' , function: TSubjectInfo begin Result := MainForm.FrameStand1.New(nil, 'lightbox'); end ); ``` `DefineRoute` returns the navigator, so definitions chain: ```pascal Navigator(FormStand1) .DefineRoute('home', ...) .DefineRoute('detail', ...) .DefineRoute('wait', ...); ``` Each name can be defined once (a second `DefineRoute` with the same name raises an exception). `IsRouteDefined(AName)` checks it. ## Routing | Call | Effect | |---|---| | `RouteTo(AName)` | creates the route if it is not active (calling its definition), pushes it on the stack, fires `OnCreateRoute`, shows it. If it is already active, just shows it again | | `RouteTo(AName, True)` | same, but **transient**: the route is not pushed on the stack | | `CloseRoute(AName)` | removes the route from the active ones and from the stack (wherever it is in the stack), hides it (stand animations), frees it, then fires `OnCloseRoute` | | `StackPop` | closes the route on top of the stack (the Back button) | | `CloseAllRoutes(AFilter)` | closes every active route, or the ones for which `AFilter(Name)` is `True` | | `CloseAllRoutesExcept(AName)` | closes every active route but one | | `RouteToDelayed(AName, ATransient, ADelay_ms)` | `RouteTo` after a delay (default 100 ms) | | `CloseRouteDelayed(AName, ATransient, ADelay_ms)` | `CloseRoute` after a delay | An unknown route name raises *Route definition not found: ...*. ```pascal // open a detail screen, then go back Navigator.RouteTo('detail'); // ... Navigator.CloseRoute('detail'); ``` ### Transient routes A transient route is an overlay that should not take part in Back navigation: a wait screen, a splash, an animation. The HelloWorld and Skia101 demos open one and close it after a few seconds: ```pascal Navigator.RouteTo('spinner', True); Navigator.CloseRouteDelayed('spinner', True, 3000); ``` The navigator remembers which routes were opened as transient: `CloseRoute`, `CloseAllRoutes` and `CloseAllRoutesExcept` handle them without being told (the `ATransient` parameter of `CloseRoute` is kept for compatibility). ::: info Before v.1.2 Up to v.1.1, `CloseRoute` removed the **top** of the stack whatever route it closed, and needed the same `ATransient` value used with `RouteTo`; `CloseAllRoutes` treated every route as non-transient and raised `EListError` (*Unbalanced stack or queue operation*) with a transient route open. On v.1.1, close routes in reverse order of opening and close transient routes with `CloseRoute(AName, True)` first. ::: ### Closing a route from inside it A route often closes itself from one of its own buttons (the X action button of a scaffold). That is fine: `CloseRoute` hides the screen first and frees it afterwards, through the stand. To close a route and open another one in the same handler, close first and then route: ```pascal .AddActionButton('X' , procedure begin Navigator.CloseRoute('settings'); Navigator.RouteTo('home'); end ) ``` If the next screen must appear only after the hide animation, use `RouteToDelayed`. ## The stack and the Back button `Stack` (a `TStack`) holds the names of the non-transient routes, in opening order; `Breadcrumb` returns them as an array, first opened first. The navigator handles the Android Back button and Esc by itself (since v.1.4): ```pascal Navigator(FormStand1) .SetCatchBackKey(True); ``` | Situation | Back / Esc | |---|---| | more than one route on the stack | `StackPop`: the top route closes; the key is cleared, so Android does not close the app | | only the root route | not handled: it goes to the form (on Android the app closes) | | a transient route is open (a wait screen) | consumed, nothing is closed | `CatchBackKey` hooks `OnKeyDown` of the form that owns the navigator's `FormStand`; your own `OnKeyDown` handler still receives every key the navigator does not handle. Without the hook, call `Navigator.HandleBackKey(Key, KeyChar)` from your handler — it returns `True` when it handled the key: ```pascal procedure TMainForm.FormKeyDown(Sender: TObject; var Key: Word; var KeyChar: WideChar; Shift: TShiftState); begin if Navigator.HandleBackKey(Key, KeyChar) then Exit; // other keys... end; ``` ::: info Why not TFrameStand's Push/Pop? TFrameStand 2.3 has its own navigation stack (`Push`/`Pop`) and Back handling (`CatchBackKey` on a stand). The navigator does not delegate to them: `Push` hides the page below while routes stay visible under the new one (overlays, wait screens), routes can be built by any stand (`DefineRoute` with a function), and closing a route through TFrameStand would skip the navigator's bookkeeping (active routes, stack, `OnCloseRoute`). The navigator follows the same rules for the keys instead. If you also show dialogs with `FormStand1.ShowModal` and its `CatchBackKey`, set it after `Navigator.SetCatchBackKey(True)`: the last hook installed runs first, so Back cancels the dialog before it closes a route. ::: ## The safe area Android 15 and later draw every app **edge-to-edge** (RAD Studio 13 targets SDK 35+): the form covers the status bar and the navigation bar. On iOS the notch and the home indicator take part of the screen too. The navigator keeps the routes clear of them by default: - when its `FormStand` is owned by a form, it puts a `TLayout` (`Align = Client`) in that form and makes it the `DefaultParent` of the `FormStand` (unless you set one): the routes created without an explicit parent are shown in it; - it hooks `OnSafeAreaChanged` of the form and gives the safe area insets to that layout as margins. Your own `OnSafeAreaChanged` handler is still called. The area under the system bars shows the background of the form (`Fill`). On Windows and macOS the insets are zero and nothing changes. ```pascal // an app that handles the safe area by itself: before the first route is shown Navigator(FormStand1).SetSafeArea(False); ``` `Navigator.SafeAreaLayout` is the layout (nil when `SafeArea` is `False`). Turning `SafeArea` off while routes are shown in it raises an exception. Routes created with an explicit parent (`DefineRoute(Name, Proc, AParent)`) stay where you put them: give that parent the insets yourself if it covers the whole form. ## Events | Event | Fired | |---|---| | `OnCreateRoute: TProc` | after a route is created by `RouteTo` (not when an active route is shown again) | | `OnCloseRoute: TProc` | after a closed route has been hidden and freed | Both have a fluent setter (`SetOnCreateRoute`, `SetOnCloseRoute`). The QRCode demo uses them to stop the camera when the reader screen closes, to close the app with the home route and to show the breadcrumb in the window caption: ```pascal Navigator.SetOnCloseRoute( procedure (ARouteName: string) begin if ARouteName = 'QRReader' then MainData.StopCameraScanning(True); if ARouteName = 'home' then Close; Caption := string.Join(' - ', Navigator.Breadcrumb); end ).SetOnCreateRoute( procedure (ARouteName: string) begin Caption := string.Join(' - ', Navigator.Breadcrumb); end ); ``` ## Closing the application When the main form closes while routes are open, close the routes first so that their hide animations and `[Hide]` methods run, and let the home route's `OnCloseRoute` close the form: ```pascal procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction); begin if Navigator.ActiveRoutes.Count > 0 then begin Action := TCloseAction.caNone; Navigator.CloseAllRoutes(); end; end; ``` ## Inspecting the navigator | Member | Description | |---|---| | `ActiveRoutes: TDictionary` | the open routes and their infos; `ActiveRoutes['home'].Subject` is the screen | | `RouteDefinitions: TDictionary>` | the definitions | | `IsRouteActive(AName)`, `IsRouteDefined(AName)` | checks | | `FormStand` | the `TFormStand` the navigator was initialized with | The full list is in the [TNavigator reference](/reference/navigator). --- Source: https://andrea-magni.github.io/FMXER/guide/styling # Styling & Theming FMXER elements get their look from three places: the **application colors** in `TAppColors`, the **stands** that wrap them, and the **setters** of each element. ## Application colors `TAppColors` (unit `FMXER.UI.Consts`) holds a handful of class properties that the elements read **when they are created**: | Property | Default | Used by | |---|---|---| | `PrimaryColor` | `MATERIAL_AMBER_800` (`#FF8F00`) | scaffold title bar, action buttons, card title, background frames/forms, chip text and border | | `PrimaryTextColor` | white | scaffold title, `TTextFrame` text | | `SolidBackgroundColor` | light gray | chip background | | `LightBackgroundColor` | silver | card content | | `DividerColor` | `MATERIAL_GREY_800` | `THorzDividerFrame` | Set them once, at startup, before the first route is shown: ```pascal uses FMXER.UI.Consts; procedure TMainForm.FormCreate(Sender: TObject); begin TAppColors.PrimaryColor := TAppColors.MATERIAL_INDIGO_800; TAppColors.PrimaryTextColor := TAlphaColorRec.White; Navigator(FormStand1); // ... end; ``` `TAppColors` also declares the Material Design palette at the 400 and 800 shades (`MATERIAL_RED_400` ... `MATERIAL_BLUE_GREY_800`), see the [reference](/reference/app-colors). ::: tip Text color `TTextFrame` uses `PrimaryTextColor` (white by default) because it is often placed on the primary color: title bars, action buttons, snackbars. On a light background call `SetFontColor`. ::: Changing a `TAppColors` property affects the elements created **afterwards**. To restyle a screen that is already open, close and reopen its route. ## Stands A stand is the visual layer between an element and its slot: a fade, a slide, a shadow, a background. FMXER containers come with a few stands in their own style book (`Stands`): | Container | Stands | |---|---| | `TColumnForm`, `TRowForm` | `fadeIn`: the element fades in (`OnShow_FadeIn`) | | `TScaffoldForm` | `snackbar`: a translucent black band at the bottom; it springs up from the bottom edge (`OnShow_Anim`, elastic) and slides back down (`OnHide_Anim`) | Choose a stand with the container's stand property (`ElementStand`, `ContentStand`, `TitleDetailStand`, ...) before adding the element. To use **your own stands**, design them in a `TStyleBook` (see [Designing Stands](https://andrea-magni.github.io/TFrameStand/guide/stands)) and give it to the container's stands — they are public fields of every container: ```pascal Col.FrameStand1.StandBook := MainForm.MyStands; // a TStyleBook with a 'slideIn' stand Col.FormStand1.StandBook := MainForm.MyStands; Col.ElementStand := 'slideIn'; Col.AddFrame(50); ``` The routes themselves are shown through the navigator's `TFormStand`: give it a style book and pass a stand name to `DefineRoute(AName, AConfigProc, AParent, AStandName)` to animate whole screens. ## Element setters Elements expose setters for what is commonly changed: `TTextFrame.SetFontColor`/`SetFontSize`/`SetFontWeight`/`SetFontFamily`/`SetHorzAlign`/`SetMaxLines`, `TButtonFrame.SetBackgroundFillColor`/`SetBackgroundStrokeColor`/`SetBackgroundOpacity`, `TBackgroundFrame.SetFillColor`/`SetStrokeColor`/`SetStrokeThickness`, `TChipFrame.SetBackgroundColor`/`SetForegroundColor`, and so on. Each [element page](/elements/) lists them. Everything else is reachable: the controls of an element are published fields, so you can always go one level down. ```pascal Button.ButtonControl.StyleLookup := 'listitembutton'; Edit.EditControl.KeyboardType := TVirtualKeyboardType.EmailAddress; List.ListView.Transparent := True; ``` ## Margins, paddings, alignment The `TControl` helpers in `FMXER.UI.Misc` set the common layout properties fluently on any element or control: ```pascal Glyph.SetPadding(5); Text.SetMargin(10, 0, 10, 0); // left, top, right, bottom Logo.SetAlignRight.SetWidth(64); Bubbles.SetHitTest(False); // clicks go through ``` Forms have their own helper with `SetPadding`, `SetWidth` and `SetHeight`. See the [helpers reference](/reference/ui-misc#control-helpers). ## Fonts FMXER uses Skia text (`TSkLabel`) in its text elements. `TTextFrame.SetFontFamily` accepts any family available to Skia; to ship a custom font with the app, deploy it and register it with `TSkDefaultProviders.RegisterTypeface`, as `FMXER.IconFontsData` does for the icon font on Android. --- Source: https://andrea-magni.github.io/FMXER/guide/state # State & Events FMXER has no state-management library of its own: elements are created by closures, and the closures are where state lives. This page collects the patterns used in the demos, and the rules that keep them safe. ## Element events are closures Elements expose their events as anonymous-method properties or setters, not as `TNotifyEvent`s: | Element | Event | |---|---| | `TButtonFrame` | `SetOnClickProc(TProc)` (also `SetOnClickHandler`, `OnClickHandler`), `OnUpdateHandler: TProc` | | `TEditFrame` | `OnChangeProc: TProc` — `True` while typing (`OnChangeTracking`), `False` on `OnChange` | | `TTrackbarFrame` | `OnChangeProc: TProc` | | `TColorPickerFrame` | `OnChangeProc: TProc` | | `TTextFrame` | `SetOnClickProc(TProc)` (also `SetOnClick`) | | `TGlyphFrame` | `SetOnClickProc(TProc)` | | `TChipFrame` | `SetOnClickProc(TProc)` (also `OnClickHandler`) | | `TActionButtonFrame` | `SetOnClickProc(TProc)` (also `OnClickProc`) | | `TImageFrame`, `TSVGFrame`, `TAnimatedImageFrame`, `TQRCodeFrame` | `SetOnTapHandler` / `OnTapHandler` (tap on mobile, mouse up on Windows) | | `TListViewFrame` | a `TListViewItemClickProc` per item (`AddItem`), `SetOnSearchChange` | Assign them in the configuration procedure: ```pascal Col.AddFrame(70 , procedure (Track: TTrackbarFrame) begin Track .SetCaption('Volume') .SetMin(0).SetMax(100).SetValue(50) .SetOnChangeProc( procedure (AValue: Single) begin Track.SetExtraText(Round(AValue).ToString + '%'); end ); end ); ``` ## Sharing values between elements ### Captured variables Elements built in the same procedure can share a local variable. The HelloWorld demo keeps a reference to an edit frame and reads it from a button of another element: ```pascal var LEditFrame: TEditFrame := nil; Col .AddFrame(100 , procedure (Frame: TEditFrame) begin Frame.SetCaption('Name'); LEditFrame := Frame; // keep it for later end ) .AddFrame(60 , procedure (Button: TButtonFrame) begin Button.SetText('Greet').SetOnClickHandler( procedure begin ShowMessage('Hello, ' + LEditFrame.GetText); end ); end ); ``` This is safe as long as both elements belong to the same screen: they are freed together. ### Application state in a data module For values that outlive a screen — the user, a selection, a setting — use a data module or an object created at startup, and let routes read it **when they are built**. The LibraryStore demo stores the selected author in `RemoteData.selectedAuthorId` before routing to the books of that author: ```pascal List.AddItem(Author.Surname + ' ' + Author.Name, '', -1 , procedure (const AItem: TListViewItem) begin RemoteData.selectedAuthorId := AItem.Tag; Navigator.RouteTo('books_by_author'); // reads selectedAuthorId while it builds end ); ``` Because a route is rebuilt each time it is opened, this is often all you need. ## Notifications across screens When an open screen must react to a change made elsewhere, the demos use `System.Messaging`. `TAppState` in the HelloWorld demo sends a message whenever its value changes: ```pascal type TSomethingChangedMsg = class(TMessage); procedure TAppState.SetSomething(const Value: string); begin if FSomething <> Value then begin FSomething := Value; TMessageManager.DefaultManager.SendMessage(Self, TSomethingChangedMsg.Create(FSomething)); end; end; ``` and an element subscribes in its configuration procedure: ```pascal TMessageManager.DefaultManager.SubscribeToMessage(TSomethingChangedMsg , procedure (const Sender: TObject; const M: TMessage) begin QR.SetContent((M as TSomethingChangedMsg).Value); end ); ``` ### Subscriptions and the lifetime of elements ::: danger Unsubscribe when the element goes away A subscription lives in the global message manager, the element does not: when its route closes, the element is freed and the subscription stays. The next message runs the closure on a **freed object** (an access violation, or worse), and opening the route again adds a second subscription. ::: Tie the subscription to the element. A tiny component owned by the element does it — when the element is freed, so is the guard, and the guard unsubscribes: ```pascal uses System.Classes, System.Messaging; type TSubscriptionGuard = class(TComponent) private FMessageClass: TClass; FId: Integer; public constructor Create(const AOwner: TComponent; const AMessageClass: TClass; const AListener: TMessageListenerMethod); reintroduce; overload; constructor Create(const AOwner: TComponent; const AMessageClass: TClass; const AListener: TMessageListener); reintroduce; overload; destructor Destroy; override; end; constructor TSubscriptionGuard.Create(const AOwner: TComponent; const AMessageClass: TClass; const AListener: TMessageListener); begin inherited Create(AOwner); FMessageClass := AMessageClass; FId := TMessageManager.DefaultManager.SubscribeToMessage(AMessageClass, AListener); end; constructor TSubscriptionGuard.Create(const AOwner: TComponent; const AMessageClass: TClass; const AListener: TMessageListenerMethod); begin inherited Create(AOwner); FMessageClass := AMessageClass; FId := TMessageManager.DefaultManager.SubscribeToMessage(AMessageClass, AListener); end; destructor TSubscriptionGuard.Destroy; begin TMessageManager.DefaultManager.Unsubscribe(FMessageClass, FId); inherited; end; ``` ```pascal TSubscriptionGuard.Create(QR, TSomethingChangedMsg , procedure (const Sender: TObject; const M: TMessage) begin QR.SetContent((M as TSomethingChangedMsg).Value); end ); ``` The demos use this guard: HelloWorld declares it in `Data.AppState` and gives one to each QR code that follows the edit; LibraryStore wraps it in `TRetrieveMsg.Subscribe(ABF, ...)` (`Utils.Msg`), owned by the activity animation in the title detail area. Note that hiding an element (`HideTitleDetailContent`) does not free it: the subscription stays active until the route closes. The same rule applies to anything that can call back later: timers, `TDelayedAction`, background threads, data-module listener lists (the QRCode demo clears its camera listeners in `OnCloseRoute`). ## Doing work in the background Long operations (REST calls, database queries) should not block the UI thread while a screen is built. The LibraryStore demo runs its MARS requests asynchronously and fills the list in the callback, while the scaffold's title detail area shows an activity animation: ```pascal List.ItemBuilderProc := procedure begin RemoteData.RetrieveBooks( procedure (ABooks: TArray) // called on the UI thread begin for var LBook in ABooks do List.AddItem(LBook.Title, LBook.Author.Surname); end , procedure (AError: string) begin Scaffold.ShowSnackBar(AError, 3000); end ); end; ``` When the callback can arrive after the screen was closed, check that the route is still active (`Navigator.IsRouteActive`) before touching its elements, as the Skia101 demo does for its QR code timer. --- Source: https://andrea-magni.github.io/FMXER/guide/custom-elements # Writing Your Own Elements Any frame or form can be an FMXER element: `SetContentAsFrame` works with every `TFrame` descendant. A few conventions make your own elements feel like the built-in ones — and the README invites you to contribute them back. ## A simple element A *badge*: a number in a colored circle. 1. **File > New > Multi-Device Frame**, save it as `Frames.Badge.pas`, name it `BadgeFrame`. 2. Set the frame's `Align` to `Client`, so it fills whatever slot it is put in. 3. Drop a `TCircle` (`Background`, `Align = Client`) and a `TSkLabel` inside it (`CountLabel`, `Align = Client`, centered text). 4. Write the public API as fluent setters: ```pascal unit Frames.Badge; interface uses System.SysUtils, System.Types, System.UITypes, System.Classes, FMX.Types, FMX.Controls, FMX.Forms, FMX.Objects, FMX.Skia; type TBadgeFrame = class(TFrame) Background: TCircle; CountLabel: TSkLabel; procedure BackgroundClick(Sender: TObject); private FOnClickProc: TProc; public constructor Create(AOwner: TComponent); override; function SetCount(const ACount: Integer): TBadgeFrame; function SetColor(const AColor: TAlphaColor): TBadgeFrame; function SetOnClickProc(const AProc: TProc): TBadgeFrame; end; implementation {$R *.fmx} uses FMXER.UI.Consts; constructor TBadgeFrame.Create(AOwner: TComponent); begin inherited; // pick the application colors at creation, like the built-in elements Background.Fill.Color := TAppColors.PrimaryColor; CountLabel.TextSettings.FontColor := TAppColors.PrimaryTextColor; end; procedure TBadgeFrame.BackgroundClick(Sender: TObject); begin if Assigned(FOnClickProc) then FOnClickProc(); end; function TBadgeFrame.SetColor(const AColor: TAlphaColor): TBadgeFrame; begin Result := Self; Background.Fill.Color := AColor; end; function TBadgeFrame.SetCount(const ACount: Integer): TBadgeFrame; begin Result := Self; CountLabel.Text := ACount.ToString; end; function TBadgeFrame.SetOnClickProc(const AProc: TProc): TBadgeFrame; begin Result := Self; FOnClickProc := AProc; end; end. ``` Use it like any other element: ```pascal Row.AddFrame(48 , procedure (Badge: TBadgeFrame) begin Badge.SetCount(3).SetOnClickProc( procedure begin Navigator.RouteTo('inbox'); end ); end ); ``` ## Conventions | Convention | Why | |---|---| | `Align = Client` on the frame, and a sensible design size | the slot decides the size; with `AUTO_HEIGHT`/`AUTO_WIDTH` the design size becomes the slot size | | setters named `Set...`, returning the element | fluent configuration, like the built-in elements | | events as `TProc` / `TProc` (`OnClickProc`, `OnChangeProc`) | configured with closures in the configuration procedure | | read `TAppColors` in the constructor | the element follows the application colors | | keep the inner controls as published fields | users can reach them when a setter is missing | | no global variable for the frame (delete the `var BadgeFrame: TBadgeFrame;` the IDE adds) | elements are created many times, by the stands | | override `HitTestChanged` to forward `HitTest` to the inner control | so `SetHitTest(False)` makes the element click-through, as `TImageFrame` and `TBackgroundFrame` do | ## A container element To make an element that hosts other elements, add a `TFrameStand` and a `TFormStand` to it, a layout for each slot, and a pair of generic methods per slot — the pattern of every FMXER container: ```pascal type TPanelFrame = class(TFrame) ContentLayout: TLayout; FrameStand1: TFrameStand; FormStand1: TFormStand; private FContent: TSubjectInfo; FContentStand: string; public function SetContentAsFrame(const AConfigProc: TProc = nil): TPanelFrame; function SetContentAsForm(const AConfigProc: TProc = nil): TPanelFrame; property ContentStand: string read FContentStand write FContentStand; end; function TPanelFrame.SetContentAsFrame(const AConfigProc: TProc): TPanelFrame; begin Result := Self; FContent := FrameStand1.NewAndShow(ContentLayout, ContentStand, AConfigProc); end; function TPanelFrame.SetContentAsForm(const AConfigProc: TProc): TPanelFrame; begin Result := Self; FContent := FormStand1.NewAndShow(ContentLayout, ContentStand, AConfigProc); end; ``` `NewAndShow` creates the element in `ContentLayout`, inside the stand `ContentStand`, calls the configuration procedure and shows it. Keep the returned info if the container needs to hide or show the element later (`FContent.Hide`, `FContent.SubjectShow`), as `TScaffoldForm` does with its title detail area. ## Reacting to show and hide Elements are shown and hidden by TFrameStand, so they can use its [injection attributes](https://andrea-magni.github.io/TFrameStand/features/injection): a field marked `[SubjectInfo]` receives the element's info, a method marked `[Show]`, `[Hide]`, `[BeforeShow]` or `[AfterShow]` is called at that moment. `TEditFrame` uses `[Show]` to focus its edit: ```pascal type TEditFrame = class(TFrame) EditControl: TEdit; private FFocusOnShow: Boolean; protected [SubjectInfo] SI: TSubjectInfo; public [Show] procedure ShowHandler; end; procedure TEditFrame.ShowHandler; begin SI.DefaultShow; // run the stand's show animations if FFocusOnShow and EditControl.CanFocus then TDelayedAction.Execute(10 , procedure begin EditControl.SetFocus; end ); end; ``` A `[Show]` or `[Hide]` method **replaces** the default behaviour: call `SI.DefaultShow` / `SI.DefaultHide` in it. The attributes are declared in the `SubjectStand` unit. ## Contributing Elements live in `source\` with the `FMXER.` prefix (`FMXER.BadgeFrame.pas`), and are listed in `List.md`. Pull requests are welcome on [GitHub](https://github.com/andrea-magni/FMXER). --- Source: https://andrea-magni.github.io/FMXER/guide/faq # FAQ & Troubleshooting ## Building ### `F2613 Unit 'Skia.FMX' not found` Your code (or an element written for FMXER v.1.1) uses the unit names of the standalone Skia4Delphi. Since v.1.2 FMXER requires Delphi 12+ and its built-in Skia: use `System.Skia`, `FMX.Skia` and `FMX.Skia.Canvas`, see [Installation](./installation#delphi-11). ### `F2613 Unit 'SubjectStand' (or 'FMX.IconFontsImageList', 'DelphiZXIngQRCode') not found` A dependency is not in the search path. Either initialize the submodules (`git submodule update --init`) and add their source folders, or install the libraries (GetIt, setup). See [Search path](./installation#search-path). ### The MyTicketing and EKON25 - Weather demos do not compile They were written for FMXER 1.0 and use properties that became fluent setters in 1.1 (`Title` → `SetTitle`, `Text` → `SetText`, `OnClickProc` → `SetOnClickHandler`...). They are not part of `AllDemos.groupproj`; use them as reading material. ## Runtime ### `Navigator has not been initialized` Something called `Navigator` before `Navigator(FormStand1)`. Initialize it first thing in the main form's `OnCreate` (or constructor), before any route definition. ### `Route definition not found: xyz` `RouteTo` was called with a name that was never passed to `DefineRoute`. Route names are case-sensitive. Define every route before the first `RouteTo` — keeping the names in constants helps (the LibraryStore demo does). ### `EListError: Unbalanced stack or queue operation` when closing (v.1.1) `CloseAllRoutes`/`CloseAllRoutesExcept` closed a **transient** route as if it were on the stack. Fixed in v.1.2; on v.1.1 close transient routes with `CloseRoute(AName, True)` before closing the others. See [Navigation](./navigation#transient-routes). ### The Back button closes the wrong screen, or nothing (v.1.1) `CloseRoute` removed the top of the navigator's stack whatever route it closed. Fixed in v.1.2; on v.1.1 close routes in reverse order of opening. ### Text is invisible `TTextFrame` uses `TAppColors.PrimaryTextColor` (white by default), meant for text on the primary color. On a white background call `SetFontColor(TAlphaColorRec.Black)`. ### Icons are squares, or empty The *Material Design Icons Desktop* font is not available. Install it on Windows, deploy it on the other platforms. See [The icon font](./installation#the-icon-font). ### Access violation after closing a screen A closure that references elements of the closed screen ran after the screen was freed: a message subscription, a timer, a background task, a callback. Unsubscribe or cancel it when the element is freed — see [Subscriptions and the lifetime of elements](./state#subscriptions-and-the-lifetime-of-elements). ### `EInvalidCast` from `SetPadding` on a form, or from `AddActionButton(ACaption, AParams)` (v.1.1) Both fixed in v.1.2. On v.1.1 use the non-generic `SetPadding(Value)` on forms, and pass an `OnClickProc` param (or use `AddActionButton(ACaption, AOnClickProc)`). ### The list shows every item twice after pull-to-refresh (v.1.1) Fixed in v.1.2. On v.1.1 clear the items at the start of your `ItemBuilderProc` (`List.ClearItems`), or turn pull-to-refresh off with `List.ListView.PullToRefresh := False`. ### A screen with many elements is slow to open Each form element (`TColumnForm`, `TRowForm`, ...) creates a native window on Windows (about 2.5 ms), and adding elements one by one to a column costs O(N²). Use `TColumnFrame`/`TRowFrame` for columns and rows created many times, and `BatchUpdate` to add many elements; for hundreds of uniform rows use `TListViewFrame`. See [Performance](./layouts#performance-frames-forms-and-many-elements). ### The elements of a row/column overlap or are cut Every `AddFrame` gets the height (column) or width (row) you pass, 200 by default. Pass the size you want, or `AUTO_HEIGHT`/`AUTO_WIDTH` to use the element's own size, and put columns longer than the screen in a `TVertScrollFrame`. See [Composing Layouts](./layouts#sizing-top-down). ## Design ### Can I mix FMXER with classic forms? Yes. An FMXER element is just a frame or a form, and any form can be a route (`DefineRoute`); any frame can be put in an FMXER container (`SetContentAsFrame`). You can also use FMXER containers in a classic form: drop a `TFormStand` on it and call `FormStand1.NewAndShow(Layout1, '', procedure (Col: TColumnForm) ...)` to fill `Layout1` with a column. ### Can I see FMXER screens at design time? No: they are built in code. Each element can be opened in the designer on its own, and the [demos](/demos/) show complete screens. ### How do I pass data to a route? Routes have no parameters: set the value somewhere the route definition can read it (a data module, a field of the main form, a captured variable) and then `RouteTo`. The route's configuration procedure runs at that moment and reads the current value. See [State & Events](./state#application-state-in-a-data-module). ### Is there an AI agent skill for FMXER? Yes: the `fmxer-development` skill in the `Skills/` folder of the repository, installable as a Claude Code plugin, with npx or by hand. See [AI Agent Skill](./agent-skills). The [llms.txt](https://andrea-magni.github.io/FMXER/llms.txt) and [llms-full.txt](https://andrea-magni.github.io/FMXER/llms-full.txt) files of this site give an AI assistant the whole documentation in one request. --- Source: https://andrea-magni.github.io/FMXER/guide/building # Building & Testing The repository has a build script and a DUnitX test suite, used to check every change on the command line. ## build.cmd ```bash build.cmd 13 all demos ``` | Argument | Meaning | |---|---| | version | Delphi version: `13` (default), `12` | | platform | `win32` (default, `dcc32`), `win64` (`dcc64`), `all` (both) | | `demos` | also build every demo project | The arguments can be given in any order. For each platform the script compiles: 1. every unit of `source\` (a generated program that uses them all); 2. the test suite `tests\FMXERTests.dpr`, then runs it; 3. with `demos`, every `.dpr` under `demos\`. Everything is written under `build\\` (ignored by git): the projects and their `.res` files are not touched. At the end it prints a summary of every step of every platform (also in `build\\summary.txt`); the exit code is 0 when every step succeeded. Each step has its log (`*.compile.log`, `FMXERTests.log`) next to its output. Run `build.cmd all demos` before a release: Win64 catches what Win32 hides, such as `Length` and `Count` being `NativeInt` there (the tests compare them with `Assert.AreEqual`). ### Dependencies By default the script uses the submodules in `lib\`. To use other folders — installed libraries, other clones — set these environment variables first: | Variable | Default | |---|---| | `FMXER_TFRAMESTAND` | `lib\TFrameStand\source` | | `FMXER_IFIL` | `lib\IconFontsImageList\Source` | | `FMXER_ZXINGQR` | `lib\DelphiZXingQRCode\Source` | | `FMXER_ZXING` | `lib\ZXing.Delphi\Lib\Classes` (QRCode demo) | | `FMXER_MARS` | none: the MARS-Curiosity `Source` folder (LibraryStore demo) | ## The test suite `tests\FMXERTests.dpr` is a DUnitX console program. FMX runs headless enough on Windows for the tests: forms and frames are created and shown through their stands, but no window appears. Timers and delayed actions are pumped by a small loop (`Tests.Common.Pump`). | Unit | Covers | |---|---| | `Tests.Navigator` | routes, stack and breadcrumb, `StackPop`, transient routes, `CloseAllRoutes`/`CloseAllRoutesExcept`, events, delayed routing | | `Tests.Elements` | scaffold action buttons and snackbar, pair stands, card glyph, icon font cache, list view refresh and item handlers, form helpers | Run it from the IDE or directly: ```bash build\13\Win32\tests\FMXERTests.exe --pause ``` A NUnit-style XML report (`FMXERTests.xml`) is written next to the executable. To test a new defect, add a test that fails first, then fix the code: the tests of v.1.2 were written this way, and 11 of the 21 fail on v.1.1. ## Packages `packages\12Athens` and `packages\13Florence` hold the runtime packages (`FMXERPackage`, `FMXERQRCodePackage`, see [Installation](./installation#packages)) and `FMXER.groupproj`. Build them from the IDE or with MSBuild; to keep the IDE's own `Bpl` and `Dcp` folders untouched (where the installed packages are), pass the output folders: ```bash msbuild packages\13Florence\FMXERPackage.dproj /p:Config=Release /p:Platform=Win64 /p:DCC_BplOutput=C:\Temp\fmxer\bpl /p:DCC_DcpOutput=C:\Temp\fmxer\dcp /p:DCC_DcuOutput=C:\Temp\fmxer\dcu ``` A new unit in `source\` goes in both packages folders: `FMXERPackage`, or `FMXERQRCodePackage` when it uses DelphiZXingQRCode. ## The setup `setup\build-setup.cmd` builds `build\setup\FMXER__Setup.exe` with Inno Setup 6, from a clean export of `HEAD` (committed files only). The setup is based on the same Ethea InnoSetup library as the TFrameStand and MARS-Curiosity setups; the version is `LibraryVersion` in `setup\Setup.iss` (and `version.txt` for TMS Smart Setup, see `tmsbuild.yaml`). --- Source: https://andrea-magni.github.io/FMXER/guide/agent-skills # AI Agent Skill FMXER ships with an [Agent Skill](https://code.claude.com/docs/en/skills) that teaches AI coding agents (Claude Code, Cowork, and any tool supporting the open `SKILL.md` format) how to work with FMXER. It lives in the [`Skills/`](https://github.com/andrea-magni/FMXER/tree/master/Skills) folder of the repository: | Skill | What it does | | --- | --- | | **`fmxer-development`** | FireMonkey apps with FMXER: [named routes](./navigation) with `TNavigator` (Back button, transient routes, passing data), the [scaffold](/elements/scaffold), [containers and explicit sizing](./layouts), [elements](/elements/) and their fluent setters, [icons](/elements/icons) and [`TAppColors`](./styling), [state and events](./state), [your own elements](./custom-elements), with the rules that avoid the common mistakes (invisible white `TTextFrame` text, missing sizes, helpers that return `TControl`, closures that outlive their elements, old Skia unit names...). It includes the exact declarations of the public API, taken from the source. | A skill is a folder with a `SKILL.md` file (instructions, plus a `description` that tells the agent *when* to use it) and `references/` files read on demand. Once installed, the agent uses it by itself when your code uses FMXER or you ask for screens, routes, layouts or elements in an FMXER app. ::: info Since v.1.2 The skill describes v.1.2 and later: Delphi 12+ with the Skia of RAD Studio (`System.Skia`, `FMX.Skia`) and the v.1.2 behaviour of the navigator (transient routes remembered, `CloseRoute` removing the closed route from the stack). ::: ## Installation in Claude Code (recommended: plugin) The FMXER repository is also a **Claude plugin marketplace**. From Claude Code: ``` /plugin marketplace add andrea-magni/FMXER /plugin install fmxer@fmxer ``` The skill is available at once and is updated when a new version is published. It can also be invoked explicitly: ``` /fmxer:fmxer-development ``` FMXER is built on TFrameStand, which has [its own skill](https://andrea-magni.github.io/TFrameStand/guide/agent-skills): install it too if you also use stands, dialogs or the TFrameStand lifecycle directly. ## Installation with npx (any AI tool) For other agentic tools (Cursor, Codex, ...), or without plugins, the [skills CLI](https://github.com/vercel-labs/skills) installs the same skill into any compatible tool (requires Node.js): ``` npx skills add andrea-magni/FMXER ``` ## Manual installation (Claude Code / Cowork) Copy the skill folder into one of the standard skill locations: - `.claude/skills/` inside your project: project-level, shared with your team via git; - `~/.claude/skills/`: personal, available in every project. Example, from your project root (Windows): ``` git clone https://github.com/andrea-magni/FMXER xcopy /E /I FMXER\Skills\fmxer-development .claude\skills\fmxer-development ``` Verify with `/skills`, or ask the agent to add a screen to your app. ## Other AI agents The skill is plain Markdown in the open Agent Skills format. Agents without skill support can read it from their instructions file (`AGENTS.md`, `.cursorrules`, custom instructions...): ```markdown When working on Delphi FireMonkey code that uses FMXER, read and follow Skills/fmxer-development/SKILL.md (and its references/ folder). ``` The [llms.txt](https://andrea-magni.github.io/FMXER/llms.txt) and [llms-full.txt](https://andrea-magni.github.io/FMXER/llms-full.txt) files of this site give an assistant the whole documentation in one request. ## Usage examples With the skill installed, prompts like these produce idiomatic FMXER code: - *"Add a settings screen with a server address, a dark mode switch and a Save button, reachable from the home screen."* (a `Routes.settings.pas` unit with `DefineRoute`, a column in a `TVertScrollFrame`, `TEditFrame`, `TContainerFrame.SetContentAs`, `RouteTo`/`CloseRoute`) - *"Show the customers in a searchable list and open the details of the one I tap."* (`TListViewFrame` with `ItemBuilderProc`, the selection stored before `RouteTo`) - *"Show a spinner while the data loads, then the results."* (a transient route or the scaffold's title detail area, `ShowSnackBar` for errors, `IsRouteActive` in the callback) - *"Update the badge on the home screen when a new message arrives."* (`System.Messaging` with a subscription tied to the element's lifetime) --- Source: https://andrea-magni.github.io/FMXER/elements/ # Element Catalog Every element is a unit in `source\` named `FMXER..pas`, with its `.fmx`. Add the unit to the `uses` clause and pass the class to a container's slot method (`SetContentAsFrame`, `AddFrame`, ...). ## Screens and containers | Element | Kind | Purpose | Page | |---|---|---|---| | `TScaffoldForm` | form | screen with title bar, title detail area, content, floating action buttons, snackbar | [Scaffold](./scaffold) | | `TColumnForm` | form | elements stacked vertically, fixed heights | [Containers](./containers#tcolumnform) | | `TRowForm` | form | elements side by side, fixed widths | [Containers](./containers#trowform) | | `TColumnFrame`, `TRowFrame` | frame | the same as frames: cheaper, for columns and rows created many times | [Containers](./containers#tcolumnframe-and-trowframe) | | `TVertScrollForm` / `TVertScrollFrame` | form / frame | vertical scroll box around one element | [Containers](./containers#tvertscrollframe-and-tvertscrollform) | | `THorzScrollFrame` | frame | horizontal scroll box around one element | [Containers](./containers#thorzscrollframe) | | `TStackFrame` | frame | layers, one on top of the other | [Containers](./containers#tstackframe) | | `TCenterForm` | form | one element of fixed size, centered | [Containers](./containers#tcenterform) | | `TContainerForm` / `TContainerFrame` | form / frame | one element (or any FMX control) in a slot | [Containers](./containers#tcontainerframe-and-tcontainerform) | | `TMarginFrame`, `TPaddingFrame` | frame | a container meant to carry margins or paddings | [Containers](./containers#tmarginframe-and-tpaddingframe) | | `TBackgroundForm` / `TBackgroundFrame` | form / frame | one element on a filled, bordered rectangle | [Containers](./containers#tbackgroundframe-and-tbackgroundform) | | `TAccessoryFrame` | frame | content with optional left and right accessories | [Containers](./containers#taccessoryframe) | | `THorzPairFrame` | frame | two elements, 50% / 50% | [Containers](./containers#thorzpairframe) | | `TCardFrame` | frame | titled card with optional glyphs and content | [Containers](./containers#tcardframe) | | `THorzDividerFrame` | frame | horizontal line | [Containers](./containers#thorzdividerframe) | ## Content | Element | Purpose | Page | |---|---|---| | `TTextFrame` | Skia text, clickable | [Text & Input](./text-and-input#ttextframe) | | `TButtonFrame` | button with caption, extra text and background | [Text & Input](./text-and-input#tbuttonframe) | | `TColorButtonFrame` | button with a color swatch | [Text & Input](./text-and-input#tcolorbuttonframe) | | `TEditFrame` | edit with caption, prompt, extra text, password mode | [Text & Input](./text-and-input#teditframe) | | `TTrackbarFrame` | track bar with caption and extra text | [Text & Input](./text-and-input#ttrackbarframe) | | `TColorPickerFrame` | color panel with caption | [Text & Input](./text-and-input#tcolorpickerframe) | | `TChipFrame`, `TChipsFrame` | a chip; a flow of chips | [Text & Input](./text-and-input#tchipframe-and-tchipsframe) | | `TActionButtonFrame` | round floating button with an overlay element | [Scaffold](./scaffold#action-buttons) | | `TImageFrame` | bitmap image, tappable | [Images & Media](./images-and-media#timageframe) | | `TLogoFrame` | an image from a resource (the FMXER logo by default) | [Images & Media](./images-and-media#tlogoframe) | | `TSVGFrame` | SVG image (Skia) | [Images & Media](./images-and-media#tsvgframe) | | `TAnimatedImageFrame` | Lottie / animated image (Skia) | [Images & Media](./images-and-media#tanimatedimageframe) | | `TActivityBubblesFrame` | animated "working" bubbles with a caption | [Images & Media](./images-and-media#tactivitybubblesframe) | | `TPaintBoxFrame` | Skia paint box with a draw handler | [Images & Media](./images-and-media#tpaintboxframe) | | `TGlyphFrame` | an image of an image list, clickable | [Icons](./icons#tglyphframe) | | `TIconFontsGlyphFrame` | a Material Design icon | [Icons](./icons#ticonfontsglyphframe) | | `TWebViewFrame` | web browser (URL or HTML) | [Images & Media](./images-and-media#twebviewframe) | | `TListViewFrame` | list view with per-item click handlers, search, item builder | [Lists](./lists) | | `TQRCodeFrame` | rounded QR code from text, with logo and custom paint | [QR Codes](./qrcode) | ## Support units | Unit | Content | Page | |---|---|---| | `FMXER.Navigator` | `TNavigator`, `Navigator()` | [TNavigator](/reference/navigator) | | `FMXER.UI.Misc` | `TElementDef`, `Param()`, control and form helpers, `TSubjectInfoContainer`, `LocalFile`, `LocalFilesPath` | [FMXER.UI.Misc](/reference/ui-misc) | | `FMXER.UI.Consts` | `TAppColors` | [TAppColors](/reference/app-colors) | | `FMXER.UI.Types` | `TOnTapHandler` | [Images & Media](./images-and-media#tap-handlers) | | `FMXER.IconFontsData` | `TIconFonts`, `IconFonts()` | [Icons](./icons) | | `Icons.Utils`, `Icons.MaterialDesign` | icon entries, collections, the Material Design Icons table | [Icons](./icons#icon-entries) | | `QRCode.Utils`, `QRCode.Render` | QR code to SVG, SVG to bitmap, rounded renderer | [QR Codes](./qrcode#qrcode-utils) | ::: tip Missing something? Writing an element takes a frame and a few setters: see [Writing Your Own Elements](/guide/custom-elements). ::: --- Source: https://andrea-magni.github.io/FMXER/elements/scaffold # Scaffold `TScaffoldForm` (unit `FMXER.ScaffoldForm`) is the screen skeleton of most FMXER apps, after Flutter's `Scaffold` and Material Design: a title bar, a content area, floating action buttons at the bottom right and a snackbar for short messages. ```text ┌──────────────────────────────┐ │ Title [detail] │ ← TitleLayout (64 px, PrimaryColor, shadow) ├──────────────────────────────┤ │ │ │ content │ ← ContentLayout (client) │ │ │ ( + ) ( X )│ ← ActionButtonsLayout (50 px, bottom) │ ▓▓▓▓ snackbar text ▓▓▓▓▓▓▓▓▓ │ ← OverlayLayout ('snackbar' stand) └──────────────────────────────┘ ``` ## Title ```pascal Scaffold.SetTitle('Library Store'); ``` `SetTitle` sets the text of `TitleLabel` (a `TSkLabel`, 24 pt, `PrimaryTextColor`) and can be called at any time — the LibraryStore demo updates the title with the number of books found once the data arrives. ### Title detail The title bar has a second slot, `TitleDetailLayout`, laid over the whole bar: put an element there and align it where you want it. The LibraryStore demo shows an activity animation on the right while it loads data: ```pascal Scaffold.SetTitleDetailContentAsFrame( procedure (Back: TBackgroundFrame) begin Back .SetContentAsFrame() .SetFillColor(TAlphaColorRec.White) .SetMargin(5, 2, 5, 2) .SetAlignRight .SetWidth(64); end ); // later Scaffold.HideTitleDetailContent; // with an optional delay and a callback Scaffold.ShowTitleDetailContent; ``` `TitleDetailContent` returns the element, `TitleDetailContentVisible` tells whether it is shown. The HelloWorld demo puts a small QR code there. ## Content ```pascal Scaffold.SetContentAsFrame( procedure (List: TListViewFrame) begin // ... end ); ``` `SetContentAsFrame` / `SetContentAsForm` fill `ContentLayout`; `Content` returns the element. The content can be anything — usually a list, a column in a scroll box, or a stack of layers. ## Action buttons Floating round buttons, added from right to left in `ActionButtonsLayout`, each one a `TActionButtonFrame` (a circle in `PrimaryColor` with an element on top): | Overload | Button shows | |---|---| | `AddActionButton(ACaption, AOnClickProc)` | a text (`TTextFrame`) | | `AddActionButton(ACaption, AParams)` | a text; the click handler is the `OnClickProc` param | | `AddActionButton(AImageList, AImageIndex, AOnClickProc)` | an image of an image list (`TGlyphFrame`) | | `AddActionButton(AIcon, AColor, AOnClickProc)` | a Material Design icon in `AColor` | | `AddActionButtonOverlayFrame(AConfigProc, AOnClickProc)` | any frame element | | `AddActionButtonOverlayForm(AConfigProc, AOnClickProc)` | any form element | ```pascal uses FMXER.IconFontsData, FMXER.UI.Consts; Scaffold .AddActionButton(IconFonts.MD.close, TAppColors.PrimaryTextColor , procedure begin Navigator.CloseRoute('home'); end ) .AddActionButton('?' , procedure begin Scaffold.ShowSnackBar('Tap X to close', 2000); end ); ``` `ActionButtonStand` names the stand used for the buttons (none by default). Without an `OnClickProc` param, `AddActionButton(ACaption, AParams)` adds a button with no click handler (v.1.1 raised `EInvalidCast`). ### TActionButtonFrame The button element can also be used on its own: | Member | Description | |---|---| | `SetOverlayAsFrame(AConfigProc)`, `SetOverlayAsForm(AConfigProc)` | the element drawn on the circle (scaled into the inner 80%) | | `SetOverlayAsText(AText, AOnClickProc, AConfigProc)` | shortcut: a `TTextFrame` with `AText` | | `OnClickProc: TProc` / `SetOnClickProc(TProc)` | click handler | | `OverlayStand` | stand of the overlay element | | `BackgroundCircle` | the circle (its `Fill.Color` is `PrimaryColor` at creation) | ## Snackbar ```pascal Scaffold.ShowSnackBar('Saved', 3000); Scaffold.ShowSnackBar('Saved', 3000 , procedure begin Navigator.CloseRoute('edit'); // after the snackbar is gone end ); ``` `ShowSnackBar(AText, ADuration_ms, AThen)` shows a `TTextFrame` in the `snackbar` stand — a translucent black band that springs up from the bottom — hides it after `ADuration_ms` and then calls `AThen`. Several snackbars can be on screen at once. ## Stands | Property | Used by | |---|---| | `ContentStand` | `SetContentAs*` | | `TitleDetailStand` | `SetTitleDetailContentAs*` | | `ActionButtonStand` | `AddActionButton*` | The scaffold's own style book (`Stands`) contains the `snackbar` stand. See [Styling](/guide/styling#stands). ## Members | Member | Description | |---|---| | `SetTitle(ATitle): TScaffoldForm` | title text | | `SetContentAsFrame(AConfigProc)`, `SetContentAsForm(AConfigProc)` | content | | `SetTitleDetailContentAsFrame(AConfigProc)`, `SetTitleDetailContentAsForm(AConfigProc)` | title detail element | | `ShowTitleDetailContent`, `HideTitleDetailContent(ADelay = 0; AThen = nil)` | show/hide the title detail element | | `AddActionButton(...)`, `AddActionButtonOverlayFrame`, `AddActionButtonOverlayForm` | action buttons (see above) | | `ShowSnackBar(AText, ADuration_ms[, AThen])` | snackbar | | `Content`, `TitleDetailContent: TSubject` | the elements | | `TitleDetailContentVisible: Boolean` | | | `ContentStand`, `TitleDetailStand`, `ActionButtonStand: string` | stand names | | `TitleLayout`, `TitleBackground`, `TitleLabel`, `ContentLayout`, `OverlayLayout`, `ActionButtonsLayout` | the controls, for fine tuning | --- Source: https://andrea-magni.github.io/FMXER/elements/containers # Containers Containers arrange other elements. Each one has a `TFrameStand` and a `TFormStand` (public fields `FrameStand1`, `FormStand1`) that create its children, and one or more slots. For how to combine them, see [Composing Layouts](/guide/layouts). ## TColumnForm Unit `FMXER.ColumnForm`. Elements stacked from top to bottom, each in a slot of fixed height. ```pascal Col .AddFrame(40, procedure (T: TTextFrame) begin T.SetContent('Title'); end) .AddFrame(1) .AddForm(75, procedure (Row: TRowForm) begin { ... } end); ``` | Member | Description | |---|---| | `AddFrame(AHeight = 200; AConfigProc = nil): TColumnForm` | adds a frame element in a slot `AHeight` high; `AUTO_HEIGHT`: the element's height | | `AddFrame(AColDef: TElementDef)` | same, height from the `Height` param | | `AddForm(AHeight = 200; AConfigProc = nil)`, `AddForm(AColDef)` | adds a form element; `AUTO_HEIGHT`: the form's `Height` after the configuration procedure | | `Elements: TList` | the slots, in order; `Elements[i].ContainedFrame` returns the element | | `ElementStand: string` / `SetElementStand(AStand)` | stand for the next elements (default stand: `fadeIn`) | | `GetContent: TLayout`, `ContentLayout` | the layout holding the slots | | `DEFAULT_ELEMENT_HEIGHT = 200`, `AUTO_HEIGHT = -1` | see [Automatic sizes](/guide/layouts#automatic-sizes) | The column's `Height` grows with every element (it starts at 1), so a column in a [TVertScrollFrame](#tvertscrollframe-and-tvertscrollform) scrolls over its whole content. `BatchUpdate(ABuildProc): TColumnForm` runs `ABuildProc` with the realign of the slots suspended: use it to add more than a few tens of elements (see [Performance](/guide/layouts#performance-frames-forms-and-many-elements)). ## TColumnFrame and TRowFrame Units `FMXER.ColumnFrame`, `FMXER.RowFrame` (since v.1.4). The same API as `TColumnForm` and `TRowForm` — `AddFrame`, `AddForm`, `AUTO_HEIGHT`/`AUTO_WIDTH`, `TElementDef`, `Elements`, `SetElementStand`, `SetElementAlign` (row), `BatchUpdate` — as **frames**, about ten times cheaper to create than the forms (no native window). Use them for columns and rows created many times: rows of a list-like column, columns in cards. ```pascal Col.AddFrame(TColumnForm.AUTO_HEIGHT , procedure (Row: TRowFrame) begin Row .AddFrame(48 , procedure (G: TIconFontsGlyphFrame) begin G.SetIcon(IconFonts.MD.account); end) .AddFrame(200 , procedure (T: TTextFrame) begin T.SetContent('Andrea').SetFontColor(TAlphaColorRec.Black); end); Row.Height := 48; end ); ``` A frame column starts with `Height` 0 (a row with `Width` 0) and grows with its slots; with `AUTO_HEIGHT` (or in a scroll box) the parent takes that size. Inside a slot of fixed size they fill the slot, like any frame. ## TRowForm Unit `FMXER.RowForm`. The horizontal twin of the column: slots of fixed width, from the left. | Member | Description | |---|---| | `AddFrame(AWidth = 200; AConfigProc = nil): TRowForm`, `AddFrame(AColDef)` | adds a frame element; `Width` param for the definition overload; `AUTO_WIDTH`: the element's width | | `AddForm(AWidth = 200; AConfigProc = nil)`, `AddForm(AColDef)` | adds a form element | | `ElementAlign: TAlignLayout` / `SetElementAlign(AAlign)` | alignment of the slots, `Left` by default (`Right` fills from the right) | | `Elements`, `ElementStand` / `SetElementStand`, `GetContent`, `DEFAULT_ELEMENT_WIDTH = 200`, `AUTO_WIDTH = -1` | as in the column | The row's `Width` grows with every element. `BatchUpdate(ABuildProc): TRowForm` as in the column. ## TVertScrollFrame and TVertScrollForm Units `FMXER.VertScrollFrame`, `FMXER.VertScrollForm`. A `TVertScrollBox` with one element, aligned to the top with its own height — usually a column. ```pascal Scroll .SetTouchWidth(0) .SetContentAsForm( procedure (Col: TColumnForm) begin // elements... end ); ``` | Member | Description | |---|---| | `SetContentAsFrame(AConfigProc)`, `SetContentAsForm(AConfigProc)` | the content; a frame keeps its design height, or the one set by `AConfigProc` | | `SetTouchWidth(AWidth)`, `GetTouchWidth` (and `TouchWidth` in the form) | right padding of the scroll box, 50 by default: a strip to scroll with a finger | | `ContentStand` / `SetContentStand(AStand)` | stand of the content (ignored before v.1.3) | | `OnShowProc`, `OnResizeProc`, `OnResizedProc: TProc` | frame only: after show, on resize, after resize | | `VerticalScrollbox` | the scroll box | ## THorzScrollFrame Unit `FMXER.HorzScrollFrame`. A `THorzScrollBox` with one element aligned to the left with its own width — usually a row. | Member | Description | |---|---| | `SetContentAsFrame(AConfigProc)`, `SetContentAsForm(AConfigProc)` | the content; a frame keeps its design width, or the one set by `AConfigProc` | | `TouchHeight` / `SetTouchHeight(AHeight)` | bottom padding of the scroll box | | `ContentStand` / `SetContentStand(AStand)`, `HorizontalScrollBox` | | ## TStackFrame Unit `FMXER.StackFrame`. Layers: each element fills the frame, the last added is on top. | Member | Description | |---|---| | `AddFrame(AConfigProc = nil; AName = ''): TStackFrame` | adds a layer; unnamed layers are called `layer0`, `layer1`, ... | | `CloseFrame(AName)` | hides and closes a layer | | `DefaultStand` | stand of the layers (the `DefaultStandName` of its frame stand) | Frames only (no `AddForm`). ## TCenterForm Unit `FMXER.CenterForm`. One element of a given size in the center of the form. ```pascal Center.SetContentAsFrame(240, 240 , procedure (QR: TQRCodeFrame) begin QR.SetContent('https://github.com/andrea-magni/FMXER'); end ); ``` | Member | Description | |---|---| | `SetContentAsFrame(AWidth, AHeight, AConfigProc): TCenterForm` | | | `SetContentAsForm(AWidth, AHeight, AConfigProc): TCenterForm` | | | `ContentStand` / `SetContentStand(AStand)` | | ## TContainerFrame and TContainerForm Units `FMXER.ContainerFrame`, `FMXER.ContainerForm`. The simplest container: one element filling a slot. Routes often use `TContainerForm` for screens that are a single element (a full-screen animation, a wait screen). | Member | Description | |---|---| | `SetContentAsFrame(AConfigProc)`, `SetContentAsForm(AConfigProc)` | the content | | `SetContentAs(AConfigProc): TContainerFrame` | frame only: creates any FMX object `T` (a `TSwitch`, a `TCalendar`, a `TMapView`...) inside an empty frame | | `Content: TSubjectInfo`, `ContentStand` / `SetContentStand(AStand)` | | ```pascal Navigator.DefineRoute('bubbles' , procedure (Form: TContainerForm) begin Form.SetContentAsFrame; end ); ``` ## TMarginFrame and TPaddingFrame Units `FMXER.MarginFrame`, `FMXER.PaddingFrame`. Both are `TContainerFrame` descendants with nothing added: their name says what you will set on them. The MarginAndPadding demo shows the difference: ```pascal .AddFrame(ACol.Width , procedure (AM: TMarginFrame) begin AM.SetContentAsFrame.SetMargin(50); // space outside the frame end ) .AddFrame(ACol.Width , procedure (AP: TPaddingFrame) begin AP.SetContentAsFrame.SetPadding(50); // space inside the frame end ) ``` ## TBackgroundFrame and TBackgroundForm Units `FMXER.BackgroundFrame`, `FMXER.BackgroundForm`. One element on a `TRectangle` filled with `PrimaryColor`. | Member | Description | |---|---| | `SetContentAsFrame(AConfigProc)`, `SetContentAsForm(AConfigProc)` | the content, inside the rectangle | | `SetFillColor(AColor)`, `SetStrokeColor(AColor)`, `SetStrokeThickness(AThickness)` | fluent setters | | `Fill: TBrush`, `Stroke: TStrokeBrush` | the rectangle's brushes, for gradients and bitmaps | | `Content`, `ContentStand` / `SetContentStand(AStand)`, `BackgroundRectangle` | | `TBackgroundFrame` forwards `HitTest` to the rectangle, so `SetHitTest(False)` makes it click-through. ## TAccessoryFrame Unit `FMXER.AccessoryFrame`. A content area with optional fixed-width accessories on the left and on the right (hidden until set). | Member | Description | |---|---| | `SetContentAsFrame(AConfigProc)`, `SetContentAsForm(AConfigProc)` | the content | | `SetLeftAsFrame(AWidth, AConfigProc)`, `SetLeftAsForm(AWidth, AConfigProc)` | the left accessory, `AWidth` wide | | `SetRightAsFrame(AWidth, AConfigProc)`, `SetRightAsForm(AWidth, AConfigProc)` | the right accessory | | `SetContentStand`, `SetLeftStand`, `SetRightStand` (and the properties `ContentStand`, `LeftStand`, `RightStand`) | stands | All the methods return the frame. Example in [Composing Layouts](/guide/layouts#side-by-side-accessory-and-pair). ## THorzPairFrame Unit `FMXER.HorzPairFrame`. Two halves that scale with the frame (`Align = Scale`). | Member | Description | |---|---| | `SetLeftContentAsFrame(AConfigProc)`, `SetLeftContentAsForm(AConfigProc)` | left half | | `SetRightContentAsFrame(AConfigProc)`, `SetRightContentAsForm(AConfigProc)` | right half | | `SetContentAsFrame(ALeftConfigProc, ARightConfigProc)`, `SetContentAsForm(...)` | both at once | | `SetLeftContentStand(AStandName)`, `SetRightContentStand(AStandName)` (and `GetLeftContentStand`, `GetRightContentStand`) | stands of the halves; set them before the content (they were ignored up to v.1.1) | ## TCardFrame Unit `FMXER.CardFrame`. A card: a title bar in `PrimaryColor` with optional glyphs on the left and right, and a content area in `LightBackgroundColor`. ```pascal Col.AddFrame(150 , procedure (Card: TCardFrame) begin Card .SetTitle('Today') .SetGlyphAsFrame( procedure (Glyph: TIconFontsGlyphFrame) begin Glyph.SetIcon(IconFonts.MD.weather_sunny, TAlphaColorRec.White); end ) .SetContentAsFrame( procedure (Text: TTextFrame) begin Text.SetContent('Sunny, 24°').SetFontColor(TAlphaColorRec.Black); end ); end ); ``` | Member | Description | |---|---| | `Title: string` / `SetTitle(ATitle)` | title text (a `TTextFrame` in the title bar) | | `SetGlyphAsFrame`, `SetGlyphAsForm` | left glyph (50 px) | | `SetGlyphRAsFrame`, `SetGlyphRAsForm` | right glyph | | `SetContentAsFrame`, `SetContentAsForm` | content | | `ContentStand` / `SetContentStand(AStand)`, `GlyphStand`, `GlyphRStand` | stands | | `Detail: string` | stored, not displayed (v.1.1) | ## THorzDividerFrame Unit `FMXER.HorzDividerFrame`. A horizontal `TLine` in `TAppColors.DividerColor`. Give it a 1-pixel slot: `Col.AddFrame(1)`. ## TSubjectInfoContainer Unit `FMXER.UI.Misc`. The `TLayout` used as slot by columns and rows; it keeps the `TSubjectInfo` of its element: ```pascal var LText := Col.Elements[0].ContainedFrame; if Assigned(LText) then LText.SetContent('Updated'); ``` --- Source: https://andrea-magni.github.io/FMXER/elements/text-and-input # Text & Input Elements that show text or take input. All of them use Skia labels (`TSkLabel`) for their text, and fill their slot (`Align = Client`). ## TTextFrame Unit `FMXER.TextFrame`. A text, centered by default, in `TAppColors.PrimaryTextColor`. ```pascal Col.AddFrame(80 , procedure (Text: TTextFrame) begin Text .SetContent('FMXER builds this screen in code') .SetFontColor(TAlphaColorRec.Black) .SetFontSize(18) .SetFontWeight(TFontWeight.Bold) .SetHorzAlign(TSkTextHorzAlign.Leading) .SetMaxLines(2) .SetOnClickProc( procedure begin ShowMessage('Clicked'); end ); end ); ``` | Setter | Getter | Description | |---|---|---| | `SetContent(AText)` | `GetContent` | the text | | `SetFontColor(AColor)` | `GetFontColor` | | | `SetFontSize(ASize)` | `GetFontSize` | | | `SetFontFamily(AFamily)` | | | | `SetFontWeight(TFontWeight)` | `GetFontWeight` | | | `SetHorzAlign(TSkTextHorzAlign)` | `GetHorzAlign` | `Leading`, `Center`, `Trailing`, `Justify` | | `SetVertAlign(TTextAlign)` | `GetVertAlign` | | | `SetMaxLines(ALines)` | `GetMaxLines` | | | `SetOnClickProc(TProc)` (or `SetOnClick`) | `GetOnClickProc` | | The label is `TextContent`. `TTextFrame` is also what the scaffold uses for snackbars and for text action buttons. ## TButtonFrame Unit `FMXER.ButtonFrame`. A `TButton` driven by a `TAction`, with an optional caption above, an extra text below and a background rectangle. ```pascal Col.AddFrame(60 , procedure (Button: TButtonFrame) begin Button .SetText('Save') .SetBackgroundVisible(False) .SetOnClickProc( procedure begin // save end ); Button.OnUpdateHandler := procedure (AAction: TAction) begin AAction.Enabled := Document.Modified; end; end ); ``` | Member | Description | |---|---| | `SetText(AText)` | the button text (the action's caption) | | `SetCaption(ACaption)` | label above the button | | `SetExtraText(AText)` | label below the button | | `SetOnClickProc(TProc)` (or `SetOnClickHandler`) / `OnClickHandler` | click (the action's `OnExecute`) | | `OnUpdateHandler: TProc` | the action's `OnUpdate`: enable, check, rename the button | | `SetBackgroundFillColor`, `SetBackgroundStrokeColor`, `SetBackgroundStrokeThickness`, `SetBackgroundOpacity`, `SetBackgroundVisible` | the rectangle behind the button | | `BackgroundFill`, `BackgroundStroke` | its brushes | | `IsDefault` | the button's `Default` | | `ButtonControl`, `ButtonAction`, `CaptionLabel`, `ExtraLabel`, `BackgroundRectangle` | controls | ## TColorButtonFrame Unit `FMXER.ColorButtonFrame`. A `TButtonFrame` with a color swatch (`ColorRect`, a rounded rectangle) on the right. The QRCode demo uses two of them to pick the foreground and background colors of the code. | Member | Description | |---|---| | `SetColorValue(AColor)` / `GetColorValue` | swatch color | | `SetColorStrokeValue(AColor)` / `GetColorStrokeValue` | swatch border | | `SetColorVisible(AVisible)` / `GetColorVisible` | | Plus everything of `TButtonFrame` (the inherited setters return `TButtonFrame`: call the color setters first). ## TEditFrame Unit `FMXER.EditFrame`. A `TEdit` with an optional caption above and an extra text below (each visible only when not empty). ```pascal Col.AddFrame(100 , procedure (Edit: TEditFrame) begin Edit .SetCaption('Password') .SetTextPrompt('at least 8 characters') .SetPassword(True) .SetFocusOnShow(True) .OnChangeProc := procedure (ATracking: Boolean) begin if not ATracking then // OnChange: editing finished Edit.SetExtraText(IfThen(Length(Edit.GetText) < 8, 'Too short', '')); end; end ); ``` | Setter | Getter | Description | |---|---|---| | `SetText(AText)` | `GetText` | | | `SetCaption(ACaption)` | `GetCaption` | label above | | `SetTextPrompt(APrompt)` | `GetTextPrompt` | placeholder | | `SetExtraText(AText)` | `GetExtraText` | label below (hints, errors) | | `SetPassword(Boolean)` | `GetPassword` | | | `SetFocusOnShow(Boolean)` | `GetFocusOnShow` | focus the edit when the element is shown | | `OnChangeProc: TProc` | | `True` from `OnChangeTracking` (every keystroke), `False` from `OnChange` | `IfThen` is in `System.StrUtils`. The edit is `EditControl`: set `KeyboardType`, `ReturnKeyType`, `MaxLength` there. ## TTrackbarFrame Unit `FMXER.TrackbarFrame`. A `TTrackBar` with caption and extra text. The QRCode demo uses it for the corner radius of the modules. | Setter | Getter | |---|---| | `SetValue`, `SetMin`, `SetMax`, `SetFrequency`, `SetTracking` | `GetValue`, `GetMin`, `GetMax`, `GetFrequency`, `GetTracking` | | `SetCaption(ACaption)`, `SetExtraText(AText)` | `GetCaption`, `GetExtraText` | | `SetCaptionPosition(TCaptionPosition; AMargin = 5)` | `Left`, `Right`, `Top`, `Bottom` | | `SetOnChangeProc(TProc)` / `OnChangeProc` | the new value | ## TColorPickerFrame Unit `FMXER.ColorPickerFrame`. A `TColorPanel` (hue, saturation, brightness) with caption and extra text. | Member | Description | |---|---| | `SetColor(AColor)` / `GetColor` | | | `SetCaption`, `SetExtraText` (and getters) | labels | | `OnChangeProc: TProc` | the new color | The QRCode demo defines one route per color to pick (`DefineColorPickerRoute`), with a getter and a setter for the value. ## TChipFrame and TChipsFrame Units `FMXER.ChipFrame`, `FMXER.ChipsFrame`. A chip is a small rounded button (`TSpeedButton` on a `TRoundRect`); `TChipsFrame` lays chips out in a `TFlowLayout` that wraps. ```pascal Col.AddFrame(50 , procedure (Chips: TChipsFrame) begin Chips .SetHorizontalGap(5) .SetVerticalGap(5) .DefaultConfig := procedure (Chip: TChipFrame) begin Chip.Width := 60; Chip.OnClickHandler := procedure begin ShowMessage(Chip.Button.Text); end; end; Chips .AddChip('Delphi') .AddChip('FMX') .AddChip('Skia' , procedure (Chip: TChipFrame) begin Chip.SetBackgroundColor(TAppColors.PrimaryColor).SetForegroundColor(TAlphaColorRec.White); end ); end ); ``` | `TChipFrame` | | |---|---| | `SetText(AText)` | | | `SetBackgroundColor(AColor)` | fill (default `SolidBackgroundColor`) | | `SetForegroundColor(AColor)` | text and border (default `PrimaryColor`) | | `SetOnClickProc(TProc)` / `OnClickHandler: TProc` | | | `TChipsFrame` | | |---|---| | `AddChip(AText, AConfigProc = nil)` | adds a chip; without `AConfigProc`, `DefaultConfig` is applied | | `DefaultConfig: TProc` | configuration for chips added without their own | | `SetHorizontalGap`, `SetVerticalGap` | spacing of the flow layout | Chips are plain frames created by `TChipsFrame` (not through a stand), so they have no show animation. --- Source: https://andrea-magni.github.io/FMXER/elements/images-and-media # Images & Media Elements that draw: bitmaps, SVG, Lottie animations, custom drawing with Skia, and a web view. The Skia101 demo shows most of them. ## Tap handlers `TImageFrame`, `TSVGFrame`, `TAnimatedImageFrame` (and `TQRCodeFrame`) have an `OnTapHandler`: ```pascal TOnTapHandler = reference to procedure(AImage: TFMXObject; APoint: TPointF); ``` Assign it with the `OnTapHandler` property or the fluent `SetOnTapHandler`. It is called on `OnTap` on mobile and on `OnMouseUp` on Windows (where FMX raises no tap without a touch screen). The point is in the coordinates of the inner control. These elements also forward `HitTest` to the inner control: `SetHitTest(False)` makes them click-through, `SetHitTest(True)` lets them receive taps. The type is declared once, in `FMXER.UI.Types`; the element units alias it, so a variable of type `TOnTapHandler` can be assigned to any of these elements (since v.1.2.1: before, each unit had its own type). ## TImageFrame Unit `FMXER.ImageFrame`. A `TImage` (`ContentImage`, also exposed as `Image`); add `FMX.Objects` to the `uses` clause to work with it. ```pascal Scaffold.SetContentAsFrame( procedure (Img: TImageFrame) begin Img.Image.Bitmap.LoadFromFile(LocalFile('photo.jpg')); Img.Image.WrapMode := TImageWrapMode.Fit; Img.SetHitTest(True); Img.OnTapHandler := procedure (AImage: TFMXObject; APoint: TPointF) begin Navigator.CloseRoute('image'); end; end ); ``` ## TLogoFrame Unit `FMXER.LogoFrame`. A `TImage` (`LogoImage`) showing the FMXER logo by default — the placeholder used in many demos. `SetLogoResource('MyLogo')` (or the `LogoResource` property) loads a bitmap from an `RCDATA` resource of the application. ## TSVGFrame Unit `FMXER.SVGFrame`. A Skia `TSkSvg` (`SVG`). | Member | Description | |---|---| | `LoadFromFile(AFileName[, AEncoding])` | reads the SVG source from a file | | `SetSVGSource(ASource)` | SVG source as a string | | `SetWrapMode(TSkSvgWrapMode)` | `Fit`, `FitCrop`, `Stretch`, `Tile`, ... | | `SetOverrideColor(AColor)` | paints every shape in one color (monochrome icons) | | `SetOpacity(AOpacity)` | | | `OnTapHandler` | | ```pascal SVGF .LoadFromFile(LocalFile('TSkSvg.svg')) .SetWrapMode(TSkSvgWrapMode.Tile) .SetOverrideColor(TAlphaColorRec.Orange) .SetOpacity(0.33); ``` ## TAnimatedImageFrame Unit `FMXER.AnimatedImageFrame`. A Skia `TSkAnimatedImage` (`ContentImage`, also `Image`): Lottie JSON, animated GIF and WebP. | Member | Description | |---|---| | `LoadFromFile(AFileName)` | loads the animation | | `SetWrapMode(TSkAnimatedImageWrapMode)` | | | `SetAnimationSpeed(ASpeed)` | 1 = normal speed | | `SetOpacity(AOpacity)` | | | `OnTapHandler` | | The `media` folder of the repository has several Lottie animations from [LottieFiles](https://lottiefiles.com) used by the demos (backgrounds, loaders, success marks). ## TActivityBubblesFrame Unit `FMXER.ActivityBubblesFrame`. A Lottie "bubbles" animation (embedded in the `.fmx`) with a caption: `SetText(AText)` or the `Text` property. Used as a busy indicator: a transient route with a `TContainerForm` around it, or in the scaffold's title detail area. ## TPaintBoxFrame Unit `FMXER.PaintBoxFrame`. A Skia paint box (`PaintBox: TSkPaintBox`): draw anything with the Skia canvas. ```pascal Scaffold.SetContentAsFrame( procedure (Paint: TPaintBoxFrame) begin Paint.SetOnDrawHandler( procedure (const ACanvas: ISkCanvas; const ADest: TRectF; const AOpacity: Single) begin var LPaint: ISkPaint := TSkPaint.Create; LPaint.Color := TAppColors.PrimaryColor; LPaint.AntiAlias := True; ACanvas.DrawCircle(ADest.CenterPoint, ADest.Width / 4, LPaint); end ); end ); ``` | Member | Description | |---|---| | `SetOnDrawHandler(TOnDrawHandler)` / `OnDrawHandler` | `procedure(const ACanvas: ISkCanvas; const ADest: TRectF; const AOpacity: Single)` | | `SetOnTapHandler(TPaintBoxTapHandler)` / `OnTapHandler` | `TPaintBoxTapHandler = reference to procedure(APaintBox: TObject; APoint: TPointF)` (the old name `TOnTapHandler` of this unit is deprecated) | Call `PaintBox.Redraw` to repaint. The Skia101 demo fills one with a sweep gradient, the QRCode demo draws the camera frame and the detected points on it. ## TWebViewFrame Unit `FMXER.WebViewFrame`. A `TWebBrowser` (`WebBrowser1`). | Member | Description | |---|---| | `URL: string` / `SetURL(AURL)` | navigates to a URL | | `HTML: string` / `SetHTML(AHTML)` | loads an HTML string (`LoadFromStrings`) | ```pascal Scaffold.SetContentAsFrame( procedure (Web: TWebViewFrame) begin Web.URL := 'https://github.com/andrea-magni/FMXER'; end ); ``` `TWebBrowser` is a native control: it is drawn above the FMX controls, so stand animations and overlapping elements do not apply to it. On Windows it uses Edge WebView2 when available (see the FMX documentation for the runtime requirements). ## Loading files: LocalFile `LocalFile(AFileName)` (unit `FMXER.UI.Misc`) is the helper the demos use to find their media: on Windows it returns `..\..\..\..\media\` (relative to the demo executable in `demos\\Win32\Debug`), elsewhere `TPath.GetDocumentsPath + AFileName` (deploy the files to `.\assets\internal` on Android, to `StartUp\Documents` on iOS). The folder is the `LocalFilesPath` variable: set it to use `LocalFile` with the folder of your own app. --- Source: https://andrea-magni.github.io/FMXER/elements/lists # Lists `TListViewFrame` (unit `FMXER.ListViewFrame`) wraps a FireMonkey `TListView` — virtualized, native-looking, with search and pull-to-refresh — and adds a click handler per item. Use it for long or data-driven lists, where a column of elements would be too heavy. ## Static items ```pascal Scaffold.SetContentAsFrame( procedure (List: TListViewFrame) begin List.ItemAppearance := 'ImageListItem'; List.CanSwipeDelete := False; List.AddItem('Books', '', UIUtils.BookImageIndex , procedure (const AItem: TListViewItem) begin Navigator.RouteTo('books_all'); end ); List.AddItem('Authors', '', UIUtils.AuthorImageIndex , procedure (const AItem: TListViewItem) begin Navigator.RouteTo('authors'); end ); end ); ``` `AddItem(AText, ADetailText = ''; AImageIndex = -1; AOnSelect = nil): TListViewItem` adds an item and returns it, so you can set its `Tag`, `Data` or `Objects`. `AOnSelect` is called when the item is clicked. Images come from `ListView.Images`, which is the shared [icon font image list](./icons) unless you assign another one. ## Items from data: the item builder Set `ItemBuilderProc` to a procedure that adds the items. It runs at once and again on pull-to-refresh and `RefreshList`: ```pascal List.ItemAppearance := 'ImageListItemBottomDetail'; List.ItemBuilderProc := procedure begin RemoteData.RetrieveBooks( procedure (ABooks: TArray) begin List.SearchVisible := Length(ABooks) > 3; for var LBook in ABooks do begin var LItem := List.AddItem(LBook.Title , LBook.Author.Surname + ', ' + LBook.Author.Name , UIUtils.BookImageIndex , procedure (const AItem: TListViewItem) begin RemoteData.selectedBookId := AItem.Tag; Navigator.RouteTo('book_single'); end ); LItem.Tag := LBook.id; end; end ); end; ``` The LibraryStore demo is built this way, with the data coming from a MARS REST server. The frame clears the list before calling the builder. `PullToRefresh` is on by default; turn it off with `List.ListView.PullToRefresh := False` if you do not need it. ::: info Before v.1.2 Up to v.1.1 pull-to-refresh did not clear the items before calling the builder (every item appeared twice): on v.1.1 call `List.ClearItems` at the beginning of the builder. ::: ## Search `SearchVisible := True` shows the list view's search box, which filters the items by text. `SearchText` reads or sets its text, `SetOnSearchChange(TProc)` is called when it changes. ## Members | Member | Description | |---|---| | `AddItem(AText, ADetailText, AImageIndex, AOnSelect): TListViewItem` | adds an item with its click handler | | `ClearItems` | removes every item | | `ItemBuilderProc: TProc` | builds the items; setting it runs it, on an empty list | | `RefreshList` | clears and rebuilds | | `ItemAppearance: string` | the list view's item appearance (`'ListItem'`, `'ListItemRightDetail'`, `'ImageListItem'`, `'ImageListItemBottomDetail'`, ...) | | `AccessoryVisible: Boolean` | the item accessory (chevron) | | `CanSwipeDelete: Boolean` | swipe to delete (on by default in `TListView`) | | `SearchVisible: Boolean`, `SearchText: string`, `SetOnSearchChange(TProc)` | search box | | `SelectedItem: TListViewItem` | | | `ListView: TListView` | the list view | `TListViewItemClickProc = reference to procedure (const AItem: TListViewItem)` is the type of the item handlers; each handler is stored in its item (`Data['FMXER.OnSelect']`, the `ON_SELECT_DATA` constant), so it goes away with the item. Add `FMX.ListView.Appearances` to the `uses` clause for `TListViewItem`. --- Source: https://andrea-magni.github.io/FMXER/elements/qrcode # QR Codes `TQRCodeFrame` (unit `FMXER.QRCodeFrame`) draws a QR code from a text, with rounded modules rendered as SVG by Skia. Encoding is done by [DelphiZXingQRCode](https://github.com/foxitsoftware/DelphiZXingQRCode); the rounded renderer (`QRCode.Render`) comes from the Skia4Delphi samples. ```pascal Col.AddFrame(200 , procedure (QR: TQRCodeFrame) begin QR .SetContent('https://github.com/andrea-magni/FMXER') .SetOverrideColor(TAlphaColorRec.Navy) .SetRadiusFactor(0.3) .SetMargin(2) .SetHitTest(True); QR.OnTapHandler := procedure (AImage: TFMXObject; APoint: TPointF) begin Navigator.RouteTo('qrcode'); end; end ); ``` The code is regenerated whenever the content, the radius factor or the paint handler change, so the same element can follow a value — the HelloWorld demo updates it while you type in an edit. ## Members | Member | Description | |---|---| | `SetContent(AText)` | the text to encode (encoding chosen automatically) | | `SetRadiusFactor(AFactor)` | corner radius of the modules, relative to their size: 0 = squares, 0.1 by default, 0.5 = dots | | `SetOverrideColor(AColor)` | paints the whole code in one color | | `OnBeforePaint: TOnBeforePaintHandler` | customizes the Skia paint before rendering (gradients, shaders) | | `SetSVGSource(ASource)` | shows any SVG instead of a generated code | | `LoadLogoContentFromFile(AFileName)`, `SetLogoVisible`, `SetLogoWidth`, `SetLogoHeight` | an image or animation (`QRCodeLogo`, a `TSkAnimatedImage`) in the center of the code | | `OnTapHandler` | tap (mouse up on Windows), see [Images & Media](./images-and-media#tap-handlers) | | `QRCode: TSkSvg` | the SVG control | The renderer leaves the central module free for a logo. Keep logos small: QR codes tolerate a limited amount of covered area. ### A gradient code ```pascal QR.OnBeforePaint := procedure (APaint: ISkPaint; AModules: T2DBooleanArray) begin APaint.Shader := TSkShader.MakeGradientLinear( PointF(Length(AModules) - 6, 6), PointF(6, Length(AModules) - 6) , TAppColors.MATERIAL_ORANGE_800, TAppColors.MATERIAL_DEEP_PURPLE_400 , TSkTileMode.Clamp); end; ``` `AModules` is the matrix of the code (`True` = dark module), its size is the number of modules per side. The Skia101 demo uses this handler, with a code that shows the current time and changes every half second. ## QRCode.Utils | Routine | Description | |---|---| | `TQRCode.TextToSvg(AContent, ABeforePaint = nil, ARadiusFactor = 0.1): string` | the SVG of a QR code, without any control | | `SvgToBitmap(ASvg, ABGColor, AColor, AWidth, AHeight, AOnReady: TProc)` | renders an SVG of `AWidth` x `AHeight` into a bitmap with a 10-pixel margin of `ABGColor` on every side, and passes it to `AOnReady` (the bitmap is freed afterwards) | The QRCode demo combines them to share a code as an image; in essence: ```pascal SvgToBitmap(TQRCode.TextToSvg(QRCodeContent), QRCodeBGColor, QRCodeColor, 1024, 1024 , procedure (ABitmap: TBitmap) begin ShowShareSheetAction1.Bitmap.Assign(ABitmap); ShowShareSheetAction1.Execute; end ); ``` ## Reading QR codes FMXER has no element to *read* codes, but the **QRCode demo** shows how to do it: a `TCameraComponent` gives the frames, [ZXing.Delphi](https://github.com/Spelt/ZXing.Delphi) 4 decodes them in background (one `IScanManager` for every frame, `ScanAsync`: a frame that comes while a scan runs is skipped), and a `TPaintBoxFrame` draws the frame, the points found while scanning and the outline of the code read; camera permission, torch and Back handling included. See [Demos](/demos/#qrcode). --- Source: https://andrea-magni.github.io/FMXER/elements/icons # Icons FMXER draws icons from the **Material Design Icons** font (5,918 glyphs, version 5.9.18) through a shared [TIconFontsImageList](https://github.com/EtheaDev/IconFontsImageList). Icons are vector glyphs: they scale to any size and take any color. The font file must be available at runtime: see [The icon font](/guide/installation#the-icon-font). ## IconFonts: the shared image list Unit `FMXER.IconFontsData`. `IconFonts` returns a singleton data module (created on first use) with one `TIconFontsImageList`: ```pascal uses FMXER.IconFontsData; var LIndex := IconFonts.AddIcon(IconFonts.MD.account, TAppColors.PrimaryColor); // LIndex is an image index in IconFonts.ImageList ``` | Member | Description | |---|---| | `AddIcon(AIcon: TIconEntry[; AColor]): Integer` | adds the icon to the image list, returns its index | | `AddIcon(ACodePoint: Integer[; AColor]): Integer` | same, by code point | | `ImageList: TIconFontsImageList` | the image list (assign it to `TGlyph.Images`, `TListView.Images`, ...) | | `MD: TMaterialDesign` | the icon table, one property per icon | Without a color, `AddIcon` uses the image list's `FontColor` (black). Each icon is added once per color: calling `AddIcon` again with the same icon and color returns the index of the image already in the list, so it is safe in code that runs many times — a route definition, a list builder. ::: info Before v.1.2 Up to v.1.1 `AddIcon` added a new image at **every** call, and the list grew for the whole life of the app. On v.1.1 keep the indexes, as the LibraryStore demo does in `Utils.UI`: ```pascal function TUIUtils.GetBookImageIndex: Integer; begin if FBookImageIndex = -1 then FBookImageIndex := IconFonts.AddIcon(IconFonts.MD.book, TAppColors.PrimaryColor); Result := FBookImageIndex; end; ``` ::: ## Icon entries Unit `Icons.MaterialDesign` declares `TMaterialDesign`, a record with one `TIconEntry` per icon, named after the icon with `_` instead of `-`: `MD.account`, `MD.arrow_left`, `MD.qrcode_scan`, `MD.weather_sunny`. Browse the names at [pictogrammers.com](https://pictogrammers.com/library/mdi/) (version 5.9 or older). Unit `Icons.Utils` declares the types: | Type | Description | |---|---| | `TIconEntry` | `name`, `codepoint`, `index`, `aliases`; converts implicitly to `Integer` (code point) and `string` (name) | | `TIconCollection` | a named set of entries, with `ForEach`, `Contains(AName / ACodepoint)`, `FindEntry(AName, out AEntry, ACaseInsensitive)`, `FindEntry(ACodepoint, out AEntry)` | | `TIconManager` | `TIconManager.Instance.Collections`: the registered collections (`Icons.MaterialDesign` registers itself) | ```pascal var LEntry: TIconEntry; for var LCollection in TIconManager.Instance.Collections do if LCollection.FindEntry('rocket-launch', LEntry) then Glyph.SetIcon(LEntry); ``` `Icons.MaterialDesign.pas` and `Icons.Utils.pas` are generated by [IconFontsImageListUtility](https://github.com/andrea-magni/IconFontsImageListUtility), which can produce the same units for other icon fonts (the HelloWorld_IFIL demo also has a Font Awesome unit). ## TIconFontsGlyphFrame Unit `FMXER.IconFontsGlyphFrame`. A `TGlyphFrame` already bound to `IconFonts.ImageList`. ```pascal Accessory.SetLeftAsFrame(50 , procedure (Glyph: TIconFontsGlyphFrame) begin Glyph .SetIcon(IconFonts.MD.qrcode_edit, TAppColors.PrimaryColor) .SetPadding(5); end ); ``` | Member | Description | |---|---| | `SetIcon(AIcon: TIconEntry)` | adds the icon (default color) and shows it | | `SetIcon(AIcon: TIconEntry; AColor)` | adds the icon in `AColor` and shows it | | `SetIcon(AImageIndex: Integer)` | shows an image already in `IconFonts.ImageList` | Plus the members of `TGlyphFrame`, below. ## TGlyphFrame Unit `FMXER.GlyphFrame`. A `TGlyph` showing an image of any image list, clickable. | Member | Description | |---|---| | `SetImages(AImageList)` / `GetImages` | the image list | | `SetImageIndex(AIndex)` / `GetImageIndex` | the image | | `SetOnClickProc(TProc)` / `GetOnClickProc` | click handler; the frame is clickable only when one is set | ## Icons in other elements - Scaffold action buttons: `AddActionButton(IconFonts.MD.close, TAppColors.PrimaryTextColor, procedure begin ... end)` — see [Scaffold](./scaffold#action-buttons). - List items: `TListViewFrame` uses `IconFonts.ImageList` as `ListView.Images` unless you assign another list; pass the index to `AddItem`. --- Source: https://andrea-magni.github.io/FMXER/reference/navigator # TNavigator Unit `FMXER.Navigator`. Named routes on top of a `TFormStand`. For a guided tour see [Navigation](/guide/navigation). ```pascal function Navigator(const AFormStand: TFormStand = nil): TNavigator; ``` Returns the singleton navigator. When it is not initialized yet and `AFormStand` is assigned, it initializes it first (`TNavigator.Init(AFormStand)`). Raises `Exception` (*Navigator has not been initialized*) when called with no form stand before initialization. ## Class members | Member | Description | |---|---| | `class procedure Init(const AFormStand: TFormStand)` | creates the singleton; raises if already initialized | | `class property Instance: TNavigator` | the singleton; raises if not initialized | | `class property Initialized: Boolean` | whether `Init` was called (and the class destructor has not run yet) | | `class procedure ReleaseInstance` | frees the singleton (since v.1.4); the routes still open are not closed | | `constructor Create(const AFormStand: TFormStand)` | another navigator, with its own routes and stack; free it yourself | The singleton is freed by the class destructor, at unit finalization. `Navigator(AFormStand)` raises when the singleton exists with another form stand. ## Route definition | Member | Description | |---|---| | `DefineRoute(const ARouteName: string; const ARouteDef: TFunc): TNavigator` | registers a route built by `ARouteDef`, which must create the subject and return its info (it is shown by the navigator) | | `DefineRoute(const ARouteName: string; const AConfigProc: TProc = nil; const AParent: TFmxObject = nil; const AStandName: string = ''): TNavigator` | registers a route built as `FormStand.New(AParent, AStandName)` + `AConfigProc` | | `IsRouteDefined(const ARouteName: string): Boolean` | | | `RouteDefinitions: TDictionary>` | the definitions (a duplicate name raises `EListError` on `DefineRoute`) | ## Routing | Member | Description | |---|---| | `RouteTo(const ARouteName: string; const ATransient: Boolean = False)` | if the route is not active: builds it, adds it to `ActiveRoutes`, pushes it on `Stack` (unless transient), fires `OnCreateRoute`. Then shows it (`SubjectShow`). Raises `Exception` (*Route definition not found*) for unknown names | | `RouteToDelayed(const ARouteName: string; const ATransient: Boolean; const ADelay_ms: Integer = 100)` | `RouteTo` through `TDelayedAction` | | `CloseRoute(const ARouteName: string; const ATransient: Boolean = False)` | if active: `HideAndClose` with `OnCloseRoute` as continuation, removes it from `ActiveRoutes` and, unless it was opened as transient, from `Stack` (wherever it is). `ATransient` is ignored since v.1.2: the navigator remembers it. No-op for inactive routes | | `CloseRouteDelayed(const ARouteName: string; const ATransient: Boolean; const ADelay_ms: Integer = 100)` | `CloseRoute` through `TDelayedAction` | | `StackPop` | `CloseRoute(Stack.Peek)`, if the stack is not empty | | `CloseAllRoutes(const AFilter: TPredicate = nil)` | `CloseRoute(Name)` for every active route accepted by `AFilter` | | `CloseAllRoutesExcept(const ARouteName: string)` | `CloseAllRoutes` with a filter excluding `ARouteName` | ::: info Before v.1.2 `CloseRoute` popped the top of `Stack` rather than removing `ARouteName`, and `CloseAllRoutes` raised `EListError` with a transient route open. See [Navigation](/guide/navigation#transient-routes). ::: ## State | Member | Description | |---|---| | `ActiveRoutes: TDictionary` | open routes | | `IsRouteActive(const ARouteName: string): Boolean` | | | `Stack: TStack` | non-transient routes, last opened on top | | `Breadcrumb: TArray` | `Stack` as an array, in opening order | | `FormStand: TFormStand` | the form stand passed to `Init` | ## Events | Member | Description | |---|---| | `OnCreateRoute: TProc` / `SetOnCreateRoute(...): TNavigator` | after a route is built by `RouteTo` | | `OnCloseRoute: TProc` / `SetOnCloseRoute(...): TNavigator` | after a route is hidden and closed | ## Back button | Member | Description | |---|---| | `HandleBackKey(var AKey: Word; var AKeyChar: WideChar): Boolean` | Back (`vkHardwareBack`) or Esc: `StackPop` when the stack has more than one route; consumed without action while a transient route is open; not handled at the root. Clears `AKey`/`AKeyChar` and returns `True` when handled | | `CatchBackKey: Boolean` / `SetCatchBackKey(AValue): TNavigator` | hooks `OnKeyDown` of the form that owns `FormStand` and calls `HandleBackKey`; the previous handler gets the keys not handled. Raises when `FormStand` is not owned by a form | --- Source: https://andrea-magni.github.io/FMXER/reference/ui-misc # FMXER.UI.Misc Unit `FMXER.UI.Misc`: element definitions, parameters, fluent helpers for controls and forms, the slot layout of columns and rows, and a file helper. The tap handler type shared by the image elements is in `FMXER.UI.Types` (see [Images & Media](/elements/images-and-media#tap-handlers)). ## TElementDef A configuration procedure plus named parameters, as one value. ```pascal TElementDef = record Params: TArray; ConfigProc: TProc; constructor Create(const AConfigProc: TProc; const AParams: TParams = []); function FindParam(const AName: string; out AParam: TNameValueParam): Boolean; function ParamByName(const AName: string; const ADefaultValue: TValue): TValue; overload; function ParamByName(const AName: string): TValue; overload; class operator Implicit(AConfigProc: TProc): TElementDef; end; ``` Parameter names are case-insensitive. `ParamByName` returns `ADefaultValue` (or `TValue.Empty`) when the parameter is missing. | Consumer | Parameters | |---|---| | `TColumnForm.AddFrame(TElementDef)`, `AddForm(...)` | `Height` (Integer, default 200; `TColumnForm.AUTO_HEIGHT` for the element's own height) | | `TRowForm.AddFrame(TElementDef)`, `AddForm(...)` | `Width` (Integer, default 200; `TRowForm.AUTO_WIDTH` for the element's own width) | | `TScaffoldForm.AddActionButton(ACaption, AParams)`, `AddActionButtonOverlayFrame(TElementDef)` | `OnClickProc` (`TProc`) | ```pascal Col.AddFrame( TElementDef.Create( procedure (Logo: TLogoFrame) begin Logo.Opacity := 0.5; end , [ Param('Height', 120) ]) ); var LBack: TProc := procedure begin Navigator.StackPop; end; Scaffold.AddActionButton('Back', [ OnClickProc(LBack) ]); ``` ::: tip Anonymous methods and AddActionButton With `AddActionButton`, pass a variable to `OnClickProc`, as above: an anonymous method written inline in `[ OnClickProc(...) ]` there does not compile with Delphi 13 (*E2250 There is no overloaded version of 'OnClickProc'*) unless the call is qualified (`FMXER.UI.Misc.OnClickProc(...)`). Elsewhere the inline form works. ::: ## Parameters | Declaration | Description | |---|---| | `TNameValueParam = record Name: string; Value: TValue; end` | a parameter | | `TParams = TArray` | | | `Param(AName; AValue: TValue)` | builds a parameter | | `Param(AName; AValue: TProc)`, `Param(AName; AValue: TFunc)` | the same for anonymous methods (stored with their type) | | `OnClickProc(AValue: TProc)` | shortcut for `Param('OnClickProc', ...)` | ## Control helpers `TFMXERFrameHelper = class helper for TControl`: fluent setters for any control, frame element included. Each returns the control (`TControl`); the generic overloads return it as `T`, to keep chaining the element's own setters. | Method | Sets | |---|---| | `SetMargin(AMargin)`, `SetMargin(AMargin)` | all four margins | | `SetMargin(ALeft, ATop, ARight, ABottom)`, `SetMargin(...)` | margins | | `SetMarginLR(AMargin)`, `SetMarginTB(AMargin)`, `SetMarginT(ATop)`, `SetMarginB(ABottom)` | some margins | | `SetPadding(APadding)`, `SetPadding(APadding)` | all four paddings | | `SetPadding(ALeft, ATop, ARight, ABottom)`, `SetPadding(...)` | paddings | | `SetPaddingLR(APadding)`, `SetPaddingTB(APadding)` | some paddings | | `SetAlignClient`, `SetAlignTop`, `SetAlignBottom`, `SetAlignLeft`, `SetAlignRight`, `SetAlignContents` | `Align` | | `SetHitTest(AHitTest)` | `HitTest` (elements forward it to their inner control) | | `SetWidth(AWidth)`, `SetHeight(AHeight)` | size | ```pascal Button.SetMargin(5).SetText('OK'); Text.SetAlignClient.SetMargin(5, 0, 0, 0); ``` ::: info One helper per class Delphi uses only one class helper per class at a time. If your code declares another helper for `TControl`, the one closest in scope hides `TFMXERFrameHelper`. ::: ## Form helpers `TFMXERFormHelper = class helper for TCommonCustomForm`: | Method | Sets | |---|---| | `SetPadding(APadding): TCommonCustomForm` | the form's padding (moved with the content when the form is shown through a `TFormStand`) | | `SetWidth(AWidth)`, `SetHeight(AHeight): TCommonCustomForm` | size | | `SetPadding(APadding): T` | the same, returning the form as `T` (up to v.1.1 the constraint was `TControl` and it always raised `EInvalidCast`) | ## TFMXERStands ```pascal TFMXERStands = record class function NewSizedFrame(const AFrameStand: TFrameStand; const AParent: TFmxObject; const AStandName: string; const AAlign: TAlignLayout): TFrameInfo; static; end; ``` Like `TFrameStand.New`, but the frame is aligned with `AAlign` (`Top`, `Left`...) **before** it enters the stand, so it keeps its design size: with its usual `Align = Client` it would take the size of the stand at once. The frame is not shown: configure it, size its slot on it, then call `Show` on the returned info. Columns, rows and scroll boxes use it for [automatic sizes](/guide/layouts#automatic-sizes); use it in your own containers to do the same. ## TFMXERSlots ```pascal TFMXERSlots = record const AUTO_SIZE = -1; class function AddFrame(const AFrameStand: TFrameStand; const AContent: TControl; const AOwner: TComponent; const AElements: TList; const AStandName: string; const AVertical: Boolean; const ASlotAlign: TAlignLayout; const ASize: Integer; const AConfigProc: TProc): Single; static; class function AddForm(...same parameters, a TFormStand...): Single; static; end; ``` The one implementation of the slots of `TColumnForm`, `TColumnFrame`, `TRowForm` and `TRowFrame` (since v.1.4): it adds a `TSubjectInfoContainer` to `AContent` after the existing ones (`AVertical`: at the bottom; otherwise at the end of the row), creates the element in it through the stand, runs `AConfigProc`, sizes the slot (`ASize`, or the element's own size with `AUTO_SIZE`), shows the element and adds the slot to `AElements`. It returns the slot size, by which the container grows. Use it to write your own column-like container. ## TSubjectInfoContainer ```pascal TSubjectInfoContainer = class(TLayout) function ContainedFrame: T; function ContainedForm: T; property SubjectInfo: TSubjectInfo; end; ``` The slot layout created by `TColumnForm` and `TRowForm` for each element (`Elements` list). `ContainedFrame` / `ContainedForm` return the element, or `nil` when the slot holds the other kind. ## TCaptionPosition `TCaptionPosition = (Left, Right, Top, Bottom)`, used by `TTrackbarFrame.SetCaptionPosition`. ## LocalFile ```pascal var LocalFilesPath: string; function LocalFile(const AFileName: string): string; ``` `LocalFile` returns `TPath.Combine(LocalFilesPath, AFileName)`. `LocalFilesPath` is, by default, the media folder of the repository seen from a demo executable on Windows (`..\..\..\..\media\`, from `demos\\\`) and `TPath.GetDocumentsPath` on the other platforms. Set it at startup to use `LocalFile` with your own folder (since v.1.2.1). --- Source: https://andrea-magni.github.io/FMXER/reference/app-colors # TAppColors Unit `FMXER.UI.Consts`. Application colors read by the elements when they are created, and the Material Design palette. See [Styling & Theming](/guide/styling). ## Application colors | Class property | Default | Read by | |---|---|---| | `PrimaryColor` | `MATERIAL_AMBER_800` | `TScaffoldForm` (title bar), `TActionButtonFrame` (circle), `TCardFrame` (title bar), `TBackgroundFrame`/`TBackgroundForm` (fill), `TChipFrame` (text and border) | | `PrimaryTextColor` | `TAlphaColorRec.White` | `TScaffoldForm` (title), `TTextFrame` (text) | | `SolidBackgroundColor` | `TAlphaColorRec.Lightgray` | `TChipFrame` (fill) | | `LightBackgroundColor` | `TAlphaColorRec.Silver` | `TCardFrame` (content) | | `DividerColor` | `MATERIAL_GREY_800` | `THorzDividerFrame` | The defaults are set by the class constructor of `TAppColors`. Assign new values before the elements are created (typically before the first `RouteTo`). ## Material Design palette Constants of type `TAlphaColor`, shades 400 and 800 of the [Material Design 2014 palette](https://m2.material.io/design/color/the-color-system.html): | Hue | 400 | 800 | |---|---|---| | Red | `MATERIAL_RED_400` `#EF5350` | `MATERIAL_RED_800` `#C62828` | | Pink | `MATERIAL_PINK_400` `#EC407A` | `MATERIAL_PINK_800` `#AD1457` | | Purple | `MATERIAL_PURPLE_400` `#AB47BC` | `MATERIAL_PURPLE_800` `#6A1B9A` | | Deep Purple | `MATERIAL_DEEP_PURPLE_400` `#7E57C2` | `MATERIAL_DEEP_PURPLE_800` `#4527A0` | | Indigo | `MATERIAL_INDIGO_400` `#5C6BC0` | `MATERIAL_INDIGO_800` `#283593` | | Blue | `MATERIAL_BLUE_400` `#42A5F5` | `MATERIAL_BLUE_800` `#1565C0` | | Light Blue | `MATERIAL_LIGHT_BLUE_400` `#29B6F6` | `MATERIAL_LIGHT_BLUE_800` `#0277BD` | | Cyan | `MATERIAL_CYAN_400` `#26C6DA` | `MATERIAL_CYAN_800` `#00838F` | | Teal | `MATERIAL_TEAL_400` `#26A69A` | `MATERIAL_TEAL_800` `#00695C` | | Green | `MATERIAL_GREEN_400` `#66BB6A` | `MATERIAL_GREEN_800` `#2E7D32` | | Light Green | `MATERIAL_LIGHT_GREEN_400` `#9CCC65` | `MATERIAL_LIGHT_GREEN_800` `#558B2F` | | Lime | `MATERIAL_LIME_400` `#D4E157` | `MATERIAL_LIME_800` `#9E9D24` | | Yellow | `MATERIAL_YELLOW_400` `#FFEE58` | `MATERIAL_YELLOW_800` `#F9A825` | | Amber | `MATERIAL_AMBER_400` `#FFCA28` | `MATERIAL_AMBER_800` `#FF8F00` | | Orange | `MATERIAL_ORANGE_400` `#FFA726` | `MATERIAL_ORANGE_800` `#EF6C00` | | Deep Orange | `MATERIAL_DEEP_ORANGE_400` `#FF7043` | `MATERIAL_DEEP_ORANGE_800` `#D84315` | | Brown | `MATERIAL_BROWN_400` `#8D6E63` | `MATERIAL_BROWN_800` `#4E342E` | | Grey | `MATERIAL_GREY_400` `#BDBDBD` | `MATERIAL_GREY_800` `#424242` | | Blue Grey | `MATERIAL_BLUE_GREY_400` `#78909C` | `MATERIAL_BLUE_GREY_800` `#37474F` | All the constants are fully opaque (`$FF` alpha). --- Source: https://andrea-magni.github.io/FMXER/reference/units # Units & Dependencies Every unit is in `source\`. FMXER has no packages: add `source\` to the search path (see [Installation](/guide/installation)). ## Units | Unit | Declares | Depends on | |---|---|---| | `FMXER.Navigator` | `TNavigator`, `Navigator()` | TFrameStand | | `FMXER.UI.Misc` | `TElementDef`, `TNameValueParam`, `TParams`, `Param()`, `OnClickProc()`, `TFMXERFrameHelper`, `TFMXERFormHelper`, `TSubjectInfoContainer`, `TFMXERStands`, `TFMXERSlots`, `TCaptionPosition`, `LocalFile()`, `LocalFilesPath` | TFrameStand | | `FMXER.UI.Consts` | `TAppColors` | — | | `FMXER.UI.Types` | `TOnTapHandler` | — | | `FMXER.IconFontsData` | `TIconFonts` (data module), `IconFonts()`, `ICON_FONTS_FAMILYNAME`, `ICON_FONTS_FILENAME` | IconFontsImageList, Skia | | `Icons.Utils` | `TIconEntry`, `TIconCollection`, `TIconManager` | — | | `Icons.MaterialDesign` | `TMaterialDesign`, `MD` | — | | `QRCode.Utils` | `TQRCode`, `TOnBeforePaintHandler`, `SvgToBitmap()` | DelphiZXingQRCode, Skia | | `QRCode.Render` | `TQRCodeRender`, `IQRCodeRender`, `T2DBooleanArray` | Skia | | `FMXER.ScaffoldForm` | `TScaffoldForm` | TFrameStand, Skia, IconFontsImageList | | `FMXER.ColumnForm`, `FMXER.RowForm` | `TColumnForm`, `TRowForm` | TFrameStand | | `FMXER.ColumnFrame`, `FMXER.RowFrame` | `TColumnFrame`, `TRowFrame` | TFrameStand | | `FMXER.CenterForm`, `FMXER.ContainerForm`, `FMXER.BackgroundForm`, `FMXER.VertScrollForm` | the other form containers | TFrameStand | | `FMXER.ContainerFrame`, `FMXER.MarginFrame`, `FMXER.PaddingFrame`, `FMXER.StackFrame`, `FMXER.VertScrollFrame`, `FMXER.HorzScrollFrame`, `FMXER.AccessoryFrame`, `FMXER.HorzPairFrame`, `FMXER.BackgroundFrame` | frame containers | TFrameStand | | `FMXER.CardFrame`, `FMXER.ActionButtonFrame` | | TFrameStand, Skia | | `FMXER.TextFrame`, `FMXER.ButtonFrame`, `FMXER.ColorButtonFrame`, `FMXER.EditFrame`, `FMXER.TrackbarFrame`, `FMXER.ColorPickerFrame`, `FMXER.ActivityBubblesFrame`, `FMXER.AnimatedImageFrame`, `FMXER.SVGFrame`, `FMXER.PaintBoxFrame` | Skia elements | Skia (and TFrameStand for the edit, track bar and color picker) | | `FMXER.QRCodeFrame` | `TQRCodeFrame` | Skia, DelphiZXingQRCode | | `FMXER.GlyphFrame`, `FMXER.ImageFrame`, `FMXER.LogoFrame`, `FMXER.WebViewFrame`, `FMXER.ChipFrame`, `FMXER.ChipsFrame`, `FMXER.HorzDividerFrame` | plain FMX elements | — | | `FMXER.IconFontsGlyphFrame`, `FMXER.ListViewFrame` | | IconFontsImageList | ## Dependencies | Library | Units used | Notes | |---|---|---| | [TFrameStand](https://andrea-magni.github.io/TFrameStand/) | `SubjectStand`, `FrameStand`, `FormStand` | v.1.6 or later; v.2.3 recommended | | Skia (built into Delphi 12+) | `System.Skia`, `FMX.Skia` | `TSkLabel`, `TSkSvg`, `TSkAnimatedImage`, `TSkPaintBox` | | [IconFontsImageList](https://github.com/EtheaDev/IconFontsImageList) | `FMX.IconFontsImageList` | v.3.2.2 in the submodule | | [DelphiZXingQRCode](https://github.com/foxitsoftware/DelphiZXingQRCode) | `DelphiZXIngQRCode` | QR code encoding | ## Third-party code and assets - `QRCode.Render.pas` comes from the [Skia4Delphi](https://github.com/skia4delphi/skia4delphi) webinar samples (rounded QR code rendering). - `media\Material Design Icons Desktop.ttf`: [Material Design Icons](https://pictogrammers.com/library/mdi/) by Pictogrammers, Apache License 2.0. - The Lottie animations in `media\` come from [LottieFiles](https://lottiefiles.com). ## Files of the repository | Path | Content | |---|---| | `source\` | the units and their `.fmx` | | `demos\` | demo projects, `AllDemos.groupproj` | | `lib\` | submodules: TFrameStand, IconFontsImageList, DelphiZXingQRCode, ZXing.Delphi | | `media\` | logos, the icon font, Lottie animations and SVG files used by the demos | | `List.md` | the list of the elements | | `docs\` | this documentation (VitePress) | --- Source: https://andrea-magni.github.io/FMXER/demos/ # Demos The `demos` folder has one project per topic; ten of them are in `demos\AllDemos.groupproj` (MyTicketing and EKON25 - Weather are not). They target Windows, macOS, iOS and Android. Every demo has the same search path, set once for all platforms: `..\..\source` and the submodules `lib\TFrameStand\source`, `lib\IconFontsImageList\Source`, `lib\DelphiZXingQRCode\Source` (QRCode also the `lib\ZXing.Delphi\Lib\Classes` folders). Clone with `--recursive` (see [Installation](/guide/installation)) and the demos build as they are. If you installed TFrameStand and IconFontsImageList instead and left `lib\` empty, the IDE library path is used. The MARS client of LibraryStore is not a submodule: install MARS or add its `Source` folder to the library path. Media files (Lottie animations, SVG, the icon font `Material Design Icons Desktop.ttf`) come from the `media` folder: on Windows the demos read them from there with `LocalFile`, on mobile they are deployed with the app. | Demo | Shows | Extra requirements | |---|---|---| | [NewProjectTemplate](#newprojecttemplate) | the smallest FMXER app, to copy | — | | [HelloWorld](#helloworld) | scaffold, stack, scrolling column, rows, chips, edit, QR code, transient route, app state | — | | [Column](#column) | columns, cards, `TElementDef`, scrolling | — | | [MarginAndPadding](#marginandpadding) | margins and paddings | — | | [Skia101](#skia101) | images, SVG, Lottie, QR code with gradient, Skia paint box | — | | [WebView](#webview) | a web page in a scaffold | — | | [DataSet](#dataset) | LiveBindings to a FireDAC dataset, route stands | FireDAC | | [LibraryStore](#librarystore) | lists filled from a REST server, async loading, multiple routes | MARS client, a MARS server | | [HelloWorld_IFIL](#helloworld-ifil) | an image list of icon fonts on the main form | — | | [QRCode](#qrcode) | QR code generator and camera reader, color pickers, share sheet | ZXing.Delphi, camera | | [MyTicketing](#myticketing) | a login column (edits, password, default button enabled by `OnUpdateHandler`) leading to a scaffold | — | | [EKON25 - Weather](#ekon25-weather) | a card with a nested column in a scaffold, conference starting point | — | ## NewProjectTemplate The starting point for a new app: a main form with a `TFormStand`, one route unit (`Routes.home`) with a scaffold, a logo and a close action button, `OnCloseRoute` closing the app, Back/Esc handled by the navigator (`SetCatchBackKey`). The project is set up for every platform, including the iOS simulator on Apple silicon, and deploys the icon font. Copy the folder to start a new project. ## HelloWorld The richest tour of the elements, in three routes: - **home**: a scaffold whose content is a `TStackFrame` with a white background, a tiled SVG, a Lottie animation and a `TVertScrollFrame` with a column: a row of three buttons, a chip, a flow of chips, an edit, a QR code that follows what you type, and a custom frame (`Frames.Custom1`) designed in the IDE. A small QR code is in the title detail area; two action buttons show a snackbar and close the app. - **bubble**: a transient route with `TActivityBubblesFrame`, opened and closed after 3 seconds with `RouteTo('bubble', True)` + `CloseRouteDelayed`. - **qrcode**: a full-screen QR code of the text typed in the home screen; tap to close. Shared state is a data module (`Data.AppState`) that broadcasts changes with `System.Messaging` ; each subscriber is tied to the lifetime of its QR code by a `TSubscriptionGuard` (see [State & Events](/guide/state#subscriptions-and-the-lifetime-of-elements)). ## Column `TBackgroundForm` routes with columns: a menu of two texts that open the other routes, a plain column and the same column in a `TVertScrollFrame`. Shows `TContainerFrame.SetContentAs` (a plain *Close me* button that calls `Navigator.StackPop`), `TLogoFrame`, `THorzDividerFrame`, `TCardFrame` and the `TElementDef` overload with `Param('Height', ...)`. Closing the window closes the routes first. ## MarginAndPadding A `TVertScrollForm` with a column of logos: the same logo plain, in a `TMarginFrame` with `SetMargin(50)`, in a `TPaddingFrame` with `SetPadding(50)`, and in a `TContainerFrame` with both. Elements are sized from the column width (`ACol.Width`) to get squares. ## Skia101 A menu (`TListViewFrame` in a `TStackFrame`) of Skia-based screens, with `TAppColors.PrimaryColor` set to blue: - spinners: a transient route closed after 1, 5 or 10 seconds; - `TImageFrame`, `TAnimatedImageFrame` (Lottie), `TSVGFrame`: tap to close; - a QR code that shows the current time, updated by a background task, with a gradient shader (`OnBeforePaint`); - a `TPaintBoxFrame` filled with a Skia sweep gradient (route `freeHandDrawing`, a starting point for drawing), with a *Back* button in the title detail area. ## WebView The smallest route: a scaffold with a `TWebViewFrame` opening a URL, defined and routed in one fluent statement. ## DataSet A list of employees from a `TFDMemTable` (data module `Data.Main`) in a frame designed in the IDE with LiveBindings (`Frames.MyDataSetList`), and a detail frame (`Frames.MyDataSetDetail`) shown by a second route. The detail route is defined with a stand, `slideRightLeft`, from the main form's style book: the whole screen slides in. Action buttons use images from a `TImageList`. ## LibraryStore A small catalog app that reads books and authors from a [MARS-Curiosity](https://github.com/andrea-magni/MARS) REST server (`Data.Remote`, `TMARSNetClient` with asynchronous `GETAsync`): - **home**: a list with two entries; - **books_all**, **books_by_author**, **authors**, **book_single**: lists built by an `ItemBuilderProc`, titles updated with the number of results, search box when there are more than three items, an activity animation in the title detail area while loading (driven by `TRetrieveMsg` messages, unsubscribed when the route closes), errors in a snackbar; - icons added once and cached in `Utils.UI`. It needs a MARS server exposing the LibraryStore resources. The address is in two constants at the top of `Data.Remote`: `SERVER_URL` (`http://localhost:8080/rest`, desktop) and `SERVER_URL_ANDROID` (`http://10.0.2.2:8080/rest`, the host machine as seen from the Android emulator; on a device put the LAN address of the server). On desktop the environment variable `FMXER_LIBRARYSTORE_URL`, when set, wins over the constant. ## HelloWorld_IFIL The HelloWorld idea with a `TIconFontsImageList` dropped on the main form instead of the shared `IconFonts`: icons inserted at runtime by code point (`MD.account_clock.codepoint`), used by action buttons and by a `TGlyphFrame` in a `TBackgroundForm`. ## QRCode A complete app, and the most recent demo: - **home**: a stack with background, Lottie animation and a column of `TAccessoryFrame` rows (icon + button) to the other routes; - **QRGenerator**: an edit for the content, two `TColorButtonFrame`s in a `THorzPairFrame` that open color picker routes, a `TTrackbarFrame` for the corner radius, the code (`TQRCodeFrame`) and a share action (`SvgToBitmap` + `TShowShareSheetAction`); - **QRCodeColorSelection**, **QRCodeBGColorSelection**: the same route definition (`DefineColorPickerRoute`) with different getters and setters; - **QRReader**: camera preview in a `TPaintBoxFrame`, frames decoded in background by ZXing.Delphi 4 (`TMainData.ScanFrame`: one `IScanManager`, `ScanAsync`, the result in the main thread), the points found while scanning drawn as they fade, the code read outlined (its corners, `TReadResult.Position`), torch toggle when the device has one, result with actions to open the content in the **Webview** route or to generate it again; - **SingleFrame**: the last camera frame (or the frame of the last result, with its outline), with the same result actions; scanning a single picture is still marked *Not yet implemented*; - **Webview**: the scanned content in a `TWebViewFrame`. The main form shows the breadcrumb in the caption on Windows, closes the camera in `OnCloseRoute` and handles Back/Esc with `StackPop`. ## MyTicketing A conference demo, ported to the fluent API of 1.1: - **login**: a `TColumnForm` with a logo, two `TEditFrame`s (`SetCaption`, `SetPassword(True)`) that store what you type in a data module (`Data.Remote`), and a default *Login* button (`SetOnClickHandler`) enabled by its `OnUpdateHandler` only when both fields are filled; the login succeeds with `andrea` / `password`; - **home**: a scaffold with a logo and two action buttons (a snackbar, close the route). ## EKON25 - Weather The starting point of a conference session (EKON 25): a scaffold whose content is a column with a `TCardFrame` titled with a location, and a *Get weather* button in a column nested in the card. The weather frame (`Frames.WeatherSituation`) and the data module are placeholders, never completed. --- Source: https://andrea-magni.github.io/FMXER/release-notes # Release Notes Releases are published on [GitHub](https://github.com/andrea-magni/FMXER/releases). ## v.1.6 — 2026-10-19 **Safe area (Android 15+ edge-to-edge, iOS notch)**: the routes no longer go under the status bar and the navigation bar. The navigator shows them in a layout of the form that gets the safe area insets as margins (`TNavigator.SafeArea`, on by default; `SetSafeArea(False)` for apps that handle it themselves). See [The safe area](/guide/navigation#the-safe-area). **Android: no crash without the icon font**. On Android `FMXER.IconFontsData` registered `Material Design Icons Desktop.ttf` from the documents folder without checking it: with Skia of RAD Studio 13 a missing file raises in the unit initialization, and the app closed at start (runtime error 217, white screen). The font is now registered only when present, as on Windows: without it the icons are missing, the app starts. Deploy it to `assets\internal` (see [The icon font](/guide/installation#the-icon-font)). **Demos and submodules**: - Android64 deployment of the demo media: the font, the Lottie animations and the images were deployed for Android 32-bit only (and the JSON files of QRCode outside `assets\internal`), so the 64-bit APKs had none of them. Every demo now deploys the font, and its media, to `assets\internal` for Android and Android64. - Every demo project updated for RAD Studio 13 (project version 20.5, the Android system libraries of RAD Studio 13, min SDK 24 and target SDK 36, the Android64 deployment as RAD Studio 13 writes it; no `libsk4d.so`: Skia is linked statically). Weather and MyTicketing get an Android manifest template. - Demo permissions: only what each demo uses (QRCode: camera and internet; LibraryStore and WebView: internet; the others none). Debug builds also get INTERNET, which RAD Studio adds for the debugger. - Demo launcher icon: the FMXER rocket (`media\android`), as an adaptive icon (also themed on Android 13+) and as legacy icons for every density, instead of the default FireMonkey icon. Package names `eu.andreamagni.`. - HelloWorld_IFIL: black screen on Android. With the Skia raster canvas (the default on Android) its `TIconFontsImageList` left the whole window black; the demo now uses the GPU canvas (`GlobalUseSkiaRasterWhenAvailable := False`), as HelloWorld does. - `lib/TFrameStand` updated to TFrameStand v.2.4 (interactive gestures of the frames registered with the hosting form). - `lib/ZXing.Delphi` updated to ZXing.Delphi 4.0 (branch `4.0`): 52 formats, parallel decoding, `IScanManager` and `ScanAsync`. - QRCode demo: the scanner thread and its frame queue are replaced by one `IScanManager` with `ScanAsync` (decoding in background, the result in the main thread); the code read is outlined from its corners (`TReadResult.Position`). `Threads.Scanner.pas` removed. ## v.1.5.3 — 2026-10-16 **Icon font on Windows**: without the *Material Design Icons Desktop* font installed, icons were drawn as empty boxes (for example the action button of the NewProjectTemplate demo). - The setup installs the font for the current user (it stays when FMXER is uninstalled, because other applications may use it). - `FMXER.IconFontsData` registers with Skia the `.ttf` found next to the executable: deploy the font with your application and it works on machines where the font is not installed. See [The icon font](/guide/installation#the-icon-font). ## v.1.5.2 — 2026-10-16 **Setup and IDE**: - The setup installs the libraries the QRCode demo needs, in `lib\DelphiZXingQRCode` and `lib\ZXing.Delphi`: the demos compile after a setup install. The LibraryStore demo needs the MARS-Curiosity client (its setup). See [Setup](/guide/installation#setup). - New design-time package `dclFMXERPackage` (Delphi 12 and 13): FMXER, with its version, in the splash screen and in the About box of the IDE. It registers no components. The setup and TMS Smart Setup install it. - The QRCode and NewProjectTemplate demos open with Win32 as the active platform, like the other demos. Library code is unchanged from v.1.5. ## v.1.5.1 — 2026-10-16 **Setup fixes** (the v.1.5 setup did not install): - The setup found no RAD Studio version: the `FMXER.groupproj` files of the packages had no build groups, which the setup reads to find the packages and their Delphi version. `setup\build-setup.cmd` now checks them before compiling the setup. - When TFrameStand or IconFontsImageList was not installed, the setup copied the files and then failed building the packages (`E2202 Required package 'IconFontsImageListFMX' not found`). It now checks for their packages (in the Dcp folder of the IDE and in the library path) and, if one is missing, stops before installing anything and says what to install. See [Setup](/guide/installation#setup). Library code is unchanged from v.1.5. ## v.1.5 — 2026-10-16 **Packages and installation** (ANALYSIS P4): - Runtime packages for Delphi 12 Athens and 13 Florence (`packages\12Athens`, `packages\13Florence`): `FMXERPackage` (all of `source\` but the QR code units) and `FMXERQRCodePackage` (`TQRCodeFrame`, `QRCode.*`, with DelphiZXingQRCode), Win32 and Win64. - [TMS Smart Setup](/guide/installation#tms-smart-setup): `tmsbuild.yaml`, with TFrameStand as a dependency. - A [setup](/guide/installation#setup) (Inno Setup, like TFrameStand's): packages and the source folder in the library path of every platform. **Demos and build** (ANALYSIS P3, E3): - Every demo uses the same search path: `..\..\source` and the `lib\` submodules (with empty submodule folders the IDE library path is used); the icon font is deployed from `media\`. - LibraryStore reads the MARS server URL from constants, or from the `FMXER_LIBRARYSTORE_URL` environment variable on desktop; the Android default is `10.0.2.2` (the host seen from the emulator). - `build.cmd [13|12] [win32|win64|all] [demos]`: Win64 with `dcc64`; outputs in `build\\`. The test suite compiles and passes on Win64 too (55 tests on each platform). ## v.1.4 — 2026-10-16 **Faster columns and rows** (ANALYSIS D3), measured on Delphi 13, Windows: - `TColumnFrame` and `TRowFrame` (new units): the API of `TColumnForm` and `TRowForm` as frames, about ten times cheaper to create (a form creates a native window: 2.4 ms against 0.2 ms). - `BatchUpdate` on the four of them: adding many elements realigns once, linear instead of quadratic (400 texts: 0.34 s instead of 1.5–2 s). - `TFMXERSlots`: the one implementation of the slots, usable for your own containers. **Back button** (ANALYSIS D5): `Navigator.SetCatchBackKey(True)` — Back and Esc close the top route, the root is left to the form, a transient route consumes the key. `HandleBackKey` for your own `OnKeyDown`. The NewProjectTemplate demo uses it. **Navigator instances** (ANALYSIS D4): `Navigator(AFormStand)` with another form stand raises (it returned the first navigator); `TNavigator.ReleaseInstance`; more navigators with `TNavigator.Create`, each with its own routes and stack. 55 tests. ## v.1.3 — 2026-10-16 **Automatic sizes** (ANALYSIS D1): - `TColumnForm.AddFrame(TColumnForm.AUTO_HEIGHT)` and `TRowForm.AddFrame(TRowForm.AUTO_WIDTH)` (and `AddForm`): the slot takes the element's own size, the one in its `.fmx` or the one set by the configuration procedure. See [Automatic sizes](/guide/layouts#automatic-sizes). - `TVertScrollFrame`, `TVertScrollForm`, `THorzScrollFrame`: a frame content keeps its design size (it got 50 pixels: TFrameStand aligns the frame to its stand before the configuration procedure runs), and `ContentStand` is used (it was ignored). - `TFMXERStands.NewSizedFrame` to do the same in your own containers. **Uniform fluent API** (ANALYSIS D2), all additions, existing code compiles unchanged: - every slot method returns its container (`TContainerForm`, `TVertScrollForm`, `THorzScrollFrame`, `TCardFrame`, `TActionButtonFrame` had procedures); - `SetOnClickProc` on every clickable element; `SetOnTapHandler` on the image elements; - setters for the properties: `TCardFrame.SetTitle`, `TWebViewFrame.SetURL`/`SetHTML`, `TLogoFrame.SetLogoResource`, `TActivityBubblesFrame.SetText`, `TVertScrollForm.SetTouchWidth`, `THorzScrollFrame.SetTouchHeight`, `SetContentStand` on the single-slot containers, `SetElementStand` and `TRowForm.SetElementAlign`. 37 tests. ## v.1.2.1 — 2026-10-16 - `TOnTapHandler` is declared once, in the new unit `FMXER.UI.Types`: `TImageFrame`, `TSVGFrame`, `TAnimatedImageFrame` and `TQRCodeFrame` share it (each unit keeps an alias), so one handler variable fits them all. `TPaintBoxFrame` uses `TPaintBoxTapHandler` (its `TOnTapHandler` is deprecated). - `SvgToBitmap`: the same 10-pixel margin on every side and the size asked (the code was 20 pixels smaller, off-center). - `LocalFile` takes its folder from the new `LocalFilesPath` variable (same defaults). - `TColumnForm`, `TRowForm`: their form elements are closed with them, like the frame elements. - Documentation workflow on Node 24 actions. Tests for each change (26 tests). ## v.1.2 — 2026-10-16 **Breaking change**: FMXER requires **Delphi 12 or later** and uses the Skia built into RAD Studio: the units are `System.Skia`, `FMX.Skia` and `FMX.Skia.Canvas` instead of Skia4Delphi's `Skia`, `Skia.FMX` and `Skia.FMX.Graphics` (replace them in your `uses` clauses; the `.fmx` files do not change). On Delphi 11 use v.1.1. Fixes, each with a test in the new DUnitX suite (`tests\`, run by `build.cmd`): - `TNavigator.CloseRoute` removes the closed route from the stack, wherever it is (it popped the top of the stack); the navigator remembers transient routes, so `CloseRoute` no longer needs `ATransient` and `CloseAllRoutes`/`CloseAllRoutesExcept` no longer raise `EListError` with a transient route open. - `TScaffoldForm.AddActionButton(ACaption, AParams)` without an `OnClickProc` param no longer raises `EInvalidCast`; `ShowSnackBar` frees the snackbar after hiding it. - `TListViewFrame`: pull-to-refresh and `RefreshList` no longer duplicate the items; click handlers are stored in the items (no stale handlers after `ClearItems` or swipe-to-delete). Setting `ItemBuilderProc` now clears the list before running the builder. - `TIconFonts.AddIcon` returns the existing image for an icon and color already added, instead of adding a new one at every call. - `THorzPairFrame.SetLeftContentStand`/`SetRightContentStand` store the stand name; `TCardFrame.SetGlyphAsForm` keeps the glyph info; `TFMXERFormHelper.SetPadding` works (constraint `TCommonCustomForm`). - `lib/TFrameStand` submodule updated to TFrameStand v.2.3 (FMXER still compiles with v.1.6). - `build.cmd`: builds the source, the tests and the demos; documentation site (this one). - Demos: MyTicketing and EKON25 - Weather ported to the fluent API (all 13 demos compile); HelloWorld and LibraryStore remove their message subscriptions when their elements are freed; HelloWorld_IFIL uses the icon table of `source`; the QRCode demo frees its share stream. - AI agent skill `fmxer-development` in `Skills/`; the repository is a Claude plugin marketplace (see [AI Agent Skill](/guide/agent-skills)). Also in this release, from the work on master after v.1.1 (up to February 2023): - New elements: `TAccessoryFrame` (content with left/right accessories), `THorzPairFrame` (50/50 pair), `TTrackbarFrame`, `TColorPickerFrame`, `TColorButtonFrame`. - `TIconFontsGlyphFrame` and `TGlyphFrame` upgraded (fluent setters, `SetIcon` overloads); IconFontsImageList as a submodule. - `TListViewFrame`: search box (`SearchVisible`, `SearchText`, `SetOnSearchChange`). - `TEditFrame` and `TTextFrame` upgraded; `SetWidth`/`SetHeight` helpers for forms; fluent ("chaining") setters across the elements. - New demos: **QRCode** (generator and camera reader with ZXing.Delphi), **NewProjectTemplate** (also for the iOS simulator on Apple silicon), **WebView**; HelloWorld with an animated background and a QR code. ## v.1.1 — 2022-11-03 **Breaking changes**: FMXER now depends on [Skia4Delphi](https://github.com/skia4delphi/skia4delphi) (4.0). Many changes in the framework: porting from v.1.0 may need some work. - Skia-based elements and the **Skia101** demo; support for Skia4Delphi 4.0.0. - Transient routes (`RouteTo(AName, True)`): routes that do not go on the stack. - New elements: `TWebViewFrame`, `TActivityBubblesFrame`, `TMarginFrame`, `TPaddingFrame`, `TVertScrollForm`; **MarginAndPadding** demo. - `TListViewFrame`: click handlers per item, `ItemAppearance`, `SelectedItem`, `AccessoryVisible`, `SearchVisible`. - `TEditFrame.FocusOnShow`, `TLogoFrame.LogoResource`, fixes to `TVertScrollFrame` and `TCardFrame`. - **LibraryStore** demo (MARS REST client) and **EKON25 - Weather** demo; projects upgraded to Delphi 11.1 Alexandria. ## v.1.0 — 2021-11-11 Initial release of the FMXER framework: `TNavigator`, scaffold, columns, rows, scroll boxes, cards and the first elements, built on TFrameStand, with Material Design icons through IconFontsImageList. FMXER was first shown at EKON 23 and [CodeRage 2019](https://youtu.be/RiK2re19Kyk) (October/November 2019).