10.8. A Complete Event-Based GUI Example

In this chapter we have shown the steps in creating an event-based graphical program that gives the end user an interface and working area or canvas. The steps are:

We’ve shown these steps in bits and pieces, and in this section, we want to show a more complex example that pulls all of this together in one program. The complete example is shown below. You should copy and paste this into a blank CodeSkulptr3 window to run it and see how it works. The code is fairly well commented, but in the text below the code we draw your attention to some important aspects of how this all works together.

  1# """"""""""""""""""""""""""""""""""""""""""""""""""""""
  2# Dr. Celine's COMP 1000 Event-based Programming Example
  3# """"""""""""""""""""""""""""""""""""""""""""""""""""""
  4
  5import simplegui
  6
  7# Global Variables
  8CANVAS_WIDTH = 400
  9CANVAS_HEIGHT = 400
 10# current drawing state
 11line_col = "Black"
 12line_wid = 3
 13fill_col = "Red"
 14stamp_text = "Hello!"
 15radius = 10
 16fontsize = 12
 17canvas_col = "Grey"
 18# list vars to store info about circles
 19circle_list = []
 20circle_col_list = []
 21circle_line_wid_list = []
 22# list vars to store info about text stamps
 23text_list = []
 24text_pos_list = []
 25text_col_list = []
 26# boolean variable to differentiate clicks/drags
 27dragged = False # we are not dragging when program starts
 28
 29################################################################
 30# GUI Control Handlers
 31################################################################
 32
 33def clear_handler():
 34    """
 35    Gets called when clear button is clicked
 36    Clears all lists to remove content from canvas
 37    """
 38    clear_canvas()
 39
 40def bkg_handler():
 41    """
 42    Background toggle button - toggles between white and grey
 43    """
 44    toggle_background()
 45
 46def lw_up_handler():
 47    """
 48    Linewidth + handler
 49    """
 50    change_line_width(True)
 51
 52def lw_dn_handler():
 53    """
 54    Linewidth - handler
 55    """
 56    change_line_width(False)
 57
 58def stamp_txt_handler(txt):
 59    """
 60    Stamp text input box handler
 61    When user types in text and then hits the enter key, this is called
 62    """
 63    set_stamp_text(txt)
 64
 65def draw(canvas):
 66    """
 67    Draw handler, takes canvas as input, keeps canvas up to date
 68    This is called automatically, many times/second, as part of the SimpleGUI module
 69    DO NOT call this function from other parts of the code
 70    """
 71    for index in range(len(circle_list)):
 72        canvas.draw_circle(circle_list[index], radius, circle_line_wid_list[index], line_col, circle_col_list[index])
 73
 74    for index in range(len(text_list)):
 75        canvas.draw_text(text_list[index],text_pos_list[index], fontsize, text_col_list[index])
 76
 77###########################################################
 78# Input Device Event Handlers
 79###########################################################
 80
 81# Handler for mouse drag events. Takes one parameter:
 82#   a tuple of the current position of the mouse
 83# This gets called continuously while the user is dragging
 84def drag(pos):
 85    """
 86    Mouse drag handler
 87    """
 88    global dragged
 89    if not dragged: # start of a drag, store that we are dragging
 90        dragged = True
 91    add_circle(pos)
 92
 93
 94# Handler for mouse click events. Takes one parameter:
 95#   a tuple of the position of the mouse at moment of click
 96def click(pos):
 97    """
 98    Mouse click handler, if a real click (not end of drag)
 99    this adds a text stamp to the canvas at location of click
100    """
101    global dragged
102    if dragged:
103        # this was just the end of drag, not a real click
104        # don't do anything
105        dragged = False
106    else:
107        add_text_stamp(pos)
108
109# Keypress handler
110def key_handler(key):
111    """
112    Handles key presses
113    """
114    if chr(key) == 'R':
115        set_fill_color("Red")
116    elif chr(key) == 'G':
117        set_fill_color("Green")
118    elif chr(key) == 'B':
119        set_fill_color("Blue")
120    elif chr(key) == 'C':
121        clear_canvas()
122    elif key == 38:
123        change_line_width(True)
124    elif key == 40:
125        change_line_width(False)
126    else:
127        #do nothing
128        print("Unknown key event. Try pressing r, g, or b")
129        print("key is:", key)
130
131
132###############################
133# Other Functions
134###############################
135
136def clear_canvas():
137    """
138    Clears all lists to remove content from canvas
139    """
140    circle_list.clear()
141    circle_col_list.clear()
142    circle_line_wid_list.clear()
143    text_list.clear()
144    text_pos_list.clear()
145    text_col_list.clear()
146
147def change_line_width(up):
148    """
149    Increases line width by 1 if true is passed,
150    otherwise, decreases line width
151    """
152    global line_wid
153    MIN_LINE_WIDTH = 1
154    MAX_LINE_WIDTH = 5
155    if (up): # increase
156        if line_wid < MAX_LINE_WIDTH:
157            line_wid += 1
158    else: # decrease
159        if line_wid > MIN_LINE_WIDTH:
160            line_wid -= 1
161    lw_label.set_text("Line width: " + str(line_wid))
162
163def set_fill_color(col):
164    """ updates fill color for subsequent drawing, updates label """
165    global fill_col
166    fill_col = col
167    fc_label.set_text("Fill color: " + str(fill_col))
168
169def toggle_background():
170    """ Toggle canvas background between white & grey
171        updates button text """
172    if (bkg_button.get_text() == 'White Background'):
173        canvas_col = "White"
174        bkg_button.set_text('Grey Background')
175    else:
176        canvas_col = "Grey"
177        bkg_button.set_text('White Background')
178    frame.set_canvas_background(canvas_col)
179
180def set_stamp_text(txt):
181    """ updates stamp text for subsequent drawing, update label """
182    global stamp_text
183    stamp_text = txt
184    text_stamp_label.set_text("Text stamp: " + stamp_text)
185    inp.set_text("")
186
187def add_text_stamp(pos):
188    """ add a new text stamp to the list of text stamps"""
189
190    text_list.append(stamp_text)            # store stamp text
191    text_pos_list.append(pos)               # store stamp location
192    text_col_list.append(fill_col)          # store stamp color
193
194def add_circle(pos):
195    """ Add a circle to the circle list"""
196    circle_list.append(pos)                 # store circle position
197    circle_col_list.append(fill_col)        # store color for circle
198    circle_line_wid_list.append(line_wid)   # store circle line-width
199
200
201#######################################################
202# Set up window, GUI controls & register event handlers
203#######################################################
204
205# Frame
206frame = simplegui.create_frame("COMP 1000 Demo", CANVAS_WIDTH, CANVAS_HEIGHT)
207frame.set_canvas_background(canvas_col)
208
209# Create & Register Buttons & Labels
210# assign labels and bkgd button to vars for updating
211frame.add_button('Clear', clear_handler)
212bkg_button = frame.add_button('White Background', bkg_handler)
213fc_label = frame.add_label("Fill color: " + str(fill_col))
214lw_label = frame.add_label("Line width: " + str(line_wid))
215frame.add_button('+', lw_up_handler)
216frame.add_button('-', lw_dn_handler)
217text_stamp_label = frame.add_label("Text stamp: " + stamp_text)
218inp = frame.add_input('New text stamp:', stamp_txt_handler, 50)
219
220
221# Register Keyboard and Mouse Event Handlers
222frame.set_draw_handler(draw)
223frame.set_keydown_handler(key_handler)
224frame.set_mousedrag_handler(drag)
225frame.set_mouseclick_handler(click)
226
227# Show the frame and start listening
228frame.start()

The code uses big comments with lots of #### marks to section off different parts: the global variables at the top (lines 7-27), the function handlers for GUI controls (lines 29-75) and for input device events (lines 77-129), other functions (lines 132-198) and then the code at the bottom that sets up the GUI, registers the event handlers and tells Python to start listening (lines 201-228).

The way this code works is that there is some drawing state that is saved in the collection of global variables. As the user interacts with the canvas, circles are drawn when the user drags and text is stamped when the user clicks. The color and size of the circles, the color and fontsize of the text, and the background color of the canvas are determined by the values of the global variables. By interacting with the GUI controls, the user can change some of these things (the fill color of the circles/text, the outline width for the circles). The user can also change the value of the text stamp by typing text into the text input box and hitting enter. Whenever such changes are made, they only impact subsequent drawing actions on the canvas.

Everything that the user draws on the canvas (which in this case is only circles and text stamps) is stored in a series of lists. There are three ‘parallel’ lists to store information about the circles: the position, the color, and the outline width. So, every time a new circle is made because the user continues to drag the mouse, a position, a color and an outline width is added to the three lists that store this information. Thus, these three lists will always all be the same length. Similarly, every time the user clicks on the canvas, a text stamp is added. This involves storing the position, the text string, and the color, in three separate lists.

Some of the drawing state is changed via key presses. To change the fill color for circles/text, the user has to press ‘r’, ‘g’, or ‘b’ on their keyboard (see lines 114-119, which all call the set_fill_color() function).

The user can clear the canvas two ways: by clicking on the clear button, or by typing ‘c’ on the keyboard. Note that both of these handlers do the same thing: they call the clear_canvas() function. In fact, all of the handlers simply call another function that does the work. While we could have just put the code directly in the handler, it is better to have separate functions so that the code can be invoked in other ways.

The canvas background color is not something that we have to draw as part of the draw() method - it is drawn automatically for us by the SimpleGUI module. We can specify the color of the background, though, which we do after we create the initial frame, see line 207. In addition, the user can toggle the background color between grey and white by pressing the <color> background button. Note that this button always shows the other color. So, if the background is currently grey, the button says “White background” telling the user what will happen if they press the button. If you look at the code for the toggle_background() function on lines 169-178, you’ll note that this code checks what the current background color is, sets the background color to the other one, sets the button to label to say the opposite, and then calls set_canvas_background() to actually update the canvas background color.

10.8.1. Practice Exercises

You should play with this code and modify it in different ways to help yourself explore, understand and practice using event-based programming. Here are a few things to try:

  • add more colours associated with other keyboard letters

  • add font size buttons for small, medium and large, along with a global variable for font size

  • add a ‘clear stamps’ button that, when pressed, clears only the text stamps, but not the circles

10.8.2. Global Variables in Event-Based Programs

As you look through this code, you might have observed that we have been editing global variables throughout. You may be thinking “I thought we weren’t supposed to do that???”. Remember that the typical way to avoid using global variables is to pass the information around as parameters and return values. But when the action in the program is handled by event handlers that the Python system calls, we can’t add arbitrary parameters, and we don’t want to return values to the operating system that called our event handler functions. That is why we are having to write new values to these global variables. This is okay for this class, because you are just learning. But it isn’t elegant, and programmers like things to be elegant. Most programmers would consider the code above to be quite clunky because of the use of global variables and parallel lists to store information about circles and text stamps.

You may be wondering if there is a better way to do this. And there is. The better way to handle sets of information like what we see in this example is through object-oriented programming. You’ve played with object-oriented programming a bit already - we introduced it in Chapter 4 when we introduced the Turtles module. In using Turtles you have been creating objects (turtles and screens) and calling methods on objects (like forward() and pen_down()). In the next section, you will see a version of the program above completely rewritten in an object-oriented fashion, and not a single global variable is assigned in any of the functions.

You have attempted of activities on this page