Skip to content

nostdahl.com

A technology journal
Article archive
← All articles
Programming

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 TPanel aligned to the top with a TButton named btnOpen.
  • A TStringGrid named grdReadings aligned to the left.
  • A TChart named chtTemperature aligned to the client area. The chart comes from the TAChart package which ships with Lazarus.
  • A TOpenDialog named dlgOpen.

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.

pascal
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.

pascal
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.

pascal
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 PathDelim or ConcatPaths instead of hard-coding backslashes, and remember that Linux file names are case sensitive.
  • Line endings. TStringList handles both, but if you write files yourself use LineEnding rather 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. AutoSize and 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.

Continue exploring
← All articlesMore in Programming →