Python GUI for Engineers: The Basics

1 Introduction

In my first blog here, we will talk about the basics of crating Graphical User Interface (GUI) programs using Python's FreeSimpleGUI library. We will learn about the five basic steps (and two additional steps) of GUI creation, introducing the most fundamental library functions as we go through. Finally, we will apply these steps to create a simple engineering application.

This article is primarily targeted towards engineers who are familiar with the Python programming language. However, I structured this blog to be simple and detailed enough so that I hope people who are not engineers and people who are not familiar with Python will benefit from this.

1.1 About FreeSimpleGUI

FreeSimpleGUI is a Python library that allows us to create simple GUI programs, even way simpler and faster than using GUI frameworks such as Qt, tkinter and wxPython. This is possible because FreeSimpleGUI wraps long chains of code of the underlying framework to just simple function calls. This I think makes the library perfect for beginners of GUI.

FreeSimpleGUI originated from PySimpleGUI library. Until version 4.60.5, PySimpleGUI was open-source, meaning everyone can use and modify it for free. When PySimpleGUI changed its license to proprietary after this version and started charging for a regular subscription fee to use it, people forked version 4.60.5 and renamed it to FreeSimpleGUI. The current version of FreeSimpleGUI is 5.2.0 and has undergone several enhancements compared to version 4.60.5.

Personally, I have done a couple of programs using this library, like the soil nailed wall analysis program of our office as shown in Figure 1 below.

Figure 1. Soil nailing analysis program of our office, created in Python

To use the FreeSimpleGUI library, install it via the terminal/command prompt using 

pip install freesimplegui freesimpleguiqt freesimpleguiwx freesimpleguiweb

2 The template

Almost all Python programs that use the FreeSimpleGUI library start with a template like the one shown in Figure 2. I suggest that you create a Python file containing this template and then load this file every time you will create a new program.

Figure 2. A typical template for FreeSimpleGUI programs

A typical GUI program is usually created in five steps.

Step 1. We import the FreeSimpleGUI library that we need. We can use one of the four variants available:

  • FreeSimpleGUI uses the tkinter GUI framework.

  • FreeSimpleGUIQt uses the Qt6 GUI framework via PySide2 and PySide6.

  • FreeSimpleGUIWx uses the wxPython GUI framework.

  • FreeSimpleGUIWeb uses the Remi web-based GUI framework.

If the mention of the underlying frameworks seems overwhelming to you, just remember that the look and feel of your program will vary depending on the variant that you use. For example, programs created using FreeSimpleGUIQt may look more modern than when using FreeSimpleGUI. Furthermore, some features in one variant may not be available in other variants. For example, web applications are only possible using FreeSimpleGUIWeb. Hence, my suggestion is to try all of these variants one by one so that you can pick the one that best suits you.

Step 2. We create a list of lists, inside which we code the elements that corresponds to each part of the program and arrange them in the way that we want it to appear. We will see this in action when we do a simple example program in the next section.

Step 3. We place the layout inside the window, and then make the window appear.

Step 4. While the window is open and running, we let the program take some inputs from the user, and then do something useful to those inputs. We will see this in action when we do a simple example program in the next section.

It is important to note here that each user input event stores the results in two variables: event and values.

  • event is the name or ID of the element that was used for user input. For example, if the button [Calculate] is pressed, then the string "Calculate" will be stored to the event variable. If that button [Calculate] has an ID "-BUTTON_CALCULATE-" defined, then the string "-BUTTON_CALCULATE-" will be stored to the event variable instead.
  • values is a dictionary containing the IDs of all user input elements in the program the corresponding values that they contain at the time of user input. 

Step 5. If the user input is to close the program, either by pressing the [Exit] button (event == "Exit"), pressing the [X] button at the top bar (event == sg.WIN_CLOSED), or force closing it through the Task Manager or System Monitor (event is None), then the window should disappear.

3 Example

We will use a simple example just to get you started. More advanced examples will be discussed in the succeeding articles.

Figure 3 shows what the final output of this example will be. A user will enter the length of the beam, uniform load throughout the span, and the type of supports at the ends. The program will then calculate the maximum positive and negative bending moments. In case of no input or wrong inputs, an error popup will appear.


Figure 3. Simple beam moment calculator.

To develop this program, we will go through the five steps that we mentioned earlier.

Step 1: Import. We will use the FreeSimpleGUIQt library here. Take note that we will replace FreeSimpleGUIQt with sg as we go on.

import FreeSimpleGUIQt as sg

Step 2: Layout. We will now create the layout for the program. 

First, notice that the program has four rows of window elements. The layout list could probably contain four lists, as shown in Figure 4. There probably are other ways to do this, possibly with just one row. But this is the most natural way of layouting a typical FreeSimpleGUI program.

Figure 4. Setting up the containers for layout for this example

Next, we fill each row with window elements. Looking at the first row, it probably contains four elements: three text elements, and one input element. In FreeSimpleGUI, text elements are created using the sg.Text(text) function, while input elements are created using the sg.Input() function. This is shown in Figure 5 below.

Figure 5. Filling the first row of layout with elements for this example

The same can be said about the second row as shown in Figure 6 below.

Figure 6. Filling the second row of layout with elements for this example

The third row probably only has two elements: a text element, and a dropdown element. In FreeSimpleGUI, a dropdown element is created using the sg.Combo(list, <value>) function, where list is a list of options that can be chosen from the dropdown, while <value> is the option that is displayed at start. For our example, the dropdown should take four options: ["pinned", "fixed", "propped", "cantilever"], and defaults to "pinned" at program start. See Figure 7.

Figure 7. Filling the third row of layout with elements for this example

The last row probably has two elements as well: a button element, and a text element where we can display the results. Button elements are created in FreeSimpleGUI using the sg.Button(text) element. Right now, the text element contains nothing because we'll put things in it dynamically. See Figure 8.

Figure 8. Filling the last row of layout with elements for this example

Here's the copiable version of the list layout for Step 2.

layout = [
[sg.Text("Beam length"), sg.Text("L ="), sg.Input(), sg.Text("m")],
[sg.Text("Uniform load"), sg.Text("w ="), sg.Input(), sg.Text("kN/m")],
[sg.Text("Supports at ends"), sg.Combo(["pinned", "fixed", "propped", "cantilever"], "pinned")],
[sg.Button("Calculate"), sg.Text("")]
]

Step 3: Display. We will display the window on the screen with title "Simple Beam Calculator" and containing the layout that we've just created in Step 2.

window = sg.Window("Simple beam calculator", layout)

Right now, if we try to run the code, nothing happens. This is because the program has not been set up yet to wait continuously for user input. This is where Step 4 comes in.

Step 4: User input. Let us make the program wait continuously for user input. 

while True :
event, values = window.read()

Right now, if we try to run the code, we will get the window as shown in Figure 9 below.

Figure 9. The window after Step 4 for this example

This is looking good. However, if we try to close this window by pressing the [X] key, we see that the code/IDE still runs and does not terminate. This is because the code right now does not know what to do with the [X] key press; we haven't coded it yet. Step 5 should resolve this.

Step 5: Exit. If the user has pressed the [X] key, then the program should close. In FreeSimpleGUI, the [X] key press is represented by sg.WIN_CLOSED, while the act of closing the active window is done using the window.close() function. The revised while loop is shown below.

# Step 4
while True :
event, values = window.read()

# Step 5: User presses the [X] button.
if event == sg.WIN_CLSED :
break

# Step 5: Close the active window.
window.close()

We can now press the [X] key, and the program will fully close without running in the background.

Now, of course, the program does absolutely nothing right now. If we try to enter values in the input elements and press [Calculate], nothing will happen. This is because we haven't implemented the core functionality of the program. This leads us to Step 6.

Step 6: Functionality. We will code the things that will allow us to extract user input values from the window, do stuff with those values, and then throw some results back to the window.

First, we will assign a key to elements that require user input or will change in value while the program is running. In our case, we have two input elements, one dropdown element, one button element, and one text element where we will display our results. The corresponding FreeSimpleGUI functions for them will be given an additional optional argument key, as shown in the modified layout code below.

# Step 2
layout = [
[sg.Text("Beam length"), sg.Text("L ="), sg.Input(key="-L-"), sg.Text("m")],
[sg.Text("Uniform load"), sg.Text("w ="), sg.Input(key="-W-"), sg.Text("kN/m")],
[sg.Text("Supports at ends"), sg.Combo(["pinned", "fixed", "propped", "cantilever"], "pinned", key="-SUPPORT-")],
[sg.Button("Calculate", key="-CALCULATE-"), sg.Text("", key="-RESULTS-")]
]

Next, we will add the condition when the user presses [Calculate]. Of course, we will use the key for the [Calculate] button to do that.

# Step 5: User presses the [X] button.
if event == sg.WIN_CLOSED :
break
# Step 6: User presses [Calculate] instead.
elif event == "-CALCULATE-" :
...

Inside this newly added conditional, we can now extract the values. Recall that the values of user input are stored in the values variable that we made earlier.

# Step 6: User presses [Calculate] instead.
elif event == "-CALCULATE-" :
try :
# Extract input values.
L = float(values["-L-"])
w = float(values["-W-"])
support_type = values["-SUPPORT-"]
except Exception as e :
# If no input or input not a number, throw an error popup to the user.
sg.popup_error("Wrong inputs.", title="Error")
continue

Three points:

  • User input values are all strings. This means that, since beam length L and uniform load w are decimal numbers, the input values must be converted from strings to floats/doubles. This is done using the float(text) function.
  • Notice that we enclosed the data extraction in a try-except. The program will first try to do the data extraction. If there are no errors, then the program can safely go the next part. If errors occur, e.g. when there are no inputs or the inputs are not decimal numbers, then the program will enter the except part. 
  • The except part is useful for throwing popups telling the user that an error has occurred. In FreeSimpleGUI, popups for errors are done using the sg.popup_error(text, title) functions.

Warning! It is always recommended to place data extraction inside try-except. Otherwise, your program will crash upon encountering errors such as the one shown in Figure 10 below. You've been warned!

Figure 10. Program crashed because there are unhandled errors

Then, we calculate the required moments for each support type as shown.

# Calculate moments
if support_type == "pinned" :
M_p = w*L**
2/8 # maximum positive moment
M_n = 0 # maximum negative moment
elif support_type == "fixed" :
M_p = w*L**
2/24
M_n = w*L**2/12
elif support_type == "propped" :
M_p =
9*w*L**2/128
M_n = w*L**2/8
else : # support_type == "cantilever"
M_p =
0
M_n = w*L**2/2

Finally, we throw these results to the main window so that the user can see it. In FreeSimpleGUI, this is done by accessing the specific element itself (not its value) using its key, then using its update(value) function to update the value contained in this element. In our example, we want the maximum positive and negative bending moments to be displayed at the same time to 2 decimal places. We do

window["-RESULTS-"].update(rf"M+_max = {round(M_p, 2)} kN-m, M-_max- = {round(M_n, 2)} kN-m")

Now, we can use the program to try out different values and calculate things.

Step 7: Customization. At this point, you will notice that the look and feel of our program in Figure 9 is not quite the same as what is shown in Figure 3: the text and inputs are not aligned, the inputs and dropdown are of different sizes, it's just all over the place. This leads us to Step 7, which allows us to add some code to customize the window so that it becomes more pleasing to the user.

Customization is done using the additional optional arguments of each element. For example, if we want to change the color of the text and the background of a text element, the sg.Text() function has optional arguments text_color and background_color, respectively. Hence,

sg.Text("Beam length", text_color='yellow', background_color='green')

leads to the one shown in Figure 11 below.

Figure 11. Customizing text elements

For available customizations for each element, refer to FreeSimpleGUI documentation online. Or, if you have an IDE with tooltips enabled, you can hover your mouse on top of the element function name, and you will see the available customization arguments in the tooltip. Figure 11 below shows an example.

Figure 11. Function tooltips inside the IDE

Since customizations will make the code too long, it is recommended to avoid doing the customization directly inside the list layout. Instead, take the element out of layout, do the customization, assign it to a convenient variable name, and place the variable name inside layout. Not only will this make your code a lot cleaner, but it will also be easier for you to search and modify elements in case of changes or errors.

For our example, layout will be changed to the code below. Notice that in the third row of layout, we added two filler text elements with nothing written inside (text_support_fill and text_support_fill2). This is to force the dropdown element to align with and be of the same size with the input elements above it.

text_length = sg.Text("Beam length", size=(12,0.6))
text_length_label = sg.Text("L =", size=(5,0.6), justification='r')
input_length = sg.Input(key="-L-", size=(12,0.6))
text_length_units = sg.Text("m", size=(4,0.6))

text_load = sg.Text("Uniform load", size=(12,0.6))
text_load_label = sg.Text("w =", size=(5,0.6), justification='r')
input_load = sg.Input(key="-W-", size=(12,0.6))
text_load_units = sg.Text("kN/m", size=(4,0.6))

text_support = sg.Text("Support at ends", size=(12,0.6))
text_support_fill = sg.Text("", size=(5,0.6))
combo_support = sg.Combo(["pinned", "fixed", "propped", "cantilever"], "pinned", size=(12,0.6), key="-SUPPORT-")
text_support_fill2 = sg.Text("", size=(4,0.6))

button_calculate = sg.Button("Calculate", size=(8, 1))
text_result = sg.Text("", key="-RESULTS-")

layout = [
[text_length, text_length_label, input_length, text_length_units],
[text_load, text_load_label, input_load, text_load_units],
[text_support, text_support_fill, combo_support, text_support_fill2],
[button_calculate, text_result]
]

And with that, we have the final program as shown in Figure 3.

4 Source code

The full code for this example can be downloaded from my Github. You may use the code to add more functionality to the program and customize it to your liking. Enjoy!

Comments