Unable to delete a child from Window

In the following code I try to delete two buttons - but one of them always stays.
The script creates following window:

Pressing ‘First’, ‘Second’ or ‘Third’ toggle button inside the first column will change the label of the button to ‘CLICKED’; clicking one of the toggle buttons on the second column will change the label to ‘PRESSED’:

Pressing ‘Add Infinity’ adds two more toggle buttons (with the label ‘Infinity’) to the end of the first / second column:

So far so good. However pressing ‘Delete Number’ will delete the first two toggle buttons (labeled as ‘First’):

BUT those buttons will not get removed as shown here:

For above screenshot I’ve enlarged the window horizontically. As shown the ‘First’ button (which is pressed) is behind the (unpressed) ‘Second’ button within the first column - and the second ‘First’ button is shown as well.

My environment:

Python 3.10.8 (main, Nov 1 2022, 14:18:21) [GCC 12.2.0] on linux
Type “help”, “copyright”, “credits” or “license” for more information.

import wx
wx.version()
‘4.2.0 gtk3 (phoenix) wxWidgets 3.2.0’

And here the script:



#!/usr/bin/env python

import math
import time
import wx

class Example(wx.Frame):

    def __init__(self, *args, **kw):
        super(Example, self).__init__(*args, **kw)
        
        self.SetTitle("Button Lists")
        
        self.panel = wx.Panel(self)
        hbox = wx.BoxSizer(wx.HORIZONTAL)
        
        self.label_list = ('First','Second','Third')

        self.button_scrollbox = wx.ScrolledWindow(self.panel, style=wx.VSCROLL)
        self.button_scrollbox.SetScrollRate(10, 10)
        self.button_scrollbox.EnableScrolling(True, True)
        
        self.button_grid = wx.GridSizer(2)
        self.first_button_list = []
        self.second_button_list = []

        for ix, param_name in enumerate(self.label_list):
            self.first_button_list.append(wx.ToggleButton(self.button_scrollbox, label=param_name))
            self.second_button_list.append(wx.ToggleButton(self.button_scrollbox, label=param_name))
            self.button_grid.Add(self.first_button_list[ix])
            self.button_grid.Add(self.second_button_list[ix])
            self.first_button_list[ix].Bind(wx.EVT_TOGGLEBUTTON, self.on_button_click)
            self.second_button_list[ix].Bind(wx.EVT_TOGGLEBUTTON, self.on_button_click)
            
        add_button = wx.Button(self.button_scrollbox, label='Add Infinity')
        self.button_grid.Add(add_button)
        add_button.Bind(wx.EVT_BUTTON, self.on_add)
        
        del_button = wx.Button(self.button_scrollbox, label='Delete Number')
        self.button_grid.Add(del_button)
        del_button.Bind(wx.EVT_BUTTON, self.on_del)
        
        self.button_scrollbox.SetSizerAndFit(self.button_grid)
        
        hbox.Add(self.button_scrollbox, 1, wx.ALIGN_TOP | wx.ALL, 0)
        
        self.panel.SetSizerAndFit(hbox)
        self.Layout()
        self.Centre()   


    def on_button_click(self, event):
        but_clicked = event.GetEventObject()
        for ix in range(len(self.label_list)):
            if but_clicked == self.first_button_list[ix]:
                self.first_button_list[ix].SetLabel('CLICKED')
            if not self.first_button_list[ix].GetValue():
                self.first_button_list[ix].SetLabel(self.label_list[ix])
            if but_clicked == self.second_button_list[ix]:
                self.second_button_list[ix].SetLabel('PRESSED')
            if not self.second_button_list[ix].GetValue():
                self.second_button_list[ix].SetLabel(self.label_list[ix])
                
    def on_add(self, event):
        self.label_list = self.label_list + ('Infinity',)
        self.first_button_list.append(wx.ToggleButton(self.button_scrollbox, label='Infinity'))
        self.first_button_list[-1].SetLabel('Infinity')
        self.first_button_list[-1].Bind(wx.EVT_TOGGLEBUTTON, self.on_button_click)
        self.button_grid.Add(self.first_button_list[-1])
        
        self.second_button_list.append(wx.ToggleButton(self.button_scrollbox, label='Infinity'))
        self.second_button_list[-1].SetLabel('Infinity')
        self.second_button_list[-1].Bind(wx.EVT_TOGGLEBUTTON, self.on_button_click)
        self.button_grid.Add(self.second_button_list[-1])
        self.button_scrollbox.SetSizerAndFit(self.button_grid)
        
        self.button_scrollbox.SendSizeEventToParent()

        
    def on_del(self, event):
        print('deleting...')
        print('Children button_scrollbox',len(self.button_scrollbox.GetChildren()))
        self.label_list = self.label_list[1:]
        self.first_button_list.remove(self.first_button_list[0])
        self.second_button_list.remove(self.second_button_list[0])
        # self.button_grid.Hide(0)
        self.button_grid.Remove(0)
        self.button_grid.Remove(0)
        self.button_scrollbox.RemoveChild(self.button_scrollbox.GetChildren()[0])
        self.button_scrollbox.RemoveChild(self.button_scrollbox.GetChildren()[0])
        print('Children button_scrollbox',len(self.button_scrollbox.GetChildren()))

        self.button_scrollbox.SendSizeEventToParent()

def main():
    app = wx.App()
    ex = Example(None)
    ex.Show()
    app.MainLoop()


if __name__ == '__main__':
    main()

I’ve tried to remove the children from the sizer as well as from the window:

        self.button_grid.Remove(0)
        self.button_grid.Remove(0)
        self.button_scrollbox.RemoveChild(self.button_scrollbox.GetChildren()[0])
        self.button_scrollbox.RemoveChild(self.button_scrollbox.GetChildren()[0])

but without succes…

What’s wrong and how I can delete those buttons?

Many thanks in advance for any hints / comments,

Call Destroy or DestroyLater on the item to be destroyed.
Just removing it from it’s parent window does not destroy it.
E.g.

self.button_scrollbox.GetChildren()[0].Destroy()
# or
self.button_scrollbox.GetChildren()[0].DestroyLater()

It should not be necessary to call RemoveChild.

If an event happens on a destroyed element, your application will crash. Therefore DestroyLater usually is the safer method.

sizer.Remove(child) should destroy the child, though.

I did not closely look at your code. If you can’t fix your issue with these infos, please post a minimal sample.

1 Like

Thanks a lot!
Using ‘Destroy’ instead of ‘RemoveChild works!’

Events aren’t delivered to destroyed windows, except maybe timer events. wx.Window.DestroyLater is not in the official documentation, and I do not believe it’s necessary.

If you have an example to prove me wrong, I’d like to see it. Can you show an example where, after Destroy, an event is delivered causing a crash?

for newbies :stuck_out_tongue_closed_eyes:

import wx

class Gui(wx.Frame):

    def __init__(self, parent):
        super().__init__(parent, title='fake')

        self.Bind(wx.EVT_WINDOW_DESTROY, lambda _: self.SendSizeEvent())

        self.Centre()
        self.Show()

app = wx.App()
Gui(None)
app.MainLoop()

have your buttons in a list, loop over it and add them to your sizer
if anything changes use Clear of the sizer (default keeps buttons, otherwise they are destroyed), loop over the button list and add buttons to sizer as desired (ShowItems & Layout must be done afterwards) :wink:

This seems to be related to race conditions and is often platform dependent.
E.g. on wxGlade I’m initially adding a dummy panel which is then destroyed on the first size event. On most platforms that’s OK, but on OpenSUSE with wxPython 4.1.1 this crashes and so I had to add this commit:

This is just the latest commit related to Destroy, but there are other points where I had to add it. Feel free to search…

I hope you report these platform-specific bugs and not just work around them. Although I can understand if you don’t, as you seem to run into a lot of them.

DestroyLater is a workaround, but not a perfect solution: If event handlers run on objects that the application thinks are gone, then user code may do nonsensical things in the event handler.

I read the documentation of DestroyLater such that this behaviour is not a bug.
I was not aware until last year that DestroyLater is actually a feature of wxPython, not of wxWidgets.

Nowadays I could debug wxPython/wxWidgets, at least on Windows, but when I first saw such issues, I could not.

:roll_eyes:

def _Window_DestroyLater(self):
    """
    Schedules the window to be destroyed in the near future.

    This should be used whenever Destroy could happen too soon, such
    as when there may still be events for this window or its children
    waiting in the event queue.
    """
    self.Hide()
    wx.GetApp().ScheduleForDestruction(self)
Window.DestroyLater = _Window_DestroyLater
del _Window_DestroyLater

There is no actual event being sent here. You are calling SendSizeEvent in the middle of destroying the frame, and wxPython catches you in the act and issues a RuntimeError.

If it’s not a bug, then applications would have to maintain their own record of which windows are destroyed, just so that the state can be checked in event handler prior to doing anything.

Consider even this simple program:

import wx
app = wx.App()
fr = wx.Frame(None, -1)
fr.Bind(wx.EVT_SET_FOCUS, lambda event:fr.SetTitle('Got focus'))
fr.Show()
app.MainLoop()

When you close the frame, the wx.Frame is destroyed. If the focus event might be delivered after that, then this program has a bug: It potentially calls SetTitle on a destroyed frame.

Who’s to say that DestroyLater even fixes this general class of problems? Any code that runs between now and the next idle state might potentially post more events with the same problem. It may be that DestroyLater only reduces the frequency of errors, not eliminates them.

sorry, I was probably still pondering over this highly interesting ‘use case’ of destroying a window in the size handler at the first such event: I felt so sorry for the poor b…, it’s a stillbirth (because the first is fired before any of the Show is visible) :cry:

in the event handler one can condition IsBeingDeleted and a DestroyLater being detected :thinking:

IsBeingDeleted raises an exception if the object has already been Destroyed. So if you’re taking that aproach then every event handler will end up looking like this:

    def OnSomeEvent(self, evt):
        try:
            if self.IsBeingDeleted():
                return
        except RuntimeError:
            return
        ...actual event handling...

in case one loses sight of self the wrapper might help out (of course that should not happen on a regular bases) :crazy_face:

import wx

class Gui(wx.Frame):

    def __init__(self, parent):
        super().__init__(parent, title='IsBeingDeleted')
        self.CreateStatusBar()
        wx.Button(self, label='left click me, please')

        swh = True
        def set_sb(_):
            nonlocal swh
            if test.__nonzero__() and not test.IsBeingDeleted():
                if swh:
                    test.GetStatusBar().SetStatusText('hello')
                    swh = False
                else:
                    test.GetStatusBar().SetStatusText('')
                    swh = True
        self.Bind(wx.EVT_BUTTON, set_sb)
        test = Test(self)

        self.Centre()
        self.Show()

class Test(wx.Frame):

    def __init__(self, parent):
        super().__init__(parent, title='please close me')
        self.CreateStatusBar()
        self.Bind(wx.EVT_WINDOW_DESTROY,
            lambda _: parent.GetStatusBar().SetStatusText(
                            f'\n{self.__class__} being destroyed'))
        self.Show()

app = wx.App()
Gui(None)
app.MainLoop()