Surviving with EVT_KILL_FOCUS under Microsoft Windows
Using Focus Events for Pleasure and Profit
When you use a complex input form in your wxPython application, you may find that you need to perform an action as soon as the user tabs out of an input field. For example, an "invoice" input form may need to update the invoice total at the bottom of the form whenever the user enters a value into one of the "unit price" fields. Another example might be to validate the contents of an input field as soon as the user tries to tab out of the field, rather than validating when the user clicks on the "OK" button to close the form.
The usual way to perform these actions is to use the EVT_KILL_FOCUS command to tell wxPython to call a routine as soon as the user tries to move out of your input field. For example:
def __init__(self): ... self.field = wxTextCtrl(self, -1, "") EVT_KILL_FOCUS(self.field, self.onFocusLost) ... def onFocusLost(self, event): """ Respond to the user leaving our input field. """ ...
Of course, you can also use EVT_SET_FOCUS to make wxPython call a routine when the user moves into the field as well. You might want to do this to remember the value of the field as the user moves into the field, and only perform an action if the field's value was changed, like this:
def __init__(self): ... self.field = wxTextCtrl(self, -1, "") EVT_SET_FOCUS(self.field, self.onFocusGained) EVT_KILL_FOCUS(self.field, self.onFocusLost) self.curFieldValue = None ... def onFocusGained(self, event): self.curFieldValue = self.field.GetValue() def onFocusLost(self, event): if self.field.GetValue() != self.curFieldValue: # Field value changed -> respond appropriately. ...
The Problem
The scheme described above may seem like a straightforward way of dealing with the user moving into and out of input fields. Unfortunately, however, due to the wonderful vaguaries of Microsoft Windows, the above code can actually cause your wxPython application to crash in certain situations.
Because wxPython is built directly on top of the native operating system's own user-interface elements and their related event-handling logic, oddities of the underlying operating system unfortunately do flow through to your wxPython application. In this case, there are several events which get sent to the input fields and other user-interface elements, including events such as:
- Focus Gained
- Focus Lost
- Window (wxFrame) Closed
These events from the operating system are then passed directly on to your wxPython application via the EVT_SET_FOCUS, EVT_KILL_FOCUS, and EVT_CLOSE commands.
Now, when the user closes a window (for example, a non-modal dialog box containing input fields), Microsoft Windows does things in the following (completely illogical) order:
- Close the wxFrame, and destroy all objects related to it.
- Send the currently focused input field a "Focus Lost" event.
Because wxPython automatically handles the destruction of the wxFrame and all contained wxWindows within it (including your input fields), this means that if the user closes the wxFrame you will actually receive the EVT_KILL_FOCUS event after the input field object itself has been destroyed. If you then attempt to access a method of the input field object in your onFocusLost method, for example like this:
self.field.GetValue()
your application will crash in a most public and embarrassing way. Under Linux it will work perfectly well, but it will crash horribly under MS Windows -- even though your code looks perfectly simple and correct.
The Workaround
To prevent this problem from occurring, you need to add code to your application which remembers if the wxFrame has been closed, and have your EVT_KILL_FOCUS method check to see if this has happened before it attempts to access any of the input field's methods. Here is one way you could achieve this:
class MyFrame(wxFrame): def __init__(self): ... self.field = wxTextCtrl(self, -1, "") self.wasClosed = false self.curFieldValue = None EVT_CLOSE(self, self.onCloseFrame) EVT_SET_FOCUS(self, self.onFocusGained) EVT_KILL_FOCUS(self, self.onFocusLost) ... def onCloseFrame(self, event): self.wasClosed = true def onFocusGained(self, event): self.curFieldValue = self.field.GetValue() def onFocusLost(self, event): if self.wasClosed: return if self.field.GetValue() != self.curFieldValue: # Field value changed -> respond appropriately. ...
A More Generic Solution
The above scheme works quite well when you have a relatively simple input form, but doesn't work as well if you have a more complex form involving subclasses of wxTextCtrl. In the above examples, all the code for dealing with focus is in the frame itself -- but in many situations you will want to have the focus-handling logic within the input field, like this:
class MyInputField(wxTextCtrl): def __init__(self): wxTextCtrl.__init__(self, -1, "") EVT_KILL_FOCUS(self, onFocusLost) def onFocusLost(self, event): value = self.GetValue() if not self.isValid(value): ...
In this situation, it is much harder to know if the enclosing frame has been closed, because unfortunately EVT_CLOSE can only be applied to a wxFrame, not the individual wxWindows within it. If you are building a generic input field which can be used in any number of frames, figuring out if the enclosing frame has been closed before attempting to access the field's methods can be quite tricky.
A different approach is to add code to the wxFrame which handles this focus problem transparently. This can be done as follows:
class MyFrame(wxFrame): def __init__(self): ... EVT_CLOSE(self, self.onCloseFrame) ... def onCloseFrame(self, event): win = wxWindow_FindFocus() if win != None: win.Disconnect(-1, -1, wxEVT_KILL_FOCUS) self.Destroy()
This has the effect of disabling the EVT_KILL_FOCUS method when the frame is closed, preventing your onFocusLost() method from being called in the first place. This means you don't need to have any code at all in your wxTextCtrl subclass to handle the problems with EVT_KILL_FOCUS under MS Windows -- which simplifies your code and means you don't have to remember to add this in for each new wxTextCtrl subclass that you create.
Of course, you do still need to remember to add the above code to each of your wxFrame objects. To get around this, you can even go further, as described below.
Using a Mix-In Class
If you want to hide the details of this bug-fix as much as possible, you can create a mix-in class which implements this bug-fix in a way which makes it trivial for you to include in each of your various frames. Once you've defined your mix-in, all you need to do is remember to add it to the definition for each of your frames where focus handling may take place.
Here's the definition for the FrameMixIn class:
class FrameMixIn(wxWindow): def prepareFrame(self, closeEventHandler=None): self._closeHandler = closeEventHandler EVT_CLOSE(self, self.closeFrame) def closeFrame(self, event): win = wxWindow_FindFocus() if win != None: win.Disconnect(-1, -1, wxEVT_KILL_FOCUS) if self._closeHandler != None: self._closeHandler(event) else: event.Skip()
To use this mix-in class, you simply define your various frames like this:
class MyFrame(wxFrame, FrameMixIn): def __init__(self, parent): wxFrame.__init__(self, parent, ...) self.prepareFrame() ....
If you need to have a separate EVT_CLOSE handler for a frame, simply include the name of your close handler in the call to self.prepareFrame, like this:
self.prepareFrame(myCloseHandler)
Conclusion
Unfortunately, because wxPython is so inextricably bound to the underlying operating system for implementing the user interface, wxPython programmers have to deal with some subtle program crashes caused by the operating system itself. This problem with EVT_KILL_FOCUS is one such example. However, by following the example solutions presented above, you should be able to get around this problem in whatever way makes the most sense for your particular application.
If you have any problems getting the above examples to work, or can suggest improvements to this cookbook recipe, please contact the author at ewestra@dnet.com.
Comments
I just came up with a new approach which is useful for composable controls. It lets you determine whether a particular sub-set of children are still available for interaction:
def _stillAlive( self, *controls ): """Use a hack to figure out if our children are still alive""" children = self.GetChildren() for control in controls: if control not in children: return 0 return 1
You can then use a test like this to determine whether you can use a given child of the window:
if self._stillAlive( self.hour, self.minute ):
With that, you can short-circuit when the EVT_KILL_FOCUS event is passed in after the window contents are destroyed.
Enjoy,
Mike C. Fletcher
It seems that this issue also occurs on Mac OS X (tested with wxPython 2.8.9.1). The workaround described above also works there.
For those of you using "import wx" to import wxPython, the code is as follows:
import wx class MyFrame(wx.Frame): def __init__self): ... wx.EVT_CLOSE(self, self.onCloseFrame) ... def onCloseFrame(self, event): win = wx.Window_FindFocus() if win != None: # Note: you really have to use wx.wxEVT_KILL_FOCUS # instead of wx.EVT_KILL_FOCUS here: win.Disconnect(-1, -1, wx.wxEVT_KILL_FOCUS) self.Destroy()
Roel van Os