Case Study 2: Hello Lazarus on Three Operating Systems

Overview

One of Lazarus's most compelling features is cross-platform development: write once, compile for Windows, Linux, and macOS. But "cross-platform" does not mean "identical on every platform." This case study walks through building a single application and compiling it on all three operating systems, documenting the differences, the surprises, and the practical workflow for multi-platform Lazarus development.


The Application

We build a simple system information viewer: a form that displays the operating system name, the screen resolution, the current date/time, and a few environment variables. This gives us enough platform-dependent information to see real differences.

unit SysInfoMain;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls;

type

  { TfrmSysInfo }

  TfrmSysInfo = class(TForm)
    lblTitle: TLabel;
    mmoInfo: TMemo;
    btnRefresh: TButton;
    procedure btnRefreshClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    procedure GatherInfo;
  public
  end;

var
  frmSysInfo: TfrmSysInfo;

implementation

{$R *.lfm}

{ TfrmSysInfo }

procedure TfrmSysInfo.FormCreate(Sender: TObject);
begin
  Caption := 'System Information Viewer';
  lblTitle.Caption := 'System Information';
  lblTitle.Font.Size := 14;
  lblTitle.Font.Style := [fsBold];
  mmoInfo.ReadOnly := True;
  mmoInfo.ScrollBars := ssVertical;
  mmoInfo.Font.Name := 'Courier New';
  GatherInfo;
end;

procedure TfrmSysInfo.btnRefreshClick(Sender: TObject);
begin
  GatherInfo;
end;

procedure TfrmSysInfo.GatherInfo;
begin
  mmoInfo.Lines.Clear;
  mmoInfo.Lines.Add('=== Operating System ===');
  {$IFDEF WINDOWS}
  mmoInfo.Lines.Add('Platform: Windows');
  {$ENDIF}
  {$IFDEF LINUX}
  mmoInfo.Lines.Add('Platform: Linux');
  {$ENDIF}
  {$IFDEF DARWIN}
  mmoInfo.Lines.Add('Platform: macOS');
  {$ENDIF}

  mmoInfo.Lines.Add('');
  mmoInfo.Lines.Add('=== Screen ===');
  mmoInfo.Lines.Add(Format('Resolution: %d x %d',
    [Screen.Width, Screen.Height]));
  mmoInfo.Lines.Add(Format('Pixels per inch: %d',
    [Screen.PixelsPerInch]));

  mmoInfo.Lines.Add('');
  mmoInfo.Lines.Add('=== Date and Time ===');
  mmoInfo.Lines.Add('Now: ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now));
  mmoInfo.Lines.Add('UTC: ' + FormatDateTime('yyyy-mm-dd hh:nn:ss',
    LocalTimeToUniversal(Now)));

  mmoInfo.Lines.Add('');
  mmoInfo.Lines.Add('=== Paths ===');
  mmoInfo.Lines.Add('Executable: ' + Application.ExeName);
  mmoInfo.Lines.Add('Temp dir: ' + GetTempDir);
  mmoInfo.Lines.Add('Home dir: ' + GetUserDir);
  mmoInfo.Lines.Add('Path separator: ' + PathDelim);
  mmoInfo.Lines.Add('Directory separator: ' + DirectorySeparator);

  mmoInfo.Lines.Add('');
  mmoInfo.Lines.Add('=== Compiler ===');
  mmoInfo.Lines.Add(Format('FPC version: %d.%d.%d',
    [FPC_VERSION, FPC_RELEASE, FPC_PATCH]));
  mmoInfo.Lines.Add('Target CPU: ' + {$I %FPCTARGETCPU%});
  mmoInfo.Lines.Add('Target OS: ' + {$I %FPCTARGETOS%});

  mmoInfo.Lines.Add('');
  mmoInfo.Lines.Add('=== LCL ===');
  mmoInfo.Lines.Add('Widgetset: ' + LCLPlatformDirNames[WidgetSet.LCLPlatform]);
end;

end.

Note: For the widgetset line, you need to add LCLPlatformDef and InterfaceBase to your uses clause.


Platform Observations

Windows (win32/win64 widgetset)

  • Appearance: The form uses the current Windows theme. Buttons are standard Windows buttons. The memo uses the standard edit control. Font rendering uses GDI/DirectWrite.
  • Executable size: A release build with stripping produces an executable around 3–4 MB.
  • DPI handling: Application.Scaled := True in the .lpr file enables automatic DPI scaling. On a 150% DPI display, controls scale correctly.
  • Path separator: Backslash (\). The PathDelim constant handles this automatically.
  • Temp directory: Typically C:\Users\<name>\AppData\Local\Temp\.

Linux (gtk2 widgetset)

  • Appearance: The form uses the current GTK2 theme. If the user has a dark theme, your application will respect it — buttons, backgrounds, and text colors all follow the theme. This means hardcoded colors (like setting a panel to clWhite) may look wrong on dark themes.
  • Executable size: Slightly larger than Windows (the GTK2 widgetset has more overhead), typically 4–6 MB stripped.
  • Fonts: The default font is different from Windows. If you set Font.Name := 'Courier New' and the user does not have that font, the system substitutes — often DejaVu Sans Mono or similar. Use Font.Name := 'monospace' for a safer generic name.
  • Path separator: Forward slash (/).
  • Dependencies: Unlike the Windows build, the Linux build depends on shared libraries (libgtk, libglib, etc.). These are standard on desktop Linux distributions but may be missing on minimal server installations.

macOS (cocoa widgetset)

  • Appearance: The form uses native Cocoa controls. Buttons have the rounded macOS style. The overall look is distinctly macOS.
  • Application bundle: macOS expects applications to be packaged as .app bundles (a specific directory structure). Lazarus can create this automatically in the project options.
  • Retina displays: macOS Retina displays have 2x pixel density. The LCL handles this, but custom drawing code (Chapter 30) needs to account for the scaling factor.
  • Menu bar: macOS places the menu bar at the top of the screen, not on the window. If your form has a TMainMenu, the LCL moves it to the system menu bar automatically.
  • Path separator: Forward slash (/), same as Linux.

Cross-Platform Best Practices

Use Constants, Not Hardcoded Paths

{ Bad }
ConfigPath := 'C:\Users\' + UserName + '\AppData\config.ini';

{ Good }
ConfigPath := GetAppConfigDir(False) + 'config.ini';

The GetAppConfigDir, GetUserDir, GetTempDir, and Application.Location functions return platform-appropriate paths.

Use PathDelim and DirectorySeparator

FullPath := BaseDir + PathDelim + FileName;
{ Or use ConcatPaths: }
FullPath := ConcatPaths([BaseDir, SubDir, FileName]);

Do Not Hardcode Font Names

{ Bad — Courier New might not exist on Linux }
Memo1.Font.Name := 'Courier New';

{ Better — use a generic family }
Memo1.Font.Name := 'monospace';

{ Best — use platform conditionals }
{$IFDEF WINDOWS}
Memo1.Font.Name := 'Consolas';
{$ELSE}
Memo1.Font.Name := 'monospace';
{$ENDIF}

Use Anchors, Not Absolute Positions

Different platforms have different default font sizes, button heights, and spacing. If you position controls with absolute pixel coordinates, your layout may break on another platform. Use anchor properties instead:

  • Set Anchors to [akTop, akLeft, akRight] for a control that stretches horizontally.
  • Set Anchors to [akLeft, akTop, akBottom] for a control that stretches vertically.
  • Use BorderSpacing for margins between controls and their anchor points.

Test on All Target Platforms

There is no substitute for testing. Set up virtual machines (Linux VM on Windows, etc.) or use cross-compilation (Chapter 32) to build for all targets, then test the resulting executables on real installations.


Conditional Compilation

Free Pascal provides platform-detection defines that you can use for platform-specific code:

{$IFDEF WINDOWS}
  { Windows-specific code }
{$ENDIF}

{$IFDEF LINUX}
  { Linux-specific code }
{$ENDIF}

{$IFDEF DARWIN}
  { macOS-specific code }
{$ENDIF}

{$IFDEF UNIX}
  { Linux OR macOS (any Unix-like system) }
{$ENDIF}

Use these sparingly. Most of your code should be platform-independent. Conditional compilation is for the exceptions: platform-specific paths, platform-specific library calls, or platform-specific UI adjustments.


Executable Size Comparison

Platform Debug Build Release Build (Stripped)
Windows x64 ~18 MB ~3.5 MB
Linux x64 (gtk2) ~22 MB ~4.8 MB
macOS arm64 (cocoa) ~20 MB ~4.2 MB

Compare this to an Electron "Hello World" at 150+ MB. Or a Java application requiring a 200+ MB JRE installation. Lazarus executables are remarkably lean.


Lessons Learned

  1. Cross-platform means compatible, not identical. Expect visual differences between platforms and design for flexibility.
  2. Use LCL-provided functions for paths, fonts, and platform detection rather than hardcoding platform-specific values.
  3. Conditional compilation ({$IFDEF}) handles the rare cases where platform-specific code is unavoidable.
  4. Anchors and auto-sizing are essential for layouts that work across platforms with different default sizes.
  5. Test on all target platforms. The LCL handles most differences, but platform-specific bugs are real and can only be caught by testing.
  6. Native executables are small and fast on every platform — this is a genuine advantage of the Free Pascal/Lazarus stack.