My co-worker showed me a neat little app that showed snowflakes falling on the Windows desktop. The snow would pile up on windows, and the snow didn’t draw on top of other windows, only the desktop wallpaper.
I wondered how I could accomplish this using wxPython. wx has a ScreenDC, but this draws on the entire screen, over windows and all. I wanted to draw under windows but over the desktop.
The solution I came up with isn’t cross platform, but it works. The idea is to use ctypes to invoke some win32 API’s to find the appropriate window to draw on. The next important thing is knowing about the function wxWindow::AssociateHandle. You can associate a window with any system-wide HWND and the wxWindow will start acting upon that window instead.
On windows, the control we want to draw on can be found with Spy++ and it’s hierarchy is:
- Progman
- SHELLDLL_DefView
- SysListView32
SysListView32 is the control that shows all the desktop icons. To get a handle to this control, we use the win32 functions GetDesktopWindow and FindWindowEx.
The following function finds a window from the specified name hierarchy.
def find_window(parent, names): if not names: return parent name = unicode(names[0]) child = 0 while True: child = ctypes.windll.user32.FindWindowExW(parent, child, name, 0) if not child: return 0 result = find_window(child, names[1:]) if result: return result
We use it like this:
desktop = ctypes.windll.user32.GetDesktopWindow() handle = 0 handle = handle or find_window(desktop, ['Progman', 'SHELLDLL_DefView', 'SysListView32']) handle = handle or find_window(desktop, ['WorkerW', 'SHELLDLL_DefView', 'SysListView32'])
On Windows 7 (and maybe Vista too), Progman is replaced by WorkerW, so we have to try both hierarchies and use the first match we find.
Then we just create a dummy window and associate it with the handle we found with find_window.
self.AssociateHandle(handle)
After that, a WindowDC can be used to draw on the desktop wallpaper. The usual flicker-reducing techniques can’t really be used as we aren’t getting paint events. Instead, what I did was invalidate small regions of the desktop with RefreshRect, followed by Update and then finally drawing the next frame. Like this:
dc = wx.WindowDC(self) for flake in self.flakes: self.RefreshRect(flake.rect) self.Update() flake.update() flake.draw(dc) dc.Destroy()
I called my snowflake implementation wxSnow, and you can get it here:
http://www.michaelfogleman.com/wxsnow-snow-falling-on-your-windows-desktop/