pythontkintermainloop

Python Tkinter: wait for user input before continuing with the mainloop


I am using Tkinter with Python 3.11 on Windows.

The following program creates a simple window with a menu. The user would click "Game -> Select game directory", whereupon game_directory_selection() function is called for the user to select a directory.

If the chosen directory contains a single file with the name utilX.dat (X being 0..9), the directory is used as game directory and some general game configuration is read here

self.conf = Config(self.game_directory)

and then the specific player configuration is read here

self.player = Player(self.game_directory, self.player_id)

because 'X' in utilX.dat would be the player_id.

However, if the directory contains several player files (let's say util1.dat, util3.dat and util7.dat), then the self.display_file_selection() function is called to allow the player to choose one. This is done by reusing the window to create a new Frame, a Label, a Listbox with the player files and a Button. Once the user has chosen one player file, this Frame is destroyed and the player specific file can be read.

import tkinter as tk
from tkinter import Menu, Text, Label, Button, Listbox, StringVar, ttk
from tkinter import filedialog, messagebox
from tkinter import ttk
import os.path
import sys
import time
from pathlib import Path

#from inventory_view import InventoryViewer
from player import Player
from config import Config


class Game:
    def __init__(self, root):
        self.game_directory = None
        self.filelist = None
        self._selected_indices = ''
        self.playerfile = None
        self.conf = None

        self.root = root
        self.root.title("My Game Client")

        self.create_main_window()
        
        print('playerfile', self.playerfile) # (**)

    def create_main_window(self):
        # Create menu
        self.menu = Menu(self.root)
        self.root.config(menu=self.menu)

        # Game menu
        self.game_menu = Menu(self.menu, tearoff=0)
        self.menu.add_cascade(label="Game", menu=self.game_menu)
        self.game_menu.add_command(label="Select game directory", command=self.game_directory_selection)
        self.game_menu.add_separator()
        self.game_menu.add_command(label="Save game") # TO DO: add command
        self.game_menu.add_command(label="Inventory viewer", command=self.inventory_view)
        self.game_menu.add_command(label="Exit", command=self.root.quit)



    def game_directory_selection(self):
        """ Choose game directory, or quit."""

        while True:
            self.game_directory = tk.filedialog.askdirectory()
            if not self.game_directory:
                if tk.messagebox.askyesno(
                    icon='warning',
                    title="Game directory selection",
                    message="You haven't chosen a game directory. Do you want to quit?"):
                    sys.exit(0)
            else:
                p = Path(self.game_directory)
                self.filelist = list(p.glob('util[0-9].dat'))
                
                if not self.filelist:
                    tk.messagebox.showerror(
                        title='Directory error',
                        message='Player files not found in directory')
                else:
                    # If there is more than one player file,
                    # display a selection dialog to choose one.
                    if len(self.filelist) > 1:
                        filelist_tmp = [f.name for f in self.filelist]
                        self.display_file_selection(filelist_tmp)
                    else:
                        self.playerfile = self.filelist[0]
                        # Extract player ID from filename
                        self.player_id = self.playerfile.stem[-1]
                        print('self.player_id', self.player_id)
                    break
        
        # Disable the menu entry for selecting game directory
        self.game_menu.entryconfigure('Select game directory', state=tk.DISABLED)        

        # Read configuration files into self.conf variable
        self.conf = Config(self.game_directory)

        # Read player files into self.player variable
        self.player = Player(self.game_directory, self.player_id) # (*)



    def choose_file(self):
        self._selected_indices = self._listbox.curselection()
        self.playerfile = self.filelist[self._selected_indices[0]]
        
        # Extract player ID from filename
        self.player_id = self.playerfile.stem[-1]
        print('self.player_id in choose_file', self.player_id)
        self.gameselectionframe.destroy()
        


    def display_file_selection(self, filelist):
        
        # Destroy the previous frame if it exists
        # try:
        #     _gameselectionframe = self.root.nametowidget('gameselectionframe')
        #     if _gameselectionframe:
        #         _gameselectionframe.destroy()
        # except KeyError:
        #     pass

        self.gameselectionframe = ttk.Frame(self.root, padding=(2, 2, 2, 2), name='gameselectionframe')
        self.gameselectionframe.pack()

        display_label = ttk.Label(self.gameselectionframe, text="Several player files were found. Please choose one.", width=60)
        display_label.pack()

        pl = tk.StringVar(value=filelist)
        self._listbox = tk.Listbox(self.gameselectionframe, listvariable=pl, height=6, width=20)
        self._listbox.selection_set(0)
        self._listbox.pack()

        btn = ttk.Button(self.gameselectionframe, text='Select', command=self.choose_file)
        btn.pack()


    def inventory_view(self):
        if self.playerfile:
            self.player = Player(self.game_directory)
            #self.viewer = InventoryView(self.player.planets, self.conf)
            
        else:
            tk.messagebox.showerror(
                title='Player file error',
                message='No player file selected. Please select a game directory first.')



if __name__ == "__main__":
    root = tk.Tk()
    game = Game(root)
    root.mainloop()

The problem I have when there are several player files (like in the example with the 3 player files) is that

self.player = Player(self.game_directory, self.player_id) # (*)

is executed before the user has a chance to choose one player file. Then self.player_id is None and the file cannot be properly read. What I need in this case is to prevent the program from continuing until I have set self.player_id properly.

I have tried making game_directory_selection() use a tk.Toplevel window with grab_set() to "halt" program/loop execution until the user chooses a file, but to no avail.

I have also tried using quit() at the beginning of display_file_selection() to exit the main loop and then mainloop() at the end of display_file_selection() to resume the main loop.

I have omitted the implementation of Config and Player classes because they are not relevant. They just read those files onto dictionaries for later use.

Also, (**) prints None before the player chooses the directory. Which doesn't bother me because I don't want to print anything there, it is just there to illustrate the fact that the mainloop doest't wait for self.create_main_window() to finish before continuing.

Of course, there may be a better way or best practice for doing this, instead of the way I am tackling it...

In this question, they use wait_variable() but they say a normal GUI application does not generally work that way. I have also read this question.

Thanks in advance.


Solution

  • I can't run your code to test it but I would move code with self.player = Player() to function

       def set_player(self):
            # Disable the menu entry for selecting game directory
            self.game_menu.entryconfigure('Select game directory', state=tk.DISABLED)        
    
            # Read configuration files into self.conf variable
            self.conf = Config(self.game_directory)
    
            # Read player files into self.player variable
            self.player = Player(self.game_directory, self.player_id) # (*)
    

    and execute in two places where you set self.player_id = ...
    (in game_directory_selection() and in choose_file()).
    This way it would be execute always when you have correct value in this variable.

    Something like this: (see comments # <--- here in code)

        def game_directory_selection(self):
            """ Choose game directory, or quit."""
    
            while True:
                self.game_directory = tk.filedialog.askdirectory()
                if not self.game_directory:
                    if tk.messagebox.askyesno(
                        icon='warning',
                        title="Game directory selection",
                        message="You haven't chosen a game directory. Do you want to quit?"):
                        sys.exit(0)
                else:
                    p = Path(self.game_directory)
                    self.filelist = list(p.glob('util[0-9].dat'))
                    
                    if not self.filelist:
                        tk.messagebox.showerror(
                            title='Directory error',
                            message='Player files not found in directory')
                    else:
                        # If there is more than one player file,
                        # display a selection dialog to choose one.
                        if len(self.filelist) > 1:
                            filelist_tmp = [f.name for f in self.filelist]
                            self.display_file_selection(filelist_tmp)
                        else:
                            self.playerfile = self.filelist[0]
                            # Extract player ID from filename
                            self.player_id = self.playerfile.stem[-1]
                            print('self.player_id', self.player_id) 
    
                            self.set_player()  # <--- here
    
                        break
    
    
        def choose_file(self):
            self._selected_indices = self._listbox.curselection()
            self.playerfile = self.filelist[self._selected_indices[0]]
            
            # Extract player ID from filename
            self.player_id = self.playerfile.stem[-1]
            print('self.player_id in choose_file', self.player_id)
    
            self.set_player()   # <--- here
    
            self.gameselectionframe.destroy()