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



Enter fullscreen mode Exit fullscreen mode

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



Enter fullscreen mode Exit fullscreen mode

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)



Enter fullscreen mode Exit fullscreen mode

we can now add our calculator to our app



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

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



Enter fullscreen mode Exit fullscreen mode

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 = ''



Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

and using it like that



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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

usage



self.entry(0, 0)


Enter fullscreen mode Exit fullscreen mode

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)


Enter fullscreen mode Exit fullscreen mode

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)


Enter fullscreen mode Exit fullscreen mode

usage



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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

usage



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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

usage



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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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)



Enter fullscreen mode Exit fullscreen mode

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)


Enter fullscreen mode Exit fullscreen mode

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 = ''   


Enter fullscreen mode Exit fullscreen mode

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)


Enter fullscreen mode Exit fullscreen mode

clear buttons resets all



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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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)



Enter fullscreen mode Exit fullscreen mode

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

Top comments (6)

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

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.