Python: tkinter window presented in an OO manner - python

I have been trying to represent tkinter window in an OO manner. There are few answers here, and I have managed to produce a working code, but I simply do not understand why it's working.
import tkinter as tk
class CurrConv:
def __init__(self, window, date):
self.window = window
window.title("Currency Converter")
window.geometry("350x150+300+300")
self.label = tk.Label(text="Date: {}\n".format(date))
self.label.pack()
self.text_box = tk.Text()
self.text_box.insert("2.0", "100 is: {}\n".format(100))
self.text_box.insert("3.0", "24 is: {}".format(24))
self.text_box.pack()
self.button = tk.Button(text="Quit", width=8, command=window.quit)
self.button.place(relx=0.5, rely=0.9, anchor="center",)
def main():
window = tk.Tk()
app = CurrConv(window, 1234)
window.mainloop()
if __name__ == "__main__":
main()
The thing I don't understand is the usage of "app" object. It is used nowhere, and usually when we create an object (in any programing language), we invoke certain actions on it. However here we do nothing with the app object. Class encapsulation appears to indirectly modify "window", which is confusing, to say the least.
Next, I don't understand how labels and text boxes are added to "window", when in the code I nowhere create those in "window", I create them on "self", which would be "app", which is no longer used.
Bottom line is, for the reasons above, I do not understand how the above code works.
Thanks in advance.
I hope I was clear enough.

Class encapsulation appears to indirectly modify "window", which is confusing, to say the least.
Yes, that is what the code is doing, and it's incorrect IMHO. The code inside of CurrConv.__init__ at least arguably should not be directly modifying the root window for some of the very reasons you put in your question.
Next, I don't understand how labels and text boxes are added to "window", when in the code I nowhere create those in "window", I create them on "self", which would be "app", which is no longer used.
Your terminology is a bit incorrect. "I nowhere create those in 'window'" isn't true. By creating the widgets without an explicit master they default to being created in the root window. The reference to the widget is created in the class (self.label, etc) but the widget itself is created in the root window. IMHO this is also not the proper way to write a tkinter application. Explicit is better than implicit.
For me, the proper way to write this class would be for CurrConv to inherit from a tkinter Frame (or some other widget). Everything it creates should go inside itself, and then the code that creates the instance of CurrConv is responsible for adding it to a window. This solves your problem of not being able to create multiple windows.
Also, the widgets inside of CurrConv should explicitly set the master of any child widgets it creates rather than relying on a default master.
Example:
class CurrConv(tk.Frame):
def __init__(self, window, date):
super().__init__(window)
self.label = tk.Label(self, ...)
self.text_box = tk.Text(self, ...)
self.button = tk.Button(self, ...)
...
...
With that, everything is nicely encapsulated inside the class, and you can create multiple instances inside multiple windows very easily:
root = tk.Tk()
conv1 = CurrConv(root)
conv1.pack(fill="both", expand=True)
top = tk.Toplevel(root)
conv2 = CurrConv(top)
conv2.pack(fill="both", expand=True)
In the above case, it's perfectly fine for the class to modify the window because it's part of the code that creates the window in the first place.
You can still use a class for the application as a whole which you can use to hold some global state. To further prove the usefulness of creating CurrConv as a subclass of Frame, the following example adds currency converters to a notebook instead of separate windows.
class CurrConvApp():
def __init__(self):
self.root = tk.Tk()
self.root.title("Currency Converter")
self.root.geometry("350x150+300+300")
self.notebook = ttk.Notebook(self.root)
cc1 = CurrConv(self.notebook)
cc2 = CurrConv(self.notebook)
self.notebook.add(cc1, text="USD to EUR")
self.notebook.add(cc2, text="EUR to USD")
...
def start(self):
self.root.mainloop()
if __name__ == "__main__":
app = CurrConvApp()
app.start()

For the first question, yes app is an object. But it is an instance of the class CurrConv. When you initialize a class, you call the class's __init__ method, and this case, by doing so, you execute the statements in that method: modifying the window (which you passed as a parameter whin you created app) and adding widgets to it. So although app is not directly used, it had the side effect of doing those things when it was created. And for that reason, since you only need the initialization method, assigning to a variable is not necessary, you can just make it like CurrConv(window, 1234).
For the second, yes, you didn't mention window when you created the widgets, but when the master of a new Tkinter widget is not specified, it takes the main master (the root, created using tk.Tk()) as it's master.

Related

Why are attributes of a tk object being 'retroactively' changed?

Personal project, I'm thinking it would be cool to be able to create a one to has many relationship between windows, so when a "parent" window is closed all of its "children" are also also closed.
So here is the window class that creates new windows via the Tk() function:
from tkinter import *
class Window:
def __init__(self, title):
self.create(title)
def create(self,title):
self.window = Tk()
self.window.title(title)
self.window.protocol("WM_DELETE_WINDOW",self.delete)
def child(self, title):
self.create(title)
def delete(self):
print(f'Destroying: {self.window.title()}')
self.window.destroy()
parentclass1 = Window("ParentClass1")
parentclass2 = Window("ParentClass2")
parentclass3 = Window("ParentClass3")
print(parentclass1.window.title())
print(parentclass2.window.title())
print(parentclass3.window.title())
mainloop()
This works fine. Each window opens, and when its title is queried each instance returns the correct title:
print(parentclass1.window.title()) #=\> "ParentClass1"
print(parentclass2.window.title()) #=\> "ParentClass2"
print(parentclass3.window.title()) #=\> "ParentClass3"
What I want to be able to do is call the child method on the parentclass2 instance and instantly set up a relationship between parentclass2 and the newly created instance. I.e parentclass2 is the parent and the newly created instance is the child of parentclass2.
However before I get even to setting up this relationship via an array, a very weird thing happens when I use the child method:
parentclass2.child("ChildOfParentClass2")
print(parentclass1.window.title()) #=> "ParentClass1"
print(parentclass2.window.title()) #=> "ChildOfParentClass2"
print(parentclass3.window.title()) #=> "ParentClass1"
parentclass2.window.title() now returns the string "ChildOfParentClass2".
This is odd. self.window = Tk() is clearly being called twice, separately, and yet somehow setting the title of "ChildOfParentClass2" is "going up the stack" and is renaming ParentClass2 to ChildOfParentClass2?
I don't think its the .title method that's doing this. I think parentclass2.window is literally being turned into childofparentclass2.window.
I am aware that tkinter is behaving weirdly because I'm trying to force it into my object orientated approach...but it would be cool to use it this way so would appreciate an answer.
Can any one explain this weird behaviour, and maybe how it could be solved and I'll be able to call parentclass2.child("ChildOfParentClass2") and have it work as expected?
I've tried using Toplevel() in child and Tk() in init but exactly the same weird behavior occurs:
def __init__(self, title):
self.window = Tk()
self.create(title)
def create(self,title):
self.window.title(title)
self.window.protocol("WM_DELETE_WINDOW",self.delete)
def child(self, title):
self.window = Toplevel() # thought this would work tbh
self.create(title)
The reason for the odd behavior is that in create you're redefining self.window to be the newly created window. It no longer represents the original window. So, when you print the title of what you think is the main window you actually are printing the title of the child window.
If you want to create a child of a root window, you need to create instances of Toplevel. You can then pass the root window in as the master of the Toplevel to create the parent/child relationship.
def child(self, title):
new_window = Toplevel(master=self.window)
new_window.title(title)
When you do this, child windows will automatically be deleted when the parent dies. You don't have to do anything at all to make that happen, that's the default behavior of tkinter widgets.
Bear in mind that if you create more than one instance of Tk, each is isolated from the other. Images, variables, fonts, and other widgets created in one cannot communicate with or be moved to another. Each gets their own separate internal tcl interpreter.

There is a way to wait on a user's answer in tkinter?

I'm developing an application what have its functions set in different files.
The main file have a tkinter interface and the buttons, entrys and labels are in other file, like this:
Mainfile.py
from tkinter import *
class Program:
def __init__(self, root):
root.geometry('200x200')
self.main_frame = Frame(root)
self.main_frame.pack()
import Buttons
self.branch = Buttons.New_Button(self.main_frame)
#Here i wuold like to verify the hipotetic variable after the main_frame were destroyed
if self.branch.hipotetic_variable:
root.mainloop()
app = Program(Tk())
Buttons.py
from tkinter import *
import functools
class New_Button:
def __init__(self, using_frame):
self.button_1 = Button(using_frame, text = 'Button 1', command=functools.partial(self.Func, using_frame))
self.button_1.pack()
def Func(self, to_destroy):
to_destroy.destroy()
#Here is the hipotetic variable what i would like to verify with if statment
self.hipotetic_variable = True
The problem is that I want to keep managing the program in the main file calling the other functions and implementing it, but I cannot verify if it's time to update the screen because mainloop makes impossible to verify it using a while loop and an hipotetic variable that's created after user pressed button.
I wold like to know if there is an way to update an variable contained in the Buttons.py file on Mainfile.py to keep implementing all other canvas in this file.
Your if self.branch.hipotetic_variable: check in the Program.__init__() method is only going to be executed when the Program class instance gets created initially, which is before the button that could change the value of the variable could have been pressed. You also don't want to make the hipotetic_variable an attribute of the Button because that will be destroyed along with the Frame it is in when that's destroyed in the button callback function.
Tkinter applications are user-event driven, meaning that they're "run" by responding to events (that's what mainloop is all about). This type of programming paradigm is different from the procedural or imperative one you're probably used to.
Therefore to do what you want requires setting things up so an event that the program can respond to will be generated, which in this case to when the frame is destroyed. One way to do that is by taking advantage of tkinter Variable classes to hold this hipotetic variable you're interested in. It looks like a boolean, so I used a tkinter BooleanVar to hold its value. One interesting thing about Variables is that you can have changes to their values "traced" by defining functions to be called whenever that happens. That's what I have done in the code below, and the callback function in this case — check_hipotetic_variable() — updates a Label to display the new value of the variable when it's called.
Below is your code with the modifications necessary to use a tkinter BooleanVar and trace changes to its value.
Mainfile.py
from tkinter import *
import Buttons
class Program:
def __init__(self, root):
root.geometry('200x200')
self.main_frame = Frame(root)
self.main_frame.pack()
self.notice_lbl = Label(root, text='')
self.notice_lbl.pack(side=BOTTOM)
self.hipotetic_variable = BooleanVar(value=False)
# Set up a trace "write" callback for whenever its contents are changed.
self.hipotetic_variable.trace('w', self.check_hipotetic_variable)
self.branch = Buttons.New_Button(self.main_frame, self.hipotetic_variable)
root.mainloop()
def check_hipotetic_variable(self, *args):
"""Display value of the hipotetic variable."""
value = self.hipotetic_variable.get()
self.notice_lbl.config(text=f'hipotetic variable is: {value}')
app = Program(Tk())
Buttons.py
from tkinter import *
import functools
class New_Button:
def __init__(self, using_frame, variable):
self.button_1 = Button(using_frame, text = 'Button 1',
command=functools.partial(self.Func, using_frame))
self.button_1.pack()
self.variable = variable # Save for use in callback.
def Func(self, to_destroy):
to_destroy.destroy()
self.variable.set(True) # # Change value of the variable.
P.S. I noticed you're not following the PEP 8 - Style Guide for Python Code, which makes reading your code harder to read and follow that if you're were following them — for that reason I strongly suggest you read the guide and start following the suggestions, especially the Naming Conventions which apply to functions and variable names, as well as the names of script files.

Object oriented Tkinter, best way to communicate between widgets in gui with many frames

I am trying to figure out what the best way to communicate between different widgets is, where the widgets are custom classes inheriting from tkinter widgets and I have several frames present (to help with layout management). Consider for example the following simple gui (written for python 3, change tkinter to Tkinter for python 2):
from tkinter import Frame,Button,Tk
class GUI(Frame):
def __init__(self, root):
Frame.__init__(self,root)
self.upper_frame=Frame(root)
self.upper_frame.pack()
self.lower_frame=Frame(root)
self.lower_frame.pack()
self.upper_btn1 = Button(self.upper_frame, text="upper button 1")
self.upper_btn2 = Button(self.upper_frame, text="upper button 2")
self.upper_btn1.grid(row=0,column=0)
self.upper_btn2.grid(row=0,column=1)
self.lower_btn = CustomButton(self.lower_frame, "lower button 3")
self.lower_btn.pack()
class CustomButton(Button):
def __init__(self,master,text):
Button.__init__(self,master,text=text)
self.configure(command=self.onClick)
def onClick(self):
print("here I want to change the text of upper button 1")
root = Tk()
my_gui = GUI(root)
root.mainloop()
The reason I put them in different frames is because I want to use different layout managers in the two different frames to create a more complicated layout. However, I want the command in lower_btn to change properties of eg upper_btn1.
However, I can not immediately access upper_btn1 from the instance lower_btn of the customized class CustomButton. The parent of lower_btn is frame2, and the frame2 parent is root (not the GUI instance). If I change the parent of lower_btn to the GUI instance, ie
self.lower_btn = CustomButton(self, "lower button")
it will not be placed correctly when using pack(). If I change the parent of frame1/frame2 to the GUI instance, ie
self.upper_frame=Frame(self)
self.upper_frame.pack()
self.lower_frame=Frame(self)
self.lower_frame.pack()
I could in principle access the upper_btn1 from lower_btn by self.master.master.upper_btn1, BUT then the frame1/frame2 are not placed correctly using pack() (this I don't understand why). I can of course pass the GUI instance as separate variable, on top of master, to CustomButton, ie something like
class CustomButton(Button):
def __init__(self,master,window,text):
Button.__init__(self,master,text=text)
self.window=window
self.configure(command=self.onClick)
def onClick(self):
self.window.upper_btn1.configure(text="new text")
and then change the construction of lower_btn to
self.lower_btn = CustomButton(self.lower_frame,self, "lower button 3")
but is that the "correct" way of doing it?
So, what is the best way to reorganize this gui? Should I pass GUI as a separate variable on top of the master/parent argument? Should I change the master/parent of the buttons and/or the frames (and somehow fix the issues with the layout management)? Or should I make the commands of the buttons as methods of the GUI instance so they can communicate with the widgets in that GUI, although this does not really feel like object oriented programming? I can't seem to find a working object oriented design that feels "correct". Also, an explanation why pack() does not work for frame1/frame2 if I make the GUI instance (self) the master, would also be appreciated, as that would have been my intuitive, most object oriented, approach.
Edit: Maybe the best way is to not use frames inside frames at all, and use only grid() and then use colspan/rowspan to give the layout management more flexibility.
A year late, but: One way to communicate between widgets is to instantiate some kind of control center object that receives knowledge about the state of your widgets and then compels other widgets to act on that information. This 'manager' functionality will be independent of the layout of your widgets.
Here's an implementation that's customized to your example. The idea is to add a .manager attribute to the lower button, and to notify this manager when clicked. The GUI class remains unchanged.
from tkinter import Frame,Button,Tk
class Manager(object):
def __init__(self, gui):
self.gui = gui
self.gui.lower_btn.manager = self
def onClick(self):
self.gui.upper_btn2.configure(text="changed text")
class GUI(Frame):
def __init__(self, root):
Frame.__init__(self,root)
self.upper_frame=Frame(root)
self.upper_frame.pack()
self.lower_frame=Frame(root)
self.lower_frame.pack()
self.upper_btn1 = Button(self.upper_frame, text="upper button 1")
self.upper_btn2 = Button(self.upper_frame, text="upper button 2")
self.upper_btn1.grid(row=0,column=0)
self.upper_btn2.grid(row=0,column=1)
self.lower_btn = CustomButton(self.lower_frame, "lower button 3")
self.lower_btn.pack()
class CustomButton(Button):
def __init__(self,master,text):
Button.__init__(self,master,text=text)
self.configure(command=self.onClick)
self.manager = None
def onClick(self):
if self.manager:
self.manager.onClick()
else:
print("here I want to change the text of upper button 1")
root = Tk()
my_gui = GUI(root)
Manager(my_gui)
root.mainloop()

Tkinter add Widgets (a Button) to a new second Frame

I created a Frame, gave it a menubar. Works just fine. The purpose of the entry in the menubar is to open a new frame, in which u can change some settings. The creation of the new Window works also. However I can't create widgets on the new created window. I tried it with a Button and got a
TclError: can't invoke "button" command: application has been destroyed
I tried to google it and found Cannot invoke button command: application has been destroyed which didn't quite helped me.
Further I found a solution were u have to create a parent class (which inherrits from Frame) and than create all other Frames within it, but on the first view it looked pretty complicated. Especially because the creation of the second window seems to work in the first place.
I know this is probably a really basic question, so thanks in advance for your time
def perfSettings():
perfFrame = Tk(className=" Performanz Einstellungen")
perfFrame.configure(bg='#F2F2F2')
perfFrame.geometry("300x300")
perfFrame.mainloop()
btn = Button(master=perfFrame, text='Speichern', command=myPerfSettingValue.getValues, width=37)
btn.pack()
# Button(perfFrame, text='Abbrechen', command=perfFrame.destroy, width=37).grid(row=0 ,column=1 )
class perfSettingsValue:
def __init__(self):
self.bvhSteps = 0
def getValues(self):
pass
#Hauptfenster
root = Tk(className="BoneMapping & SkeletonEstimation")
root.configure(bg='#F2F2F2')
root.geometry("1300x600")
myPerfSettingValue = perfSettingsValue()
menubar = Menu(root)
sdmenu = Menu(menubar, tearoff=0)
sdmenu.add_command(label="Performanz", command=perfSettings)
menubar.add_cascade(label='Einstellungen',menu=sdmenu)
root.config(menu=menubar)
The key problem here is that you are trying to add a button after starting the mainloop which effectively blocks the execution of the program. The error you are getting is because the line that adds the button gets executed after the window has been closed.
Your problem will be solved if you modify your function like this:
def perfSettings():
perfFrame = Tk(className=" Performanz Einstellungen")
perfFrame.configure(bg='#F2F2F2')
perfFrame.geometry("300x300")
btn = Button(master=perfFrame, text='Speichern', command=myPerfSettingValue.getValues, width=37)
btn.pack()
perfFrame.mainloop()
This is not the only problem though. Instead of creating a new instance of Tk, you should create a new Toplevel instance, which will, in your case, act just as a Tk instance, but have a lot less tendency to cause trouble.
Finally, you should consider reading on the object oriented approach to designing tkinter applications. There are far too many variants of that to be appropriately elaborated here but I certainly recommend you take the effort to learn to use one of them. It will make your code more comprehensible and maintainable. My usual approach is to create a class that inherits from Toplevel or Tk for every type of window I am going to use.

Removing Tkinter Objects (created in a function) in a separate function

I need to be able to clear my tkinter window of all objects (with a function), and create the objects again with a function. However, I cannot access the objects created with my first function with the second function. I recreated my problem below.
import tkinter
window = tkinter.Tk()
def create():
test = tkinter.Button(window, text="Example", command=delete)
test.place(x=75, y=100)
def delete():
test.place_forget()
create()
window.mainloop()
This returns the error - NameError: name 'test' is not defined
Here's a quick sample of how your code might look, using an object oriented structure:
import tkinter as tk
class MyApp: # No need to inherit 'object' in Python 3
def __init__(self, root):
self.root = root
def create_button(self):
self.test_button = tk.Button(self.root,
text="Example",
command=self.delete_button)
self.test_button.place(x=75, y=100)
def delete_button(self):
self.test_button.place_forget()
def run(self):
self.create_button()
self.root.mainloop()
if __name__=='__main__':
root = tk.Tk()
app = MyApp(root)
app.run()
You create a MyApp object that 'owns' the button, and has methods that explicitly act on the things that it owns. Any method of the MyApp object has a reference to various widgets, via the self argument that automatically gets sent in.
This is a lot more code than you had before, and to be honest, for what your code does right now, it's an overkill. Malik's solution of using global is probably fine. However, if you want to add more widgets, layer them out, have them interact in more complex ways etc, then using global can introduce hard-to-find bugs, and makes it incredibly hard to wrap your head around what's going on.
Any non-trivial use of Tkinter that I have seen has used an object-oriented style similar to the above example.
As an aside, I wouldn't create the delete function - using the .config method to set the command after you create the button would be better:
def create_button(self):
self.test_button = tk.Button(self.root, text="Example")
self.test_button.config(command=self.test_button.place_forget)
self.test_button.place(x=75, y=100)
Using .config allows you to set commands that are methods of the button you just created, which you can't do when you set the command as a part of the button instantiation.
Well if you're using two different functions, you're going to need global variables:
import tkinter
window = tkinter.Tk()
test = None
def create():
global test
test = tkinter.Button(window, text="Example", command=delete)
test.place(x=75, y=100)
def delete():
global test
test.destroy() # or place_forget if you want
window.after(5000, create) # button reappears after 5 seconds
create()
window.mainloop()
Your delete function could not destroy the button as it was only defined in the create function. The workaround is to create a global variable that can be accessed by both.

Categories

Resources