Event Driven Programming

What will we cover?

So far we have been looking at batch oriented programs. Recall that programs can be batch oriented, whereby they start, do something then stop, or event driven where they start, wait for events and only stop when told to do so - by an event. How do we create an event driven program? We'll look at this in two ways - first we will simulate an event environment then we'll create a very simple GUI program that uses the operating system and environment to generate events.

Simulating an Event Loop

Every event driven program has a loop somewhere that catches received events and processes them. The events may be generated by the operating environment, as happens with virtually all GUI programs or the program itself may go looking for events as is often the case in embedded control systems such as used in cameras etc.

We will create a program that looks for precisely one type of event - keyboard input - and processes the results until some quit event is received. In our case the quit event will be the space key. We will process the incoming events in a very simple manner - we will simply print the ASCII code for that key. We'll use Python for this because it has a nice, easy to use function for reading keys one at a time - getch(). This function comes in two varieties depending on the operating system you use. If you are using Linux it's found in the curses module, if you use Windows it's in the msvcrt module. I'll use the Windows version first then I'll discuss the Linux option in more detail. You will need to run these programs from an OS command prompt since any IDE, like IDLE, being a GUI, will capture the keystrokes differently.

First we implement the event handler functions that will be called when a keypress is detected then the main program body which simply starts up the event gathering loop and calls the appropriate event handling function when a valid event is detected.

The Windows version

import msvcrt
import sys

# First the event handlers
def doKeyEvent(key):
    if key == '\x00' or key == '\xe0':
       key = msvcrt.getch()
    print ( ord(key), ' ', end='')
    sys.stdout.flush() # make sure it appears on screen
    
def doQuit(key):
    print() # force a new line
    raise SystemExit

# first clear some screen space
lines = 25 
for n in range(lines): print()

# Now the main event-loop
while True:
    ky = msvcrt.getch()
    if len(str(ky)) != 0:
        # we have a real event
        if " " in str(ky):
            doQuit(ky)
        else: 
            doKeyEvent(ky)

Notice that what we do with the events is of no interest to the main body, it simply collects the events and passes them to the event handlers. This independence of event capture and processing is a key feature of event driven programming.

Note 1: getch() returns bytes so we need to convert them to a string and since the resulting string is not simply the letter we need to use the in test to check for the quit condition.

Note 2: Where the key was non ASCII - a Function key for example - we needed to fetch a second character from the keyboard, this is because these special keys actually generate pairs of bytes and getch only retrieves one at a time. The actual value of interest is the second byte.

THe Linux/MacOS version

Linux and MacOS X programmers can't use the msvcrt library so must use another module called curses instead. The resultant code is very similar to the windows code but there are a few modifications required, as shown below:

import curses as c

def doKeyEvent(key):
    if key == '\x00' or key == '\xe0': # non ASCII key
       key = screen.getch()     # fetch second character
    screen.addstr(str(key)+' ') # uses global screen variable

def doQuitEvent(key):
    c.resetty() # set terminal settings
    c.endwin()  # end curses session
    raise SystemExit


# clear the screen of clutter, save current settings, stop characters auto 
# echoing to screen and then tell user what to do to quit
screen = c.initscr()
c.savetty()
c.noecho()
screen.addstr("Hit space to end...\n")

# Now mainloop runs "forever"
while True:
     ky = screen.getch()
     if ky != -1:
       # send events to event handling functions
       if ky == ord(" "): # check for quit event
         doQuitEvent(ky)
       else: 
         doKeyEvent(ky)

c.endwin()

You'll see that the usual print commands don't work under curses and instead we have to use curses own screen handling functions. Further, curses getch returns -1 when there is no key pressed rather than an empty string. Aside from that the logic of the program is identical to the Windows version.

Note that the curses.endwin() should restore your screen to normal but in some cases it may not work completely. If you wind up with an invisible cursor, no carriage return etc. You can fix it if you exit Python with Ctrl-D and use the Linux command:

$ stty echo -nl

That's 'nl' for NewLine by the way, not n-one. Hopefully that will restore things to normal.


If we were creating this as a framework for use in lots of projects we would probably include a call to an initialization function at the start and a cleanup function at the end. The programmer could then use the loop part and provide his own initialization, processing and cleanup functions.

That's exactly what most GUI type environments do, in that the loop part is embedded in the operating environment or framework and applications are contractually required to provide the event handling functions and hook these into the event loop in some way.

Let's see that in action as we explore Python's Tkinter GUI library.

A GUI program

For this exercise we'll use the Python Tkinter toolkit. This is a Python wrapper around the Tk toolkit originally written as an extension to the Tcl programming language ()and also available for Perl and Ruby). The Python version is an object oriented framework which is, in my opinion, considerably easier to work with than the original Tk version. We will look much more closely at the principles of GUI programming in the GUI topic.

I am not going to dwell much on the GUI aspects in this topic, rather I want to focus on the style of programming - using Tkinter to handle the event loop and leaving the programmer to create the initial GUI and then process the events as they arrive.

In the example we create an application class KeysApp which creates the GUI in the __init__ method and binds the space key to the doQuitEvent method. The class also defines the required doQuitEvent method.

The GUI itself simply consists of a text entry widget whose default behavior is to echo characters typed onto the display.

Creating an application class is quite common in OO event driven environments because there is a lot of synergy between the concepts of events being sent to a program and messages being sent to an object. The two concepts map on to each other very easily. An event handling function thus becomes a method of the application class.

Having defined the class we simply create an instance of it and then send it the mainloop message.

The code looks like this:


# Use from X import * to save having to preface everything 
# as tkinter.xxx
from tkinter import *
import sys
        

# Create the application class which defines the GUI 
# and the event handling methods
class KeysApp(Frame):
    def __init__(self): # use constructor to build GUI
        super().__init__()
        self.txtBox = Text(self)
        self.txtBox.bind("<space>", self.doQuitEvent)
        self.txtBox.pack()
        self.pack()

    def doQuitEvent(self,event):
        sys.exit()
        

# Now create an instance and start the event loop running
myApp = KeysApp()
myApp.mainloop()

Note: If you run this from inside IDLE you will find the program doesn't close properly but simply prints an exit message in the shell window. Don't worry, that's just IDLE trying to be helpful. If you run it from a command prompt everything should be just fine.

Notice that we don't even implement a key event handler! That's because the default behavior of the Text widget is to print out the keys pressed. However that does mean our programs are not really functionally equivalent. In the console version we printed the ASCII codes of all keys rather than only printing the alphanumeric versions of printable keys as we do here. There's nothing to prevent us capturing all of the keypresses and doing the same thing. To do so we would add the following line to the __init__ method:

self.txtBox.bind("<Key>", self.doKeyEvent)

And the following method to process the event:

def doKeyEvent(self,event):
    str = "%d\n" % event.keycode
    self.txtBox.insert(END, str)
    return "break"

Note 1: the key value is stored in the keycode field of the event. I had to look at the source code of Tkinter.py to find that out... Recall that curiosity is a key attribute of a programmer?!

Note 2: return "break" is a magic signal to tell Tkinter not to invoke the default event processing for that widget. Without that line, the text box displays the ASCII code followed by the actual character typed, which is not what we want here.

That's enough on Tkinter for now. This isn't meant to be a Tkinter tutorial, that's the subject of the next topic. There are also several books on using Tk and Tkinter.

Event Driven Programming in VBScript and JavaScript

Both VBScript and JavaScript can be used in an event driven manner when programming a web browser. Normally when a web page containing script code is loaded the script is executed in a batch fashion as the page loads. However if the script contains nothing but function definitions the execution will do nothing but define the functions ready for use, but the functions will not be called initially. Instead, in the HTML part of the page the functions will be bound to HTML elements - usually within a Form element - such that when events occur the functions are called. We have already seen this in the JavaScript example of getting user input, when we read the input from an HTML form. Let's look at that example again more closely and see how it really is an example of event driven programming within a web page:

<form name='entry'>
<p>Type value then click outside the field with your mouse</p>
<input Type='text' 
          Name='data' 
          onChange='alert("We got a value of " + document.entry.data.value);'/>
</form>

Notice that there is no JavaScript function defined as such, just a call to alert which is associated with the onChange attribute of the input element. onChange is in fact one of many events which HTML elements can generate. We can hook up arbitrary JavaScript code to fire whenever the events occur. We could create a function and call it instead of the alert call. Let's see how that looks:

<script type="text/javascript">
function echoValue(){
   alert("We got a value of " + document.entry.data.value);
}
</script>

<form name='entry'>
<p>Type value then click outside the field with your mouse</p>
<input Type='text' Name='data' onChange='echoValue()'/>
</form>

The script part simply defines a JavaScript function, echoValue, that replicates the alert call we had earlier. The input element now has that function assigned as its onChange event handler. The function is then executed when the input value changes. The event loop that detects the event is embedded inside the browser.

Although our function simply called alert it could have done much more. In fact, it could have been a complex program in its own right, whatever we chose to put inside the function body.

VBScript can be used in exactly the same way except that the function definitions are all in VBScript instead of JavaScript, like this:

<script type="text/vbscript">
Sub EchoInput()
   MsgBox "We got a value of " & Document.entry2.data.value
End Sub
</script>

<form name='entry2'>
<p>Type value then click outside the field with your mouse</p>
<input Type='text' Name='data' onChange='EchoInput()'/>
</form>

Thus we can see that web browser code can be written in batch form or event driven form or a combination of styles to suit our needs.

Things to remember

Previous  Next