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:
- A display area (TEdit, read-only) showing the current number or result.
- Number buttons (0–9) and a decimal point button.
- Operator buttons (+, −, ×, ÷).
- An equals button (=) that computes the result.
- A clear button (C) that resets the calculator.
- 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
- Keyboard support: Handle the form's
OnKeyPressevent to allow typing digits and operators from the keyboard. - Memory functions: Add M+, M−, MR (memory recall), and MC (memory clear) buttons.
- History display: Add a TMemo below the calculator that shows previous calculations.
- Percentage button: Add a % button that divides the current display by 100.
- Backspace: Add a ← button that removes the last digit from the display.
Lessons Learned
- Shared event handlers reduce code duplication dramatically. Ten buttons, one handler.
- The
Tagproperty is a convenient way to attach an integer identifier to any component. - State machines are a natural fit for event-driven applications. Track state with private fields and transition between states in event handlers.
- Business logic (the
Calculatefunction) should be independent of the UI, even in a small application. - The Sender parameter is the key to making shared handlers work — it tells you which component triggered the event.