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

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, everytime 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 bbe the same length. Similarly, everytime 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 change 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 110-121, which all call the set_fill_color() function on lines 159-163).

The user can clear the canvas two ways: by pressing the clear button, or by pressing ‘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 lines 212 and 213. 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 164-173, 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 the SimpleGUI frame method, set_canvas_background() to actually update the canvas background color.

11.8.1. 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 of 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