简体   繁体   中英

Tkinter.TopLevel; passing a function from another class?

I'm currently writing a small program, and I want to put a main menu of sorts to open first, with buttons on this main menu which open a seperate window. In order to bring this main menu and seperate window together (They're both classes), I've made a third container class, as shown below:

class1(tk.TopLevel):
    def __init__(self, arg1):
           #make button with arg1 function as the attached command
class2:
    ....
    def arg1(self):
        #initialize main app 

 class3:
      def __init__(self):
          class2()
          class1(arg1)

The issue I face is I don't want class 2 to be open a window until the button is pressed; is there a way to do this? With toplevel theres always self.withdraw, which I was planning to use to remove the mainmenu when needed after the button is pressed.

What I'm thinking (Please tell me if this sounds reasonable) is to have the main menu class take the abstract function, and the "container" class acts as the middleman and has a method within it which creates and destroys the class1 object (the main app) when called. This method would then be what I attach to the mainmenu.

Can I get some help/feedback on how to approach this?

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 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM