A from-scratch, syllabus-mapped walkthrough of Delphi / Object Pascal — from your very first Hello World to full GUI programs with real components.
Every Object Pascal console program starts with a `program` header and ends with `end.` — note the full stop, not a semicolon. The code between `begin` and `end.` is what actually runs.
Save this as Main.pas, then compile it with `fpc -Mdelphi Main.pas` and run the executable it produces.
program Main;
{$MODE DELPHI}
begin
WriteLn('Hello, World!');
end.All variables are declared up front in a `var` block, before `begin`. You cannot declare one halfway through your code the way you can in Java.
The types you'll use constantly: Integer (whole numbers), Real (decimals), String (text), Char (one character) and Boolean (True/False).
program Main;
{$MODE DELPHI}
var
age: Integer;
price: Real;
name: String;
grade: Char;
passed: Boolean;
begin
age := 17;
price := 149.99;
name := 'Thandi';
grade := 'A';
passed := True;
WriteLn(name, ' is ', age, ' years old.');
end.ReadLn reads a line of input and converts it to match the variable's type — no Scanner object required.
You can read several values at once by listing them, in which case the user separates them with spaces.
program Main;
{$MODE DELPHI}
var
name: String;
a, b: Integer;
begin
WriteLn('What is your name?');
ReadLn(name);
WriteLn('Enter two numbers:');
ReadLn(a, b);
WriteLn('Hi ', name, ', the sum is ', a + b);
end.Arithmetic is +, -, * and /. Integer division is `div` and the remainder is `mod` — / always gives a Real, even for two Integers.
Comparison uses =, <>, <, >, <= and >=. Logical operators are the words `and`, `or` and `not`.
WriteLn formats numbers with a colon: `value:width:decimals`.
var
total, count: Integer;
average: Real;
begin
total := 47;
count := 5;
WriteLn(total div count); // 9 — whole part only
WriteLn(total mod count); // 2 — remainder
average := total / count;
WriteLn(average:0:2); // 9.40 — two decimals
end.The rule that catches everyone: there is no semicolon before `else`. A semicolon ends the statement, so putting one there breaks the if.
When more than one statement belongs to a branch, wrap it in begin…end.
`case` handles many discrete values cleanly, and works on Integer and Char (not String) — and can match ranges like `0..4`, not just single values.
if mark >= 80 then
WriteLn('Distinction')
else if mark >= 50 then
WriteLn('Pass')
else
WriteLn('Fail');
case grade of
'A': WriteLn('Excellent');
'B', 'C': WriteLn('Good');
else
WriteLn('Keep working');
end;
case age of
0..12: WriteLn('Child');
13..19: WriteLn('Teenager');
else
WriteLn('Adult');
end;
if (mark >= 0) and (mark <= 100) then
WriteLn('Valid mark');`for` counts a known number of times — use `to` to count up, `downto` to count down. You cannot step by 2; use a while loop for that.
`while` tests before each pass, so it may run zero times. `repeat…until` tests after, so it always runs at least once, and it stops when the condition becomes True — the opposite of while.
for i := 1 to 10 do
WriteLn(i);
for i := 10 downto 1 do
Write(i, ' ');
WriteLn;
i := 1;
while i <= 100 do
begin
total := total + i;
i := i + 1;
end;
repeat
WriteLn('Enter a positive number:');
ReadLn(n);
until n > 0;A static array declares its own index range — `array[1..30]` starts at 1, unlike Java where arrays always start at 0. CAPS questions usually index from 1.
Dynamic arrays use `array of` and are sized at run time with SetLength; those always start at index 0, and High() gives the last valid index.
var
marks: array[1..5] of Integer;
names: array of String;
i, total: Integer;
begin
marks[1] := 65;
marks[2] := 72;
total := 0;
for i := 1 to 5 do
total := total + marks[i];
WriteLn('Average: ', (total / 5):0:2);
SetLength(names, 3); // indices 0, 1, 2
names[0] := 'Sipho';
for i := 0 to High(names) do
WriteLn(names[i]);
end.A 2D array is declared with two index ranges — `array[1..rows, 1..cols]` — and models a grid: a seating plan, a mark sheet, a game board. Think of the first index as the row and the second as the column.
Every 2D array walk is a loop nested inside a loop. The outer loop picks a row; the inner loop walks every column in that row. Swap which loop is outer to walk column-by-column instead of row-by-row — that's the whole trick behind row totals vs column totals.
There's no single ready-made function for summing a row or column — you build it from the nested loop, accumulating into a running total exactly like a 1D array sum.
const
ROWS = 3;
COLS = 4;
var
grid: array[1..ROWS, 1..COLS] of Integer;
row, col, rowTotal, colTotal: Integer;
begin
// fill with random values 1..9
Randomize;
for row := 1 to ROWS do
for col := 1 to COLS do
grid[row, col] := Random(9) + 1;
// row-by-row totals
for row := 1 to ROWS do
begin
rowTotal := 0;
for col := 1 to COLS do
rowTotal := rowTotal + grid[row, col];
WriteLn('Row ', row, ' total: ', rowTotal);
end;
// column-by-column totals — loop order flipped
for col := 1 to COLS do
begin
colTotal := 0;
for row := 1 to ROWS do
colTotal := colTotal + grid[row, col];
WriteLn('Column ', col, ' total: ', colTotal);
end;
end.Strings are indexed from 1, and Length gives the character count. Join them with +.
The routines you'll use most: Copy (substring), Pos (find), UpperCase / LowerCase, Trim, IntToStr / StrToInt and FloatToStr / StrToFloat. Most live in the SysUtils unit, so add `uses SysUtils;`.
program Main;
{$MODE DELPHI}
uses
SysUtils;
var
s: String;
i: Integer;
begin
s := 'Information Technology';
WriteLn(Length(s)); // 22
WriteLn(s[1]); // I — first character, index 1
WriteLn(Copy(s, 1, 11)); // Information
WriteLn(Pos('Tech', s)); // 13
WriteLn(UpperCase(s));
for i := 1 to Length(s) do
if s[i] = 'o' then
WriteLn('Found an o at position ', i);
WriteLn('Total: ' + IntToStr(42));
end.A procedure does something; a function returns a value. Both are declared above the main begin block.
In a function you return by assigning to the special variable `Result`.
Parameters are passed by value by default. Prefix one with `var` to pass by reference, so changes flow back to the caller.
program Main;
{$MODE DELPHI}
procedure Greet(name: String);
begin
WriteLn('Hello, ', name, '!');
end;
function Area(width, height: Real): Real;
begin
Result := width * height;
end;
procedure Swap(var a, b: Integer);
var
temp: Integer;
begin
temp := a;
a := b;
b := temp;
end;
var
x, y: Integer;
begin
Greet('Lerato');
WriteLn(Area(3, 4):0:2);
x := 1; y := 2;
Swap(x, y);
WriteLn(x, ' ', y); // 2 1
end.A record groups related fields under one name — the Pascal equivalent of a simple data-holding class, and very common in CAPS papers.
Combine a record with an array to model a table of data.
type
TLearner = record
Name: String;
Grade: Integer;
Average: Real;
end;
var
learners: array[1..3] of TLearner;
i: Integer;
begin
learners[1].Name := 'Ayanda';
learners[1].Grade := 12;
learners[1].Average := 78.5;
for i := 1 to 3 do
if learners[i].Average >= 75 then
WriteLn(learners[i].Name, ' earned a distinction');
end.A class bundles data (fields) with the code that operates on it (methods). Declare the class in a `type` block, then write the method bodies below it.
Objects are references, so you must Create them — and, unlike Java, Free them when you're done, because Pascal has no garbage collector.
The constructor is literally named `constructor`, and by convention called Create.
type
TCar = class
private
FMake: String;
FSpeed: Integer;
public
constructor Create(AMake: String);
procedure Accelerate(amount: Integer);
function Describe: String;
end;
constructor TCar.Create(AMake: String);
begin
FMake := AMake;
FSpeed := 0;
end;
procedure TCar.Accelerate(amount: Integer);
begin
FSpeed := FSpeed + amount;
end;
function TCar.Describe: String;
begin
Result := FMake + ' doing ' + IntToStr(FSpeed) + ' km/h';
end;
var
car: TCar;
begin
car := TCar.Create('Toyota');
try
car.Accelerate(60);
WriteLn(car.Describe);
finally
car.Free;
end;
end.`private` members are invisible outside the class; `public` ones form its interface. Keeping fields private is encapsulation.
Instead of Java-style getX/setX pairs, Delphi has properties: they look like fields to the caller but route through methods, letting you validate on write.
type
TLearner = class
private
FName: String;
FMark: Integer;
procedure SetMark(value: Integer);
public
property Name: String read FName write FName;
property Mark: Integer read FMark write SetMark;
end;
procedure TLearner.SetMark(value: Integer);
begin
// Reject impossible marks instead of silently storing them.
if (value >= 0) and (value <= 100) then
FMark := value
else
FMark := 0;
end;
var
l: TLearner;
begin
l := TLearner.Create;
l.Name := 'Kabelo';
l.Mark := 150; // rejected by SetMark
WriteLn(l.Name, ': ', l.Mark); // Kabelo: 0
l.Free;
end.A child class is declared with `class(TParent)` and inherits everything the parent has.
Call the parent's version of a method with the `inherited` keyword — most commonly in the constructor, so the parent can set itself up first.
type
TVehicle = class
protected
FWheels: Integer;
public
constructor Create(AWheels: Integer);
function Describe: String; virtual;
end;
TMotorbike = class(TVehicle)
public
constructor Create;
function Describe: String; override;
end;
constructor TVehicle.Create(AWheels: Integer);
begin
FWheels := AWheels;
end;
function TVehicle.Describe: String;
begin
Result := 'A vehicle with ' + IntToStr(FWheels) + ' wheels';
end;
constructor TMotorbike.Create;
begin
inherited Create(2);
end;
function TMotorbike.Describe: String;
begin
Result := 'Motorbike — ' + inherited Describe;
end;Mark a method `virtual` in the parent and `override` in the child, and Delphi picks the right version at run time based on what the object actually is.
That means you can hold different subclasses in one array of the parent type and call the same method on each.
type
TShape = class
public
function Area: Real; virtual; abstract;
function Name: String; virtual; abstract;
end;
TCircle = class(TShape)
private
FRadius: Real;
public
constructor Create(r: Real);
function Area: Real; override;
function Name: String; override;
end;
// … TRectangle declared the same way …
var
shapes: array[1..2] of TShape;
i: Integer;
begin
shapes[1] := TCircle.Create(3);
shapes[2] := TRectangle.Create(4, 5);
for i := 1 to 2 do
WriteLn(shapes[i].Name, ': ', shapes[i].Area:0:2);
for i := 1 to 2 do
shapes[i].Free;
end.A unit is a separate .pas file holding reusable code. Everything under `interface` is visible to other files; everything under `implementation` is private to the unit.
The unit name must match the filename exactly — unit MathUtils lives in MathUtils.pas. Bring it in with `uses MathUtils;`.
In the Coding Arena, extra files you add are compiled as units alongside Main.pas automatically.
// --- MathUtils.pas ---
unit MathUtils;
{$MODE DELPHI}
interface
function Triple(n: Integer): Integer;
implementation
function Triple(n: Integer): Integer;
begin
Result := n * 3;
end;
end.
// --- Main.pas ---
program Main;
{$MODE DELPHI}
uses
MathUtils;
begin
WriteLn(Triple(7)); // 21
end.try…except recovers from runtime errors like bad input, instead of crashing.
try…finally is different: it doesn't catch anything, it just guarantees the cleanup runs. That's how you make sure objects get Freed.
uses
SysUtils;
var
n: Integer;
begin
try
Write('Enter a number: ');
ReadLn(n);
WriteLn(100 div n);
except
on E: EDivByZero do
WriteLn('Can''t divide by zero!');
on E: Exception do
WriteLn('Something went wrong: ', E.Message);
end;
end.A TextFile variable is your handle onto a file on disk. AssignFile links it to a filename; Reset opens it for reading, Rewrite creates it empty for writing, and Append opens it for adding to the end — you always CloseFile when you're done.
The standard pattern for reading is a while loop driven by Eof (end of file): keep calling ReadLn until there's nothing left.
In the Coding Arena, a problem that needs a supplied data file ships it alongside your program — you AssignFile to the exact filename shown on the problem page and it's already sitting there when your code runs.
program Main;
{$MODE DELPHI}
var
f: TextFile;
line: String;
total: Integer;
begin
total := 0;
AssignFile(f, 'STOCK.TXT');
Reset(f);
while not Eof(f) do
begin
ReadLn(f, line);
WriteLn('Read: ', line);
total := total + 1;
end;
CloseFile(f);
WriteLn('Lines read: ', total);
// writing is the mirror image
AssignFile(f, 'RESULTS.TXT');
Rewrite(f);
WriteLn(f, 'Total lines: ', total);
CloseFile(f);
end.Everything so far has been a console program — text in, text out. A form application is the other half of CAPS: a real window with buttons, edit boxes and labels that a user clicks and types into.
Open the Form Designer, drag components from the palette onto the canvas, and give each one a name that starts with a short prefix for its type — btn for TButton, edt for TEdit, lbl for TLabel, mem for TMemo, pnl for TPanel. This is a strong CAPS convention: memos expect btnCalculate, not Button1.
Double-click a component in the Designer to jump straight to its default event handler (a button's OnClick) — that's where you write what happens when the user interacts with it.
Every component you see in the palette lives in the Delphi Bible's shim: TLabel, TEdit, TMemo, TButton, TCheckBox, TRadioButton, TComboBox, TListBox, TGroupBox, TPanel, plus the whole Additional/Data/Non-visual groups (TStringGrid, TDBGrid, TTimer and friends).
// Dropped from the palette: LabelGreeting (TLabel), EditName (TEdit),
// ButtonHello (TButton) — this is the .pas side; the Form Designer writes the
// matching .dfm for you.
type
TForm1 = class(TForm)
LabelGreeting: TLabel;
EditName: TEdit;
ButtonHello: TButton;
procedure ButtonHelloClick(Sender: TObject);
end;
var
Form1: TForm1;
implementation
procedure TForm1.ButtonHelloClick(Sender: TObject);
begin
LabelGreeting.Caption := 'Hello, ' + EditName.Text + '!';
end;
end.Every property you'd normally set once in the Object Inspector at design time can also be changed while the program is running — that's most of what an event handler actually does.
Font is its own object hanging off every component, with Size, Name, Style and Color of its own. Style is a *set*, so you add fsBold/fsItalic/fsUnderline/fsStrikeOut to it rather than assigning a single value — write `Font.Style := Font.Style + [fsBold]` to add bold without disturbing an existing underline.
Color is a separate property from Font.Color — Color paints the component's own background, Font.Color paints its text.
procedure TForm1.btnFormatClick(Sender: TObject);
begin
// Text and appearance, all set from code
edtOutput.Text := 'Hello world';
edtOutput.Font.Size := 14;
edtOutput.Font.Style := edtOutput.Font.Style + [fsUnderline];
edtOutput.Color := clGreen;
end;
procedure TForm1.btnResetClick(Sender: TObject);
begin
// Style is a set — subtract from it to remove just one style,
// rather than wiping out bold/italic that might already be on.
edtOutput.Font.Style := edtOutput.Font.Style - [fsUnderline];
edtOutput.Color := clWhite;
edtOutput.Enabled := not edtOutput.Enabled; // toggle, doesn't need an if
end;A GUI program has no console, so the CAPS convention for multi-line output is a TMemo (often named redOutput or memOutput) with results appended via its Lines property — Lines.Add(text) adds one line, Lines.Clear wipes it, and Lines.Text reads the whole thing back as one String.
This is the direct GUI equivalent of the console programs' WriteLn — everywhere the console guide above says WriteLn(x), a form program says redOutput.Lines.Add(x) instead. Nested loops over 1D and 2D arrays work exactly the same way; only where the output goes changes.
procedure TForm1.btnShowGridClick(Sender: TObject);
const
ROWS = 3;
COLS = 4;
var
grid: array[1..ROWS, 1..COLS] of Integer;
row, col: Integer;
rowText: String;
begin
Randomize;
redOutput.Lines.Clear;
for row := 1 to ROWS do
for col := 1 to COLS do
grid[row, col] := Random(9) + 1;
for row := 1 to ROWS do
begin
rowText := '';
for col := 1 to COLS do
rowText := rowText + IntToStr(grid[row, col]) + ' ';
redOutput.Lines.Add(rowText); // one line per row, not per cell
end;
end;Real Delphi connects to a Microsoft Access database (a .mdb file) through three components: TADOConnection (the link to the file), TADOTable (one open table) and TDataSource (the bridge that lets visual controls like a DBGrid display it). CodeIEB's palette includes all three so a form compiles and its layout matches what you'll build in class — but this sandbox has no real Jet/ACE OLEDB provider to actually open a .mdb file, so this section is reference material for your school's real Delphi install, not something "Run in Browser" can execute.
The Object Inspector workflow: drop TADOConnection, set its ConnectionString (Jet 4.0 OLE DB Provider, point it at the .mdb), set LoginPrompt to False. Drop TADOTable, set its Connection to the ADOConnection, its TableName to a real table, Active to True. Drop TDataSource, set its DataSet to the ADOTable. Then a DBGrid's DataSource points at the TDataSource — that one chain is what puts real rows on screen with no code at all.
Once connected, almost everything routes through the ADOTable: First/Last/Next/Prior move a record pointer, Eof tells you when you've walked off the end, and table['FieldName'] reads a field's value at the current position.
// Traversing every record — the pattern behind every database question
tblLearners.First;
while not tblLearners.Eof do
begin
if tblLearners['Grade'] = 12 then
redOutput.Lines.Add(tblLearners['Surname']);
tblLearners.Next;
end;
// Inserting a new record
tblLearners.Insert;
tblLearners['Surname'] := 'Dlamini';
tblLearners['Grade'] := 11;
tblLearners.Post;
// Editing the current record
tblLearners.Edit;
tblLearners['Grade'] := tblLearners['Grade'] + 1;
tblLearners.Post;
// Deleting matching records — Delete auto-advances, so only call Next
// when you did NOT just delete, or you skip the record right after it
tblLearners.First;
while not tblLearners.Eof do
begin
if tblLearners['Grade'] > 12 then
tblLearners.Delete
else
tblLearners.Next;
end;
// Sorting — field name, then ASC or DESC, comma-separated for multiple keys
tblLearners.Sort := 'Surname ASC, Grade DESC';Ready to put it into practice?
Every technique above shows up in the Coding Arena problem set.
Try the Coding Arena →