1
\$\begingroup\$

I am trying to build a very simple user interface in Python 3 using Tkinter to be used in windows. and have written a code which is not efficient - it is excessively long for no good reason. I am looking for advice, perhaps even conceptual, about how to make this short and elegant.

Here is a simple excerpt from the code, for only 2 periods. I have many periods, and have created each of them as a class, which is the problem that I am trying to replace. I presume that we should just have one Period class, objects of which will be called sequentially. I do not understand how to do this with Tkinter.

import tkinter as tk
from tkinter import ttk

class Game(tk.Tk):

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

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

        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 (Period1, Period2):
            frame = F(container, self)
            self.frames[F] = frame
            frame.grid(row=0, column=0, sticky='nsew')

    def show_frame(self, cont):
        frame = self.frames[cont]
        frame.tkraise()

class Period1(tk.Frame):

    def __init__(self, parent, controller):

        tk.Frame.__init__(self, parent)

        buttonNext = ttk.Button(self, text='next', command=lambda: controller.show_frame(Period2))
        buttonNext.pack()           

class Period2(tk.Frame):

    def __init__(self, parent, controller):

        tk.Frame.__init__(self, parent)

        buttonNext = ttk.Button(self, text='next', command=lambda: controller.show_frame(Period1))
        buttonNext.pack()

app = Game()
app.mainloop()
\$\endgroup\$
1
\$\begingroup\$

I don't exactly know what you mean by "call" the object. Objects aren't typically something you call. I'm going to assume that in this context you meant "show".

The code you copied was for having completely different "pages" which could be shown by name in any order. However, you want several identical "periods" that you want to access sequentially, or perhaps by number (eg: "show period 1", "show period 2", etc.)

So, assuming that each period is identical except for it's number, the first step is to create a single Period class. It seems reasonable that each period know it's number so you might want to add that to the constructor:

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

Next, in your Game, instead of looping over a list of classes, you need a simpler loop that runs as many times as you want to have periods. For example, if you are creating a game with four periods, you might modify the loop in Game to look like this:

self.periods = []
for period_number in range(4):
    period = Period(container, self, period_number)
    self.periods.append(period)
    period.grid(row=0, column=0, sticky='nsew')

With that, self.periods[0] refers to the first period, self.periods[1] is the second, and so on.

You then need to modify show_frame to take a number rather than a class or name of a class. It should also be renamed to something like show_period to make the intent more clear (I've left error checking out to keep it short):

def show_period(self, period_number):
    frame = self.frames[period_number]
    frame.tkraise()

With that, you now have the ability to show by number any period you want. Since you want to be able to step through them sequentially, I recommend creating a method named next_period which can show the next period and handle any logic about what to do after the last period.

The simplest implementation will be to have show_period remember what the current period is, and then in next_period you simply need to add one:

def show_period(self, period_number):
    ...
    self.current_period = period_number

The implementation of next_period is pretty easy:

def next_period(self):
    if self.current_period == len(self.periods):
        <special code to handle the last period...>
    self.show_period(self.current_period+1)

With that, you can add a button to your Period class that will automatically take it to the next period, without it having to know what the next period is:

class Period(tk.Frame):
    def __init__(self, parent, controller, number):
        ...
        self.next_button = tk.Button(self, text="Next", command=self.next)
        ...

    def next(self):
        self.controller.next_period()

Note: This is not necessarily the best way to structure this program, it is simply one way of doing it. The point was to illustrate how to create multiple instances of a single class, and step through them one at a time.

\$\endgroup\$
  • \$\begingroup\$ I have followed your suggestion, but now nothing shows in the GUI screen... \$\endgroup\$ – splinter Dec 5 '16 at 1:43
  • \$\begingroup\$ @splinter: it sounds like you forgot to call grid on each period. I updated my answer to include that step; I had falsely assumed that step was obvious. \$\endgroup\$ – Bryan Oakley Dec 5 '16 at 2:07
  • \$\begingroup\$ What I have done is to add place() indtead of grid(). Why is that when we use place() nothing shows up? Also, when we use pack(), the next button appears 4 times! Any inuition on why? \$\endgroup\$ – splinter Dec 5 '16 at 2:37
  • \$\begingroup\$ @splinter: When you use place, the widget won't grow to fit all of its contents. Without seeing your code I can only guess, but it's possible that the widget is there but is only 1x1 pixel. That's one reason why I recommend to avoid using place unless pack and grid simply won't work. \$\endgroup\$ – Bryan Oakley Dec 17 '16 at 18:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.