# 15. GUI and Event Driven Programming ## 15.1. Graphical User Interfaces A **graphical user interface** (GUI) allows a user to interact with a computer program using a pointing device that manipulates small pictures on a computer screen. The small pictures are called `icons` or `widgets`. Various types of pointing devices can be used, such as a mouse, a stylus pen, or a human finger on a touch screen. We refer to programs that use a **graphical user interface** as “GUI programs.” A GUI program is very different from a program that uses a **command line interface** which receives user input from typed characters on a keyboard. Typically programs that use a **command line interface** perform a series of tasks in a predetermined order and then terminate. However, a GUI program creates the `icons` and `widgets` that are displayed to a user and then it simply waits for the user to interact with them. The order that tasks are performed by the program is under the user’s control – not the program’s control! This means a GUI program must keep track of the “state” of its processing and respond correctly to user commands that are given in any order the user chooses. This style of programming is called “event driven programming.” In fact, by definition, all *GUI programs* are *event-driven programs*. ## 15.2. GUI Programming An GUI program has the following structure: * Create the icons and widgets that are displayed to a user and organize them inside a screen window. * Define functions that will process user and application events. * Associate specific user events with specific functions. * Start an infinite event-loop that processes user events. When a user event happens, the event-loop calls the function associated with that event. A GUI program’s interface is composed of `widgets` displayed in a window. Your computer’s **operating system** controls the creation and manipulation of windows on your computer’s display screen. The operating system also controls the pointing devices on your computer, such as a mouse or a touch screen. Therefore, your computer’s operating system is what recognizes events that happen in a window. Your operating system sends events to your program in the order they are generated by a user. Your program’s event-loop responds to these events. All GUI programs have the same event-loop, so there is an event-loop provided for you and it looks something like this: ```python while True: # Get the next event from the operating system event = get_next_event() # Get the function that is assigned to handle this event a_function_to_handle_the_event = event-handlers[event] # If a function has been assigned to handle this event, call the function if a_function_to_handle_the_event: a_function_to_handle_the_event() # Call the event-handler function # Stop processing events if the user gives a command to stop the application if window_needs_to_close: break # out of the event-loop ``` Again, you do not implement an event-loop in a GUI program. The event loop has already been written for you. You make this event-loop work by associating a function (which is called an `event-handler` or a `callback function`) to a specific event. We will show you how to do this in a few lessons. First, let’s learn how to create a *GUI interface* which is the `widgets` a user sees when a GUI program runs. ## 15.3. GUI Programming Options Python does not implement GUI, event-driven-programming in its core functionality. GUI programming is implemented using imported modules which are often referred to as “toolkits.” Anyone can implement external modules that facilitate GUI programming, and many people have. Therefore you have many options available to you for GUI programming. A partial list of options can be found at
. The following lessons explain how to use the `Tkinter` toolkit to create GUI programs. Once you understand how GUI programming works, you should be able to learn how to use any of the other available toolkits without much difficulty. ## 15.4. TKinter `TKinter` is an abbreviation for “TK interface”. “TK” is a platform independent, customizable, and configurable GUI library. The Python module `TKinter` allows Python programs to use the TK libraries. An overview of TK can be found at
## 15.5. Tkinter Pre-programmed Interfaces `Tkinter` provides a set of standard GUI dialog boxes that can be used with minimal programming. These are described in the next lesson. (A *dialog box* is a small window on a computer screen in which a user is prompted to provide information or select commands.) ## 15.6. Tkinter Custom Interfaces `Tkinter` also provides the functionality to create any user interface imaginable. To create a custom GUI program you basically do five things: * Create instances of the widgets you want in your interface. * Define the layout of the widgets (i.e., the location and size of each widget). * Create functions that will perform your desired actions on user generated events. * Connect your functions to specific user events. * Start a GUI event-loop. Each of these tasks are explain in detail in the following lessons. Note: All coding examples in these lessons assume you are using Python 3.5 or greater. ## 15.7. Hello World Many programming languages are introduced to new users by showing them how to display “Hello world!” on the screen. This is considered to be the simplest possible program you can write in the language. In that spirit, here is a GUI program that displays “Hello World!: ```python import tkinter as tk from tkinter import ttk # Create the application window window = tk.Tk() # Create the user interface my_label = ttk.Label(window, text="Hello World!") my_label.grid(row=1, column=1) # Start the GUI event loop window.mainloop() ``` ## 15.8. Tkinter Standard Dialog Boxes There are many common programming tasks that can be performed using pre-defined GUI dialog boxes. The following discussion describes these dialog boxes and provides some simple examples. You can refer to the Python documentation for additional optional parameters. ### 15.8.1. Messages A `messagebox` can display information to a user. There are three variations on these dialog boxes based on the type of message you want to display. The functions’ first parameter gives a name for the dialog box which is displayed in the window’s header. The second parameter is the text of the message. The functions return a string which is typically ignored. ```python from tkinter import messagebox messagebox.showinfo("Information","Informative message") messagebox.showerror("Error", "Error message") messagebox.showwarning("Warning","Warning message") ``` ### 15.8.2. Yes/No Questions The tkinter `messagebox` object also allows you to ask a user simple yes/no type questions and varies the button names based on the type of question. These functions are: ```python from tkinter import messagebox answer = messagebox.askokcancel("Question","Do you want to open this file?") answer = messagebox.askretrycancel("Question", "Do you want to try that again?") answer = messagebox.askyesno("Question","Do you like Python?") answer = messagebox.askyesnocancel("Question", "Continue playing?") ``` The return value is a Boolean, True or False, answer to the question. If “cancel” is an option and the user selects the “cancel” button, `None` is returned. ### 15.8.3. Single Value Data Entry If you want to ask the user for a single data value, either a string, integer, or floating point value, you can use a `simpledialog` object. A user can enter the requested value and hit “OK”, which will return the entered value. If the user hits “Cancel,” then `None` is returned. ```python import tkinter as tk from tkinter import simpledialog application_window = tk.Tk() answer = simpledialog.askstring("Input", "What is your first name?", parent=application_window) if answer is not None: print("Your first name is ", answer) else: print("You don't have a first name?") answer = simpledialog.askinteger("Input", "What is your age?", parent=application_window, minvalue=0, maxvalue=100) if answer is not None: print("Your age is ", answer) else: print("You don't have an age?") answer = simpledialog.askfloat("Input", "What is your salary?", parent=application_window, minvalue=0.0, maxvalue=100000.0) if answer is not None: print("Your salary is ", answer) else: print("You don't have a salary?") ``` ### 15.8.4. File Chooser A common task is to select the names of folders and files on a storage device. This can be accomplished using a `filedialog` object. Note that these commands do not save or load a file. They simply allow a user to select a file. Once you have the file name, you can open, process, and close the file using appropriate Python code. These dialog boxes always return you a “fully qualified file name” that includes a full path to the file. Also note that if a user is allowed to select multiple files, the return value is a tuple that contains all of the selected files. If a user cancels the dialog box, the returned value is an empty string. ```python import tkinter as tk from tkinter import filedialog import os application_window = tk.Tk() # Build a list of tuples for each file type the file dialog should display my_filetypes = [('all files', '.*'), ('text files', '.txt')] # Ask the user to select a folder. answer = filedialog.askdirectory(parent=application_window, initialdir=os.getcwd(), title="Please select a folder:") # Ask the user to select a single file name. answer = filedialog.askopenfilename(parent=application_window, initialdir=os.getcwd(), title="Please select a file:", filetypes=my_filetypes) # Ask the user to select a one or more file names. answer = filedialog.askopenfilenames(parent=application_window, initialdir=os.getcwd(), title="Please select one or more files:", filetypes=my_filetypes) # Ask the user to select a single file name for saving. answer = filedialog.asksaveasfilename(parent=application_window, initialdir=os.getcwd(), title="Please select a file name for saving:", filetypes=my_filetypes) ``` ### 15.8.5. Color Chooser Tkinter includes a nice dialog box for choosing colors. You provide it with a parent window and an initial color, and it returns a color in two different specifications: 1) a RGB value as a tuple, such as `(255, 0, 0)` which represents red, and 2) a hexadecimal string used in web pages, such as `"#FF0000"` which also represents red. If the user cancels the operation, the return values are `None` and `None`. ```python from tkinter import colorchooser rgb_color, web_color = colorchooser.askcolor(parent=application_window, initialcolor=(255, 0, 0)) ``` ## 15.9. GUI Widgets As we discussed in the introduction, a GUI program allows a user to interact with a computer program using a pointing device that manipulates small pictures called `icons` or `widgets`. The first task of a GUI program is to create the widgets needed for a program’s interface. Each widget is designed for specific purposes and your program will be more user friendly if you use each widget according to its intended purpose. Widgets are basically images on a computer screen and they have a “look-and-feel” depending on the details of how the image is drawn. The “look-and-feel” of a widget is typically controlled by the operating system. For example, GUI programs on a Macintosh computer typically look different from programs on a Microsoft Windows computer. The `tkinter` module implements two versions of widgets: one is “generic,” which makes widgets look the same regardless of what computer your program is running on, and the other implements widgets that emulate a computer’s “look-and-feel”. How you import the `tkinter` module determines which widgets are defined. Using the import statements shown below, the standard convention uses the name `tk` to access the “generic” widgets and the name `ttk` to access the stylized, “look-and-feel” widgets. You always need to import the `tk` functionality because that allows you to create an application window. You can import the `ttk` functionality if you want “look-and-feel” widgets. You can inter-mix the `tk` and `ttk` widgets in an interface if you so choose. ```python # To use the "generic" widgets import tkinter as tk # To use the stylized, "look-and-feel" widgets from tkinter import ttk ``` The following two charts list the standard, pre-defined widgets in the `tkinter` module. The following widgets are used for user input. In some cases you have a choice between the `tk` and `ttk` versions. In other cases you must use the `tk` version because the equivalent `ttk` versions don’t exist. | Widget | Purpose | | --- | --- | | `tk.Button`, `ttk.Button` | Execute a specific task; a “do this now” command. | | `tk.Menu` | Implements toplevel, pulldown, and popup menus. | | `ttk.Menubutton` | Displays popup or pulldown menu items when activated. | | `tk.OptionMenu` | Creates a popup menu, and a button to display it. | | `tk.Entry`, `ttk.Entry` | Enter one line of text. | | `tk.Text` | Display and edit formatted text, possibly with multiple lines. | | `tk.Checkbutton`, `ttk.Checkbutton` | Set on-off, True-False selections. | | `tk.Radiobutton`, `ttk.Radiobutton` | Allow one-of-many selections. | | `tk.Listbox` | Choose one or more alternatives from a list. | | `ttk.Combobox` | Combines a text field with a pop-down list of values. | | `tk.Scale`, `ttk.Scale` | Select a numerical value by moving a “slider” along a scale. | The following figure shows examples of these widgets. You can download and run this python program, [all\_user\_input\_widgets.py](../_static/Programs/all_user_input_widgets.py), to interact with the widgets.  Examples of user input widgets The following `widgets` display information to a user, but have no user interaction: | Widget | Purpose | | --- | --- | | `tk.Label`, `ttk.Label` | Display static text or an image. | | `tk.Message` | Display static multi-line text. | | `ttk.Separator` | Displays a horizontal or vertical separator bar. | | `ttk.Progressbar` | Shows the status of a long-running operation. | | `ttk.Treeview` | Displays a hierarchical collection of items. | You do not need to memorize the above lists, but you should probably re-read the lists again so that you are familiar with what is possible in a TKinter GUI interface. (Note that the TKinter module is customizable, which means that you can create your own widgets, but that is beyond what we will study in these lessons.) ## 15.10. Creating Widgets After importing the Tkinter modules as shown above, the first thing you need to do is create a window for your application. This is done by creating a `Tk` object: ```python application_window = tk.Tk() ``` Then you create widgets and add them to the window’s widget hierarchy. For example, to create a button you would call either the `tk` or the `ttk` `Button` method and send the `application_window` as the first argument: ```python cmd_button = tk.Button(application_window, text="Example") # or cmd_button = ttk.Button(application_window, text="Example") ``` The parameters needed to correctly create each widget varies, so you will need to refer to the Python documentation for each specific widget type. As of fall 2016, the most current version of the Tkinter module is version 25 and its documentation can be found at
Notice that in the above code, `Tk()` and `Button()` are both capitalized. By convention, this indicates that the window and the button are instances of a Python class. The Tkinter module is entirely object-oriented and makes extensive use of object-oriented language features. ## 15.11. Layout Mangers A widget will not be visible in a window until you assign it a size and location within it’s parent widget. Assigning a specific size and location to every widget is tedious and error-prone. In addition, the desired behaviour for most GUI interfaces is that the widgets resize and relocate in reasonable ways if their parent window is re-sized. Trust me, you don’t what to write code to resize and relocate widgets every time you develop a GUI program! Therefore, `layout managers` are included in the Tkinter module to do this work for you. You just have to give some basic positioning information to a `layout manager` so it can calculate a position and a size for each widget. There are three `layout managers` in the `Tkinter` module: | Layout Manager | Description | | --- | --- | | place | You specify the exact size and position of each widget. | | pack | You specify the size and position of each widget *relative to each other*. | | grid | You place widgets in a cell of a 2-dimensional table defined by rows and columns. | You should **never** mix and match these layout managers. Use only one of them for the widget layout within a particular “parent widget”. (Widgets are organized in a hierarchy, which is explained in the next lesson.) ## 15.12. Specifying Dimensions By default, a size or position value specified as an integer is in pixel units. If a different unit of measurement is desired, you can use a string and append an appropriate unit symbol. The following units are recognized: | Example | Units | | --- | --- | | 37 | pixels | | “37c” | centimeters | | “37i” | inches | | “37m” | millimeters | | “37p” | “points” (as in a 12 point font). (One point is approximately 1/72 of an inch.) | Please note that some of the widgets change the meaning of their `width` and `height` based on the widget’s configuration. For example, if you have a `Button` with text on it, and you set the button’s width and height, the size values are considered to be in `text` units. However, if you have a `Button` with an image on its face, the button’s width and height values are interpreted as pixels. If you have a widget whose size is not acting as you would expect, it may be because the units are different than you expect. In general use, you rarely specify the size of widgets. Your `layout manager` will set the size of a widget based on the contents of the widget and if you want the widget to be larger, you added some `padding` around it. ## 15.13. *Place* Layout Manager All widgets have a `.place(x, y, width, height)` method that can be used to specify the exact location and size of a widget. You can use this method to create a static interface that will always look the same regardless of the parent’s window size. This layout method is rarely used because the GUI window can’t be easily re-sized in user-friendly ways. ## 15.14. *Grid* Layout Manager All widgets have a `.grid(row=r, column=c)` method that will place a widget in the cell (r,c) of a 2-D table. By default the widget is rendered in the middle of the cell and as small as possible. This can be changed using other optional parameters. The `sticky` option allows you to move or stretch a widget to the borders of a cell. There are 4 options, `tk.E`, `tk.W`, `tk.N`, and `tk.S`, which are abbreviations for **east**, **west**, **north**, and **south** respectively. These options are ‘bit flags’ that can be combined in any combination using simple addition. When you specify opposing cell edges, the widget will stretch to reach both boundaries. Here are some examples: | `sticky` Parameter | Description | | --- | --- | | `tk.W` | Move the widget to the left cell boundary. | | `tk.W + tk.N` | Move the widget left and up so that it is in the upper-left corner. | | `tk.E + tk.W` | Stretch the widget so that it fills the cell horizontally. | | `tk.E + tk.W + tk.S` | Stretch the widget so that it fills the cell horizontally and move it down to the bottom cell boundary. | | `tk.E + tk.W + tk.N + tk.S` | Stretch the widget so it fills the entire cell. | In addition, if you do not want the widget to be rendered as small as possible, you can add space to the widget using `ipadx` and `ipady`, which makes the widget itself larger. (The “i” stands for internal padding.) Or you can add space around the widget to force a cell to be larger using `padx` and `pady`. This makes the area inside the table cell larger. You can also have a widget span more than one cell in the grid using `columnspan` and `rowspan` parameters. Here is an example grid command that stretches a button widget to the full width of a grid cell and adds extra space vertically to the grid cell: ```python my_button = tk.Button(application_window, text="Example") my_button.grid(row=3, column=1, sticky=tk.E + tk.W, pady=10) ``` One final note about grid layouts. You might think that you need to tell `tkinter` how big your grid layout is – but you don’t. It will figure out how big the grid table is by simply examining all of the widgets inside a container widget. If there are cells that were not assigned a widget, those cells will be empty in the interface. To accomplish a desired interface design using a `grid` `layout manager` is typically a process of experimentation. Don’t expect to get your desired layout on the first try. ## 15.15. *Pack* Layout Manager The `pack` layout manager tries to place each widget “next to” the previous widget in a container. You can call it without any arguments and it will try to position and size a widget in reasonable ways. To control the exact rendering of a widget using `pack` is more complex than the `grid` layout and therefore you are encouraged to use the `grid` layout manager for most interface design problems. For situations where you want to have a series of widgets in a vertical or horizontal row, the `pack` layout manager is fairly simple to use. The `side` parameter can be `TOP`, `BOTTOM`, `LEFT`, or `RIGHT` and specifies which side of the container to “pack” the widget against. You also use the `ipadx`, `ipady`, `padx` and `pady` parameters to add extra space around a widget. The parameter `fill` can be `X`, `Y`, or `BOTH` to specify whether a widget stays as small as possible or whether if expands to fill all available space inside it’s parent widget. If you want the widget to stretch to fill the available space, you also have to set the `expand` parameter to `True`. Here is an example `pack` command for a button widget: ```python my_button = tk.Button(application_window, text="Example") my_button.pack(side=tk.TOP, fill=tk.Y, expand=True, pady=10) ``` To accomplish a desired interface design using a `pack` `layout manager` is typically a process of experimentation. Don’t expect to get your desired layout on the first try. ### 15.15.1. Summary To summarize, let’s review two very important rules: * A widget will not be visible in a window until you assign it a size and location within it’s parent widget. * You should **never** mix and match `layout managers`; use only one type of layout manager for the widgets within a particular parent widget. ## 15.16. Widget Groupings You will design a more user friendly interface if you group and organize your widgets in a coherent design. Tkinter has four basic ways to group widgets. These are described in the following table. They are often referred to as “container” widgets because in the widget hierarchy of a GUI program they are the parent widget of a group of related widgets. | Widget | Purpose | | --- | --- | | `tk.Frame`, `ttk.Frame` | Create a container for a set of widgets to be displayed as a unit. | | `ttk.LabelFrame` | Group a number of related widgets using a border and a title. | | `tk.PanedWindow` | Group one or more widgets into “panes”, where the “panes” can be re-sized by the user by dragging separator lines. | | `ttk.Notebook` | A tabbed set of frames, only one of which is visible at any given time. | Widgets are always organized as a hierarchy, where the main application window is the root of the hierarchy. Typically, the child widgets of an application window are a combination of “frames”. The “frames” hold other widgets. A “frame” will not be visible until it is assigned a size and location using a layout manager. The image below shows examples of the four types of widget “containers”. The “containers” in this example used a `grid` layout manager on a 2x2 grid.  Examples of grouping widgets For the `Frame` and `LabelFrame` groups, the frame is the “parent” of the widgets displayed inside the frame. That is, when the buttons were created, the frame was the first parameter to the `tk.Button()` function. For the `PanedWindow` and `Notebook` groups, you use an `.add(widget)` function to add your widgets to the group. You are still creating a hierarchy of widgets, but the syntax is different. You can download and run this example program, [all\_frame\_widgets.py](../_static/Programs/all_frame_widgets.py) that allows you to interact with the four types of “contaners.” ## 15.17. Command Events When a user clicks on certain types of widgets, like a button, you typically want a specific action to be performed. This is accomplished by setting the `command` attribute of a widget to a specific `event handler` function. This can be any function that receives no arguments as parameters. You can set the `event handler` function using a “named parameter” when you create the widget, or set the widget’s `command` attribute using a dictionary lookup. For example: ```python def my_function(): print("my_function was called.") my_button = tk.Button(application_window, text="Example", command=my_function) # or my_button = tk.Button(application_window, text="Example") my_button['command'] = my_function ``` Note that you are setting the `command` property of the widget to a *function reference* – you are not calling the function! Therefore, do not put parentheses after the function name when you make the assignment. The following widgets define a `command` property which defines a function that is called from the application’s event loop whenever a specific, predefined event is performed by a user. | Widget | The user event that causes the `command` function to be executed: | | --- | --- | | `Button` | The user places their pointing device cursor over the button and presses and releases the left mouse button. The function is actually called on the button release. | | `Checkbutton` | If the state of the check box changes, the function is called. | | `Radiobutton` | If the state of the radio box changes, the function is called. | | `Scale` | The function is called if the slider moves. The function is passed one argument, the new scale value. | The following widgets do **not** have a `command` property, but they use other properties to respond to user events: | Widget | Property | User events: | | --- | --- | --- | | `Menu` | `postcommand` | Every time someone brings up this menu. | | `Combobox` | `postcommand` | When the user clicks on the down-arrow. | | `Combobox` | `validatecommand` | Dynamically validate the widget’s text content. | | `Entry` | `validatecommand` | Dynamically validate the widget’s text content. | Note that the `Label`, `Message`, and `Separator` widgets do not respond to user events and therefore have no associated event handlers. ## 15.18. Hello World Again As a simple example, here is an enhanced “Hello World” program that contains a quit button that has a single `command` event handler. If the quit button is pressed by a user, the window’s destroy method is called – which closes the window. ```python import tkinter as tk from tkinter import ttk # Create the application window window = tk.Tk() # Create the user interface my_label = ttk.Label(window, text="Hello World") my_label.grid(row=1, column=1) quit_button = ttk.Button(window, text="Quit") quit_button.grid(row=2, column=1) quit_button['command'] = window.destroy # Start the GUI event loop window.mainloop() ``` ## 15.19. Other Events Using `command` events for simple actions is the easiest way to handle user events. For more complex situations, the next lesson explains how to associate “lower level” events with `event handler` functions. ## 15.20. Low-Level Event Processing In the previous lessons you learned how to associate simple user actions with the most common user events, such as clicking on a button. For simple GUI programs this is typically sufficient. But for more complex programs you might need to process “lower level” events, such as recognizing when the user’s cursor is over a widget, or when a widget becomes the focus of user input. To handle these types of events you need to implement **event binding**. This lesson provides an overview of how to process any event that happens on a computer. We will not use these techniques in most of the simple GUI programs we discuss, but you should be aware of what is possible. ## 15.21. Focus Remember from our previous discussion that the operating system is in control of your computer’s screen, keyboard, mouse, and other input devices. When the user interacts with these input devices the operating system generates `event objects` that capture specific event information. Your computer is running multiple processes at any given time, so how does the computer know which process to send an event to? All GUI operating systems have the concept of an “active window.” Only the “active window” receives events. We say that the “active window” has the **focus** of the user. Typically the application whose window is in front of all other windows on the computer screen has the operating system’s **focus**. Only the process with the **focus** receives user events; the other running applications do **not** receive user events. The concept of **focus** goes further than the applications running on a computer. Inside an application, only one widget at a time can have the **focus** of the user. The widget with the **focus** is the widget that receives keyboard events. For example, suppose you had a GUI interface that contained two different `Entry` widgets. If the user hits an “a” key, which widget has an “a” added to their existing text? It is the widget that currently has the **focus**. ## 15.22. Event Binding When your program has the **focus** it can potentially receive many more events than it desires to process. For example, you may not care that the pointing device just moved one pixel to the right. All “low level” user events are ignored by your program unless you specifically request that a particular type of event be sent to a specific function in your program. The task of associating a specific `event handler` function with a specific “low level” event is called **binding**. An event is always **bound** to a specific widget. For example, if you wanted to know when the cursor of a pointing device has moved over a widget, you would bind an `"
"` event to the widget and specify a function to call when the event happens. This is done with the `.bind(event_description, event_handler)` method of a widget. Once this bind operation is complete and the application’s main event loop has started, every time a pointing device is moved over the widget, the event handler will be called. Here is an example of binding an `"
"` event to a Button. ```python def process_event(event): print("The process_event function was called.") my_button = tk.Button(application_window, text="Example") my_button.bind("
", process_event) ``` It is best practice to “stub out” event handlers and verify that events are being processed correctly before you start developing an application’s actual processing code. Here is an example of a “stubbed” out `event handler`: ```python def my_function(): print("my_function was called.") ``` ## 15.23. Event Descriptors An event represents an action that needs to be processed. Events are caused by a user when they click in a window or type on the keyboard. Events can also be generated by software, such as a request to redraw the application’s window after changes have been made. In a GUI program, **all** processing is done by responding to events. In Tkinter, events are defined as strings using a pre-defined syntax. The general format of an event description is `
`, where the `modifier` and `detail` portions are optional. For example, a `
` event is generated by any change of state of any mouse button, while a `
` event will only be generated if the mouse state changes while the SHIFT key on the keyboard is down. And a `
` event will only be generated if the left mouse button (associated with the number 1) changes state. Below is a list of the most widely used events along with a brief description of each one. You can add or remove `modifier` and `detail` values to make events more or less specific. | Pointer Related Events | Description | | --- | --- | | `"
"` | The left mouse button was pressed. | | `"
"` | The middle mouse button was pressed. | | `"
"` | The right mouse button was pressed. | | `"
"` | With the left mouse button held down, the mouse changed location. | | `"
"` | With the middle mouse button held down, the mouse changed location. | | `"
"` | With the right mouse button held down, the mouse changed location. | | `"
"` | The left mouse button was released. | | `"
"` | The left mouse button was double-clicked. | | `"
"` | The mouse pointer just moved over a particular widget. | | `"
"` | The mouse pointer is no longer over a particular widget. | | `"
"` | A widget just received the keyboard focus. | | `"
"` | A widget just lost the keyboard focus. | | `"
"` | A widget just changed its size or position. | | Keyboard Related Events | Description | | --- | --- | | `"
"` | The user pressed any key on the keyboard. | | `"
"` | The user pressed the Enter key. | | `"
"` | The user pressed the Backspace key. | | `"
"` | The user pressed the Tab key. | | `"
"` | The user pressed the Escape key. | | `"
"` | The user pressed the Page-up key. | | `"
"` | The user pressed the Page-down key. | | `"
"` | The user pressed the up arrow key. | | `"
"` | The user pressed the down arrow key. | | `"
"` | The user pressed the left arrow key. | | `"
"` | The user pressed the right arrow key. | | `"
"` | The user pressed the F1 key. | | `"
"` | The user pressed the F2 key. | | `"
"` | The user pressed the “a” key. | | `"
"` | The user pressed the “b” key. | | `"
"` | The user pressed the “c” key. | | `"
"` | The user pressed the up arrow key while the shift key was down. | | `"
"` | The user pressed the up arrow key while the alt key was down. | | `"
"` | The user pressed the up arrow key while the control key was down | | etc… | | ## 15.24. Event Objects When a user generates an event, or the software generates an event, an `event object` is created. This object is automatically passed to the function that is registered to handle the event. Every event handler function that is **bound** to an event using the `.bind(event_description, function_handler)` function must be defined to receive one parameter, an `event object`. An `event object` contains the following attributes. | Event Object Attribute | Description | | --- | --- | | `.widget` | The widget this event was **bound** to. This is a reference to a `Tkinter` widget instance; it is not a string name. | | `.x`, `.y` | The current mouse position, relative to the application’s window, in pixels. | | `.x_root`, `.y_root` | The current mouse position relative to the upper left corner of the screen, in pixels. | | `.char` | For keyboard events only, this is the character code of the key pressed or released as a string. | | `.keysym` | For keyboard events only, the key symbol. | | `.keycode` | For keyboard events only, the key code (i.e., the key’s Unicode decimal value). | | `.num` | For mouse button events only, the button number. | | `.width`, `.height` | For configure events only, the new size of the widget, in pixels. | A typical event handler will use the values in the `event object` it receives to perform an appropriate action related to the event. For example, ```python def process_event(event): if event.x > 10 and event.y > 20: # do something ``` ## 15.25. Event Processing The operating system generates events in the order the user or the program creates them. An application’s GUI event loop receives the events in this same order and then calls the appropriate event handler. Therefore, events are processed in the same order they are created. Events can’t be processed unless the application’s GUI event-loop is running. If an individual event handler takes a long time to process an event, other events will get “queued up” waiting for a chance to be processed. It is considered bad GUI programming for any event handler to take up too much processing time. An event handler should do as little processing as possible to accomplish its intended task and then quit. This returns control of the application back to the event loop. Events are always associated with a widget. If you want to know every event that happens inside an application’s window, then bind events to the widget that is your application’s window. If you only want to know about button click events on a particular button, then bind an event handler to that specific button. In general, events should be associated with the most specific widget possible. Some widgets, such as a Notebook that implements a tabbed set of frames, have predefined events that are used to manipulate them. These are called “bind\_class” event bindings and they bind certain events to all instances of a particular widget type. In general you should not modify or change these types of event bindings because a user expects a certain behaviour from a particular type of widget and changing that behaviour can make the entire user interface confusing to a user. ## 15.26. The Design of GUI Programs For very simple GUI programs, no special program design is needed, as demonstrated in the previous “Hello World” example programs. However, any non-trivial GUI program will require extensive use of global variables if the structure of the code does not use a Python `class`. You have learned in previous lessons that global variables are bad because they make debugging programs more difficult. Therefore we want a design for GUI programs that avoids global variables as much as possible. To demonstrate this, let’s look at two versions of a simple program that increments a counter each time a user clicks a button. The first version of this code does not use a `class` definition and requires that a global variable called my\_counter be used. This is because the label that represents the counter is created in the `create_user_interface` function but it must be accessed in the event handler function `increment_counter`. In fact, the event handlers of a GUI program almost always need access to multiple widgets in the program’s interface and the values can’t be passed as parameters because an `command` `event handler` function receives no parameters and a `bind` `event handler` function receives exactly one parameter – an `event object`. Study the following example and pay close attention to where the `my_counter` global variable is used. ```python import tkinter as tk from tkinter import ttk global my_counter def create_user_interface(application_window): global my_counter my_counter = ttk.Label(application_window, text="0") my_counter.grid(row=0, column=0) increment_button = ttk.Button(application_window, text="Add 1 to counter") increment_button.grid(row=1, column=0) increment_button['command'] = increment_counter quit_button = ttk.Button(application_window, text="Quit") quit_button.grid(row=2, column=0) quit_button['command'] = window.destroy def increment_counter(): global my_counter my_counter['text'] = str(int(my_counter['text']) + 1) # Create the application window window = tk.Tk() create_user_interface(window) # Start the GUI event loop window.mainloop() ``` Let’s compare the above program to an identical application that is designed as a Python `class`. The `class` encapsulates all of the values needed for the GUI interface and the `event handlers` and we don’t need global variables! ```python import tkinter as tk from tkinter import ttk def main(): # Create the entire GUI program program = CounterProgram() # Start the GUI event loop program.window.mainloop() class CounterProgram: def __init__(self): self.window = tk.Tk() self.my_counter = None # All attributes should be initialize in init self.create_widgets() def create_widgets(self): self.my_counter = ttk.Label(self.window, text="0") self.my_counter.grid(row=0, column=0) increment_button = ttk.Button(self.window, text="Add 1 to counter") increment_button.grid(row=1, column=0) increment_button['command'] = self.increment_counter quit_button = ttk.Button(self.window, text="Quit") quit_button.grid(row=2, column=0) quit_button['command'] = self.window.destroy def increment_counter(self): self.my_counter['text'] = str(int(self.my_counter['text']) + 1) if __name__ == "__main__": main() ``` Notice the following about this design: * The application’s window is created in the constructor (`__init__`) of the `CounterProgram` class and then the interface widgets are created by a call to `create_widgets`. * The event handler, `increment_counter` can access the label `self.my_counter` using the object’s attributes. * The code creates an instance of the class `CounterProgram` and starts the GUI event-loop. It is recommended that you develop all of your GUI programs as Python Classes. For complex designs, a Python `Class` can help manage the complexity of the code and the scoping of variables. ## 15.27. Common Widget Properties Each widget has a set of properties that define its visual appearance on the computer screen and how it responds to user events. There is a set of properties that all `tk` widgets have in common. Some of these are shown in the following table. `ttk` Attributes Remember that `ttk` widgets match the “look and feel” of the computer that is running your program, so there are limited attributes you can change for `ttk` widgets. See the `ttk` `style` attribute information at
if you want to modify `ttk` widgets.) | Common Widget Properties | Description | | --- | --- | | `bg` | Background color. | | `fg` | Foreground color. | | `width` | Width in pixels | | `height` | Height in pixels | | `borderwidth` | The size of the border in pixels. | | `text` | Text displayed on the widget. | | `font` | The font used for text on the widget. | | `cursor` | The shape of the cursor when the cursor is over the widget. | | `activeforeground` | The color of the text when the widget is activated. | | `activebackground` | The color of the background when the widget is activated. | | `image` | An image to be displayed on the widget. | You can treat a widget object as a dictionary and use the property names as keys to access and change the property values. For example, to change the background color and width of a widget whose object variable is named `sam`, you could do this: ```python sam = tk.Button(application_window, text="Sam's Button") sam['bg'] = 'red' sam['width'] = 60 # pixels ``` ## 15.28. Specific Widget Properties Each type of widget has properties specific to its intended use. Here are some examples: | Specific Widget Properties | Applies to | Description | | --- | --- | --- | | `variable` | Checkbutton Radiobutton Entry Spinbox Text | An Tk object that will be changed by interaction with the widget. | | `from_` | Scale | The starting value of a `scale` (i.e., a slider) | | `to` | Scale | The ending value of a `scale` (i.e., a slider) | | `orient` | Scale | HORIZONTAL or VERTICAL | | `resolution` | Scale | The increment amount along a `scale` (i.e., a slider) | Some widgets have a “variable” that a user manipulates. For such widgets you must create a special tk object that stores the widget’s value. Basically you are creating a non-visible widget that can have events associated with it. You can access the widget’s value through `.set(new_value)` and `.get()` methods on the `variable object`. There are four types of `variable objects`: | tk variable object | Description | | --- | --- | | `tk.BooleanVar` | A tk object that holds a single Boolean value | | `tk.IntVar` | A tk object that holds a single integer value | | `tk.DoubleVar` | A tk object that holds a single double value | | `tk.StringVar` | A tk object that holds a single string value | Let’s take a `tk.Checkbutton` as an example and walk through the steps needed to get and set its “value”. You need to do three things: 1. Create a `tk.BooleanVar` object. (Actually any type of `variable object` can be used.) 2. Give the `tk.BooleanVar` object the initial value you want for the checkbutton. 3. Assign the `variable` attribute of the checkbutton to the `tk.BooleanVar object`. In code this looks like this: ```python checkbutton_value = tk.BooleanVar() checkbutton_value.set(True) # The checkbutton will be "checked" my_checkbutton = tk.Checkbutton(parent_widget, text="Example check box") my_checkbutton['variable'] = checkbutton_value ``` If you want the value of the checkbutton at any time while your program is running, you `get` the value of the `variable object` like this: ```python the_current_value_of_the_checkbutton = checkbutton_value.get() ``` And you can change the state of the checkbutton at any time using a call to `.set()` like this: ```python checkbutton_value.set(FALSE) # set the checkbutton to "un-checked" ``` ## 15.29. Widget Attributes The following web pages provide a good description of the properties that can be modified for individual widgets: | Widget | Documentation Resource | | --- | --- | | `tk.Button` |
| | `ttk.Button` |
| | `tk.Menu` |
| | `tk.Menubutton` |
| | `ttk.Menubutton` |
| | `tk.OptionMenu` |
| | `ttk.Entry` |
| | `tk.Text` |
| | `tk.Checkbutton` |
| | `ttk.Checkbutton` |
| | `tk.Radiobutton` |
| | `ttk.Radiobutton` |
| | `tk.Listbox` |
| | `ttk.Combobox` |
| | `tk.Scale` |
| | `ttk.Scale` |
| | *Container Widgets* | | | `tk.Frame` |
| | `ttk.Frame` |
| | `tk.LabelFrame` |
| | `ttk.LabelFrame` |
| | `tk.PanedWindow` |
| | `ttk.PanedWindow` |
| | `ttk.Notebook` |
| ## 15.30. Timer Events GUI programs run an “event loop” that continuously receive events from the operating system and “dispatches” those events to appropriate callback functions. Nothing happens in a GUI program without an event. The application logic for some problems requires that specific events happen at some specific times. For this reason `tkinter` includes a feature to generate events under software control. These are often refered to as *timer events*. Every widget has an `after` method that will generate an event at a specific time interval from the time it is called. The method takes at least 2 arguments: the amount of time (in milliseconds) to wait before generating the event, and the callback function to call after the time has elapsed. In the example below, the function `a_callback_function` will be called one second (1000 milliseconds) after the timer-event was created. ```python def a_callback_function(): print("a_callback_function was called.") my_button = tk.Button(application_window, text="Example") my_button.after(1000, a_callback_function) ``` ### 15.30.1. Animations and Repeated Tasks If you want a specific task to be repeated on a regular interval, then the callback function that performs the task should create a new timer event each time it is called. The following example creates a callback function that is called 30 times per second. (Note that 1/30th of a second is 0.033 seconds, or 33 milliseconds.) ```python def animate(): # Draw something my_button.after(33, animate) my_button = tk.Button(application_window, text="Example") my_button.after(33, animate) ``` A widget can have more than one timer event active at any time. In fact there is no limit to the number of timer events you can create. Note that you should never use a loop to repeat a task in a GUI program. If you use a loop, the event-loop will be prevented from execution and no events will be processed while the loop is running. Always use a timer event for repeating a task, especially if a single execution of the task takes a considerable amount of CPU time. ### 15.30.2. Canceling Timer Events In some cases you may need to cancel a timer event to prevent it from executing. This is straightforward using the `after_cancel` method. Remember that a widget can have multiple active timers, so the `after_cancel` method requires one parameter which specifies which timer event to cancel. If you need to cancel an event, you must capture the return value from the call to `after` when you created the event. Here is an example: ```python def do_something(): # Some processing my_button = tk.Button(application_window, text="Example") timer_object = my_button.after(1000, do_something) ... my_button.after_cancel(timer_object) ``` ### 15.30.3. Multiple Parameters to Timer Callbacks Timer callback function can have zero or more arguments passed to them when they are called. You specify the arguments when you create the event. This makes timer callback functions extremely flexible. Below is an example of three different callback functions, each of which receives a different number of arguments. You must specify the correct number of arguments for the callback function when you create the timer event. ```python def task1(): # Do some processing def task2(alpha): # Do some processing def task3(beta, gamma): # Do some processing my_button = tk.Button(application_window, text="Example") my_button.after(1000, task1) my_button.after(2000, task2, 3) # 3 gets passed to the parameter alpha my_button.after(5000, task3, a, b) # a gets passed to the parameter beta # b gets passed to the parameter gamma ``` ## 15.31. A Programming Example Let’s develop a non-trivial GUI program to demonstrate the material presented in the previous lessons. We will develop a GUI Whack-a-mole game where a user tries to click on “moles” as they randomly pop up out of the “ground.” This discussion will take you through an *incremental development* cycle that creates a program in well-defined stages. While you might read a finished computer program from “top to bottom,” that is not how it was developed. For a typical GUI program development, you are encouraged to go through these stages: 1. Using scratch paper, physically draw a rough sketch of your user interface. 2. Create the basic structure of your program and create the major frames that will hold the widgets needed for your program’s interface. Give the frames an initial size and color so that you can visually see them, given that there are no widgets inside of them to determine their size. 3. Incrementally add all of the widgets you need for your program and size and position them appropriately. 4. Create your callback functions, stub them out, and assign them to appropriate events. Verify that the events are executing the correct functions. 5. Incrementally implement the functionality needed for each callback function. When you develop code using *incremental development* your program should always be executable. You continually add a few lines of code and then test them. If errors occur you almost always know were the errors came from! They came from the lines of code you just added. ### 15.31.1. A Whack-a-mole Game Step 1: Make sure you have a reasonable GUI design and implementation plan before you start coding. Draw a sketch of your initial design on paper and consider how a user will interact with your program.  Initial design of a Whack-a-mole game Step 2: Create the basic structure of your interface using appropriate `frame` widgets. You will need to give a size to the frames because they will contain no widgets, which is how a frame typically gets its size. It is also suggested that you give each frame a unique color so it is easy to see the area of the window it covers. Here is a basic start for our whack-a-mole game ([whack\_a\_mole\_v1.py](../_static/Programs/whack_a_mole_v1.py)): ```python def main(): # Create the entire GUI program program = WhackAMole() # Start the GUI event loop program.window.mainloop() class WhackAMole: def __init__(self): self.window = tk.Tk() self.mole_frame, self.status_frame = self.create_frames() def create_frames(self): mole_frame = tk.Frame(self.window, bg='red', width=300, height=300) mole_frame.grid(row=1, column=1) status_frame = tk.Frame(self.window, bg='green', width=100, height=300) status_frame.grid(row=1, column=2) return mole_frame, status_frame if __name__ == "__main__": main() ``` Step 3: Incrementally add appropriate widgets to each frame. Don’t attempt to add all the widgets at once. The initial design conceptualized the moles as buttons, so a grid of buttons was added to the left frame, one button for each mole. The exact size of the “mole field” needs to be determined at a future time, so initialize a CONSTANT that can be used to easily change it later. ([whack\_a\_mole\_v2.py](../_static/Programs/whack_a_mole_v2.py))   ```python import tkinter as tk from tkinter import PhotoImage def main(): # Create the entire GUI program program = WhackAMole() # Start the GUI event loop program.window.mainloop() class WhackAMole: NUM_MOLES_ACROSS = 4 def __init__(self): self.window = tk.Tk() self.mole_frame, self.status_frame = self.create_frames() self.mole_photo = PhotoImage(file="mole.png") self.mole_buttons = self.create_moles() def create_frames(self): mole_frame = tk.Frame(self.window, bg='red') mole_frame.grid(row=1, column=1) status_frame = tk.Frame(self.window, bg='green', width=100) status_frame.grid(row=1, column=2, sticky=tk.E + tk.W + tk.N + tk.S) return mole_frame, status_frame def create_moles(self): # Source of mole image: https://play.google.com/store/apps/details?id=genergame.molehammer mole_buttons = [] for r in range(WhackAMole.NUM_MOLES_ACROSS): row_of_buttons = [] for c in range(WhackAMole.NUM_MOLES_ACROSS): mole_button = tk.Button(self.mole_frame, image=self.mole_photo) mole_button.grid(row=r, column=c, padx=8, pady=8) row_of_buttons.append(mole_button) mole_buttons.append(row_of_buttons) return mole_buttons if __name__ == "__main__": main() ``` Continue to add appropriate widgets for the right frame. The final result is shown below, but recognize that it was developed little by little. ([whack\_a\_mole\_v3.py](../_static/Programs/whack_a_mole_v3.py)) ```python import tkinter as tk from tkinter import PhotoImage def main(): # Create the entire GUI program program = WhackAMole() # Start the GUI event loop program.window.mainloop() class WhackAMole: STATUS_BACKGROUND = "white" NUM_MOLES_ACROSS = 4 def __init__(self): self.window = tk.Tk() self.mole_frame, self.status_frame = self.create_frames() self.mole_photo = PhotoImage(file="mole.png") self.mole_buttons = self.create_moles() self.hit_counter, self.miss_counter, self.start_button \ = self.create_status_widgets() def create_frames(self): mole_frame = tk.Frame(self.window, bg='red') mole_frame.grid(row=1, column=1) status_frame = tk.Frame(self.window, bg=WhackAMole.STATUS_BACKGROUND) status_frame.grid(row=1, column=2, sticky=tk.N + tk.S + tk.W + tk.W) return mole_frame, status_frame def create_moles(self): # Source of mole image: https://play.google.com/store/apps/details?id=genergame.molehammer mole_buttons = [] for r in range(WhackAMole.NUM_MOLES_ACROSS): row_of_buttons = [] for c in range(WhackAMole.NUM_MOLES_ACROSS): mole_button = tk.Button(self.mole_frame, image=self.mole_photo) mole_button.grid(row=r, column=c, padx=8, pady=8) row_of_buttons.append(mole_button) mole_buttons.append(row_of_buttons) return mole_buttons def create_status_widgets(self): spacer = tk.Label(self.status_frame, text="", bg=WhackAMole.STATUS_BACKGROUND) spacer.pack(side="top", fill=tk.Y, expand=True) hit_label = tk.Label(self.status_frame, text="Number of Hits", bg=WhackAMole.STATUS_BACKGROUND) hit_label.pack(side="top", fill=tk.Y, expand=True) hit_counter = tk.Label(self.status_frame, text="0", bg=WhackAMole.STATUS_BACKGROUND) hit_counter.pack(side="top", fill=tk.Y, expand=True) spacer = tk.Label(self.status_frame, text="", bg=WhackAMole.STATUS_BACKGROUND) spacer.pack(side="top", fill=tk.Y, expand=True) miss_label = tk.Label(self.status_frame, text="Number of Misses", bg=WhackAMole.STATUS_BACKGROUND) miss_label.pack(side="top", fill=tk.Y, expand=True) miss_counter = tk.Label(self.status_frame, text="0", bg=WhackAMole.STATUS_BACKGROUND) miss_counter.pack(side="top", fill=tk.Y, expand=True) spacer = tk.Label(self.status_frame, text="", bg=WhackAMole.STATUS_BACKGROUND) spacer.pack(side="top", fill=tk.Y, expand=True) start_button = tk.Button(self.status_frame, text="Start") start_button.pack(side="top", fill=tk.Y, expand=True, ipadx=10) spacer = tk.Label(self.status_frame, text="", bg=WhackAMole.STATUS_BACKGROUND) spacer.pack(side="top", fill=tk.Y, expand=True) quit_button = tk.Button(self.status_frame, text="Quit") quit_button.pack(side="top", fill=tk.Y, expand=True, ipadx=10) spacer = tk.Label(self.status_frame, text="", bg=WhackAMole.STATUS_BACKGROUND) spacer.pack(side="top", fill=tk.Y, expand=True) return hit_counter, miss_counter, start_button if __name__ == "__main__": main() ``` Step 4: Create a callback function for each event that will cause something to happen in your program. Stub these functions out with a single print statement in each one. Bind an event to each callback function. Now test your program and make sure each event causes the correct print-line in the Python console. ([whack\_a\_mole\_v4.py](../_static/Programs/whack_a_mole_v4.py)) ```python import tkinter as tk from tkinter import PhotoImage def main(): # Create the entire GUI program program = WhackAMole() # Start the GUI event loop program.window.mainloop() class WhackAMole(): STATUS_BACKGROUND = "white" NUM_MOLES_ACROSS = 4 def __init__(self): self.window = tk.Tk() self.mole_frame, self.status_frame = self.create_frames() self.mole_photo = PhotoImage(file="mole.png") self.mole_buttons = self.create_moles() self.hit_counter, self.miss_counter, self.start_button, self.quit_button \ = self.create_status_widgets() self.set_callbacks() def create_frames(self): mole_frame = tk.Frame(self.window, bg='red') mole_frame.grid(row=1, column=1) status_frame = tk.Frame(self.window, bg=WhackAMole.STATUS_BACKGROUND) status_frame.grid(row=1, column=2, sticky=tk.E + tk.W + tk.N + tk.S) return mole_frame, status_frame def create_moles(self): # Source of mole image: https://play.google.com/store/apps/details?id=genergame.molehammer mole_buttons = [] for r in range(WhackAMole.NUM_MOLES_ACROSS): row_of_buttons = [] for c in range(WhackAMole.NUM_MOLES_ACROSS): mole_button = tk.Button(self.mole_frame, image=self.mole_photo) mole_button.grid(row=r, column=c, padx=8, pady=8) row_of_buttons.append(mole_button) mole_buttons.append(row_of_buttons) return mole_buttons def create_status_widgets(self): spacer = tk.Label(self.status_frame, text="", bg=WhackAMole.STATUS_BACKGROUND) spacer.pack(side="top", fill=tk.Y, expand=True) hit_label = tk.Label(self.status_frame, text="Number of Hits", bg=WhackAMole.STATUS_BACKGROUND) hit_label.pack(side="top", fill=tk.Y, expand=True) hit_counter = tk.Label(self.status_frame, text="0", bg=WhackAMole.STATUS_BACKGROUND) hit_counter.pack(side="top", fill=tk.Y, expand=True) spacer = tk.Label(self.status_frame, text="", bg=WhackAMole.STATUS_BACKGROUND) spacer.pack(side="top", fill=tk.Y, expand=True) miss_label = tk.Label(self.status_frame, text="Number of Misses", bg=WhackAMole.STATUS_BACKGROUND) miss_label.pack(side="top", fill=tk.Y, expand=True) miss_counter = tk.Label(self.status_frame, text="0", bg=WhackAMole.STATUS_BACKGROUND) miss_counter.pack(side="top", fill=tk.Y, expand=True) spacer = tk.Label(self.status_frame, text="", bg=WhackAMole.STATUS_BACKGROUND) spacer.pack(side="top", fill=tk.Y, expand=True) start_button = tk.Button(self.status_frame, text="Start") start_button.pack(side="top", fill=tk.Y, expand=True, ipadx=10) spacer = tk.Label(self.status_frame, text="", bg=WhackAMole.STATUS_BACKGROUND) spacer.pack(side="top", fill=tk.Y, expand=True) quit_button = tk.Button(self.status_frame, text="Quit") quit_button.pack(side="top", fill=tk.Y, expand=True, ipadx=10) spacer = tk.Label(self.status_frame, text="", bg=WhackAMole.STATUS_BACKGROUND) spacer.pack(side="top", fill=tk.Y, expand=True) return hit_counter, miss_counter, start_button, quit_button def set_callbacks(self): # Set the same callback for each mole button for r in range(WhackAMole.NUM_MOLES_ACROSS): for c in range(WhackAMole.NUM_MOLES_ACROSS): self.mole_buttons[r][c]['command'] = self.mole_hit self.start_button['command'] = self.start self.quit_button['command'] = self.quit def mole_hit(self): print("mole button hit") def start(self): print("start button hit") def quit(self): print("quit button hit") if __name__ == "__main__": main() ``` Step 5: Add appropriate functionality to the callback functions. This is where the functional logic of your particular application resides. In the case of our whack-a-mole game, we need to be able to count the number of times a user clicks on a mole when it is visible. And we need the moles to appear and disappear at random intervals. Originally each mole was a button widget, but the border around each button was distracting, so they were changed to label widgets. Two images were used to represent a mole: one image is a solid color that matches the frame’s background, and the other image is a picture of a mole. By replacing the image used for each label we can make the moles visible or invisible. A label normally does not have an associated callback, so we `bind` a left mouse click event, `"
"` to each label. We can determine whether the mouse click is a “hit” or a “miss” by examining the label under the click to see which image is currently set to the label. We use timer events to change the image on each label. Also notice the use of a messagebox to protect the program from accidental quitting. The end result is shown below. ([whack\_a\_mole\_v5.py](../_static/Programs/whack_a_mole_v5.py)) ```python import tkinter as tk from tkinter import PhotoImage from tkinter import messagebox from random import randint def main(): # Create the entire GUI program program = WhackAMole() # Start the GUI event loop program.window.mainloop() class WhackAMole: STATUS_BACKGROUND = "white" NUM_MOLES_ACROSS = 4 MIN_TIME_DOWN = 1000 MAX_TIME_DOWN = 5000 MIN_TIME_UP = 1000 MAX_TIME_UP = 3000 def __init__(self): self.window = tk.Tk() self.window.title("Whack-a-mole") self.mole_frame, self.status_frame = self.create_frames() self.mole_photo = PhotoImage(file="mole.png") self.mole_cover_photo = PhotoImage(file="mole_cover.png") self.label_timers = {} self.mole_labels = self.create_moles() self.hit_counter, self.miss_counter, self.start_button, self.quit_button \ = self.create_status_widgets() self.set_callbacks() self.game_is_running = False def create_frames(self): mole_frame = tk.Frame(self.window) mole_frame.grid(row=0, column=0) status_frame = tk.Frame(self.window, bg=WhackAMole.STATUS_BACKGROUND) status_frame.grid(row=0, column=1, sticky=tk.E + tk.W + tk.N + tk.S, ipadx=6) return mole_frame, status_frame def create_moles(self): # Source of mole image: https://play.google.com/store/apps/details?id=genergame.molehammer mole_labels = [] for r in range(WhackAMole.NUM_MOLES_ACROSS): row_of_labels = [] for c in range(WhackAMole.NUM_MOLES_ACROSS): mole_label = tk.Label(self.mole_frame, image=self.mole_photo) mole_label.grid(row=r, column=c, sticky=tk.E + tk.W + tk.N + tk.S) self.label_timers[id(mole_label)] = None row_of_labels.append(mole_label) mole_labels.append(row_of_labels) return mole_labels def create_status_widgets(self): spacer = tk.Label(self.status_frame, text="", bg=WhackAMole.STATUS_BACKGROUND) spacer.pack(side="top", fill=tk.Y, expand=True) hit_label = tk.Label(self.status_frame, text="Number of Hits", bg=WhackAMole.STATUS_BACKGROUND) hit_label.pack(side="top", fill=tk.Y, expand=True) hit_counter = tk.Label(self.status_frame, text="0", bg=WhackAMole.STATUS_BACKGROUND) hit_counter.pack(side="top", fill=tk.Y, expand=True) spacer = tk.Label(self.status_frame, text="", bg=WhackAMole.STATUS_BACKGROUND) spacer.pack(side="top", fill=tk.Y, expand=True) miss_label = tk.Label(self.status_frame, text="Number of Misses", bg=WhackAMole.STATUS_BACKGROUND) miss_label.pack(side="top", fill=tk.Y, expand=True) miss_counter = tk.Label(self.status_frame, text="0", bg=WhackAMole.STATUS_BACKGROUND) miss_counter.pack(side="top", fill=tk.Y, expand=True) spacer = tk.Label(self.status_frame, text="", bg=WhackAMole.STATUS_BACKGROUND) spacer.pack(side="top", fill=tk.Y, expand=True) start_button = tk.Button(self.status_frame, text="Start") start_button.pack(side="top", fill=tk.Y, expand=True, ipadx=10) spacer = tk.Label(self.status_frame, text="", bg=WhackAMole.STATUS_BACKGROUND) spacer.pack(side="top", fill=tk.Y, expand=True) quit_button = tk.Button(self.status_frame, text="Quit") quit_button.pack(side="top", fill=tk.Y, expand=True, ipadx=10) spacer = tk.Label(self.status_frame, text="", bg=WhackAMole.STATUS_BACKGROUND) spacer.pack(side="top", fill=tk.Y, expand=True) return hit_counter, miss_counter, start_button, quit_button def set_callbacks(self): # Set the same callback for each mole label for r in range(WhackAMole.NUM_MOLES_ACROSS): for c in range(WhackAMole.NUM_MOLES_ACROSS): self.mole_labels[r][c].bind("
", self.mole_hit) self.start_button['command'] = self.start self.quit_button['command'] = self.quit def mole_hit(self, event): if self.game_is_running: hit_label = event.widget if hit_label['image'] == self.mole_cover_photo.name: # MISSED! Update the miss counter self.miss_counter['text'] = str(int(self.miss_counter['text']) + 1) else: # HIT! Update the hit counter self.hit_counter['text'] = str(int(self.hit_counter['text']) + 1) # Remove the mole and don't update the miss counter self.put_down_mole(hit_label, False) def start(self): if self.start_button['text'] == 'Start': # Change all the mole images to a blank image and # set a random time for the moles to re-appear on each label. for r in range(WhackAMole.NUM_MOLES_ACROSS): for c in range(WhackAMole.NUM_MOLES_ACROSS): the_label = self.mole_labels[r][c] the_label['image'] = self.mole_cover_photo time_down = randint(WhackAMole.MIN_TIME_DOWN, WhackAMole.MAX_TIME_DOWN) timer_object = the_label.after(time_down, self.pop_up_mole, the_label) self.label_timers[id(the_label)] = timer_object self.game_is_running = True self.start_button['text'] = "Stop" self.hit_counter['text'] = "0" self.miss_counter['text'] = "0" else: # The game is running, so stop the game and reset everything # Show every mole and stop all the timers for r in range(WhackAMole.NUM_MOLES_ACROSS): for c in range(WhackAMole.NUM_MOLES_ACROSS): the_label = self.mole_labels[r][c] # Show the mole the_label['image'] = self.mole_photo # Delete any timer that is associated with the mole the_label.after_cancel(self.label_timers[id(the_label)]) self.game_is_running = False self.start_button['text'] = "Start" def put_down_mole(self, the_label, timer_expired): if self.game_is_running: if timer_expired: # The mole is going down before it was clicked on, so update the miss counter self.miss_counter['text'] = str(int(self.miss_counter['text']) + 1) else: # The timer did not expire, so manually stop the timer the_label.after_cancel(self.label_timers[id(the_label)]) # Make the mole invisible the_label['image'] = self.mole_cover_photo # Set a call to pop up the mole in the future time_down = randint(WhackAMole.MIN_TIME_DOWN, WhackAMole.MAX_TIME_DOWN) timer_object = the_label.after(time_down, self.pop_up_mole, the_label) # Remember the timer object so it can be canceled later, if need be self.label_timers[id(the_label)] = timer_object def pop_up_mole(self, the_label): # Show the mole on the screen the_label['image'] = self.mole_photo if self.game_is_running: # Set a call to make the mole disappear in the future time_up = randint(WhackAMole.MIN_TIME_UP, WhackAMole.MAX_TIME_UP) timer_object = the_label.after(time_up, self.put_down_mole, the_label, True) self.label_timers[id(the_label)] = timer_object def quit(self): really_quit = messagebox.askyesno("Quiting?", "Do you really want to quit?") if really_quit: self.window.destroy() if __name__ == "__main__": main() ``` ### 15.31.2. Summary We developed a complete GUI application in 5 well-designed stages. Hopefully you see the value in incremental software development. However, the end result is not necessarily easy to understand or modify for future enhancements. The next lesson will introduce a scheme for breaking complete software into more managable pieces. ## 15.32. Managing GUI Program Complexity As we explained in a previous lesson, GUI programs are best implemented as Python classes because it allows you to manage the scope of the variables in your GUI interface and callback functions. However, as GUI programs become more complex, it can become overwhelming to implement them as a single class. If a single class has over than 2,000 lines of code it is probably getting too big to effectively manage. What are some ways to effectively break down complex problems into manageable pieces? One of the most widely used ways to break down a GUI program into manageable pieces is called the Model-View-Controller software design pattern. This is often abbreviated as **MVC** (Model-View-Controller). It divides a problem into three pieces: * Model - the *model* directly manages an application’s data and logic. If the model changes, the *model* sends commands to update the user’s view. * View - the *view* presents the results of the application to the user. It is in charge of all program output. * Controller - the *controller* accepts all user input and sends commands to the model to change the model’s state. To says this in more general terms, the *controller* manages the applications input, the *model* manages the application’s “state” and enforces application consistency, and the *view* updates the output, which is what the user sees on the screen. This is basically identical to what all computer processing is composed of, which is: ```python input --> processing --> output ``` The MVC design pattern renames the pieces and restricted which part of the code can talk to the other parts of code. For MVC design: ```python controller (input) --> model (processing) --> view (output) ``` From the perspective of a GUI program, this means that the callback functions, which are called when a user causes events, are the *controller*, the *model* should perform all of the application logic, and the building and modification of the GUI widgets composes the *view*. Let’s develop a Whack-a-mole game program using this design strategy. Instead of creating one Python Class for the entire game, the code will be developed as a set of cooperating objects. So where should we begin? I would suggest that the same stages of development we used in the previous lesson are a good approach, but we will create a separate Python `class` for most of the stages. Let’s walk through the code development. ### 15.32.1. Creating the *View* Let’s create a Python `class` that builds the user interface for a Whack-a-mole game. The emphasis for this code is the creation of the widgets we need to display to the user. For this version let’s allow the moles to be placed at random locations inside the left frame. To do this we must specify an exact size for the left frame. Otherwise the code is the same as the previous version. ```python Code ``` ### 15.32.2. Creating the *Model* The *model* for this Whack-a-mole game is fairly simple. We need to keep a counter for the number of user hits on moles that are visible, and a counter for the number of times a user clicks on a mole that is not visible (or just clicks on the left frame and not a mole widget.) ### 15.32.3. Creating the *Controller* The *controller* receives user events and sends messages to the *controller* to update the *model*’s state. For our Whack-a-mole game, we have the following four basic commands we need to send to the *model*: * A user clicked on something on the left frame. * The user wants to start a new game. (The user clicked on the “Start” button.) * The user wants to stop playing a game. (The user clicked on the “Stop” button.) * The user wants to quit the application. (The user clicked on the “Quit” button.) The *controller\** needs to recognize these events and send them to appropriate methods in the *model*. The *controller* needs to define callback functions for these events and register the appropriate event with the appropriate callback. Therefore, the *controller* needs access to the widgets in the *view* object. This can easily be accomplished by passing a reference to the *view* object to the *controller* when it is created. Summary ——- ## 15.33. Exercises This page left intentionally blank ## 15.34. Glossary icon : A small picture that represents some functionality in a computer program. A user clicks on an icon to cause an action to be performed by a program. widget : A visual element of a graphical user interface that allows a user to give commands to an executing program. Example widgets include a `command button`, a `slider bar`, or a `list box`. graphical user interface : A user interacts with a computer program by pointing, clicking, and dragging icons and widgets in a window on a computer screen. GUI : An abbreviation for a “graphical user interface.” event-driven programming : A program that only executes tasks when a user specially requests a task. event loop : A built-in function of a GUI toolkit that “listens” for operating system events and then calls an appropriate event-handler for each event. event-handler : A function that processes an event. These functions are also called `callback functions`.