0

Similar to another question I've asked, but feel this is probably the best way to go about it after much consideration.

What I want is for the user to select one of the two radio buttons, hit the "Next Page" button and be brought to just one frame. Within that frame class there will be two labels, but depending on what radio button was selected on the previous frame, I want one of the two labels to appear

here is my code -

import Tkinter as tk

class MainApp(tk.Tk):

    def __init__(self, *args, **kwargs):

        tk.Tk.__init__(self, *args, **kwargs)

        # the main container that holds all the frames
        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 = {}

         # adding frames to the dictionary
        for F in (Page1,Page2):

             frame = F(container,self)

             self.frames[F] = frame

             frame.grid(row = 0, column = 0, sticky = "w")

        self.show_frame(Page1)

    def show_frame(self,page_name):

        #SHOWS A FRAME WITH THE GIVEN NAME
        for frame in self.frames.values():
            frame.grid_remove()
        frame = self.frames[page_name]
        frame.grid()

        #STACKING THE FRAMES
        #frame = self.frames[cont]
        #frame.tkraise()

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

        lbl1 = tk.Label(self,text = "Yes",font =("Helvetica",12,"bold"))
        lbl1.grid(row=1,sticky="W")

        lbl2 = tk.Label(self,text = "No",font =("Helvetica",12,"bold"))
        lbl2.grid(row=1,column=1,sticky="W")

        btn1 = tk.Button(self, text="next page", font=('MS', 24, 'bold'))
        btn1.grid(row=3,column = 0,columnspan=1)

        self.var1 = tk.BooleanVar()
        rButton1 = tk.Radiobutton(self,variable = self.var1,value=True)

        rButton1.grid(row=2,sticky = "W")

        rButton2 = tk.Radiobutton(self,variable = self.var1,value=False)
        rButton2.grid(row=2,column=1,sticky = "W")

        btn1['command']= lambda: controller.show_frame(Page2)


class Page2(tk.Frame,Page1):
    def __init__(self,parent,controller):
        tk.Frame.__init__(self,parent)

        self.var1 = Page1.var1

        lbl = tk.Label(self,text="This is reccomendation 2",font=("Helvetica",12,"bold"))
        lbl.pack_forget()

        lbl1 = tk.Label(self,text="This is reccomendation 3",font=("Helvetica",12,"bold"))
        lbl1.pack_forget()

        # if self.var1.get() == 0:
        #     lbl.pack()
        # else:
        #     lbl1.pack()

So 2 things, firstly, I'm assuming Page2 has to inherit Page1's self.var1, which I attempted doing with this -

class Page2(tk.Frame,Page1):

but only receive this error message -

    self.var1 = Page1.var1
 AttributeError: class Page1 has no attribute 'var1'

which, i find odd, because page 1 DOES have var1?! And, secondly, I'm not even sure if the pack_forget() method is the correct way of achieving this?

update 4/4/16

After a bit of digging around i discovered the StringVar variable.

so after implementing -

    def get_page(self,className):

    for page in self.frames.values():
        if str(page.__class__.__name__) == className:
            return page
        return None

in the controller, I can now access page 1's self.var1

I have updated my radio buttons -

    self.var1 = tk.StringVar()

    rButton1 = tk.Radiobutton(self,variable = self.var1,value="reco 1"
                              ,command = "this is reco 1")
    rButton1.grid(row=2,sticky = "W")

    rButton2 = tk.Radiobutton(self,variable = self.var1,value="reco 2"
                              ,command = "this is reco 2")
    rButton2.grid(row=2,column=1,sticky = "W")

from what I gathered about the StringVar, based on which radio button is selected, the string associated with the radio button gets stored in the String Var? could be entirely wrong... anyway, I have now updated page 2 -

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

    self.controller = controller

    page_one = self.controller.get_page("Page1")

    # Access to Page1's self.var1
    reco = page_one.var1.get()

    lbl = tk.Label(self,text= reco,font=("Helvetica",12,"bold"))
    lbl.pack()

The program runs, but frustratingly it doesn't run as I want it, as soon as the next page button gets pressed nothing appears on the second frame. Am I heading in the right direction with this thought process or is there another way to do this? either way the text has to appear on the next frame.

Treeno1
  • 139
  • 1
  • 2
  • 18
  • 1
    Page2 inheriting from Page1 is absolutely the wrong answer. Does this help? http://stackoverflow.com/a/32213127/7432 – Bryan Oakley Apr 03 '16 at 00:01
  • Page1 has no attribute var1 (Page1.var1). You have created an instance attribute named var1 but are referring to a class attribute=Page1.var1. The difference --> https://www.toptal.com/python/python-class-attributes-an-overly-thorough-guide –  Apr 03 '16 at 00:56
  • @BryanOakley, yes, thats great cheers! I can now access the variable from page 1 - obstacle 1 down, but I still cannot get 1 of the two labels to display depending on which radio button i select? I would be incredibly grateful for any additional guidance – Treeno1 Apr 04 '16 at 14:14
  • `import Tkinter as tk` is definitely not Python3 – Wayne Werner Apr 04 '16 at 15:48

1 Answers1

0

There are numerous problems with this, but I tried to keep things along a similar approach to the one you used. And it works... but I would normally take a rather different approach to things.

try:
    import Tkinter as tk
except ImportError:
    import tkinter as tk


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

        # the main container that holds all the frames
        container = tk.Frame(self)

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

        def show_1(button):
            self.frame_2.grid_remove()
            self.frame_1.show(button)

        def show_2(button):
            self.frame_1.grid_remove()
            self.frame_2.show(button)

        self.frame_1 = Page1(container, show_2)
        self.frame_2 = Page2(container, show_1)


class Page1(tk.Frame):
    def __init__(self,parent,callback):
        tk.Frame.__init__(self,parent)
        self.grid(row = 0, column = 0, sticky = "w")
        self.callback = callback

        lbl1 = tk.Label(self,text = "Yes",font =("Helvetica",12,"bold"))
        lbl1.grid(row=1,sticky="W")

        lbl2 = tk.Label(self,text = "No",font =("Helvetica",12,"bold"))
        lbl2.grid(row=1,column=1,sticky="W")

        btn1 = tk.Button(self, text="next page", font=('MS', 24, 'bold'))
        btn1.grid(row=3,column = 0,columnspan=1)

        self.var1 = tk.BooleanVar()
        rButton1 = tk.Radiobutton(self,variable = self.var1,value=True)

        rButton1.grid(row=2,sticky = "W")

        rButton2 = tk.Radiobutton(self,variable = self.var1,value=False)
        rButton2.grid(row=2,column=1,sticky = "W")

        btn1['command']= self.button_clicked

    def button_clicked(self):
        if self.var1.get():
            self.callback('button_one')
        else:
            self.callback('button_two')


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

    def show(self, selected_button):
        if selected_button == 'button_one':
            text = "This is reccomendation 2"
        elif selected_button == 'button_two':
            text = "This is reccomendation 3"
        else:
            text = selected_button
        print(selected_button)
        lbl = tk.Label(self,text=text,font=("Helvetica",12,"bold"))
        lbl.pack()
        self.grid()


app = MainApp()
app.mainloop()

A better approach

If I were just using Tkinter I would probably use something a little more like this:

import random
try:
    import tkinter as tk
except ImportError:
    import Tkinter as tk


class Choices(tk.Frame):
    def __init__(self, choice_a, choice_b, callback):
        super().__init__()

        self._choice = tk.StringVar()

        self.left_button = tk.Radiobutton(
            self,
            variable=self._choice,
            text=choice_a,
            value=choice_a,
        )
        self.left_button.grid(row=0, column=0, sticky=tk.W)

        self.right_button = tk.Radiobutton(
            self,
            variable=self._choice,
            text=choice_b,
            value=choice_b
        )
        self.right_button.grid(row=0, column=1, sticky=tk.E)

        self._choice.set(choice_a)

        self.next_button = tk.Button(self, text='Next', command=callback)
        self.next_button.grid(row=1, column=0, columnspan=2)

        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)
        self.grid_columnconfigure(1, weight=1)

    @property
    def choice(self):
        return self._choice.get()

    @property
    def choice_a(self):
        return self.left_button['text']

    @choice_a.setter
    def choice_a(self, value):
        self.left_button.config(text=value, value=value)

    @property
    def choice_b(self):
        return self.right_button['text']

    @choice_b.setter
    def choice_b(self, value):
        self.right_button.config(text=value, value=value)


class PossibleSelections(tk.Frame):
    def __init__(self, root, selections, command):
        super().__init__(root)
        self.command = command
        self.show_selections(selections)

    def show_selections(self, selections):
        for widget in self.winfo_children():
            widget.destroy()

        for selection in selections:
            tk.Label(self, text=selection).pack()

        tk.Button(self, command=self.command).pack()


class Example(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title('Example Chooser')

        self.choice_frame = Choices(
            'Do you like this?',
            'Do you like that?',
            self.show_options,
        )
        self.choice_frame.pack()
        self.selections_frame = PossibleSelections(self, [], self.show_choices)

    def show_options(self):
        self.choice_frame.pack_forget()

        self.selections_frame.show_selections(
            random.sample('ABCDEF', 3)+[self.choice_frame.choice],
        )
        self.selections_frame.pack()

    def show_choices(self):
        self.selections_frame.pack_forget()
        options = random.sample([
            'My favorite color is blue',
            'My favorite color is yellow',
            'My favorite color is blu-no, yelloooooow!',
            'I seek the grail',
            'My name is Arthur, King of the Britons',
        ], 2)
        self.choice_frame.choice_a = options[0]
        self.choice_frame.choice_b = options[1]
        self.choice_frame.pack()


if __name__ == '__main__':
    Example().mainloop()

Why is This Better?

In your current approach you're doing some things that most other Python developers will find strange, like keeping a dictionary of frames around. That's not necessarily a wrong approach, but there's probably a better way. If you had an unknown or unlimited number of frames, that could be a good approach. But in reality you probably are going to have two. And having them just be attributes is a good approach.

Another advantage to this second approach is something that's referred to as separation of concerns. Neither the PossibleSelections or the Choices frames have anything to do with each other. They know nothing about what properties are found on the other class, or indeed, even that the other class exists. That's a good thing, because it means if you need something different to happen when Choices is finished, you just have to change what Example does. You don't need to fiddle with Choices. That means that you will have less opportunity to introduce bugs, and more flexibility if you need to change something.

Finally, by using widget.destroy() vs _forget() we're actually destroying the widgets that we no longer need. If we simply call _forget() then we'll keep those widgets in memory. Sure, if your application only goes through 10 screens you may not ever notice the memory leak, but if your application grows you'll end out running out of memory and your application will crash.

Further improvements

Of course if I were really writing an application that was more than just a toy/assignment, I would probably use some type of variation of the Model-View-Presenter architecture. Using this architecture you'd have the application written in such a way that your UI layer (all the Tkinter classes) would only deal with interfacing with the user. Your actual core logic would be found in your model classes, with wiring happening via presenter. That's probably excessive here, but it might be a useful exercise.

Wayne Werner
  • 49,299
  • 29
  • 200
  • 290
  • Thats great @Wayne Werner. I should probably explain why I wanted it this way. I have a group assignment where I have to create a questionnaire as part of a larger program with a series of roughly ten questions. each of the questions is supposed to represent 2 university courses (Yes and no), depending on the answers, when the "Next Page" button is pressed, 1 of however many course recommendations is displayed. Obviously with more radio buttons, i will probably have to create a variable to store a tally of the answers given and display a recommendation based on that – Treeno1 Apr 04 '16 at 16:27
  • And, also, what the the problems with your approach are? – Treeno1 Apr 04 '16 at 16:32
  • Most notably your approach didn't work because `Page1` refers to the class, not the instance of the class. You actually *could* have `controller.frames[Page1].var1`, but ewww... that's knowing way too much about the members of your members. – Wayne Werner Apr 04 '16 at 20:32
  • @ WayneWerner based on your approach, if I added a button to the second page, to return to the first, how could I go about achieving this?, would it be possible to explain your code a little bit ? Thanks again – Treeno1 Apr 04 '16 at 20:57
  • @ WayneWerner, As you rightly pointed out, it is definitely not python 3! now I feel silly. its actually 2.7.10. So trying to run the code I get given super().__init__() TypeError: super() takes at least 1 argument (0 given) – Treeno1 Apr 04 '16 at 22:58
  • for Python2 you need [`super(Child, self).__init__()`](http://stackoverflow.com/q/222877/344286) – Wayne Werner Apr 05 '16 at 01:14