Event-Driven Programming
Responding to events within our GUIs!
Responding to events within our GUIs!
So far, we’ve learned to create a GUI and switch between panels in the GUI, but we’ve not really looked at how to make our GUI buttons responsive and perform the actions we want when the user clicks on them. In this chapter, we’ll dive into event-driven programming, which is the programming paradigm we use to construct applications that use GUIs and event handlers.
We’ll see how we can build our application to include multiple threads, making our application appear responsive to the user even if the application is performing calculations in another thread. This will build on the parallelism we learned in a prior chapter.
Some key terms we’ll cover in this chapter:
After this chapter, we’ll be able to update our applications to respond to user button clicks and other events.
Up to this point, we’ve only created applications that use a single thread. However, now that we are writing applications that include a GUI, we must start to build applications that use multiple threads to manage its work. Otherwise, if the application is busy working on a particular task while the user clicks a button in the GUI, the GUI won’t respond to the user until our task is complete.
To resolve this, we typically build our GUI applications in a way that the GUI runs in a separate thread from the rest of our application. In that way, the GUI is always responsive to the user, and our application can continue to do whatever it needs in additional threads. Those threads won’t impact the responsiveness of our GUI, at least if they are constructed properly.
This leads to a new programming paradigm called event-driven programming. Event-driven programming can be thought of as an alternative to imperative programming, though in practice both paradigms are used within the same program. In imperative programming, the program follows a set sequence of steps to perform an action, directly as defined in the program’s source code. In event-driven programming, the steps a program takes are determined by an external factor that generates events within the system. The program will receive those events, and then use the event received to decide what steps, if any, to perform. Of course, the process of waiting for events, receiving them, and acting upon them, is usually all done through imperative code.
Consider the diagram above. In it, a user interacts with a button in an application, which is an event. That event triggers some piece of code, which is typically called an event handler, to react to that event. The event handler examines the event, and performs the requested action.
Behind the scenes, there is another piece of code, called the event loop, that is actually watching for these events and calling the appropriate event handlers for us.
On the next few pages, we’ll dive into each of these steps in building a responsive GUI using event-driven programming.
The first step in building a program using event-driven programming is actually creating the events that you’d like to respond to. For a GUI-based program, this is actually handled by the GUI framework itself. It includes a large number of events that are already available for us to use. Instead, we have to bind those events to special functions, called event handlers, within our code. Then, when those events occur, the GUI framework will call the event handler associated with that event.
Most GUI frameworks include a large number of events that we can bind our event handlers to. There are some obvious ones, such as the clicked event for a button, or the value changed event for checkboxes, but there are actually many events that are generated by our GUI that we might not have thought of. Here are just a few events we can typically find in our GUI framework:
x
and y
coordinates of the cursor at any time through this event.Each GUI framework can handle many different events. You can find a list of some of them at the following resources:
Both Java Swing and Python tkinter include simple ways to bind an event to a function. In fact, we’ve already seen a bit of how to do that in a previous example - we created a few buttons that called a function when clicked! That is a great example of binding the clicked event to a function in our code.
Here’s a more general example in both Java and Python, this time binding the mouse moved event:
Excerpted in part from How to Write a Mouse-Motion Listener from Oracle.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class MouseDemo extends JFrame implements MouseMotionListener {
public MouseDemo() {
this.setMinimumSize(new Dimension(800, 600));
// ------------------------------
// This is the **bind** action
this.addMouseMotionListener(this);
// ------------------------------
}
// ----------------------------------------------
// These functions are the **event handlers**
// This is when the mouse is moved but not clicked
public void mouseMoved(MouseEvent e) {
this.output("Mouse Moved", e);
}
// This is when the mouse is moved while being clicked
public void mouseDragged(MouseEvent e) {
// this.output("Mouse Dragged", e);
}
// ----------------------------------------------
private void output(String event, MouseEvent e) {
System.out.println(event + " : " + e.getX() + "," + e.getY());
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new MouseDemo().setVisible(true);
}
});
}
}
import sys
import tkinter as tk
from typing import List
class MouseDemo(tk.Tk):
def __init__(self) -> None:
"""Initializer for GUI."""
tk.Tk.__init__(self)
self.minsize(width=800, height=600)
# --------------------------------
# This is the **bind** action
self.bind('<Motion>', motion)
# --------------------------------
def action_performed(self, event) -> None:
"""Event handler for GUI events."""
print("Mouse Moved : {},{}".format(event.x, event.y))
@staticmethod
def main(args: List[str]) -> None:
"""Main method."""
MouseDemo().mainloop()
# Main Guard
if __name__ == "__main__":
MouseDemo.main(sys.argv)
In both of these examples, we see how to bind an event to a function. In Java Swing, we need to add a specific type of “listener” to the object, which is an event handler in Java Swing terminology. We then implement the associated interface, which defines the function(s) we must include to react to those events. In this case, we implement the MouseMotionListener
interface.
In Python tkinter, we literally use a method called bind
along with the name of the event we’d like to bind and the function we’d like to call. This is much more straightforward, but we don’t have the benefit of a compiler and type checker making sure that our events are bound correctly, nor that we’ll get the correct data out of them. So, we have to be a bit more careful as well.
On the next page, we’ll go a bit deeper into event handlers, the functions that are called when the events occur.
At its core, an event handler is simply a piece of code that is called when a particular event happens within the GUI. The function typically receives additional information about the event, such as the source of the event and any relevant details. In some languages, we also refer to event handlers as callbacks.
In Java, most events are handled by special interfaces called “listeners” that we can implement within our code. When we bind to an event in Java Swing, we specify an object that is instantiated from a class that implements the appropriate listener for that event. Then, when the event happens, behind the scenes Java will find the associated object and call the correct method defined as part of the interface. Here’s the example from the previous page:
// ----------------------------------------------
// These functions are the **event handlers**
// This is when the mouse is moved but not clicked
public void mouseMoved(MouseEvent e) {
this.output("Mouse Moved", e);
}
// This is when the mouse is moved while being clicked
public void mouseDragged(MouseEvent e) {
// this.output("Mouse Dragged", e);
}
// ----------------------------------------------
These two methods are defined in the MouseMotionListener interface. When the event happens, it sends along an instance of the MouseEvent class that contains the information about the event, such as the location of the mouse and any buttons that are pressed.
In Python, events are typically sent directly to a function that is sometimes referred to as a “callback” function. So, when we bind to an event in Python tkinter, we simply specify the function that’d like to be called when the event happens. If we want to capture some additional data along with the function, we can do that using a lambda expression, which we saw in the earlier GUI example project.
Here’s the example callback function from the previous page:
def action_performed(self, event) -> None:
"""Event handler for GUI events."""
print("Mouse Moved : {},{}".format(event.x, event.y))
When the '<Motion>'
event occurs, Python will call this function and pass along a second parameter, which we’ve named event
in this example. The event
object contains the x
and y
coordinates of the mouse, but may also contain different information based on the event that was generated. Unfortunately, there isn’t a good source of documentation for all of these events and what information they contain, so you may have to do a bit of digging to figure out what you can expect from each type of event.
Most GUI frameworks, such as Java Swing and tkinter, handle user interactions through the use of a loop, which runs within the GUI thread and listens for events generated by the user.
When an event is generated by the user, it is first placed in an event queue, which is a queue-based data structure used to keep track of events generated by the user. The events are placed there by a thread in the program, usually part of the GUI framework, that is connected and “listening” for events from the operating system. We use a queue to keep track of events, just in case the user generates events more quickly than our program can handle them.
Then, in the GUI thread of our program, there is a loop of code that is constantly checking if the event queue contains any elements. We typically refer to this code as the event loop. If it does, it will take the first element from the queue and examine it. If that event is bound with a known event handler somewhere in our code, then the event loop will call the event handler, shown in the diagram above as a “callback” function.
Once the event handler has returned, the event loop will take the next event from the queue and act upon it, and it will continue to do so until there are no events left in the queue.
In some GUI frameworks, the event loop is also responsible for updating the GUI on the screen itself, as shown in the diagram above. So, while the event handlers are executing, the GUI screen itself cannot be updated and the application will appear to “freeze.”
So, it is very important for our event handlers to be very short and execute quickly, or else the user might notice that our application is not responsive. If the event requires a large amount of calculation, we may want to create a separate thread to handle that operation. Thankfully, most simple GUI programs will not require this, but it is always something to be aware of in case our application starts running slowly.
On the next two pages, we’ll briefly discuss the event loop for both Java Swing and Python tkinter. As always, you may skip to the language you are learning, but it may be helpful to see how both languages perform a similar task.
The Java Swing GUI toolkit uses a special thread called the Event Dispatch Thread (abbreviated EDT) to handle events. This thread is where most of the code that interacts with a GUI written in Swing actually runs, leaving the main application thread to do other tasks as needed.
In all of our examples so far, we have observed a unique piece of code in the main()
method that is used to actually launch our GUI-based programs:
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new MouseDemo().setVisible(true);
}
});
The SwingUtilities.invokeLater()
method is used within the application thread to run code within the EDT. So, when we launch our application, the first thing we do is construct a new anonymous class that implements the Runnable interface, which defines an object that can be run as a thread. Inside of that class, we place the to code to construct our GUI and make it visible in the run()
method.
In the background, the first time we call SwingUtilities.invokeLater()
, Java will see that there is no EDT running and will spawn that thread. Once it is running, then at some time in the future the run()
method of our anonymous class will be called, which actually loads the GUI within the EDT.
The other important task performed by the EDT is actually responding to events from the operating system. So, when it isn’t actively running an event handler, the EDT is the thread that is constantly checking the event queue for any new events.
When an event is received, it looks up the event’s associated GUI element, and then checks to see if any listeners are registered with that object for that type of event. If so, then it finds the listener object and calls the appropriate method for that event.
As discussed earlier, we need to make sure our event handlers do not take too long to complete. Otherwise, we’ll end up slowing down the EDT and making it respond more slowly to events. In addition, this will make our entire GUI appear to “lag” for the user, which is definitely something we want to avoid.
In Python tkinter, we have an event loop that runs in the background, handling all of the GUI updates as well as responding to any events in the event queue by calling the appropriate callback function.
In all of our examples so far, we have observed a unique piece of code in the main()
method that is used to actually launch our GUI-based programs:
MainWindow().mainloop()
The tk.Tk.mainloop()
method is used to start the event loop attached to the top-level tk.Tk
object in our GUI. This is a blocking function, meaning that it will not return as long as the GUI is running, even when it isn’t visible to the user. So, in effect, any code after this in our main()
method that is after this function call will not be executed. Instead, that thread is constantly working to update the GUI on the screen and making sure that events are handled quickly.
Therefore, if we need to create an additional thread for our application, we typically will do so before calling this mainloop()
method in our main()
method. We can also create new threads from within the event loop thread as needed.
The other important task performed by the event loop is actually responding to events from the operating system. So, when it isn’t actively running a callback, the event loop is the thread that is constantly checking the event queue for any new events.
When an event is received, it looks up the event’s associated GUI element, and then checks to see if any callbacks are registered with that object for that type of event. If so, then it calls the callback function for that event.
As discussed earlier, we need to make sure our event handlers do not take too long to complete. Otherwise, we’ll end up slowing down the event loop and making it respond more slowly to events. In addition, this will make our entire GUI appear to “lag” for the user, which is definitely something we want to avoid.
In this chapter, we learned about event-driven programming and how to configure our GUI-based programs to respond to actions taken by the user.
When the user interacts with our GUI, an event is created by the operating system and placed in the event queue. Then, our program uses an event loop to check the queue for incoming events, and respond to them. When an event is found, our program determines if that event has been bound to a particular event handler, also known as a listener or callback. If so, it calls the appropriate function handle that event.
The event loop is typically run in a separate thread in our program, and we must make sure that any operations performed on that thread are quick enough to prevent any lag in our GUI.
In the example project for this chapter, we’ll explore how to add some event handlers to our GUIs.
Check your understanding of the new content introduced in this chapter below - this quiz is not graded and you can retake it as many times as you want.
Quizdown quiz omitted from print view.