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.
Related
So I have the main window. When I click a button in the main window, a new widget is created (in a new window):
self.doorButton.clicked.connect(self.open_door)
def open_door(self):
self.doorwin = QtWidgets.QWidget()
self.doorui = doorwinActions(self.doors)
self.doorui.setupUi(self.doorwin)
self.doorwin.show()
The new QWidget or doorwin has only one widget - tableWidget
I use the object self.doors to populate the table. Now since I have a worker thread (QThread) updating the said object (self.doors), I use QTimer to repopulate the table every 1 second.
class doorwinActions(Ui_doorWin):
def __init__(self,doors):
self.doors = doors
# update setupUi
def setupUi(self, Widget):
super().setupUi(Widget)
Widget.move(self.left, self.top) # set location for window
Widget.setWindowTitle(self.title) # change title
self.timer = QTimer()
self.timer.timeout.connect(lambda:self.popTable())
self.timer.start(1000)
def popTable(self):
mutex.lock()
entries = len(self.doors)
self.tableWidget.setRowCount(entries)
for i in range(entries):
self.tableWidget.setItem(i,0,QtWidgets.QTableWidgetItem(str(self.doors[i][0])))
self.tableWidget.setItem(i,1,QtWidgets.QTableWidgetItem(self.doors[i][1].get_id()))
self.tableWidget.setItem(i,2,QtWidgets.QTableWidgetItem(self.doors[i][1].get_name()))
self.tableWidget.setItem(i,3,QtWidgets.QTableWidgetItem(str(self.doors[i][2])))
mutex.unlock()
When I run the program, it runs smoothly. It opens a new window. If the self.doors object is updated while the window is open, the GUI reflects the change.
BUT, the problem occurs if I reopen the window. If I close the window and then click on the button again, the program crashes with the error:
RuntimeError: wrapped C/C++ object of type QTableWidget has been deleted
From what I understand about my code, when I close the window, the whole widget window (and the table) is deleted. And when I click on the doorButton, new Qwidget/table are created. So, the question is, why would it delete something it just created?
What (sort of) works? - If I move the setup of the door window to the main window's setup, it works. So the open_door function would just be:
def open_door(self):
self.doorwin.show()
The rest would be in the main window setup. But the problem is, then even when I close the window, the QTimer is still going in the background, just eating up processing power.
So, either,
How do I stop the event when the window is closed OR
How do I stop the tableWidget from being deleted?
Your main problem is garbage collection.
When you do this:
def open_door(self):
self.doorwin = QtWidgets.QWidget()
self.doorui = doorwinActions(self.doors)
self.doorui.setupUi(self.doorwin)
self.doorwin.show()
You are creating a new QWidget. It has absolutely no other reference but self.doorwin. This means that if you call open_door again, you will be overwriting self.doorwin, and since the previous widget was not referenced anywhere else, it will get deleted along with all its contents.
Now, QTimers are tricky. You created a QTimer in doorwinActions and QTimers can be persistent even if they have no parent: they keep going on until they're stopped or deleted, and they can only be deleted explicitly or when their parent is deleted (with the exception of timers created with the static QTimer.singleShot() function).
Finally, you must remember that PyQt (like PySide) is a binding. It creates "connections" with the objects created in Qt (let's call them "C++ objects"), and through those bindings we can access those objects, their functions and so on, through python references.
But, and this is of foremost importance, both objects can have a different lifespan:
the python reference can be deleted, and the Qt object can still exist;
the Qt object can be destroyed, yet we still have the python object that referenced (past tense) it;
This is exactly what happens in your case: the self.doorui object is overwritten, but it has an object (the QTimer, self.timer) that is still alive, so the Python garbage collector will not delete it, and the timer is still able to call popTable. But, at that point, the widget (self.doorwin) and its contents have been destroyed on the "C++ side", which causes your crash: while self.tableWidget still exists as a python reference, the actual widget has been destroyed along with its parent widget, and calling its functions causes a fatal error as the binding cannot find the actual object.
Now, how to solve that?
There are various options, and it depends on what you need to do with that window. But, before that, there is something much more important.
You have been manually editing a file generated by pyuic, but those files are not intended for that. They are to be considered like "resource" files, used only for their purpose (the setupUi method), and never, EVER be manually edited. Doing that is considered bad practice and is conceptually wrong for many reasons - and their header clearly warns about that. To read more about the commonly accepted approaches for those files, read the official guidelines about using Designer.
One of those reasons is exactly related to the garbage collection issue explained above.
Note that subclassing the pyuic form class alone is also discouraged (and pointless if you want to extend the widget behavior); the most common, accepted and suggested practice is to create a class that inherits both from the Qt widget class and the UI class. The following code assumes that you have recreated the file with pyuic and named ui_doorwin.py:
# ...
from ui_doorwin import Ui_DoorWin
# ...
class DoorWin(QtWidgets.QWidget, Ui_DoorWin):
def __init__(self, doors):
super().__init__()
self.doors = doors
self.setupUi(self)
self.timer = QTimer(self) # <-- IMPORTANT! Note the "self" argument
self.timer.timeout.connect(self.popTable)
self.timer.start(1000)
def popTable(self):
# ...
With the above code you can be sure that whenever the widget gets deleted for any reason, the timer will be destroyed along with it, so the function will not be called trying to access objects that don't exist anymore.
If you need to keep using an existing instance of the window, the solution is pretty simple: create a None instance (or class) attribute and check if it already exists before creating a new one:
class SomeParent(QtWidgets.QWidget):
doorwin = None
# ...
def open_door(self):
if not self.doorwin:
self.doorwin = DoorWin()
self.doorwin.show()
The above code will not stop the table from updating, which is something you might not want, so you might choose to start and stop the timer depending on when the window is actually shown:
class DoorWin(QtWidgets.QWidget, Ui_DoorWin):
def __init__(self, doors):
super().__init__()
self.doors = doors
self.setupUi(self)
self.timer = QTimer(self)
self.timer.timeout.connect(self.popTable)
def showEvent(self, event):
if not event.spontaneous():
self.timer.start()
def hideEvent(self, event):
if not event.spontaneous():
self.timer.stop()
The event.spontaneous() check above is to prevent stopping the timer if the show/hide event is caused by system calls, like minimizing the window or changing desktop. It's up to you to decide if you want to keep the timer going on and process all data, even if the window is not shown.
Then, if you want to completely destroy the window when it's closed and when a new one is opened, do the following:
class DoorWin(QtWidgets.QWidget, Ui_DoorWin):
def __init__(self, doors):
# ... (as above)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
and then ensure that the widget exists (note that if it's closed by the user the reference still exists):
class SomeParent(QtWidgets.QWidget):
doorwin = None
# ...
def open_door(self):
if self.doorwin:
try:
self.doorwin.close()
except RuntimeError:
pass
self.doorwin = DoorWin()
self.doorwin.show()
I found the solution. My two proposed solutions are the same. If I stop the QTimer on window closing, it no longer gives me the error.
self.exitButton.clicked.connect(self.close_win)
def close_win(self):
self.timer.stop()
self.Widget.close()
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.
So I am working on a Tkinter application, the structure is somehow complex and there are often some cyclic references between child frames and parent frames or different objects.
Python 2.7 and versions before 3.4 do not collect objects that are part of a reference cycle when one of them has a __del__ method, after python 3.4 the interpreter tries harder but there are still some cases where it does not work (see this example)
At some point Tkinter Variables are used (StringVar and IntVar only).
These classes have a __del__ method, therefore when they are part of a cycle of references none of the objects in the cycle are collected by the garbage collector.
Here is a minimal reproductible example with pyobjgraph to show the presence of the objects in memory (you will need Tkinter, pyobjgraph and dot installed to run this).
try :
import Tkinter as tk
except :
import tkinter as tk
class ParentWindow(tk.Frame):
def __init__(self, root):
self.intvarframes = []
self.root = root
self.spawn = tk.Button(root, text="spawn", command=lambda :self.intvarframes.append(FrameWithIntVar(self)))
self.remove = tk.Button(root, text="remove", command=lambda :self.intvarframes.pop().remove())
self.spawn.pack()
self.remove.pack()
def tryme(self, child):
print "child"+str(child)
class FrameWithIntVar:
def __init__(self, parent):
self.parent = parent
self.frame = tk.Frame(self.parent.root)
self.entry = tk.IntVar(self.frame)
self.entry.trace("w", lambda e : self.parent.tryme(self))
self.frame.pack()
self.bigobj = MyVeryBigObject()
c = tk.Checkbutton(self.frame, text="cb", variable=self.entry)
c.pack()
def remove(self):
self.frame.destroy()
#del self.entry
class MyVeryBigObject:
def __init__(self):
self.values = list(range(10**4))
root = tk.Tk()
ParentWindow(root)
root.mainloop()
import objgraph
if objgraph.by_type("MyVeryBigObject"):
objgraph.show_backrefs(objgraph.by_type("MyVeryBigObject"), max_depth=10, filename="test.dot")
from subprocess import check_call
check_call(['dot', '-Tpng', 'test.dot', '-o', 'test.png'])
else :
print ("No MyVeryBigObject in memory")
To demonstrate, just launch the application, spawn a few checkboxes, destroy them and close the application, then open the test.png image.
As you can see there are as many instances of MyVeryBigObject as you created checkboxes.
Here the cyclic reference happen because the lambda self.parent.tryme(self) captures self (twice).
When I uncomment the del self.entry in the remove method. The objects are freed correctly.
Note that this is a simple example and in the actual application I would have to manually propagate the destruction of a frame to all of its children to make sure I destroy all the variables. While it could work, it would mean more code, more maintenance and possibly common errors that comes with manual memory management.
So the question is : Is there a simpler way to do this ?
Maybe there is a way to be noticed by tkinter when a frame is destroyed or a way to use Tkinter variables without a __del__ method but I didn't find anything yet.
Thank you in advance
Python 2 or Python 3 < 3.4
There is a Destroy event in Tkinter that is called when a children frame is destroyed so I just have to add this to every frame that contains a Tkinter Variable
self.frame.bind("<Destroy>", self.remove)
With a remove method that deletes the entry and all objects that have references to it in my current object.
Python >= 3.4
So after PEP 442 it seems that the interpreter no longer has difficulties dealing with this particular case but I have not tried it so if someone could confirm that no objects are leaked when running my example in Python >3.4 that would be great.
Note that while the interpreter tries harder to free the objects there are still some cases where it does not work (see this example)
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()
I am trying to learn python, Tkinter and oop. Below is the code that I wrote after following tutorial on effbot.org
from Tkinter import Tk, Frame, Label
class Nexus(object):
"""Top level object which represents entire app"""
def __init__(self, main_window):
self.nexus_frame = Frame(main_window)
self.nexus_frame.pack()
self.label = Label(main_window, text="Tkinter")
self.label.pack()
def main():
main_window = Tk()
nexus_app = Nexus(main_window)
main_window.wm_title("Hello World Window")
width = main_window.winfo_screenwidth()
height = main_window.winfo_screenheight()
main_window.wm_minsize(width=width-100, height=height-100)
main_window.mainloop()
if __name__ == "__main__":
main()
Here a top level window is created first and it is passed as argument to Nexus class where I am adding a frame and a label to the frame. Then I am setting the size of top level window relative to current screen size back in the main function.
My question is why was the top level window create in main function?
Could it not be created inside __init__ of Nexus class itself?
What difference would it make if main_window was create inside __init__ of Nexus class and mainloop() was started therein?
Once Tk.mainloop is entered, no further code will be executed. Instead, the Tk event loop will take over (hence the name).
What that means is that if you, eg, did something like this:
def main():
...
main_window.mainloop()
print 'Hello world!'
then that print statement would never be executed (or, at least, not while the GUI is running).
So, with that in mind, why is there a problem with creating the root window and executing main loop within the constructor (the __init__ statement)? Well, two basic reasons:
It would mean that the constructor never returns, which is unexpected. If a programmer sees this:
def main():
Nexus()
print 'Hello world!'
then he or she will expect that print statement to be executed. As a rule, you don't expect creating an instance of a class to be the kind of thing which will cause an infinite loop (as the event loop is).
Related to that is the second reason: it would not be possible to create more than one instance of Nexus, because as soon as you create one, Tk.mainloop will take over. Again, that's unexpected: a class is a description of a type of object, and you would normally expect to be able to instantiate more than one object like that.
At the moment, if you write:
def main():
...
Nexus(main_window)
Nexus(main_window)
then you'll get two copies of your Nexus window on the screen. That's expected, and sensible. The alternative would not be.
So what's the take-away message?
When you're dealing with GUI programs, entering the event loop is the last thing you want to do. Your setup might involve creating one object (as now), or it might involve creating many objects (eg, a complex GUI app might have two or three windows).
Because we want to be able to write similar code in both cases, the usual approach is to create the root window (the Tk object) once, and then pass it in as a reference to any classes that need to know about it.