How to change the attributes of a Tkinter Frame subclass? - python

Context
I am developing an app with Tkinter in Python 3.8 for some school students, that gets them to input some working out to a maths problem, then checks whether they are correct.
Approach
Each widget (or groups of widgets) are organised onto a ttk.Frame subclass, which is then instantiated in a MainWindow class that organises them using the .grid() geometry manager. For example:
class Questions(ttk.Frame):
def __init__(self, master):
super().__init__(master)
question_style = ttk.Style(self)
question_style.configure("q.TLabel", padding=30)
question = ttk.Label(self, text=f"{QA[q_number][0]}",
justify=tk.CENTER, style="q.TLabel")
question.pack(fill=tk.BOTH)
The above Questions class is responsible for displaying the questions. I have taken the same approach for pretty much every widget - navigation buttons, entry fields, etc. I took this approach because I thought it would afford me the greatest design flexibilities, as not only could I change the appearance of each widget, but also the frame on which they were placed (from my experience, school students can be very aesthetically picky).
Problem
I cannot figure out how to change the attributes of the Frame on which the widgets are placed. For instance, how would I change the height and width of the frame? I have tried the following, to no avail:
class Questions(ttk.Frame):
def __init__(self, master):
super().__init__(master)
self.height = 50
self.width = 50
question_style = ttk.Style(self)
question_style.configure("q.TLabel", padding=30)
question = ttk.Label(self, text=f"{QA[q_number][0]}",
justify=tk.CENTER, style="q.TLabel")
question.pack(fill=tk.BOTH)
I have also tried self.configure(height=50) but that didn't work either.
How can I change the frame on which these widgets are placed?
Is there a better way to tackle this? Should I abandon the Frame subclass approach?
Any help is greatly appreciated. Thanks.
p.s. I wasn't quite sure how to word this question so any advice on that is also appreciated.

I do not know Tkinter much, but have you tried by passing the parameters to the constructor of the superclass?
super().__init__(self, width=50, height=50)

Related

tkinter how to change attribute value of object using OOP?

How can I change an attribute on the object when the attribute is Frame? I want to change the color of the frame.
class MyFrame:
def __init__(self, bg_color)
self.window = tk.Frame(self.parent, bg=bg_color)
mainApp:
frame_obj = MyFrame("blue")
#want to change the color after the frame obj has been created
frame_obj.window.bg = "red" #this does not work
Tkinter is based on different progamming language, named tcl and thats why things seem a bit unpythonic here.
The tk.Frame object isnt a pure python object, rather its an wrapped object in a tcl interpreter and thats why you cant address the attributes you intuitivly think you can. You need to adress the attributes in a way the tcl interpreter is abel to handle and therefor methods are created like widget.configure.
To achive what you want with your current code, it would look like:
import tkinter as tk
root = tk.Tk()
class MyFrame():
def __init__(self, bg_color):
self.window = tk.Frame(width=500,height=500,bg=bg_color)
frame_obj = MyFrame('blue')
frame_obj.window.pack(fill=tk.BOTH)
frame_obj.window.configure(bg='yellow')
root.mainloop()
In addition, the proper way for an OOP approach for a frame would look like this:
class MyFrame(tk.Frame):
def __init__(self,master,**kwargs):
super().__init__(master)
self.configure(**kwargs)
frame_obj = MyFrame(root,width=500,height=500,bg='green')
frame_obj.pack(fill=tk.BOTH)
This way your class becomes a child of the tk.Frame object and you can adress it directly. The syntax self in this context refers directly to the tk.Frame object. Also it is good practice to use the format of
def __init__(self,master,**kwargs):
while its the same for the parent/tk.Frame. It has a single positional argument, named master and keywords arguments for the configuration of the Frame.
Please take a look at a tutorial for tkinter and for OOP. If you had you would know that. Please dont feel offended, but StackOverflow requiers a brief reasearch and that includes to read documentation and take tutorials.

tkinter: How to access a StringVar and objects from a different class?

I'm having a simple tkinter two frame application with a Label, Entry and Button widget and I want to access a StringVar() of FrameOne with a Entry and Button of FrameTwo.
If have seen a lots of examples of code, but do not get how this is been done in my example below. Many programmers are using a controller. If I would use a controller, I end up from an error to another.
For example:
FirstFrame = FrameOne(mainWindow)`
TypeError: __init__() missing 1 required positional argument: 'controller'
Which I completely understand, because I do not pass anything into the new 'controller' class argument when calling the Frame class. But I do not know what I should pass into this to solve it. Perhaps it is also caused by the lack of knowledge of using class variables (any literature tips are welcome).
The same counts for the solution to inherit FrameOne into FrameTwo. I bump into the same amount of errors applying to my code.
Another thing is that many programmers have examples of two frames that are not visible at the same time, while in my example I have two frames underneath each other at the same time.
An different related issue that I have is, what if the label widget of FrameOne was a Text widget? How do I access the widget from FrameTwo.
I could make it work with globals, but I do not want to use such writing and I will keep the access widget problem anyhow.
Please find my code below:
import tkinter as tk
class AppWindow():
def __init__(self, master):
self.master = master
master.title("Test Application")
master.geometry("1060x680")
master.grid_propagate(False)
class FrameOne(tk.Frame):
def __init__(self, parent):
super().__init__()
self["borderwidth"]=5
self["relief"]="ridge"
self.LabelText = tk.StringVar()
self.LabelText.set("It is not working yet")
self.testlabel = tk.Label(self, textvariable=self.LabelText)
self.testlabel.grid(row=1, column=1)
class FrameTwo(tk.Frame):
def __init__(self, parent):
super().__init__()
self["borderwidth"]=5
self["relief"]="ridge"
self.testentry = tk.Entry(self)
self.testentry.insert("end", "This should be working")
self.testentry.grid(row=1,column=1)
self.testbutton = tk.Button(self, text="Test the label", command=self.updatelabel)
self.testbutton.grid(row=1,column=2)
def updatelabel(self):
FrameOne.LabelText.set(self.testentry.get()) #HOW TO FIX THIS CODE THE RIGHT WAY?
#Create a window as defined in the AppWindow class
mainWindow = AppWindow(tk.Tk())
#Create a Frame as defined in class FrameOne
FirstFrame = FrameOne(mainWindow)
FirstFrame.grid(row=0, column=0) #Positioning Frame on Window
#Create a Frame as defined in class FrameOne
SecondFrame = FrameTwo(mainWindow)
SecondFrame.grid(row=1, column=0) #Positioning Frame on Window
Like with any python object, you access an attribute of an object using a reference to the object.
In your case, updatelabel should look like this:
def updatelabel(self):
FirstFrame.LabelText.set(self.testentry.get())
Note: your use of uppercase characters for instance variables makes your code much harder to comprehend. I recommend following the naming guidelines in PEP8.

Attempting to nest several frames within a single frame with Tkinter. How do I accomplish this in an object oriented fashion?

My code basically does this:
Which is clearly not what I want to try. For further clarification I would like my window to look similar to this:
from tkinter import *
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
class Encoding(tk.Tk):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.mode = StringVar()
## If I remove the next line it breaks it entirely.
self.encoding_frame = ttk.Frame(parent)
self.encrypt = ttk.Radiobutton(self.encoding_frame, text='Encrypt', variable=self.mode, value='encrypt')
self.decrypt = ttk.Radiobutton(self.encoding_frame, text='Decrypt', variable=self.mode, value='decrypt')
self.encrypt.grid(column=0, row=0, ipadx=2, sticky=W)
self.decrypt.grid(column=0, row=1, ipadx=2, sticky=W)
self.encoding_frame.grid(column=0, columnspan=3, row=2, sticky=S)
class MainApplication(tk.Frame, Encoding):
# Create a main frame here.
# Would like frames to be nested within this frame. This seems redundant but nesting with a main
# frame allows for consistent themes, and gives additional control of layout between subframes.
# Inheritance is confusing.
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.main_frame = ttk.LabelFrame(parent, text="Main Window", width=500, height=500)
self.main_frame['borderwidth'] = 3
self.main_frame['relief'] = 'raised'
self.main_frame.grid(column=0, row=0)
self.encoding = Encoding(self)
## I wrote the following line hoping that I could have main_frame become the parent frame.
self.encoding.encoding_frame = ttk.LabelFrame(self.main_frame)
if __name__ == "__main__":
app = MainApplication(root)
root.mainloop()
I am clearly not getting something right. The whole reason I rewrote the program is so that I could gain a greater understanding/confidence with object oriented code. I am hoping that I can get better insight with this, so any help would be amazing.
There are are several problems going on with your code.
Perhaps the biggest problem is that Encoding inherits from tk.Tk, MainApplication inherits from tk.Frame and Encoding (making it both a root window and a frame), and then MainApplication creates an instance of Encoding. Plus, you explicitly create another instance of tk.Tk(), giving you two root windows. That all needs to be untangled.
Inheritance create a "is a" relationship. By having MainApplication inherit from Encoding you are saying that MainApplication is a Encoding object. That is not the case in your code - an Encoding object represents only a small part of the application. For that you want composition, not inheritance, ie: MainApplication has a Encoding object.
So, the first step is to remove Encoding from the list of classes that MainApplication inherits from.
Another thing that can probably be removed is self.encoding_frame. I see no reason to have it since MainApplication itself is a frame. Instead, have MainApplication inherit from ttk.LabelFrame rather than tk.Frame.
The final thing is that since MainApplication creates Encoding, it should be responsible for calling grid or pack on the instance of Encoding.
Altogether, MainApplication can be pared down to this:
class MainApplication(ttk.LabelFrame):
def __init__(self, parent, *args, **kwargs):
ttk.LabelFrame.__init__(self, parent, *args, **kwargs)
self.configure(text="Main Window")
self['borderwidth'] = 3
self['relief'] = 'raised'
self.encoding = Encoding(self)
self.encoding.grid(row=0, column=0, sticky="ew")
That's not 100% complete, but it's a good place to start. Based on your image I'm guessing you'll have other classes for other parts of the main application -- the message widget, the key widgets, and the transcription window.
For Encoding, much of the same advice applies. Since it's only part of the application, it shouldn't inherit from tk.Tk. Instead, you can inherit from ttk.Frame and then remove self.encoding_frame since the Encoding object itself is already a frame.
With those changes, Encoding should look something like the following. Notice how the radiobuttons have self as their parent. If you're creating proper objects, all widgets inside the class need to be a child of the class itself or one of its descendants. A class like this should never put anything in parent except itself.
class Encoding(ttk.Frame):
def __init__(self, parent, *args, **kwargs):
ttk.Frame.__init__(self, parent, *args, **kwargs)
self.mode = StringVar()
self.encrypt = ttk.Radiobutton(self, text='Encrypt', variable=self.mode, value='encrypt')
self.decrypt = ttk.Radiobutton(self, text='Decrypt', variable=self.mode, value='decrypt')
self.encrypt.grid(column=0, row=0, ipadx=2, sticky=W)
self.decrypt.grid(column=0, row=1, ipadx=2, sticky=W)
Finally, since MainApplication is now a frame -- instead of inheriting from Encoding which inherits from tk.Tk -- the block of code that creates an instance of MainApplication needs to be responsible for calling pack or grid. Since MainApplication is the only widget directly inside of the root window, pack is the best choice since you don't have to remember to configure row and column weights to get proper behavior when the window is resized.
Also, I recommend creating root in the same block rather than at the very start of the program.
Your bottom block of code should look like this:
if __name__ == "__main__":
root = tk.Tk()
app = MainApplication(root)
app.pack(fill="both", expand=True)
root.mainloop()

In Python, how to make "class" (when passed as a parameter) work the same as "class.attribute"?

So, I am currently making a custom Tkinter module/API mainly just so I can get a better understanding of Tkinter, but I've run into a problem. Suppose I have the following code:
import Tkinter as tk
Class Window(object): # Equivalent of Tkinters Tk() in my module
def __init__(self):
self.root = tk.Tk()
...
Class Label(object): # Equivalent of Tkinters label widget in my module
def __init__(self, master):
self.label = tk.Label(master, text="Hello world!")
...
How can I make it so when I'm creating a label widget, I may do
master = Window()
label = Label(master)
Instead of having to do
master = Window()
label = Label(master.root)
I know I can inherit tk.Tk into my window class, but I'm just curious if there's a different way of doing it. I've done some research and it seems it may have something to do with one of the built-in methods with the leading and trailing double underscores (not sure what they're called), but I don't know what most of those do.
P.S. Sorry for bad title, wasn't sure how to describe it in a short sentence :P
You can write:
self.label = tk.Label(master.root, text="Hello world!")
But as Brian and Paul said, inherit from tk.Tk is the best thing to do.

Tkinter button calling method on an object

I have some objects which are all instances of the same class; each object has (as one of its attributes) a tkinter button, and I want each tkinter button to perform a method on the object to which it belongs. I'm not really sure how to go about doing this. I've tried tinkering with lambda functions, and replacing "self" with "self_" in case tkinter was already passing "self" to the button's command, but none of this worked; I'm new to classes and hadn't come across lambda functions before today so it didn't surprise me. Example code is below - please could someone explain how to make it work in a way which is both simple, concise and pythonic, or if such a solution does not exist then provide a work around? Thanks in advance
import tkinter as tk
from tkinter import ttk
class SpecialButton():
def __init__(self, row):
self.button = ttk.Button(root, text="button", command=self.delete)
self.button.grid(row=row, column=1)
self.label = ttk.Label(root, text="label")
self.label.grid(row=row, column=2)
def delete(self):
self.button.forget()
self.label.forget()
#some other stuff
root = tk.Tk()
for row in range(3):
SpecialButton(row)
root.mainloop()
The only problem with your code is that you need to be calling grid_forget instead of forget.
Also, the code is a bit misleading -- the delete method doesn't actually delete anything, it just removes it from view. The widgets still exist and take up memory. Are you aware of that? If you truly want to delete the widgets, call the destroy method.

Categories

Resources