Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / artificial-intelligence

Let's Code that Wicked Cool Calculator

5.00/5 (8 votes)
30 Oct 2023CPOL8 min read 12.9K   166  
Enhance your coding skills with a walkthrough on how to create a smart calculator with many features made in Python.
A tutorial on how to use different libraries and combine them all to make a nice fancy calculator. You will learn how to build a smart calculator which you can use to solve your daily math problems by using Tkinter for GUI, sympy and matplotlib for solving math issues, and pytesseract for image-to-text extraction.

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:

Python
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.

Python
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.

Python
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:

Image description

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.

Python
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:

Python
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:

Python
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:

Python
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:

Image description

First, make a new file called functions.py which will handle the math-related functions:

Python
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:

Python
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:

Python
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.

Image description

Image description

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:

Python
#   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:

Python
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:

Python
#   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:

Python
#   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:

Python
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:

Python
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.

Python
#   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:

Python
#   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:

Python
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:

Python
#   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:

Python
#   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.

Python
#   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:

Python
#   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:

Python
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:

Python
#   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:

Python
#   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:

Python
#   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:

Python
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:

Python
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)