Destroy dialog with CallAfter crashes on Ubuntu

Hi folks,

I’m running into a strange problem. I have a large program that uses wxpython for the GUI, and I use several threads to keep the GUI snappy. On occasion, I want to inform users about something (a result, error, etc) from one of those threads, so I show the user a dialog. To do so, I use the standard CallAfter formulation from a thread.

For a while I’ve also been using wx.CallAfter to Destroy these dialogs. This works fine on MacOS, Windows, and Debian. However, I’ve just had a user on Ubuntu report that the dialogs don’t work there.

I’ve been able to verify the issue. The code below is a minimal runnable example that creates a dialog from a thread. If you run this on most platforms it runs fine, and you see the dialog. If instead you run it on Ubuntu (I’ve tested 20.04 and 14.04) it gives the error:

(wx_example.py:4879): GLib-GObject-WARNING **: 13:54:01.989: invalid (NULL) pointer instance

(wx_example.py:4879): GLib-GObject-CRITICAL **: 13:54:01.989: g_signal_handlers_disconnect_matched: assertion 'G_TYPE_CHECK_INSTANCE (instance)' failed

(wx_example.py:4879): Gtk-CRITICAL **: 13:54:01.989: gtk_widget_destroy: assertion 'GTK_IS_WIDGET (widget)' failed

(wx_example.py:4879): GLib-GObject-CRITICAL **: 13:54:01.989: g_object_unref: assertion 'G_IS_OBJECT (object)' failed
Traceback (most recent call last):
  File "/home/jesse/miniconda3/envs/wx/lib/python3.8/site-packages/wx/core.py", line 3407, in <lambda>
    lambda event: event.callable(*event.args, **event.kw) )
wx._core.wxAssertionError: C++ assertion ""Assert failure"" failed at /home/conda/feedstock_root/build_artifacts/wxpython_1616628602755/work/ext/wxWidgets/src/gtk/msgdlg.cpp(281) in ShowModal(): unexpected GtkMessageDialog return code

If you remove the Destroy call, it works fine (besides the dialog not being destroyed).

Any thoughts on what’s going on here? Is there a better way to destroy the dialog after showing it? I should say that in my actual application, for reasons I can’t quite figure out, the dialog not only fails to show but trying to do so actually crashes the application with a Seg fault.

More info:
Python: 3.7.10/3.8.8
Wxpython: 4.1.1
OS: Ubuntu 20.04/14.04
Installed by: Python installed via miniconda installer, wxpython installed from conda-forge

Here’s the example code:

import threading

import wx

class TestFrame(wx.Frame):

	def __init__(self, *args, **kwargs):
		super(TestFrame, self).__init__(*args, **kwargs)

		pnl = wx.Panel(self)

		show = wx.Button(pnl, label='Show Dialog from Thread')
		show.Bind(wx.EVT_BUTTON, self._on_show_dialog)

		sizer = wx.BoxSizer()
		sizer.Add(show)
		pnl.SetSizer(sizer)

	def _on_show_dialog(self, evt):
		t = threading.Thread(target=self._show_dialog)
		t.start()
		t.join()

	def _show_dialog(self):
		dlg = wx.MessageDialog(self, 'This is a test.', 'Test',
			style=wx.ICON_INFORMATION|wx.OK|wx.STAY_ON_TOP)
		wx.CallAfter(dlg.ShowModal)
		wx.CallAfter(dlg.Destroy)


if __name__ == '__main__':
	app = wx.App()
	frm = TestFrame(None, title='TestFrame')
	frm.Show()
	app.MainLoop()

that’s what happens working against all recommendations: you have proved them valid ! :rofl:

I’d love to know what the best practice here is. Can you tell me what the right way to do this is?

I thought CallAfter was approved as a threadsafe way to call things, and I thought Dialogs needed to be destroyed. But apparently I’m missing something?

After a bit of experimentation, it still seems a little odd to me.

This doesn’t work:

def _on_show_dialog(self, evt):
    t = threading.Thread(target=self._show_dialog)
    t.start()
    t.join()

def _show_dialog(self):
    dlg = wx.MessageDialog(self, 'This is a test.', 'Test',
        style=wx.ICON_INFORMATION|wx.OK|wx.STAY_ON_TOP)
    wx.CallAfter(dlg.ShowModal)
    wx.CallAfter(dlg.Destroy)

This doesn’t work:

def _on_show_dialog(self, evt):
    t = threading.Thread(target=self._show_dialog)
    t.start()
    t.join()

def _show_dialog(self):
    wx.CallAfter(self._show_inner)

def _show_inner(self):
    dlg = wx.MessageDialog(self, 'This is a test.', 'Test',
        style=wx.ICON_INFORMATION|wx.OK|wx.STAY_ON_TOP)
    wx.CallAfter(dlg.ShowModal)
    wx.CallAfter(dlg.Destroy)

This does work:

def _on_show_dialog(self, evt):
    t = threading.Thread(target=self._show_dialog)
    t.start()
    t.join()

def _show_dialog(self):
    wx.CallAfter(self._show_inner)

def _show_inner(self):
    dlg = wx.MessageDialog(self, 'This is a test.', 'Test',
        style=wx.ICON_INFORMATION|wx.OK|wx.STAY_ON_TOP)
    dlg.ShowModal()
    dlg.Destroy()

All of them work if I don’t include the Destroy.

I’m not sure I understand the difference, but at least I can make it show the dialog.

Edit: fixed some weird copy/paste format issues on the code blocks.

In the non-working cases, you’re instantiating the wx.MessageDialog from a thread. That needs to be done in the main thread, too. So your working example is the right way to do it.

The middle example also instantiates wx.MessageDialog from the main thread, but still fails. So maybe there’s something else going on here as well?

Is there a good overview on thread safety in wxpython somewhere? I was aware that I couldn’t, for example, show windows or update widget contents, etc from a thread. But I hadn’t realized about instantiation. It makes sense, I guess, but I guess I’d just got it in my head that as long as I wasn’t trying to show it on the screen, I could work with it in a thread (e.g. create the dialog in a thread, show it from the main thread).

The general rule of thumb is don’t do anything wx related from a thread.

1 Like

Is it okay to get values of, say, textctrls in a thread? Or is that also contra-indicated? I do that occasionally for convenience, to get parameters set in the GUI for running things in threads. So far it hasn’t caused any issues, but if it might, I can certainly change it and get the values in the main thread and pass them into the other threads.

Try this modification and you will immediately spot your mistake:

    def _show_inner(self):
        dlg = wx.MessageDialog(self, 'This is a test.', 'Test',
                               style=wx.ICON_INFORMATION|wx.OK|wx.STAY_ON_TOP)
        wx.CallAfter(dlg.ShowModal)
        wx.CallAfter(print, "ShowModal called")
        wx.CallAfter(dlg.Destroy)
        wx.CallAfter(print, "Destroy called")

I’ve just uploaded an example for using a thread in a Gui: it doesn’t make it more snappy (because quicker than the MainLoop is pretty impossible), but more flashy :yum:

Just to make sure it’s clear:

  1. Don’t create, or call methods of UI objects from a non-main thread. That may not be strictly necessary in all cases on all platforms, but it’s better to be safe than sorry. Just because some incorrect thing accidentally works on one platform, or with one version of wx, does not guarantee that it will on others.

  2. The order of calls to the callables passed to CallAfter can sometimes happen in an unexpected sequence. If an earlier one yields or goes into a nested event loop then subsequent CallAfters can be called before the earlier one returns. What is happening in this case is that the dlg.Destroy is being called while the dlg.ShowModal is still running.

Thanks to everyone for the clarifications. It’s clear now where I went wrong, and how to fix it.