Short Answer:
Best way to structure a tkinter application.
Long Answer:
Prelude:
If my understanding of your programm's logic is correct - you have a "game" design, with class1
acting like a "main menu" and with class2
that mimic a "game", that starts after we done with "main menu". Your idea to use a third class is reasonable, but I found this approach a little cumbersome. Anyway, let's stick to your desired layout first, then we can try something similar. Also note fact, that I'll treat your class2
as a tk.Tk()
since you commented it as a "main app".
Options:
- 3 classes: Toplevel - main menu, Tk - main app, middleman -
container:
Here it is, your desired layout. Let's try to code something simple and then talk about it:
# imports
try:
import tkinter as tk
import tkinter.simpledialog as sd
import tkinter.messagebox as msg
except ImportError:
import Tkinter as tk
import tkSimpleDialog as sd
import tkMessageBox as msg
import random as rnd
# classes
class Class1(tk.Toplevel):
def __init__(self, master, arg_function):
tk.Toplevel.__init__(self, master)
self.minsize(350, 200)
# make button with arg_function as the attached command
# lets hold our function first
self.function_to_execute = arg_function
# define widgets
self.main_frame = tk.Frame(self)
self.function_label = tk.Label(self, text='Function is %s\n Let\'s try to call it?' % arg_function.__name__)
self.execute_button = tk.Button(self, text='Execute %s and Quit' % arg_function.__name__,
command=self.execute_and_quit)
# pack stuff
self.main_frame.pack(fill='both', expand=True)
self.function_label.pack(fill='both', expand=True)
self.execute_button.pack(fill='x')
# handle closing
self.protocol('WM_DELETE_WINDOW', lambda: self.execute_and_quit(False))
def execute_and_quit(self, execute=True):
info = None
if callable(self.function_to_execute) and execute:
info = self.function_to_execute()
self.destroy()
self.master.renew(info)
class Class2(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.minsize(350, 200)
# define widgets
self.main_frame = tk.Frame(self)
self.user_info_label = tk.Label(self)
self.quit_button = tk.Button(self, text='Quit', command=self.destroy)
# pack stuff
self.main_frame.pack(fill='both', expand=True)
self.user_info_label.pack(fill='both', expand=True)
self.quit_button.pack(fill='x')
# let's hide our app on initialisation
self.withdraw()
def renew(self, info_to_renew=None):
self.deiconify()
if info_to_renew is None or info_to_renew == '':
self.user_info_label['text'] = 'Your person is unknown'
else:
if type(info_to_renew) is str:
self.user_info_label['text'] = 'Hello, %s! Although there\'s nothing more about you...' % info_to_renew
elif info_to_renew:
self.user_info_label['text'] = 'Gosh! You\'re filthy drunkard!'
else:
self.user_info_label['text'] = 'Are you building your drink refusal skills?'
class Class3:
def __init__(self):
# hold functions
self.arg_functions_to_choose_from = [self.ask_for_name, self.ask_for_drunkenness]
# hold our windows
self.main_app = Class2()
self.main_menu = Class1(self.main_app, self.pick_random_function())
self.main_app.mainloop()
def pick_random_function(self):
return rnd.choice(self.arg_functions_to_choose_from)
def ask_for_name(self):
return sd.askstring(title='Please, introduce yourself', prompt='What is your name?', parent=self.main_menu)
def ask_for_drunkenness(self):
return msg.askyesno(title='Driving under the influence of alcohol is prohibited in this state',
message='Are you drunk?', icon='question', parent=self.main_menu)
# entry point
entry_point = Class3()
As you can see - it's kinda works. But in general this layout is weak and in my humble opinion a class which represents a "bucket of variables" should be avoided. Assume that now our "employers" wants another button in main app that can show again a main menu window. We can either create instance of Class1
inside of Class2
or add a backreference to Class3
. In the first case, our layout with time becomes an illogical mess, and in the second case, our middleman not a container anymore, but a controller.
Of course, all this is just a my subjective opinion, and if you're sufficient with this approach - no one can blame you.
- 3 classes: Toplevel - main menu, Tk - main app, middleman -
controller:
Hey, that layout is what you really desired, since our "middleman" has a method within it which creates and destroys (as you mentioned).
Full code to implement this is very similar to code above (so I decided to show not a complete code, but "abstract pieces"). The differences here're in a backreferences to Class3
(which is controller now), in a relocation of all code, responsible for windows interaction, into Class3
and in the fact, that now there's no reason to pass a reference of the abstract function to Class1
, since it's has a backreference to our middleman-Class3
(we can pull this function from class directly).
# ...
# classes
class Class1(tk.Toplevel):
def __init__(self, master, controller):
tk.Toplevel.__init__(self, master)
# let's keep reference to our middleman
self.controller = controller
# define and pack widgets
# ...
# all other methods interacts with children/widgets of this window and with controller
# ...
class Class2(tk.Tk):
def __init__(self, controller):
tk.Tk.__init__(self)
# let's keep reference to our middleman
self.controller = controller
# define and pack widgets
# ...
# all other methods interacts with children/widgets of this window and with controller
# ...
class Class3:
def __init__(self):
# hold functions
self.arg_functions_to_choose_from = [self.ask_for_name, self.ask_for_drunkenness]
# hold our windows
self.main_app = Class2(self)
self.main_menu = Class1(self.main_app, self)
# all other methods interacts with windows, starts mainloop, handles events, etc...
# ...
# ...
The weakness of this layout lies in the unnecessary complication and in the repetition of functionality. The Tk()
class is already a controller to any of Tk
-related childrens and can effectively manage himself too. I believe that this layout should be left for more complex things when we try to control Tk
-related stuff and (for example) some Python/Platform/Network-related stuff from one class. So here's last (for this answer) option...
- 2 classes: Toplevel - main menu, Tk - main app, container,
controller:
Same logic, very similar layout, but with two classes only. There's no more to say, try to play around with snippet and find differences between this and first example:
# imports
#...
# classes
class Class1(tk.Toplevel):
def __init__(self, master):
tk.Toplevel.__init__(self, master)
self.minsize(350, 200)
# make button with arg_function as the attached command
# lets hold our function first
self.function_to_execute = self.master.pick_random_function()
# define widgets
self.main_frame = tk.Frame(self)
self.function_label = tk.Label(self, text='Function is %s\n Let\'s try to call it?' % self.function_to_execute.__name__)
self.execute_button = tk.Button(self, text='Execute %s and Quit' % self.function_to_execute.__name__,
command=self.execute_and_quit)
# pack stuff
self.main_frame.pack(fill='both', expand=True)
self.function_label.pack(fill='both', expand=True)
self.execute_button.pack(fill='x')
# handle closing
self.protocol('WM_DELETE_WINDOW', lambda: self.execute_and_quit(False))
def execute_and_quit(self, execute=True):
info = None
if callable(self.function_to_execute) and execute:
info = self.function_to_execute()
self.destroy()
self.master.renew(info)
class Class2(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.minsize(350, 200)
self.arg_functions_to_choose_from = [ask_for_name, ask_for_drunkenness]
# define widgets
self.main_frame = tk.Frame(self)
self.user_info_label = tk.Label(self)
self.show_menu_button = tk.Button(self, text='Repeat', command=self.show_menu)
self.quit_button = tk.Button(self, text='Quit', command=self.destroy)
# pack stuff
self.main_frame.pack(fill='both', expand=True)
self.user_info_label.pack(fill='both', expand=True)
self.show_menu_button.pack(fill='x')
self.quit_button.pack(fill='x')
self.show_menu()
def renew(self, info_to_renew=None):
self.deiconify()
if info_to_renew is None or info_to_renew == '':
self.user_info_label['text'] = 'Your person is unknown'
else:
if type(info_to_renew) is str:
self.user_info_label['text'] = 'Hello, %s! Although there\'s nothing more about you...' % info_to_renew
elif info_to_renew:
self.user_info_label['text'] = 'Gosh! You\'re filthy drunkard!'
else:
self.user_info_label['text'] = 'Are you building your drink refusal skills?'
def show_menu(self):
self.withdraw()
menu = Class1(self)
def pick_random_function(self):
return rnd.choice(self.arg_functions_to_choose_from)
# functions
def ask_for_name():
return sd.askstring(title='Please, introduce yourself', prompt='What is your name?')
def ask_for_drunkenness():
return msg.askyesno(title='Driving under the influence of alcohol is prohibited in this state',
message='Are you drunk?', icon='question')
# entry point
main_app = Class2()
main_app.mainloop()
It's not an ideal solution too, because of, again, unnecessary complexity (we trying to handle two separate windows, when our main menu and main app can both inherit from tk.Frame
class, but let's leave something to selfresearch), however, it's a more legit option than first one, if you ask me.
Conclusion:
Your question is both objective (when you ask how to pass something to a class) and subjective (when you ask how to pass something to a class, with your current structure; when you ask for feedback), hence my answer is heavily opinion-based (it's even not an answer in general, because the final decision is yours). You can find a less opinion-based answer here.