DEV Community

Cover image for Building An OOP Calculator And What It Means To Write A Widget Library
Abdur-Rahmaan Janhangeer
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

alt text

and as usual, that's some info

alt text

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

alt text

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

Elegantly Adding Elements

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

Actually adding them

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

alt text

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

alt text

Adding The Logic

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()

alt text

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

alt text

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

Latest comments (6)

Collapse
 
akashvshroff profile image
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.

Collapse
 
itr13 profile image
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()
Collapse
 
abdurrahmaanj profile image
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?

Collapse
 
alialdaw profile image
alialdaw

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

Collapse
 
itr13 profile image
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.

Thread Thread
 
abdurrahmaanj profile image
Abdur-Rahmaan Janhangeer • Edited

that's the way!

edit: kind of pet project