Add my own icon overlays

Introduction

The requirement: Add icon overlays to a folder when it is a Python package

Discussion

Things like icon overlays (those little bitmaps which appear in the bottom left-hand corner of icons, like the shortcut arrow) belong to the area of Windows programming known as Shell Extensions. (It's never bothered me particularly, but I imagine it confuses people coming from a *nix background where the shell is everything except the Windowing subsystem to discover that, in Windows, Shell programming is only the Windowing subsystem!)

The whole of the Shell API appears to have been designed in some kind of isolation from the rest of the Windows API. There are a few things which you can do either way, but most things you can only do using the Shell functionality. And icon overlays are one such thing. But if you look at a few examples of how to set them up, you'd be excused for backing away pale and trembling. Fortunately, though, once you cut through the COM-related verbiage and the C++/ATL clutter which tends to obscure the real code in the sort of examples you find online, the basic idea is quite simple. And, thanks to the sterling work of the pywin32 development team, not hard to implement under Python.

In fact, setting up a shell extension comes down to a few easy* steps:

  1. Create a class implementing the methods which the shell extension requires.
  2. Register this class as a COM Server and, probably, force explorer to restart.
  3. Make use of your Shell extension

* For some definition of easy, naturally!

Fundamentally, shell extensions are a callback mechanism. You set up a series of callback methods with a class according to the MS docs, register the class as a COM Server of the right type, and link the COM Server to the shell extension within the registry. When the shell finds it needs, say, an overlay handler, it scans the list of handlers, finds yours, instantiates your COM server and calls the methods you've implemented. Simple, no?

The Code

(after a post by Roger Upole to the python-win32 list)

import os
from win32com.shell import shell, shellcon
import winerror

class IconOverlay:
  
  _reg_clsid_ = '{4FC554DF-F0EE-4A4F-966C-9C49CCF14D59}'
  _reg_progid_ = 'TJG.PythonPackagesOverlayHandler'
  _reg_desc_ = 'Icon Overlay Handler to indicate Python packages'
  _public_methods_ = ['GetOverlayInfo', 'GetPriority', 'IsMemberOf']
  _com_interfaces_ = [shell.IID_IShellIconOverlayIdentifier]
  
  def GetOverlayInfo(self):
    return (r'C:\Program Files\TortoiseHg\icons\status\added.ico', 0, shellcon.ISIOI_ICONFILE)
  
  def GetPriority(self):
    return 50
  
  def IsMemberOf(self, fname, attributes):
    if os.path.exists (os.path.join (fname, "__init__.py")):
        return winerror.S_OK
    return winerror.E_FAIL

if __name__=='__main__':
  import win32api
  import win32con
  import win32com.server.register
  
  win32com.server.register.UseCommandLine (IconOverlay)
  keyname = r'Software\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\PyPackageOverlay'
  key = win32api.RegCreateKey (win32con.HKEY_LOCAL_MACHINE, keyname)
  win32api.RegSetValue (key, None, win32con.REG_SZ, IconOverlay._reg_clsid_)

Notes

The GUID assigned to _reg_clsid_ should of course be generated specifically by you. You can use pythoncom.CreateGuid for this. Likewise the name and descriptions are obviously settable. The _public_methods_ and _com_interfaces_ should be left alone.

For the purposes of demonstration, I'm using an overlay icon from the TortoiseHg set. If you don't have that installed you can get it from here or just use something else.

When the COM server represented by this class is started up, GetOverlayInfo is called to return the one icon which it will be using for overlays [*], and GetPriority is called to get some kind of indication of priority between cooperating icon overlay handlers. This won't help your handler to beat someone else's but if you're the the only one interested in a particular file type and you have several handlers you can use this to negotiate between them somewhat.

Now, each time explorer wants to display a file or folder, it calls the IsMemberOf method of every registered icon overlay handler to see if it's interested in overlaying the file icon. This means that a handler which is not interested in a file should return E_FAIL as fast as it can. (And shouldn't spend too long returning S_OK either). Based on that return, explorer will overlay your defined icon on the file's default icon.

* I'm using an .ico file, so the second parameter representing position is zero, and the third parameter indicates that an icon file is being used. To use an .exe or a .dll containing icons, pass that filename as the first parameter, indicate which icon to select by the second parameter, and pass shellcon.ISIOI_ICONINDEX as the third parameter.