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:
Set up any global variables that are neceesary
Create GUI controls
Create handler functions for the controls
Create handler functions for input on the working area or canvas
Register the event handlers for the controls
Register input listeners for keyboard and mouse events on the canvas
Create a draw() function that draws the working area of canvas
Tell the Python interpreter to start listening for events on the canvas and the controls
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.