Paul D
Paul D

Reputation: 91

How to get variable data from a class?

This is a shortened example of a longer application where I have multiple pages of widgets collecting information input by the user. The MyApp instantiates each page as a class. In the example, PageTwo would like to print the value of the StringVar which stores the data from an Entry widget in PageOne.

How do I do that? Every attempt I've tried ends up with one exception or another.

from tkinter import *
from tkinter import ttk

class MyApp(Tk):
    
    def __init__(self):
        Tk.__init__(self)
        container = ttk.Frame(self)
        container.pack(side="top", fill="both", expand = True)
        self.frames = {}
        for F in (PageOne, PageTwo):
            frame = F(container, self)
            self.frames[F] = frame
            frame.grid(row=0, column=0, sticky = NSEW)
        self.show_frame(PageOne)
       
    def show_frame(self, cont):
        frame = self.frames[cont]
        frame.tkraise()
        

class PageOne(ttk.Frame):
    def __init__(self, parent, controller):
        ttk.Frame.__init__(self, parent)
        ttk.Label(self, text='PageOne').grid(padx=(20,20), pady=(20,20))
        self.make_widget(controller)

    def make_widget(self, controller):
        self.some_input = StringVar
        self.some_entry = ttk.Entry(self, textvariable=self.some_input, width=8) 
        self.some_entry.grid()
        button1 = ttk.Button(self, text='Next Page',
                                  command=lambda: controller.show_frame(PageTwo))
        button1.grid()
        
class PageTwo(ttk.Frame):
    def __init__(self, parent, controller):
        ttk.Frame.__init__(self, parent)
        ttk.Label(self, text='PageTwo').grid(padx=(20,20), pady=(20,20))
        button1 = ttk.Button(self, text='Previous Page',
                             command=lambda: controller.show_frame(PageOne))
        button1.grid()
        button2 = ttk.Button(self, text='press to print', command=self.print_it)
        button2.grid()

    def print_it(self):
        print ('The value stored in StartPage some_entry = ')#What do I put here 
        #to print the value of some_input from PageOne

app = MyApp()
app.title('Multi-Page Test App')
app.mainloop()

Upvotes: 9

Views: 8344

Answers (2)

Titus Cheserem
Titus Cheserem

Reputation: 117

I faced a challenge in knowing where to place the print_it function. i added the following to make it work though I don't really understand why they are used.

def show_frame(self,page_name):
   ...
   frame.update()
   frame.event_generate("<<show_frame>>")

and added the show_frame.bind

class PageTwo(tk.Frame):
     def __init__(....):
        ....

        self.bind("<<show_frame>>", self.print_it)
        ...
     def print_it(self,event):
        ...

Without the above additions, when the mainloop is executed, Page_Two[frame[print_it()]] the print_it function executes before PageTwo is made Visible.

try:
    import tkinter as tk # python3 
    from tkinter import font as tkfont
except ImportError:
    import Tkinter as tk #python2
    import tkFont as tkfont

class SampleApp(tk.Tk):
    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)

        self.title_font = tkfont.Font(family="Helvetica", size=18, weight="bold", slant="italic")

        # data Dictionary
        self.app_data = {"name": tk.StringVar(),
                         "address": tk.StringVar()}

        # the container is where we'll stack a bunch of frames
        # on top of each other, then the one we want visible
        # will be raised above the others.

        container = tk.Frame(self)
        container.pack(side="top", fill="both", expand=True)
        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0,weight=1)

        self.frames = {}
        for F in (StartPage, PageOne, PageTwo):
            page_name = F.__name__
            frame = F(parent=container, controller=self)
            self.frames[page_name] = frame

            # put all of the pages in the same location;
            # the one on the top of the stacking order
            # will be the one that is visible
            
            frame.grid(row=0, column=0, sticky="nsew")
        self.show_frame("StartPage")

    def show_frame(self, page_name):
        ''' Show a frame for the given page name '''
        frame = self.frames[page_name]
        frame.tkraise()
        frame.update()
        frame.event_generate("<<show_frame>>")

class StartPage(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller

        label = tk.Label(self, text="this is the start page", font=self.controller.title_font)
        label.pack(side="top", fill="x", pady=10)

        # Update the Name value only
        self.entry1 = tk.Entry(self,text="Entry", textvariable=self.controller.app_data["name"])
        self.entry1.pack()


        button1 = tk.Button(self, text="go to page one", command = lambda: self.controller.show_frame("PageOne")).pack()

        button2 = tk.Button(self, text="Go to page Two", command = lambda: self.controller.show_frame("PageTwo")).pack()

class PageOne(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller

        label = tk.Label(self, text="This is page 1", font=self.controller.title_font)
        label.pack(side="top", fill="x", pady=10)

        # Update the Address value only
        self.entry1 = tk.Entry(self,text="Entry", textvariable=self.controller.app_data["address"])
        self.entry1.pack()

        button = tk.Button(self, text="Go to the start page", command=lambda: self.controller.show_frame("StartPage"))
        button.pack()


class PageTwo(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller

        # Bind the print_it() function to this Frame so that when the Frame becomes visible print_it() is called.
        self.bind("<<show_frame>>", self.print_it)

        label = tk.Label(self, text="This is page 2", font=self.controller.title_font)
        label.pack(side="top", fill="x", pady=10)

        button = tk.Button(self, text="Go to the start page",
                           command=lambda: self.controller.show_frame("StartPage"))
        button.pack()

    def print_it(self,event):
        StartPage_value = self.controller.app_data["name"].get()
        print(f"The value set from StartPage is {StartPage_value}")
        PageOne_value= self.controller.app_data["address"].get()
        print(f"The value set from StartPage is {PageOne_value}")



if __name__ == "__main__":
    app = SampleApp()
    app.mainloop()

Upvotes: 0

Bryan Oakley
Bryan Oakley

Reputation: 385970

Leveraging your controller

Given that you already have the concept of a controller in place (even though you aren't using it), you can use it to communicate between pages. The first step is to save a reference to the controller in each page:

class PageOne(ttk.Frame):
    def __init__(self, parent, controller):
        self.controller = controller
        ...

class PageTwo(ttk.Frame):
    def __init__(self, parent, controller):
        self.controller = controller
        ...

Next, add a method to the controller which will return a page when given the class name or some other identifying attribute. In your case, since your pages don't have any internal name, you can just use the class name:

class MyApp(Tk):
    ...
    def get_page(self, classname):
        '''Returns an instance of a page given it's class name as a string'''
        for page in self.frames.values():
            if str(page.__class__.__name__) == classname:
                return page
        return None

note: the above implementation is based on the code in the question. The code in the question has it's origin in another answer here on stackoverflow. This code differs from the original code slightly in how it manages the pages in the controller. This uses the class reference as a key, the original answer uses the class name.

With that in place, any page can get a reference to any other page by calling that function. Then, with a reference to the page, you can access the public members of that page:

class PageTwo(ttk.Frame):
    ...
    def print_it(self):
        page_one = self.controller.get_page("PageOne")
        value = page_one.some_entry.get()
        print ('The value stored in StartPage some_entry = %s' % value)

Storing data in the controller

Directly accessing one page from another is not the only solution. The downside is that your pages are tightly coupled. It would be hard to make a change in one page without having to also make a corresponding change in one or more other classes.

If your pages all are designed to work together to define a single set of data, it might be wise to have that data stored in the controller, so that any given page does not need to know the internal design of the other pages. The pages are free to implement the widgets however they want, without worrying about which other pages might access those widgets.

You could, for example, have a dictionary (or database) in the controller, and each page is responsible for updating that dictionary with it's subset of data. Then, at any time you can just ask the controller for the data. In effect, the page is signing a contract, promising to keep it's subset of the global data up to date with what is in the GUI. As long as you maintain the contract, you can do whatever you want in the implementation of the page.

To do that, the controller would create the data structure before creating the pages. Since we're using tkinter, that data structure could be made up of instances of StringVar or any of the other *Var classes. It doesn't have to be, but it's convenient and easy in this simple example:

class MyApp(Tk):
    def __init__(self):
        ...
        self.app_data = {"name":    StringVar(),
                         "address": StringVar(),
                         ...
                        }

Next, you modify each page to reference the controller when creating the widgets:

class PageOne(ttk.Frame):
    def __init__(self, parent, controller):
        self.controller=controller
        ...
        self.some_entry = ttk.Entry(self,
            textvariable=self.controller.app_data["name"], ...) 

Finally, you then access the data from the controller rather than from the page. You can throw away get_page, and print the value like this:

    def print_it(self):
        value = self.controller.app_data["address"].get()
        ...

Upvotes: 29

Related Questions