Case Study 1: Building a Simple Calculator

Overview

The calculator is the "Hello, World" of GUI programming. In this case study, we build a four-function calculator that demonstrates form layout, button event handling, state management, and input validation — all concepts introduced in Chapter 27. Along the way, we encounter the classic pitfalls of event-driven design: shared event handlers, state machines, and the question of where to put the logic.


Problem Statement

Build a calculator application with the following features:

  1. A display area (TEdit, read-only) showing the current number or result.
  2. Number buttons (0–9) and a decimal point button.
  3. Operator buttons (+, −, ×, ÷).
  4. An equals button (=) that computes the result.
  5. A clear button (C) that resets the calculator.
  6. Error handling for division by zero.

Design Decisions

State Management

A calculator is a simple state machine. At any moment, it is in one of three states:

  • Entering first operand: The user is typing the first number.
  • Entering second operand: The user has pressed an operator and is typing the second number.
  • Showing result: The user has pressed = and the result is displayed.

We track this with a few variables:

private
  FFirstOperand: Double;
  FOperator: Char;
  FNewEntry: Boolean;   { True if the next digit starts a new number }

Shared Event Handlers

We have 10 digit buttons plus a decimal point — 11 buttons that all do essentially the same thing: append a character to the display. Instead of writing 11 separate event handlers, we use a single shared handler and the Sender parameter to determine which button was clicked.

Similarly, the four operator buttons share a handler.


Implementation

Form Layout

The form uses a grid-like arrangement:

[ Display (TEdit, read-only)              ]
[ 7 ] [ 8 ] [ 9 ] [ ÷ ]
[ 4 ] [ 5 ] [ 6 ] [ × ]
[ 1 ] [ 2 ] [ 3 ] [ − ]
[ 0 ] [ . ] [ = ] [ + ]
[         C (Clear)                       ]

Each button is a TButton with its Caption set to the digit or operator it represents. The Tag property is set to the digit value for number buttons (0–9), which provides a cleaner way to identify digits than parsing the caption.

The Unit

unit CalcMain;

{$mode objfpc}{$H+}

interface

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

type

  { TfrmCalculator }

  TfrmCalculator = class(TForm)
    btnDigit0: TButton;
    btnDigit1: TButton;
    btnDigit2: TButton;
    btnDigit3: TButton;
    btnDigit4: TButton;
    btnDigit5: TButton;
    btnDigit6: TButton;
    btnDigit7: TButton;
    btnDigit8: TButton;
    btnDigit9: TButton;
    btnDecimal: TButton;
    btnAdd: TButton;
    btnSubtract: TButton;
    btnMultiply: TButton;
    btnDivide: TButton;
    btnEquals: TButton;
    btnClear: TButton;
    edtDisplay: TEdit;
    procedure DigitClick(Sender: TObject);
    procedure OperatorClick(Sender: TObject);
    procedure btnEqualsClick(Sender: TObject);
    procedure btnClearClick(Sender: TObject);
    procedure btnDecimalClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    FFirstOperand: Double;
    FOperator: Char;
    FNewEntry: Boolean;
    function Calculate(A, B: Double; Op: Char): Double;
  public
  end;

var
  frmCalculator: TfrmCalculator;

implementation

{$R *.lfm}

{ TfrmCalculator }

procedure TfrmCalculator.FormCreate(Sender: TObject);
begin
  FFirstOperand := 0;
  FOperator := #0;
  FNewEntry := True;
  edtDisplay.Text := '0';
  edtDisplay.ReadOnly := True;
  edtDisplay.Alignment := taRightJustify;
  edtDisplay.Font.Size := 18;
end;

procedure TfrmCalculator.DigitClick(Sender: TObject);
var
  Digit: string;
begin
  Digit := IntToStr((Sender as TButton).Tag);

  if FNewEntry then
  begin
    edtDisplay.Text := Digit;
    FNewEntry := False;
  end
  else
  begin
    if edtDisplay.Text = '0' then
      edtDisplay.Text := Digit
    else
      edtDisplay.Text := edtDisplay.Text + Digit;
  end;
end;

procedure TfrmCalculator.btnDecimalClick(Sender: TObject);
begin
  if FNewEntry then
  begin
    edtDisplay.Text := '0.';
    FNewEntry := False;
  end
  else if Pos('.', edtDisplay.Text) = 0 then
    edtDisplay.Text := edtDisplay.Text + '.';
  { If there is already a decimal point, do nothing }
end;

procedure TfrmCalculator.OperatorClick(Sender: TObject);
begin
  { If there is a pending operation, calculate it first }
  if (FOperator <> #0) and (not FNewEntry) then
    btnEqualsClick(Sender);

  FFirstOperand := StrToFloatDef(edtDisplay.Text, 0);
  FOperator := (Sender as TButton).Caption[1];
  FNewEntry := True;
end;

procedure TfrmCalculator.btnEqualsClick(Sender: TObject);
var
  SecondOperand, Result: Double;
begin
  if FOperator = #0 then
    Exit;  { No pending operation }

  SecondOperand := StrToFloatDef(edtDisplay.Text, 0);

  try
    Result := Calculate(FFirstOperand, SecondOperand, FOperator);
    edtDisplay.Text := FloatToStr(Result);
  except
    on E: Exception do
    begin
      edtDisplay.Text := 'Error';
      MessageDlg('Calculation Error', E.Message, mtError, [mbOK], 0);
    end;
  end;

  FOperator := #0;
  FNewEntry := True;
end;

procedure TfrmCalculator.btnClearClick(Sender: TObject);
begin
  FFirstOperand := 0;
  FOperator := #0;
  FNewEntry := True;
  edtDisplay.Text := '0';
end;

function TfrmCalculator.Calculate(A, B: Double; Op: Char): Double;
begin
  case Op of
    '+': Result := A + B;
    '-': Result := A - B;
    '*', '×': Result := A * B;
    '/', '÷':
    begin
      if B = 0 then
        raise Exception.Create('Division by zero');
      Result := A / B;
    end;
  else
    Result := 0;
  end;
end;

end.

Key Design Observations

Shared Handler with Tag Property

The DigitClick handler serves all 10 digit buttons. Each button's Tag property is set to its digit value (0–9) at design time in the Object Inspector. The handler uses (Sender as TButton).Tag to determine which digit was pressed. This is far cleaner than checking (Sender as TButton).Caption and parsing it.

State Machine Logic

The FNewEntry flag acts as a state toggle. When True, the next digit keystroke starts a new number (replacing the display). When False, digits are appended to the current display. This flag is set to True after an operator is pressed or a result is shown, and to False after the first digit of a new entry.

Separation of Concerns

The Calculate function is a pure function — it takes two numbers and an operator, and returns a result. It has no dependency on the UI. You could call it from a console program, a unit test, or a web API. The event handlers are thin: they extract data from the UI, call Calculate, and display the result.

Error Handling

Division by zero raises an exception inside Calculate, which is caught in btnEqualsClick. The display shows "Error" and a message dialog explains the problem. This demonstrates that exception handling from Chapter 19 works identically in GUI code.


Extensions to Try

  1. Keyboard support: Handle the form's OnKeyPress event to allow typing digits and operators from the keyboard.
  2. Memory functions: Add M+, M−, MR (memory recall), and MC (memory clear) buttons.
  3. History display: Add a TMemo below the calculator that shows previous calculations.
  4. Percentage button: Add a % button that divides the current display by 100.
  5. Backspace: Add a ← button that removes the last digit from the display.

Lessons Learned

  1. Shared event handlers reduce code duplication dramatically. Ten buttons, one handler.
  2. The Tag property is a convenient way to attach an integer identifier to any component.
  3. State machines are a natural fit for event-driven applications. Track state with private fields and transition between states in event handlers.
  4. Business logic (the Calculate function) should be independent of the UI, even in a small application.
  5. The Sender parameter is the key to making shared handlers work — it tells you which component triggered the event.