DEV Community

Cover image for The Adventures of Blink S2e7: A GUI Screen in Python
Ben Link
Ben Link

Posted on

The Adventures of Blink S2e7: A GUI Screen in Python

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
Enter fullscreen mode Exit fullscreen mode
  • /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
Enter fullscreen mode Exit fullscreen mode
  • /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
Enter fullscreen mode Exit fullscreen mode

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.

Our dialog box's layout

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Once they're up and running, you can start the screen by calling

python game_editor.py
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)