Invoking a wxPython EVT_BUTTON event programmatically - python

I've seen another question regarding this topic, but I couldn't quite get the information to work for me, so I thought I'd give my specifics - I suspect I'm just being short-sighted.
I'm trying to exercise my GUI from a test framework, which involves manually invoking an event (in this case a button press) within a test script. So far, in addition to other irrelevant guff, I have:
# In GUI class:
self.button_1 = wx.Button(self, id=wx.ID_ANY, label="Button 1")
self.button_1.Bind(wx.EVT_BUTTON, self.button_1)
# In GUI Test class:
event = wx.PyCommandEvent(X, Y)
wx.PostEvent(get_gui_instance(), event)
My problem is that I don't know what X and Y should be (assuming that the rest is ok). Any help is greatly appreciated.

btnInfo = wx.Button(self,-1,"Some Button")
evt = wx.PyCommandEvent(wx.EVT_BUTTON.typeId,btnInfo.GetId())
wx.PostEvent(self, evt) #attach event to self ... alternatively maybe attach to btnInfo
should work

So it turns out that because I've re-jigged my GUI to run in a worker thread from GUI Test, I can communicate with it directly. I should have realised this earlier, but nonetheless the result is that I don't need to bother with posting events from GUI Test to GUI since they're running in the same process.
Instead, I can now call the effects of events directly. For example, I can call on_button_press(), bypassing the actual clicking of the button, which would normally fire off the event in wxPython. This allows me to simulate user interaction and test workflows and input ranges, which is exactly what I wanted to do.
Of course, this only works because I'm running my GUI in the same process as the test suite. Posting events seems to be the way forward otherwise, and in answer to my own question, it seems custom events are the way to invoke button presses cross-process. This implies the use of some sort of "test agent" within the GUI to handle those events that are specific for testing.

import wx
class MessageDialog(wx.Dialog):
def __init__(self, message, title, tiempo = 2000):
style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
super(MessageDialog, self).__init__(None, -1, title, style=style)
text = wx.StaticText(self, -1, message)
fuente = wx.Font(pointSize = 20,
family = wx.DEFAULT,
style = wx.NORMAL,
weight = wx.LIGHT,
underline=False,
faceName="",
encoding=wx.FONTENCODING_DEFAULT)
text.SetFont(fuente)
self.ok = wx.Button(self, wx.ID_OK, "OK")
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(text,0,wx.ALIGN_CENTER_HORIZONTAL|wx.ALIGN_CENTER_VERTICAL,5)
sizer.Add(self.ok, 0, wx.EXPAND|wx.ALL, 5)
self.SetSizerAndFit(sizer)
color = "WHEAT"
self.SetBackgroundColour(color)
self.Center()
self.Refresh()
wx.FutureCall(tiempo,self.salir_ok)
def salir_ok(self):
par_btn = getattr(self, "ok")
evt = wx.PyCommandEvent(wx.EVT_BUTTON.typeId, par_btn.GetId())
wx.PostEvent(self, evt)
return
if __name__ == '__main__':
app = wx.App()
dialog = MessageDialog( 'Teclee el nombre del proveedor', 'Proveedor')
dialog.ShowModal()
dialog.Destroy()
app.MainLoop()

Related

Trying to create a dialog in another thread wxpython

I'm running a function in another thread that is supposed to fill out a dialog and then show it but it just seg faults as soon as I tried to alter the dialog in any way. I've read that this is a common issue with WxPython and that devs are not intended to directly alter dialogs in another thread.
How do I get around this? I can just call the function in my main thread but that will block my GUI and it is a lengthy operation to initialize the dialog - I would like to avoid this.
My code is similar to the below.
In the main thread
# Create the dialog and initialize it
thread.start_new_thread(self.init_dialog, (arg, arg, arg...))
The function I am calling
def init_dialog(self, arg, arg, arg....):
dialog = MyFrame(self, "Dialog")
# Setup the dialog
# ....
dialog.Show()
Even with a blank dialog and just a simple call to show inside the function I get a segmentation fault. Any help would be greatly appreciated, thanks.
I have made an applet to demonstrate keeping GUI responsive during calculations and calling the message box after the calculations.
import wx
import threading
import time
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, "I am a test frame")
self.clickbtn = wx.Button(self, label="click me!")
self.Bind(wx.EVT_BUTTON, self.onClick)
def onClick(self, event):
self.clickbtn.Destroy()
self.status = wx.TextCtrl(self)
self.status.SetLabel("0")
print "GUI will be responsive during simulated calculations..."
thread = threading.Thread(target=self.runCalculation)
thread.start()
def runCalculation(self):
print "you can type in the GUI box during calculations"
for s in "1", "2", "3", "...":
time.sleep(1)
wx.CallAfter(self.status.AppendText, s)
wx.CallAfter(self.allDone)
def allDone(self):
self.status.SetLabel("all done")
dlg = wx.MessageDialog(self,
"This message shown only after calculation!",
"",
wx.OK)
result = dlg.ShowModal()
dlg.Destroy()
if result == wx.ID_OK:
self.Destroy()
mySandbox = wx.App()
myFrame = TestFrame()
myFrame.Show()
mySandbox.MainLoop()
GUI stuff is kept in the main thread, while calculations continue unhindered. The results of the calculation are available at time of dialog creation, as you required.

Global event handlers in wxPython (Phoenix)

I'm having a problem trying to handle focus events in wxPython 3, the phoenix fork.
I'm trying to clear the value in a TextCtrl when it's focused if it still has its default value, and then restore that default value on leaving focus if the user hasn't entered a different value.
This is the handler I have:
def on_enter(self, event):
control = wx.Window.FindFocus()
if control.Id in TEXT_MAPPING.keys():
if control.Value == TEXT_MAPPING[control.Id]:
control.SetValue('')
exit_control = event.GetWindow()
if exit_control.Id in (TEXT_MAPPING.keys()):
if not exit_control.GetValue():
exit_control.SetValue(TEXT_MAPPING[exit_control.Id])
event.Skip()
The handler itself works fine, though it's a little clunky. My issue is that I would like to bind this on a global level, such that whenever any wx.EVT_SET_FOCUS event is fired, I can have it be handled by this function.
I found this:
import wx
class MyApp(wx.App):
def FilterEvent(self, evt):
print evt
return -1
app = MyApp(redirect=False)
app.SetCallFilterEvent(True)
frm = wx.Frame(None, title='Hello')
frm.Show()
app.MainLoop()
but I'm confused on how I would pass the event down to the children of MyApp.
What you can do is bind focus kill and focus select to functions, like this:
self.user_text.Bind(wx.EVT_SET_FOCUS, self.on_focus_username)
self.user_text.Bind(wx.EVT_KILL_FOCUS, self.on_deselect_username)
where self is the panel where the TextCtrl "user_text" is located".
The functions might look like this:
def on_focus_username(self, event):
if self.user_text.GetForegroundColour() != wx.BLACK:
self.user_text.SetForegroundColour(wx.BLACK)
# Clear text when clicked/focused
self.user_text.Clear()
def on_deselect_username(self, event):
if self.user_text.GetLineLength(0) is 0:
self.user_text.SetForegroundColour(wx.Colour(192, 192, 192))
# Put a default message when unclicked/focused away
self.user_text.SetValue("Enter Username")
Hope this helped, if I didn't answer something clearly/correctly let me know.

WxPython - trigger checkbox event while setting its value in the code

Consider the following piece of code:
import wx
class MyFrame(wx.Frame):
def __init__(self, *args, **kwds):
wx.Frame.__init__(self, *args, **kwds)
self.cb1 = wx.CheckBox(self, -1, "CheckBox 1")
self.cb2 = wx.CheckBox(self, -1, "CheckBox 2")
self.cb3 = wx.CheckBox(self, -1, "CheckBox 3")
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.cb1, 0, wx.ADJUST_MINSIZE, 0)
sizer.Add(self.cb2, 0, wx.ADJUST_MINSIZE, 0)
sizer.Add(self.cb3, 0, wx.ADJUST_MINSIZE, 0)
self.SetSizer(sizer)
self.Layout()
self.Bind(wx.EVT_CHECKBOX, self.OnCb1, self.cb1)
self.Bind(wx.EVT_CHECKBOX, self.OnCb2, self.cb2)
def OnCb1(self, evt):
self.cb2.SetValue(evt.IsChecked())
def OnCb2(self, evt):
self.cb3.SetValue(evt.IsChecked())
if __name__ == "__main__":
app = wx.PySimpleApp(0)
frame = MyFrame(None, -1, "")
app.SetTopWindow(frame)
frame.Show()
app.MainLoop()
Here I have 3 checkboxes bound together, so cb2 gets checked when cb1 does and cb3 gets checked when cb2 does. However, when I set the value of cb2 in OnCb1 routine, the cb2 checkbox event is not triggered, and cb3 checkbox remains unchecked. So I'd like to find a way to trigger somehow cb2 event manually to check all 3 boxes at once when checking only cb1. I'd be very grateful if anyone gives me a hint.
Use wx.PostEvent... like so:
class launcherWindow(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, parent=None, title='New Window')
#now add the main body, start with a panel
panel = wx.Panel(self)
#instantiate a new dropdown
self.productDropDown = wx.ComboBox(panel, size=wx.DefaultSize, style = wx.CB_READONLY)
#get the products and product subtypes
self.productDict = self.getProductsAndSubtypes()
#setup subtypes first, just in case, since onProductSelection will reference this
self.productSubtypeDropDown = wx.ComboBox(panel, size=wx.DefaultSize, style = wx.CB_READONLY)
#add products
for product in self.productDict.keys():
self.productDropDown.Append(product)
#bind selection event
self.productDropDown.Bind(wx.EVT_COMBOBOX, self.onProductSelection)
#set default selection
self.productDropDown.SetSelection(0)
#pretend that we clicked the product selection, so it's event gets called
wx.PostEvent(self.productDropDown, wx.CommandEvent(wx.wxEVT_COMMAND_COMBOBOX_SELECTED))
#now add the dropdown to a sizer, set the sizer for the panel, fit the panel, etc...
def onProductSelection(self, event):
productSelected = self.productDropDown.GetStringSelection()
productSubtypes = self.productDict[productSelected]
#clear any existing product subtypes, since each product may have different ones
self.productSubtypeDropDown.Clear()
for productSubtype in productSubtypes:
self.productSubtypeDropDown.Append(productSubtype)
#select the first item by default
self.productSubtypeDropDown.SetSelection(0)
I couldn't adopt nmz787's code directly, and had to hack around a bit, but I finally got it working for
changing the state of a checkbox and
having the checkbox receive the event,
just as if the user clicked on it.
I thought I'd post my code in case others are stuck trying to get this to work too. Since it only depended on the checkbox and not on which control was calling it I factored it out as an independent "top-level" function, not as a method on any class.
def toggleCheckBox(cb):
evt = wx.CommandEvent(commandType=wx.EVT_CHECKBOX.typeId)
evt.SetEventObject(cb)
cb.SetValue( not cb.GetValue())
wx.PostEvent(cb, evt)
I have no idea why the CommandEvent constructor required a keyword, or if there's a more sensible, robust way to get the typeId required, but this worked for me.
I'm not experienced with wxPython so I can't give you a specific example, but I do know that setting the value programmatically will not trigger command events for the widgets. My assumption is that you will have to manually post the event for cb2 after you set its value. You can review a similar question here: wxPython: Calling an event manually
What I might suggest is to subclass the wx.CheckBox and create a SetValueWithEvent() or similar method that will both call SetValue, and post a wx. EVT_CHECKBOX event.
PyQt has similar situations where signals may or may not be emitted when programmatically setting values on a widget. They will sometimes give you more than one signal that you can listen for to accomodate either way. Unfortunately, based only on my limited exposure to wxPython examples, I think its a lot more primitive, and a bit less pythonic. So you seem to have to do things yourself a little more often.

How to set the value of a wx.combobox by posting an event

NOTE: This appears to be an OSX specific problem.
The code below demonstrates the problem I am running into. I am creating a wx.ComboBox and trying to mimic it's functionality for testing purposes by posting a wxEVT_COMMAND_COMBOBOX_SELECTED event... this event strangely works fine for wx.Choice, but it doesn't do anything to the ComboBox.
There doesn't appear to be a different event that I can post to the combobox, but maybe I'm missing something.
I'm running this code on Python 2.5 on a Mac OSX 10.5.8
import wx
app = wx.PySimpleApp()
def on_btn(evt):
event = wx.CommandEvent(wx.wxEVT_COMMAND_COMBOBOX_SELECTED,combobox.Id)
event.SetEventObject(combobox)
event.SetInt(1)
event.SetString('bar')
combobox.Command(event)
app.ProcessPendingEvents()
frame = wx.Frame(None)
panel = wx.Panel(frame, -1)
# This doesn't work
combobox = wx.ComboBox(panel, -1, choices=['foo','bar'])
# This works
#combobox = wx.Choice(panel, -1, choices=['foo','bar'])
combobox.SetSelection(0)
btn = wx.Button(panel, -1, 'asdf')
btn.Bind(wx.EVT_BUTTON, on_btn)
sz = wx.BoxSizer()
sz.Add(combobox)
sz.Add(btn)
panel.SetSizer(sz)
frame.Show()
app.MainLoop()
UPDATE: I hooked up the combobox to a handler of wx.EVT_COMBOBOX to see what event type is being caught there and I got the id 10016 which matches wxEVT_COMMAND_COMBOBOX_SELECTED... so generating this command event certainly SHOULD cause the ComboBox to update.
This seems to be a specific bug from OSX.
Both alternatives work perfect in windowsXP.
I think this fixes it, or at least points the way towards a more complete fix.
Edit: you can use PyCommandEvent if you prefer, as well as using SetInt and SetString to put more information in the Event if needed. But it is necessary, as far as I can tell, to set the selection of the combobox as well.
import wx
app = wx.PySimpleApp()
def on_btn(evt):
combobox.Selection=1
wx.PostEvent(combobox, wx.CommandEvent(wx.wxEVT_COMMAND_COMBOBOX_SELECTED))
print "foo"
def on_select(evt):
print "selected", combobox.Selection
frame = wx.Frame(None)
panel = wx.Panel(frame, -1)
# This doesn't work
combobox = wx.ComboBox(panel, -1, choices=['foo','bar'])
# This works
# combobox = wx.Choice(panel, -1, choices=['foo','bar'])
combobox.SetSelection(0)
btn = wx.Button(panel, -1, 'asdf')
btn.Bind(wx.EVT_BUTTON, on_btn)
combobox.Bind(wx.EVT_COMBOBOX, on_select)
sz = wx.BoxSizer()
sz.Add(combobox)
sz.Add(btn)
panel.SetSizer(sz)
frame.Show()
app.MainLoop()

wxPython: Handling events in a widget that is inside a notebook

I have a wxPython notebook, in this case a wx.aui.AuiNotebook. (but this problem has happened with other kinds of notebooks as well.) In my notebook I have a widget, in this case a subclass of ScrolledPanel, for which I am trying to do some custom event handling (for wx.EVT_KEY_DOWN). However, the events are not being handled. I checked my code outside of the notebook, and the event handling works, but when I put my widget in the notebook, the event handler doesn't seem to get invoked when the event happens.
Does the notebook somehow block the event? How do I solve this?
I tried reproducing your problem but it worked fine for me. The only thing I can think of is that there is one of your classes that also binds to wx.EVT_KEY_DOWN and doesn't call wx.Event.Skip() in its callback. That would prevent further handling of the event. If your scrolled panel happens to be downstream of such an object in the sequence of event handlers it will never see the event.
For reference, here's an example that worked for me (on Windows). Is what you're doing much different than this?
import wx
import wx.aui, wx.lib.scrolledpanel
class AppFrame(wx.Frame):
def __init__(self, *args, **kwds):
wx.Frame.__init__(self, *args, **kwds)
# The notebook
self.nb = wx.aui.AuiNotebook(self)
# Create a scrolled panel
panel = wx.lib.scrolledpanel.ScrolledPanel(self, -1)
panel.SetupScrolling()
self.add_panel(panel, 'Scrolled Panel')
# Create a normal panel
panel = wx.Panel(self, -1)
self.add_panel(panel, 'Simple Panel')
# Set the notebook on the frame
self.sizer = wx.BoxSizer()
self.sizer.Add(self.nb, 1, wx.EXPAND)
self.SetSizer(self.sizer)
# Status bar to display the key code of what was typed
self.sb = self.CreateStatusBar()
def add_panel(self, panel, name):
panel.Bind(wx.EVT_KEY_DOWN, self.on_key)
self.nb.AddPage(panel, name)
def on_key(self, event):
self.sb.SetStatusText("key: %d [%d]" % (event.GetKeyCode(), event.GetTimestamp()))
event.Skip()
class TestApp(wx.App):
def OnInit(self):
frame = AppFrame(None, -1, 'Click on a panel and hit a key')
frame.Show()
self.SetTopWindow(frame)
return 1
if __name__ == "__main__":
app = TestApp(0)
app.MainLoop()

Categories