A calculator in five stages
Build a real calculator in half an hour — with a history, keyboard control and exact arithmetic. It already calculates after stage 2.
At the end of this tutorial you won't have a practice program but a tool you'll actually use: a calculator with a keypad, a history of recent calculations, keyboard control and a display that writes numbers the German way.
It's the same calculator that sits on the Built with Kiste page — not a stripped-down practice clone.
We build it in five stages. Each one ends with a program that starts and does something. Your calculator already calculates after stage 2 — everything after that you add to something that works, not into the blue.
All you need is Kiste. If it isn't installed yet: download it here, one click, done.
A note on the code: Kiste is a German programming language, so the keywords are German —
wennis „if",sonstis „else",funktionis „function",nimmis „let" andgibis „return". The explanations below tell you what each part does.
Stage 1 · A window with a display
Start with what the user sees first. Create a file called taschenrechner.ki.
The display isn't an ordinary line of text but a canvas — a surface the program paints on itself. The reason: soon a large number has to sit flush right with the running calculation above it in small type. A standard label can't do that, and painting it yourself is easier than it sounds.
// Etappe 1 — ein Fenster mit einer Anzeige.
nutze gui
fest A_BREITE = 336
fest A_HOEHE = 96
fest FELD = "#0f1318"
fest HELL = "#f2f4f7"
nimm anzeige = "0"
nimm feld = gui.leinwand(A_BREITE, A_HOEHE)
funktion zeichne_anzeige() {
gui.leeren(feld)
gui.rechteck(feld, 0, 0, A_BREITE, A_HOEHE, FELD)
nimm breite = länge(anzeige) * 19
gui.text_an(feld, A_BREITE - 16 - breite, 44, anzeige, HELL, 34)
}
gui.thema("dunkel")
zeichne_anzeige()
nimm fenster = gui.fenster_öffne("Kiste — Taschenrechner", 344, 104)
gui.zeige(fenster, feld)
gui.starte()
Build and run it — programs with a window always go through kiste build:
kiste build taschenrechner.ki
taschenrechner.exe
You get a dark window with a 0 at the right edge.
The flush-right trick: gui.text_an places text at a left position. To make the
number stick to the right, we estimate its width (länge * 19 at font size 34) and subtract
that from the right edge. Not pixel-perfect, but perfectly good for digits.
Why fest instead of nimm? fest is a value that never changes (a constant). If you
later write A_BREITE = 400 by accident, Kiste tells you instead of quietly breaking
something.
Stage 2 · It calculates
Now the keys arrive — and with them the moment the window turns into a calculator.
A calculator has to remember four things:
nimm anzeige = "0" // was gerade groß dasteht
nimm gespeichert = 0.0 // die Zahl von vor dem Rechenzeichen
nimm operator = "" // welches Zeichen gedrückt wurde
nimm neue_zahl = wahr // fängt die nächste Ziffer eine neue Zahl an?
neue_zahl („new number") is the least conspicuous but most important flag. Type 12 and
the digits should stick together. Type 12 + 3 and the 3 must start a new number
rather than turning into 123. That flag is what switches between the two.
The digits
funktion tippe(z) {
wenn neue_zahl {
anzeige = z
neue_zahl = falsch
} sonst {
wenn anzeige == "0" {
anzeige = z
} sonst {
anzeige = anzeige + z
}
}
zeichne_anzeige()
}
The arithmetic
anwenden („apply") performs the pending calculation. Note the number type: we convert the
display with als_dezimal, not with als_komma. More on that in a moment.
funktion anwenden() {
nimm jetzt = als_dezimal(anzeige)
wenn operator == "+" {
gespeichert = gespeichert + jetzt
}
wenn operator == "-" {
gespeichert = gespeichert - jetzt
}
wenn operator == "×" {
gespeichert = gespeichert * jetzt
}
wenn operator == "÷" {
gespeichert = gespeichert / jetzt
}
anzeige = "{gespeichert}"
}
funktion rechne_weiter(op) {
wenn operator == "" {
gespeichert = als_dezimal(anzeige)
} sonst {
wenn nicht neue_zahl {
anwenden()
}
}
operator = op
neue_zahl = wahr
zeichne_anzeige()
}
funktion gleich() {
wenn operator != "" {
anwenden()
operator = ""
neue_zahl = wahr
zeichne_anzeige()
}
}
The clever bit sits in rechne_weiter („keep calculating"): press 2 + 3 + 4 and the
second + should first work out 2 + 3 and carry on with the result. That's what the
sonst branch does — it chains calculations without you pressing = every time.
Creating and wiring the keys
Every key takes two steps: first the button, then what it should do.
nimm k7 = gui.knopf_neu("7")
gui.bei_klick(k7, funktion(k) { tippe("7") })
Repeat that for the digits 0 to 9. The operators follow the same pattern, only with
rechne_weiter:
nimm k_mal = gui.knopf_neu("×")
gui.bei_klick(k_mal, funktion(k) { rechne_weiter("×") })
nimm k_gleich = gui.knopf_neu("=")
gui.bei_klick(k_gleich, funktion(k) { gleich() })
Arranging the keys
You don't have to calculate any positions. You tell gui.raster how many columns you want
and hand it the keys in reading order:
// Die letzte Reihe ist noch nicht voll — Komma, Löschen und Prozent
// kommen in Etappe 3 dazu.
nimm gitter = gui.raster(4, [
k7, k8, k9, k_div,
k4, k5, k6, k_mal,
k1, k2, k3, k_minus,
k0, k_gleich, k_plus
])
Finally stack the display on top of the keypad and make the window taller. This replaces the last lines from stage 1 — the window no longer shows the canvas alone but both, one above the other:
nimm inhalt = gui.vertikal([feld, gitter])
nimm fenster = gui.fenster_öffne("Kiste — Taschenrechner", 344, 300)
gui.zeige(fenster, inhalt)
gui.starte()
If you accidentally leave the old lines in, two windows open at startup — now you know why.
Rebuild, start, click 7 × 8 =: 56. You have a working calculator.
Why dezimal and not komma?
Type 0.1 + 0.2 into almost any other programming language. The result isn't 0.3 but
0.30000000000000004. That's not a bug in those languages, it's the nature of floating point:
it calculates in binary and cannot represent a tenth exactly — just as you cannot write a
third exactly as a decimal.
Kiste has its own number type for exactly this case. dezimal works in decimal places and
is exact. That's why your calculator really does show 0,3 after 0,1 + 0,2. The same
goes for money — and a calculator is very much like money.
Stage 3 · The missing keys, and nothing crashes any more
Now C, ±, %, the decimal comma and backspace join in. All of them follow the pattern
you already know. Two spots are interesting.
One line at the very top first: from here on we work with text — checking whether a
decimal separator is already there, cutting off the last character. Those functions live in
the text module, and you pull modules in when you need them. So add this below
nutze gui:
nutze gui
nutze text
Forget it and Kiste will tell you at startup that it doesn't know text.
First: dividing by zero. Kiste raises an error on division by zero — rightly so, since
silently handing back „infinity" would only hide programming mistakes. But a calculator must
not crash when somebody types 5 ÷ 0. So we ask beforehand and remember an error state:
nimm fehler = falsch
// … in anwenden():
wenn operator == "÷" {
wenn jetzt == 0 {
fehler = wahr
} sonst {
gespeichert = gespeichert / jetzt
}
}
The display then shows a text instead of the number:
nimm gross = anzeige
wenn fehler {
gross = "Nicht definiert"
}
And every key that starts fresh input clears the error away:
funktion tippe(z) {
wenn fehler {
alles_löschen()
}
// … wie bisher
}
Second: the decimal separator may appear only once. 3,,14 wouldn't be a number. One
line prevents it:
funktion komma() {
wenn neue_zahl {
anzeige = "0."
neue_zahl = falsch
} sonst {
wenn nicht text.enthält(anzeige, ".") {
anzeige = anzeige + "."
}
}
zeichne_anzeige()
}
Note: internally we write a dot — that's the notation als_dezimal understands. We only
turn it into a comma when displaying, in the next stage.
Backspace cuts off the last character, the sign key puts a minus in front or takes it away, and percent divides by 100. The keypad now has five full rows:
nimm gitter = gui.raster(4, [
k_c, k_vz, k_proz, k_div,
k7, k8, k9, k_mal,
k4, k5, k6, k_minus,
k1, k2, k3, k_plus,
k_rueck, k0, k_komma, k_gleich
])
Stage 4 · A look of its own, and German numbers
The calculator works — now it should look like something too.
Colours. Every button can have its own background colour. Since that repeats twenty times, creating a button moves into a small function that hands the finished one back:
fest ZIFFER = "#2b323c"
fest FUNKTION = "#232932"
fest OPERATOR = "#4a3a18"
fest GLEICH = "#a06c0f"
funktion taste(beschriftung, farbe) {
nimm k = gui.knopf_neu(beschriftung)
gui.setze_knopf_farbe(k, farbe)
gib k
}
nimm k7 = taste("7", ZIFFER)
gui.bei_klick(k7, funktion(k) { tippe("7") })
Two lines per key become two lines again — only now it looks the way you want. The
operators get a warm tone and the = the strongest one, so you can tell at a glance what
is a digit and what does the calculating.
German numbers. 8680 reads badly, 8 680 reads well. The rule: a space after every
third digit from the right. So we walk through the number character by character and count
how many still follow:
funktion gruppiert(t) {
nimm aus = ""
nimm n = länge(t)
nimm i = 0
solange i < n {
aus = aus + text.zeichen_bei(t, i)
nimm rest = n - i - 1
wenn rest > 0 und rest % 3 == 0 {
aus = aus + " "
}
i = i + 1
}
gib aus
}
And the dot becomes a comma. For that we split the number at its dot: group the front part, leave the back part alone.
funktion hübsch(t) {
nimm stücke = text.zerlegt(t, ".")
wenn länge(stücke) == 2 {
gib gruppiert(stücke[0]) + "," + stücke[1]
}
gib gruppiert(t)
}
The small line. Above the large number the display now shows what is being calculated —
1 240 × 7 =. For that the program keeps a second text called neben and paints it smaller
and greyer above the number.
Stage 5 · History and keyboard
Two things are missing from the finished program.
The history is a second canvas to the right of the keypad. It shows the last six calculations. Instead of a growing list we use six fixed slots and push everything back by one whenever a new calculation arrives:
nimm v_rechnung = ["", "", "", "", "", ""]
nimm v_ergebnis = ["", "", "", "", "", ""]
funktion merke(r, e) {
nimm a = v_rechnung
nimm b = v_ergebnis
nimm i = 5
solange i > 0 {
a[i] = a[i - 1]
b[i] = b[i - 1]
i = i - 1
}
a[0] = r
b[0] = e
zeichne_verlauf()
}
gui.horizontal puts the two side by side:
nimm links = gui.vertikal([feld, gitter])
nimm inhalt = gui.horizontal([links, verlauf])
The keyboard is the final polish — and almost free, because all the operating logic already sits in functions. The key handler simply calls the same functions the buttons do:
gui.bei_taste(fenster, funktion(f) {
nimm t = gui.letzte_taste(f)
wenn text.enthält("0123456789", t) {
tippe(t)
}
wenn t == "," oder t == "." {
komma()
}
wenn t == "+" {
rechne_weiter("+")
}
wenn t == "=" oder t == "eingabe" {
gleich()
}
wenn t == "escape" {
alles_löschen()
}
wenn t == "rücktaste" {
rücktaste()
}
})
Because both paths use the same functions, they cannot drift apart. Whatever you change about a key applies to mouse and keyboard at once.
Window size: set it to exactly the content — 524 × 304. Kiste doesn't spread leftover
space around, so it would otherwise sit there as dead margin at the bottom right.
Done — what now?
That's the whole calculator: a single file, no extra assets, no framework.
You can download the complete source — feel free to compare it with yours if something is stuck somewhere.
Three ideas to build on, from easy to hard:
- A square-root key.
mathe.wurzelalready exists — you only have to hang it on a button. Where does it fit in the grid? - A memory (M+ / MR). One more variable, two more keys — and your calculator can remember a number.
- A clickable history. A click on an old calculation brings its result back into the
display. For that you need
gui.bei_maus_klickon the history canvas and a little arithmetic to work out which line was hit.