wxPython
GUI windows from a cmd2
interpreter command even though both the interpreter and wx.App
must be run on the main thread?I am creating a command-line interpreter in Python 3.x using the cmd2
module. One command in my interpreter will allow me to create a "non-blocking" wxPython
GUI window with a countdown timer that runs in a separate thread. It needs to run separately from the main interpreter thread because otherwise it will prevent entering additional commands while the timer runs.
When I try to use the wxTimer
object to allow my GUI to update its progress bar and check the remaining time, I get the following error:
wx._core.wxAssertionError: C++ assertion "wxThread::IsMain()" failed at ..\..\src\common\timerimpl.cpp(60) in wxTimerImpl::Start(): timer can only be started from the main thread
Any attempt to start the timer breaks the code. For example, the following simple code does not work:
import wx
import threading
import cmd2
class TimerFrame(wx.Frame):
def __init__(self, time):
super().__init__(None, title=str(time), size=(300, 300))
self.InitUI()
def InitUI(self):
self.timer = wx.Timer(self, 1)
self.timer.Start(100)
self.Bind(wx.EVT_TIMER, self.OnTimer, id=1)
self.Show()
def OnTimer(self, event):
print("Updating")
class Interpreter(cmd2.Cmd):
def __init__(self):
super().__init__()
self.app = wx.App()
def do_timer(self, _):
threading.Thread(target=self.createTimer, args=(self.app,)).start()
def createTimer(self, app):
TimerFrame("Window Title")
app.MainLoop()
if __name__ == "__main__":
Interpreter().cmdloop()
Note that commenting out the line containing timer.Start(100)
allows windows to be created in separate threads successfully, but they lack the necessary Timer
functionality.
Other things I have tried that do not work:
wx.App
in the new thread instead of passing it (results in the same error but for the MainLoop
rather than Timer
)wx.App
in the main thread and running its MainLoop
there (blocks the command line from accepting further commands)cmdloop
must be run from the main thread)The docs have a section on Integrating cmd2 with event loops:
Many Python concurrency libraries involve or require an event loop which they are in control of such as
asyncio
,gevent
,Twisted
, etc.
While this is specifically talking about networking-focused event loops, it's actually the same issue with GUI event loops like wx's.
The tl;dr is that instead of calling cmdloop()
, blocking the entire main thread until the interpreter exits, you just call preloop()
, and then you repeatedly call onecmd()
(or onecmd_plus_hooks()
or runcmd_plus_hooks()
, as appropriate) from a wx
callback.
In other words, you drive the cmd2
event loop from the wx
one, so the wx
loop can just take over the thread.
wx
also has a way of driving its event loop manually. See wx.EventLoopBase
and related classes. (There's probably some good example code out there similar to the code in the cmd2
docs, but you'd have to search for it.) The idea is pretty much the same: instead of running the wx
loop and blocking the thread forever, you manually create a wx.EventLoop
and wx.EventLoopActivator
, and then you repeatedly call while loop.Pending(): loop.Dispatch()
(and probably app.ProcessIdle()
) from the cmd2
event loop.
In other words, you drive the wx
event loop from the cmd2
one, so the cmd2
loop can just take over the thread.
If neither of those work for you, you can probably use multiprocessing
instead of threading
. That way, both event loops are running in the main thread—but one of them is just running in the main thread of a child process.
That can be a problem for GUI apps, so putting wx
in the child process may not work (or, worse, may work on some platforms/setups but not others, or may work but mysteriously fail every so often…), but cmd2
presumably only needs to see stdin/stdout/tty, so it probably will work.