1

Following Bryan Oakley's answer to this post Best way to structure a tkinter application?, I would like to move the contents of tab1 to its own class, but leave say_hello and get_item in the main class as they are. How do I do that?

import tkinter as tk
from tkinter import ttk, N, S, END, EXTENDED

class MainApplication(tk.Frame):
    def __init__(self, root, *args, **kwargs):
        tk.Frame.__init__(self, root, *args, **kwargs)
        self.root = root
        self.nb = ttk.Notebook(root)

        self.tab1 = ttk.Frame(self.nb)
        self.tab2 = ttk.Frame(self.nb)

        self.nb.add(self.tab1, text='TAB1')
        self.nb.add(self.tab2, text='TAB2')

        self.nb.grid(row=0, column=0)

        #contents of tab1 - which I would like to place in their own class
        self.frame = tk.Frame(self.tab1)
        self.frame.grid(row=0, column=0)

        self.button = tk.Button(self.frame, text='Hello', command=self.say_hello)
        self.button.grid(row=0, column=0)

        self.items = ['A','B','C']

        self.listbox = tk.Listbox(self.frame, selectmode=EXTENDED, exportselection=0)
        self.listbox.grid(row=1, column=0)

        self.scrollbar = tk.Scrollbar(self.frame, orient='vertical')
        self.scrollbar.grid(row=1, column=1, sticky=N+S)
        self.listbox.config(yscrollcommand=self.scrollbar.set)
        self.scrollbar.config(command=self.listbox.yview)
        self.listbox.bind('<<ListboxSelect>>', self.get_item)

        for item in self.items:
            self.listbox.insert(END, item) 


        #contents of tab2 - which should be in their own class
        #...
        #...


    def say_hello(self):
        print('hello')

    def get_item(self, evt):
        w = evt.widget
        index = int(w.curselection()[0])
        print('item selected = {}'.format(self.items[index]))

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

EDIT:

Thank you Saad for your detailed response. I ran your code, studied it, and learned quite a bit. However, I modified my question to make it more focused.

Murchak
  • 183
  • 1
  • 3
  • 17
  • probably better suited for [codereview] – Reblochon Masque May 17 '20 at 10:39
  • @stovfl et al. thank you for the review. I edited the post to ask a very specific question, that is to move the contents of tab1 to its own class, but leave the say_hello & get_item methods in the main class. How to handle that. Is this specific enough to address what I am asking? – Murchak May 17 '20 at 18:44

1 Answers1

2

According to me, I think everyone has their own style of writing and organizing their files and classes. My way of organizing code might not be the best. But I'll try my best to make it organized and simpler for you. Though there is no hard and fast rule on how to organize any Tkinter application. But yes OOP is the best way to organize big projects as they make the code small and easier to understand.

You can create a class for each tab (Tab1, Tab2, ..) in a separate file (tab1.py, tab2.py, ..) but I would rather have all my tabs in one file named as tabs.py. So I can import them like so..

import tabs

tabs.Tab1()
tabs.Tab2()
...

Kindly go through each comment in the code and my points to get most of your answers.

I've divided your code into 4 parts.

  1. First I made a base frame class for all tabs. In that base frame class, we can configure things that all tabs require in common. For example, let's say you want a frame in each tab that has background color 'lightyellow'. So you can create a base class and put it in a separate file as baseframe.py. like so...

    # baseframe.py
    import tkinter as tk
    
    class BaseFrame(tk.Frame):
        "This is a base frame for Tabs."
        def __init__(self, master=None, **kw):
            super().__init__(master=master, **kw)
            # This will be your baseframe, if you need it.
            # Do something here that is common for all tabs.
            # for example if you want to have the same color for every tab.
    
            # These are just some example, change it as per your need.
            self['bg'] = 'lightyellow'
            self['relief'] = 'sunken'
            self['borderwidth'] = 5
            ...
    
        def show_info(self):
            "This function is common to every tab"
            # You don't necessarily need this, 
            # This is just to show you the possibilities.
    
            # Do something in this function, that you want to have in every tab.
            ...
            ...
            print('show_info', self)
            return
    
  2. Would it make sense to have a separate file with a function in it for creating a generic Listbox?

    I think a CustomListbox class would be nice, you can be very creative in creating this class but I kept it simple. I added a scrollbar inside of CustomListbox class and configured it.

    You can either create file customlistbox.py for this. or change the name of baseframe.py to basewidgets.py and keep both BaseFrame and CustomListbox in one file.

    # customlistbox.py
    import tkinter as tk
    
    class CustomListbox(tk.Listbox):
        "Tkinter listbox which has vertical scrollbar configured."
        def __init__(self, master=None, cnf={}, **kw):
            super().__init__(master=master, cnf=cnf, **kw)
            # Config scrollbar
            self.scrollbar = tk.Scrollbar(self.master,orient='vertical',command=self.yview)
            self.config(yscrollcommand=self.scrollbar.set)
    

    See in tab classes how to use it.

  3. Here's tabs.py file,

    # tabs.py
    import tkinter as tk
    from baseframe import BaseFrame
    from customlistbox import CustomListbox
    from tkinter import ttk, N, S, END, EXTENDED
    
    class Tab1(BaseFrame):
        "This is Tab 1."
        def __init__(self, master=None, **kw):
            super().__init__(master=master, **kw)
            self.button2 = tk.Button(self,text='Welcome to Tab1', command=self.show_info)
            self.button2.grid(row=0, column=0)
            self.button = tk.Button(self, text='Hello', command=self.say_hello)
            self.button.grid(row=1, column=0)
    
            self.items = ['A','B','C']
    
            self.listbox = CustomListbox(self, selectmode=EXTENDED, exportselection=0)
            self.listbox.grid(row=2, column=0)
            self.listbox.scrollbar.grid(row=2, column=1, sticky=N+S)
            self.listbox.bind("<<ListboxSelect>>", self.get_item)
    
            for item in self.items:
                self.listbox.insert(END, item)
    
        def say_hello(self):
            "Callback function for button."
            print('hello')
    
        def get_item(self, evt):
            "Internal function."
            w = evt.widget
            index = int(w.curselection()[0])
            print('item selected = {}'.format(self.items[index]))
    
    class Tab2(BaseFrame):
        "This is Tab2."
        def __init__(self, master=None, **kw):
            super().__init__(master=master, **kw)
            self.button = tk.Button(self,text='Welcome to Tab2', command=self.show_info)
            self.button.grid(row=0, column=0)
    
  4. Finally comes the main.py file. Here you can import your tabs (import tabs). I organized the MainApplication class according to my comfort. For example, rather than creating each instance (self.tab1, self.tab2,..) for tabs, I created a list self.tabs to contain all the tabs, which is more convenient to access through the index of self.tabs.

    # main.py
    import tkinter as tk
    import tabs as tb
    from tkinter import ttk, N, S, END, EXTENDED
    
    
    class MainApplication(ttk.Notebook):
        def __init__(self, master=None, **kw):
            super().__init__(master=master, **kw)
            self.master = master
            self.tabs = []  # All tabs list, easier to access.
    
            # Add tabs here.
            self.add_tab(tb.Tab1, text='TAB1')
            self.add_tab(tb.Tab2, text='TAB2')
            ...
            ...
    
            # Excess methods of Tabs:-
            self.tabs[0].say_hello()
            self.tabs[1].show_info()
    
            # for example: Bind say_hello() with "Button-2" to this class.
            self.bind('<Button-2>', lambda evt: self.tabs[0].say_hello())
    
    
        def add_tab(self, tab, **kw):
            "Adds tab to the notebook."
            self.tabs.append(tab(self))
            self.add(self.tabs[-1], **kw)
    
    
    if __name__ == "__main__":
        root = tk.Tk()
    
        # Creating an container.
        container = tk.Frame(root)
        container.grid()
    
        # Initializing the app.
        app = MainApplication(container)
        app.grid()
    
       root.mainloop()
    

Hopefully, this has made a lot of things clear to you.

Saad
  • 3,340
  • 2
  • 10
  • 32
  • Thank you so much Saad for your detailed response. I am going to apply your approach to my projects in general, but for now I modified my question to be very specific. Would you help with the specifics of what I a really getting at? In your response you moved say_hello and get_item along with the contents of tab1. What if I want to leave those methods in the main body. How do I do that? – Murchak May 17 '20 at 18:56
  • @Murchak: You can leave those in the main class if you want and access tabs from the `self.tabs` to config them in the main class. Watch [this video](https://youtu.be/IYHJRnVOFlw) to get a better understanding of oops with Tkinter. – Saad May 17 '20 at 20:21
  • Thanks Saad. Yes, I have see literally dozens of videos like that which don't really go beyond the basics. For example, the stuff that you explained lies outside of the scope of ANY video on tkinter that I have ever watched. That is the reason why I posted my question here to get more details for experienced pros. In my modified question, I am asking something very specific that I have not seen in videos or online tutorials. – Murchak May 17 '20 at 23:00