CS61A Lab 08b: User Interfaces

August 14, 2013

In this lab, you will be creating a Graphical User Interface (GUI) for the game of Hog. To build the user interface, we'll be using Tkinter, which is included in Python's standard library. (Tkinter itself is Python binding for Tk, which is an open source, cross-platform toolkit for creating GUIs.)

Introduction

Create a new file called my_ui.py and type the following:

from tkinter import *

root = Frame()
root.pack()
root.mainloop()

What does this code mean?

  1. from tkinter import *
  2. This line imports everything inside of the tkinter library.

  3. root = Frame()
  4. This line creates a Frame object. A Frame is a type of Widget, which is a just visual component of a GUI. Other widgets include entry fields, checkbuttons, and labels. A Frame is a special type of widget that just contains other widgets.

  5. root.pack()
  6. This line makes the Frame "visible" to the user -- but since Frames have no content, it still looks like we can't see anything!

  7. root.mainloop()

    This line starts the event loop. This loop will continuously wait for user input; when the user does something, the loop sends a message to components in the GUI. You can program the components to respond to the message however you want.

Congratulations, you've just created your first GUI! However, if you run your program with python3 my_ui.py, you'll notice that it just creates an empty frame. Not a very interesting window yet...

An Event-Driven Paradigm

The programs we have been writing so far have all followed a sequential structure -- we can arrange our code so that it will always run in a certain order.

UI programs are different. They are instead based on events. Events are "triggered" by various user inputs, such as mouse clicks, keyboard inputs, and such. As you might have guessed, the user can click, type, and move the cursor around in any random way -- and the programmer won't be able to predict the order in which the events are generated!

To handle the fact that events could occur in unpredictable sequences, we use callback functions. Whenever an event is triggered, the event loop notifies the relevant widget(s) and their callback functions are called.

If that description is still unclear, try typing the following code in your my_ui.py file:

from tkinter import *

root = Frame()
root.pack()

def handle_button():
    print("Button pushed.")

button = Button(root, text="Press me!", command=handle_button)
button.pack()
root.config(padx=100, pady=100)

root.mainloop()

We've seen the code that's not bolded already. But what does the bolded code do?

  1. button = Button(root, text="Press me!", command=handle_button)
  2. This line creates a Button widget. A Button is what you expect a button to be -- a clickable widget that does something when you click it. Each argument in the Button constructor means something:

    1. root: the parent widget -- i.e. the widget in which this Button will be contained
    2. text: the text displayed on the Button
    3. command: the action that occurs when the Button is pressed

    The command argument will be the callback function, which is just a normal Python function.

  3. def handle_button():
  4. These two lines just define the callback function. When the button is pressed, it will call this function, which prints "button pushed." to your terminal.

  5. button.pack()
  6. This line just makes the Button visible. By default, the pack places widgets in the center of the parent widget.

  7. root.config(padx=100, pady=100)
  8. This line denotes how many pixels lie between the button and the edge of the main Frame.

This is what our program will look like. Try pressing the button!

Button UI

Hog GUI

For this section, you can copy the starter code by typing this into your terminal:

cp ~cs61a/public_html/su13/lab/lab08b/hog_gui.py .
cp -r ~cs61a/public_html/su13/lab/lab08b/images .

Don't forget the "." at the end of the command!

Take a moment to read through the hog_gui.py. The first half (HogState) just implements game logic -- which should be familiar to you (it's actually all of phase one!).

The second half of the code, the HogGUI class, handles the GUI. The code might look a bit intimidating at first, but let's break it down into parts:

  1. Initialization: the primary method is the __init__, which you know (from OOP) is what the constructor will call. Notice that the __init__ method is further divided into "Widget creation" and "Widget packing."
    1. Widget creation: Here, we call widget constructors (like Label). We also call three "init" helper methods -- don't worry about these, this is so our __init__ isn't too lengthy.
    2. Widget packing: to make widgets visible, we have to "pack" them. The order in which you pack widgets determines their order on the GUI.
  2. Take turn: When the "Roll" button is pressed, it calls the take_turn method. This method has 3 sections:
    1. Extracting the number of rolls from the entry field
    2. Displaying the dice
    3. Displaying the state of the game and handling "game overs"
  3. Display methods: these are helper methods that display various things to the screen:
    1. Display and hide individual dice
    2. Display state (i.e. display the scores of players
    3. Display status (e.g. how many points a player scored)
    4. Display at the end of a game
    5. Restarting behavior

Next, try running the program with

python3 hog_gui.py

You'll notice that a simple GUI pops up:

Hog GUI

... but nothing works. Here's where you come in!

Q1: Getting the number of rolls

Right now, the only thing that happens when we click "Roll" is an error message appears in our terminal -- it complains that num_rolls is not defined!

Your job is to get the numerical value entered in the Entry field and save it (as an int) in a local variable called num_rolls. That way, we will be able to tell the HogState how many rolls we want to make.

To do this, find the comment that says "Q1: get num_rolls from self.entry_field" in the take_turn method. Here, you will write code dealing with a widget called an Entry field. An Entry field is a widget that allows users to input text.

For our GUI, there is an instance variable conveniently named self.entry_field that refers to the Entry object next to the "Roll" button. To get the text from an Entry field, you can call its get method. It will return the contents of the Entry as a string, so you will have to convert it into an int before storing it in num_rolls.

For example, if we have an Entry object called my_entry_field, we can store the contents in another variable like this:

content = my_entry_field.get()

When you finish this question, you should be able to see status messages like "Player 1 scored 2 points" when you enter a number into the entry field and click "Roll." Our game works!

Q2: Updating scores

Unfortunately, even though we can roll dice and score, our GUI doesn't display the current game state -- we don't know who's winning!

To fix this, implement the display_state method. This question involves modifying Labels, which are widgets that just display text. Our GUI has 2 labels called self.p1_label and self.p2_label that display scores for each player.

Every widget has a config method, which we can use to set a property for the widget. For Labels, we can use it to set the text:

my_label.config(text='new text')

One last hint: the HogState class has two property methods, p1 and p2, that each return the score for their respective player. These should be the strings you set for your labels.

When you finish this question, you should see that the players' scores are updating!

Q3: Displaying Dice

Let's add more features! Right now, we can't see what outcomes are being rolled. In the take_turn method, find the comment that says "Q3: display or hide dice". Here, you will implement code that displays what each player rolls.

Fortunately, we've already created two methods, display_dice and hide_dice that can display or hide individal dice. All you have to do is call each method with the appropriate dice_id (e.g. 0 for the first dice) and the outcome (what number came up on that die).

Notice that, a few lines up in the source code, we have extracted a dictionary of outcomes from the HogState. The keys of this dictionary are integers (specifying the dice_id of the dice we should display), and the values are the outcomes for those specific dice.

Your code in the for should look something like this: for every dice_id from 0 to 9, you will do one of two things:

Once you finish this question, you should be able to see dice show up on your GUI!

Q4: Displaying the winner

You might have noticed that the game doesn't stop once we hit our goal. To fix this, we will have to modify two parts of our program.

PART A, go to the display_end method and find the comment that says "Q4: displaying the winner in self.status". self.status is a Label -- but you already know (from Q2) how to change the text of a Label! Go ahead and add a line that configures the Label to display a message that announces the winner.

Hint: self.state, our HogState, has a property method called winner.

PART B: the solution to Part A displays a message, but it doesn't actually prevent the game from continuing if the user presses the "Roll" button. Go to the take_turn method and find the comment that says "Q4: terminate the method if the game is over". Here, you should add a couple of lines to immediately terminate the method if the game is over.

Hint: self.state, our HogState, has a property method called over that is True if the game is over.

Q5: Restart

Finally, we would like to restart the game after it ends. This will involve creating a new Button widget and registering its callback method.

To start, go to the __init__ method and find the comment that says "Q5: initialize restart button". Here, create a Button widget by use the Button constructor. We have provided a method called self.restart, which you should register to the Restart button as a callback. The parent widget (parent frame) of the button you create is called master.

Finally, find the comment that says "Q5: pack restart button". Somewhere below that line (not necessarily right below), call the button's pack method to make it visible. Then, try moving the line around to see how this affects where on the GUI it shows up!

You have now completed your Hog GUI! If you are interested in learning more about tkinter, Google it!

Hog GUI