> "The best tool is the one that lets you focus on the problem, not the tool."
Learning Objectives
- Describe the Lazarus IDE and its major components: form designer, Object Inspector, code editor, component palette
- Explain the LCL (Lazarus Component Library) and its role as a cross-platform abstraction layer
- Create a simple GUI application with a form, button, and label
- Explain the event-driven programming paradigm and contrast it with sequential execution
- Identify the files that make up a Lazarus project (.lpr, .pas, .lfm)
- Build PennyWise GUI v1 with a main form displaying an expense list
In This Chapter
Chapter 27: Introduction to Lazarus — The IDE, the Component Library, and Your First GUI Application
"The best tool is the one that lets you focus on the problem, not the tool." — Anders Hejlsberg (creator of Turbo Pascal and Delphi)
For twenty-six chapters, every program you have written has lived inside the console. Your screen has been a monochrome rectangle of text: prompts, responses, neatly formatted tables of numbers. PennyWise has tracked expenses, sorted them, saved them to typed files, organized them into class hierarchies, and even traversed recursive category trees — all without a single button, window, or mouse click.
That changes now.
In this chapter, we introduce Lazarus, the free, open-source Rapid Application Development (RAD) environment for Free Pascal. By the end of this chapter, you will have built your first graphical desktop application — a real window, with real buttons, that responds to real mouse clicks. More importantly, you will understand the fundamental paradigm shift that GUI programming demands: the move from sequential, top-to-bottom execution to event-driven programming, where your code sits and waits for things to happen.
This is a threshold concept. Once you cross it, you will never think about programs the same way again.
27.1 What Is Lazarus?
Lazarus is a free, open-source IDE (Integrated Development Environment) for Free Pascal that provides a complete Rapid Application Development experience. If you have ever heard of Delphi — the legendary RAD tool that dominated Windows development in the 1990s and 2000s — then Lazarus is its open-source counterpart. The compatibility is not accidental: Lazarus was explicitly designed to provide a Delphi-like experience using Free Pascal as its compiler backend.
Let us be specific about what Lazarus gives you:
A visual form designer. You design your application's user interface by dragging and dropping components onto a form. A button here, a text box there, a list view along the side. You see exactly what your application will look like before you write a single line of code.
A component library. The LCL (Lazarus Component Library) provides hundreds of pre-built components — buttons, labels, text fields, combo boxes, grids, trees, menus, toolbars, database connectors, image viewers, and much more. These components are cross-platform: the same code that creates a button on Windows creates a button on Linux and macOS.
A native compiler. Lazarus uses Free Pascal, which compiles your code to native machine code. No virtual machine. No interpreter. No runtime dependency. The executable you produce is a standalone binary that runs at full speed on the target operating system.
An integrated debugger. Set breakpoints, step through code, inspect variables — all within the IDE.
Cross-platform targeting. Write your code once, compile it for Windows, Linux, macOS, FreeBSD, and more. The LCL abstracts away the differences between each platform's native widget toolkit.
💡 Intuition: The RAD Advantage Traditional GUI programming involves writing code to create every element: instantiate a button, set its position (x=100, y=200), set its size (width=80, height=25), set its text ("OK"), register its click handler. In Lazarus, you drag a button from the palette, drop it on the form, type "OK" in the Object Inspector, and double-click to generate the click handler. The tedious boilerplate is eliminated. You focus on logic, not layout.
Lazarus vs. Delphi
Delphi is the commercial IDE from Embarcadero (originally Borland). It uses Object Pascal with its own compiler and an extensive commercial component library called the VCL (Visual Component Library) for Windows and FireMonkey (FMX) for cross-platform. Delphi is powerful and polished, but it costs hundreds of dollars and requires a license.
Lazarus is free. Its compiler (Free Pascal) is free. Its component library (LCL) is free. Its source code is available under the GPL and LGPL. You can build commercial applications with it without paying a cent. For a student learning Pascal, this is exactly what you want.
The two are largely source-compatible. If you learn Lazarus, you can move to Delphi with minimal adjustment. If you find Delphi code online, you can usually compile it in Lazarus with small modifications. The concepts, the component model, the event-driven architecture — all of it transfers.
Why Not Electron, Qt, or GTK Directly?
A fair question. If we want cross-platform GUI applications, why not use Electron (JavaScript + Chromium), Qt (C++), or GTK (C)?
Electron bundles an entire web browser engine into every application. A "Hello World" Electron app is over 100 megabytes. A Lazarus "Hello World" is under 5 megabytes — and it starts in milliseconds, not seconds. For a personal finance tracker like PennyWise, this difference is not academic. It is the difference between an application that respects the user's computer and one that does not.
Qt is excellent, but it requires C++ — a language vastly more complex than Pascal. Qt also has licensing complications (LGPL with commercial exceptions) and a steep learning curve.
GTK is C-based, which means manual memory management, function-pointer callbacks, and no integrated visual designer comparable to Lazarus.
Lazarus gives us the best combination: visual design, native performance, a comprehensible language, and zero cost. For learning GUI programming, it is hard to beat.
27.2 The Lazarus IDE Tour
When you open Lazarus for the first time, you will see several windows — and if you are used to single-window IDEs like VS Code, this can be disorienting. Lazarus uses a multi-window layout by default (though you can switch to a docked layout in View > IDE Options > Desktop). Let us walk through each component in detail.
The Main Menu and Toolbar
At the top of the screen, you will find the main menu bar and a toolbar with icons for common operations: New, Open, Save, Run, Step Into, Step Over. The main menu gives you access to everything — file operations, project management, compiler options, search, view settings, and help.
The toolbar provides one-click access to the most frequent operations. From left to right, you will typically find: New Unit, Open File, Save, Save All, then a gap, then Run (the green arrow), Pause, Stop, Step Into, Step Over, Step Out. The Run button (F9) is the one you will click most often — it compiles your project and launches the executable. If there are compilation errors, the Messages window at the bottom displays them and the executable is not launched.
Below the toolbar sits the Component Palette — a tabbed strip of icons representing the components you can drop onto your forms. This is the toolbox of visual programming.
The Component Palette in Detail
The Component Palette is organized by category. Each tab groups related components:
| Tab | Contents |
|---|---|
| Standard | TButton, TLabel, TEdit, TMemo, TCheckBox, TRadioButton, TListBox, TComboBox, TPanel, TGroupBox, TRadioGroup, TScrollBox |
| Additional | TBitBtn, TSpeedButton, TImage, TShape, TBevel, TNotebook, TStaticText, TMaskEdit, TCheckListBox, TSplitter |
| Common Controls | TTrackBar, TProgressBar, TTreeView, TListView, TStatusBar, TToolBar, TPageControl, TTabSheet, TDateTimePicker, THeaderControl |
| Dialogs | TOpenDialog, TSaveDialog, TColorDialog, TFontDialog, TFindDialog, TReplaceDialog, TSelectDirectoryDialog |
| Data Controls | TDBGrid, TDBNavigator, TDBEdit, TDBMemo, TDBText, TDBImage, TDBListBox, TDBComboBox, TDBCheckBox |
| Data Access | TDataSource, TSQLQuery, TSQLite3Connection, TSQLTransaction, TSQLScript |
| System | TTimer, TIdleTimer, TProcess, TApplicationProperties |
| Misc | TColorButton, TSpinEdit, TFloatSpinEdit, TArrow, TCalendar |
You do not need to memorize this. You will learn the components gradually, chapter by chapter. For now, just know that the palette exists and that it is organized by function.
To use a component, click its icon on the palette and then click on the form where you want it to appear. Alternatively, double-click the icon to place it at the center of the form. If you need multiple copies of the same component, hold Shift while clicking the palette icon, then click the form repeatedly — each click places another instance.
The Form Designer
The large, central area displays your form — the window of your application. When you create a new project, Lazarus gives you an empty form named Form1. This is literally the window that your users will see. You can resize it by dragging its edges. You can change its title by editing the Caption property. And you can populate it by dragging components from the palette and dropping them onto the form surface.
Each component you drop becomes a visual element on the form. A TButton becomes a button. A TLabel becomes a text label. A TEdit becomes a text input field. You can move them by dragging, resize them by pulling their handles, and configure them through the Object Inspector.
The form designer supports several useful features you should know about:
Alignment guides. When you drag a component near another component, dotted guide lines appear showing alignment. These help you position components consistently without manual pixel counting.
Grid snapping. By default, components snap to an invisible grid. You can configure the grid size in Tools > Options > Form Editor, or disable snapping entirely for pixel-precise placement.
Selection and multi-selection. Click a component to select it. Hold Shift and click to add to the selection. Drag a rectangle around multiple components to select them all. With multiple components selected, you can align, space, or resize them together using the alignment toolbar or the Position menu.
Component ordering. When components overlap, right-click and choose "Move to Front" or "Send to Back" to control the Z-order (which component appears on top).
Tab order editor. Right-click the form and choose "Tab Order" to see and rearrange the order in which the Tab key moves focus between controls.
The Object Inspector
The Object Inspector is a two-column panel (usually docked on the left) that shows every property and event of the currently selected component. It has two main tabs and an additional Favorites tab:
Properties tab: Here you can set the component's Name (its identifier in your code), its Caption (the text it displays), its Color, its Font, its Width, Height, Left, Top, and dozens of other properties. Changes you make here take effect immediately on the form.
Properties are organized hierarchically. The Font property expands to reveal sub-properties: Font.Name, Font.Size, Font.Style, Font.Color. The Anchors property expands to reveal akTop, akLeft, akRight, akBottom. You click the + to expand a property and the - to collapse it.
Some properties use dropdown selectors (like Color, which shows a color picker), some use check boxes (like Visible), and some use set editors (like Font.Style, which lets you check individual styles: bold, italic, underline, strikeout).
Events tab: Here you can see every event the component can fire: OnClick, OnChange, OnKeyPress, OnMouseEnter, OnResize, and so on. Double-clicking an event slot creates an event handler — a procedure in your source code that runs when that event occurs. If a handler already exists, double-clicking takes you to it in the source editor.
You can also assign an existing handler to an event by selecting it from the dropdown in the event slot. This is how you connect multiple components to the same event handler.
Favorites tab: Lets you pin frequently used properties for quick access. Right-click any property and choose "Add to Favorites."
📊 Analogy: The Object Inspector as a Control Panel Think of the Object Inspector as the settings panel for a smart device. The Properties tab is like the configuration settings: color, size, label, visibility. The Events tab is like the automation rules: "When this happens, do that." You do not need to write code to change a button's color; you set it in the Object Inspector. You do write code for what happens when the button is clicked.
The Source Editor
Below (or beside) the form, you will find the Source Editor — a full-featured code editor with syntax highlighting, code completion, bracket matching, and find/replace. This is where you write the Pascal code that gives your application its behavior.
Every form has a corresponding .pas unit file. When you select a form and press F12, Lazarus toggles between the visual form designer and the source code. When you double-click a button on the form, Lazarus creates the event handler procedure in the source code and places your cursor inside it, ready for you to type the logic.
The source editor provides several productivity features:
Code completion. Type an object name followed by a period (e.g., Button1.) and press Ctrl+Space. A popup appears listing all available properties and methods. Select one and press Enter to insert it.
Parameter hints. When you type a function name and an opening parenthesis, a tooltip appears showing the expected parameters.
Code templates. Type an abbreviation and press Ctrl+J to expand it into a code template. For example, typing begin and pressing Ctrl+J can expand into a begin..end block.
Jump to declaration. Ctrl+Click on any identifier to jump to its declaration. This works for your own code, LCL components, and even standard library functions.
Class completion. In the type declaration, type a method signature (e.g., procedure DoSomething;), then press Ctrl+Shift+C. Lazarus creates the method body in the implementation section automatically.
The Messages Window
At the bottom, the Messages window shows compiler output: errors, warnings, hints, and linking information. When you press F9 (Run), Free Pascal compiles your project, and any issues appear here. Double-clicking an error message takes you to the offending line in the source editor.
The Messages window color-codes its output: errors are typically shown in red or bold, warnings in yellow or regular text, hints in a lighter color. Pay attention to warnings — they often signal real problems (unused variables, uninitialized variables, type mismatches) that the compiler lets pass but that may cause bugs at runtime.
The Debugger
Lazarus integrates with GDB (GNU Debugger) to provide a full debugging experience within the IDE. You can:
Set breakpoints. Click in the gutter (the gray strip to the left of line numbers) to set a breakpoint. When execution reaches that line, the program pauses and you can inspect its state.
Step through code. F7 (Step Into) executes the current line and enters any procedure calls. F8 (Step Over) executes the current line without entering procedure calls. Shift+F8 (Step Out) continues execution until the current procedure returns.
Inspect variables. Hover over any variable in the source editor to see its current value. Or use the Watches window (View > Debug Windows > Watches) to monitor specific expressions.
View the call stack. The Call Stack window shows the chain of procedure calls that led to the current line — invaluable for understanding how your code reached a particular point.
Evaluate expressions. Ctrl+F7 opens an Evaluate window where you can type any expression and see its result. You can even call functions and modify variables in the paused program.
For PennyWise, the debugger becomes essential when event handlers interact in unexpected ways or when data does not flow as expected between forms. Rather than sprinkling ShowMessage calls throughout your code (the GUI equivalent of "printf debugging"), use breakpoints and watches to observe your program's state without modifying it.
27.3 The LCL: Lazarus Component Library
The LCL is the heart of Lazarus. It is a class library — a hierarchy of object-oriented components — that provides the building blocks for graphical applications. If you have used Delphi's VCL, the LCL will feel immediately familiar. If you have not, here is the essential architecture.
The Component Hierarchy
Everything in the LCL descends from a single root class: TObject. The key classes in the hierarchy are:
TObject
└── TPersistent (can save/load properties)
└── TComponent (can be owned, managed in the IDE)
└── TControl (has position, size, visibility)
├── TGraphicControl (lightweight, no window handle)
│ └── TLabel
│ └── TShape
└── TWinControl (has its own window handle)
└── TCustomControl
└── TButtonControl
│ └── TButton
└── TCustomEdit
│ └── TEdit
└── TCustomForm
└── TForm
The distinction between TGraphicControl and TWinControl matters: a TWinControl has its own operating system window handle and can receive keyboard focus. A TGraphicControl is lighter — it is drawn on its parent's surface and cannot receive focus. TLabel is a graphic control (it just displays text), while TEdit is a windowed control (users type into it).
Let us walk through each level of this hierarchy, because understanding it helps you understand how every LCL component works:
TObject is the root of every class in Object Pascal. It provides basic methods like Create, Free, ClassName, and ClassParent. Every LCL component is ultimately a TObject.
TPersistent adds the ability to save and load properties. When Lazarus writes the .lfm file (the visual design of your form), it uses TPersistent's streaming mechanism to serialize every component's properties to text. When the application loads, it reads the .lfm and reconstructs the components. This is why you can design a form visually and it appears identically at runtime.
TComponent adds the concept of ownership. Every component has an Owner — usually the form that contains it. When the owner is destroyed, it automatically destroys all the components it owns. This is how memory management works in GUI applications: you create components owned by the form, and the form cleans them up when it closes. You rarely need to call Free on a component you placed on a form at design time.
TComponent also adds the Name property, which uniquely identifies a component within its owner. When you name a button btnSave in the Object Inspector, that becomes both the identifier in your Pascal code (btnSave: TButton) and the name used in the .lfm file for serialization.
TControl adds visual properties: Left, Top, Width, Height, Visible, Enabled, Color, Font, Anchors, Align, Hint, Cursor. Every visible element on your form is a TControl. The Parent property determines which container a control appears inside — a button's parent is typically a panel or the form itself.
TGraphicControl is for lightweight controls that do not need their own window handle. They draw on their parent's surface, which makes them efficient but means they cannot receive keyboard input. Labels, shapes, and images are graphic controls.
TWinControl is for heavyweight controls that have their own window handle from the operating system. They can receive keyboard focus, host child controls, and interact directly with the OS windowing system. Edit boxes, list boxes, panels, forms — all are windowed controls.
Cross-Platform Abstraction
The LCL achieves cross-platform compatibility through a widgetset architecture. The LCL defines the interface — the API that you program against. Behind the scenes, the widgetset layer translates LCL calls into the native toolkit of the target platform:
| Platform | Widgetset | Native Toolkit |
|---|---|---|
| Windows | win32/win64 | Windows API (GDI) |
| Linux | gtk2, gtk3, qt5 | GTK or Qt |
| macOS | cocoa | Cocoa (native macOS) |
| FreeBSD | gtk2, qt5 | GTK or Qt |
When you place a TButton on your form, Lazarus does not draw a button from scratch. It asks the operating system to create a native button using the platform's native toolkit. On Windows, your button looks like a Windows button. On macOS, it looks like a macOS button. On Linux with GTK, it looks like a GTK button. Your code is identical in all cases.
This is profoundly different from frameworks like Java Swing (which draws its own controls and never quite looks native) or Electron (which uses web rendering for everything). LCL applications are genuinely native on every platform.
The widgetset architecture means that when you compile your Lazarus project for a different platform, the compiler links against a different widgetset implementation. Your Pascal code does not change. The TButton on your form is the same TButton in your code. Only the underlying implementation — the bridge to the native OS — is different. This is polymorphism at the system level.
⚠️ Caution: Platform Differences Cross-platform does not mean identical-on-every-platform. Font sizes differ between Windows and Linux. Default button heights differ between macOS and Windows. The file dialog looks completely different on each OS. Your layout should use anchors and auto-sizing rather than hardcoded pixel positions, and you should test on your target platforms. We will cover this in detail in Chapter 32.
The Component Model
Every LCL component has three facets:
- Properties define the component's state: its text, color, size, position, visibility, enabled/disabled status.
- Methods define what the component can do:
Show,Hide,SetFocus,Repaint,Free. - Events define what the component responds to:
OnClick,OnChange,OnKeyDown,OnPaint,OnClose.
Properties are set at design time (in the Object Inspector) or at runtime (in code). Events are handled by writing procedures that Lazarus connects to the component. This property-method-event model is called PME, and it is the conceptual framework for everything we do with the LCL.
Understanding PME is crucial because it maps directly to how you think about GUI development. When you ask yourself "What should this button look like?" you are thinking about properties. When you ask "What should happen when the user clicks this button?" you are thinking about events. When you ask "What can I tell this button to do from my code?" you are thinking about methods.
27.4 Your First GUI Application
Enough theory. Let us build something.
Step 1: Create a New Project
Open Lazarus. Go to Project > New Project > Application. Lazarus creates a new project with:
- A project file (
project1.lpr) - A main form (
Form1) with its unit file (unit1.pas) and form file (unit1.lfm)
You should see an empty form in the designer and a skeleton unit in the source editor.
Step 2: Set the Form Properties
Click on the empty form. In the Object Inspector, find the Caption property and change it from Form1 to My First Lazarus App. Notice how the form's title bar updates immediately. Set Width to 400 and Height to 300.
Step 3: Add a Button
In the Component Palette, find the Standard tab. Click on TButton (the icon that looks like a small button). Then click on the form where you want the button to appear. A button labeled Button1 appears on the form.
In the Object Inspector, change the button's Caption to Click Me!. Set its Width to 120 and Height to 40. Position it roughly in the center of the form.
Step 4: Add a Label
From the Standard tab, click TLabel and place it above the button. Change its Caption to Hello! Click the button below.. Set its Font.Size to 12 to make it larger.
Step 5: Write the Event Handler
Here is the crucial moment. Double-click the button on the form. Lazarus switches to the source editor and creates this procedure:
procedure TForm1.Button1Click(Sender: TObject);
begin
end;
This is an event handler — a procedure that Lazarus will call whenever the user clicks Button1. The Sender parameter tells you which component triggered the event (useful when multiple components share the same handler).
Type the following inside the begin..end block:
procedure TForm1.Button1Click(Sender: TObject);
begin
Label1.Caption := 'You clicked the button!';
Button1.Caption := 'Click Me Again!';
end;
Step 6: Run
Press F9 (or click the green Run arrow). Free Pascal compiles your project, and your application launches. You see a window with the title "My First Lazarus App," a label, and a button. Click the button. The label changes. The button text changes. You have built a GUI application.
Press the window's close button (the X) to return to the IDE.
💡 Intuition: What Just Happened?
Behind the scenes, Lazarus generated a Windows message loop (or its equivalent on other platforms) that sits and waits for user input. When you clicked the button, the operating system generated a "mouse click" message, the LCL routed it to Button1, Button1 recognized it as a click event, and it called your Button1Click procedure. Your code ran, changed the label and button captions, and control returned to the message loop to wait for the next event. The entire architecture of event-driven programming was working beneath that single click.
Building a Temperature Converter
Let us build a slightly more interesting first application — a temperature converter with two text fields and two buttons. This exercise reinforces the event-driven model and introduces reading data from controls.
Step 1: Create a new Application project (Project > New Project > Application).
Step 2: Set the form's Caption to Temperature Converter, Width to 400, and Height to 250. Change the form's Position property to poScreenCenter so it appears centered on screen.
Step 3: Add the following components:
- A
TLabelwith CaptionFahrenheit:near the top-left. - A
TEditnamededtFahrenheitnext to the label. Clear itsTextproperty. - A
TLabelwith CaptionCelsius:below the first label. - A
TEditnamededtCelsiusnext to it. Clear itsTextproperty. - A
TButtonnamedbtnFtoCwith CaptionF -> Cbetween the two edit fields on the right. - A
TButtonnamedbtnCtoFwith CaptionC -> Fbelow it. - A
TLabelnamedlblResultat the bottom with Caption (empty) andFont.Sizeset to11.
Step 4: Double-click btnFtoC and write:
procedure TForm1.btnFtoCClick(Sender: TObject);
var
F, C: Double;
begin
if TryStrToFloat(edtFahrenheit.Text, F) then
begin
C := (F - 32) * 5 / 9;
edtCelsius.Text := Format('%.2f', [C]);
lblResult.Caption := Format('%.2f F = %.2f C', [F, C]);
end
else
begin
lblResult.Caption := 'Please enter a valid number in Fahrenheit.';
edtFahrenheit.SetFocus;
end;
end;
Step 5: Double-click btnCtoF and write:
procedure TForm1.btnCtoFClick(Sender: TObject);
var
F, C: Double;
begin
if TryStrToFloat(edtCelsius.Text, C) then
begin
F := C * 9 / 5 + 32;
edtFahrenheit.Text := Format('%.2f', [F]);
lblResult.Caption := Format('%.2f C = %.2f F', [C, F]);
end
else
begin
lblResult.Caption := 'Please enter a valid number in Celsius.';
edtCelsius.SetFocus;
end;
end;
Step 6: Press F9 to run. Type 212 in the Fahrenheit field and click F -> C. The Celsius field shows 100.00. Type 0 in the Celsius field and click C -> F. The Fahrenheit field shows 32.00.
Notice the pattern: read data from a control (edtFahrenheit.Text), process it (the conversion formula), and write the result back to a control (edtCelsius.Text). This read-process-write cycle is the fundamental pattern of GUI event handlers.
The Generated Code
Let us examine what Lazarus generated for our first application. Press F12 to switch to the source editor and look at the complete unit:
unit Unit1;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls;
type
{ TForm1 }
TForm1 = class(TForm)
Button1: TButton;
Label1: TLabel;
procedure Button1Click(Sender: TObject);
private
public
end;
var
Form1: TForm1;
implementation
{$R *.lfm}
{ TForm1 }
procedure TForm1.Button1Click(Sender: TObject);
begin
Label1.Caption := 'You clicked the button!';
Button1.Caption := 'Click Me Again!';
end;
end.
Let us dissect this:
{$mode objfpc}{$H+}— Compiler directives: Object Free Pascal mode with long strings (AnsiString by default).- The
usesclause imports the LCL units we need:Classes(base classes),SysUtils(utilities),Forms(form support),Controls(control base classes),Graphics(drawing),Dialogs(message boxes),StdCtrls(standard controls like TButton and TLabel). TForm1 = class(TForm)— Our form is a class that inherits fromTForm. The button and label are fields of this class. They are not local variables; they belong to the form.{$R *.lfm}— This compiler directive links the.lfmform file, which contains the visual design (positions, sizes, properties of all components). The IDE maintains this file automatically.- The
Button1Clickprocedure is a method ofTForm1. It is connected toButton1'sOnClickevent through the form file.
This is the pattern for all Lazarus development: a form class with components as fields, event handler methods that respond to user actions, and a .lfm file that stores the visual layout.
27.5 Event-Driven Programming
This section describes the most important concept in this chapter — and arguably the most important paradigm shift in this entire book.
The Sequential Model
Every program you have written so far follows the sequential model:
1. Program starts.
2. Execute statement 1.
3. Execute statement 2.
4. If condition, execute statement 3a, else execute statement 3b.
5. Loop: execute statements 4 through 7.
6. Program ends.
You, the programmer, control the order of execution. The program does what you tell it, in the order you tell it, and when it runs out of instructions, it stops. Even when you added user input with ReadLn, the program was still sequential — it paused at the ReadLn, waited for input, and then continued on its predetermined path.
The Event-Driven Model
GUI applications work fundamentally differently:
1. Application starts.
2. Create the form and its components.
3. Enter the EVENT LOOP.
4. Wait for something to happen (a click, a key press, a timer tick...).
5. When something happens, call the appropriate event handler.
6. Return to step 4.
7. (Repeat until the user closes the application.)
The critical difference is step 4: the program waits. It does not execute the next line of code. It does not proceed to the next instruction. It sits in the event loop, doing nothing, consuming almost no CPU, until the operating system tells it that something has happened.
This is counterintuitive for programmers trained in the sequential model. You might ask: "If the program is waiting, how does anything happen?" The answer is that the operating system is the active party. It monitors the mouse, the keyboard, the screen, the network. When the user moves the mouse over your window, the OS generates a "mouse move" message. When the user clicks a button, the OS generates a "mouse click" message. When a timer expires, the OS generates a "timer" message. These messages are placed in your application's message queue, and the event loop processes them one at a time.
🔗 Cross-Reference: The Observer Pattern If you have studied design patterns (or if you read ahead to Chapter 33), you will recognize event-driven programming as an application of the Observer pattern. Components are subjects that notify observers (event handlers) when something interesting happens. The message loop is the infrastructure that delivers these notifications.
The Message Loop
At the heart of every GUI application is the message loop (also called the event loop or main loop). In pseudocode:
while not Terminated do
begin
message := GetNextMessage(); { blocks until a message arrives }
TranslateMessage(message);
DispatchMessage(message); { routes to the correct handler }
end;
You almost never write this loop yourself — the LCL creates it for you when you call Application.Run in the project file. But understanding that it exists is essential for understanding how your event handlers get called.
When the loop dispatches a message to a component, the component examines the message type and fires the appropriate event. A left mouse button click on a TButton fires the OnClick event. A key press in a TEdit fires the OnKeyPress event. A timer tick fires the OnTimer event.
The Lifecycle of a Click
Let us trace exactly what happens when the user clicks a button, from the hardware level up to your event handler:
- The user presses the left mouse button. The mouse hardware generates an interrupt signal.
- The operating system's input subsystem receives the interrupt and determines which window is under the cursor.
- The OS places a
WM_LBUTTONDOWNmessage (on Windows) or equivalent into the application's message queue. - The application's event loop picks up the message with
GetNextMessage(). DispatchMessage()routes it to the correct window — in this case, the button's window handle.- The LCL's internal handler for the button receives the message. It checks: did the mouse go down and come back up within the button's bounds? If yes, this qualifies as a "click."
- The button fires its
OnClickevent by calling the method pointer stored in itsOnClickproperty. - Your
Button1Clickprocedure executes. - Your procedure returns, control returns to the LCL handler, which returns to
DispatchMessage, which returns to the event loop. - The event loop calls
GetNextMessage()again — and waits.
This entire chain — from mouse hardware to your procedure and back — typically takes less than a millisecond. The user perceives it as instantaneous.
Writing Event Handlers
An event handler is simply a method of your form class that matches a specific signature. For most events, the signature is:
procedure TForm1.SomeHandler(Sender: TObject);
The Sender parameter identifies which component triggered the event. This is important when multiple components share the same handler:
procedure TForm1.AnyButtonClick(Sender: TObject);
begin
if Sender = Button1 then
Label1.Caption := 'You clicked Button 1'
else if Sender = Button2 then
Label1.Caption := 'You clicked Button 2';
end;
Some events have additional parameters. A keyboard event provides the key that was pressed:
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
if Key = #13 then { Enter key }
begin
ShowMessage('You pressed Enter! Text is: ' + Edit1.Text);
Key := #0; { consume the key }
end;
end;
A mouse event provides the position and button state:
procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
Label1.Caption := Format('Mouse click at (%d, %d)', [X, Y]);
end;
Events You Will Use Constantly
Here are the events you will encounter most often in the chapters ahead:
| Event | Fired When | Common Use |
|---|---|---|
OnClick |
User clicks a component | Button actions, menu selections |
OnChange |
A component's value changes | Validating input, updating displays |
OnKeyPress |
User presses a key | Keyboard shortcuts, input filtering |
OnCreate |
Form is being created | Initialization, loading data |
OnClose |
Form is being closed | Saving data, cleanup |
OnCloseQuery |
Form is about to close | Prompting to save unsaved changes |
OnResize |
Form or component is resized | Adjusting layout |
OnPaint |
Component needs to be redrawn | Custom drawing (Chapter 30) |
OnTimer |
Timer interval elapses | Periodic updates, animation |
OnShow |
Form is about to become visible | Last-minute setup before display |
The Mental Trap: Long-Running Event Handlers
Here is a mistake that every GUI programmer makes exactly once. You write an event handler that takes a long time — loading a large file, performing a complex calculation, waiting for a network response. While your event handler is running, the message loop is blocked. No other events are processed. The window cannot be repainted. The user cannot click anything. The application appears frozen.
⚠️ Caution: Never Block the Main Thread
Event handlers should complete quickly — ideally in under 100 milliseconds. If you need to perform a long operation, you must either break it into small chunks (using Application.ProcessMessages to let the event loop breathe between chunks) or move the work to a background thread (Chapter 36). For now, keep your event handlers fast.
This is why understanding the message loop matters. It is not just theoretical architecture — it directly affects how you write code. Every event handler runs inside the message loop. If your handler does not return, the loop does not continue, and the application freezes.
27.6 The Project File Structure
A Lazarus project consists of several files. Understanding them is important for version control, for debugging, and for understanding what Lazarus does behind the scenes.
The Project File (.lpr)
The .lpr file (Lazarus Project Resource) is the main program file. It looks like this:
program Project1;
{$mode objfpc}{$H+}
uses
{$IFDEF UNIX}
cthreads,
{$ENDIF}
{$IFDEF HASAMIGA}
athreads,
{$ENDIF}
Interfaces, // this includes the LCL widgetset
Forms,
Unit1;
{$R *.res}
begin
RequireDerivedFormResource := True;
Application.Scaled := True;
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
This is equivalent to the main function in C or the if __name__ == '__main__' block in Python. It initializes the application, creates the main form, and starts the event loop with Application.Run. The {$IFDEF UNIX} cthreads {$ENDIF} block conditionally includes threading support on Unix-like systems, which is needed for multi-threaded applications.
The {$R *.res} directive includes Windows resource files (icons, version information). The Interfaces unit loads the appropriate widgetset for the current platform.
Let us break down the startup sequence:
Application.Initializeperforms platform-specific initialization. On Windows, it registers window classes and initializes the common controls library. On Linux, it initializes the GTK or Qt toolkit.Application.Scaled := Trueenables High-DPI awareness. On screens with 150% or 200% scaling, controls will be scaled appropriately rather than appearing tiny.Application.CreateForm(TForm1, Form1)creates an instance of TForm1 and assigns it to theForm1variable. The first form created withCreateFormbecomes the main form — closing it terminates the application. If your project has multiple forms, they are all created here, in order.Application.Runenters the message loop. Execution stays inside this call until the application terminates.
Unit Files (.pas)
Each form has a corresponding .pas unit file. This is where your code lives. You can also create non-visual units (for business logic, data structures, utility functions) using File > New Unit. Non-visual units are plain Pascal units with no form.
Form Files (.lfm)
Each form has a corresponding .lfm file that stores the visual design in a text-based format:
object Form1: TForm1
Left = 300
Height = 300
Top = 200
Width = 400
Caption = 'My First Lazarus App'
object Label1: TLabel
Left = 100
Height = 20
Top = 80
Width = 200
Caption = 'Hello! Click the button below.'
Font.Size = 12
ParentFont = False
end
object Button1: TButton
Left = 140
Height = 40
Top = 140
Width = 120
Caption = 'Click Me!'
OnClick = Button1Click
end
end
This is a human-readable text format. You can edit it by hand if needed (right-click the form, choose "View Source (.lfm)"), but normally you let the form designer maintain it. Notice the line OnClick = Button1Click — this is how the form file connects the button's click event to your event handler procedure.
💡 Intuition: .lfm Is Your Form's Blueprint
The .lfm file is to your form what a blueprint is to a building. It describes every component, every property, every event connection. When the application starts, Lazarus reads this file and constructs the actual form from it. This is called streaming — the form is serialized to the .lfm file at design time and deserialized from it at runtime.
Session and Configuration Files
Lazarus also generates several files you generally do not edit:
| File | Purpose |
|---|---|
.lpi |
Project information (XML): compiler options, search paths, dependencies |
.lps |
Session file: editor state, breakpoints, bookmarks |
.compiled |
Tracks last compilation |
lib/ folder |
Compiled units (.o, .ppu files) |
For version control, you should track .lpr, .pas, .lfm, and .lpi files. You should generally ignore .lps, .compiled, and the lib/ directory.
27.7 From Console to GUI: The Mental Model Shift
The transition from console to GUI programming is not just a matter of learning new components. It requires a fundamental change in how you think about program structure. Let us make this explicit.
Console Thinking vs. GUI Thinking
| Console | GUI |
|---|---|
| The program controls the flow | The user controls the flow |
| Code runs top to bottom | Code runs in response to events |
ReadLn pauses for input |
Events arrive asynchronously |
| One thing happens at a time | Multiple events may queue up |
Output is WriteLn |
Output is changing component properties |
| State lives in variables | State lives in components and variables |
| Program ends when code finishes | Program ends when user closes the window |
Where Does Your Logic Go?
In a console program, your logic lives in the main begin..end block and the procedures it calls. In a GUI program, your logic is distributed across event handlers. There is no single "main flow." Instead, you have a collection of responses:
- When the user clicks "Add Expense," validate the input and add it to the list.
- When the user selects an item in the list, display its details.
- When the user clicks "Save," write the data to disk.
- When the form loads, read existing data from disk.
Each of these is a separate event handler. The order in which they execute depends entirely on the user's actions, not on your code's structure.
Separating Logic from UI
This distributed structure makes one thing critically important: keep your business logic separate from your UI code. If your expense validation logic is buried inside Button1Click, you cannot reuse it, you cannot test it independently, and you cannot change the UI without risk of breaking the logic.
The pattern we will follow throughout Part V is:
- Business logic in separate units (no LCL dependencies).
- UI code in form units (reads from components, calls business logic, updates components).
- Event handlers are thin — they extract data from the UI, call business logic, and display results.
This is not just good practice; it is the same separation of concerns you learned with records and classes in earlier chapters, applied to a new context. Rosa's PennyWise already has a TExpense class, a TBudget class, and a TReport class from Part III. Those classes will work unchanged in our GUI. We just need to write the UI layer that connects them to visual components.
An Analogy: The Restaurant
Think of a console program as a food truck: the cook takes the order, prepares the food, and hands it through the window. One cook, one customer, linear flow.
A GUI program is a restaurant. There is a dining room (the form) with tables and chairs (components). There are waiters (event handlers) who respond to customers' requests. There is a kitchen (business logic) that prepares the food. The maitre d' (the event loop) coordinates everything. Customers arrive unpredictably, multiple tables are active at once, and the kitchen needs to handle orders in whatever sequence they come.
You do not redesign the kitchen every time you redecorate the dining room. And you do not put the stove in the dining room. Keep the concerns separate.
Common Beginner Mistakes in the Transition
As you move from console to GUI programming, watch out for these common pitfalls:
Mistake 1: Using ReadLn in a GUI application. In a console program, ReadLn blocks and waits for input. In a GUI program, there is no console to read from. If you call ReadLn from a GUI event handler, the application hangs. Use edit controls, combo boxes, and dialogs to collect input instead.
Mistake 2: Using WriteLn for output. WriteLn writes to the console (standard output). In a GUI application, there may be no console. Set Label.Caption, use ShowMessage, or update a TMemo control instead.
Mistake 3: Storing data only in local variables. In a console program, local variables persist as long as the procedure runs — and for a simple program, that might be the entire runtime. In a GUI program, each event handler is a separate procedure call. Local variables are created when the handler starts and destroyed when it returns. Data that must survive between events should be stored as private fields of the form class.
Mistake 4: Assuming execution order. In a console program, you know that line 10 runs before line 20. In a GUI program, you cannot assume that btnSave.OnClick runs before btnDelete.OnClick. The user controls the order. Design your event handlers so that they work correctly regardless of the order in which they are called.
27.8 Project Checkpoint: PennyWise GUI v1
It is time to begin the transformation of PennyWise from a console application to a graphical one. In this checkpoint, we create the first GUI version: a main form with an expense list, a total display, and the ability to add expenses through a simple input area.
Design
Our PennyWise GUI v1 will have:
- A main form with the title "PennyWise — Personal Finance Manager"
- A TStringGrid showing the expense list (columns: Date, Description, Category, Amount)
- A TPanel at the top with input controls: TDateTimePicker for the date, TEdit for description, TComboBox for category, TEdit for amount, and a TButton labeled "Add Expense"
- A TLabel at the bottom showing the total expenses
- A TButton labeled "Clear All" to reset the list
Here is how to lay it out step by step:
- Create a new Lazarus Application project. Name it
PennyWiseGUI. - Set the form's
NametofrmMain,CaptiontoPennyWise — Personal Finance Manager,Widthto700,Heightto500, andPositiontopoScreenCenter. - Drop a
TPanelonto the form. Set itsNametopnlInput,AligntoalTop, andHeightto50. SetBevelOutertobvNonefor a flat look. - Inside
pnlInput, add: aTLabelwith CaptionDate:, aTDateTimePickernameddtpDate, aTLabelwith CaptionDescription:, aTEditnamededtDescription(clear itsText), aTLabelwith CaptionCategory:, aTComboBoxnamedcboCategory, aTLabelwith CaptionAmount:, aTEditnamededtAmount(clear itsText), and aTButtonnamedbtnAddwith CaptionAdd Expense. - Drop a
TStringGridnamedsgExpensesonto the form. Set itsAligntoalClientso it fills the remaining space. - Drop a
TPanelat the bottom. SetAligntoalBottom,Heightto40,BevelOutertobvNone. Inside it, place aTLabelnamedlblTotalon the left (Caption empty,Font.Size= 12,Font.Style= [fsBold]) and aTButtonnamedbtnClearwith CaptionClear Allon the right.
Implementation
unit PennyWiseMain;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Forms, Controls, Graphics, Dialogs, Grids,
StdCtrls, ComCtrls, ExtCtrls;
type
{ TfrmMain }
TfrmMain = class(TForm)
btnAdd: TButton;
btnClear: TButton;
cboCategory: TComboBox;
edtDescription: TEdit;
edtAmount: TEdit;
dtpDate: TDateTimePicker;
lblTotal: TLabel;
lblDate: TLabel;
lblDescription: TLabel;
lblCategory: TLabel;
lblAmount: TLabel;
pnlInput: TPanel;
sgExpenses: TStringGrid;
procedure btnAddClick(Sender: TObject);
procedure btnClearClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
FNextRow: Integer;
procedure UpdateTotal;
procedure InitializeGrid;
procedure InitializeCategories;
public
end;
var
frmMain: TfrmMain;
implementation
{$R *.lfm}
{ TfrmMain }
procedure TfrmMain.FormCreate(Sender: TObject);
begin
InitializeGrid;
InitializeCategories;
FNextRow := 1; { Row 0 is the fixed header row }
end;
procedure TfrmMain.InitializeGrid;
begin
sgExpenses.ColCount := 4;
sgExpenses.RowCount := 2; { 1 fixed + 1 data row }
sgExpenses.FixedRows := 1;
sgExpenses.Cells[0, 0] := 'Date';
sgExpenses.Cells[1, 0] := 'Description';
sgExpenses.Cells[2, 0] := 'Category';
sgExpenses.Cells[3, 0] := 'Amount';
sgExpenses.ColWidths[0] := 100;
sgExpenses.ColWidths[1] := 200;
sgExpenses.ColWidths[2] := 120;
sgExpenses.ColWidths[3] := 100;
end;
procedure TfrmMain.InitializeCategories;
begin
cboCategory.Items.Clear;
cboCategory.Items.Add('Food & Dining');
cboCategory.Items.Add('Transportation');
cboCategory.Items.Add('Housing');
cboCategory.Items.Add('Utilities');
cboCategory.Items.Add('Entertainment');
cboCategory.Items.Add('Health');
cboCategory.Items.Add('Education');
cboCategory.Items.Add('Other');
cboCategory.ItemIndex := 0;
end;
procedure TfrmMain.btnAddClick(Sender: TObject);
var
Amount: Double;
begin
{ Validate the amount }
if not TryStrToFloat(edtAmount.Text, Amount) then
begin
MessageDlg('Invalid Amount',
'Please enter a valid numeric amount.',
mtError, [mbOK], 0);
edtAmount.SetFocus;
Exit;
end;
if Amount <= 0 then
begin
MessageDlg('Invalid Amount',
'Amount must be greater than zero.',
mtError, [mbOK], 0);
edtAmount.SetFocus;
Exit;
end;
{ Validate the description }
if Trim(edtDescription.Text) = '' then
begin
MessageDlg('Missing Description',
'Please enter a description for this expense.',
mtWarning, [mbOK], 0);
edtDescription.SetFocus;
Exit;
end;
{ Add the row to the grid }
if FNextRow >= sgExpenses.RowCount then
sgExpenses.RowCount := sgExpenses.RowCount + 1;
sgExpenses.Cells[0, FNextRow] := FormatDateTime('yyyy-mm-dd', dtpDate.Date);
sgExpenses.Cells[1, FNextRow] := Trim(edtDescription.Text);
sgExpenses.Cells[2, FNextRow] := cboCategory.Text;
sgExpenses.Cells[3, FNextRow] := Format('%.2f', [Amount]);
Inc(FNextRow);
{ Clear inputs for next entry }
edtDescription.Text := '';
edtAmount.Text := '';
edtDescription.SetFocus;
{ Update the total }
UpdateTotal;
end;
procedure TfrmMain.btnClearClick(Sender: TObject);
begin
if MessageDlg('Confirm Clear',
'Are you sure you want to clear all expenses?',
mtConfirmation, [mbYes, mbNo], 0) = mrYes then
begin
sgExpenses.RowCount := 2;
sgExpenses.Cells[0, 1] := '';
sgExpenses.Cells[1, 1] := '';
sgExpenses.Cells[2, 1] := '';
sgExpenses.Cells[3, 1] := '';
FNextRow := 1;
UpdateTotal;
end;
end;
procedure TfrmMain.UpdateTotal;
var
I: Integer;
Total, Val: Double;
begin
Total := 0;
for I := 1 to FNextRow - 1 do
begin
if TryStrToFloat(sgExpenses.Cells[3, I], Val) then
Total := Total + Val;
end;
lblTotal.Caption := Format('Total Expenses: $%.2f', [Total]);
lblTotal.Font.Style := [fsBold];
lblTotal.Font.Size := 12;
end;
end.
A Detailed Walkthrough
Let us trace through the user experience step by step to see how the code responds to each action:
Application starts. Application.CreateForm(TfrmMain, frmMain) creates the form, which triggers FormCreate. This calls InitializeGrid (setting up column headers and widths) and InitializeCategories (populating the category dropdown). The form appears on screen. The event loop starts. Nothing else happens until the user does something.
User types in the Description field. Each keystroke generates OnKeyPress and OnChange events on edtDescription. We have not assigned handlers for those events, so the LCL handles them internally — the text appears in the field. This illustrates an important point: events fire constantly, even when you do not handle them. The default behavior handles most routine interactions.
User selects a category. The user clicks the dropdown arrow on cboCategory, sees the list of categories, and clicks one. The OnChange event fires — but again, we have not assigned a handler. The selection changes, and that is enough.
User clicks "Add Expense." Now our code runs. btnAddClick validates the amount (is it a valid number? is it positive?) and the description (is it non-empty?). If validation passes, it adds a row to the grid, increments FNextRow, clears the input fields, moves focus back to the description field (for quick consecutive entries), and calls UpdateTotal to recalculate and display the sum.
Notice how the event handler is structured: validate, perform the action, update the display, set focus for the next operation. This is a standard pattern for data-entry event handlers.
User clicks "Clear All." btnClearClick uses MessageDlg to confirm the destructive action. If the user clicks Yes, the grid is cleared and the total is updated. If the user clicks No, nothing happens. Always confirm destructive actions.
Testing Your Checkpoint
- Run the application (F9).
- Enter a date, description, category, and amount. Click "Add Expense." The expense should appear in the grid.
- Add several more expenses. The total should update after each addition.
- Try entering an invalid amount (letters, negative numbers). The validation should catch it.
- Click "Clear All." Confirm the dialog. The grid should be emptied.
Rosa looks at this and lights up. After 26 chapters of text menus and WriteLn statements, PennyWise finally looks like a real application. Tomas immediately wants to add a pie chart. We tell him to wait until Chapter 30.
✅ Checkpoint Checklist - [ ] Main form displays with the title "PennyWise — Personal Finance Manager" - [ ] Input panel contains date picker, description field, category dropdown, amount field, and Add button - [ ] Expense grid shows Date, Description, Category, and Amount columns - [ ] Validation rejects empty descriptions and non-numeric amounts - [ ] Total label updates after each addition - [ ] Clear All button prompts for confirmation before clearing
27.9 Summary
This chapter introduced you to Lazarus, the free, open-source RAD IDE for Free Pascal, and to the fundamental paradigm shift that GUI programming requires.
What we covered:
- Lazarus is a Delphi-compatible RAD IDE that produces native, cross-platform desktop applications. It is free, open-source, and extraordinarily capable.
- The IDE consists of the form designer (visual layout), Object Inspector (property/event editing), source editor (code with code completion, jump-to-declaration, and class completion), component palette (pre-built controls organized by category), messages window (compiler output), and integrated debugger (breakpoints, watches, stepping).
- The LCL (Lazarus Component Library) is a cross-platform component hierarchy built on the Property-Method-Event model. It uses widgetsets to create native controls on each platform. The hierarchy — TObject, TPersistent, TComponent, TControl, TGraphicControl/TWinControl — determines each component's capabilities.
- Event-driven programming replaces the sequential model. Your code does not run top to bottom; it responds to events (clicks, key presses, timer ticks) dispatched by the message loop. Event handlers must be fast — long handlers freeze the UI. We traced the full lifecycle of a click from hardware interrupt to handler execution and back.
- Project files include
.lpr(main program with startup sequence),.pas(unit code),.lfm(form layout via streaming), and.lpi(project configuration). - The mental model shift from console to GUI requires distributing logic across event handlers, keeping business logic separate from UI code, and thinking in terms of user actions rather than program flow. Common mistakes include using ReadLn, WriteLn, assuming execution order, and storing data only in local variables.
- PennyWise GUI v1 brought our finance tracker into the visual world with a string grid, input controls, validation, and a running total. We also built a temperature converter to practice the read-process-write pattern of GUI event handlers.
🚪 Threshold Concept: Event-Driven Programming The transition from sequential to event-driven programming is one of the great paradigm shifts in computer science. In a console program, you are the director: you control every moment of execution. In a GUI program, you are the responder: you set the stage, and then you wait for the user to act. This inversion of control is uncomfortable at first, but it is the foundation of every graphical, web, and mobile application in the world. If you understand event handlers, message loops, and the PME model, you understand the architecture of modern interactive software.
In the next chapter, we dive deep into the controls that make up a user interface — buttons, text fields, combo boxes, grids — and we learn to build complex, polished data-entry forms. PennyWise will get a proper expense entry form with tab navigation, validation feedback, and a professional layout.
"The gap between 'Hello World' and a usable application is smaller in Lazarus than in any other environment I have used." — A common sentiment in the Lazarus community forums