Skip to main content

BMI Calculator using Python and Tkinter

Body Mass Index can be calculated using a simple formula kg/m2, where the weight is represented in kilograms (kg) and the height is represented in metres (m). The article presents code to create a simple GUI to calculate BMI based on user input values. The GUI is implemented using tkinter and tkinter.ttk libraries in Python language.

Environment

The code is executed in the following environment,

  • Operating System - Windows 11
  • Python version - 3.10
  • Editor - Visual Studio Code

Disclaimer

The look and feel of the GUI may vary with the underlying Operating System, ie., running the same code presented here may produce different looking user interface depending on the Operating System.

First of all, the different input from the user needs to be identified, to calculate the BMI at least two inputs are required one for weight and one for height. Also, the calculated BMI value will be output back to the user. And, a call to action button to trigger the calculation after input is filled. Considering the above, the GUI design includes 3 labels, 3 entries and 2 buttons. The three labels indicate the type of input/output, the first two entries take input for height and weight, the last entry is set to read-only and used to display the calculated BMI value back to the user. One button for triggering calculation and other to clear the input fields.

The identified widgets will be placed in a grid fashion using the grid() geometry manager of tkinter. The design of the GUI is done using basic blocks and arranged in a format that well represents the final output. The final design of the layout is shown below,


Starting with the imports, only the tkinter related libraries are imported for the project.

from tkinter import *
from tkinter import ttk
from tkinter import messagebox

There is also a global list (initially empty), to keep track of the different entries in the GUI. The entries are identified using index (enums are better, but still).

entries = []

A group of widgets share some of the settings, for example, the labels are all on column 0 of the grid. To minimize the code, each widget is wrapped under a user-defined class for reuse of code. The first group of widgets are the labels, the label class takes the text of the label and grid location for the row (remember all labels are on column 0). To align the labels, right alignment is used (ie., sticky='E') and some padding for spacing in both the directions. Finally, all the labels have the first character underlined, the underlined characters are then bound as shortcuts to force focus on the entry corresponding to the label. The label class is the simplest of all as shown below,

class BMILabel:
    def __init__(self, root, labelText, row):
        self.label = ttk.Label(root, text=labelText, underline=0)
        self.label.grid(row=row, column=0, sticky='E', padx=10, pady=10)

The second group of widgets are the entry fields, which are used to take input from the user. Like labels, all entries are on column 1, only row changes in the grid. Each entry is associated with a string variable and has a constant width. Apart from the constructor, the class also exposes few public methods.

  • getText() method - returns the current text available on the entry
  • clear() method - clears the current text in the entry
  • setRO() method - mark the entry as read-only (used for the output BMI entry)
  • setText() method - clear the current text set the new text on the entry (used for the output BMI entry)
  • focus() method - force the focus on the entry
  • __format__() method - the default method overridden to providing string format functionality to the object

The class definition of the entry widget is shown below,

class BMIEntry:
    def __init__(self, root, row):
        self.textVar = StringVar()
        self.entry = ttk.Entry(root, width=30, textvariable=self.textVar)
        self.entry.grid(row=row, column=1)

    def getText(self):
        return self.entry.get().strip()

    def clear(self):
        self.textVar.set("")
        self.entry.delete(0, END)

    def setRO(self):
        self.entry.config(state="readonly")

    def setText(self, text):
        self.clear()
        self.textVar.set(text)

    def focus(self):
        self.entry.focus_force()

    def __format__(self, __format_spec):
        return format(self.getText, __format_spec)

The final group of widgets are the buttons, both the buttons are on the same column and row, except for the alignment, one button is aligned to the right and another to the left. The alignment is input along with the text to display on the button, a third argument is added to underline the shortcut character. Since both the buttons' text start with 'C' only one button can use the short cut <Alt+C>. The buttons are also given a small padding in the Y direction. The button class exposes one public method bind() which add a callback function when the button is pressed. The class definition for the button is as follows,

class BMIButton:
    def __init__(self, root, buttonText, sticky, shortCutIndex=0):
        self.button = ttk.Button(root, text=buttonText, padding=5)
        self.button.config(underline=shortCutIndex)
        self.button.grid(row=3, column=1, sticky=sticky, pady=10)

    def bind(self, func):
        self.button.config(command=func)

The next step is to add all the widgets to the UI, the reusable code comes in handy here. All the widgets are added using only a few lines of code. First the labels are added with the needed input for the row and text. The entries are added then, the entry for the output is set to read-only here, all the entries are added to the global list and a focus is forced to the first entry field when user launches the application. The buttons are added with a handler.

def addLabels(root):
    BMILabel(root, "Height (cm)", 0)
    BMILabel(root, "Weight (kg)", 1)
    BMILabel(root, "BMI", 2)


def addEntries(root):
    height = BMIEntry(root, 0)
    weight = BMIEntry(root, 1)
    bmi = BMIEntry(root, 2)
    bmi.setRO()
    entries.extend([height, weight, bmi])
    height.focus()


def addButtons(root):
    calculate = BMIButton(root, "Calculate BMI", 'E')
    calculate.bind(calculateBMI)
    clear = BMIButton(root, "Clear", 'W', shortCutIndex=1)
    clear.bind(handleClear)

The shortcuts are configured next, for each underlined word in the UI a short cut is added to either focus on the entry or to click the button. These shortcuts allow the application to be operational without a mouse.

def addShortCuts(root):
    root.bind("<Alt-h>", lambda _: entries[0].focus())
    root.bind("<Alt-w>", lambda _: entries[1].focus())
    root.bind("<Alt-c>", lambda _: calculateBMI())
    root.bind("<Alt-l>", lambda _: handleClear())

The callback functions are defined next, there are two callback functions one to clear the entries and other to calculate and display the result BMI. The clear callback is fairly simple, it iterates through the list of global entries and calls the clear method on each one of them. The BMI calculation is the call-to-action in the application, first the input values are validated to be a proper numerical value, otherwise an exception is thrown which translates to an error message to the user. After input is validated, the standard formula is applied to calculate the BMI value. Additionally, the BMI range is also obtained and displayed to the user.

def handleClear():
    for e in entries:
        e.clear()    


def calculateBMI():
    try:
        heightInCm = float(entries[0].getText())
        weightInKg = float(entries[1].getText())
        if not (0 < weightInKg < 1000):
            raise
        if not (0 < heightInCm < 300):
            raise
    except:
        messagebox.showerror("Invalid Input", "Entered input is invalid")
        return

    bmiValue = (weightInKg/ (heightInCm / 100) ** 2)
    bmi = entries[2]
    bmi.setText(f"{bmiValue:.1f} - {getBMIRange(bmiValue)}")


def getBMIRange(bmiValue):
    if 0.0 < bmiValue < 18.5:
        return "Underweight"
    if 18.5 <= bmiValue < 25.0:
        return "Normal"
    if 25.0 <= bmiValue < 30.0:
        return "Overweight"
    return "Obese"

Plumbing all the logic together, the main function looks like,

def main():
    root = Tk()
    root.title("BMI Calculator")
    root.geometry("320x180")
    root.resizable(False, False)

    addLabels(root)
    addEntries(root)
    addButtons(root)
    addShortCuts(root)
    
    root.mainloop()

if __name__ == "__main__":
    main()    

The below image shows the initial state of the GUI,

After entering input and calculating BMI, the GUI looks like,

In case of error, the following message box is displayed

Thanks for reading. Please leave comments/suggestions if any.

Comments

Popular posts from this blog

Tic-tac-toe game using Python and tkinter

Tic-tac-toe is a popular two player game where each player tries to occupy an empty slot in a 3x3 board until one of them wins or the entire board is filled without any winner. The winner has to occupy three continuous cells in any direction, including the two diagonals. In this article, a version of the tic-tac-toe game is coded using Python and tkinter library.

Using hilite.me API in Python

hilite.me is a light weight website made by Alexander Kojevnikov , used to format source code from various programming languages into formatted HTML snippets which can be added to blogs, articles, and other web pages as needed. I personally use the API to format the source code snippets in this blog, and it has greatly reduced the time taken to complete an article, much thanks to the creator. In this article, an automated way of using the hilite.me API through Python and a simple HTML document is created with formatted code snippets.