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.
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:
| Property | Value |
|---|---|
Text | Calculator |
Size | 320, 460 |
FormBorderStyle | FixedSingle |
MaximizeBox | False |
StartPosition | CenterScreen |
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:
| Property | Value |
|---|---|
(Name) | txtDisplay |
Text | 0 |
TextAlign | Right |
Font | Segoe UI, 24pt |
ReadOnly | True |
BackColor | White |
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 0–9, ., +, −, ×, ÷, =, 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:
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 shows3. - 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.
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:
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
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:
double a = 0.1 + 0.2;
Console.WriteLine(a); // 0.30000000000000004
Console.WriteLine(a == 0.3); // FalseThat 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
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:
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:
| Input | Expected |
|---|---|
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-calculation | Everything 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.
-
1Ten digit buttons, one handler. How does it know which was pressed?
Show the answer
A. Cast
senderback toButtonand read itsTextorTag.senderis always the control that raised the event. Any time you are writing near-identical handlers, share one and branch onsender. -
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 reads5; typing3must give3, not53. That is the flag every buggy calculator gets wrong. -
3
9.0 / 0on 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. -
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
decimalfor money — it is base-10 and exact. For a calculator display,doubleformatted withG15is fine.
#Key takeaways
- One event handler can serve many controls — cast
senderto 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. doubleis inexact; format withG15for display, and usedecimalfor money.KeyPreviewplusPerformClick()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.