Windows explorer context menus with sub-menus using pywin32
Asked Answered
B

2

24

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)
Birck answered 31/5, 2012 at 12:33 Comment(3)
Hi GP89. Do post back here if you find a solution. I'm trying to add a simple context menu which only shows up for a particular folder but I have no idea about Win32 programming.Wooldridge
Hey, it took a long time but I'm almost there with it now. I couldn't find any way to debug or really any examples on the net. I'll post it up in the next day or two :)Birck
I put the answer on below, if you take out the file bit in the register and unregister methods and return 0 from the Query method if you're not in the folder you're interested in you should be able to get what you want fairly easilyBirck
B
23

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)
Birck answered 6/6, 2012 at 9:25 Comment(4)
Does this work on Windows 10? Am not having much luck with it?Tibbetts
@Tibbetts I'm not sure sorry, I haven't used a windows system in a few years now. Was working with windows 7 when I was doing thisBirck
Found some alternatives msdn.microsoft.com/en-gb/library/windows/desktop/… and some comments about that page (as it contains errors) here, social.msdn.microsoft.com/Forums/windowsdesktop/en-US/…Tibbetts
Has anyone gotten this to work in Windows 10 yet? I want to launch Python-based files and folder processing utilities, e.g., renaming, etc. If anyone has a solution can you please post the solution here or the link to it here? Thank you.Seagoing
H
0

11 Years after:

The accepted answer didn't work for me on Windows11. Here's a script that works:

"""
PathCatcher is a Windows utility that allows one to right-click on 
a folder or a file in Explorer and save its path to the clipboard.
If this module is run by itself, it installs PathCatcher to the registry.
After it is installed, when one clicks on a file or folder, "PathCatcher" 
appears in the right-click menu.
This module also contains some useful code for accessing the Windows
clipboard and registry.
OLD: Requires ctypes -- download from SourceForge.
Jack Trainor 2007
Updated by The Famous Unknown in 20181128
Several fixes on Jack's code, which was already 11 years old (Thanks jack!)
Supports both python2 and 3
Doesn't require ctypes as before
To install run python pathcatcher.py once
"""
import sys
import win32con
import time
import os
import win32api
import win32clipboard

# IMPORTANT #
#Specify the python path manually - PYTHONPATH ENV can be unrealiable
pythonwExePath = "e:\\anaconda2\\pythonw.exe" # pythonw exe path



_input = None #
#handles input on py2 and py3
if sys.version_info[0] == 2:
    _input = raw_input
elif sys.version_info[0] == 3:
    _input = input


""" Windows Clipboard utilities """
def GetClipboardText():
    win32clipboard.OpenClipboard()
    data = win32clipboard.GetClipboardData()
    win32clipboard.CloseClipboard()
    return data

def SetClipboardText(dir_or_file):
    win32clipboard.OpenClipboard()
    win32clipboard.EmptyClipboard()
    win32clipboard.SetClipboardText(dir_or_file)
    win32clipboard.CloseClipboard()

""" Windows Registry utilities """
def OpenRegistryKey(hiveKey, key):
    keyHandle = None
    try:
        curKey = ""
        keyItems = key.split('\\')
        for keyItem in keyItems:
            if curKey:
                curKey = curKey + "\\" + keyItem
            else:
                curKey = keyItem
            keyHandle = win32api.RegCreateKey(hiveKey, curKey)
    except Exception as e:
        keyHandle = None
        print ("OpenRegistryKey failed:", e)
    return keyHandle

def ReadRegistryValue(hiveKey, key, name):
    """ Simple api to read one value from Windows registry.
    If 'name' is empty string, reads default value."""
    data = typeId = None
    try:
        hKey = win32api.RegOpenKeyEx(hiveKey, key, 0, win32con.KEY_ALL_ACCESS)
        data, typeId = win32api.RegQueryValueEx(hKey, name)
        win32api.RegCloseKey(hKey)
    except Exception as e:
        print ("ReadRegistryValue failed:", e)
    return data, typeId

def WriteRegistryValue(hiveKey, key, name, typeId, data):
    """ Simple api to write one value to Windows registry.
    If 'name' is empty string, writes to default value."""
    try:
        keyHandle = OpenRegistryKey(hiveKey, key)
        win32api.RegSetValueEx(keyHandle, name, 0, typeId, data)
        win32api.RegCloseKey(keyHandle)
    except Exception as e:
        print ("WriteRegistry failed:", e)

""" misc utilities """


def WriteLastTime():
    secsString = str(time.time())
    WriteRegistryValue(win32con.HKEY_CLASSES_ROOT, r"*\shell\PathCatcher\time", "", win32con.REG_SZ, secsString)

def ReadLastTime():
    secs = 0.0
    secsString, dateTypId = ReadRegistryValue(win32con.HKEY_CLASSES_ROOT, r"*\shell\PathCatcher\time", "")
    if secsString:
        secs = float(secsString)
    return secs

def AccumulatePaths(path):
    """ Windows creates a Python process for each selected file on right-click.
    Check to see if this invocation is part of current batch and accumulate to clipboard """
    lastTime = ReadLastTime()
    now = time.time()
    if (now - lastTime) < 1.0:
        SetClipboardText(GetClipboardText() + "\n" + path)
    else:
        SetClipboardText(path)
    WriteLastTime()

#########################################################
def InstallPathCatcher():
    """ Installs PathCatcher to the Windows registry """
    command = '%s %s "%s"' % (pythonwExePath, os.getcwd()+"/"+sys.argv[0], "%1")
    WriteRegistryValue(win32con.HKEY_CLASSES_ROOT, r"*\shell\PathCatcher\Command", "", win32con.REG_SZ, command)
    WriteRegistryValue(win32con.HKEY_CLASSES_ROOT, r"Folder\shell\PathCatcher\Command", "", win32con.REG_SZ, command)
    WriteLastTime()

#########################################################
if __name__ == "__main__":
    try:
        # log requests
        with open("d:/pathcatcher.log", "a") as f:
            f.write( "{}".format( sys.argv[1]+"\n" ) )
    except:
        pass
    if len(sys.argv) > 1:
        """ If invoked through a right-click, there will be a path argument """
        path = sys.argv[1]

        AccumulatePaths(path)
    else:
        """ If module is run by itself, install PathCatcher to registry """
        InstallPathCatcher()
        _input("PathCatcher installed.\nPress RETURN...")

Notes:

  • Change pythonwExePath variable to match your settings
  • Run on an elevated command prompt/terminal without arguments to register the first time
  • By default, the script copies the current file/folder path to the clipboard, but can be modified for other things, just get the path argument with sys.argv[1] and work from here.
  • You can change several settings on HKEY_CLASSES_ROOT\*\shell\PathCatcher, like the name PathCatcher that appears on right click, to something like MyScript. Restart windows explorer after to apply changes.
  • Tested on Python3.7
  • Gist
Hautbois answered 30/6, 2023 at 21:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.