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.

--RobinDunn

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

--ChrisMellon

HookingTheWndProc (last edited 2008-03-11 10:50:31 by localhost)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.