使用pywin32创建带有子菜单的Windows资源管理器上下文菜单

24

我试图使用Python添加一些带有图标和子菜单的Shell扩展,但我在使用pywin32的演示之后就束手无策了。我尝试通过谷歌搜索来获取更多信息,但似乎没有任何结果。

我相信我需要注册一个COM服务器,以便根据右键单击的文件/文件夹所在位置和文件类型等更改子菜单中的选项。

# 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)

嗨,GP89。如果你找到解决方案,请在这里回复。我正在尝试添加一个简单的上下文菜单,它只会出现在特定的文件夹中,但我对Win32编程一无所知。 - Mridang Agarwalla
嘿,花了很长时间,但现在我几乎完成了。我找不到任何调试的方法,也没有在网上找到任何示例。我会在接下来的一两天内发布它 :) - GP89
我把答案放在下面了,如果你在注册和注销方法中去掉文件位,并且如果你不在你感兴趣的文件夹中,在查询方法中删除return 0,那么你应该能够相当容易地得到你想要的结果。 - GP89
2个回答

23

经过多次尝试和谷歌搜索,我找到了如何做到这一点。

下面的示例显示带有子菜单和图标的菜单。

# 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)

这个在Windows 10上能用吗?我用它运行不太顺利。 - Dynite
@Dynite 对不起,我不确定,我已经好几年没有使用Windows系统了。当时我正在使用Windows 7进行这项工作。 - GP89
找到了一些替代方案 https://msdn.microsoft.com/zh-cn/library/windows/desktop/cc144171(v=vs.85).aspx#cascade_subcommands,以及关于该页面(因为它包含错误)的一些评论在这里,https://social.msdn.microsoft.com/Forums/windowsdesktop/en-US/3ee4702a-f75a-4790-9332-e50f241efd6f/unable-to-create-extended-cascaded-menu-using-extendedsubcommandskey?forum=windowsuidevelopment。 - Dynite
有人在Windows 10上成功运行过这个吗?我想要启动基于Python的文件和文件夹处理工具,例如重命名等。如果有人有解决方案,请在此处发布解决方案或链接。谢谢。 - Rich Lysakowski PhD

0
11年后:
接受的答案在Windows11上对我无效。这是一个有效的脚本:
"""
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...")

注意:

  • pythonwExePath变量更改为与您的设置相匹配
  • 提升权限的命令提示符/终端上运行,不带参数以进行首次注册
  • 默认情况下,脚本将当前文件/文件夹的路径复制到剪贴板,但可以根据需要进行修改,只需使用sys.argv[1]获取路径参数并从此处开始操作。
  • 您可以在HKEY_CLASSES_ROOT\*\shell\PathCatcher上更改多个设置,例如右键单击时显示的名称PathCatcher,可以更改为MyScript之类的内容。应用更改后,请重新启动Windows资源管理器。
  • 已在Python3.7上进行测试
  • Gist

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接