wxPython + weakref proxy = closing wx.Frame does not yield None - python

I'm currently making a Python application with wxWigets that has two windows. The first one is the main "controller" window, and the second one is intended to be a data display window.
I want to have some mechanism in place for the first window to know wherever the second window was already spawned, and if yes, if it was closed by the user. I though about using Python's weakref.proxy(), since based on my little understanding of the language, it seemed that if an object is closed/destroyed/deallocated/GC'ed, any attempts to call my proxy would return a None value, which could be conveniently checked with Python's is None / is not None operators.
As long as the window is spawned once, the proxy works as intended, and returns None if the window is not yet spawned, or a reference to the object otherwise. But as soon as I close the secondary window, the proxy object won't revert to None as expected, and my application will crash with a ReferenceError: weakly-referenced object no longer exists.
I remember trying to solve this previously and the most functioning solution I found was checking the object's class name against an internal wx class, like:
if windowObject.__class__.__name__ is not "_wxPyDeadObject": #do stuff
This, however, seems like a very hackish solution to me, and I'd like to know if there's any better way out other than the above. Below is some basic code which reproduces this issue of mine.
import wx
import weakref
class SillyWindow(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, parent=None, title="Spawned Window")
self.Show()
class ExWindow(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, parent=None, title="Main Window")
self.panel = wx.Panel(self)
self.button = wx.Button(self.panel, label="Spawn window!")
self.Bind(wx.EVT_BUTTON, self.spawn, self.button)
self.txt = wx.TextCtrl(self.panel, pos=(0,100))
self.wind = None
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.update, self.timer)
self.timer.Start(50)
self.Show()
def spawn(self,event):
if self.wind is None: # Preventing multiple spawning windows
self.wind = weakref.proxy(SillyWindow())
def update(self,event):
if self.wind is not None:
self.txt.SetValue(str(self.wind))
else:
self.txt.SetValue("None")
app = wx.App(False)
frame = ExWindow()
app.MainLoop()

As you've seen, when a wx widget object has been destroyed the Python proxy object's class is swapped with one that changes it to raise an exception when you try to use it. It also has a __nonzero__ method so you can do things like this instead of digging into the guts of the object to find it's class name:
if not windowObject:
# it has been destroyed already
return
Another thing to keep in mind is that top-level windows are not destroyed at the time they are closed or their Destroy method has been called. Instead they are added to a pending delete list which is processed as soon as there are no more pending events. You can test for that case (closed but not yet destroyed) by calling the frame's IsBeingDeleted method. Also, the C++ parts of the UI objects hold their own reference to the Python object too, although that will be decref'd when the C++ object is destroyed. So some or all of these things could be interfering with your weafref approach. Personally I would just use an if statement like the above. Short. Sweet. Simple.
P.S. Some of the details I've mentioned here are specific to wxPython Classic, and are not handled the same in Phoenix. However using an if statement like the above still works.

Related

PyQt5 Controller cannot access children names

for a python program I created a gui with QtDesigner. Inside my program the gui is initiated and calls the .ui-file to implement content.
The gui class object is than given to a Controller, and here it gets tricky:
Instead of one Controller, there are a main controller and some sub-controller for different parts of the gui. The main Controller does one or two general things and than hands over different parts of the gui to different sub controller.
See the following example for better understanding:
import Controller # import the folder with the controller
def __init__(self, window):
self.window = window # self.window is the handed over gui
self.sub_controls()
def sub_controls(self):
Controller.Sub_Controller_1(window = self.window.part_1)
Controller.Sub_Controller_2(window = self.window.part_2)
Controller.Sub_Controller_3(window = self.window.part_3)
...
The sub-Controller is set up like this:
def __init__(self, window):
self.window = window
... # do stuff:
-----------------------------------------
by trying to call a widget in this Sub-Controller (lets say its a label called timmy), i get an error:
self.window.timmy.setVisible(False)
AttributeError: 'QWidget' object has no attribute 'timmy'
but by using the children()-Method, which returns a list of all children in the gui, I may access this label:
self.window.children()[1].setVisible(False)
This works well and hides timmy.
By trying to do this in the main Controller, it works fine as usual:
self.window.timmy.setVisible(False) # works fine here
I also tried to save the sub-controller object like this:
def sub_controls(self):
self.save_part_1 = Controller.Sub_Controller_1(window = self.window.part_1)
but this doesn't work.
Does any one have a suggestion, how I could solve this Problem?
Sure, I couldt access just all widgets with the children()-method-list, but this is laborious because of the great amount of widgets. Same thing applies to reassigning every child.
PS:
Unfortunately I cannot show you the original Code due to company guidelines.
Ok so I figured out, that the tree Structure of in each other nested widgets has nothing to do with the naming of the widgets. For example:
MainWindow
->centralWidget
->otherWidget
->Button
In this you can address "Button" with self.Button, because (how I believe) the name is saved in MainWindow-Level. If you cut out "otherWidget", the QPushButton in it still exists, but cannot addressed by name.
So it's not possible to just hand over a part of your Gui to a Handler, at least if it's build with QtDesigner.
My Solution for me to the original Problem is to hand over the complete Gui to every sub handler:
def __init__(self, window):
self.window = window # self.window is the handed over gui
self.sub_controls()
def sub_controls(self):
Controller.Sub_Controller_1(window = self.window)
Controller.Sub_Controller_2(window = self.window)
# Controller.Sub_Controller_3(window = self.window.part_3) # before

Unreal Pyside6 widgets wont get removed by garbage collector

I am unable to remove PySide6 widgets in Unreal 5 python. Will describe the code first:
I have my ArtToolsUI class inheriting from QMainWindow. In this class I set some basic stuff like groupboxes, layouts etc.:
class ArtToolsUI(QMainWindow):
def __init__(self):
super().__init__()
self.setAttribute(Qt.WA_DeleteOnClose, True)
self._app = self._get_qt_app()
self.resize(600,400)
self.editor_tools_buttons = [] #for showcase this has no widgets in it
self._main_widget = self._set_layouts()
def _get_qt_app(self):
qt_app = QApplication.instance()
qt_app.setQuitOnLastWindowClosed(True)
return qt_app
def get_v_layout(widgets):
widgets = make_list(widgets)
layout = QVBoxLayout()
layout.setObjectName("vert_layout")
for widget in widgets:
layout.addWidget(widget)
return layout
def _set_layouts(self):
default_grpbox_names = {"tools":self.editor_tools_buttons}
all_grp_boxes = []
for key in default_grpbox_names:
layout = get_v_layout(default_grpbox_names[key]) # right now this just returns empty QVBoxLayout
new_grp_box = QGroupBox(title=key)
new_grp_box.setAttribute(Qt.WA_DeleteOnClose, True)
new_grp_box.setLayout(layout)
all_grp_boxes.append(new_grp_box)
main_layout = get_v_layout(all_grp_boxes)
main_widget = QWidget()
main_widget.setObjectName("tat_main_widget")
main_widget.setLayout(main_layout)
self.setCentralWidget(main_widget)
return main_widget
I also implemented function for removing all widgets in this unreal python session. This method is called from different module.
def _remove_widgets():
qt_app = QApplication.instance()
if qt_app != None:
all_qt_widgets = qt_app.allWidgets()
for widget in all_qt_widgets:
widget.setParent(None)
widget.close()
widget.deleteLater()
This _remove_widgets() method goes through all existing widgets, which should be destroyed, but for some reason only my ArtToolsUI main window is getting destroyed and all other widgets are still hanging around in the memory.
On the other side when I manually click on the X button to close the window it closes everything.
Does anyone know what might be the problem?
Ok I think I figured it out.
When running PySide6 on unreal you actualy cannot start your own QT event loop ( if you do, Unreal Editor will freeze and wait till you end your window). QT somehow finds unreal GUI event loop, which is responsible for everything .. except answering deleteLater().
Only two ways ( I found) for deleting UI are:
When user clicks on X button to end the Window -> This automatically ends and removes all widgets in that window.
When calling QApplication.closeAllWindows() -> This calls all windows, but definitelly better than if they stay in memory.
Both ways seem to bypass deleteLater() functionality, as documentation for deleteLater() says:
The object will be deleted when control returns to the event loop.
At the end I ended up with this functionality:
I am not deleting dialogs, I just hide them on close().
Then when creating them I ask if there are dialogs with my specific object name (object name is class property at every window I have)
This will prevent spawning multiple widgets ( 10k widgets is aprox. 1.2GB RAM)
ADD: There is also possibility to use self.setAttribute(Qt.WA_DeleteOnClose, True) on closing. This will delete dialog from memory as well.

Python object instantiation from within another object - is it a memory leak?

Consider the following small Python GUI program, where the user can click a button over and over to cycle between window 1 and window 2:
import tkinter
class Window1:
def __init__(self, parent):
# Initialize a new GUI window
self.parent = parent
self.window = tkinter.Toplevel(self.parent)
self.window.title('Window 1')
# Make a button to launch Window 2
launch_window_2_button = tkinter.Button(self.window, text='Launch Window 2')
launch_window_2_button.config(command=self.launch_window_2)
launch_window_2_button.pack()
def launch_window_2(self):
self.window.destroy()
Window2(self.parent)
class Window2:
def __init__(self, parent):
# Initialize a new GUI window
self.parent = parent
self.window = tkinter.Toplevel(self.parent)
self.window.title('Window 2')
# Make a button to launch Window 1
launch_window_1_button = tkinter.Button(self.window, text='Launch Window 1')
launch_window_1_button.config(command=self.launch_window_1)
launch_window_1_button.pack()
def launch_window_1(self):
self.window.destroy()
Window1(self.parent)
if __name__ == "__main__":
# Initialize and hide the root GUI
# (each class will spawn a new window that is a child of the root GUI)
root = tkinter.Tk()
root.withdraw()
Window1(root) # Start by spawning Window 1
root.mainloop()
Question 1: Since each new class is being instantiated from within another class, is this a memory leak?
Question 2: Is this the most correct and Pythonic way to code this application?
Question 3: Assuming that the answer to question 1 is no, what if I changed Window1(self.parent) to self.something = Window1(self.parent). Now that there is a reference, is it a memory leak now?
Question 1: Since each new class is being instantiated from within another class, is this a memory leak?
No. And I think you are confusing between a few things memory leak and object references not being used. #kindall explains it beautifully in his comment under your question.
The best way to understand what is going is to know that the tkinter.TopLevel(..) constructor is side-effects based. It will hold the reference to your window within the root object, so that it knows how to deal with various windows. And so is the constructor of your class. Once it creates a self.window and somehow has root have reference to it, its job is done. The reference of this object is held too, although not explicitly (see answer to your Question 3 below).
Question 2: Is this the most correct and Pythonic way to code this application?
The fact that references to the objects Window1 and Window2 objects are not used bugs me too. Other than that, I'd probably store away the references of the window elements(buttons, etc) also within the object -- They might not be of any immediate use, but they might be later on.
Question 3: Assuming that the answer to question 1 is no, what if I
changed the following lines:
Window1(self.parent)
to:
self.something = Window1(self.parent)
Now that there is a reference, is it a memory leak now?
Think about the below line:
launch_window_2_button.config(command=self.launch_window_2)
In order for that line to work, tkinter has to store the reference of self.launch_window2 (and thereby self) somewhere, so your self.something is not doing anything significant at all.. :)

Is deleteLater() necessary in PyQt/PySide?

Since there is already a Garbage Collector in Python, is deleteLater() necessary in PyQt/PySide?
It depends what you mean by "necessary".
An application could potentially consume a lot of memory if (for example) care is not taken when closing widgets. The QObject-based classes are designed to be (optionally) linked together in a hierarchy. When a top-level object is deleted, Qt will automatically delete all its child objects as well. However, when closing widgets (which are sub-classes of QObject), automatic deletion will only happen if the Qt.WA_DeleteOnClose attribute is set (which, by default, it usually isn't).
To illustrate, try repeatedly opening and closing the dialog in this demo script, and watch how the global list of objects grows:
import sys
from PyQt5 import QtCore, QtWidgets
class Window(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.checkbox = QtWidgets.QCheckBox('Delete')
self.button = QtWidgets.QPushButton('Open', self)
self.button.clicked.connect(self.openDialog)
layout = QtWidgets.QHBoxLayout(self)
layout.addWidget(self.checkbox)
layout.addWidget(self.button)
def openDialog(self):
widget = QtWidgets.QDialog(self)
if (self.checkbox.isChecked() and
not widget.testAttribute(QtCore.Qt.WA_DeleteOnClose)):
widget.setAttribute(QtCore.Qt.WA_DeleteOnClose)
for child in self.findChildren(QtWidgets.QDialog):
if child is not widget:
child.deleteLater()
label = QtWidgets.QLabel(widget)
button = QtWidgets.QPushButton('Close', widget)
button.clicked.connect(widget.close)
layout = QtWidgets.QVBoxLayout(widget)
layout.addWidget(label)
layout.addWidget(button)
objects = self.findChildren(QtCore.QObject)
label.setText('Objects = %d' % len(objects))
print(objects)
widget.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 100, 50)
window.show()
sys.exit(app.exec_())
With PyQt/PySide, there are two aspects to object ownership: the Python part, and the Qt part. Often, removing the last Python reference to an object won't be enough to fully clean up, because there could still be a reference held on the Qt side.
In general, Qt tends not to implicity delete objects. So if your application creates and removes lots of QObjects (or opens and closes lots of QWidgets), you may need to take steps to delete them explicitly if memory usage is a concern.
UPDATE:
Just to add to the points above on object ownership. Sometimes, it is possible to hold a Python reference to an object, whilst the Qt part gets deleted. When this happens, you will see an error like this:
RuntimeError: underlying C/C++ object has been deleted
Usually, the Qt documentation will give some hints about when this might happen. For instance, QAbstractItemView.setModel gives this warning:
The view does not take ownership of the model unless it is the model's parent object...
This is telling you that you must either keep a Python reference to the object, or pass a suitable parent object to the object's constructor, because Qt will not always automatically reparent it.
One application of deleteLater can be cleaning up of yourself, i.e. scheduling a delete of an QObject (for example in threading) to free resources from within the object itself.
Here for example someone is using it in connection with the signal thread.finished. It might be restricted to cases with heavy signalling though.

Python - Check for keypresses without using 100% cpu

I'm writing a GUI program in Python using Tkinter and I need a way to check if a keypress is happening without using all my cpu. Currently I'm using the threading module to start a thread that will check for the keypress without freezing the interface (Tkinter). I use win32api.GetKeyState() in a while loop inside my thread so that it constantly checks the status of the key because it needs to be able to tell if the key is being pressed even when the window doesnt have focus. The problem is the program uses 100% cpu the moment I start the thread. If I put a time.sleep() in the loop it cuts back the cpu usage dramatically BUT there is a delay between the actual keypress and the time that it knows that you are pressing a key.
Is there a way to capture a keypress the very moment it gets pressed even when the window is out of focus WITHOUT using so much cpu?
from Tkinter import *
import win32api
class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
coords = StringVar()
Label(master=self, textvariable=coords).pack()
def GetCoords():
coords.set(str(win32api.GetCursorPos()))
root.bind_all("<Scroll_Lock>", self.GetCoords)
root = Tk()
app = Application(master=root)
#root.wm_iconbitmap(default='INSERT ICON HERE')
#root.wm_title("TITLE OF PROGRAM")
#app.master.maxsize(640, 480)
app.master.minsize(640, 480)
app.master.resizable(0, 0)
app.mainloop()
app.quit()
That script give me the following result:
AttributeError: Application instance has no attribute 'GetCoords'
You want to catch key events, instead of polling for the current keyboard state.
See Events and Bindings in the TkInter docs, which has a simple example that does exactly what you want (plus, it's cross-platform instead of Win32-only).
And this is generally an issue with all GUI programming (and network servers, for that matter), and the answer is always the same. You don't directly use non-blocking "check for current X status" calls usefully, with or without threads. In the main thread, you ask the event loop "call my function when X status changes", or you create a background thread and make a blocking "wait forever until X happens" call.
The Wikipedia page on Event loop actually has a pretty good description of this.
Looking at your edited version, you've got a completely new problem now:
class Application(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
coords = StringVar()
Label(master=self, textvariable=coords).pack()
def GetCoords():
coords.set(str(win32api.GetCursorPos()))
root.bind_all("<Scroll_Lock>", self.GetCoords)
GetCoords is a local function defined inside Application.__init__. But you're trying to use it as if it were a method of Application. You can't do self.GetCoords unless GetCoords is a method of self. That's exactly what the error message AttributeError: Application instance has no attribute 'GetCoords' means.
But you can just pass the local function GetCoords, just by taking out the self. prefix. I'm not sure this will do what you think (because I'm not sure whether you can close over a StringVar like that or not), but… try it and see.
Alternatively, you can make GetCoords a method just by moving it out of the def __init__ and giving it a self parameter. Then you can access self.GetCoords, and get a bound method, which you can pass exactly as you're trying to. However, in that case, it won't be able to access coords anymore, since that's a local variable inside __init__. To fix that, change that local variable into a member variable, by using self.coords everywhere in __init__ and GetCoords (and anywhere else) instead of coords.

Categories