Build a Calculator App in Visual C#

A complete Windows Forms desktop app — drag the buttons, wire the events, and handle the state machine that every calculator tutorial gets subtly wrong.

C# Beginner 18 min read Watch the video (9:01)

Most GUI tutorials give you a text box, a button, and a MessageBox. That teaches you almost nothing, because the hard part of a GUI app is not the buttons — it is the state.

A calculator is the perfect size for learning it properly. Everyone knows how one should behave, and it turns out that behaviour is trickier than it looks.

#Setup

Install Visual Studio Community (free) and during installation tick the .NET desktop development workload. Without it, there is no Windows Forms template.

Then: Create a new project → Windows Forms App (.NET) → C#. Name it Calculator.

You get a blank form and a designer. The Toolbox panel (View → Toolbox) has your controls; Properties (F4) configures the selected one.

#Design the form

Set these on the form itself:

PropertyValue
TextCalculator
Size320, 460
FormBorderStyleFixedSingle
MaximizeBoxFalse
StartPositionCenterScreen

Fixing the border matters here — a resizable window whose buttons do not move looks broken.

Now drop in a TextBox across the top for the display:

PropertyValue
(Name)txtDisplay
Text0
TextAlignRight
FontSegoe UI, 24pt
ReadOnlyTrue
BackColorWhite

ReadOnly is deliberate. Let people type directly into the display and you inherit every possible malformed input — 12..3, --5, abc. Buttons only means every value that reaches your parser was built by your own code.

Then add buttons for 09, ., +, , ×, ÷, =, C and .

#Wire the digits — one handler for all ten

The naive approach is ten separate handlers with nearly identical bodies. Do this instead:

Double-click btn7 to generate a handler, rename it to DigitButton_Click, then in the Properties panel of every digit button, go to the Events tab (the lightning bolt), find Click, and pick DigitButton_Click from the dropdown.

Ten buttons, one handler:

csharp
private void DigitButton_Click(object sender, EventArgs e)
{
    Button button = (Button)sender;
    string digit = button.Text;

    if (isNewEntry || txtDisplay.Text == "0")
    {
        txtDisplay.Text = digit;
        isNewEntry = false;
    }
    else
    {
        txtDisplay.Text += digit;
    }
}

sender is the control that raised the event. Casting it back to Button gives you access to its Text, so the handler knows which digit was pressed without you writing a switch.

#The state machine

Here is where calculators get interesting. Press 5, +, 3, =. The app must remember:

  • The first number (5) — because the display now shows 3.
  • The pending operation (+).
  • Whether the next digit starts a new number or appends to the current one.

That last flag is the one every buggy calculator gets wrong. After you press +, the display still reads 5. Press 3 and you want 3, not 53. But press 3 after pressing 5 with no operator between, and you do want 53.

csharp
public partial class Form1 : Form
{
    private double accumulator = 0;      // the value carried forward
    private string pendingOp = "";       // the operation waiting for a right operand
    private bool isNewEntry = true;      // should the next digit replace the display?

    public Form1()
    {
        InitializeComponent();
    }
}

Three fields. That is the entire model.

#The operator handler

Assign this to +, , × and ÷ the same way — one handler, all four:

csharp
private void OperatorButton_Click(object sender, EventArgs e)
{
    string op = ((Button)sender).Text;

    // Chained operations: 5 + 3 + 2 should show 8 when the second + is pressed
    if (pendingOp != "" && !isNewEntry)
    {
        Calculate();
    }
    else
    {
        accumulator = double.Parse(txtDisplay.Text);
    }

    pendingOp = op;
    isNewEntry = true;
    lblOperation.Text = $"{accumulator} {op}";
}

The chaining check is what makes 5 + 3 + 2 = produce 10. Without it, the second + would simply overwrite the pending operation and you would get 7 — a bug present in a genuinely large number of calculator tutorials.

!isNewEntry guards the case where someone presses two operators in a row. 5 + × 3 should just change the operator, not calculate anything.

#The maths

csharp
private void Calculate()
{
    double current = double.Parse(txtDisplay.Text);
    double result;

    switch (pendingOp)
    {
        case "+":
            result = accumulator + current;
            break;
        case "−":
            result = accumulator - current;
            break;
        case "×":
            result = accumulator * current;
            break;
        case "÷":
            if (current == 0)
            {
                txtDisplay.Text = "Cannot divide by zero";
                ResetState();
                return;
            }
            result = accumulator / current;
            break;
        default:
            return;
    }

    accumulator = result;
    txtDisplay.Text = result.ToString("G15");
}

private void btnEquals_Click(object sender, EventArgs e)
{
    if (pendingOp == "") return;

    Calculate();
    pendingOp = "";
    isNewEntry = true;
    lblOperation.Text = "";
}

Two details worth understanding.

The divide-by-zero check. In C#, integer division by zero throws DivideByZeroException, but floating point division by zero returns Infinity — no exception at all. Your display would read . Since we are using double, we must check explicitly.

ToString("G15"). This is the one that surprises everyone:

csharp
double a = 0.1 + 0.2;
Console.WriteLine(a);              // 0.30000000000000004
Console.WriteLine(a == 0.3);       // False

That is not a C# bug. Binary floating point cannot represent 0.1 exactly, the same way decimal cannot represent one third exactly. Formatting with G15 — fifteen significant digits — rounds off the error and displays 0.3.

#Clear, backspace and the decimal point

csharp
private void btnClear_Click(object sender, EventArgs e)
{
    txtDisplay.Text = "0";
    ResetState();
}

private void ResetState()
{
    accumulator = 0;
    pendingOp = "";
    isNewEntry = true;
    lblOperation.Text = "";
}

private void btnBackspace_Click(object sender, EventArgs e)
{
    if (isNewEntry) return;

    if (txtDisplay.Text.Length > 1)
    {
        txtDisplay.Text = txtDisplay.Text.Substring(0, txtDisplay.Text.Length - 1);
    }
    else
    {
        txtDisplay.Text = "0";
        isNewEntry = true;
    }
}

private void btnDecimal_Click(object sender, EventArgs e)
{
    if (isNewEntry)
    {
        txtDisplay.Text = "0.";
        isNewEntry = false;
    }
    else if (!txtDisplay.Text.Contains("."))
    {
        txtDisplay.Text += ".";
    }
}

The decimal handler is three lines of pure edge case, and it is the difference between a calculator and a toy. Contains(".") blocks 3..14. The isNewEntry branch turns a bare . into 0. rather than leaving an unparseable string in the display.

#Keyboard support

A calculator you cannot type into is annoying. Set the form's KeyPreview property to True — this lets the form see keystrokes before the focused control does — then:

csharp
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
    switch (e.KeyCode)
    {
        case Keys.D0: case Keys.NumPad0: btn0.PerformClick(); break;
        case Keys.D1: case Keys.NumPad1: btn1.PerformClick(); break;
        // ... and so on for 2–9
        case Keys.Add:      case Keys.Oemplus:  btnPlus.PerformClick();  break;
        case Keys.Subtract: case Keys.OemMinus: btnMinus.PerformClick(); break;
        case Keys.Multiply: btnTimes.PerformClick();  break;
        case Keys.Divide:   btnDivide.PerformClick(); break;
        case Keys.Enter:    case Keys.Return: btnEquals.PerformClick(); break;
        case Keys.Back:     btnBackspace.PerformClick(); break;
        case Keys.Escape:   btnClear.PerformClick(); break;
        case Keys.Decimal:  case Keys.OemPeriod: btnDecimal.PerformClick(); break;
    }

    e.Handled = true;
}

PerformClick() raises the button's Click event exactly as a mouse click would — so your existing handlers run unchanged and the button even animates. No logic is duplicated.

#Test it like a user

Press these sequences and confirm each one:

InputExpected
5 + 3 =8
5 + 3 + 2 =10 (chaining works)
5 + 3 = =8 (a second = does nothing)
9 ÷ 0 =An error message, not
0.1 + 0.2 =0.3, not 0.30000000000000004
5 + × 3 =15 (the operator was replaced)
. 5 + . 5 =1 (leading decimals become 0.5)
C mid-calculationEverything resets

That table is your test suite. Run it every time you change the logic.

Check yourself

4 questions on what you just read. No score is kept — it is only here to catch the bits that did not stick.

  1. 1Ten digit buttons, one handler. How does it know which was pressed?

    Show the answer

    A. Cast sender back to Button and read its Text or Tag.

    sender is always the control that raised the event. Any time you are writing near-identical handlers, share one and branch on sender.

  2. 2Which flag makes digit entry behave correctly?

    Show the answer

    A. One tracking whether the next digit starts a new number or appends to the current one.

    After pressing + the display still reads 5; typing 3 must give 3, not 53. That is the flag every buggy calculator gets wrong.

  3. 39.0 / 0 on doubles. What happens?

    Show the answer

    A. It returns Infinity — no exception is thrown.

    Only integer division by zero throws. With doubles your display quietly reads , so you must check for it yourself.

  4. 4Why should currency never use double?

    Show the answer

    A. Binary floating point cannot represent decimal fractions exactly, so 0.1 + 0.2 != 0.3.

    Use decimal for money — it is base-10 and exact. For a calculator display, double formatted with G15 is fine.

#Key takeaways

  • One event handler can serve many controls — cast sender to find out which fired.
  • A GUI app's difficulty lives in its state. Three fields model an entire calculator.
  • The "is this a new entry?" flag is what makes digit entry behave correctly.
  • Floating-point division by zero yields Infinity, not an exception. Check it yourself.
  • double is inexact; format with G15 for display, and use decimal for money.
  • KeyPreview plus PerformClick() adds keyboard support without duplicating any logic.

Finished this one?

The challenge above has no posted solution — that is deliberate. Get it working, then come back for the next lesson.

More tutorials Subscribe