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.
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()