Your First Cross-Platform Desktop App with Lazarus and the LCL
Bjørn T. Nøstdahl
~5 min read
Tutorials for Lazarus often stop at a button that shows a message box. That tells you how the form designer works, but not how a real application feels. In this article we build something I actually use: a small viewer that loads a temperature log from a CSV file, shows it in a grid and plots it in a chart. Then we compile the same source on Windows and Linux without changing a line.
Installing Lazarus
On Windows, download the installer from the Lazarus website. It bundles the Free Pascal compiler, the IDE and the component library in one package, and it works out of the box. On Debian or Ubuntu, sudo apt install lazarus gives you a slightly older but perfectly usable version. On the Raspberry Pi the same command works, which we will come back to in a later article.
If you want the latest versions, or several versions side by side, look at fpcupdeluxe. It is a small GUI that downloads and builds Free Pascal and Lazarus for you, including cross-compilers.
Starting the project
Create a new Application project. Lazarus gives you a project file (.lpi), a program file (.lpr), and a unit with a form (Unit1.pas and Unit1.lfm). Save everything into a folder immediately and rename the form unit to something meaningful. I called mine MainForm.pas with the form named frmMain.
Now drop these components onto the form from the palette:
- A
TPanelaligned to the top with aTButtonnamedbtnOpen. - A
TStringGridnamedgrdReadingsaligned to the left. - A
TChartnamedchtTemperaturealigned to the client area. The chart comes from the TAChart package which ships with Lazarus. - A
TOpenDialognameddlgOpen.
Set Align on each component instead of placing them by hand. Alignment and anchors are how the LCL handles resizing, and they are also how you avoid layouts that look fine on Windows and overlap on GTK.
Reading the file
A log line in my file looks like 2023-09-18T14:05:00;living-room;21.4. The code below reads it into a list of records, which keeps the file handling separate from the user interface.
unit ReadingStore;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Generics.Collections;
type
TReading = record
Timestamp: TDateTime;
Sensor: string;
Celsius: Double;
end;
TReadingList = specialize TList<TReading>;
function LoadReadings(const FileName: string): TReadingList;
implementation
uses
DateUtils;
function LoadReadings(const FileName: string): TReadingList;
var
Lines, Parts: TStringList;
Line: string;
Reading: TReading;
Fmt: TFormatSettings;
begin
Result := TReadingList.Create;
Fmt := DefaultFormatSettings;
Fmt.DecimalSeparator := '.';
Lines := TStringList.Create;
Parts := TStringList.Create;
try
Parts.Delimiter := ';';
Parts.StrictDelimiter := True;
Lines.LoadFromFile(FileName);
for Line in Lines do
begin
Parts.DelimitedText := Line;
if Parts.Count <> 3 then
Continue;
Reading.Timestamp := ISO8601ToDate(Parts[0], False);
Reading.Sensor := Parts[1];
Reading.Celsius := StrToFloat(Parts[2], Fmt);
Result.Add(Reading);
end;
finally
Parts.Free;
Lines.Free;
end;
end;
end.
Two details are worth pointing out. specialize is how Free Pascal's objfpc mode instantiates a generic type; in Delphi mode you would write TList<TReading> directly. And StrToFloat with an explicit format settings record matters because on a Norwegian Windows machine the default decimal separator is a comma, while the CSV file uses a dot. Parsing with the machine's locale works on the developer's PC and fails on the first customer's, so settle it on day one.
Wiring up the form
Double-click the button to create its OnClick handler, then fill the grid and chart.
procedure TfrmMain.btnOpenClick(Sender: TObject);
var
Readings: TReadingList;
Reading: TReading;
Row: Integer;
begin
if not dlgOpen.Execute then
Exit;
Readings := LoadReadings(dlgOpen.FileName);
try
grdReadings.RowCount := Readings.Count + 1;
FSeries.Clear;
Row := 1;
for Reading in Readings do
begin
grdReadings.Cells[0, Row] := DateTimeToStr(Reading.Timestamp);
grdReadings.Cells[1, Row] := Reading.Sensor;
grdReadings.Cells[2, Row] := FormatFloat('0.0', Reading.Celsius);
FSeries.AddXY(Reading.Timestamp, Reading.Celsius);
Inc(Row);
end;
finally
Readings.Free;
end;
end;
FSeries is a TLineSeries that I create in the form's OnCreate handler and attach to the chart. Set the chart's bottom axis to use a date-time formatter and you get readable timestamps along the X axis.
procedure TfrmMain.FormCreate(Sender: TObject);
begin
FSeries := TLineSeries.Create(chtTemperature);
FSeries.Title := 'Temperature';
chtTemperature.AddSeries(FSeries);
grdReadings.ColCount := 3;
grdReadings.Cells[0, 0] := 'Time';
grdReadings.Cells[1, 0] := 'Sensor';
grdReadings.Cells[2, 0] := 'Celsius';
end;
Press F9. If everything went well you have a resizable window that loads a file and draws a line. The whole application is one executable with no dependencies, and it started in a fraction of a second.
The same source on Linux
Copy the project folder to a Linux machine with Lazarus installed, open the .lpi file and press F9 again. For this application nothing changes, but a few things will surprise you on larger projects, so I will list what I have hit so far:
- Paths. Use
PathDelimorConcatPathsinstead of hard-coding backslashes, and remember that Linux file names are case sensitive. - Line endings.
TStringListhandles both, but if you write files yourself useLineEndingrather than a literal. - Fonts and sizes. Default fonts differ between Windows, GTK and Qt, so a label that fits on one platform can be clipped on another.
AutoSizeand anchors fix most of this. - Platform-specific code. Wrap it in
{$IFDEF WINDOWS}and{$IFDEF UNIX}blocks, and keep those blocks small and in one unit.
How much of your current desktop codebase would survive a platform change this easily? For most of the C# WinForms applications I have worked on, the honest answer is none of it.
Where to go from here
The natural next steps are saving the user's last folder in a settings file, handling bad lines with a proper error message, and perhaps reading the log live from a serial port or an MQTT broker instead of a file. I will cover some of these in later articles.
Questions for you
- What was the first real application you built with Lazarus or Delphi, and does it still run?
- Have you shipped an LCL application on both Windows and Linux? Which widget set did you choose on Linux, GTK2 or Qt5, and why?
- Would you rather see a follow-up on packaging and installers, or on live data sources?
Write to me at bjorn.nostdahl@nostdahl.com. The next article covers the topic that scares people most when they come from a garbage-collected language: who owns an object, and when does it get freed.