Hey pals! Welcome to another Adventure of Blink! We're continuing our Season 2 build of Hangman today with some front-end work in our Python app... making screens!
TL/DR: Youtube!
Come watch this episode on Youtube - just leave me a like and a subscribe please!
Well, almost...
In order to complete today's build you'll need to add a couple of API calls to our Flask layer. If you remember back in Episode 3, we only built 2 routes: /add
and /random
.
We're going to need to add a few new routes now:
-
/getall
: Retrieves all the entries so we can add them to our text box
@app.route('/getall', methods=['GET'])
def get_all_items():
try:
# Find all records in the collection
words = list(collection.find({}, {"_id": 0})) # Exclude _id field from the response
return jsonify(words), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
-
/edit
: Change the contents of a record
@app.route("/edit", methods=["POST"])
def edit_word():
"""API endpoint to edit an existing word and its hint."""
data = request.get_json() # Get the JSON data from the request
phrase = data.get('phrase') # The new or existing phrase
hint = data.get('hint') # The new or existing hint
original_phrase = data.get('original_phrase') # The identifier for the word to update
if not original_phrase:
return jsonify({"error": "Original phrase is required"}), 400
try:
# Find the document with the original phrase and update it
result = collection.update_one(
{"phrase": original_phrase},
{"$set": {"phrase": phrase, "hint": hint}}
)
if result.matched_count == 0:
return jsonify({"error": "Phrase not found"}), 404
return jsonify({"message": "Word updated successfully"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
-
/delete
: Remove from the list
@app.route("/delete", methods=["DELETE"])
def delete_word():
"""API endpoint to delete a word and its hint."""
data = request.get_json() # Get the JSON data from the request
phrase = data.get('phrase') # The phrase to delete
if not phrase:
return jsonify({"error": "Phrase is required"}), 400
try:
# Delete the document where the phrase matches
result = collection.delete_one({"phrase": phrase})
if result.deleted_count == 0:
return jsonify({"error": "Phrase not found"}), 404
return jsonify({"message": "Word deleted successfully"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
On to building a front end!
The first challenge around building a GUI application in Python is deciding what framework you want to build from.
In the past, I've used pygame, and I found it to be great for graphical work like making sprites animate and moving characters around on the screen... but one place it was sorely lacking was its ability to build menus and dialogs for things like settings pages or even an inventory system. It just felt like too much work to construct those things in that manner.
For our hangman, I've elected to use Tkinter. It has a pre-built collection of little widgets that make dialog-based apps work, and I think it will be useful for Hangman, which doesn't require a great deal of animation and image management but does require us to have an easy-to-use interface for the user to make changes.
And... spoiler alert, that's what we're building today to learn the basics of Tkinter - a dialog box where you manage the words & phrases in the database!
What it's going to look like
We're going to have a window with a list box in it, that loads up to contain all the phrases. It will show the phrase, the hint, and the last-used date/time stamp. Below that box, we'll have 3 buttons: one to add a new phrase, one to edit an existing phrase, and one to delete a phrase.
For this screen I've named the file game_editor.py
and placed it in the hangman
folder, next to the main.py
there.
Here's our code, with comments to explain how it all works:
import tkinter as tk
from tkinter import ttk, messagebox
import requests
# Constants for your Flask API URL
API_BASE_URL = "http://localhost:5001/"
# Each screen is its own class. This is cool because
# you can test it by running python screen.py
class WordEditorApp(tk.Tk):
def __init__(self):
super().__init__()
self.title("Phrase Editor")
self.geometry("750x300")
# Create a Treeview widget
self.word_tree = ttk.Treeview(self, columns=("phrase", "hint", "lastused"), show="headings")
# columnspan is important here because it tells the
# grid that our treeview is spread across 3 columns.
# This means that our 3 buttons on the next row can
# be spaced evenly.
self.word_tree.grid(columnspan=3, row=0, column=0, sticky="nsew")
# Define the columns
self.word_tree.heading("phrase", text="Phrase")
self.word_tree.heading("hint", text="Hint")
self.word_tree.heading("lastused", text="Last Used")
# Set column widths
self.word_tree.column("phrase", width=250)
self.word_tree.column("hint", width=250)
self.word_tree.column("lastused", width=250)
# Buttons for CRUD operations
# CRUD = Create, Read, Update, Delete
# Note that we pass each button the name of the class
# method that we want to execute when it's clicked
self.add_button = tk.Button(self, text="Add Word", command=self.add_word_popup)
self.add_button.grid(row=1, column=0)
self.edit_button = tk.Button(self, text="Edit Word", command=self.edit_word_popup)
self.edit_button.grid(row=1, column=1)
self.delete_button = tk.Button(self, text="Delete Word", command=self.delete_word)
self.delete_button.grid(row=1, column=2)
# Load words initially
self.load_words()
def load_words(self):
"""Fetch words from the database via Flask API."""
try:
for item in self.word_tree.get_children():
self.word_tree.delete(item)
# Our first API call in action!
response = requests.get(f"{API_BASE_URL}/getall")
if response.status_code == 200:
words = response.json()
for word in words:
self.word_tree.insert("", "end", values=(word["phrase"], word["hint"], word["last_used"]))
else:
messagebox.showerror("Error", "Failed to fetch words from the database.")
except Exception as e:
messagebox.showerror("Error", f"An error occurred: {e}")
# When you click "Add", you'll get a little pop up window
# Where you provide the phrase and the hint.
def add_word_popup(self):
"""Popup for adding a new word."""
self.edit_popup("Add Phrase", save_callback=self.add_word)
# When you click edit, you'll get a popup that populates
# with the current selection.
def edit_word_popup(self):
"""Popup for editing the selected word."""
selected_word = self.word_tree.selection()
item_values = self.word_tree.item(selected_word)["values"]
if selected_word:
self.edit_popup("Edit Phrase", item_values[0], item_values[1], save_callback=self.edit_word)
# This is the popup builder - because it has all the
# same components, it just populates differently.
def edit_popup(self, title, word=None, hint=None, save_callback=None):
"""Create a popup for adding/editing a word and its hint."""
popup = tk.Toplevel(self)
popup.title(title)
# Word (phrase) field
tk.Label(popup, text="Phrase:").grid(row=0, column=0)
word_entry = tk.Entry(popup)
word_entry.grid(row=0, column=1)
if word:
word_entry.insert(0, word)
# Hint field
tk.Label(popup, text="Hint:").grid(row=1, column=0)
hint_entry = tk.Entry(popup)
hint_entry.grid(row=1, column=1)
if hint:
hint_entry.insert(0, hint)
# Save button
save_button = tk.Button(popup, text="Save",
command=lambda: save_callback(word_entry.get(), hint_entry.get(), popup))
save_button.grid(row=2, column=0, columnspan=2)
# When you add a phrase, here's how it happens
def add_word(self, phrase, hint, popup):
"""Add a word to the database."""
try:
response = requests.post(f"{API_BASE_URL}/add", json={"phrase": phrase, "hint": hint})
if response.status_code == 201:
self.load_words()
popup.destroy()
else:
messagebox.showerror("Error", "Failed to add phrase.")
except Exception as e:
messagebox.showerror("Error", f"An error occurred: {e}")
# When you edit a phrase, here's how it happens
def edit_word(self, phrase, hint, popup):
"""Edit the selected word in the database."""
selected_word = self.word_tree.selection()
if not selected_word:
return
try:
item_values = self.word_tree.item(selected_word)["values"]
old_phrase = item_values[0]
response = requests.put(f"{API_BASE_URL}/edit", json={"original_phrase": old_phrase, "phrase": phrase, "hint": hint})
if response.status_code == 200:
self.load_words()
popup.destroy()
else:
messagebox.showerror("Error", "Failed to update phrase.")
except Exception as e:
messagebox.showerror("Error", f"An error occurred: {e}")
# When you delete a phrase, here's how it happens
def delete_word(self):
"""Delete the selected word from the database."""
selected_word = self.word_tree.selection()
if not selected_word:
return
try:
item_values = self.word_tree.item(selected_word)["values"]
phrase = item_values[0]
data = {
"phrase": phrase
}
# Send the DELETE request
response = requests.delete(f"{API_BASE_URL}/delete",json=data)
if response.status_code == 200:
self.load_words()
else:
messagebox.showerror("Error", "Failed to delete phrase.")
except Exception as e:
messagebox.showerror("Error", f"An error occurred: {e}")
# Initialize the app
if __name__ == "__main__":
app = WordEditorApp()
app.mainloop()
Try it out
That's a lot of code - how do we try this out to see if it's working right?
First, you'll need to redeploy your database/api containers with the new API routes:
# Windows / Linux
docker-compose up --build
# Mac
docker compose up --build
Once they're up and running, you can start the screen by calling
python game_editor.py
Wrapping up
As with all of our build this season, I'm not going to bore you with every little detail; my goal is to make sure you know how things work, not to drag you through every line of code in the project. You've now seen how to build your first screen - tune in next week and we'll continue building the front end!