Code for How to Make a Calculator with Tkinter in Python Tutorial


View on Github

calculator.py

from tkinter import *
import tkinter.font as font
from functools import partial
import ctypes
import json
import re

# so the functions can be used from the math module can be used in the lineedit.
import math

ctypes.windll.shcore.SetProcessDpiAwareness(1)

# Colors
buttonColor = (255, 255, 255)
historyPanelBackground = (255, 255, 255)

# Tkinter Setup
root = Tk()
root.geometry("550x270")
root.title("Calculator")

# Setting icon for the Application
photo = PhotoImage(file = "icon.png")
root.iconphoto(False, photo)

# Loading Font from font name
myFont = font.Font(family='Consolas', size=12)

# Formula Templates
formulas = [
    ['Pythagoras->c', '(({a}**2)+({b}**2))**0.5 ? a=5 & b=5'],
    ['Pythagoras->c**2', '({a}**2)+({b}**2) ? a=5 & b=5'],
    ['pq->(x1, x2)', '-({p}/2) + sqrt(({p}/2)**2 - ({q})), -({p}/2) - sqrt(({p}/2)**2 - ({q})) ? p=-1 & q=-12'],
    ['abc->(x1, x2)', 'quadratic_formula({a}, {b}, {c}) ? a=1 & b=5 & c=6'],
    ['Incline->y', '{m}*{x} + {q} ? m=4 & x=5 & q=6'],
]

# All the history equations are in this list.
history = []
# Where the history file is located.
historyFilePath = 'history.json'
print("Reading history from:", historyFilePath)
# Creating History file if it does not exist.
try:
    with open(historyFilePath, 'x') as fp:
        pass
    print("Created file at:", historyFilePath)
except:
    print('File already exists')

# converting RGB values to HEX
def rgb_to_hex(rgb):
    return "#%02x%02x%02x" % rgb

# Add something to the current calculation
def addSymbol(event=None, symbol=None):

    if symbol == '<':
        entryVariable.set(entryVariable.get()[:-1])
    else:
        entryVariable.set(entryVariable.get()+symbol)

def varChange(*args):
    evaluationString = entryVariable.get().replace(' ', '').split('?')[0]

    print('Before insertion: ',evaluationString)

    if len(entryVariable.get().split('?')) == 2:

        parameters = entryVariable.get().replace(' ', '').split('?')[1]

        for param in parameters.split('&'):
            where, what = param.split('=')
            evaluationString = re.sub('{'+where+'}', what, evaluationString)

    try:
        print('After insertion: ', evaluationString)
        resultLabel.config(text=str(eval(evaluationString)))
    except:
        resultLabel.config(text='Invalid Input')

def saveCurrentInputToHistory(event=None):
    if entryVariable.get() in history:
        return

    history.append(entryVariable.get())

    with open(historyFilePath, 'w') as file:
        file.write(json.dumps(history))

    updateListBox()

def updateListBox(event=None):
    global history

    historyList.delete(0, END)

    try:
        with open(historyFilePath, 'r') as file:
            history = json.loads(file.read())
    except json.decoder.JSONDecodeError:
        print('File does not contain JSON')

    for index, item in enumerate(history):
        historyList.insert(index, item)

def setEntryFromHistory(event=None):
    historyItem = historyList.get(historyList.curselection()[0])
    entryVariable.set(historyItem)

def addFormula(formula=''):
    saveCurrentInputToHistory()
    entryVariable.set(formula)

def quadratic_formula(a, b, c):

    disc = b**2 - 4 * a * c

    x1 = (-b - math.sqrt(disc)) / (2 * a)
    x2 = (-b + math.sqrt(disc)) / (2 * a)

    return(x1, x2)

# Work with Frames to split the window in two parts: the calculator and the History Panel.

# Calculation Panel
calcSide = Frame(root)
calcSide.pack(side=LEFT, fill=BOTH, expand=1)

# Entry Variable for the calculations
entryVariable = StringVar(root, '4/2**2')
entryVariable.trace('w', varChange)

Entry(calcSide, textvariable=entryVariable, font=myFont, borderwidth=0).pack(fill=X, ipady=10, ipadx=10)
resultLabel = Label(calcSide, text='Result', font=myFont, borderwidth=0,anchor="e")
resultLabel.pack(fill=X, ipady=10)

# History Panel
historySide = Frame(root, bg=rgb_to_hex(historyPanelBackground))
historySide.pack(side=LEFT, fill=BOTH, expand=1)

historyTopBar = Frame(historySide)
historyTopBar.pack(fill=X)
Label(historyTopBar, text='History').pack(side=LEFT)
Button(historyTopBar, text='Save Current Input', bg=rgb_to_hex(buttonColor), borderwidth=0, command=saveCurrentInputToHistory).pack(side=RIGHT)

historyList = Listbox(historySide, borderwidth=0)
historyList.pack(fill=BOTH, expand=True)
historyList.bind("<Double-Button-1>", setEntryFromHistory)

# Insert stuff into the history
updateListBox()

# Button Symbols (and their position)
symbols = [
    ['1', '2', '3', '+'],
    ['4', '5', '6', '-'],
    ['7', '8', '9', '/'],
    ['0', '.', '<', '*'],
]

for rowList in symbols:
    # Make a row
    row = Frame(calcSide)
    row.pack(fill=BOTH, expand=True)
    for symbol in rowList:
        # Making and packing the Button
        Button(
            row, text=symbol, command=partial(addSymbol, symbol=symbol),
            font=myFont, bg=rgb_to_hex(buttonColor), borderwidth=0) \
        .pack(side=LEFT, fill=BOTH, expand=1)
        # Change button color each iteration for gradient.
        buttonColor = (buttonColor[0] - 10, buttonColor[1] - 10, buttonColor[1] - 2)


menubar = Menu(root)

filemenu = Menu(menubar, tearoff=0)

# Add all Formulas to the dropdown menu.
for formula in formulas:
    filemenu.add_command(label=formula[0], command=partial(addFormula, formula[1]))

filemenu.add_separator()

# Quit command
filemenu.add_command(label="Exit", command=root.quit)

menubar.add_cascade(menu=filemenu, label='Formulas')

root.config(menu=menubar)


# Call the var change once so it is evaluated withhout actual change.
varChange('foo')

root.mainloop()