0

So I have my Tkinter application that consist of multiple frame

All these multiple frames contain the same basic structure of many buttons; the only difference is that the buttons have a different bg on each page.

In my actual project, these buttons contain so many options, and so having to write the same basic code each time for all pages makes my code look unnecessarily long.

So I'm thinking: Is there a way to put all these buttons into a dictionary or list, and pack them onto each separate frame? (Bear in mind the button will need to inherit the bg variable from the specific frame.)

I've created a minimal example to illustrate what I mean:

import tkinter as tk 
from tkinter import *

listt = []
self = None
bg_colour_for_this_frame = None
button1 = Button(self,text="Button 1",bg=bg_colour_for_this_frame,fg='white')
button2 = Button(self,text="Button 2",bg=bg_colour_for_this_frame,fg='blue')
button3 = Button(self,text="Button 3",bg=bg_colour_for_this_frame,fg='orange')
listt.append(button1)
listt.append(button2)
listt.append(button3)

class Tkinter(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 (StartPage, SecondPage):
            frame = F(container, self)
            self.frames[F] = frame
            frame.grid(row=0, column=0, sticky="nsew")
        self.show_frame(StartPage)

    def show_frame(self, cont):
        frame = self.frames[cont]
        frame.tkraise()
        frame.winfo_toplevel().geometry("860x864")
        frame.configure(bg='#000000')

class StartPage(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        Button(self,text='SecondPage',command=lambda:controller.show_frame(SecondPage)).pack()
        for s in listt:
            s.pack()

class SecondPage(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        Button(self,text='StartPage',command=lambda:controller.show_frame(StartPage)).pack()
        for s in listt:
            s.pack()

app = Tkinter()
app.mainloop()

Or maybe, instead of having a list, use a dictionary:

listt = {'button1':'Button[root,text="Button 1",bg=bg_colour_for_this_frame,fg="white"]',
        'button2':'Button[root,text="Button 2",bg=bg_colour_for_this_frame,fg="red"]',
        'button3':'Button[root,text="Button 3",bg=bg_colour_for_this_frame,fg="blue"]',
       }

I get the error:

s.pack()
AttributeError: 'str' object has no attribute 'pack'

Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
coderoftheday
  • 1,987
  • 4
  • 7
  • 21

1 Answers1

3

Since you can't create the Buttons before the page they're on exists, It would be simpler to make a function and call it during the initialization of each of the page classes — like the make_buttons() shown below:

import tkinter as tk
from tkinter import *

# Button options for all pages.
BTN_OPTS = [dict(text="Button 1", fg='white'),
            dict(text="Button 2", fg='blue'),
            dict(text="Button 3", fg='orange')]


def make_buttons(parent, bg_colour):
    return [Button(parent, bg=bg_colour, **opts) for opts in BTN_OPTS]


class Tkinter(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 (StartPage, SecondPage):
            frame = F(container, self)
            self.frames[F] = frame
            frame.grid(row=0, column=0, sticky="nsew")
        self.show_frame(StartPage)

    def show_frame(self, cont):
        frame = self.frames[cont]
        frame.tkraise()
        frame.winfo_toplevel().geometry("860x864")
        frame.configure(bg='#000000')


class StartPage(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        Button(self, text='SecondPage',
               command=lambda: controller.show_frame(SecondPage)).pack()
        for btn in make_buttons(self, 'Pink'):
            btn.pack()


class SecondPage(tk.Frame):
    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        Button(self, text='StartPage',
               command=lambda: controller.show_frame(StartPage)).pack()
        for btn in make_buttons(self, 'green'):
            btn.pack()

app = Tkinter()
app.mainloop()

A more sophisticated and object-oriented approach would be to define a base class for all page classes that had a method in it something like the function above, and then derive the concrete subclasses from that allowing them just inherit the method. It also gets rid of the global data because the button options are now in a (base) class attribute.

Here's a runnable example of how it could be done that way. Note: it requires Python 3.6+ because it uses object.__init_subclass__() which was added in that version:

import tkinter as tk


class Tkinter(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 (StartPage, SecondPage):
            frame = F(container, self)
            self.frames[F] = frame
            frame.grid(row=0, column=0, sticky="nsew")
        self.show_frame(StartPage)

    def show_frame(self, cont):
        frame = self.frames[cont]
        frame.tkraise()
        frame.winfo_toplevel().geometry("860x864")
        frame.configure(bg='#000000')


class BasePage(tk.Frame):
    # Button options common to all pages.
    BTN_OPTS = [dict(text="Button 1", fg='white'),
                dict(text="Button 2", fg='blue'),
                dict(text="Button 3", fg='orange')]

    @classmethod
    def __init_subclass__(cls, /, bg_color, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.bg_color = bg_color

    def __init__(self, parent, controller, text, command):
        super().__init__(parent)
        tk.Button(self, text=text, command=command).pack()  # Next page button.
        for btn in (tk.Button(self, bg=self.bg_color, **opts) for opts in self.BTN_OPTS):
            btn.pack()


class StartPage(BasePage, bg_color='pink'):
    def __init__(self, parent, controller):
        super().__init__(parent, controller, text='SecondPage',
                         command=lambda: controller.show_frame(SecondPage))


class SecondPage(BasePage, bg_color='green'):
    def __init__(self, parent, controller):
        super().__init__(parent, controller, text='StartPage',
                         command=lambda: controller.show_frame(StartPage))


app = Tkinter()
app.mainloop()
martineau
  • 119,623
  • 25
  • 170
  • 301
  • yeah i done it this way, i just wanted to see if it was possible to assign python lines to a variable or list, thanks though – coderoftheday Sep 22 '20 at 21:50
  • The code in your question proves it's possible to put tkinter `Button`s in lists (and the same is true of dictionaries) — so exactly what you're asking is unclear to me. – martineau Sep 22 '20 at 22:10
  • You can't create the `Button`s before the page they are going to be on exists — which makes creating and saving them in some global list (or dictionary) problematic. In both of my code snippets, the buttons for each page are constructed during subclass' initialization. I think the best you could do is store the _option combinations_ for each button in some container, which is then used that in each subclass to construct (and pack) them in a manner similar to what I've shown. – martineau Sep 23 '20 at 12:20
  • I updated my answer to show putting them in a `list` of dictionaries and using it to create the `Button`s and `pack()` them as each page is initialized. – martineau Sep 23 '20 at 13:06
  • wow that's the type of thing I was looking for, thanks – coderoftheday Sep 23 '20 at 16:56