One of the main advantages of making applications with Tkinter is that it is very easy to set up a basic GUI with a script of a few lines. As the programs get more complex, it becomes more difficult to separate logically each part, so an organized structure will help us to keep our code clean.
Structuring a Tkinter application
Getting ready
We will take the following program as an example:
from tkinter import * root = Tk() btn = Button(root, text="Click me!") btn.config(command=lambda: print("Hello, Tkinter!")) btn.pack(padx=120, pady=30) root.title("My Tkinter app") root.mainloop()
It creates a main window with a button that prints Hello, Tkinter! in the console each time it is clicked. The button is placed with a padding of 120px in the horizontal axis and 30px in the vertical axis. The last statement starts the main loop, which processes user events and updates the GUI until the main window is destroyed:
You can execute the program and verify that it is working as expected. However, all our variables are defined in the global namespace, and the more widgets you add, the more difficult it becomes to reason about the parts where they are used.
These maintainability issues can be addressed with basic OOP techniques, which are considered good practice in all types of Python programs.
How to do it...
To improve the modularity of our simple program, we will define a class that wraps our global variables:
import tkinter as tk class App(tk.Tk): def __init__(self): super().__init__() self.btn = tk.Button(self, text="Click me!", command=self.say_hello) self.btn.pack(padx=120, pady=30) def say_hello(self): print("Hello, Tkinter!") if __name__ == "__main__": app = App() app.title("My Tkinter app") app.mainloop()
Now, each variable is enclosed in a specific scope, including the command function, which is moved as a separate method.
How it works...
First, we replaced the wildcard import with the import ... as syntax to have better control over our global namespace.
Then, we defined our App class as a Tk subclass, which now is referenced via the tk namespace. To properly initialize the base class, we will call the __init__ method of the Tk class with the built-in super() function. This corresponds to the following lines:
class App(tk.Tk): def __init__(self): super().__init__() # ...
Now, we have a reference to the App instance with the self variable, so we will add all the Button widget as an attribute of our class.
Although it may look overkill for such a simple program, this refactoring will help us to reason about each part, the button instantiation is separated from the callback that gets executed when it is clicked, and the application bootstrapping is moved to the if __name__ == "__main__" block, which is a common practice in executable Python scripts.
We will follow this convention through all the code samples, so you can take this template as the starting point of any larger application.
There's more...
We subclassed the Tk class in our example, but it is also common to subclass other widget classes. We did this to reproduce the same statements that we had before we refactored the code.
However, it may be more convenient to subclass Frame or Toplevel in larger programs, such as those with multiple windows. This is because a Tkinter application should have only one Tk instance, and the system creates one automatically if you instantiate a widget before you create the Tk instance.
Keep in mind that this decision does not affect the structure of our App class since all widget classes have a mainloop method that internally starts the Tk main loop.