Introduction
Sometimes you may need to catch a Windows message that is not already handled by wxWidgets, so there is no wxEvent for it. With a bit of help from the PyWin32 modules it is possible to hook into the WndProc chain for a wxWindow and watch for the message you are interested in.
The magic is in the win32gui.SetWindowLong function. When used with the win32con.GWL_WNDPROC flag it causes a new WndProc to be set for the window, and returns the old one. This lets you write a function in Python that can get first crack at all the Windows messages being sent to the window, and if you are not interested in them then pass them on to the original wxWidgets WndProc.
Code Sample
1 import wx
2 import win32api
3 import win32gui
4 import win32con
5
6 class TestFrame(wx.Frame):
7 def __init__(self):
8 wx.Frame.__init__(self, None, title="WndProc Test", size=(200,150))
9 p = wx.Panel(self)
10
11 # Set the WndProc to our function
12 self.oldWndProc = win32gui.SetWindowLong(self.GetHandle(),
13 win32con.GWL_WNDPROC,
14 self.MyWndProc)
15
16 # Make a dictionary of message names to be used for printing below
17 self.msgdict = {}
18 for name in dir(win32con):
19 if name.startswith("WM_"):
20 value = getattr(win32con, name)
21 self.msgdict[value] = name
22
23
24 def MyWndProc(self, hWnd, msg, wParam, lParam):
25 # Display what we've got.
26 print (self.msgdict.get(msg), msg, wParam, lParam)
27
28 # Restore the old WndProc. Notice the use of wxin32api
29 # instead of win32gui here. This is to avoid an error due to
30 # not passing a callable object.
31 if msg == win32con.WM_DESTROY:
32 win32api.SetWindowLong(self.GetHandle(),
33 win32con.GWL_WNDPROC,
34 self.oldWndProc)
35
36 # Pass all messages (in this case, yours may be different) on
37 # to the original WndProc
38 return win32gui.CallWindowProc(self.oldWndProc,
39 hWnd, msg, wParam, lParam)
40
41 app = wx.PySimpleApp()
42 f = TestFrame()
43 f.Show()
44 app.MainLoop()
Here is a similar example using the ctypes module.
1 import ctypes
2 import wx
3
4 from ctypes import c_long, c_int
5
6 # grab the functions we need - unicode/not doesn't really matter
7 # for this demo
8 SetWindowLong = ctypes.windll.user32.SetWindowLongW
9 CallWindowProc = ctypes.windll.user32.CallWindowProcW
10
11
12 # a function type for the wndprc so ctypes can wrap it
13 WndProcType = ctypes.WINFUNCTYPE(c_int, c_long, c_int, c_int, c_int)
14
15 # constants
16 GWL_WNDPROC = -4
17
18 class TestFrame(wx.Frame):
19 def __init__(self, parent):
20 wx.Frame.__init__(self, parent)
21 # need to hold a reference to WINFUNCTYPE wrappers,
22 # so they don't get GCed
23 self.newWndProc = WndProcType(self.MyWndProc)
24 self.oldWndProc = SetWindowLong(
25 self.GetHandle(),
26 GWL_WNDPROC,
27 self.newWndProc
28 )
29
30
31 def MyWndProc(self, hWnd, msg, wParam, lParam):
32 print msg,wParam,lParam
33 return CallWindowProc(
34 self.oldWndProc,
35 hWnd,
36 msg,
37 wParam,
38 lParam
39 )
40
41 app =wx.App(False)
42 f = TestFrame(None)
43 f.Show()
44 app.MainLoop()
Comments
-- M. Vernier -- Michael Krause
Thanks for the fixes and suggestions guys. I've incorporated your code above.
You must do this to capture any Windows messages that are outside the domain of the wxPython (and wxWidgets) system. This includes you own custom messages and more obscure things like WM_DEVICECHANGE. If wx doesn't catch it at the low level, then your event registration is not going to help. I originally was trying solutions based on PythonEvents.py in the PythonDemo app, but Robin pointed me here. Thanks Robin! I wrote a little module that provides a class that you can mixin with any wxWindows window to get this functionality. I've tested it with WxFrame and wxPanel, but I didn't go any lower that that. Comments are welcome. It is attached as WndProcHookMixin.py
--KevinGMoore
I've modified Kevin's mixin class to be pure ctypes, so you can get this functionality with no additional dependencies under python 2.5. Attached: WndProcHookMixinCtypes.py