Search code examples
pythoncontextmenupywin32shell-extensions

Windows explorer context menus with sub-menus using pywin32


I'm trying add some shell extensions using python with icons and a sub menu but I'm struggling to get much further than the demo in pywin32. I can't seem to come up with anything by searching google, either.

I believe I need to register a com server to be able to change the options in submenu depending on where the right clicked file/folder is and the type of file etc.

# A sample context menu handler.
# Adds a 'Hello from Python' menu entry to .py files.  When clicked, a
# simple message box is displayed.
#
# To demostrate:
# * Execute this script to register the context menu.
# * Open Windows Explorer, and browse to a directory with a .py file.
# * Right-Click on a .py file - locate and click on 'Hello from Python' on
#   the context menu.

import pythoncom
from win32com.shell import shell, shellcon
import win32gui
import win32con

class ShellExtension:
    _reg_progid_ = "Python.ShellExtension.ContextMenu"
    _reg_desc_ = "Python Sample Shell Extension (context menu)"
    _reg_clsid_ = "{CED0336C-C9EE-4a7f-8D7F-C660393C381F}"
    _com_interfaces_ = [shell.IID_IShellExtInit, shell.IID_IContextMenu]
    _public_methods_ = shellcon.IContextMenu_Methods + shellcon.IShellExtInit_Methods

    def Initialize(self, folder, dataobj, hkey):
        print "Init", folder, dataobj, hkey
        self.dataobj = dataobj

    def QueryContextMenu(self, hMenu, indexMenu, idCmdFirst, idCmdLast, uFlags):
        print "QCM", hMenu, indexMenu, idCmdFirst, idCmdLast, uFlags
        # Query the items clicked on
        format_etc = win32con.CF_HDROP, None, 1, -1, pythoncom.TYMED_HGLOBAL
        sm = self.dataobj.GetData(format_etc)
        num_files = shell.DragQueryFile(sm.data_handle, -1)
        if num_files>1:
            msg = "&Hello from Python (with %d files selected)" % num_files
        else:
            fname = shell.DragQueryFile(sm.data_handle, 0)
            msg = "&Hello from Python (with '%s' selected)" % fname
        idCmd = idCmdFirst
        items = ['First Python content menu item!']
        if (uFlags & 0x000F) == shellcon.CMF_NORMAL: # Check == here, since CMF_NORMAL=0
            print "CMF_NORMAL..."
            items.append(msg)
        elif uFlags & shellcon.CMF_VERBSONLY:
            print "CMF_VERBSONLY..."
            items.append(msg + " - shortcut")
        elif uFlags & shellcon.CMF_EXPLORE:
            print "CMF_EXPLORE..."
            items.append(msg + " - normal file, right-click in Explorer")
        elif uFlags & CMF_DEFAULTONLY:
            print "CMF_DEFAULTONLY...\r\n"
        else:
            print "** unknown flags", uFlags
        win32gui.InsertMenu(hMenu, indexMenu,
                            win32con.MF_SEPARATOR|win32con.MF_BYPOSITION,
                            0, None)
        indexMenu += 1
        for item in items:
            win32gui.InsertMenu(hMenu, indexMenu,
                                win32con.MF_STRING|win32con.MF_BYPOSITION,
                                idCmd, item)
            indexMenu += 1
            idCmd += 1

        win32gui.InsertMenu(hMenu, indexMenu,
                            win32con.MF_SEPARATOR|win32con.MF_BYPOSITION,
                            0, None)
        indexMenu += 1
        return idCmd-idCmdFirst # Must return number of menu items we added.

    def InvokeCommand(self, ci):
        mask, hwnd, verb, params, dir, nShow, hotkey, hicon = ci
        win32gui.MessageBox(hwnd, "Hello", "Wow", win32con.MB_OK)

    def GetCommandString(self, cmd, typ):
        # If GetCommandString returns the same string for all items then
        # the shell seems to ignore all but one.  This is even true in
        # Win7 etc where there is no status bar (and hence this string seems
        # ignored)
        return "Hello from Python (cmd=%d)!!" % (cmd,)

def DllRegisterServer():
    import _winreg
    folder_key = _winreg.CreateKey(_winreg.HKEY_CLASSES_ROOT,
    "Folder\\shellex")
    folder_subkey = _winreg.CreateKey(folder_key, "ContextMenuHandlers")
    folder_subkey2 = _winreg.CreateKey(folder_subkey, "PythonSample")
    _winreg.SetValueEx(folder_subkey2, None, 0, _winreg.REG_SZ,
    ShellExtension._reg_clsid_)

    file_key = _winreg.CreateKey(_winreg.HKEY_CLASSES_ROOT,
    "*\\shellex")
    file_subkey = _winreg.CreateKey(file_key, "ContextMenuHandlers")
    file_subkey2 = _winreg.CreateKey(file_subkey, "PythonSample")
    _winreg.SetValueEx(file_subkey2, None, 0, _winreg.REG_SZ,
    ShellExtension._reg_clsid_)

    print ShellExtension._reg_desc_, "registration complete."

def DllUnregisterServer():
    import _winreg
    try:
        folder_key = _winreg.DeleteKey(_winreg.HKEY_CLASSES_ROOT,

        "Folder\\shellex\\ContextMenuHandlers\\PythonSample")
        file_key = _winreg.DeleteKey(_winreg.HKEY_CLASSES_ROOT,

        "*\\shellex\\ContextMenuHandlers\\PythonSample ")
    except WindowsError, details:
        import errno
        if details.errno != errno.ENOENT:
            raise
    print ShellExtension._reg_desc_, "unregistration complete."

if __name__=='__main__':
    from win32com.server import register
    register.UseCommandLine(ShellExtension,
                   finalize_register = DllRegisterServer,
                   finalize_unregister = DllUnregisterServer)

Solution

  • I found out how to do this after a lot of trial and error and googling.

    The example below shows a menu with a submenu and icons.

    # A sample context menu handler.
    # Adds a menu item with sub menu to all files and folders, different options inside specified folder. 
    # When clicked a list of selected items is displayed.
    #
    # To demostrate:
    # * Execute this script to register the context menu. `python context_menu.py --register`
    # * Restart explorer.exe- in the task manager end process on explorer.exe. Then file > new task, then type explorer.exe
    # * Open Windows Explorer, and browse to a file/directory.
    # * Right-Click file/folder - locate and click on an option under 'Menu options'.
    
    import os
    import pythoncom
    from win32com.shell import shell, shellcon
    import win32gui
    import win32con
    import win32api
    
    class ShellExtension:
        _reg_progid_ = "Python.ShellExtension.ContextMenu"
        _reg_desc_ = "Python Sample Shell Extension (context menu)"
        _reg_clsid_ = "{CED0336C-C9EE-4a7f-8D7F-C660393C381F}"
        _com_interfaces_ = [shell.IID_IShellExtInit, shell.IID_IContextMenu]
        _public_methods_ = shellcon.IContextMenu_Methods + shellcon.IShellExtInit_Methods
    
        def Initialize(self, folder, dataobj, hkey):
            print "Init", folder, dataobj, hkey
            win32gui.InitCommonControls()
            self.brand= "Menu options"
            self.folder= "C:\\Users\\Paul\\"
            self.dataobj = dataobj
            self.hicon= self.prep_menu_icon(r"C:\path\to\icon.ico")
    
    
        def QueryContextMenu(self, hMenu, indexMenu, idCmdFirst, idCmdLast, uFlags):
            print "QCM", hMenu, indexMenu, idCmdFirst, idCmdLast, uFlags
    
            # Query the items clicked on
            files= self.getFilesSelected()
    
            fname = files[0]
            idCmd = idCmdFirst
    
            isdir= os.path.isdir(fname)
            in_folder= all([f_path.startswith(self.folder) for f_path in files])
    
            win32gui.InsertMenu(hMenu, indexMenu,
                win32con.MF_SEPARATOR|win32con.MF_BYPOSITION,
                0, None)
            indexMenu += 1
    
            menu= win32gui.CreatePopupMenu()
            win32gui.InsertMenu(hMenu,indexMenu,win32con.MF_STRING|win32con.MF_BYPOSITION|win32con.MF_POPUP,menu,self.brand)
            win32gui.SetMenuItemBitmaps(hMenu,menu,0,self.hicon,self.hicon)
    #        idCmd+=1
            indexMenu+=1
    
            if in_folder:
                if len(files) == 1:
                    if isdir:
                        win32gui.InsertMenu(menu,0,win32con.MF_STRING,idCmd,"Item 1"); idCmd+=1
                    else:
                        win32gui.InsertMenu(menu,0,win32con.MF_STRING,idCmd,"Item 2")
                        win32gui.SetMenuItemBitmaps(menu,idCmd,0,self.hicon,self.hicon)
                        idCmd+=1
            else:
                win32gui.InsertMenu(menu,0,win32con.MF_STRING,idCmd,"Item 3")
                win32gui.SetMenuItemBitmaps(menu,idCmd,0,self.hicon,self.hicon)
                idCmd+=1
    
            if idCmd > idCmdFirst:
                win32gui.InsertMenu(menu,1,win32con.MF_SEPARATOR,0,None)
    
            win32gui.InsertMenu(menu,2,win32con.MF_STRING,idCmd,"Item 4")
            win32gui.SetMenuItemBitmaps(menu,idCmd,0,self.hicon,self.hicon)
            idCmd+=1
            win32gui.InsertMenu(menu,3,win32con.MF_STRING,idCmd,"Item 5")
            win32gui.SetMenuItemBitmaps(menu,idCmd,0,self.hicon,self.hicon)
            idCmd+=1
    
            win32gui.InsertMenu(menu,4,win32con.MF_SEPARATOR,0,None)
    
            win32gui.InsertMenu(menu,5,win32con.MF_STRING|win32con.MF_DISABLED,idCmd,"Item 6")
            win32gui.SetMenuItemBitmaps(menu,idCmd,0,self.hicon,self.hicon)
            idCmd+=1
    
            win32gui.InsertMenu(hMenu, indexMenu,
                                win32con.MF_SEPARATOR|win32con.MF_BYPOSITION,
                                0, None)
            indexMenu += 1
            return idCmd-idCmdFirst # Must return number of menu items we added.
    
        def getFilesSelected(self):
            format_etc = win32con.CF_HDROP, None, 1, -1, pythoncom.TYMED_HGLOBAL
            sm = self.dataobj.GetData(format_etc)
            num_files = shell.DragQueryFile(sm.data_handle, -1)
            files= []
            for i in xrange(num_files):
                fpath= shell.DragQueryFile(sm.data_handle,i)
                files.append(fpath)
            return files
    
        def prep_menu_icon(self, icon): #Couldn't get this to work with pngs, only ico
            # First load the icon.
            ico_x = win32api.GetSystemMetrics(win32con.SM_CXSMICON)
            ico_y = win32api.GetSystemMetrics(win32con.SM_CYSMICON)
            hicon = win32gui.LoadImage(0, icon, win32con.IMAGE_ICON, ico_x, ico_y, win32con.LR_LOADFROMFILE)
    
            hdcBitmap = win32gui.CreateCompatibleDC(0)
            hdcScreen = win32gui.GetDC(0)
            hbm = win32gui.CreateCompatibleBitmap(hdcScreen, ico_x, ico_y)
            hbmOld = win32gui.SelectObject(hdcBitmap, hbm)
            # Fill the background.
            brush = win32gui.GetSysColorBrush(win32con.COLOR_MENU)
            win32gui.FillRect(hdcBitmap, (0, 0, 16, 16), brush)
            # unclear if brush needs to be feed.  Best clue I can find is:
            # "GetSysColorBrush returns a cached brush instead of allocating a new
            # one." - implies no DeleteObject
            # draw the icon
            win32gui.DrawIconEx(hdcBitmap, 0, 0, hicon, ico_x, ico_y, 0, 0, win32con.DI_NORMAL)
            win32gui.SelectObject(hdcBitmap, hbmOld)
            win32gui.DeleteDC(hdcBitmap)
    
            return hbm
    
        def InvokeCommand(self, ci):
            mask, hwnd, verb, params, dir, nShow, hotkey, hicon = ci
            win32gui.MessageBox(hwnd, str(self.getFilesSelected()), "Wow", win32con.MB_OK)
    
        def GetCommandString(self, cmd, typ):
            # If GetCommandString returns the same string for all items then
            # the shell seems to ignore all but one.  This is even true in
            # Win7 etc where there is no status bar (and hence this string seems
            # ignored)
            return "Hello from Python (cmd=%d)!!" % (cmd,)
    
    def DllRegisterServer():
        import _winreg
        folder_key = _winreg.CreateKey(_winreg.HKEY_CLASSES_ROOT,
        "Folder\\shellex")
        folder_subkey = _winreg.CreateKey(folder_key, "ContextMenuHandlers")
        folder_subkey2 = _winreg.CreateKey(folder_subkey, "PythonSample")
        _winreg.SetValueEx(folder_subkey2, None, 0, _winreg.REG_SZ,
        ShellExtension._reg_clsid_)
    
        file_key = _winreg.CreateKey(_winreg.HKEY_CLASSES_ROOT,
        "*\\shellex")
        file_subkey = _winreg.CreateKey(file_key, "ContextMenuHandlers")
        file_subkey2 = _winreg.CreateKey(file_subkey, "PythonSample")
        _winreg.SetValueEx(file_subkey2, None, 0, _winreg.REG_SZ,
        ShellExtension._reg_clsid_)
    
        print ShellExtension._reg_desc_, "registration complete."
    
    def DllUnregisterServer():
        import _winreg
        try:
            folder_key = _winreg.DeleteKey(_winreg.HKEY_CLASSES_ROOT,
    
            "Folder\\shellex\\ContextMenuHandlers\\PythonSample")
            file_key = _winreg.DeleteKey(_winreg.HKEY_CLASSES_ROOT,
    
            "*\\shellex\\ContextMenuHandlers\\PythonSample")
        except WindowsError, details:
            import errno
            if details.errno != errno.ENOENT:
                raise
        print ShellExtension._reg_desc_, "unregistration complete."
    
    if __name__=='__main__':
        from win32com.server import register
        register.UseCommandLine(ShellExtension,
                       finalize_register = DllRegisterServer,
                       finalize_unregister = DllUnregisterServer)