Nested Class factory with tkinter - python
I'm trying to build a script for import in my future projects.
That Script should create some tk.Frames in a tk.Frame and let me edit the created ones in a main.
I think, the best way to get there is to create a Holder_frame class and put some nested classes in.
so I could call them in my main with Holder_frame.F1.
I tried a lot of code and I ended up here making me an account.
Anyway here is where Im at:
import tkinter as tk
from tkinter import Frame,Button
class BaseClass(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.master = master
self.pack()
class Holder_frame(tk.Frame):
Names = []
def __init__(self, master, frames=2):
tk.Frame.__init__(self, master)
self.master = master
frame_names = Holder_frame.Names
for i in range(0,frames):
frame_names.append("F"+str(i+1))
print(frame_names)
Holder_frame.factory()
def factory():
print(Holder_frame.Names)
print(type(BaseClass))
for idex,i in enumerate (Holder_frame.Names):
print(i)
class NestedClass(BaseClass):
pass
NestedClass.__name__ = i
NestedClass.__qualname__ = i
if __name__ == "__main__":
root = tk.Tk()
def raise1():
Holder_frame.F1.tkraise()
def raise2():
Holder_frame.F2.tkraise()
holder=Holder_frame(root,frames=2)
holder.grid(row=1,column=0)
b1 = tk.Button(root, text='1', command=raise1)
b1.grid(row=0,column=0)
b2 = tk.Button(root, text='2', command=raise2)
b2.grid(row=0,column=1)
root.mainloop()
Everything works fine, till I try to call a Frame.
(AttributeError 'Holder_frame' object has no attribute 'F1')
I think my problem is the structure but need some help to solve it.
Any suggestions?
If I'm getting it right I think you mean to have some sort of a Base class that has some configuration which a set of frames have in common like for example you want to have 10 frames of 300x400 geometry and of a brown background in common and later having another set of frames with a different configuration, which can be accessed in an organised way. Then I would say you have an interesting way but I would rather use a list or a dictionary anyway.
Here are some approaches to achieve this goal.
Approach 1
In this approach, I've created a function that returns a dictionary with all the frames created and contained in it like in format ({..., 'F20': tkinter.frame, ...})
import tkinter as tk
def get_base_frames(num, master, cnf={}, **kw):
"""
Create list of frames with common configuration options.
Args:
num (int): Number of frames to be created.
master (tk.Misc): Takes tkinter widget or window as a parent for the frames.
cnf (dict): configuration options for all the frames.
kw: configuration options for all the frames.
Return:
Dictionary of frames ({..., 'F20': tkinter.frame, ...}).
"""
return {f'F{n+1}': tk.Frame(master, cnf=cnf, **kw) for n in range(num)}
if __name__ == "__main__":
root = tk.Tk()
frame_holder = get_base_frames(10, root, width=50, height=50, bg='brown')
# Frames can be accessed through their names like so.
print(frame_holder.get('F1'))
Approach 2
Here I've used class and objects. Where I made this class Frames though you can name it anything you want. I also added some important method like cget() and configure(), through these methods once get a value to an option and configure options for all the frames respectively. There are more useful methods like bind() and bind_all() if you need those just modify this class as per your need.
import tkinter as tk
class Frames(object):
def __init__(self, master=None, cnf={}, **kw):
super().__init__()
num = cnf.pop('num', kw.pop('num', 0))
for n in range(num):
self.__setattr__(f'F{n+1}', tk.Frame(master, cnf=cnf, **kw))
def configure(self, cnf={}, **kw):
"""Configure resources of a widget.
The values for resources are specified as keyword
arguments. To get an overview about
the allowed keyword arguments call the method keys.
"""
for frame in self.__dict__:
frame = self.__getattribute__(frame)
if isinstance(frame, tk.Frame):
if not cnf and not kw:
return frame.configure()
frame.configure(cnf=cnf, **kw)
config = configure
def cget(self, key):
"""Return the resource value for a KEY given as string."""
for frame in self.__dict__:
frame = self.__getattribute__(frame)
if isinstance(frame, tk.Frame):
return frame.cget(key)
__getitem__ = cget
if __name__ == "__main__":
root = tk.Tk()
frame_holder = Frames(root, num=10, width=10,
bd=2, relief='sunken', bg='yellow')
# Frames can be accessed through their naems like so.
print(frame_holder.F4)
print(frame_holder['bg'])
frame_holder.config(bg='blue')
print(frame_holder['bg'])
Approach 3
If you want to have differently configured frames contained in one class, where all those frames have some method in common or some attribute in common.
import tkinter as tk
class BaseFrame(tk.Frame):
def __init__(self, master=None, cnf={}, **kw):
super().__init__(master=master, cnf={}, **kw)
def common_function(self):
"""This function will be common in every
frame created through this class."""
# Do something...
class FrameHolder(object):
def __init__(self, master=None, cnf={}, **kw):
kw = tk._cnfmerge((cnf, kw))
num = kw.pop('num', len(kw))
for n in range(num):
name = f'F{n+1}'
cnf = kw.get(name)
self.__setattr__(name, BaseFrame(master, cnf))
if __name__ == "__main__":
root = tk.Tk()
holder = FrameHolder(root,
F1=dict(width=30, height=40, bg='black'),
F2=dict(width=50, height=10, bg='green'),
F3=dict(width=300, height=350, bg='blue'),
F4=dict(width=100, height=100, bg='yellow'),
)
print(holder.F1)
print(holder.__dict__)
Approach 4
This is the approach that OP is trying to achieve.
import tkinter as tk
class BaseClass(tk.Frame):
def __init__(self, master, cnf={}, **kw):
kw = tk._cnfmerge((cnf, kw))
cnf = [(i, kw.pop(i, None))
for i in ('pack', 'grid', 'place') if i in kw]
tk.Frame.__init__(self, master, **kw)
self.master = master
if cnf:
self.__getattribute__(cnf[-1][0])(cnf=cnf[-1][1])
class Container(tk.Frame):
"""Container class which can contain tkinter widgets.
Geometry (pack, grid, place) configuration of widgets
can also be passed as an argument.
For Example:-
>>> Container(root, widget=tk.Button,
B5=dict(width=30, height=40, bg='black',
fg='white', pack=(), text='Button1'),
B6=dict(width=50, height=10, bg='green', text='Button2',
place=dict(relx=0.5, rely=1, anchor='s')))
"""
BaseClass = BaseClass
def __init__(self, master=None, cnf={}, **kw):
kw = tk._cnfmerge((cnf, kw))
wid = kw.pop('widget', tk.Frame)
for name, cnf in kw.items():
geo = [(i, cnf.pop(i, None))
for i in ('pack', 'grid', 'place') if i in cnf]
setattr(Container, name, wid(master, cnf))
if geo:
manager, cnf2 = geo[-1]
widget = getattr(Container, name)
getattr(widget, manager)(cnf=cnf2)
if __name__ == "__main__":
root = tk.Tk()
Container(root, widget=Container.BaseClass,
F1=dict(width=30, height=40, bg='black', relief='sunken',
pack=dict(ipadx=10, ipady=10, fill='both'), bd=5),
F2=dict(width=50, height=10, bg='green',
pack=dict(ipadx=10, ipady=10, fill='both')),
)
Container(root, widget=tk.Button,
B5=dict(width=30, height=40, bg='black',
fg='white', pack={}, text='Button1'),
B6=dict(width=50, height=10, bg='green', text='Button2',
place=dict(relx=0.5, rely=1, anchor='s')),
)
print(Container.__dict__)
root.mainloop()
A lot can be done and can be modified according to one's needs, these are just some approaches that I think will work very well to automate and keep a set of frames in shape and together.
There can be multiple ways to do this or maybe something better and efficient than these, feel free to give suggestions and share something new.
One solution to this problem, I think, as I don't fully understand your question, but this here was my solution:
import tkinter as tk
from tkinter import Frame,Button
class BaseClass(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.master = master
self.pack()
class Holder_frame(tk.Frame):
def __init__(self, master, frames=2):
tk.Frame.__init__(self, master)
self.master = master
self.frame_names = []
for i in range(frames):
Holder_frame.create_frames("F"+str(i+1), self)
#classmethod
def create_frames(cls, name, master):
setattr(cls, name, tk.Frame(master))
if __name__ == "__main__":
root = tk.Tk()
def raise1():
print(type(Holder_frame.F1))
def raise2():
print(type(Holder_frame.F2))
holder=Holder_frame(root,frames=2)
holder.grid(row=1,column=0)
b1 = tk.Button(root, text='1', command=raise1)
b1.grid(row=0,column=0)
b2 = tk.Button(root, text='2', command=raise2)
b2.grid(row=0,column=1)
print(Holder_frame.__dict__.items())
root.mainloop()
The use of setattr allows one to add variables to the class, just like if you were to type a function into the code. This allows you to access frames from outside the class as somewhat of a "global variable"
I used a file to test if it work outside as an imported module too:
# main.py
from nested_class import Holder_frame
import tkinter as tk
root = tk.Tk()
holder=Holder_frame(root,frames=1000)
holder.grid(row=1,column=0)
print(Holder_frame.__dict__.items())
root.mainloop()
I hope this answers your question,
James
EDIT:
After thinking there is, what I think, to be a cleaner system for what you want. With the code from this post one can see that your my written system could be replaced by a ttk.Notebook, and by removing the top bar by using style.layout('TNotebook.Tab', []), one can see that you would get a frame widget that could have frame widgets inside of it:
import tkinter as tk
import tkinter.ttk as ttk
class multiframe_example:
def __init__(self, master):
self.master = master
style = ttk.Style()
style.layout('TNotebook.Tab', [])
notebook = ttk.Notebook(self.master)
notebook.grid(row=0, column=0)
self.master.grid_rowconfigure(0, weight=1)
self.master.grid_columnconfigure(0, weight=1)
tab1 = tk.Frame(self.master, width=500, height=500, background="green")
tab2 = tk.Frame(self.master, width=500, height=500)
tab3 = tk.Frame(self.master, width=500, height=500)
notebook.add(tab1)
notebook.add(tab2)
notebook.add(tab3)
notebook.select(0) # select tab 1
notebook.select(1) # select tab 2
notebook.select(2) # select tab 3
def main():
root = tk.Tk()
root.geometry("500x500")
multiframe_example(root)
root.mainloop()
if __name__ == '__main__':
main()
Hope this code can support you and does as you would like!
Related
Override class where mutliple kwargs are used in different functions
I am looking to override the Tkinter Frame, Button, Lable and Entry widgets to have them creted and packed in the same line of code. (IMO 2 lines to do this and get a reference for the obj is ugly, inefficient and harder to read even if it provides more flexability). My previous code was this: def cFrame(self, element, bg="white", borderwidth=0, relief="groove", side=None, padx=0, pady=0, height=0, width=0, expand=0, fill=None, image=None, highlightbackground=None, highlightcolor=None, highlightthickness=0, ipadx=0, ipady=0): f = self.TkUtil.Frame(element, bg=bg, borderwidth=borderwidth, relief=relief, height=height, width=width, image=image, highlightbackground=highlightbackground, highlightcolor=highlightcolor, highlightthickness=highlightthickness) f.pack(side=side, padx=padx, pady=pady, ipadx=ipadx, ipady=ipady, expand=expand, fill=fill) return f My proposed class would look like this: class cFrame(Frame): def __init__(self, master, **kwargs): Frame.__init__(*(self, master), **kwargs) self.pack(**kwargs) The issue with this being **kwargs does not care if the keyword is valid and returns 'bad option -"kwarg"'. I have tried expanding the function a bit more but I keep having issues: class cButton(Button): def __init__(self, master, **kwargs): Button.__init__(*(self, master)) self.conf(**kwargs) def conf(self, **kwargs ): __pk_ops = {} for k, v in kwargs.items(): try: self.configure({k:v}) except: __pk_ops[k] = v self._pk(__pk_ops) def _pk(self, __ops): self.pack(__ops) In this code, I have to loop through all kwargs before and indervidually configure them which is a lot of wasted time over the 1000's of widgets needed. Additionally, I have issues where self.configure(text="sometext") errors so 'text' is passed through to the self.pack() method because for some magic reason it doesn't like that option in particular (same for compound in this example too). My end goal is to have x = Frame(root, bg="black") \n x.pack(side=TOP) be replaced with x = cFrame(root, bg="black", side=TOP) without default any of the options like my current code making any changes difficult and any sort of theam almost impossible.
You can create a custom class to override the pack() as below: def cWidget: def pack(self, **kw): super().pack(**kw) # call original pack() return self # return widget itself # can do the same for grid() and place() Then create custom widget class as below: import tkinter as tk class cButton(cWidget, tk.Button): pass # do the same for other widgets you want Now you can use the custom button class as below: ... root = tk.Tk() def clicked(): print('Button clicked') btn['text'] = 'Clicked' # can use 'btn' to change its text as well btn = cButton(root, text='Click me!', command=clicked, bg='gold').pack(padx=10, pady=10) print(f'{btn=}') # here you won't get None print(isinstance(btn, tk.Button)) # here you get True ... Note that for Python 3.8+, you can simply use the Walrus Operator (:=): (btn := tk.Button(root, text='Click me!')).pack(padx=5, pady=5)
tkinter: Why am I getting a small window plus my main window and gridding is off? __init__ problem?
Creates two windows and gridding is not correct. Some additional comments in the code initiation. I have used this approach, without the super init with no problem, many times. Advice appreciated. Thanks # timhockswender#gmail.com import tkinter as tk from tkinter import ttk class constants_page(tk.Frame): def __init__(self): super(constants_page, self).__init__() # from stackoverflow # if not used error = 'constants_page' object has no attribute 'tk' # if used, another tiny window is opened # in addtion to the constants_page self.constants_page = tk.Tk() self.constants_page.geometry("1000x500") #width*Length self.constants_page.title("Owen's Unit Conversion App") self.constants_page.configure(background='light blue') self.CreateWidgets() def CreateWidgets(self): self.value_label = ttk.Label(self.constants_page,text="Value----->" , width =10 ) self.value_label.grid(row=0, column=1, columnspan=1, sticky='nse') # Problem: not gridding properly self.title_label = ttk.Label(self.constants_page, text="Important Physical Constants", anchor=tk.CENTER, font=("Arial",20)).grid(row=2, columnspan=2) for r in range(2): self.constants_page.rowconfigure(r, weight=1, uniform='row') for c in range(2): self.constants_page.columnconfigure(c, weight=1 ) def Show_Page(): # Create the entire GUI program program = constants_page() program.mainloop() if __name__ == "__main__": Show_Page()
The super call expects you to provide a root window (an instance of tk.Tk()). If you don't provide one it defaults to the first root window opened, and if none has been opened yet then it helpfully opens one for you. A few lines later you open a second one yourself. The easy fix is to remove the self.constants_page = tk.Tk() line. The proper fix is to make the Tk() instance outside of the class and pass it in. This allows you to use the Frame class itself to lay out widgets (use self instead of self.constants_page). Try this: import tkinter as tk from tkinter import ttk class constants_page(tk.Frame): def __init__(self, master=None, **kwargs): super().__init__(master, **kwargs) master.geometry("1000x500") #width*Length master.title("Owen's Unit Conversion App") self.configure(background='light blue') self.CreateWidgets() def CreateWidgets(self): self.value_label = ttk.Label(self,text="Value----->" , width =10 ) self.value_label.grid(row=0, column=1, columnspan=1, sticky='nse') self.title_label = ttk.Label(self, text="Important Physical Constants", anchor=tk.CENTER, font=("Arial",20)).grid(row=2, columnspan=2) for r in range(2): self.rowconfigure(r, weight=1, uniform='row') for c in range(2): self.columnconfigure(c, weight=1 ) def Show_Page(): # Create the entire GUI program program = tk.Tk() win = constants_page(program) win.pack() program.mainloop() if __name__ == "__main__": Show_Page()
How to use classes to display different screens in tkinter
In my tkinter project I have 2 classes namely input and search in my code. Both these classes are working well individually and contain a bunch of sub-pages under them through which I'm able to navigate. However I'm not able to switch between the 2 classes. As my project is rather large I have provided my approach as a general code below. InputOrSearch = False class Input: # class 1 [...] class Search: # class 2 def __init__(self, screen): self.screen = screen def CheckPage(self, page, optmenu=None): if page == 1: self.Clear() self.search_menu() def Clear(self): for widget in self.screen.winfo_children(): widget.destroy() [...] inputscreen = Input(gui) searchscreen = Search(gui) def inputorsearch(): if not InputOrSearch: inputscreen.CheckPage(1) else: searchscreen.CheckPage(1) while True: inputorsearch() gui.mainloop() This is the approach I have used and although this leads correctly to Input it doesn't seem to be working for Search for some reason.
The easest solution is to make each of your classes a subclass of Frame. You can then easily switch between them by destroying one and creating an instance of the other, or creating them all at startup and then hiding one and showing the other. import tkinter as tk class Input(tk.Frame): def __init__(self, parent): super().__init__(parent) label = tk.Label(self, text="I am Input.") label.pack(side="top", fill="both", expand=True) class Search(tk.Frame): def __init__(self, parent): super().__init__(parent) label = tk.Label(self, text="I am Search.") label.pack(side="top", fill="both", expand=True) def inputorsearch(): if not InputOrSearch: searchscreen.pack_forget() inputscreen.pack(fill="both", expand=True) else: inputscreen.pack_forget() searchscreen.pack(fill="both", expand=True) gui = tk.Tk() inputscreen = Input(gui) searchscreen = Search(gui) InputOrSearch = True inputorsearch() gui.mainloop()
How to select all instances of a widget in tkinter?
So does anyone know of a way to 'get' all Labels, for example from a program or window in Tk. i.e like root.winfo.children but only for a type of widget. Also I know you can use lists, but i want to know if there is a better way?
You could use the universal winfo_toplevel() method to get the top-level window containing any widget and a list comprehension to filter the class of the items that winfo_children() returns so it only contains widgets of the desired type. Here's an example of doing that: from pprint import pprint import tkinter as tk class Application(tk.Frame): def __init__(self, master=None): tk.Frame.__init__(self, master) self.grid() self.createWidgets() def createWidgets(self): self.quitButton = tk.Button(self, text='Test', command=self.find_buttons) self.quitButton.grid() nested_frame = tk.Frame(self) # Nest some widgets an extra level for testing. self.quitButton = tk.Button(nested_frame, text='Quit', command=self.quit) self.quitButton.grid() nested_frame.grid() def find_buttons(self): WIDGET_CLASSNAME = 'Button' toplevel = self.winfo_toplevel() # Get top-level window containing self. # Use a list comprehension to filter result. selection = [child for child in get_all_children(toplevel) if child.winfo_class() == WIDGET_CLASSNAME] pprint(selection) def get_all_children(widget): """ Return a list of all the children, if any, of a given widget. """ result = [] # Initialize. return _all_children(widget.winfo_children(), result) def _all_children(children, result): """ Recursively append all children of a list of widgets to result. """ for child in children: result.append(child) subchildren = child.winfo_children() if subchildren: _all_children(subchildren, result) return result app = Application() app.master.title('Sample application') app.mainloop()
How do we change the title of this frame?
I was playing around with some Tkinter code that I found online: from Tkinter import * class ScrolledList(Frame): def __init__(self, options, parent=None): Frame.__init__(self, parent) self.pack(expand=YES, fill=BOTH) self.makeWidgets(options) def handleList(self, event): index = self.listbox.curselection() label = self.listbox.get(index) self.runCommand(label) def makeWidgets(self, options): sbar = Scrollbar(self) list = Listbox(self, relief=SUNKEN) sbar.config(command=list.yview) list.config(yscrollcommand=sbar.set) sbar.pack(side=RIGHT, fill=Y) list.pack(side=LEFT, expand=YES, fill=BOTH) pos = 0 for label in options: list.insert(pos, label) pos = pos + 1 list.bind('<Double-1>', self.handleList) self.listbox = list def runCommand(self, selection): print 'You selected:', selection if __name__ == '__main__': options = map((lambda x: 'Lumberjack-' + str(x)), range(20)) ScrolledList(options).mainloop() My question is: where is the frame created? I don't see anything like: F1 = Tkinter.Frame() Say if I wanted to add a label it would be: label = Tkinter.Label(F1) I'm looking into being able to add labels, and destroy the whole window when done (most likely add frame.destroy() line after print selection but I don't know what to address in that code).
Frames don't have titles; this works because Tkinter automagically creates a Tk instance the first time any widget is created and Tkinter detects that the root window hasn't been created yet (HT #BryanOakley). If you want to alter the window title, explicitly create a Tk instance and provide it as the parent to the ScrolledList: if __name__ == '__main__': options = map((lambda x: 'Lumberjack-' + str(x)), range(20)) app = Tk() app.title('Demo') ScrolledList(options, parent=app) app.mainloop() In many ways this is better, as it's easier to understand what's going on. my question is were [sic] is the frame created? A ScrolledList is a Frame, that's the whole point of inheritance (class ScrolledList(Frame): means "define a new class ScrolledList that inherits its behaviour from Frame"). So the frame is created by ScrolledList(...).
As #jonrsharpe points out, a ScrolledList is a Frame because the class is derived from it. The base Frame class is initialized in the first line of the ScrolledList.__init__() method: class ScrolledList(Frame): def __init__(self, options, parent=None): Frame.__init__(self, parent) # <- calls base class constructor ... Also, frames don't have a titles, so the closest way to have one is to add it to the window the frame is inside of. This can be done by explicitly creating the root window so you have a reference to it, use that to set its title, and then pass the window explicitly as the ScrolledList's parent: if __name__ == '__main__': root = Tk() root.title('MyTitle') root.minsize(200, 200) # also added so title is visible options = map((lambda x: 'Lumberjack-' + str(x)), range(20)) ScrolledList(options, root) # <- Passes root window as the parent root.mainloop()