Search code examples
pythonwxpythonweak-references

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


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()

Solution

  • 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.