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
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
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
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()
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
Top comments (6)
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)
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?
sorry but iam a begginer in python and i love your kind of code i want more explanation for this code please
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.
that's the way!
edit: kind of pet project
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.