Chapter 6

Chapter Sections
Section 6.1: Widgets and Grids (problems)
Section 6.2: Callbacks (problems)
Section 6.3: Random Circles
Section 6.4: The Bouncing Ball Example
Section 6.5: Sprite Animations

Section 6.1: Widgets and Grids

Introduction

A GUI (Graphical User Interface) is a program that allows users to interact with the program using graphical icons and other images resembling components such as buttons, check boxes, sliders, radio buttons, etc. The user can interact with these graphical components by clicking on them and entering text. In this chapter, we are going to use a library of code called Tkinter, which is currently installed with a standard Python installation.

Tkinter Module from tkinter import *

Tkinter Root

All Tkinter GUIs use a root object which runs an event loop. The event loop is an infinite loop that is constantly checking for any input from the user. So for example, if the program has a button, the event loop will constantly check to see if any clicks of the button have been recorded by the mouse. Although the event loop is infinite, when the user exits the program, the event loop will break and the program will stop running, similar to using the break keyword.

Shortly after importing Tkinter, usually the programmer will want to create the root object. Just creating the root object does not start the event loop. Usually the event loop is started after all of the components have been created and setup. The root object is created early on in the instructions because it is required when creating the other components of the GUI. When creating the other components, the root is passed as an argument of that component's init method. When the component is created, it knows what the root for this GUI is and the root knows about the component so it can check on events associated with the component. The name of the root class is Tk. The code sample below also creates a button component.

Creating the Tkinter Root Object from tkinter import * @@ root = Tk() @ bt = Button( root, text = "Click Here" )

Widgets

In the Tkinter library, the individual components are classes called Widgets. Nearly everything graphical that the user can interact with is called a Widget. Below is a list of Widgets.

WidgetDescription
ButtonSimple widget that looks like a button and can be clicked by the user for a result.
EntrySimple widget that is really just a thin box for entering a single line of text. These are useful when you just need a short piece of text, like a name.
TextSimple widget that can be either small or take up the whole screen. This widget is used for entering multiple lines of text.
LabelThis is the simplest commonly used widget. Usually it just shows text that cannot be edited by the user. Often placed next to another widget to explain what the other widget is for.
SpinboxWidget with a funny name that makes entering numbers easier.
CheckbuttonSimple widget that has a box that the user can check off. This widget also has text that the user does not edit that describes what it is for. Useful for True/False inputs.
ScaleWidget that has a small image (like a handle) that can be moved along a slider. This can be a more intuitive way of entering a number.
CanvasAlthough it just appears on the screen as a box, it is actually a complex widget that is used to display graphical items.

Layouts

In order for the widgets to show up where we want them, we need what is called a layout. The user does not directly see the layout, but instead sees how the widgets are arranged by the layout. With Tkinter, the root manages the layout. The primary layout we will be using is the grid layout, which lines up widgets in a grid. The individual locations in the grid are called cells. The widgets are added to the layout using the .grid( ) method which assigns a row and column to the widget for the cell where it will be placed. Every widget, regardless of the type of widget, uses the .grid( ) method for this purpose.

When the .grid( ) method is executed, we must give the row and column arguments to the method. Below we use what are called named arguments to give these values. The named argument for row is row, and the named argument for the column is column. As well, the __init__ method for the Button class uses a named variable for a string that the button will display on the screen called text.

A 3x2 Grid of Buttons from tkinter import * @@ root = Tk() @@ bt1 = Button( root, text = "Button 1" ) @ bt2 = Button( root, text = "Button 2" ) @ bt3 = Button( root, text = "Button 3" ) @ bt4 = Button( root, text = "Button 4" ) @ bt5 = Button( root, text = "Button 5" ) @ bt6 = Button( root, text = "Button 6" ) @@ bt1.grid( row = 0, column = 0 ) @ bt2.grid( row = 1, column = 0 ) @ bt3.grid( row = 2, column = 0 ) @ bt4.grid( row = 0, column = 1 ) @ bt5.grid( row = 1, column = 1 ) @ bt6.grid( row = 2, column = 1 ) @@ mainloop()



Widgets That Span Multiple Cells

Sometimes a widget may need to take up more space than the other widgets around it. A good example of this is the Text widget which may need to be much larger to hold a large section of text. To accommodate large widgets we can have the widget span multiple rows and/or columns. To do this we need to give two more numbers to the .grid( ) method. These numbers determine how many rows and columns are filled up by the widget. The two arguments are called rowspan and columnspan.

Buttons and a Text Edit from tkinter import * @@ root = Tk()@@ bt1 = Button( root, text = "Button 1" ) @ bt2 = Button( root, text = "Button 2" ) @ bt3 = Button( root, text = "Button 3" ) @ bt4 = Button( root, text = "Button 4" ) @ txt = Text(root) @@ bt1.grid( row = 1, column = 0 ) @ bt2.grid( row = 2, column = 0 ) @ bt3.grid( row = 0, column = 1 ) @ bt4.grid( row = 0, column = 2 ) @ txt.grid( row = 1, column = 1, rowspan = 3, columnspan = 3 ) @@ root.rowconfigure( 0, weight = 0 ) @ root.rowconfigure( 1, weight = 0 ) @ root.rowconfigure( 2, weight = 0 ) @ root.rowconfigure( 3, weight = 1 ) @@ root.columnconfigure( 0, weight = 0 ) @ root.columnconfigure( 1, weight = 0 ) @ root.columnconfigure( 2, weight = 0 ) @ root.columnconfigure( 3, weight = 1 ) @@ mainloop()

When a widget occupies more than one row or column, the row and column arguments are for the upper left cell of the group of cells the widget spans.

Notice on lines 17-25, there are also two methods of the root class that are used, .rowconfigure() and .columnconfigure(). These two methods can be used to adjust attributes of the rows and columns. In the example above, these two methods are used to adjust the sizes of the rows and columns by giving the rows and columns different weights. The weights determine how much space the individual rows and columns are given compared to the other rows and columns. If the weight is set to zero, that row or column will have minimal space. In the above example, the rows and columns containing the buttons are given zero weight so that they are tightly packed on the screen.

Section 6.1 Name:____________________

Widgets and Grids


Score:      /5

Problems

  1. Write the code that will create a GUI resembling the following picture:















  2. Complete The Code from tkinter import * @@ root = Tk()@@ bt1 = Button( root, text = "Button 1" ) @ bt2 = Button( root, text = "Button 2" ) @ txt = Text(root) @@ @@@@@@@@@@@@ mainloop()
  3. Write the code that will create a GUI resembling the following picture:



















  4. Complete The Code from tkinter import * @@ root = Tk()@@ sp1 = Spinbox( root, from_ = 1, to = 100 ) @ sp2 = Spinbox( root, from_ = 1, to = 100 ) @ bt1 = Button( root, text = "Button 1" ) @ bt2 = Button( root, text = "Button 2" ) @ txt = Text(root) @@ @@@@@@@@@@@@ mainloop()
  5. Write the code that will create a GUI resembling the following picture:

















  6. Complete The Code from tkinter import * @@ root = Tk()@@ en_var = StringVar() @@ en = Entry( root, textvariable = en_var ) @ sp = Spinbox( root, from_ = 1, to = 100 ) @ bt1 = Button( root, text = "Button 1" ) @ bt2 = Button( root, text = "Button 2" ) @ txt = Text(root) @@ @@@@@@@@@@@@ mainloop()

Section 6.2: Callbacks

Introduction

So far we have created widgets and placed them in a layout. However, the widgets do not yet do anything. To program a response we need to understand how to set up callback functions in order to respond to different events, such as a user clicking a button or moving a scroll bar, etc. A callback function is simply any function that is set up to be called when an event occurs. To set up a callback function for a button, we need to use the .bind( ) method. The first argument of the bind method is a string that is a code for the event type. The second argument is the function that will be the callback. When we give the callback function name, we do not use paranthesis ( ) just the name of the function. Also note that for this example we an object of a class called StringVar, more on this below.

A Callback from tkinter import * @@ root = Tk() @@ en_var = StringVar() @@ en = Entry( root, textvariable = en_var ) @ bt = Button( root, text = "Copy" ) @ sp = Spinbox( root, from_ = 1, to = 100 ) @@ en.grid( row = 0, column = 0 ) @ bt.grid( row = 1, column = 0 ) @ sp.grid( row = 2, column = 0 ) @@ def show_number(event): @ ~global en @ ~global sp @ ~n = sp.get() @ ~en_var.set( n ) @@ bt.bind( "< Button-1>", show_number ) @@ mainloop()

Notice that the en_var object of the StringVar class is the named argument textvariable of the en object of the Entry class. It is the object en_var that stores the string value stored in the entry en. This class has methods .set( str_var ) and .get( ) to get and set the string stored in the variable, .set( str_var ) takes a string argument to set the string, .get( ) returns the string currently stored in the StringVar.

Some Events
Events Descriptions
"< Key>" Key press.
"< Button-1>" Left mouse click.
"< Button-2>" Middle mouse button click.
"< Button-3>" Right mouse click.
"< Double-Button-1>" Left mouse double click.
"< Double-Button-2>" Middle mouse button double click.
"< Double-Button-3>" Right mouse double click.
"< ButtonRelease-1>" Left mouse button release.
"< ButtonRelease-2>" Middle mouse button release.
"< ButtonRelease-3>" Right mouse button release.

Section 6.2 Name:____________________

Callbacks


Score:      /5

Problems

  1. Write the code for a GUI that has a button that when clicked takes a string stored in an Entry and displays it in another Entry. Create the objects, set the grid layout. You will also need a StringVar object. Then create a function to carry out the action and bind the function to the button.
  2. ~~~~~~~~~~ @@@@@ @@@@@ @@@@@ @@@@@ @@@@@ @@@
  3. Write the code for a GUI that has a Spinbox that when changed updates a Label with the number in the Spinbox.
  4. ~~~~~~~~~~ @@@@@ @@@@@ @@@@@ @@@@@ @@@@@ @@@@@
  5. Write the code for a GUI that has 3 buttons and a Label and and 3 functions that respond to each of the buttons. Each button should cause a different string to appear in the Label.
  6. ~~~~~~~~~~ @@@@@ @@@@@ @@@@@ @@@@@ @@@@@ @@@@@

Section 6.3: Drawing - Random Circle Example

Introduction

The Canvas widget is useful for displaying images. The view itself is simply a frame, but within that frame images can be seen. The Canvas class has methods for creating different geometrical figures and for opening and displaying image files.

A few methods of the Canvas class. Description
.create_rectangle( x, y, width, height, fill = color)Creates a rectangle at position (x, y).
.create_oval( x, y, width, height, fill = color)Creates a oval at position (x, y) which will fit into the rectangle given by the width and height.
.create_image( x, y, image = image_object )Paints an image that has already been loaded up onto the canvas. The image_object is an object of the PhotoImage class.

The example below creates randomly placed circles of random size and random color. The code to create the random numbers for the location, size, and color of the circles is contained inside of the make_circle function.

Random Circles from tkinter import * @ from random import * @@@ root = Tk() @@ WINDOW_SIZE = 400 @@ make_btn = Button( root, text = "Make Circle" ) @ sp = Spinbox( root, from_ = 1, to = 100 ) @ canvas = Canvas( root, width = WINDOW_SIZE, height = WINDOW_SIZE) @@ make_btn.grid( row = 1, column = 0 ) @ sp.grid( row = 1, column = 1 ) @ canvas.grid( row = 0, column = 0, rowspan = 1, columnspan = 3 ) @@ root.columnconfigure( 0, weight = 0 ) @ root.columnconfigure( 1, weight = 0 ) @ root.columnconfigure( 2, weight = 1 ) @@ # background @ canvas.create_rectangle(0, 0, WINDOW_SIZE+2, WINDOW_SIZE+2, fill = "white") @@@ def set_color_string( red, green, blue ): @ ~ color = "#" @ ~ comp = hex(red) @ ~ comp = comp[2:4] @ ~ color += comp @ ~ comp = hex(green) @ ~ comp = comp[2:4] @ ~ color += comp @ ~ comp = hex(blue) @ ~ comp = comp[2:4] @ ~ color += comp @ ~ return color @@ def make_circle( event ): @ ~ global canvas @ ~ global sp @ ~ count = int( sp.get() ) @ ~ i = 0 @ ~ while i < count: @ ~~ size = randint( 10, 100 ) @ ~~ X = randint( 0, 100 ) + randint( 0, 100 ) + randint( 0, 100 ) @ ~~ Y = randint( 0, 100 ) + randint( 0, 100 ) + randint( 0, 100 ) @ ~~ red = randint( 16, 255 ) @ ~~ green = randint( 16, 255 ) @ ~~ blue = randint( 16, 255 ) @ ~~ color = set_color_string( red, green, blue ) @ ~~ canvas.create_oval(X, Y, X+size, Y+size, fill = color ) @ ~~ i += 1 @@ make_btn.bind( "< Button-1>", make_circle ) @@ mainloop()

Section 6.4: Animations

Introduction

The Tk class has a timer built into it. The timer can be set to call a function at regular intervals. The timer is useful because the rate is fast enough that it can be used for animations.

An animation is actually lots of still pictures. But the pictures are displayed in a sequence and are just a little bit different from each other. When an animation is played, each picture is displayed, one after another, very quickly. The pictures switch so quickly that the viewer perceives that the picture is changing continuously.

Dog Animation Wolf Animation

Programming Examples

In the first example, we have an animation of a bouncing ball. The picture of the ball itself is simple, its just a filled in circle, but the ball moves by changing its position gradually. Because the animation is generated by computer code, the motion can be random, not fixed like a drawing.

from tkinter import * @ from random import * @@@ root = Tk() @@ WINDOW_SIZE = 400 @ BALL_SIZE = 40 @ INITIAL_VELOCITY_RANGE = 50 @ BALL_MOVE_SPACE = WINDOW_SIZE - BALL_SIZE @@ animate_btn = Button( root, text = "Animate" ) @ kick_btn = Button( root, text = "Kick" ) @ canvas = Canvas( root, width = WINDOW_SIZE, height = WINDOW_SIZE ) @@ animate_btn.grid( row = 1, column = 0 ) @ kick_btn.grid( row = 1, column = 2 ) @ canvas.grid( row = 0, column = 0, rowspan = 1, columnspan = 3 ) @@ root.columnconfigure( 0, weight = 0 ) @ root.columnconfigure( 1, weight = 1 ) @ root.columnconfigure( 2, weight = 0 ) @@ ball_X = randint( 0, WINDOW_SIZE - BALL_SIZE ) @ ball_Y = randint( 0, 100 ) @ ball_velocity_X = float( randint( -INITIAL_VELOCITY_RANGE, INITIAL_VELOCITY_RANGE) ) @ ball_velocity_Y = float( randint( -INITIAL_VELOCITY_RANGE, INITIAL_VELOCITY_RANGE) ) @ ball = canvas.create_oval( ball_X, ball_Y, ball_X+BALL_SIZE, ball_Y+BALL_SIZE, fill = "#0000FF" ) @@@ def update(): @ ~ global ball_velocity_X @ ~ global ball_velocity_Y @ ~ global ball @ ~ global ball_X @ ~ global ball_Y @ ~ global canvas @ ~ global root @@ ~ ball_X += ball_velocity_X @ ~ ball_Y += ball_velocity_Y @ ~ canvas.coords( ball, int( ball_X ), int( ball_Y ), int( ball_X ) + BALL_SIZE, int( ball_Y ) + BALL_SIZE ) @@ ~ if ball_X < 0 and ball_velocity_X < 0: @ ~~ ball_velocity_X = -0.95*ball_velocity_X @ ~ if ball_X > BALL_MOVE_SPACE and ball_velocity_X > 0: @ ~~ ball_velocity_X = -0.95*ball_velocity_X @ ~ if ball_Y < 0 and ball_velocity_Y < 0: @ ~~ ball_velocity_Y = -0.95*ball_velocity_Y @ ~ if ball_Y > BALL_MOVE_SPACE and ball_velocity_Y > 0: @ ~~ ball_velocity_Y = -0.95*ball_velocity_Y @@ ~ if ball_Y < BALL_MOVE_SPACE: @ ~~ ball_velocity_Y += 1 @@ ~ ball_velocity_X = 0.95*ball_velocity_X @ ~ ball_velocity_Y = 0.95*ball_velocity_Y @ ~ root.after( 30, update ) @@@ def start_update( event ): @ ~ update() @@@ def kickBall(event): @ ~ global ball_velocity_X @ ~ global ball_velocity_Y @ ~ ball_velocity_X = float( randint( -INITIAL_VELOCITY_RANGE, INITIAL_VELOCITY_RANGE ) ) @ ~ ball_velocity_Y = float( randint( -INITIAL_VELOCITY_RANGE, INITIAL_VELOCITY_RANGE ) ) @@@ animate_btn.bind( "< Button-1>", start_update ) @ kick_btn.bind( "< Button-1>", kickBall ) @@ mainloop()

In the code above, a oval object is created called ball. The Canvas class has a method called .coords( object_id, left_x, top_y, right_x, bottom_y ). This method is used by the update callback function which updates the location of the ball. In the code there are also velocity variables, ball_velocity_X and ball_velocity_Y. These variables represent how fast the ball is moving in the vertical and horizontal directions. The meaning of the word velocity is similar to speed, except velocity also denotes a direction.

There are in total three callback functions: start_update simply starts the update function; kickBall creates new random velocities which causes the ball to go in a new direction; and update which is the most complicated function. The update function moves the ball and detects collisions with the walls (the sides of the view). The amount that the ball is moved is equal to the velocity variables in each direction. The units for the velocity variables are then the number of pixels the ball moves in one update: pixels per update. It only takes one line of code to move the ball with the .coords method, but the lines of code for detecting collisions are more complicated. There are 4 if-statements that check if the ball has collided with one of the four walls. When a collision occurs, the ball "bounces" by switching the direction of one of the velocity variables. The ball's downward velocity is also added to each update to simulate gravity. Finally, the ball's velocities are reduced to simulate wind resistance.


Section 6.5: Sprite Animations

Programming Examples

A sprite is a two dimensional image of an object. Older games, before more complex 3D graphics were invented, where entirely created with sprites, such as the original Mario Brothers game. Sprites are frequently organized into sprite sheets that contain all of the frames of the different animations for the sprite.

Wolf Animation

In the code below, to make things simpler, a sprite sheet such as the one above has been chopped up into image files containing individual frames. These are .png files. The data for the images in the code is stored in objects of the PhotoImage class. The __init__ method of the PhotoImage class allows a file name to be passed so that the image file is opened and stored in the object of PhotoImage.

from tkinter import * @ from random import * @@@ root = Tk() @@ WINDOW_SIZE = 400 @@ direction = 1 @ frame_index = 0 @ is_walking = False @@ walk_btn = Button( root, text = "Walk" ) @ stand_btn = Button( root, text = "Stand" ) @ back_btn = Button( root, text = "Backward" ) @ right_btn = Button( root, text = "Right" ) @ fwd_btn = Button( root, text = "Forward" ) @ left_btn = Button( root, text = "Left" ) @ canvas = Canvas( root, width = WINDOW_SIZE, height = WINDOW_SIZE ) @@ walk_btn.grid( row = 1, column = 0 ) @ stand_btn.grid( row = 1, column = 1 ) @ fwd_btn.grid( row = 1, column = 3 ) @ back_btn.grid( row = 1, column = 4 ) @ left_btn.grid( row = 1, column = 5 ) @ right_btn.grid( row = 1, column = 6 ) @@ canvas.grid( row = 0, column = 0, rowspan = 1, columnspan = 7 ) @@ root.columnconfigure( 2, weight = 1 ) @@@ stand_back = PhotoImage(file = "sprite_images/stand_back.png") @ stand_right = PhotoImage(file = "sprite_images/stand_right.png") @ stand_fwd = PhotoImage(file = "sprite_images/stand_fwd.png") @ stand_left = PhotoImage(file = "sprite_images/stand_left.png") @@ walk_back = [] @ walk_back.append( PhotoImage(file = "sprite_images/walk_back_0.png") ) @ walk_back.append( PhotoImage(file = "sprite_images/walk_back_1.png") ) @ walk_back.append( PhotoImage(file = "sprite_images/walk_back_2.png") ) @ walk_back.append( PhotoImage(file = "sprite_images/walk_back_3.png") ) @ walk_back.append( PhotoImage(file = "sprite_images/walk_back_4.png") ) @ walk_back.append( PhotoImage(file = "sprite_images/walk_back_5.png") ) @ walk_back.append( PhotoImage(file = "sprite_images/walk_back_6.png") ) @ walk_back.append( PhotoImage(file = "sprite_images/walk_back_7.png") ) @@ walk_right = [] @ walk_right.append( PhotoImage(file = "sprite_images/walk_right_0.png") ) @ walk_right.append( PhotoImage(file = "sprite_images/walk_right_1.png") ) @ walk_right.append( PhotoImage(file = "sprite_images/walk_right_2.png") ) @ walk_right.append( PhotoImage(file = "sprite_images/walk_right_3.png") ) @ walk_right.append( PhotoImage(file = "sprite_images/walk_right_4.png") ) @ walk_right.append( PhotoImage(file = "sprite_images/walk_right_5.png") ) @ walk_right.append( PhotoImage(file = "sprite_images/walk_right_6.png") ) @ walk_right.append( PhotoImage(file = "sprite_images/walk_right_7.png") ) @@ walk_fwd = [] @ walk_fwd.append( PhotoImage(file = "sprite_images/walk_fwd_0.png") ) @ walk_fwd.append( PhotoImage(file = "sprite_images/walk_fwd_1.png") ) @ walk_fwd.append( PhotoImage(file = "sprite_images/walk_fwd_2.png") ) @ walk_fwd.append( PhotoImage(file = "sprite_images/walk_fwd_3.png") ) @ walk_fwd.append( PhotoImage(file = "sprite_images/walk_fwd_4.png") ) @ walk_fwd.append( PhotoImage(file = "sprite_images/walk_fwd_5.png") ) @ walk_fwd.append( PhotoImage(file = "sprite_images/walk_fwd_6.png") ) @ walk_fwd.append( PhotoImage(file = "sprite_images/walk_fwd_7.png") ) @@ walk_left = [] @ walk_left.append( PhotoImage(file = "sprite_images/walk_left_0.png") ) @ walk_left.append( PhotoImage(file = "sprite_images/walk_left_1.png") ) @ walk_left.append( PhotoImage(file = "sprite_images/walk_left_2.png") ) @ walk_left.append( PhotoImage(file = "sprite_images/walk_left_3.png") ) @ walk_left.append( PhotoImage(file = "sprite_images/walk_left_4.png") ) @ walk_left.append( PhotoImage(file = "sprite_images/walk_left_5.png") ) @ walk_left.append( PhotoImage(file = "sprite_images/walk_left_6.png") ) @ walk_left.append( PhotoImage(file = "sprite_images/walk_left_7.png") ) @@ img = canvas.create_image( 80, 80, image = stand_right ) @@@ def update(): @ ~ global is_walking @ ~ global direction @ ~ global frame_index @@ ~ if is_walking: @ ~~ if direction == 0: @ ~~~ canvas.itemconfig( img, image = walk_back[frame_index] ) @ ~~ elif direction == 1: @ ~~~ canvas.itemconfig( img, image = walk_right[frame_index] ) @ ~~ elif direction == 2: @ ~~~ canvas.itemconfig( img, image = walk_fwd[frame_index] ) @ ~~ else: @ ~~~ canvas.itemconfig( img, image = walk_left[frame_index] ) @ ~~ frame_index += 1 @ ~~ if frame_index == 8: @ ~~~ frame_index = 0 @ ~ else: @ ~~ if direction == 0: @ ~~~ canvas.itemconfig( img, image = stand_back ) @ ~~ elif direction == 1: @ ~~~ canvas.itemconfig( img, image = stand_right ) @ ~~ elif direction == 2: @ ~~~ canvas.itemconfig( img, image = stand_fwd ) @ ~~ else: @ ~~~ canvas.itemconfig( img, image = stand_left ) @@ ~ root.after( 50, update ) @@@ def set_to_walk( event ): @ ~ global is_walking @ ~ is_walking = True @@ def set_to_stand( event ): @ ~ global is_walking @ ~ is_walking = False @@ def set_to_back( event ): @ ~ global direction @ ~ direction = 0 @@ def set_to_right( event ): @ ~ global direction @ ~ direction = 1 @@ def set_to_fwd( event ): @ ~ global direction @ ~ direction = 2 @@ def set_to_left( event ): @ ~ global direction @ ~ direction = 3 @@@ walk_btn.bind( "< Button-1>", set_to_walk ) @ stand_btn.bind( "< Button-1>", set_to_stand ) @ back_btn.bind( "< Button-1>", set_to_back ) @ right_btn.bind( "< Button-1>", set_to_right ) @ fwd_btn.bind( "< Button-1>", set_to_fwd ) @ left_btn.bind( "< Button-1>", set_to_left ) @@ update() @ mainloop()