Abdur-Rahmaan Janhangeer

Posted on

# Building An OOP Calculator And What It Means To Write A Widget Library

In this post we are going to code an OOP calculator, actually making it a widget for easy plug and play. Python's flexibility and oop make a nice match!

## Our Skeleton

We are using tkinter (actually once again, it's pronounced tee-kay-inter) and widget guys means window gadget

``````from tkinter import *

class App:
def __init__(self, master):
self.master = master

root = Tk()
app = App(root)
root.title('dev.to calculator')
root.mainloop()

``````

we get

and as usual, that's some info

## Our Calculator

here is a normal class

``````class Calculator:
def __init__(self, parent, x, y):
pass

``````

we add a frame to hold all our elements so that we just plug in a frame

``````class Calculator:
def __init__(self, parent, x, y):
self.parent = parent

self.container = Frame(self.parent)
self.container.grid(row=x, column=y)

``````

we can now add our calculator to our app

``````class App:
def __init__(self, master):
self.master = master

calc = Calculator(self.master, 0, 0)

``````

we get

## Some Initialisations In our Calculator

``````class Calculator:
def __init__(self, parent, x, y):
self.button_font = ('Verdana', 15)
self.entry_font = ('Verdana', 20)
self.parent = parent

self.button_width = 4
self.button_height = 1
self.container = Frame(self.parent)
self.container.grid(row=x, column=y)

self.string = ''

``````

those two are just font name and font size
self.button_font = ('Verdana', 15)
self.entry_font = ('Verdana', 20)

a frame can be thought of as a container

in self.string we'll be storing what the what we'll be displaying on our calculator screen

## A Note On Grid

grid tells us where to place our elements, the coordinates start from 0, 0

instead of each time adding two lines of code for one button, we'll add a simple line by defining this in the calculator class

``````def button(self, char_, x_, y_):
self.b = Button(
self.container, text=char_, width=self.button_width,
height=self.button_height, font=self.entry_font)
self.b.grid(row=x_, column=y_)
``````

and using it like that

``````self.button('7', 1, 0)
``````

same for our text field we'd be displaying our characters

``````def entry(self, x_, y_):
self.entry = Text(
self.container, font=self.entry_font, state=DISABLED,
height=self.button_height//2, width=self.button_width*5)
self.entry.grid(row=x_, column=y_, columnspan=5, sticky='we')
``````

usage

``````self.entry(0, 0)
``````

state=DISABLED is used so that we can't write anything by placing our cursor
columnspan expands our element in other columns

in our calculator class

``````        self.entry(0, 0)

self.button('7', 1, 0)
self.button('8', 1, 1)
self.button('9', 1, 2)

self.button('4', 2, 0)
self.button('5', 2, 1)
self.button('6', 2, 2)

self.button('1', 3, 0)
self.button('2', 3, 1)
self.button('3', 3, 2)

self.button('0', 4, 0)

self.button('+', 1, 3)
self.button('-', 1, 4)
self.button('*', 2, 3)
self.button('/', 2, 4)

self.button('(', 3, 3)
self.button(')', 3, 4)
``````

here is what we get

## Adding the remove one char, equal and clear buttons

we'll have three buttons, one to remove a char if we mistakenly added one
one to evaluate our result and one to clear the display completely

the evaluate button

``````def button_eq(self, char_, x_, y_):
self.b = Button(
self.container, text=char_, width=self.button_width,
height=self.button_height, font=self.entry_font)
self.b.grid(row=x_, column=y_, sticky='we', columnspan=2)
``````

usage

``````self.button_eq('=', 4, 1)
``````

the remove one char button

``````def button_rem(self, char_, x_, y_):
self.b = Button(
self.container, text=char_, width=self.button_width,
height=self.button_height, font=self.entry_font)
self.b.grid(row=x_, column=y_)
``````

usage

``````self.button_rem('<', 4, 4)
``````

the clear button

``````def button_clear(self, char_, x_, y_):
self.b = Button(
self.container, text=char_, width=self.button_width,
height=self.button_height, font=self.entry_font)
self.b.grid(row=x_, column=y_)
``````

usage

``````self.button_clear('clear', 4, 3)
``````

## Where we are

code upto now

``````from tkinter import *

class Calculator:
def __init__(self, parent, x, y):
self.button_font = ('Verdana', 15)
self.entry_font = ('Verdana', 20)
self.parent = parent

self.button_width = 4
self.button_height = 1
self.container = Frame(self.parent)
self.container.grid(row=x, column=y)

self.string = ''

self.entry(0, 0)

self.button('7', 1, 0)
self.button('8', 1, 1)
self.button('9', 1, 2)

self.button('4', 2, 0)
self.button('5', 2, 1)
self.button('6', 2, 2)

self.button('1', 3, 0)
self.button('2', 3, 1)
self.button('3', 3, 2)

self.button('0', 4, 0)

self.button('+', 1, 3)
self.button('-', 1, 4)
self.button('*', 2, 3)
self.button('/', 2, 4)

self.button('(', 3, 3)
self.button(')', 3, 4)

self.button_eq('=', 4, 1)

self.button_clear('clear', 4, 3)
self.button_rem('<', 4, 4)

def button_eq(self, char_, x_, y_):
self.b = Button(
self.container, text=char_, width=self.button_width,
height=self.button_height, font=self.entry_font)
self.b.grid(row=x_, column=y_, sticky='we', columnspan=2)

def button_rem(self, char_, x_, y_):
self.b = Button(
self.container, text=char_, width=self.button_width,
height=self.button_height, font=self.entry_font)
self.b.grid(row=x_, column=y_)

def button_clear(self, char_, x_, y_):
self.b = Button(
self.container, text=char_, width=self.button_width,
height=self.button_height, font=self.entry_font)
self.b.grid(row=x_, column=y_)

def button(self, char_, x_, y_):
self.b = Button(
self.container, text=char_, width=self.button_width,
height=self.button_height, font=self.entry_font)
self.b.grid(row=x_, column=y_)

def entry(self, x_, y_):
self.entry = Text(
self.container, font=self.entry_font, state=DISABLED,
height=self.button_height//2, width=self.button_width*5)
self.entry.grid(row=x_, column=y_, columnspan=5, sticky='we')

class App:
def __init__(self, master):
self.master = master

calc = Calculator(self.master, 0, 0)

root = Tk()
app = App(root)
root.title('dev.to calculator')
root.mainloop()
``````

our gui

displaying the text makes the textarea normal, adds then disables it again

``````    def display(self, text_):
self.entry.config(state=NORMAL)
self.entry.delete('1.0', END)
self.entry.insert('1.0', text_)
self.entry.config(state=DISABLED)

``````

clicking on any button adds the char on the button to the display

``````    def normal_button_click(self, text_):
self.string = '' + self.string + text_
self.display(self.string)
``````

pressing the equal button evaluated whatever we're writing. we are using py's eval, so we can use ** for exponential

``````    def equal_button_click(self):
self.display(eval(self.string))
self.string = ''
``````

removing one char just reduces the string by excluding the last char

``````    def rem_button_click(self):
self.string = '' + self.string[0:-1]
self.display(self.string)
``````

clear buttons resets all

``````    def clear_button_click(self):
self.display('')
self.string = ''
``````

## Our Buttons With Added Commands

a normal command is added by

command = function_name

but a command with parameters is added by

command=lambda: self.function(parameter_)

``````    def button(self, char_, x_, y_):
self.b = Button(
self.container, text=char_, width=self.button_width,
height=self.button_height, font=self.entry_font,
command=lambda: self.normal_button_click(char_))
self.b.grid(row=x_, column=y_)

def button_eq(self, char_, x_, y_):
self.b = Button(
self.container, text=char_, width=self.button_width,
height=self.button_height, font=self.entry_font,
command=self.equal_button_click)
self.b.grid(row=x_, column=y_, sticky='we', columnspan=2)

def button_rem(self, char_, x_, y_):
self.b = Button(
self.container, text=char_, width=self.button_width,
height=self.button_height, font=self.entry_font,
command=self.rem_button_click)
self.b.grid(row=x_, column=y_)

def button_clear(self, char_, x_, y_):
self.b = Button(
self.container, text=char_, width=self.button_width,
height=self.button_height, font=self.entry_font,
command=self.clear_button_click)
self.b.grid(row=x_, column=y_)
``````

## Finally It

our complete code

``````from tkinter import *

class Calculator:
def __init__(self, parent, x, y):
self.button_font = ('Verdana', 15)
self.entry_font = ('Verdana', 20)
self.parent = parent

self.button_width = 4
self.button_height = 1
self.container = Frame(self.parent)
self.container.grid(row=x, column=y)

self.string = ''

self.entry(0, 0)

self.button('7', 1, 0)
self.button('8', 1, 1)
self.button('9', 1, 2)

self.button('4', 2, 0)
self.button('5', 2, 1)
self.button('6', 2, 2)

self.button('1', 3, 0)
self.button('2', 3, 1)
self.button('3', 3, 2)

self.button('0', 4, 0)

self.button('+', 1, 3)
self.button('-', 1, 4)
self.button('*', 2, 3)
self.button('/', 2, 4)

self.button('(', 3, 3)
self.button(')', 3, 4)

self.button_eq('=', 4, 1)

self.button_clear('clear', 4, 3)
self.button_rem('<', 4, 4)

def entry(self, x_, y_):
self.entry = Text(
self.container, font=self.entry_font, state=DISABLED,
height=self.button_height//2, width=self.button_width*5)
self.entry.grid(row=x_, column=y_, columnspan=5, sticky='we')

def button(self, char_, x_, y_):
self.b = Button(
self.container, text=char_, width=self.button_width,
height=self.button_height, font=self.entry_font,
command=lambda: self.normal_button_click(char_))
self.b.grid(row=x_, column=y_)

def button_eq(self, char_, x_, y_):
self.b = Button(
self.container, text=char_, width=self.button_width,
height=self.button_height, font=self.entry_font,
command=self.equal_button_click)
self.b.grid(row=x_, column=y_, sticky='we', columnspan=2)

def button_rem(self, char_, x_, y_):
self.b = Button(
self.container, text=char_, width=self.button_width,
height=self.button_height, font=self.entry_font,
command=self.rem_button_click)
self.b.grid(row=x_, column=y_)

def button_clear(self, char_, x_, y_):
self.b = Button(
self.container, text=char_, width=self.button_width,
height=self.button_height, font=self.entry_font,
command=self.clear_button_click)
self.b.grid(row=x_, column=y_)

def display(self, text_):
self.entry.config(state=NORMAL)
self.entry.delete('1.0', END)
self.entry.insert('1.0', text_)
self.entry.config(state=DISABLED)

def normal_button_click(self, text_):
self.string = '' + self.string + text_
self.display(self.string)

def equal_button_click(self):
self.display(eval(self.string))
self.string = ''

def rem_button_click(self):
self.string = '' + self.string[0:-1]
self.display(self.string)

def clear_button_click(self):
self.display('')
self.string = ''

class App:
def __init__(self, master):
self.master = master

calc = Calculator(self.master, 0, 0)

root = Tk()
app = App(root)
root.title('dev.to calculator')
root.mainloop()
``````

## Exploring Widget Flexibility

Since our calculator is a widget, we can add mooore

``````        calc = Calculator(self.master, 0, 0)
calc = Calculator(self.master, 0, 1)
calc = Calculator(self.master, 1, 0)
calc = Calculator(self.master, 1, 1)

``````

gives us

on a single screen, each functioning as an app on it's own

## So ... A Widget Library

A widget library is just some gui classes. Easy isn't it?

cover img credit: rawpixels on unsplash

Mikael Klages

I made a version based on your code, where I tried to decrease duplicate code, and increase extendability.

The biggest change was putting all the buttons in a 2D list, and rather base the grid position based on that. This makes it easier to move buttons around and add new ones, as you don't have to calculate their grid-position, or update the position of other buttons. It also makes it less prone to mistakes.

The second biggest change was making all the buttons use a single method, which meant I could get rid of a lot of duplicate code, where the only difference was the "command" parameter. I also made it have an optional parameter for width to allow for the "equals button" to use the system too.

Other than that it works exactly the same as your program (other than having equals set self.string with the return value rather than an empty string)

``````from tkinter import *

BUTTON_WIDTH = 4

def b_info(symbol, operation=None, width=1):
if operation is None:
operation = lambda s: s+symbol
return (symbol, operation, width)

class Calculator:
def __init__(self, parent, x, y):
self.button_font = ('Verdana', 15)
self.entry_font = ('Verdana', 20)
self.parent = parent

self.button_height = 1
self.container = Frame(self.parent)
self.container.grid(row=x, column=y)

self.string = ''

self.entry(0, 0)

numbers = [
[b_info(str(i)) for i in range(n, n+3)]
for n in range(7, 0, -3)
]
buttons = [
[ b_info('+'), b_info('-') ],
[ b_info('*'), b_info('/') ],
[ b_info('('), b_info(')') ],
[
b_info('0'),
b_info('=', lambda s: str(eval(s)), 2),
b_info('clear', lambda s: ''),
b_info('<', lambda s: s[:-1])
],
[]
]
buttons = [i+j for i,j in zip(numbers+[[]], buttons)]

for row, y in zip(buttons, range(1, len(buttons)+2)):
x = 0 if y > 0 else 2
for b in row:
self.button(b[0], b[1], x, y, b[2])
x += b[2]

def entry(self, x, y):
self.entry = Text(
self.container, font=self.entry_font, state=DISABLED,
height=self.button_height//2, width=BUTTON_WIDTH * 5)
self.entry.grid(row=x, column=y, columnspan=5, sticky='we')

def button(self, char, operation, x, y, w):
print(x, y)
def execute():
self.string = operation(self.string)
self.display(self.string)

button = Button(
self.container, text=char, width=BUTTON_WIDTH,
height=self.button_height, font=self.entry_font,
command=execute
)
button.grid(row=y, column=x, sticky='we', columnspan=w)

def display(self, text_):
self.entry.config(state=NORMAL)
self.entry.delete('1.0', END)
self.entry.insert('1.0', text_)
self.entry.config(state=DISABLED)

class App:
def __init__(self, master):
self.master = master
calc = Calculator(self.master, 0, 0)

root = Tk()
app = App(root)
root.title('dev.to calculator')
root.mainloop()
``````

Abdur-Rahmaan Janhangeer

thank you very much. yes, the two main improvements are legitimate ones, easier positioning and a general purpose button function. indeed, a novel view of the affair. just some bits of comments appreciated.

i'll refine and build something on those. else, are you interested in a widget lib collab?

alialdaw

sorry but iam a begginer in python and i love your kind of code i want more explanation for this code please

Mikael Klages

I'm a bit full on projects at the moment, but if you have a git repo or something I can check it out when I have time.

Abdur-Rahmaan Janhangeer • Edited on

that's the way!

edit: kind of pet project

Akash Shroff

Hey, I love your tutorials. I used your OOP framework to build my own Tkinter GUI for a typing test. The script without the OOP foundation runs perfectly but the OOP script returns an error - _tkinter.TclError: Slave index 1 out of bounds. All the details for my code and subsequent error are outlined here: stackoverflow.com/questions/616273..., I'd greatly appreciate if you could have a look. Best.

## Build Anything...

Use any Linode offering to create something for the DEV x Linode Hackathon 2022. A variety of prizes are up for grabs, inculding \$1,000 USD. π

β Join the Hackathon <-