|
| 1 | +# Calculator with tkinter |
| 2 | +**How to make a simple calculator with tkinter which has a history function and variable insertion.** |
| 3 | + |
| 4 | +## Idea |
| 5 | +In this tutorial, we are going to make a calculator with Tkinter the built-in GUI Library in python. We are going to make a 3 by 3 Grid with buttons that represent the possible inputs, a live result showing system, a history of calculations feature, and variable insertion. |
| 6 | + |
| 7 | +So let's get right into it. |
| 8 | + |
| 9 | +## Imports |
| 10 | +As always we start with the imports. Because we make the UI with tkinter we need to import it, we import everything from `tkinter` so we don't have to write less code later on. We also import the font module from tkinter so we can change the fonts on our elements later. |
| 11 | + |
| 12 | +We continue by getting the `partial` function from `functools`. This is a genius function that excepts another function as a first argument and some args and kwargs and it will return a reference to this function with those arguments. This is especially useful when we want to insert one of our functions to a command argument of a button or a key binding. |
| 13 | + |
| 14 | +In the next line, we import `ctypes`, which allows us to enable high dpi, making our window look way sharper. This is done with the function call in the last line of this sections code block. |
| 15 | + |
| 16 | +Because we'll save our history in a JSON file we import the `json` module to parse json and stringify lists. |
| 17 | + |
| 18 | +We also need `re` the built-in regex module for our variable insertion feature. |
| 19 | + |
| 20 | +Last but not least we get math, but we do not don't need it we just import it so it can be used in the line edit of the calculator. |
| 21 | + |
| 22 | +```python |
| 23 | +from tkinter import * |
| 24 | +import tkinter.font as font |
| 25 | +from functools import partial |
| 26 | +import ctypes |
| 27 | +import json |
| 28 | +import re |
| 29 | + |
| 30 | +# so the functions that can be used from the math module can be used in the line edit. |
| 31 | +import math |
| 32 | + |
| 33 | +ctypes.windll.shcore.SetProcessDpiAwareness(1) |
| 34 | +``` |
| 35 | + |
| 36 | +## Variables and tkinter setup |
| 37 | + |
| 38 | +Next up we make some variables and we set up the tkinter. |
| 39 | + |
| 40 | +The first two variables are just colors for our buttons and for the background of the history panel. |
| 41 | + |
| 42 | +Next, we set up tkinter by calling its Tk class and saving this object in the `root` variable. We then set the window dimensions with the `geometry()` method and the window title with `title()`. |
| 43 | + |
| 44 | +We then import an image from our directory which we can set to be the icon of our program. |
| 45 | + |
| 46 | +Last but not least we import the font `Consolas` in the size 12. We do this with the `Font` class from the font module of tkinter. |
| 47 | + |
| 48 | +```python |
| 49 | +# Colors |
| 50 | +buttonColor = (255, 255, 255) |
| 51 | +historyPanelBackground = (255, 255, 255) |
| 52 | + |
| 53 | +# Tkinter Setup |
| 54 | +root = Tk() |
| 55 | +root.geometry("550x270") |
| 56 | +root.title("Calculator") |
| 57 | + |
| 58 | +# Setting icon for the Application |
| 59 | +photo = PhotoImage(file = "icon.png") |
| 60 | +root.iconphoto(False, photo) |
| 61 | + |
| 62 | +# Loading Font from font name |
| 63 | +myFont = font.Font(family='Consolas', size=12) |
| 64 | +``` |
| 65 | + |
| 66 | +## Formulas and variable insertions |
| 67 | + |
| 68 | +Now I am going to explain the variable insertion feature or at least I will try to! So the idea is that we can have a space after our equations where we declare variables that are inserted into the equation by placeholders. Let us look at this in a concrete example. If we type text below into the line edit. |
| 69 | + |
| 70 | +``` |
| 71 | +{a} * {b} ? a=7 & b=3 |
| 72 | +``` |
| 73 | + |
| 74 | +... should net us this result ... |
| 75 | + |
| 76 | +``` |
| 77 | +21 |
| 78 | +``` |
| 79 | +... because where `a` was the 7 will be placed and where the `b` is we will insert the 3 and after that the equation will be evaluated and this will be `21`. |
| 80 | + |
| 81 | +We will later look at how this is done in practice. |
| 82 | + |
| 83 | +Below we also have a list of formulas that can be inserted into the line edit. We will make them pickable from a Menu. |
| 84 | + |
| 85 | +```python |
| 86 | +# Formula Templates |
| 87 | +formulas = [ |
| 88 | + ['Pythagoras->c', '(({a}**2)+({b}**2))**0.5 ? a=5 & b=5'], |
| 89 | + ['Pythagoras->c**2', '({a}**2)+({b}**2) ? a=5 & b=5'], |
| 90 | + ['pq->(x1, x2)', '-({p}/2) + sqrt(({p}/2)**2 - ({q})), -({p}/2) - sqrt(({p}/2)**2 - ({q})) ? p=-1 & q=-12'], |
| 91 | + ['abc->(x1, x2)', 'quadratic_formula({a}, {b}, {c}) ? a=1 & b=5 & c=6'], |
| 92 | + ['Incline->y', '{m}{x} + {q} ? m=4 & x=5 & q=6'], |
| 93 | +] |
| 94 | +``` |
| 95 | + |
| 96 | +## History setup |
| 97 | + |
| 98 | +Next, we set up the history feature. |
| 99 | + |
| 100 | +We start by declaring a list that will hold our history items. Then we have a variable that holds the location of the `history.json` file. |
| 101 | + |
| 102 | +In the end, we have a try and except block, where there is an attempt to make the file at the specified location. This is just done so the file exists in all cases. |
| 103 | + |
| 104 | +```python |
| 105 | +# All the history equations are in this list. |
| 106 | +history = [] |
| 107 | + |
| 108 | +# Where the history file is located. |
| 109 | +historyFilePath = 'history.json' |
| 110 | + |
| 111 | +print("Reading history from: ",historyFilePath ) |
| 112 | + |
| 113 | +# Creating History file if it does not exist. |
| 114 | +try: |
| 115 | + with open(historyFilePath, 'x') as fp: |
| 116 | + pass |
| 117 | + print("Created file at:", historyFilePath) |
| 118 | +except: |
| 119 | + print('File already exists') |
| 120 | +``` |
| 121 | + |
| 122 | +## RGB to hex and math function. |
| 123 | + |
| 124 | +Now we talk about two functions that hold only minor importance. |
| 125 | + |
| 126 | +The first one simply converts RGB colors to hex colors because tkinter only allows color names and hex colors. |
| 127 | + |
| 128 | +The second one is a custom math function that can be used in the line edit. |
| 129 | + |
| 130 | +```python |
| 131 | +# converting RGB values to HEX |
| 132 | +def rgb_to_hex(rgb): |
| 133 | + return "#%02x%02x%02x" % rgb |
| 134 | + |
| 135 | +def quadratic_formula(a, b, c): |
| 136 | + disc = b**2 - 4 * a * c |
| 137 | + |
| 138 | + x1 = (-b - math.sqrt(disc)) / (2 * a) |
| 139 | + x2 = (-b + math.sqrt(disc)) / (2 * a) |
| 140 | + |
| 141 | + return(x1, x2) |
| 142 | +``` |
| 143 | + |
| 144 | +## Helper functions |
| 145 | + |
| 146 | +Now we will go over the several helper functions that are needed to make the program work. |
| 147 | + |
| 148 | +### `addSymbol` |
| 149 | + |
| 150 | +This function will be called from the buttons to insert operators like `*` or `+` and numbers into the line edit. That's where the symbol parameter comes in. Now if the symbol is `<` we won't append it, we will shorten the current input. We actually change the String Variable which holds the current input. |
| 151 | + |
| 152 | +```python |
| 153 | +# Add something to the current calculation |
| 154 | +def addSymbol(event=None, symbol=None): |
| 155 | + |
| 156 | + if symbol == '<': |
| 157 | + entryVariable.set(entryVariable.get()[:-1]) |
| 158 | + else: |
| 159 | + entryVariable.set(entryVariable.get()+symbol) |
| 160 | +``` |
| 161 | + |
| 162 | +### `varChange` |
| 163 | + |
| 164 | +This function will be connected to change events on the input variable. In this function, we will also evaluate the equation and insert it into the result label. |
| 165 | + |
| 166 | +Earlier we took a look at how the variable insertion feature functioned and now we look at how we do this in practice. |
| 167 | + |
| 168 | +As you see we first split the input string by the `?` and then we save this to the `evaluationString` variable. |
| 169 | + |
| 170 | +After that, we check if the input string split by `?` would result in a list with two items. If that's the case we know that there are insertion variables. Then we get this side of the string and we loop over another splitted version of this where the separator is `&`. There we modify the `evaluationString` with these variables. |
| 171 | + |
| 172 | +In either case, we will try to insert the evaluated value into the result label. Now it could be that this doesn't work because the input is invalid so we cover that case. |
| 173 | + |
| 174 | +```python |
| 175 | +def varChange(*args): |
| 176 | + evaluationString = entryVariable.get().replace(' ', '').split('?')[0] |
| 177 | + |
| 178 | + print('Before insertion: ',evaluationString) |
| 179 | + |
| 180 | + if len(entryVariable.get().split('?')) == 2: |
| 181 | + |
| 182 | + parameters = entryVariable.get().replace(' ', '').split('?')[1] |
| 183 | + |
| 184 | + for param in parameters.split('&'): |
| 185 | + where, what = param.split('=') |
| 186 | + evaluationString = re.sub('{'+where+'}', what, evaluationString) |
| 187 | + |
| 188 | + try: |
| 189 | + print('After insertion: ', evaluationString) |
| 190 | + resultLabel.config(text=str(eval(evaluationString))) |
| 191 | + except: |
| 192 | + resultLabel.config(text='Invalid Input') |
| 193 | +``` |
| 194 | + |
| 195 | +### `saveCurrentInputToHistory` |
| 196 | + |
| 197 | +This function simply saves the current line edit input to the history file. We first check if the value is already in the list so we don't have duplicates. We then save the history list to the history file. Here we use the `json.dump()` function. and in the end, we call the `updateListBox` function, which will be explained later. |
| 198 | + |
| 199 | +```python |
| 200 | +def saveCurrentInputToHistory(event=None): |
| 201 | + if entryVariable.get() in history: |
| 202 | + return |
| 203 | + |
| 204 | + history.append(entryVariable.get()) |
| 205 | + |
| 206 | + with open(historyFilePath, 'w') as file: |
| 207 | + file.write(json.dumps(history)) |
| 208 | + |
| 209 | + updateListBox() |
| 210 | +``` |
| 211 | + |
| 212 | +### `updateListBox` |
| 213 | + |
| 214 | +This function will delete all the contents of the history list and display them again, that's why need the history variable here. We then delete all the stuff in the list with the `delete(start, end)` method. Then we get open the history file and get the json from there. And in the end, we loop over the history list and insert those values into the history list. |
| 215 | + |
| 216 | +```python |
| 217 | +def updateListBox(event=None): |
| 218 | + global history |
| 219 | + |
| 220 | + historyList.delete(0, END) |
| 221 | + |
| 222 | + try: |
| 223 | + with open(historyFilePath, 'r') as file: |
| 224 | + history = json.loads(file.read()) |
| 225 | + except json.decoder.JSONDecodeError: |
| 226 | + print('File does not contain JSON') |
| 227 | + |
| 228 | + for index, item in enumerate(history): |
| 229 | + historyList.insert(index, item) |
| 230 | +``` |
| 231 | + |
| 232 | +### `setEntryFromHistory` and `addFormula` |
| 233 | + |
| 234 | +These two functions have simple jobs. |
| 235 | + |
| 236 | +The `setEntryFromHistory` enables us to click on a list item and this item will then be inserted into the line edit. |
| 237 | + |
| 238 | +The `addFormula` function will do the same just for the formulas chosen from the dropdownmenu. |
| 239 | + |
| 240 | +```python |
| 241 | +def setEntryFromHistory(event=None): |
| 242 | + historyItem = historyList.get(historyList.curselection()[0]) |
| 243 | + entryVariable.set(historyItem) |
| 244 | + |
| 245 | +def addFormula(formula=''): |
| 246 | + saveCurrentInputToHistory() |
| 247 | + entryVariable.set(formula) |
| 248 | +``` |
| 249 | + |
| 250 | +## UI |
| 251 | + |
| 252 | +Now we will make the UI. I wont go into detail to much. There are some different to take away from the code. |
| 253 | + |
| 254 | +The layout is done with the `pack` method of all widgets, and make the two-column setup by using `Frame`'s |
| 255 | + |
| 256 | +```python |
| 257 | +# Work with Frames to split the window in two parts: the calculator and the History Panel. |
| 258 | + |
| 259 | +# Calculation Panel |
| 260 | +calcSide = Frame(root) |
| 261 | +calcSide.pack(side=LEFT, fill=BOTH, expand=1) |
| 262 | + |
| 263 | +# Entry Variable for the calculations |
| 264 | +entryVariable = StringVar(root, '4/2**2') |
| 265 | +entryVariable.trace('w', varChange) |
| 266 | + |
| 267 | +Entry(calcSide, textvariable=entryVariable, font=myFont, borderwidth=0).pack(fill=X, ipady=10, ipadx=10) |
| 268 | +resultLabel = Label(calcSide, text='Result', font=myFont, borderwidth=0,anchor="e") |
| 269 | +resultLabel.pack(fill=X, ipady=10) |
| 270 | + |
| 271 | +# History Panel |
| 272 | +historySide = Frame(root, bg=rgb_to_hex(historyPanelBackground)) |
| 273 | +historySide.pack(side=LEFT, fill=BOTH, expand=1) |
| 274 | + |
| 275 | +historyTopBar = Frame(historySide) |
| 276 | +historyTopBar.pack(fill=X) |
| 277 | +Label(historyTopBar, text='History').pack(side=LEFT) |
| 278 | +Button(historyTopBar, text='Save Current Input', bg=rgb_to_hex(buttonColor), borderwidth=0, command=saveCurrentInputToHistory).pack(side=RIGHT) |
| 279 | + |
| 280 | +historyList = Listbox(historySide, borderwidth=0) |
| 281 | +historyList.pack(fill=BOTH, expand=True) |
| 282 | +historyList.bind("<Double-Button-1>", setEntryFromHistory) |
| 283 | +``` |
| 284 | + |
| 285 | +We also call this function so the list updates on startup. |
| 286 | +```python |
| 287 | +# Insert stuff into the history |
| 288 | +updateListBox() |
| 289 | +``` |
| 290 | +Below you see how the Buttons are made. We first define a list with other lists in it where the symbols on the button are layed out. |
| 291 | + |
| 292 | +Then we loop over this first list and make a new frame for each row. we continue by looping over these inner lists and generating buttons with the given symbols. |
| 293 | + |
| 294 | +We set the background color on these buttons to our button color and then we lower every number from the button color tuple, this will give us a nice gradient for the buttons. |
| 295 | + |
| 296 | +```python |
| 297 | +# Button Symbols (and their position) |
| 298 | +symbols = [ |
| 299 | + ['1', '2', '3', '+'], |
| 300 | + ['4', '5', '6', '-'], |
| 301 | + ['7', '8', '9', '/'], |
| 302 | + ['0', '.', '<', '*'], |
| 303 | +] |
| 304 | + |
| 305 | +for rowList in symbols: |
| 306 | + |
| 307 | + # Make a row |
| 308 | + row = Frame(calcSide) |
| 309 | + row.pack(fill=BOTH, expand=True) |
| 310 | + for symbol in rowList: |
| 311 | + |
| 312 | + # Making and packing the Button |
| 313 | + Button( |
| 314 | + row, |
| 315 | + text=symbol, |
| 316 | + command=partial(addSymbol, symbol=symbol), |
| 317 | + font=myFont, |
| 318 | + bg=rgb_to_hex(buttonColor), |
| 319 | + borderwidth=0 |
| 320 | + ).pack( |
| 321 | + side=LEFT, |
| 322 | + fill=BOTH, |
| 323 | + expand=1 |
| 324 | + ) |
| 325 | + |
| 326 | + # Change button color each iteration for gradient. |
| 327 | + buttonColor = (buttonColor[0] - 10, buttonColor[1] - 10, buttonColor[1] - 2) |
| 328 | +``` |
| 329 | + |
| 330 | +We also make a menu where we have all our formulas ready to be inserted. |
| 331 | + |
| 332 | +```python |
| 333 | +menubar = Menu(root) |
| 334 | + |
| 335 | +filemenu = Menu(menubar, tearoff=0) |
| 336 | + |
| 337 | +# Add all Formulas to the dropdown menu. |
| 338 | +for formula in formulas: |
| 339 | + filemenu.add_command(label=formula[0], command=partial(addFormula, formula[1])) |
| 340 | + |
| 341 | +filemenu.add_separator() |
| 342 | + |
| 343 | +# Quit command |
| 344 | +filemenu.add_command(label="Exit", command=root.quit) |
| 345 | + |
| 346 | +menubar.add_cascade(menu=filemenu, label='Formulas') |
| 347 | + |
| 348 | +root.config(menu=menubar) |
| 349 | +``` |
| 350 | + |
| 351 | +Now, in the end, we call the `valueChange` function so the input is evaluated on startup and we call the `mainloop()` method. |
| 352 | + |
| 353 | +```python |
| 354 | +# Call the var change once so it is evaluated without actual change. |
| 355 | +varChange('foo') |
| 356 | + |
| 357 | +root.mainloop() |
| 358 | +``` |
| 359 | + |
| 360 | +## Showcase |
| 361 | + |
| 362 | +Below you see a little showcase of how the calculator works. |
| 363 | + |
| 364 | + |
| 365 | + |
| 366 | +## Conclusion |
| 367 | + |
| 368 | +Excellent! You have successfully created a calculator using Python code! See how you can add more features to this program such as more formulas or a converter for different things like inches to centimers |
| 369 | +## Conclusion |
| 370 | + |
| 371 | +Excellent! You have successfully created a **...** using Python code! See how you can add more features to this program such as **...** . |
0 commit comments