Let's Code that Wicked Cool Calculator





5.00/5 (8 votes)
Enhance your coding skills with a walkthrough on how to create a smart calculator with many features made in Python.
Introduction
I know many of you loved the article that I posted earlier this month, so if you didn't have the chance to read it or if you are willing to apply your skills and learn Python or web development, I encourage you to read it.
You will learn many libraries and how to combine them to make a useful application.
I like my articles to be useful, exciting, challenging, and to simulate real-world scenarios, so I have crafted this tutorial for you. I hope you learn something valuable from it. So let's begin.
Background
You need to know the basics of Python to follow the tutorial as this is not a Python course.
I recommend you read this article to get familiar with Tkinter and be ready to go (optional).
For more information on sympy library, click here.
Requirements
Create a new virtual environment:
python -m venv .venv
Activate the virtual environment:
. .venv/Scripts/activate
Download the file here or copy its content, then paste it into a new file called requirements.txt.
Install the requirements by using the following command:
pip install -r requirements.txt
Also, download the Tesseract setup which is necessary for image-to-text extraction and follow the steps shown. Download here.
Implementation
To build a simple window in Tkinter, we must import the module and then create a window that will contain all our elements, give it a title, and finally call the window.
Create a new file named main.py, then write the following code:
import tkinter as tk
win = tk.Tk()
win.title('Hello world')
win.mainloop()
Make sure you have activated the virtual environment then to run the program, execute the command:
python main.py
You should get a blank window with a Hello world
header.
Now we need to fill in the window with some Tkinter widgets.
Tkinter has many widgets, in this tutorial, we will use Button
, Entry
, Text
, Frame
, and LabelFrame
which is a panel with a title above.
Every Tkinter widget has a parent, so a button can be inside a window or a frame (panel) which is a container where you can put widgets inside of it.
Let's create a basic interface with an entry (textbox
) and some buttons.
import tkinter as tk
win = tk.Tk()
win.title('Smart Calculator')
frm_txtbox = tk.Frame()
frm_txtbox.pack()
txt_box = tk.Text(master=frm_txtbox, width=32, height=8, font=('default', 16))
txt_box.insert(tk.INSERT, 'Type here your math problem ...')
txt_box.pack()
win.mainloop()
This will create a basic user interface with an entry to type some information.
First, we initialized the window and created a frame called frm_txtbox
and put in place by pack()
function. Then, we created a Textbox
inside frm_txtbox (master=frm_txtbox)
and some parameters to customize it.
However, it does nothing so let's update the code to make some buttons.
import tkinter as tk
win = tk.Tk()
win.title('Smart Calculator')
frm_txtbox = tk.Frame()
frm_txtbox.pack()
txt_box = tk.Text(master=frm_txtbox, width=32, height=8, font=('default', 16))
txt_box.insert(tk.INSERT, 'Type here your math problem ...')
txt_box.pack()
frm_standard = tk.LabelFrame(text='Standard', font=('default', 12))
frm_standard.pack()
btn_parentheses_right = tk.Button(master=frm_standard, text='(', width=5, height=2, cursor='hand2', font=('default', 12))
btn_parentheses_left = tk.Button(master=frm_standard, text=')', width=5, height=2, cursor='hand2', font=('default', 12))
btn_seven = tk.Button(master=frm_standard, text='7', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_eight = tk.Button(master=frm_standard, text='8', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_nine = tk.Button(master=frm_standard, text='9', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_divide = tk.Button(master=frm_standard, text='/', width=5, height=2, cursor='hand2', font=('default', 12))
btn_square = tk.Button(master=frm_standard, text='²', width=5, height=2, cursor='hand2', font=('default', 12))
btn_square_root = tk.Button(master=frm_standard, text='√', width=5, height=2, cursor='hand2', font=('default', 12))
btn_four = tk.Button(master=frm_standard, text='4', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_five = tk.Button(master=frm_standard, text='5', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_six = tk.Button(master=frm_standard, text='6', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_multiply = tk.Button(master=frm_standard, text='*', width=5, height=2, cursor='hand2', font=('default', 12))
btn_cube = tk.Button(master=frm_standard, text='³', width=5, height=2, cursor='hand2', font=('default', 12))
btn_cube_root = tk.Button(master=frm_standard, text='∛', width=5, height=2, cursor='hand2', font=('default', 12))
btn_one = tk.Button(master=frm_standard, text='1', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_two = tk.Button(master=frm_standard, text='2', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_three = tk.Button(master=frm_standard, text='3', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_minus = tk.Button(master=frm_standard, text='-', width=5, height=2, cursor='hand2', font=('default', 12))
btn_pi = tk.Button(master=frm_standard, text='Ⲡ', width=5, height=2, cursor='hand2', font=('default', 12))
btn_x = tk.Button(master=frm_standard, text='x', width=5, height=2, cursor='hand2', font=('default', 12))
btn_zero = tk.Button(master=frm_standard, text='0', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_dot = tk.Button(master=frm_standard, text='.', width=5, height=2, cursor='hand2', font=('default', 12))
btn_equal = tk.Button(master=frm_standard, text='=', width=5, height=2, cursor='hand2', font=('default', 12))
btn_plus = tk.Button(master=frm_standard, text='+', width=5, height=2, cursor='hand2', font=('default', 12))
i,j = 0,0
for btn in frm_standard.children:
frm_standard.children[btn].grid(row=j, column=i)
i += 1
if i == 6:
i = 0
j += 1
win.mainloop()
You should get something like this:
The first change is we added the standard calculator buttons like the numbers from 0 to 9 and the fundamental operations.
Then I created two variables, i
and j
to place the buttons in order using the grid function which requires two parameters as you can see (row and column). You may ask why i
is set to zero when it's 6, well .. once we have created six buttons, we need to move to a new line to insert those buttons. You can, of course, add buttons the way you like. But I found this is the proper order to do this.
For large-scale applications, we can avoid confusion and make our code manageable by splitting our code into files, let's create a new file called gui_layout.py where we will make the full layout of the GUI.
from main import *
# Layout the standard default panel
def place_std_btns():
i,j = 0,0
for btn in frm_standard.children:
frm_standard.children[btn].grid(row=j, column=i)
i += 1
if i == 6:
i = 0
j += 1
place_std_btns()
frm_txtbox.pack()
txt_box.pack()
frm_standard.pack()
win.mainloop()
Update main.py as the following:
import tkinter as tk
win = tk.Tk()
win.title('Smart Calculator')
frm_txtbox = tk.Frame()
txt_box = tk.Text(master=frm_txtbox, width=32, height=8, font=('default', 16))
txt_box.insert(tk.INSERT, 'Type here your math problem ...')
frm_standard = tk.LabelFrame(text='Standard', font=('default', 12))
btn_parentheses_right = tk.Button(master=frm_standard, text='(', width=5, height=2, cursor='hand2', font=('default', 12))
btn_parentheses_left = tk.Button(master=frm_standard, text=')', width=5, height=2, cursor='hand2', font=('default', 12))
btn_seven = tk.Button(master=frm_standard, text='7', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_eight = tk.Button(master=frm_standard, text='8', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_nine = tk.Button(master=frm_standard, text='9', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_divide = tk.Button(master=frm_standard, text='/', width=5, height=2, cursor='hand2', font=('default', 12))
btn_square = tk.Button(master=frm_standard, text='²', width=5, height=2, cursor='hand2', font=('default', 12))
btn_square_root = tk.Button(master=frm_standard, text='√', width=5, height=2, cursor='hand2', font=('default', 12))
btn_four = tk.Button(master=frm_standard, text='4', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_five = tk.Button(master=frm_standard, text='5', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_six = tk.Button(master=frm_standard, text='6', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_multiply = tk.Button(master=frm_standard, text='*', width=5, height=2, cursor='hand2', font=('default', 12))
btn_cube = tk.Button(master=frm_standard, text='³', width=5, height=2, cursor='hand2', font=('default', 12))
btn_cube_root = tk.Button(master=frm_standard, text='∛', width=5, height=2, cursor='hand2', font=('default', 12))
btn_one = tk.Button(master=frm_standard, text='1', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_two = tk.Button(master=frm_standard, text='2', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_three = tk.Button(master=frm_standard, text='3', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_minus = tk.Button(master=frm_standard, text='-', width=5, height=2, cursor='hand2', font=('default', 12))
btn_pi = tk.Button(master=frm_standard, text='Ⲡ', width=5, height=2, cursor='hand2', font=('default', 12))
btn_x = tk.Button(master=frm_standard, text='x', width=5, height=2, cursor='hand2', font=('default', 12))
btn_zero = tk.Button(master=frm_standard, text='0', bg='white', width=5, height=2, cursor='hand2', font=('default', 12))
btn_dot = tk.Button(master=frm_standard, text='.', width=5, height=2, cursor='hand2', font=('default', 12))
btn_equal = tk.Button(master=frm_standard, text='=', width=5, height=2, cursor='hand2', font=('default', 12))
btn_plus = tk.Button(master=frm_standard, text='+', width=5, height=2, cursor='hand2', font=('default', 12))
We have now some sort of a calculator but still... It does not do anything.
Let's make the buttons functional so that when we click on them, they do something.
Update gui_layout.py:
from main import *
# Layout the standard default panel
def place_std_btns():
i,j = 0,0
for btn in frm_standard.children:
frm_standard.children[btn].grid(row=j, column=i)
i += 1
if i == 6:
i = 0
j += 1
# Adds to the text-box what the button contains in it
def insert_btn_txt(btn):
txt_box.config(state='normal')
txt_box.insert(tk.END, btn['text'])
txt_box.config(state='disabled')
# Make every button functional by assigning a function to it
def assign_btn_funcs():
for btn in frm_standard.children:
frm_standard.children[btn]['command'] = lambda x=frm_standard.children[btn]: insert_btn_txt(x)
# Calls the layout functions above and layout the gui elements
def init_gui_layout():
place_std_btns()
frm_txtbox.pack()
txt_box.pack()
frm_standard.pack()
assign_btn_funcs()
init_gui_layout()
We have added a function called assign_btn_funcs
to make every button on the screen functional by assigning a lambda function which will add to the textbox what the button contains in it. Say we clicked on 7, then 7 will be added to the textbox.
But if you may have noticed, the default text is still apparent, so let's remove it by adding a delete function that removes the default text by calling a clear text function:
from main import *
# Layout the standard default panel
def place_std_btns():
i,j = 0,0
for btn in frm_standard.children:
frm_standard.children[btn].grid(row=j, column=i)
i += 1
if i == 6:
i = 0
j += 1
# Clears all text from text box
def clear_txt():
txt_box.config(state='normal')
txt_box.delete('1.0', tk.END)
txt_box.config(state='disabled')
# Deletes 'Type here your math problem ...' to let the user add input
def delete_paceholder():
if txt_box.get(1.0, "end-1c") == 'Type here your math problem ...':
clear_txt()
# Adds to the text-box what the button contains in it
def insert_btn_txt(btn):
delete_paceholder()
txt_box.config(state='normal')
txt_box.insert(tk.END, btn['text'])
txt_box.config(state='disabled')
# Make every button functional by assigning a function to it
def assign_btn_funcs():
for btn in frm_standard.children:
frm_standard.children[btn]['command'] = lambda x=frm_standard.children[btn]: insert_btn_txt(x)
# Calls the layout functions above and layout the gui elements
def init_gui_layout():
place_std_btns()
frm_txtbox.pack()
txt_box.pack()
frm_standard.pack()
assign_btn_funcs()
init_gui_layout()
win.mainloop()
Now we will restructure the code so everything should be in its proper place.
We will have four files:
First, make a new file called functions.py which will handle the math-related functions:
from widgets import *
# Clears all text from text box
def clear_txt():
txt_box.config(state='normal')
txt_box.delete('1.0', tk.END)
txt_box.config(state='disabled')
# Deletes 'Type here your math problem ...' to let the user add input
def delete_paceholder():
if txt_box.get(1.0, "end-1c") == 'Type here your math problem ...':
clear_txt()
# Adds to the text-box what the button contains in it
def insert_btn_txt(btn):
delete_paceholder()
txt_box.config(state='normal')
txt_box.insert(tk.END, btn['text'])
txt_box.config(state='disabled')
Secondly, change the gui_layout.py as the following:
from functions import *
# Layout the standard default panel
def place_std_btns():
i,j = 0,0
for btn in frm_standard.children:
frm_standard.children[btn].grid(row=j, column=i)
i += 1
if i == 6:
i = 0
j += 1
# Make every button functional by assigning a function to it
def assign_btn_funcs():
for btn in frm_standard.children:
frm_standard.children[btn]['command'] = lambda x=frm_standard.children[btn]: insert_btn_txt(x)
# Calls the layout functions above and layout the gui elements
def init_gui_layout():
place_std_btns()
frm_txtbox.pack()
txt_box.pack()
frm_standard.pack()
After that, make a new file (widgets.py) and move main.py content inside widgets.py.
Finally adjust main.py as shown below:
from gui_layout import assign_btn_funcs, init_gui_layout
from widgets import win
assign_btn_funcs()
init_gui_layout()
win.mainloop()
We have now the foundation to go on!
Before we add more user interfaces and math functions, let's discover some interesting Sympy functions so we get a sense on what's going on.
As we can see, Sympy has many features like calculating an integral or plotting a function.
This is the library we will use to make life easier and fulfill our program requirements.
Recently, we made our standard calculator interface, however, we did not make it calculate anything so let's add some buttons that are going to be essential like submitting our input and removing it.
We will add the basic navigation buttons, so open widgets.py and the code as shown below:
# Navigation gui elements
frm_nav_buttons = tk.Frame(pady=8, padx=5)
btn_submit = tk.Button(master=frm_nav_buttons, text='Submit', bg='lightgray', width=5, height=2, cursor='hand2', font=('default', 11))
btn_remove = tk.Button(master=frm_nav_buttons, text='⌫', bg='lightgray', width=5, height=2, cursor='hand2', font=('default', 11))
btn_clear_txt = tk.Button(master=frm_nav_buttons, text='Clear', bg='lightgray', width=5, height=2, cursor='hand2', font=('default', 11))
btn_new_line = tk.Button(master=frm_nav_buttons, text='⤶', bg='lightgray', width=5, height=2, cursor='hand2', font=('default', 11))
btn_sci_functions= tk.Button(master=frm_nav_buttons, text='∑ⅆഽ', bg='lightgray', width=5, height=2, cursor='hand2', font=('default', 11))
btn_symbols = tk.Button(master=frm_nav_buttons, text='abc', bg='lightgray', width=5, height=2, cursor='hand2', font=('default', 11))
btn_open_image = tk.Button(master=frm_nav_buttons, text='🖼', bg='lightgray', width=5, height=2, cursor='hand2', font=('default', 11))
Now to actually render the widgets, we need to call the pack and grid functions.
Change the gui_layout.py as the following:
from functions import *
# Layout the standard default panel
def place_std_btns():
i,j = 0,0
for btn in frm_standard.children:
frm_standard.children[btn].grid(row=j, column=i)
i += 1
if i == 6:
i = 0
j += 1
# Layout the main navigation panel (submit clear abc ...)
def place_nav_panel():
txt_box.grid(row=1, column=0, sticky='new')
i = 0
for btn in frm_nav_buttons.children:
frm_nav_buttons.children[btn].grid(row=0, column=i)
i += 1
# Make every button functional by assigning a function to it
def assign_btn_funcs():
for btn in frm_standard.children:
frm_standard.children[btn]['command'] = lambda x=frm_standard.children[btn]: insert_btn_txt(x)
# Calls the layout functions above and layout the gui elements
def init_gui_layout():
place_std_btns()
place_nav_panel()
frm_txtbox.pack()
frm_nav_buttons.pack()
frm_standard.pack()
With that done, you should see the navigation panel populated with numerous buttons that we will program later to do interesting stuff.
Let's program the first four buttons and see what will happen.
We already have the clear function ready. We will now add the delete character functionally and insert new line inside functions.py:
# Removes a char from text box
def remove_char():
txt_box.config(state='normal')
txt_box.delete('end-2c', tk.END)
txt_box.config(state='disabled')
# Adds a new line to the text-box
def insert_new_line():
delete_paceholder()
txt_box.config(state='normal')
txt_box.insert(tk.END, '\n')
txt_box.config(state='disabled')
The reason I am adding txt_box.config(state='normal')
and txt_box.config(state='disabled')
to make sure only the buttons have the ability to add input into the textbox. We don't want the user to add random entries.
With that done, update the assign_btn_funcs
function in gui_layout.py to assign those functions to the corresponding buttons:
# Make every button functional by assigning a function to it
def assign_btn_funcs():
for btn in frm_standard.children:
frm_standard.children[btn]['command'] = lambda x=frm_standard.children[btn]: insert_btn_txt(x)
btn_remove.configure(command=lambda: remove_char())
btn_clear_txt.configure(command=lambda: clear_txt())
btn_new_line.configure(command=lambda: insert_new_line())
We are now able to add, remove, and clear the textbox.
We need to program the submit button so when we add some input. The input will be processed then we will make a decision based on what we have entered. For example, if we enter a function, the function will be plotted, if we enter an equation, the equation must be solved, if we enter some mathematical expression, then we will need to calculate that expression, and so on...
First, we will add a function that will process the raw input that we will provide in the textbox.
Add the following code at the top of functions.py:
import sympy
# Creates a graph from the input provided, might as well create numerous graphs
def plot_expression():
exprs = process_input()
if txt_box.index(tk.INSERT)[0] == '1':
sympy.plot(sympy.sympify(exprs[0]), xlabel='x', ylabel='f(x)')
if txt_box.index(tk.INSERT)[0] == '2':
sympy.plot(sympy.sympify(exprs[0]), sympy.sympify(exprs[1]), xlabel='x', ylabel='f(x)')
if txt_box.index(tk.INSERT)[0] == '3':
sympy.plot(sympy.sympify(exprs[0]), sympy.sympify(exprs[1]), sympy.sympify(exprs[2]), xlabel='x', ylabel='f(x)')
# Find the index of the last digit after char ex: √
def digit_index(expr, char):
start_index = expr.index(char) + 1
index = 0
while True:
if expr[start_index].isdigit() or expr[start_index].isalpha():
index = start_index
else:
return index
start_index += 1
# Remove all terms to the left side and change their signs with the equal sign removed
def process_equation(equation):
equal_index = equation.index('=')
expr1 = sympy.sympify(equation[:equal_index])
expr2 = sympy.sympify(equation[equal_index + 1:])
return expr1 - expr2
# Remove all terms to the left side and change their signs with the inequal sign removed
def process_inequality(inequality, char):
inequality_index = inequality.index(char)
expr1 = sympy.sympify(inequality[:inequality_index])
expr2 = sympy.sympify(inequality[inequality_index + 1:])
final_expr = expr1 - expr2
coeff = int(final_expr.coeff([x for x in final_expr.free_symbols][0]))
if coeff < 0:
if char == '>':
return final_expr, '<'
elif char == '<':
return final_expr, '>'
elif char == '≥':
return final_expr, '≤'
elif char == '≤':
return final_expr, '≥'
else:
return final_expr, char
# Adds numbers into a list and return that list
def extract_numbers(expr):
numbers = []
for char in expr:
if char.isdigit():
numbers.append(char)
return float(''.join(numbers))
# If the expression has a symobl say x it returns true otherwise false
def has_symbol(expr):
try:
right_parentheses = expr.index('(')
left_parentheses = expr.index(')')
for char in expr[right_parentheses + 1:left_parentheses]:
if char.isalpha() and char != 'Ⲡ' and char != 'e':
return True
return False
except:
for char in expr:
if char in 'abcdefghijklmnopqrstuvwxyz':
return True
return False
# Seperates numbers and symbols by adding a multiplication sign
# so python can understand it for exapmle: (2x) becomes (2*x)
def add_star(expr):
if 'sin' not in expr and 'cos' not in expr and 'tan' not in expr and 'cot' not in expr and 'log' not in expr:
for i in range(len(expr)):
if expr[i].isdigit() and expr[i + 1].isalpha() and expr[i+1] != '°' or expr[i] == ')' and expr[i+1] == '(' and expr[i+1] != '°':
expr = expr[:i+1] + '*' + expr[i+1:]
if expr[i].isalpha() and expr[i + 1].isalpha() and expr[i+1] != '°' or expr[i] == ')' and expr[i+1] == '(' and expr[i+1] != '°':
if str(sympy.pi) not in expr:
expr = expr[:i+1] + '*' + expr[i+1:]
return expr
# Takes the raw input from the user and convert it to sympy epxression
# so the input can be processed
def process_input():
exprs = []
for i in range(1, int(txt_box.index(tk.INSERT)[0]) + 1):
expr = txt_box.get(f'{i}.0', f'{i+1}.0')
expr = expr.replace('Ⲡ', str(sympy.pi))
expr = expr.replace('e', str(sympy.E))
expr = expr.replace('²', '** 2 ')
expr = expr.replace('³', '** 3 ')
expr = add_star(expr)
if '(' in expr and expr[0] != '(':
parentheses_indexes = [m.start() for m in re.finditer("\(", expr)]
for parentheses_index in parentheses_indexes:
if not expr[parentheses_index - 1].isalpha():
expr = expr[:parentheses_index] + '*' + expr[parentheses_index:]
if '√' in expr:
square_root_index = digit_index(expr, '√')
expr = expr.replace('√', '')
expr = expr[:square_root_index] + '** 0.5 ' + expr[square_root_index:]
if '∛' in expr:
cube_root_index = digit_index(expr, '∛')
expr = expr.replace('∛', '')
expr = expr[:cube_root_index] + '** (1/3) ' + expr[cube_root_index:]
if '°' in expr:
deg = extract_numbers(expr)
func = expr[:3]
expr = f'{func}({sympy.rad(deg)})'
if '=' in expr:
expr = process_equation(expr)
if '>' in str(expr):
expr = process_inequality(expr, '>')
elif '≥' in str(expr):
expr = process_inequality(expr, '≥')
elif '<' in str(expr):
expr = process_inequality(expr, '<')
elif '≤' in str(expr):
expr = process_inequality(expr, '≤')
try:
i_index = expr.index('i')
if expr[i_index - 1].isdigit():
expr = expr.replace('i', 'j')
except:
pass
exprs.append(expr)
return exprs
We start the process_input
function first with a list of expressions, this list will contain all the rows we have entered as a user, and this expression int(txt_box.index(tk.INSERT)[0]) + 1
will convert the current line count into an integer and then add one to get the last line. After that, we iterate over each line and change each symbol so Python can understand the mathematical expression. For instance, if we entered ² then python will have a problem identifying the symbol, so we change it to ** 2.
We need to separate each variable with the coefficient, using the add star function which adds a multiplication sign after each coefficient. Then we make sure a parenthesis is separated from the coefficient by a multiplication sign. Afterward, we change every symbol to an understandable symbol as before to make sure Sympy and python understand like changing the root symbol to ** 0.5. Then we see if there is an equal sign or inequal sign, we call the functions to move all the terms to one side in order to solve it.
This is the core function of our application which turns the raw mathematical expression into a Python expression to be processed.
It's about time we see the results. So let's do it.
Add the submit function before the plot_expression
function:
import re
# Decides what action to take depending on the input
def submit():
exprs = txt_box.get('1.0', tk.END)
if '=' in exprs:
compute('solve_equality')
elif '<' in exprs or '>' in exprs or '≥' in exprs or '≤' in exprs:
compute('solve_inequality')
else:
if has_symbol(exprs):
plot_expression()
else:
compute('calculate_expression')
Notice that the submit function will call compute which calls process input.
Now add the compute function that will return the result and add it to the text.
# Performs a computation given the operation required then returns the result
def compute(operation):
txt_box.config(state='normal')
expr = process_input()[0]
if operation == 'calculate_expression':
result = f'{round(float(sympy.sympify(expr)), 2)}'
elif operation == 'solve_equality':
exprs = process_input()
solutions = None
if len(exprs) == 1:
solutions = sympy.solve(sympy.sympify(exprs[0]))
if len(solutions) == 1:
solutions = solutions[0]
elif len(exprs) == 2:
solutions = sympy.solve((sympy.sympify(exprs[0]), sympy.sympify(exprs[1])))
result = solutions
elif operation == 'solve_inequality':
symbol = [symbol for symbol in sympy.solve(expr[0], dict=True)[0].items()][0][0]
solution = [symbol for symbol in sympy.solve(expr[0], dict=True)[0].items()][0][1]
result = f'{symbol}{expr[1]}{solution}'
elif operation == 'factor_expression':
result = sympy.sympify(expr).factor()
elif operation == 'expand_expression':
result = sympy.sympify(expr).expand()
elif operation == 'absolute':
result = abs(int(sympy.sympify(expr)))
elif operation == 'limit':
value = ent_limit_value.get()
value = value.replace('∞', str(sympy.S.Infinity))
limit = sympy.Limit(sympy.sympify(expr), sympy.Symbol('x'), sympy.sympify(value)).doit()
result = limit
elif operation == 'derivative':
derivative = sympy.Derivative(sympy.sympify(expr), sympy.Symbol('x')).doit()
result = derivative
elif operation == 'integral':
integral = sympy.Integral(sympy.sympify(expr), sympy.Symbol('x')).doit()
result = integral
elif operation == 'summation':
x = sympy.Symbol('x')
summation = sympy.summation(sympy.sympify(expr), (x, sympy.sympify(ent_summation_start.get()), sympy.sympify(ent_summation_n.get())))
result = summation
txt_box.insert(tk.END, f'\n{result}')
txt_box.config(state='disabled')
Now run the app and try to enter something and see what happens.
It's rewarding, isn't it?
With that said, there a few things left to do.
Let's add the scientific functions panel to our program so when we click on ∑ⅆഽ, it show us the panel.
Open widgets.py and add the following code at the end of the file:
# Scientific mode gui elements
frm_sci = tk.LabelFrame(text='Sci', font=('default', 12))
lbl_trigonometry = tk.Label(master=frm_sci, text='Trigonometry:', font=('default', 12))
lbl_inequality = tk.Label(master=frm_sci, text='Inequality:', width=8, height=1, font=('default', 12))
lbl_calculus = tk.Label(master=frm_sci, text='Calculus:', width=8, height=1, font=('default', 12))
lbl_log = tk.Label(master=frm_sci, text='Log:', width=4, height=1, font=('default', 12))
lbl_other = tk.Label(master=frm_sci, text='Other:', width=4, height=1, font=('default', 12))
frm_trig = tk.Frame(master=frm_sci, pady=8)
deg_type_choice = tk.IntVar()
btn_sin = tk.Button(master=frm_trig, text='sin', width=5, height=1, font=('default', 12), cursor='hand2')
btn_cos = tk.Button(master=frm_trig, text='cos', width=5, height=1, font=('default', 12), cursor='hand2')
btn_tan = tk.Button(master=frm_trig, text='tan', width=5, height=1, font=('default', 12), cursor='hand2')
btn_cot = tk.Button(master=frm_trig, text='cot', width=5, height=1, font=('default', 12), cursor='hand2')
btn_degree = tk.Button(master=frm_trig, text='°', width=5, height=1, font=('default', 12), cursor='hand2')
frm_inequality = tk.Frame(master=frm_sci, pady=8)
btn_greater = tk.Button(master=frm_inequality, text='>', width=5, height=1, font=('default', 12), cursor='hand2')
btn_greater_equal = tk.Button(master=frm_inequality, text='≥', width=5, height=1, font=('default', 12), cursor='hand2')
btn_less = tk.Button(master=frm_inequality, text='<', width=5, height=1, font=('default', 12), cursor='hand2')
btn_less_equal = tk.Button(master=frm_inequality, text='≤', width=5, height=1, font=('default', 12), cursor='hand2')
frm_calculus = tk.Frame(master=frm_sci, pady=8)
btn_limit = tk.Button(master=frm_calculus ,text='Limit:\n x-->', width=5, height=1, font=('default', 12), cursor='hand2')
ent_limit_value = tk.Entry(master=frm_calculus, width=5, font=('default', 12))
btn_insert_infinity = tk.Button(master=frm_calculus, text='∞', width=5, height=1, font=('default', 12), cursor='hand2')
btn_derivative = tk.Button(master=frm_calculus, text='ⅆ', width=5, height=1, font=('default', 12), cursor='hand2')
btn_integral = tk.Button(master=frm_calculus, text='⎰', width=5, height=1, font=('default', 12), cursor='hand2')
frm_log = tk.Frame(master=frm_sci, pady=8)
base_choice = tk.IntVar()
btn_log = tk.Button(master=frm_log, text='log', width=5, height=1, font=('default', 12))
lbl_base = tk.Label(master=frm_log, text='Base:', width=5, height=1, font=('default', 12))
ent_base = tk.Entry(master=frm_log, width=5, font=('default', 12))
btn_e = tk.Button(master=frm_log, text='e', width=5, height=1, font=('default', 12), cursor='hand2')
frm_expand_factor = tk.Frame(master=frm_sci, pady=8)
btn_expand = tk.Button(master=frm_expand_factor, text='Expand', bg='white', width=6, height=1, font=('default', 12), cursor='hand2')
btn_factor = tk.Button(master=frm_expand_factor, text='Factor', bg='white', width=6, height=1, font=('default', 12), cursor='hand2')
frm_other_sci = tk.Frame(master=frm_sci, pady=8)
ent_summation_n = tk.Entry(master=frm_other_sci, width=5, font=('default', 12))
btn_summation = tk.Button(master=frm_other_sci, text='∑', width=5, height=1, font=('default', 12), cursor='hand2')
btn_absolute = tk.Button(master=frm_other_sci, text='| |', width=5, height=1, font=('default', 12), cursor='hand2')
btn_imag = tk.Button(master=frm_other_sci, text='I', width=5, height=1, font=('default', 12), cursor='hand2')
btn_factorial = tk.Button(master=frm_other_sci, text='!', width=5, height=1, font=('default', 12), cursor='hand2')
ent_summation_start = tk.Entry(master=frm_other_sci, width=5, font=('default', 12))
Now to map the functions with buttons, add the code shown below at the end of assign_btn_function
:
for btn in frm_trig.children:
frm_trig.children[btn]['command'] = lambda x=frm_trig.children[btn]: insert_btn_txt(x)
btn_log.configure(command=lambda: insert_btn_txt(btn_log))
btn_e.configure(command=lambda: insert_btn_txt(btn_e))
btn_factorial.configure(command=lambda: insert_btn_txt(btn_factorial))
btn_absolute.configure(command=lambda: compute('absolute'))
btn_imag.configure(command=lambda: insert_btn_txt(btn_imag))
btn_derivative.configure(command=lambda: compute('derivative'))
btn_integral.configure(command=lambda: compute('integral'))
btn_greater.configure(command=lambda: insert_btn_txt(btn_greater))
btn_greater_equal.configure(command=lambda: insert_btn_txt(btn_greater_equal))
btn_less.configure(command=lambda: insert_btn_txt(btn_less))
btn_less_equal.configure(command=lambda: insert_btn_txt(btn_less_equal))
btn_remove.configure(command=lambda: remove_char())
btn_clear_txt.configure(command=lambda: clear_txt())
btn_new_line.configure(command=lambda: insert_new_line())
btn_sci_functions.configure(command=lambda: show_hide_sci_functions())
btn_symbols.configure(command=lambda: show_hide_symbols())
btn_open_image.config(command=lambda: read_from_image(open_file()))
btn_expand.configure(command=lambda: compute('expand_expression'))
btn_factor.configure(command=lambda: compute('factor_expression'))
btn_limit.configure(command=lambda: compute('limit'))
btn_insert_infinity.configure(command=lambda: ent_limit_value.insert(tk.END, '∞'))
btn_summation.configure(command=lambda: compute('summation'))aster=frm_other_sci, text='I', width=5, height=1, font=('default', 12), cursor='hand2')
btn_factorial = tk.Button(master=frm_other_sci, text='!', width=5, height=1, font=('default', 12), cursor='hand2')
ent_summation_start = tk.Entry(master=frm_other_sci, width=5, font=('default', 12))
Add this function to place the elements in the correct order:
# Layout the functions panel (sin cos tan ...)
def place_sci_func_btns():
ent_summation_n.grid(row=0, column=0)
btn_summation.grid(row=1, column=0)
btn_absolute.grid(row=1, column=1)
btn_imag.grid(row=1, column=2)
btn_factorial.grid(row=1, column=3)
ent_summation_start.grid(row=2, column=0)
i = 0
for btn in frm_calculus.children:
frm_calculus.children[btn].grid(row=0, column=i)
i += 1
i = 0
for btn in frm_expand_factor.children:
frm_expand_factor.children[btn].grid(row=0, column=i, padx=4)
i += 1
i = 0
for btn in frm_log.children:
frm_log.children[btn].grid(row=0, column=i)
i += 1
i = 0
for btn in frm_trig.children:
frm_trig.children[btn].grid(row=0, column=i)
i += 1
i = 0
for btn in frm_inequality.children:
frm_inequality.children[btn].grid(row=0, column=i)
i += 1
Update the init_gui_layout
to actually render the gui
components:
# Calls the layout functions above and layout the gui elements
def init_gui_layout():
place_nav_panel()
place_std_btns()
place_sci_func_btns()
frm_txtbox.pack()
frm_nav_buttons.pack()
frm_standard.pack()
lbl_trigonometry.pack()
frm_trig.pack()
lbl_inequality.pack()
frm_inequality.pack()
lbl_calculus.pack()
frm_calculus.pack()
lbl_log.pack()
frm_log.pack()
lbl_other.pack()
frm_other_sci.pack()
frm_expand_factor.pack()
Finally, add this function to functions.py to show and hide the scientific functions panel.
# Triggers the functions panel
# Ex: If it is visible it will hide it
def show_hide_sci_functions():
if frm_sci.winfo_ismapped():
frm_standard.pack()
frm_sci.pack_forget()
frm_symbols.pack_forget()
else:
frm_standard.pack_forget()
frm_symbols.pack_forget()
frm_sci.pack()
Now you should be able to use the scientific functions.
In the same manner, we will program the symbols button.
First, add the symbols widgets which are the letters from a to z:
# Symbols mode gui elements
frm_symbols = tk.LabelFrame(text='Symbols', font=('default', 12))
# Generating buttons from a to z using list comprehension and chr()
symbol_btns = [tk.Button(master=frm_symbols, text=chr(i), width=5, height=2, cursor='hand2', font=('default', 12))
for i in range(97, 123)]
To map the symbol buttons, add the code shown below at the end of assign_btn_function
:
btn_symbols.configure(command=lambda: show_hide_symbols())
for btn in frm_symbols.children:
frm_symbols.children[btn]['command'] = lambda x=frm_symbols.children[btn]: insert_btn_txt(x)
Now add the place_symbols_btns
to place the elements in the correct order:
# Layout the symbols panel
def place_symbols_btns():
i, j = 0, 0
for btn in frm_symbols.children:
frm_symbols.children[btn].grid(row=j, column=i)
i += 1
if i % 10 == 0:
j += 1
i = 0
Finally, update functions.py by adding show_hide_symbols function
to trigger the symbols panel and update show_hide_sci_functions:
# Triggers the symobls panel
# Ex: If it is visible it will hide it
def show_hide_symbols():
if frm_symbols.winfo_ismapped():
frm_standard.pack()
frm_symbols.pack_forget()
frm_sci.pack_forget()
else:
frm_symbols.pack()
frm_standard.pack_forget()
frm_sci.pack_forget()
# Triggers the functions panel
# Ex: If it is visible it will hide it
def show_hide_sci_functions():
if frm_sci.winfo_ismapped():
frm_standard.pack()
frm_sci.pack_forget()
frm_symbols.pack_forget()
else:
frm_standard.pack_forget()
frm_symbols.pack_forget()
frm_sci.pack()
Finally, update init_gui_layout
function in gui_layout.py:
# Calls the layout functions above and layout the gui elements
def init_gui_layout():
place_nav_panel()
place_std_btns()
place_sci_func_btns()
place_symbols_btns()
frm_txtbox.pack()
frm_nav_buttons.pack()
frm_standard.pack()
lbl_trigonometry.pack()
frm_trig.pack()
lbl_inequality.pack()
frm_inequality.pack()
lbl_calculus.pack()
frm_calculus.pack()
lbl_log.pack()
frm_log.pack()
lbl_other.pack()
frm_other_sci.pack()
frm_expand_factor.pack()
Almost done! Now we need to program the last button, which will convert the text on an image to a string that Python can grasp.
Go to function.py and add the following functions:
def open_file():
filetypes = (
('Images files', '*.png'),
('All files', '*.*')
)
file_path = fd.askopenfile(filetypes=filetypes).name
return file_path
# Read text from image given the image path
# If text is clear then returns the text as string
def read_from_image(image_path):
from PIL import Image
from pytesseract import pytesseract
# Defining paths to tesseract.exe
# and the image we would be using
path_to_tesseract = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
# image_path = r"test.png"
# Opening the image & storing it in an image object
img = Image.open(image_path)
# Providing the tesseract executable
# location to pytesseract library
pytesseract.tesseract_cmd = path_to_tesseract
# Passing the image object to image_to_string() function
# This function will extract the text from the image
text = pytesseract.image_to_string(img)
delete_paceholder()
# Displaying the extracted text
txt_box.config(state='normal')
txt_box.insert(tk.END, text[:-1])
txt_box.config(state='disabled')
and add the line below:
from tkinter import filedialog as fd
which will import the file dialog from Tkinter to get the image path.
Wrapping Up
Congratulations, well done! I hope my article was of use. If you have any suggestions, I will be happy to respond to you.
Thank you for your time, take care.
To discover more interesting projects, click here.
History
- 30th October, 2023: Initial version