I am working on a prototype of GUI testing/demo code and I am struggling to execute actions that will be done on modal windows. I do understand a few things: to communicate with a modal window, the code that does that must be in a separate thread, so it starts concurrently with the modal dialog. Also, wx commands should not be executed directly in that separate thread; they should be placed into a CallAfter() so that they get called by the main event loop.
Clearly from how my code is failing there is more I need to know. Iād really love some help from anyone who better understands how to properly write multi-threaded code that properly interacts with wx.
My test is for code to āmanageā a stock dialog from my application (here isolated in file dlg.py) by clicking on the āSet Allā button and to then close that dialog via the āOKā button. The dialog and the concurrent tread are started in response to clicking on the āOpen modal windowā button in the main frame.
I have tried three approaches to doing this and each is failing in a different way.
- In test1.py, the active part of the code is
threading.Thread(target=doInWindow,
args=('Select atoms for action',['Set All','OK'])).start()
OpenDialog(choices)
print('selected=',dlgResults['selected'])
where doInWindow calls invokeButton(), which uses this to invoke each of the two buttons:
buttonevt = wx.PyCommandEvent(wx.EVT_BUTTON.typeId, btn.GetId())
frame.ProcessEvent(buttonevt)
This test script runs without errors but fails to work properly. The problem in test1.py is that while the āOKā button gets activated by the above, the previous call that is supposed to activate the āSet Allā button does not. FWIW, all buttons in the dialog are owned by the main wx.Dialog frame (no panels, etc.) and Iām pretty sure btn.GetId() is giving me the right Id for the āSet Allā button.
- In test2.py, the only change is that instead of using PyCommandEvent/ProcessEvent to generate an event, I use code like this to use the mouse to invoke the button:
pos = frame.ClientToScreen(btn.GetPosition() + btn.GetSize()/2)
sim = wx.UIActionSimulator()
sim.MouseMove(pos.x,pos.y)
time.sleep(0.1)
sim.MouseClick(wx.MOUSE_BTN_LEFT)
time.sleep(0.1)
This does seem to properly invoke both buttons, but fails at the .ShowModal() with a āwx._core.wxAssertionError: C++ assertion āhandled == 1ā failedā¦ā error characteristic of a wx command being called from a thread ā but as far as I am aware, Iām not doing that. There are effectively two upper level routines in openSelect (which is called by an event handler and thus is part of the event loop). OpenDialog is called directly by openSelect, so that is not in a thread and doInWindow, is called in a thread, but that uses only CallAfter calls:
def doInWindow(winname,bnlList):
time.sleep(0.1) # wait for window to open
wx.CallAfter(getButtons,winname,bnlList)
count = waitForDone(asyncResults)
buttons,frame = asyncResults['buttons'],asyncResults['frame']
for btxt,b in zip(bnlList,buttons):
wx.CallAfter(invokeButton2,frame,b)
count = waitForDone(asyncResults)
print('invoke done',btxt,b,count)
so I donāt see what Iām doing wrong. (waitForDone is plain Python that has some time.sleep() calls, but does not interact with wx.)
- My understanding is that since the routine to be run by my callback (openSelect) will take some time to complete, it really should not be run directly by the event loop, but rather I should invoke that in a thread. So in test3.py I place openSelect into a thread and revised openSelect so that it uses CallAfter for all routines that use wx:
threading.Thread(target=doInWindow,
args=('Select atoms for action',['Set All','OK'])).start()
wx.CallAfter(OpenDialog,choices)
time.sleep(0.1)
while len(dlgResults) == 0: time.sleep(0.1)
This also fails with the same wx._core.wxAssertionError error as before.
FWIW, Iām developing this on a Mac running Python 3.13 and wx 4.2.3
dlg.py (12.2 KB)
test1.py (4.1 KB)
test2.py (4.2 KB)
test3.py (4.6 KB)