GUI Development: Lazarus LCL, Delphi VCL & FMX
All three share the same model: a form class whose published fields are components placed in the designer, properties set in the Object Inspector, and event handlers (method pointers) written in code.
38.1 Frameworks at a glance
LCL (Lazarus Component Library) Windows, Linux, macOS, FreeBSD, Raspberry Pi native widgets via *widgetsets*: win32, gtk2, gtk3, qt5, qt6, cocoa VCL (Visual Component Library) Windows (32/64-bit) native Win32 controls, VCL Styles FMX (FireMonkey)
Windows, macOS, iOS, Android, Linux (via FMXLinux) GPU-rendered, styled controls
All three share the same model: a form class whose published fields are components placed in the designer, properties set in the Object Inspector, and event handlers (method pointers) written in code.
38.2 A Lazarus form
The project file (todoapp.lpr) generated by Lazarus:
Part of the form file (mainform.lfm) — plain text, editable, and version-control friendly:
unit MainForm;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Edit1: TEdit; // created from the .lfm file
Button1: TButton;
ListBox1: TListBox;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure ListBox1DblClick(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.lfm} // links the form description
procedure TForm1.FormCreate(Sender: TObject);
begin
Caption := 'To-Do List';
Edit1.TextHint := 'New task...';
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
if Trim(Edit1.Text) = '' then
begin
ShowMessage('Please enter a task.');
Exit;
end;
ListBox1.Items.Add(Trim(Edit1.Text));
Edit1.Clear;
Edit1.SetFocus;
end;
procedure TForm1.ListBox1DblClick(Sender: TObject);
begin
if (ListBox1.ItemIndex >= 0) and
(MessageDlg('Delete this task?', mtConfirmation, [mbYes, mbNo], 0) = mrYes) then
ListBox1.Items.Delete(ListBox1.ItemIndex);
end;
end.program TodoApp;
{$mode objfpc}{$H+}
uses
{$IFDEF UNIX}cthreads,{$ENDIF}
Interfaces, // selects the LCL widgetset
Forms, MainForm;
{$R *.res}
begin
RequireDerivedFormResource := True;
Application.Scaled := True;
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.object Form1: TForm1
Left = 300
Height = 360
Top = 200
Width = 420
Caption = 'Form1'
OnCreate = FormCreate
object Edit1: TEdit
Left = 8
Top = 8
Width = 300
end
object Button1: TButton
Left = 316
Top = 8
Caption = 'Add'
OnClick = Button1Click
end
end38.3 Creating controls at run time
Owner controls lifetime; Parent controls display. Set Align, Anchors, and BorderSpacing (LCL) or Margins/Padding (VCL/FMX) for resizable layouts.
procedure TForm1.AddButton(const ACaption: string; ATop: Integer);
var
B: TButton;
begin
B := TButton.Create(Self); // owner: the form frees it
B.Parent := Self; // parent: where it is displayed
B.Caption := ACaption;
B.SetBounds(8, ATop, 120, 30);
B.OnClick := @Button1Click; // Delphi: B.OnClick := Button1Click;
end;38.4 Delphi VCL and FMX differences
Vcl.Forms, Vcl.StdCtrls, Vcl.Dialogs FMX.Forms, FMX.StdCtrls, FMX.Dialogs
Caption (labels/buttons), Text (edits) Text everywhere Integer pixels Single (device-independent) native Windows + VCL Styles styles (TStyleBook), platform-specific default styles
Align, Anchors, TGridPanel, TFlowPanel Align, TLayout, TGridPanelLayout, TFlowLayout, TScaledLayout GDI/GDI+, TCanvas, Direct2D
GPU canvas, 3D (TViewport3D), effects, animations data-aware controls (TDBGrid, TDBEdit) and LiveBindings
38.5 Common components across frameworks
TButton, TBitBtn, TSpeedButton TButton, TSpeedButton
TCheckBox, TRadioButton, TRadioGroup TCheckBox, TRadioButton
TListBox, TComboBox, TListView, TTreeView TStringGrid, TDrawGrid, TDBGrid TStringGrid, TGrid
TPanel, TGroupBox, TPageControl, TScrollBox TPanel, TLayout, TTabControl, TScrollBox
TMainMenu, TPopupMenu, TActionList TOpenDialog, TSaveDialog, ShowMessage, MessageDlg
TOpenDialog, ShowMessage, TDialogService TPaintBox + OnPaint, TImage TPaintBox, TImage
38.6 GUI guidelines
Keep business logic out of form units: forms call services/classes in separate units (testable without a GUI).
Use TActionList to share logic between menus, toolbars, and buttons.
Never block the main thread with long operations — use threads/tasks and update the UI via Synchronize/Queue.
Test High-DPI scaling (Application.Scaled, per-monitor DPI awareness in project options).
For Lazarus cross-platform apps, avoid Windows-only units and check each widgetset.