Check out the GitHub repo here.
I have been using Toga for about a month now to create a desktop GUI in Python. Toga has a few quirks and it takes some getting used to, so here is a simple tutorial to create an app to display some of the ways to use Toga.
Since Hangman is a simple game with tons of versions online, this is a great way to be introduced to Toga’s capabilities. Briefcase and Toga together make it easy to deploy native applications regardless of platform. Toga's system uses widgets, which enable the developer to add buttons, text inputs, and selection dropdowns using the native system's design. It is very possible to deploy a fully functioning app across platforms in a matter of minutes.
Briefcase is the system that makes it easy to deploy the app across platforms. Toga manages the design of the application, which includes adding containers, widgets, and pages. To get started deploying the app, I would recommend following the first three steps of this tutorial. The rest of this post will discuss the actual design and implementation of a toga app.
Disclaimer: I got the Hangman words from here, to ensure the words were interesting and made sense. I drew all my pictures myself using my iPad, feel free to use them
Videos of Application
Losing Hangman
Winning Hangman
General Toga Tips
Column boxes will stretch out the width of the window/available space
For most widgets, create a callback or handler function that saves the value into an instance variable
The Style pack is how you change the font for the labels. You can create one particular style, and use that as a variable for each of your labels
The entry point of an app will be app.py
Toga loads the entire page at once, and does not dynamically check the page for changes
There is no listening feature
Bug Check: when passing functions in, make sure you do not call the function, and only pass the name. If you pass in a function call instead of a function name, toga will run the function and will not let you interact with the page.
.
File Directory
This is the file directory used for the project. The images are all inside the "hangman_images" folder
Code Breakdown
Read-in Word File
All hangman words were stored in a text file and are read into the application once the game begins.
def read_in_words():
file_path = Path(__file__).parent / "english_words.txt"
lines = []
with open(file_path, "r") as file:
for line in file:
lines.append(line.strip()) # add each line to the list, stripped of any whitespace
return lines
Hangman App
The rest of the functions in this app are inside the hangman class app.
def game_state(self):
box = toga.Box(style=Pack(direction=COLUMN))
#heading
box.add(toga.Label("Guess a Letter!", style=Hangman.title_style ))
box.add(toga.Label("Word: " + " ".join(self.revealed_word), style=Hangman.secret_word_style))
box.add(toga.TextInput(validators=[self.max_length, self.letters_only], on_change=self.process_letter))
# used letters + lives
box.add(toga.Label("Used Letters: " + " ".join(self.used_letters), style=Hangman.title_style))
box.add(toga.Label("Lives: " + str(self.lives), style=Hangman.title_style))
#buttons
box.add(toga.Button("Start a New Game", style=Hangman.button_style, on_press=self.start_new_game))
box.add(toga.Button("Play Hangman with Own Word", style=Hangman.button_style, on_press=self.enter_own_word))
#image
box.add(toga.ImageView(image=self.hangman_image_path + self.hangman_image + ".PNG"))
self.main_window.content = box
All styles for the rest of the application are declared here, as class variables. Look up system settings to identify all the possible fonts that could be used. (This was designed on a Windows system. Keep in mind that the same fonts do not exist on other systems.)
class Hangman(toga.App):
valid_alphabet = set("abcdefghijklmnopqrstuvwxyz")
all_words = read_in_words()
#styles
game_font = "Century Schoolbook"
title_style = Pack(padding=(0, 5), font_family=game_font, font_size=20, text_align="center", font_weight="bold")
button_style = Pack(padding=(0, 5), font_family=game_font, font_size=15, text_align="center")
game_over_style = Pack(padding=(0, 5), font_family=game_font, font_size=50, text_align="center", font_weight="bold")
secret_word_style = Pack(padding=(0, 5), font_family=game_font, font_size=35, text_align="center", font_weight="bold")
Startup
The startup function is the entry point into the app. This will be the first function that is run, unless it is explicitly told to run a different function. The Main Window is the object that controls what is shown to the user, so for anything to be shown, the content of the main window must be set to the preferred box/container.
In toga, every widget or object needs to be a child of a box or a container.
Here, there are two buttons. One will enter a new game with a random word and the other allows the user to enter their own word.
def startup(self):
self.secret_word = ""
print("starting hangman")
main_box = toga.Box(style=Pack(direction=COLUMN))
main_box.add(toga.Label("Welcome to Hangman!", style=Hangman.title_style ))
main_box.add(toga.Button("Play Hangman" , style=Hangman.button_style, on_press=self.start_new_game))
main_box.add(toga.Button("Start Game with Own Word", style=Hangman.button_style, on_press=self.enter_own_word))
self.main_window = toga.MainWindow(title="Hangman", size=(600, 600))
self.main_window.content = main_box
self.main_window.show()
Enter Own Word
def enter_own_word(self, widget):
box = toga.Box(style=Pack(direction=COLUMN))
box.add(toga.Label("Enter your secret word:", style=Hangman.title_style ))
box.add(toga.TextInput(placeholder="Enter Your Word Here", on_change=self.update_secret_word_handler)) #could add valitors here if needed
box.add(toga.Button("Start Game", style=Hangman.button_style, on_press=self.start_new_game))
self.main_window.content = box
self.main_window.show()
Update Secret Word Handler
This is a simple function that updates the text input upon any change into an instance variable, so it is accessed by the other functions. Another way to save the text without creating an additional function is saving the widget itself into a global variable and accessing its value.
#save variable handler
def update_secret_word_handler(self, widget):
self.secret_word = widget.value
Alternate way:
self.enter_secret_word_widget = toga.TextInput(placeholder="Enter Your Word Here")
box.add(self.enter_secret_word_widget)
#later in different function
self.secret_word = self.enter_secret_word_widget.value
Start New Game
This function kicks off the new game of hangman. It first checks if the widget that calls the function is either requiring a random word or already has a secret word. (For debugging purposes, I print out the word)
The variables for the game are initialized, including a set for the secret word’s letters, correct letter guesses, all used letters, and lives. In this case, the path(file) combo is used to test on different computers, but converted it to a string because toga’s image view widget will not work with a pure path object. It is initialized with the first hangman image.
Finally, the game state function, which handles all the in-game logic and visuals, is called.
def start_new_game(self, widget):
if widget.text == "Play Hangman" or widget.text == "Start a New Game":
self.secret_word = self.get_word() # get a random word
print(self.secret_word)
#initialize variables used for each game
self.letters = set(self.secret_word)
self.used_letters = []
self.lives = 7
self.correct_letters = set()
self.hangman_image_path = str(Path(__file__).parent / "hangman_images" )
self.hangman_image = "/hangman-0" #set to beginning
self.revealed_word = ["_"] * len(self.secret_word)
self.game_state()
Game State
This function is the main game controller. This function provides all the Toga visuals to the game. All visuals are added to the same box in order as they should appear on the screen, with the styles specified as before. The text input widget includes validators that restrict the acceptable text, and a callback function for when the text is changed.
Similar steps are followed for the rest of the labels and butons.
def game_state(self):
box = toga.Box(style=Pack(direction=COLUMN))
#heading
box.add(toga.Label("Guess a Letter!", style=Hangman.title_style ))
box.add(toga.Label("Word: " + " ".join(self.revealed_word), style=Hangman.secret_word_style))
box.add(toga.TextInput(validators=[self.max_length, self.letters_only], on_change=self.process_letter))
# used letters + lives
box.add(toga.Label("Used Letters: " + " ".join(self.used_letters), style=Hangman.title_style))
box.add(toga.Label("Lives: " + str(self.lives), style=Hangman.title_style))
#buttons
box.add(toga.Button("Start a New Game", style=Hangman.button_style, on_press=self.start_new_game))
box.add(toga.Button("Play Hangman with Own Word", style=Hangman.button_style, on_press=self.enter_own_word))
#image
box.add(toga.ImageView(image=self.hangman_image_path + self.hangman_image + ".PNG"))
self.main_window.content = box
Validators
Toga allows for text input lines to have validator functions. Include a pass a string of functions to ensure the text input follows a certain rule. If there is no issue, return None. If it is, return a string with the validator error.
# validators
def letters_only(self, letter):
lowercase = letter.lower()
if lowercase in Hangman.valid_alphabet:
return None
else:
return "Please enter a letter"
def max_length(self, letter):
if len(letter) > 1:
return "Please enter only one letter"
else:
return None
Process Letters
This function is used to process any changes with the text input. This function determines if the letter was a correct guess or not, which then therefore controls game ending and which image is shown.
This function doesn’t include any toga visuals, but controls game logic. It checks if the guessed letter is inside secret word. If it is, it will replace the underscore with that letter. If it is not, it subtracts a life and updates the hangman image.
At the end of the function, the end game conditions are checked. Toga will only check/update the screen when a new function is called. It does not “listen” for changes, so I put the end game check in this function, and then call the correct function accordingly.
def process_letter(self, widget):
self.used_letters.append(widget.value)
if widget.value in self.letters: #if letter is in word, replace underscore with letter
self.correct_letters = self.correct_letters.union(widget.value)
for i in range(len(self.secret_word)):
if self.secret_word[i] in self.correct_letters:
self.revealed_word[i] = self.secret_word[i]
else: #else, remove lives & change image
self.lives = self.lives - 1
number = int(self.hangman_image[-1]) + 1
self.hangman_image = self.hangman_image[:-1] + str(number)
if self.lives == 0: #check if game is over
self.end_game(victory=False)
elif "_" not in self.revealed_word:
self.end_game(victory=True)
else:
self.game_state()
End Game
This function is called when a end game condition is met. It will update the image and title based on which end game it is (victory or defeat), and then display the correct word.
Similar to the other displays, the main_window.content is set as the box and then shown to the user.
def end_game(self, victory=False):
box = toga.Box(style=Pack(direction=COLUMN))
if victory:
box.add(toga.Label("You won! :)", style=Hangman.game_over_style))
img_path = self.hangman_image_path + "/victory.PNG"
else:
box.add(toga.Label("You lost! :C", style=Hangman.game_over_style))
img_path = self.hangman_image_path + "/death.PNG"
box.add(toga.Label("The word was: " + self.secret_word, style=Hangman.secret_word_style))
box.add(toga.Button("Start a New Game", style=Hangman.button_style, on_press=self.start_new_game))
box.add(toga.Button("Play Hangman with Own Word", style=Hangman.button_style, on_press=self.enter_own_word))
box.add(toga.ImageView(image=img_path))
self.main_window.content = box
self.main_window.show()
Kill Game
This function is used for developer purposes, lettting the developer the end game early if needed during testing. In Toga, functions called by a widget require the widget requirement, like text input’s on change, or a button’s on press. It will throw an error if the function is called without this widget argument. Since it is desirable to be able to kill the game either from a widget or from a logical process, the kill_game/end_game functions should accommodate both behaviors as shown in this code snippet:
def kill_game(self, widget):
self.end_game(victory=False)
And that’s a Wrap!
Thank you for reading this blog post and I hope it was helpful for you in your Python/Toga journey.
Comments