Tkinter is a Python module that provides a simple and efficient way to create graphical user interfaces (GUIs) in Python. It is based on the Tk toolkit, which was originally developed for the Tcl programming language.
Using Tkinter, you can create a variety of GUI elements such as buttons, labels, menus, and text boxes, and arrange them in a window using a grid layout. You can also add event handlers to your GUI elements to respond to user input, such as button clicks.
Here is an example of a simple Tkinter GUI that displays a label and a button, and exits when the button is clicked:
1 2 3 4 5 6 7 8 9 10 11 12 13 | import tkinter as tk def on_button_click(): root.destroy() root = tk.Tk() label = tk.Label(root, text="Hello, Tkinter!") label.pack() button = tk.Button(root, text="Click me to exit", command=on_button_click) button.pack() root.mainloop() |
In a GUI application, the graphical interface that includes various widgets (such as buttons, menus, text entries, …) appears to the user and the program waits for the user to interact through an action. This action is called an event. Some of the most common events include:
- A click of a mouse button
- Moving the mouse
- Pressing a key on the keyboard
- Releasing a keyboard key
- A click on the closing cross of the main window
- A timer has elapsed (this event is not triggered by the user itself)
But how does tkinter wait for an event? Simply with an infinite loop. tkinter will only exit this loop when the user quits the program, most often by clicking on the closing cross of the window. Let’s see how it all works.
Event Loop
A minimalist example for a tkinter program is:
1 2 3 4 5 6 7 | import tkinter as tk app = tk.Tk() app.mainloop() print("We leave the program.") # We test when we leave the mainloop |
The mainloop
function is an infinite loop that waits for user input and processes it as necessary. It is an essential part of any Tkinter GUI, as it is responsible for handling events and updating the GUI as needed.
The mainloop
function will not return until the Tkinter window is closed by the user or by calling the destroy
method on the root window. Therefore, the line print("We leave the program.")
will not be executed until the Tkinter window is closed.
If you want to run code after the mainloop
function returns, you can do so by placing it outside the mainloop
function, like this:
1 2 3 4 5 6 7 | import tkinter as tk app = tk.Tk() app.mainloop() print("We leave the program.") # This line will be executed after the mainloop function returns |
After creating an instance of Tk, we call the mainloop method. It should be noted that as long as the tkinter window is displayed, the console does not yet show the text Exit program. The reason is, of course, that the mainloop method contains our infinite loop which will only be output when the window is closed. Then we see our text appear in the console.
This mainloop method is often confusing at first. We do not really understand where to place it, we want to place it at various places in the code, and its operation is often mysterious. To use a tool, it is imperative to understand it in order to reason correctly. Let’s see what the mainloop main loop is hiding.
The main loop is thus an infinite loop with which we can only interact in two ways:
By defining a function (callback) that tkinter will call when a given event occurs. Example: when the mouse moves, call the function that will display the position of the mouse in a Label. A callback is linked to an event with the bind method. It will be discussed in more detail in the next section React to an event.
By creating a timer that will execute a function after a given time. This function is commonly called a callback, which means callback. Example: in 30 seconds, call the function that checks if I have a new email. We create a new timer with the after method. This method will also be discussed in more detail in the Manage Time section.
This event loop could be schematized like this:
1 2 3 4 5 6 7 8 9 10 11 | def mainloop(app): """Synchronized event loop (pseudo code) of tkinter""" x= True while continuer: # Call callbacks registered with * after * if the time is up # For each event (keyboard, mouse, ...): #If we linked the event with * bind *, we call the callback # If you click on the cross to close the window: x= False |
At each loop turn, tkinter performs these operations:
- For each event detected since the last loop turn (such as pressing a keyboard key, moving the mouse, …) tkinter executes any callback related to this event.
- If the elapsed time is greater than the countdown time, the callback is executed.
The implementation of the mainloop
function is similar to the way the mainloop
function in Tkinter works. Here is a more detailed description of what happens in the Tkinter mainloop
function:
- The
mainloop
function enters an infinite loop that waits for events (such as user input or window updates) to occur. - When an event occurs, the
mainloop
function calls any callbacks that are registered to be called after a certain time interval, if the time interval has elapsed. - The
mainloop
function then processes the event by checking if it is associated with a callback using thebind
method. If a callback is associated with the event, the callback is called. - If the event is a window close event (e.g. the user clicks on the close button of the window), the
mainloop
function exits the loop and returns.
Note that the mainloop
function in Tkinter is implemented in C, so the actual implementation may be slightly different from what is described above. However, the general idea is the same.
The tkinter Widgets also internally use the bind method to produce the expected behaviors. So a Button widget will use a left mouse click event to call its own callback (function). This callback will determine if the mouse position was on the button and if so, will call the function provided by the user when creating the button.
The next two sections will illustrate how to link a function to an event and how to use a timer.
React to an event
As we have just seen, it is possible to link a function to an event using the bind (event, callback) method. We will now see a little more detail how to use this method.
Format of events
The first argument of the bind method is an event. It is defined by a string of the form:
“<Type-detail-modifier>”
The most important element is that of the typical medium. This is the type of event that interests us as Button for a mouse click or Key for pressing a key on the keyboard. modifier and detail add additional information about the type of event we are interested in. For example, the modifying part can indicate that one is only interested in a double-click (instead of a simple click) or if it is the combination of the Alt-X keys that is pressed (unlike the X key only). The detail part will allow us to inform if the click of the button must be the right or the left for example.
The modifier and detail parts can be omitted. For the support of a keyboard key, we simplify even more things, since we only fill in the name of the key that interests us with or without the brackets “<>”. Thus for the support of the X key we will simply have the string “x”. On the other hand if one wants the combination of keys ALT-X one will write “<Alt-x>” in square brackets, since one informs a modifier.
Types of events to start:
Type | Description |
---|---|
Button | A click of the mouse. The modifying part can inform if it is a left click (1), a click with the wheel (2) or a right click (3) |
ButtonRelease | The mouse button has been released |
KeyPress (or Key more simply) | The user has pressed a key. This is the type used by default when only the name of a key is given |
KeyRelease | The user released a key |
Enter | The user has moved the mouse in the Widget on which the event is linked |
Leave | The user has moved the mouse out of the Widget on which the event is linked |
There is a long list of event types. It is better to read the documentation to find your happiness. Some examples can be found here. Otherwise we will have a little more details with these links:
Some examples of events
"<KeyPress-s>"
: S button pressed"<Return>"
: Enter pressed down. (Warning! It’s different from “<Enter>” which is a type of event)"<Button-1>"
: Left mouse click"<Double-Button-1>"
: Double left mouse click"<Any-KeyPress>"
: Any key is pressed
The callback
The callback is the function that will be called by tkinter when the event occurs. This function must accept an argument, which will be an event object. So his signature will have to be something like:
1 2 3 4 | def my_callback(event): pass |
The event object allows tkinter to give us information about the event that has been fired in different attributes. We access these attributes as for any object, with the notation event.attribute_name. Here are the different attributes available:
- widget: This is the instance of the widget that triggered this event.
- x, y: the position of the mouse in relation to the window
- x_root, y_root: the position of the mouse in relation to the screen
- char: the character (only for a keyboard event) as a character string
- keysym: the representation of the character (only for a keyboard event)
- keycode: a unique code for a key on the keyboard
- num: the number of the button (only for a mouse event). 1 for left button, 2 for scroll wheel and 3 for right button.
- width, height: the new width and height of the widget (only for a Configure event)
- type: The type of the event, represented by a number. We can find here the correspondence between the number and the type of event
As can be seen, certain event types add additional information. The difference between keysym and char is only seen with special keys like for example F4. By pressing this key, char is “” while keysym is “F4“.
Put into practice
In order to see in action how it all works, we will write a simple program that will tell us where the mouse is when we double click left. We will have to react to the “<Double-Button-1>” event, read the x and y attributes of the event object and display the result in the console:
1 2 3 4 5 6 7 8 9 10 | import tkinter as tk def on_double_click(event): print("Position of the mouse:", event.x, event.y) app = tk.Tk() app.bind("<Double-Button-1>", on_double_click) app.mainloop() |
This code creates a simple Tkinter GUI that binds a function to the “double-click” event. When the user double-clicks the left mouse button (Button-1
) on the Tkinter window, the on_double_click
function is called with an event object as an argument.
The event
object contains information about the event, such as the position of the mouse at the time of the event. In this case, the on_double_click
function prints the position of the mouse (event.x
and event.y
) to the console.
The mainloop
function is called at the end of the code to start the Tkinter event loop. This will cause the program to wait for user input and call the appropriate event handlers as needed.
Sometimes we want to write a code that will only execute after a delay. The classic mistake is to use functions like sleep that suspend the execution of our program for a given time. With our main loop, we can not suspend the program, otherwise tkinter would no longer handle the events and in the eyes of the user, the application would look frozen (frozen). Indeed, if the user clicked with his mouse on a Widget, there would be no visible reaction.
In order to solve this problem, tkinter offers the after widget method (delay, function, * args, ** kwargs) that queues the specified function. Once the delay time expressed in milliseconds has elapsed, tkinter will execute the function function by passing it the arguments args and kwargs provided.
Let’s take a simple example. We want to make a small application with a counter that increments every second. So we just need to create a Label that will contain the text we want to show (the counter), and a function we will call every second to increment our value of 1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import tkinter as tk def increment(): "Increments the counter every second" global counter counter += 1 counter_lbl['text'] = str(counter) app = tk.Tk() counter= 0 counter_lbl = tk.Label(app, text=str(counter), font=("", 16)) counter_lbl.grid(padx=8, pady=8) app.after(1000, increment) app.mainloop() |
This code creates a simple Tkinter GUI that displays a counter that increments every second.
The increment
function is called every second using the after
method, which schedules a function to be called after a certain time interval. The counter
variable is incremented by 1 each time the increment
function is called, and the value of the counter
is displayed in a Label widget (counter_lbl
).
The mainloop
function is called at the end of the code to start the Tkinter event loop. This will cause the program to wait for user input and call the increment
function every second to update the counter.
I used the global keyword online 5 to change the counter value. There is a much cleaner way to achieve this result by using an IntVar object. This will be covered in a future part of the tutorial.
We note that the value of the Label goes from 0 to 1 in 1 second, but then nothing happens. Why ?
The reason is that after placing our function in the queue. After 1 second, tkinter executes the function and removes the function from the queue. In order to execute the function in a repetitive way, the function itself has to get back into the queue using the after method. What gives this modified code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import tkinter as tk def increment(): "Increments the counter every second" global counter counter+= 1 counter_lbl['text'] = str(counter) app.after(1000, increment) app = tk.Tk() counter= 0 counter_lbl = tk.Label(app, text=str(counter), font=("", 16)) counter_lbl.grid(padx=8, pady=8) app.after(1000, increment) app.mainloop() |
Output:
This code is similar to the previous code, with the addition of a call to app.after(1000, increment)
inside the increment
function. This will cause the increment
function to be called again after 1 second (1000 milliseconds), resulting in an infinite loop.
As a result, the counter will continue to increment every second until the Tkinter window is closed.
Note that calling the after
method inside the increment
function causes the increment
function to be called again in a recursive manner, so be careful not to create an infinite loop if you do not want the function to be called indefinitely.
Let’s add a second repetitive action to our program. We will not do anything very complicated. We’ll just add another counter but it’s going a little faster than the first one. It will increment by 1 every 0.8 seconds:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import tkinter as tk def increment(): "Increments the counter every second" global counter counter += 1 counter_lbl['text'] = str(counter) app.after(1000, increment) def increment_quick(): "Increments the counter every 0.8 seconds" global counter_quick counter_quick += 1 counter_quick_lbl['text'] = str(counter_quick) app.after(800, increment_quick) app = tk.Tk() counter= 0 counter_rapide = 0 counter_lbl = tk.Label(app, text=str(counter), font=("", 16)) counter_lbl.grid(padx=8, pady=8) counter_quick_lbl = tk.Label(app, text=str(counter_quick), font=("", 16)) counter_quick_lbl.grid(padx=8, pady=8) app.after(1000, increment) app.after(800, increment_quick) app.mainloop() |
This code creates two counters that increment at different rates using the after
method. The first counter (counter
) increments every second (1000 milliseconds) and the second counter (counter_quick
) increments every 0.8 seconds (800 milliseconds).
The values of the counters are displayed in two separate Label widgets (counter_lbl
and counter_quick_lbl
) and are updated every time the corresponding increment
function is called.
The mainloop
function is called at the end of the code to start the Tkinter event loop. This will cause the program to wait for user input and call the increment
and increment_quick
functions at the specified intervals to update the counters.
The two counters are incremented each at their own pace. It is important to realize that the main loop does not stop running while waiting for an event or that the delay of the function in the waiting list has elapsed. It is because this loop rotates continuously that it gives the illusion that actions occur simultaneously. We get the impression that each counter is an independent program. Excluding these are not Threads or other competitive mechanisms. It is simply a loop that rotates quickly and that sometimes executes the increment code and sometimes the increment_quick.
It is now clearer why any of these functions can not contain a sleep function. If this were the case, the program would stop and tkinter would no longer have the opportunity to continue its main loop. It would therefore no longer have the opportunity to observe the time that elapses and could no longer perform any functions placed in the queue thanks to after.
Reasoning with an event loop
To understand the challenges of an event loop, let’s take an example. Imagine that you want to write a little game guess the number I think.
In a console program, this simplistic game could be written as:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | from random import randint print("guess the number:") secret_number = randint(0, 100) + 1 wins = False while not wins: response = int(input("Choose a number between 1 and 100 inclusive: ")) if secret_number > response: print("The number is bigger") elif secret_number < response: print("The number is smaller") else: wins = True print("Congratulations. You found the number.") |
There is no management of the bad entries made by the user. For the example it is not important because it would weigh down the code.
If we want to transpose this game to tkinter, we immediately see several differences:
- To display a message, one must use a tkinter widget. Probably a label will do the trick.
- To request an entry to the user, we must also use a Widget. An Entry will also do the trick.
- But how do you know when the user has finished entering his number? We could add a Validate button next to the Widget Entry, or we could link the event <pressing the Enter key> with the function that would validate the input of the user.
- The messages “The number is bigger“, “The number is smaller” and “You have found the number. should appear in another Label.
But how to coordinate all this? We will first put in place the elements that the user sees at the beginning of the game:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | from random import randint import tkinter as tk def nombre_choisi(event): "Callback when the player has entered a number." nbre_choisi = int(reponse.get()) reponse.delete(0, tk.END) proposal["text"] = nbre_choisi if secret_number > nbre_choisi: result["text"] = "The number is bigger" elif secret_number < nbre_choisi: result["text"] = "The number is smaller" else: # Remove items that are no longer needed lbl_reponse.destroy() reponse.destroy() # Replace the `proposal` and` result` Labels in the # line below the title proposal.grid_forget() proposal.grid(row=1, column=0) result.grid_forget() result.grid(row=1, column=1) # We configure the label with the desired text, in the # desired font and in the desired color. result.config(text="Congratulations. You found the number.", font=("", 12), fg="green") app = tk.Tk() title = tk.Label(app, text="Guess the number : ", font=("", 16)) title.grid(row=0, columnspan=2, pady=8) secret_number = randint(0, 100) + 1 lbl_reponse = tk.Label(app, text="Choose a number between 1 and 100:") lbl_reponse.grid(row=1, column=0, pady=5, padx=5) reponse = tk.Entry(app) reponse.grid(row=1, column=1, pady=5, padx=5) reponse.bind("<Return>", nombre_choisi) proposal = tk.Label(app, text="") proposal.grid(row=2, column=0, pady=5, padx=5) result = tk.Label(app, text="") result.grid(row=2, column=1, pady=5, padx=5) app.mainloop() |
We have now seen the main differences between the structure of a sequential program and event. If you manage to keep in mind that there is a main loop running and all you can do to interact with your program is to respond to events or use countdowns, you will be able to produce consistent programs using graphical interfaces, making the program more user-friendly.
Object Oriented Programming (OOP) can also help to better structure the code.
Should it be “counter_quick = 0” instead of “counter_rapide = 0” in the example with two counters?
point made by Kristjan I found correct. This wa only prg i tried on this that didnt work until I saw comment