Todo: 集成多平台 解决因SaiNiu线程抢占资源问题 本地提交测试环境打包 和 正式打包脚本与正式环境打包bat 提交Python32环境包 改进多日志文件生成情况修改打包日志细节

This commit is contained in:
2025-09-18 15:52:03 +08:00
parent 8b9fc925fa
commit 7cfc0c22b7
7608 changed files with 2424791 additions and 25 deletions

View File

@@ -0,0 +1,125 @@
# A sample shell column provider
# Mainly ported from MSDN article:
# Using Shell Column Handlers for Detailed File Information,
# Raymond Chen, Microsoft Corporation, February 2000
#
# To demostrate:
# * Execute this script to register the namespace.
# * Open Windows Explorer
# * Right-click an explorer column header - select "More"
# * Locate column 'pyc size' or 'pyo size', and add it to the view.
# This handler is providing that column data.
import os
import stat
import commctrl
import pythoncom
from pywintypes import IID
from win32com.shell import shell, shellcon
IPersist_Methods = ["GetClassID"]
IColumnProvider_Methods = IPersist_Methods + [
"Initialize",
"GetColumnInfo",
"GetItemData",
]
class ColumnProvider:
_reg_progid_ = "Python.ShellExtension.ColumnProvider"
_reg_desc_ = "Python Sample Shell Extension (Column Provider)"
_reg_clsid_ = IID("{0F14101A-E05E-4070-BD54-83DFA58C3D68}")
_com_interfaces_ = [
pythoncom.IID_IPersist,
shell.IID_IColumnProvider,
]
_public_methods_ = IColumnProvider_Methods
# IPersist
def GetClassID(self):
return self._reg_clsid_
# IColumnProvider
def Initialize(self, colInit):
flags, reserved, name = colInit
print("ColumnProvider initializing for file", name)
def GetColumnInfo(self, index):
# We support exactly 2 columns - 'pyc size' and 'pyo size'
if index in [0, 1]:
# As per the MSDN sample, use our CLSID as the fmtid
if index == 0:
ext = ".pyc"
else:
ext = ".pyo"
title = ext + " size"
description = "Size of compiled %s file" % ext
col_id = (self._reg_clsid_, index) # fmtid # pid
col_info = (
col_id, # scid
pythoncom.VT_I4, # vt
commctrl.LVCFMT_RIGHT, # fmt
20, # cChars
shellcon.SHCOLSTATE_TYPE_INT
| shellcon.SHCOLSTATE_SECONDARYUI, # csFlags
title,
description,
)
return col_info
return None # Indicate no more columns.
def GetItemData(self, colid, colData):
fmt_id, pid = colid
fmt_id == self._reg_clsid_
flags, attr, reserved, ext, name = colData
if ext.lower() not in [".py", ".pyw"]:
return None
if pid == 0:
ext = ".pyc"
else:
ext = ".pyo"
check_file = os.path.splitext(name)[0] + ext
try:
st = os.stat(check_file)
return st[stat.ST_SIZE]
except OSError:
# No file
return None
def DllRegisterServer():
import winreg
# Special ColumnProvider key
key = winreg.CreateKey(
winreg.HKEY_CLASSES_ROOT,
"Folder\\ShellEx\\ColumnHandlers\\" + str(ColumnProvider._reg_clsid_),
)
winreg.SetValueEx(key, None, 0, winreg.REG_SZ, ColumnProvider._reg_desc_)
print(ColumnProvider._reg_desc_, "registration complete.")
def DllUnregisterServer():
import winreg
try:
key = winreg.DeleteKey(
winreg.HKEY_CLASSES_ROOT,
"Folder\\ShellEx\\ColumnHandlers\\" + str(ColumnProvider._reg_clsid_),
)
except OSError as details:
import errno
if details.errno != errno.ENOENT:
raise
print(ColumnProvider._reg_desc_, "unregistration complete.")
if __name__ == "__main__":
from win32com.server import register
register.UseCommandLine(
ColumnProvider,
finalize_register=DllRegisterServer,
finalize_unregister=DllUnregisterServer,
)

View File

@@ -0,0 +1,122 @@
# 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
import win32con
import win32gui
from win32com.shell import shell, shellcon
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 & shellcon.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
key = winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, "Python.File\\shellex")
subkey = winreg.CreateKey(key, "ContextMenuHandlers")
subkey2 = winreg.CreateKey(subkey, "PythonSample")
winreg.SetValueEx(subkey2, None, 0, winreg.REG_SZ, ShellExtension._reg_clsid_)
print(ShellExtension._reg_desc_, "registration complete.")
def DllUnregisterServer():
import winreg
try:
key = winreg.DeleteKey(
winreg.HKEY_CLASSES_ROOT,
"Python.File\\shellex\\ContextMenuHandlers\\PythonSample",
)
except OSError as 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,
)

View File

@@ -0,0 +1,84 @@
# A sample shell copy hook.
# To demostrate:
# * Execute this script to register the context menu.
# * Open Windows Explorer
# * Attempt to move or copy a directory.
# * Note our hook's dialog is displayed.
import pythoncom
import win32con
import win32gui
from win32com.shell import shell
# Our shell extension.
class ShellExtension:
_reg_progid_ = "Python.ShellExtension.CopyHook"
_reg_desc_ = "Python Sample Shell Extension (copy hook)"
_reg_clsid_ = "{1845b6ba-2bbd-4197-b930-46d8651497c1}"
_com_interfaces_ = [shell.IID_ICopyHook]
_public_methods_ = ["CopyCallBack"]
def CopyCallBack(self, hwnd, func, flags, srcName, srcAttr, destName, destAttr):
# This function should return:
# IDYES Allows the operation.
# IDNO Prevents the operation on this folder but continues with any other operations that have been approved (for example, a batch copy operation).
# IDCANCEL Prevents the current operation and cancels any pending operations.
print("CopyCallBack", hwnd, func, flags, srcName, srcAttr, destName, destAttr)
return win32gui.MessageBox(
hwnd, "Allow operation?", "CopyHook", win32con.MB_YESNO
)
def DllRegisterServer():
import winreg
key = winreg.CreateKey(
winreg.HKEY_CLASSES_ROOT,
"directory\\shellex\\CopyHookHandlers\\" + ShellExtension._reg_desc_,
)
winreg.SetValueEx(key, None, 0, winreg.REG_SZ, ShellExtension._reg_clsid_)
key = winreg.CreateKey(
winreg.HKEY_CLASSES_ROOT,
"*\\shellex\\CopyHookHandlers\\" + ShellExtension._reg_desc_,
)
winreg.SetValueEx(key, None, 0, winreg.REG_SZ, ShellExtension._reg_clsid_)
print(ShellExtension._reg_desc_, "registration complete.")
def DllUnregisterServer():
import winreg
try:
key = winreg.DeleteKey(
winreg.HKEY_CLASSES_ROOT,
"directory\\shellex\\CopyHookHandlers\\" + ShellExtension._reg_desc_,
)
except OSError as details:
import errno
if details.errno != errno.ENOENT:
raise
try:
key = winreg.DeleteKey(
winreg.HKEY_CLASSES_ROOT,
"*\\shellex\\CopyHookHandlers\\" + ShellExtension._reg_desc_,
)
except OSError as 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,
)
#!/usr/bin/env python

View File

@@ -0,0 +1,188 @@
# A sample implementation of IEmptyVolumeCache - see
# https://learn.microsoft.com/en-ca/windows/win32/lwef/disk-cleanup for an overview.
#
# * Execute this script to register the handler
# * Start the "disk cleanup" tool - look for "pywin32 compiled files"
import os
import stat
import sys
import pythoncom
import win32gui
import winerror
from win32com.server.exception import COMException
from win32com.shell import shell, shellcon
# Our shell extension.
IEmptyVolumeCache_Methods = (
"Initialize GetSpaceUsed Purge ShowProperties Deactivate".split()
)
IEmptyVolumeCache2_Methods = "InitializeEx".split()
ico = os.path.join(sys.prefix, "py.ico")
if not os.path.isfile(ico):
ico = os.path.join(sys.prefix, "PC", "py.ico")
if not os.path.isfile(ico):
ico = None
print("Can't find python.ico - no icon will be installed")
class EmptyVolumeCache:
_reg_progid_ = "Python.ShellExtension.EmptyVolumeCache"
_reg_desc_ = "Python Sample Shell Extension (disk cleanup)"
_reg_clsid_ = "{EADD0777-2968-4c72-A999-2BF5F756259C}"
_reg_icon_ = ico
_com_interfaces_ = [shell.IID_IEmptyVolumeCache, shell.IID_IEmptyVolumeCache2]
_public_methods_ = IEmptyVolumeCache_Methods + IEmptyVolumeCache2_Methods
def Initialize(self, hkey, volume, flags):
# This should never be called, except on win98.
print("Unless we are on 98, Initialize call is unexpected!")
raise COMException(hresult=winerror.E_NOTIMPL)
def InitializeEx(self, hkey, volume, key_name, flags):
# Must return a tuple of:
# (display_name, description, button_name, flags)
print("InitializeEx called with", hkey, volume, key_name, flags)
self.volume = volume
if flags & shellcon.EVCF_SETTINGSMODE:
print("We are being run on a schedule")
# In this case, "because there is no opportunity for user
# feedback, only those files that are extremely safe to clean up
# should be touched. You should ignore the initialization
# method's pcwszVolume parameter and clean unneeded files
# regardless of what drive they are on."
self.volume = None # flag as 'any disk will do'
elif flags & shellcon.EVCF_OUTOFDISKSPACE:
# In this case, "the handler should be aggressive about deleting
# files, even if it results in a performance loss. However, the
# handler obviously should not delete files that would cause an
# application to fail or the user to lose data."
print("We are being run as we are out of disk-space")
else:
# This case is not documented - we are guessing :)
print("We are being run because the user asked")
# For the sake of demo etc, we tell the shell to only show us when
# there are > 0 bytes available. Our GetSpaceUsed will check the
# volume, so will return 0 when we are on a different disk
flags = shellcon.EVCF_DONTSHOWIFZERO | shellcon.EVCF_ENABLEBYDEFAULT
return (
"pywin32 compiled files",
"Removes all .pyc and .pyo files in the pywin32 directories",
"click me!",
flags,
)
def _GetDirectories(self):
root_dir = os.path.abspath(os.path.dirname(os.path.dirname(win32gui.__file__)))
if self.volume is not None and not root_dir.lower().startswith(
self.volume.lower()
):
return []
return [
os.path.join(root_dir, p)
for p in ("win32", "win32com", "win32comext", "isapi")
]
def _WalkCallback(self, arg, directory, files):
# callback function for os.path.walk - no need to be member, but it's
# close to the callers :)
callback, total_list = arg
for file in files:
fqn = os.path.join(directory, file).lower()
if file.endswith(".pyc") or file.endswith(".pyo"):
# See below - total_list is None means delete files,
# otherwise it is a list where the result is stored. It's a
# list simply due to the way os.walk works - only [0] is
# referenced
if total_list is None:
print("Deleting file", fqn)
# Should do callback.PurgeProcess - left as an exercise :)
os.remove(fqn)
else:
total_list[0] += os.stat(fqn)[stat.ST_SIZE]
# and callback to the tool
if callback:
# for the sake of seeing the progress bar do its thing,
# we take longer than we need to...
# ACK - for some bizarre reason this screws up the XP
# cleanup manager - clues welcome!! :)
# # print("Looking in", directory, ", but waiting a while...")
# # time.sleep(3)
# now do it
used = total_list[0]
callback.ScanProgress(used, 0, "Looking at " + fqn)
def GetSpaceUsed(self, callback):
total = [0] # See _WalkCallback above
try:
for d in self._GetDirectories():
os.path.walk(d, self._WalkCallback, (callback, total))
print("After looking in", d, "we have", total[0], "bytes")
except pythoncom.error as exc:
# This will be raised by the callback when the user selects 'cancel'.
if exc.hresult != winerror.E_ABORT:
raise # that's the documented error code!
print("User cancelled the operation")
return total[0]
def Purge(self, amt_to_free, callback):
print("Purging", amt_to_free, "bytes...")
# we ignore amt_to_free - it is generally what we returned for
# GetSpaceUsed
try:
for d in self._GetDirectories():
os.path.walk(d, self._WalkCallback, (callback, None))
except pythoncom.error as exc:
# This will be raised by the callback when the user selects 'cancel'.
if exc.hresult != winerror.E_ABORT:
raise # that's the documented error code!
print("User cancelled the operation")
def ShowProperties(self, hwnd):
raise COMException(hresult=winerror.E_NOTIMPL)
def Deactivate(self):
print("Deactivate called")
return 0
def DllRegisterServer():
# Also need to register specially in:
# HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches
# See link at top of file.
import winreg
kn = r"Software\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches\{}".format(
EmptyVolumeCache._reg_desc_,
)
key = winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, kn)
winreg.SetValueEx(key, None, 0, winreg.REG_SZ, EmptyVolumeCache._reg_clsid_)
def DllUnregisterServer():
import winreg
kn = r"Software\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches\{}".format(
EmptyVolumeCache._reg_desc_,
)
try:
key = winreg.DeleteKey(winreg.HKEY_LOCAL_MACHINE, kn)
except OSError as details:
import errno
if details.errno != errno.ENOENT:
raise
print(EmptyVolumeCache._reg_desc_, "unregistration complete.")
if __name__ == "__main__":
from win32com.server import register
register.UseCommandLine(
EmptyVolumeCache,
finalize_register=DllRegisterServer,
finalize_unregister=DllUnregisterServer,
)

View File

@@ -0,0 +1,866 @@
# This is a port of the Vista SDK "FolderView" sample, and associated
# notes at https://web.archive.org/web/20081225011615/http://shellrevealed.com/blogs/shellblog/archive/2007/03/15/Shell-Namespace-Extension_3A00_-Creating-and-Using-the-System-Folder-View-Object.aspx
# A key difference to shell_view.py is that this version uses the default
# IShellView provided by the shell (via SHCreateShellFolderView) rather
# than our own.
# XXX - sadly, it doesn't work quite like the original sample. Oh well,
# another day...
import os
import pickle
import random
import sys
import commctrl
import pythoncom
import win32api
import win32con
import win32gui
import winerror
from win32com.axcontrol import axcontrol # IObjectWithSite
from win32com.propsys import propsys
from win32com.server.exception import COMException
from win32com.server.util import NewEnum as _NewEnum, wrap as _wrap
from win32com.shell import shell, shellcon
GUID = pythoncom.MakeIID
# If set, output spews to the win32traceutil collector...
debug = 0
# wrap a python object in a COM pointer
def wrap(ob, iid=None):
return _wrap(ob, iid, useDispatcher=(debug > 0))
def NewEnum(seq, iid):
return _NewEnum(seq, iid=iid, useDispatcher=(debug > 0))
# The sample makes heavy use of "string ids" (ie, integer IDs defined in .h
# files, loaded at runtime from a (presumably localized) DLL. We cheat.
_sids = {} # strings, indexed bystring_id,
def LoadString(sid):
return _sids[sid]
# fn to create a unique string ID
_last_ids = 0
def _make_ids(s):
global _last_ids
_last_ids += 1
_sids[_last_ids] = s
return _last_ids
# These strings are what the user sees and would be localized.
# XXX - it's possible that the shell might persist these values, so
# this scheme wouldn't really be suitable in a real ap.
IDS_UNSPECIFIED = _make_ids("unspecified")
IDS_SMALL = _make_ids("small")
IDS_MEDIUM = _make_ids("medium")
IDS_LARGE = _make_ids("large")
IDS_CIRCLE = _make_ids("circle")
IDS_TRIANGLE = _make_ids("triangle")
IDS_RECTANGLE = _make_ids("rectangle")
IDS_POLYGON = _make_ids("polygon")
IDS_DISPLAY = _make_ids("Display")
IDS_DISPLAY_TT = _make_ids("Display the item.")
IDS_SETTINGS = _make_ids("Settings")
IDS_SETTING1 = _make_ids("Setting 1")
IDS_SETTING2 = _make_ids("Setting 2")
IDS_SETTING3 = _make_ids("Setting 3")
IDS_SETTINGS_TT = _make_ids("Modify settings.")
IDS_SETTING1_TT = _make_ids("Modify setting 1.")
IDS_SETTING2_TT = _make_ids("Modify setting 2.")
IDS_SETTING3_TT = _make_ids("Modify setting 3.")
IDS_LESSTHAN5 = _make_ids("Less Than 5")
IDS_5ORGREATER = _make_ids("Five or Greater")
del _make_ids, _last_ids
# Other misc resource stuff
IDI_ICON1 = 100
IDI_SETTINGS = 101
# The sample defines a number of "category ids". Each one gets
# its own GUID.
CAT_GUID_NAME = GUID("{de094c9d-c65a-11dc-ba21-005056c00008}")
CAT_GUID_SIZE = GUID("{de094c9e-c65a-11dc-ba21-005056c00008}")
CAT_GUID_SIDES = GUID("{de094c9f-c65a-11dc-ba21-005056c00008}")
CAT_GUID_LEVEL = GUID("{de094ca0-c65a-11dc-ba21-005056c00008}")
# The next category guid is NOT based on a column (see
# ViewCategoryProvider::EnumCategories()...)
CAT_GUID_VALUE = "{de094ca1-c65a-11dc-ba21-005056c00008}"
GUID_Display = GUID("{4d6c2fdd-c689-11dc-ba21-005056c00008}")
GUID_Settings = GUID("{4d6c2fde-c689-11dc-ba21-005056c00008}")
GUID_Setting1 = GUID("{4d6c2fdf-c689-11dc-ba21-005056c00008}")
GUID_Setting2 = GUID("{4d6c2fe0-c689-11dc-ba21-005056c00008}")
GUID_Setting3 = GUID("{4d6c2fe1-c689-11dc-ba21-005056c00008}")
# Hrm - not sure what to do about the std keys.
# Probably need a simple parser for propkey.h
PKEY_ItemNameDisplay = ("{B725F130-47EF-101A-A5F1-02608C9EEBAC}", 10)
PKEY_PropList_PreviewDetails = ("{C9944A21-A406-48FE-8225-AEC7E24C211B}", 8)
# Not sure what the "3" here refers to - docs say PID_FIRST_USABLE (2) be
# used. Presumably it is the 'propID' value in the .propdesc file!
# note that the following GUIDs are also references in the .propdesc file
PID_SOMETHING = 3
# These are our 'private' PKEYs
# Col 2, name="Sample.AreaSize"
PKEY_Sample_AreaSize = ("{d6f5e341-c65c-11dc-ba21-005056c00008}", PID_SOMETHING)
# Col 3, name="Sample.NumberOfSides"
PKEY_Sample_NumberOfSides = ("{d6f5e342-c65c-11dc-ba21-005056c00008}", PID_SOMETHING)
# Col 4, name="Sample.DirectoryLevel"
PKEY_Sample_DirectoryLevel = ("{d6f5e343-c65c-11dc-ba21-005056c00008}", PID_SOMETHING)
# We construct a PIDL from a pickle of a dict - turn it back into a
# dict (we should *never* be called with a PIDL that the last elt is not
# ours, so it is safe to assume we created it (assume->"ass" = "u" + "me" :)
def pidl_to_item(pidl):
# Note that only the *last* elt in the PIDL is certainly ours,
# but it contains everything we need encoded as a dict.
return pickle.loads(pidl[-1])
# Start of msdn sample port...
# make_item_enum replaces the sample's entire EnumIDList.cpp :)
def make_item_enum(level, flags):
pidls = []
nums = """zero one two three four five size seven eight nine ten""".split()
for i, name in enumerate(nums):
size = random.randint(0, 255)
sides = 1
while sides in [1, 2]:
sides = random.randint(0, 5)
is_folder = (i % 2) != 0
# check the flags say to include it.
# (This seems strange; if you ask the same folder for, but appear
skip = False
if not (flags & shellcon.SHCONTF_STORAGE):
if is_folder:
skip = not (flags & shellcon.SHCONTF_FOLDERS)
else:
skip = not (flags & shellcon.SHCONTF_NONFOLDERS)
if not skip:
data = {
"name": name,
"size": size,
"sides": sides,
"level": level,
"is_folder": is_folder,
}
pidls.append([pickle.dumps(data)])
return NewEnum(pidls, shell.IID_IEnumIDList)
# start of Utils.cpp port
def DisplayItem(shell_item_array, hwnd_parent=0):
# Get the first ShellItem and display its name
if shell_item_array is None:
msg = "You must select something!"
else:
si = shell_item_array.GetItemAt(0)
name = si.GetDisplayName(shellcon.SIGDN_NORMALDISPLAY)
msg = "%d items selected, first is %r" % (shell_item_array.GetCount(), name)
win32gui.MessageBox(hwnd_parent, msg, "Hello", win32con.MB_OK)
# end of Utils.cpp port
# start of sample's FVCommands.cpp port
class Command:
def __init__(self, guid, ids, ids_tt, idi, flags, callback, children):
self.guid = guid
self.ids = ids
self.ids_tt = ids_tt
self.idi = idi
self.flags = flags
self.callback = callback
self.children = children
assert not children or isinstance(children[0], Command)
def tuple(self):
return (
self.guid,
self.ids,
self.ids_tt,
self.idi,
self.flags,
self.callback,
self.children,
)
# command callbacks - called back directly by us - see ExplorerCommand.Invoke
def onDisplay(items, bindctx):
DisplayItem(items)
def onSetting1(items, bindctx):
win32gui.MessageBox(0, LoadString(IDS_SETTING1), "Hello", win32con.MB_OK)
def onSetting2(items, bindctx):
win32gui.MessageBox(0, LoadString(IDS_SETTING2), "Hello", win32con.MB_OK)
def onSetting3(items, bindctx):
win32gui.MessageBox(0, LoadString(IDS_SETTING3), "Hello", win32con.MB_OK)
taskSettings = [
Command(
GUID_Setting1, IDS_SETTING1, IDS_SETTING1_TT, IDI_SETTINGS, 0, onSetting1, None
),
Command(
GUID_Setting2, IDS_SETTING2, IDS_SETTING2_TT, IDI_SETTINGS, 0, onSetting2, None
),
Command(
GUID_Setting3, IDS_SETTING3, IDS_SETTING3_TT, IDI_SETTINGS, 0, onSetting3, None
),
]
tasks = [
Command(GUID_Display, IDS_DISPLAY, IDS_DISPLAY_TT, IDI_ICON1, 0, onDisplay, None),
Command(
GUID_Settings,
IDS_SETTINGS,
IDS_SETTINGS_TT,
IDI_SETTINGS,
shellcon.ECF_HASSUBCOMMANDS,
None,
taskSettings,
),
]
class ExplorerCommandProvider:
_com_interfaces_ = [shell.IID_IExplorerCommandProvider]
_public_methods_ = shellcon.IExplorerCommandProvider_Methods
def GetCommands(self, site, iid):
items = [wrap(ExplorerCommand(t)) for t in tasks]
return NewEnum(items, shell.IID_IEnumExplorerCommand)
class ExplorerCommand:
_com_interfaces_ = [shell.IID_IExplorerCommand]
_public_methods_ = shellcon.IExplorerCommand_Methods
def __init__(self, cmd):
self.cmd = cmd
# The sample also appears to ignore the pidl args!?
def GetTitle(self, pidl):
return LoadString(self.cmd.ids)
def GetToolTip(self, pidl):
return LoadString(self.cmd.ids_tt)
def GetIcon(self, pidl):
# Return a string of the usual "dll,resource_id" format
# todo - just return any ".ico that comes with python" + ",0" :)
raise COMException(hresult=winerror.E_NOTIMPL)
def GetState(self, shell_items, slow_ok):
return shellcon.ECS_ENABLED
def GetFlags(self):
return self.cmd.flags
def GetCanonicalName(self):
return self.cmd.guid
def Invoke(self, items, bind_ctx):
# If no function defined - just return S_OK
if self.cmd.callback:
self.cmd.callback(items, bind_ctx)
else:
print("No callback for command ", LoadString(self.cmd.ids))
def EnumSubCommands(self):
if not self.cmd.children:
return None
items = [wrap(ExplorerCommand(c)) for c in self.cmd.children]
return NewEnum(items, shell.IID_IEnumExplorerCommand)
# end of sample's FVCommands.cpp port
# start of sample's Category.cpp port
class FolderViewCategorizer:
_com_interfaces_ = [shell.IID_ICategorizer]
_public_methods_ = shellcon.ICategorizer_Methods
description = None # subclasses should set their own
def __init__(self, shell_folder):
self.sf = shell_folder
# Determines the relative order of two items in their item identifier lists.
def CompareCategory(self, flags, cat1, cat2):
return cat1 - cat2
# Retrieves the name of a categorizer, such as "Group By Device
# Type", that can be displayed in the user interface.
def GetDescription(self, cch):
return self.description
# Retrieves information about a category, such as the default
# display and the text to display in the user interface.
def GetCategoryInfo(self, catid):
# Note: this isn't always appropriate! See overrides below
return 0, str(catid) # ????
class FolderViewCategorizer_Name(FolderViewCategorizer):
description = "Alphabetical"
def GetCategory(self, pidls):
ret = []
for pidl in pidls:
val = self.sf.GetDetailsEx(pidl, PKEY_ItemNameDisplay)
ret.append(val)
return ret
class FolderViewCategorizer_Size(FolderViewCategorizer):
description = "Group By Size"
def GetCategory(self, pidls):
ret = []
for pidl in pidls:
# Why don't we just get the size of the PIDL?
val = self.sf.GetDetailsEx(pidl, PKEY_Sample_AreaSize)
val = int(val) # it probably came in a VT_BSTR variant
if val < 255 // 3:
cid = IDS_SMALL
elif val < 2 * 255 // 3:
cid = IDS_MEDIUM
else:
cid = IDS_LARGE
ret.append(cid)
return ret
def GetCategoryInfo(self, catid):
return 0, LoadString(catid)
class FolderViewCategorizer_Sides(FolderViewCategorizer):
description = "Group By Sides"
def GetCategory(self, pidls):
ret = []
for pidl in pidls:
val = self.sf.GetDetailsEx(pidl, PKEY_ItemNameDisplay)
if val == 0:
cid = IDS_CIRCLE
elif val == 3:
cid = IDS_TRIANGLE
elif val == 4:
cid = IDS_RECTANGLE
elif val == 5:
cid = IDS_POLYGON
else:
cid = IDS_UNSPECIFIED
ret.append(cid)
return ret
def GetCategoryInfo(self, catid):
return 0, LoadString(catid)
class FolderViewCategorizer_Value(FolderViewCategorizer):
description = "Group By Value"
def GetCategory(self, pidls):
ret = []
for pidl in pidls:
val = self.sf.GetDetailsEx(pidl, PKEY_ItemNameDisplay)
if val in "one two three four".split():
ret.append(IDS_LESSTHAN5)
else:
ret.append(IDS_5ORGREATER)
return ret
def GetCategoryInfo(self, catid):
return 0, LoadString(catid)
class FolderViewCategorizer_Level(FolderViewCategorizer):
description = "Group By Value"
def GetCategory(self, pidls):
return [
self.sf.GetDetailsEx(pidl, PKEY_Sample_DirectoryLevel) for pidl in pidls
]
class ViewCategoryProvider:
_com_interfaces_ = [shell.IID_ICategoryProvider]
_public_methods_ = shellcon.ICategoryProvider_Methods
def __init__(self, shell_folder):
self.shell_folder = shell_folder
def CanCategorizeOnSCID(self, pkey):
return pkey in [
PKEY_ItemNameDisplay,
PKEY_Sample_AreaSize,
PKEY_Sample_NumberOfSides,
PKEY_Sample_DirectoryLevel,
]
# Creates a category object.
def CreateCategory(self, guid, iid):
if iid == shell.IID_ICategorizer:
if guid == CAT_GUID_NAME:
klass = FolderViewCategorizer_Name
elif guid == CAT_GUID_SIDES:
klass = FolderViewCategorizer_Sides
elif guid == CAT_GUID_SIZE:
klass = FolderViewCategorizer_Size
elif guid == CAT_GUID_VALUE:
klass = FolderViewCategorizer_Value
elif guid == CAT_GUID_LEVEL:
klass = FolderViewCategorizer_Level
else:
raise COMException(hresult=winerror.E_INVALIDARG)
return wrap(klass(self.shell_folder))
raise COMException(hresult=winerror.E_NOINTERFACE)
# Retrieves the enumerator for the categories.
def EnumCategories(self):
# These are additional categories beyond the columns
seq = [CAT_GUID_VALUE]
return NewEnum(seq, pythoncom.IID_IEnumGUID)
# Retrieves a globally unique identifier (GUID) that represents
# the categorizer to use for the specified Shell column.
def GetCategoryForSCID(self, scid):
if scid == PKEY_ItemNameDisplay:
guid = CAT_GUID_NAME
elif scid == PKEY_Sample_AreaSize:
guid = CAT_GUID_SIZE
elif scid == PKEY_Sample_NumberOfSides:
guid = CAT_GUID_SIDES
elif scid == PKEY_Sample_DirectoryLevel:
guid = CAT_GUID_LEVEL
elif scid == pythoncom.IID_NULL:
# This can be called with a NULL
# format ID. This will happen if you have a category,
# not based on a column, that gets stored in the
# property bag. When a return is made to this item,
# it will call this function with a NULL format id.
guid = CAT_GUID_VALUE
else:
raise COMException(hresult=winerror.E_INVALIDARG)
return guid
# Retrieves the name of the specified category. This is where
# additional categories that appear under the column
# related categories in the UI, get their display names.
def GetCategoryName(self, guid, cch):
if guid == CAT_GUID_VALUE:
return "Value"
raise COMException(hresult=winerror.E_FAIL)
# Enables the folder to override the default grouping.
def GetDefaultCategory(self):
return CAT_GUID_LEVEL, (pythoncom.IID_NULL, 0)
# end of sample's Category.cpp port
# start of sample's ContextMenu.cpp port
MENUVERB_DISPLAY = 0
folderViewImplContextMenuIDs = [
(
"display",
MENUVERB_DISPLAY,
0,
),
]
class ContextMenu:
_reg_progid_ = "Python.ShellFolderSample.ContextMenu"
_reg_desc_ = "Python FolderView Context Menu"
_reg_clsid_ = "{fed40039-021f-4011-87c5-6188b9979764}"
_com_interfaces_ = [
shell.IID_IShellExtInit,
shell.IID_IContextMenu,
axcontrol.IID_IObjectWithSite,
]
_public_methods_ = (
shellcon.IContextMenu_Methods
+ shellcon.IShellExtInit_Methods
+ ["GetSite", "SetSite"]
)
_context_menu_type_ = "PythonFolderViewSampleType"
def __init__(self):
self.site = None
self.dataobj = None
def Initialize(self, folder, dataobj, hkey):
self.dataobj = dataobj
def QueryContextMenu(self, hMenu, indexMenu, idCmdFirst, idCmdLast, uFlags):
s = LoadString(IDS_DISPLAY)
win32gui.InsertMenu(
hMenu, indexMenu, win32con.MF_BYPOSITION, idCmdFirst + MENUVERB_DISPLAY, s
)
indexMenu += 1
# other verbs could go here...
# indicate that we added one verb.
return 1
def InvokeCommand(self, ci):
mask, hwnd, verb, params, dir, nShow, hotkey, hicon = ci
# this seems very convoluted, but it's what the sample does :)
for verb_name, verb_id, flag in folderViewImplContextMenuIDs:
if isinstance(verb, int):
matches = verb == verb_id
else:
matches = verb == verb_name
if matches:
break
else:
raise AssertionError(ci, "failed to find our ID")
if verb_id == MENUVERB_DISPLAY:
sia = shell.SHCreateShellItemArrayFromDataObject(self.dataobj)
DisplayItem(hwnd, sia)
else:
raise AssertionError(ci, "Got some verb we weren't expecting?")
def GetCommandString(self, cmd, typ):
raise COMException(hresult=winerror.E_NOTIMPL)
def SetSite(self, site):
self.site = site
def GetSite(self, iid):
return self.site
# end of sample's ContextMenu.cpp port
# start of sample's ShellFolder.cpp port
class ShellFolder:
_com_interfaces_ = [
shell.IID_IBrowserFrameOptions,
pythoncom.IID_IPersist,
shell.IID_IPersistFolder,
shell.IID_IPersistFolder2,
shell.IID_IShellFolder,
shell.IID_IShellFolder2,
]
_public_methods_ = (
shellcon.IBrowserFrame_Methods
+ shellcon.IPersistFolder2_Methods
+ shellcon.IShellFolder2_Methods
)
_reg_progid_ = "Python.ShellFolderSample.Folder2"
_reg_desc_ = "Python FolderView sample"
_reg_clsid_ = "{bb8c24ad-6aaa-4cec-ac5e-c429d5f57627}"
max_levels = 5
def __init__(self, level=0):
self.current_level = level
self.pidl = None # set when Initialize is called
def ParseDisplayName(self, hwnd, reserved, displayName, attr):
# print("ParseDisplayName", displayName)
raise COMException(hresult=winerror.E_NOTIMPL)
def EnumObjects(self, hwndOwner, flags):
if self.current_level >= self.max_levels:
return None
return make_item_enum(self.current_level + 1, flags)
def BindToObject(self, pidl, bc, iid):
tail = pidl_to_item(pidl)
# assert tail['is_folder'], "BindToObject should only be called on folders?"
# *sob*
# No point creating object just to have QI fail.
if iid not in ShellFolder._com_interfaces_:
raise COMException(hresult=winerror.E_NOTIMPL)
child = ShellFolder(self.current_level + 1)
# hrmph - not sure what multiple PIDLs here mean?
# assert len(pidl)==1, pidl # expecting just relative child PIDL
child.Initialize(self.pidl + pidl)
return wrap(child, iid)
def BindToStorage(self, pidl, bc, iid):
return self.BindToObject(pidl, bc, iid)
def CompareIDs(self, param, id1, id2):
return 0 # XXX - todo - implement this!
def CreateViewObject(self, hwnd, iid):
if iid == shell.IID_IShellView:
com_folder = wrap(self)
return shell.SHCreateShellFolderView(com_folder)
elif iid == shell.IID_ICategoryProvider:
return wrap(ViewCategoryProvider(self))
elif iid == shell.IID_IContextMenu:
ws = wrap(self)
dcm = (hwnd, None, self.pidl, ws, None)
return shell.SHCreateDefaultContextMenu(dcm, iid)
elif iid == shell.IID_IExplorerCommandProvider:
return wrap(ExplorerCommandProvider())
else:
raise COMException(hresult=winerror.E_NOINTERFACE)
def GetAttributesOf(self, pidls, attrFlags):
assert len(pidls) == 1, "sample only expects 1 too!"
assert len(pidls[0]) == 1, "expect relative pidls!"
item = pidl_to_item(pidls[0])
flags = 0
if item["is_folder"]:
flags |= shellcon.SFGAO_FOLDER
if item["level"] < self.max_levels:
flags |= shellcon.SFGAO_HASSUBFOLDER
return flags
# Retrieves an OLE interface that can be used to carry out
# actions on the specified file objects or folders.
def GetUIObjectOf(self, hwndOwner, pidls, iid, inout):
assert len(pidls) == 1, "oops - aren't expecting more than one!"
assert len(pidls[0]) == 1, "assuming relative pidls!"
item = pidl_to_item(pidls[0])
if iid == shell.IID_IContextMenu:
ws = wrap(self)
dcm = (hwndOwner, None, self.pidl, ws, pidls)
return shell.SHCreateDefaultContextMenu(dcm, iid)
elif iid == shell.IID_IExtractIconW:
dxi = shell.SHCreateDefaultExtractIcon()
# dxi is IDefaultExtractIconInit
if item["is_folder"]:
dxi.SetNormalIcon("shell32.dll", 4)
else:
dxi.SetNormalIcon("shell32.dll", 1)
# just return the dxi - let Python QI for IID_IExtractIconW
return dxi
elif iid == pythoncom.IID_IDataObject:
return shell.SHCreateDataObject(self.pidl, pidls, None, iid)
elif iid == shell.IID_IQueryAssociations:
elts = []
if item["is_folder"]:
elts.append((shellcon.ASSOCCLASS_FOLDER, None, None))
elts.append(
(shellcon.ASSOCCLASS_PROGID_STR, None, ContextMenu._context_menu_type_)
)
return shell.AssocCreateForClasses(elts, iid)
raise COMException(hresult=winerror.E_NOINTERFACE)
# Retrieves the display name for the specified file object or subfolder.
def GetDisplayNameOf(self, pidl, flags):
item = pidl_to_item(pidl)
if flags & shellcon.SHGDN_FORPARSING:
if flags & shellcon.SHGDN_INFOLDER:
return item["name"]
else:
if flags & shellcon.SHGDN_FORADDRESSBAR:
sigdn = shellcon.SIGDN_DESKTOPABSOLUTEEDITING
else:
sigdn = shellcon.SIGDN_DESKTOPABSOLUTEPARSING
parent = shell.SHGetNameFromIDList(self.pidl, sigdn)
return parent + "\\" + item["name"]
else:
return item["name"]
def SetNameOf(self, hwndOwner, pidl, new_name, flags):
raise COMException(hresult=winerror.E_NOTIMPL)
def GetClassID(self):
return self._reg_clsid_
# IPersistFolder method
def Initialize(self, pidl):
self.pidl = pidl
# IShellFolder2 methods
def EnumSearches(self):
raise COMException(hresult=winerror.E_NOINTERFACE)
# Retrieves the default sorting and display columns.
def GetDefaultColumn(self, dwres):
# result is (sort, display)
return 0, 0
# Retrieves the default state for a specified column.
def GetDefaultColumnState(self, iCol):
if iCol < 3:
return shellcon.SHCOLSTATE_ONBYDEFAULT | shellcon.SHCOLSTATE_TYPE_STR
raise COMException(hresult=winerror.E_INVALIDARG)
# Requests the GUID of the default search object for the folder.
def GetDefaultSearchGUID(self):
raise COMException(hresult=winerror.E_NOTIMPL)
# Helper function for getting the display name for a column.
def _GetColumnDisplayName(self, pidl, pkey):
item = pidl_to_item(pidl)
is_folder = item["is_folder"]
if pkey == PKEY_ItemNameDisplay:
val = item["name"]
elif pkey == PKEY_Sample_AreaSize and not is_folder:
val = "%d Sq. Ft." % item["size"]
elif pkey == PKEY_Sample_NumberOfSides and not is_folder:
val = str(item["sides"]) # not sure why str()
elif pkey == PKEY_Sample_DirectoryLevel:
val = str(item["level"])
else:
val = ""
return val
# Retrieves detailed information, identified by a
# property set ID (FMTID) and property ID (PID),
# on an item in a Shell folder.
def GetDetailsEx(self, pidl, pkey):
item = pidl_to_item(pidl)
is_folder = item["is_folder"]
if not is_folder and pkey == PKEY_PropList_PreviewDetails:
return "prop:Sample.AreaSize;Sample.NumberOfSides;Sample.DirectoryLevel"
return self._GetColumnDisplayName(pidl, pkey)
# Retrieves detailed information, identified by a
# column index, on an item in a Shell folder.
def GetDetailsOf(self, pidl, iCol):
key = self.MapColumnToSCID(iCol)
if pidl is None:
data = [
(commctrl.LVCFMT_LEFT, "Name"),
(commctrl.LVCFMT_CENTER, "Size"),
(commctrl.LVCFMT_CENTER, "Sides"),
(commctrl.LVCFMT_CENTER, "Level"),
]
if iCol >= len(data):
raise COMException(hresult=winerror.E_FAIL)
fmt, val = data[iCol]
else:
fmt = 0 # ?
val = self._GetColumnDisplayName(pidl, key)
cxChar = 24
return fmt, cxChar, val
# Converts a column name to the appropriate
# property set ID (FMTID) and property ID (PID).
def MapColumnToSCID(self, iCol):
data = [
PKEY_ItemNameDisplay,
PKEY_Sample_AreaSize,
PKEY_Sample_NumberOfSides,
PKEY_Sample_DirectoryLevel,
]
if iCol >= len(data):
raise COMException(hresult=winerror.E_FAIL)
return data[iCol]
# IPersistFolder2 methods
# Retrieves the PIDLIST_ABSOLUTE for the folder object.
def GetCurFolder(self):
# The docs say this is OK, but I suspect it's a problem in this case :)
# assert self.pidl, "haven't been initialized?"
return self.pidl
# end of sample's ShellFolder.cpp port
def get_schema_fname():
me = win32api.GetFullPathName(__file__)
sc = os.path.splitext(me)[0] + ".propdesc"
assert os.path.isfile(sc), sc
return sc
def DllRegisterServer():
import winreg
if sys.getwindowsversion()[0] < 6:
print("This sample only works on Vista")
sys.exit(1)
key = winreg.CreateKey(
winreg.HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\"
"Explorer\\Desktop\\Namespace\\" + ShellFolder._reg_clsid_,
)
winreg.SetValueEx(key, None, 0, winreg.REG_SZ, ShellFolder._reg_desc_)
# And special shell keys under our CLSID
key = winreg.CreateKey(
winreg.HKEY_CLASSES_ROOT, "CLSID\\" + ShellFolder._reg_clsid_ + "\\ShellFolder"
)
# 'Attributes' is an int stored as a binary! use struct
attr = (
shellcon.SFGAO_FOLDER | shellcon.SFGAO_HASSUBFOLDER | shellcon.SFGAO_BROWSABLE
)
import struct
s = struct.pack("i", attr)
winreg.SetValueEx(key, "Attributes", 0, winreg.REG_BINARY, s)
# register the context menu handler under the FolderViewSampleType type.
keypath = "{}\\shellex\\ContextMenuHandlers\\{}".format(
ContextMenu._context_menu_type_,
ContextMenu._reg_desc_,
)
key = winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, keypath)
winreg.SetValueEx(key, None, 0, winreg.REG_SZ, ContextMenu._reg_clsid_)
propsys.PSRegisterPropertySchema(get_schema_fname())
print(ShellFolder._reg_desc_, "registration complete.")
def DllUnregisterServer():
import winreg
paths = [
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Desktop\\Namespace\\"
+ ShellFolder._reg_clsid_,
"{}\\shellex\\ContextMenuHandlers\\{}".format(
ContextMenu._context_menu_type_, ContextMenu._reg_desc_
),
]
for path in paths:
try:
winreg.DeleteKey(winreg.HKEY_LOCAL_MACHINE, path)
except OSError as details:
import errno
if details.errno != errno.ENOENT:
print(f"FAILED to remove {path}: {details}")
propsys.PSUnregisterPropertySchema(get_schema_fname())
print(ShellFolder._reg_desc_, "unregistration complete.")
if __name__ == "__main__":
from win32com.server import register
register.UseCommandLine(
ShellFolder,
ContextMenu,
debug=debug,
finalize_register=DllRegisterServer,
finalize_unregister=DllUnregisterServer,
)

View File

@@ -0,0 +1,81 @@
# A sample icon handler. Sets the icon for Python files to a random
# ICO file. ICO files are found in the Python directory - generally there will
# be 3 icons found.
#
# To demostrate:
# * Execute this script to register the context menu.
# * Open Windows Explorer, and browse to a directory with a .py file.
# * Note the pretty, random selection of icons!
# Use glob to locate ico files, and random.choice to pick one.
import glob
import os
import random
import sys
import pythoncom
import winerror
from win32com.shell import shell
ico_files = glob.glob(os.path.join(sys.prefix, "*.ico"))
if not ico_files:
ico_files = glob.glob(os.path.join(sys.prefix, "PC", "*.ico"))
if not ico_files:
print("WARNING: Can't find any icon files")
# Our shell extension.
IExtractIcon_Methods = "Extract GetIconLocation".split()
IPersistFile_Methods = "IsDirty Load Save SaveCompleted GetCurFile".split()
class ShellExtension:
_reg_progid_ = "Python.ShellExtension.IconHandler"
_reg_desc_ = "Python Sample Shell Extension (icon handler)"
_reg_clsid_ = "{a97e32d7-3b78-448c-b341-418120ea9227}"
_com_interfaces_ = [shell.IID_IExtractIcon, pythoncom.IID_IPersistFile]
_public_methods_ = IExtractIcon_Methods + IPersistFile_Methods
def Load(self, filename, mode):
self.filename = filename
self.mode = mode
def GetIconLocation(self, flags):
# note - returning a single int will set the HRESULT (eg, S_FALSE,
# E_PENDING - see MS docs for details.
return random.choice(ico_files), 0, 0
def Extract(self, fname, index, size):
return winerror.S_FALSE
def DllRegisterServer():
import winreg
key = winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, "Python.File\\shellex")
subkey = winreg.CreateKey(key, "IconHandler")
winreg.SetValueEx(subkey, None, 0, winreg.REG_SZ, ShellExtension._reg_clsid_)
print(ShellExtension._reg_desc_, "registration complete.")
def DllUnregisterServer():
import winreg
try:
key = winreg.DeleteKey(
winreg.HKEY_CLASSES_ROOT, "Python.File\\shellex\\IconHandler"
)
except OSError as 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,
)

View File

@@ -0,0 +1,971 @@
# A sample shell namespace view
# To demostrate:
# * Execute this script to register the namespace.
# * Open Windows Explorer, and locate the new "Python Path Shell Browser"
# folder off "My Computer"
# * Browse this tree - .py files are shown expandable, with classes and
# methods selectable. Selecting a Python file, or a class/method, will
# display the file using Scintilla.
# Known problems:
# * Classes and methods don't have icons - this is a demo, so we keep it small
# See icon_handler.py for examples of how to work with icons.
#
#
# Notes on PIDLs
# PIDLS are complicated, but fairly well documented in MSDN. If you need to
# do much with these shell extensions, you must understand their concept.
# Here is a short-course, as it applies to this sample:
# A PIDL identifies an item, much in the same way that a filename does
# (however, the shell is not limited to displaying "files").
# An "ItemID" is a single string, each being an item in the hierarchy.
# A "PIDL" is a list of these strings.
# All shell etc functions work with PIDLs, so even in the case where
# an ItemID is conceptually used, a 1-item list is always used.
# Conceptually, think of:
# pidl = pathname.split("\\") # pidl is a list of "parent" items.
# # each item is a string 'item id', but these are ever used directly
# As there is no concept of passing a single item, to open a file using only
# a relative filename, conceptually you would say:
# open_file([filename]) # Pass a single-itemed relative "PIDL"
# and continuing the analogy, a "listdir" type function would return a list
# of single-itemed lists - each list containing the relative PIDL of the child.
#
# Each PIDL entry is a binary string, and may contain any character. For
# PIDLs not created by you, they can not be interpreted - they are just
# blobs. PIDLs created by you (ie, children of your IShellFolder) can
# store and interpret the string however makes most sense for your application.
# (but within PIDL rules - they must be persistable, etc)
# There is no reason that pickled strings, for example, couldn't be used
# as an EntryID.
# This application takes a simple approach - each PIDL is a string of form
# "directory\0directory_name", "file\0file_name" or
# "object\0file_name\0class_name[.method_name"
# The first string in each example is literal (ie, the word 'directory',
# 'file' or 'object', and every other string is variable. We use '\0' as
# a field sep just 'cos we can (and 'cos it can't possibly conflict with the
# string content)
import os
import pyclbr
import sys
import commctrl
import pythoncom
import win32api
import win32con
import win32gui
import win32gui_struct
import winerror
from pywin.scintilla import scintillacon
from win32com.server.exception import COMException
from win32com.server.util import NewEnum, wrap
from win32com.shell import shell, shellcon
from win32com.util import IIDToInterfaceName
# Set this to 1 to cause debug version to be registered and used. A debug
# version will spew output to win32traceutil.
debug = 0
if debug:
import win32traceutil
# markh is toying with an implementation that allows auto reload of a module
# if this attribute exists.
com_auto_reload = True
# Helper function to get a system IShellFolder interface, and the PIDL within
# that folder for an existing file/directory.
def GetFolderAndPIDLForPath(filename):
desktop = shell.SHGetDesktopFolder()
info = desktop.ParseDisplayName(0, None, os.path.abspath(filename))
cchEaten, pidl, attr = info
# We must walk the ID list, looking for one child at a time.
folder = desktop
while len(pidl) > 1:
this = pidl.pop(0)
folder = folder.BindToObject([this], None, shell.IID_IShellFolder)
# We are left with the pidl for the specific item. Leave it as
# a list, so it remains a valid PIDL.
return folder, pidl
# A cache of pyclbr module objects, so we only parse a given filename once.
clbr_modules = {} # Indexed by path, item is dict as returned from pyclbr
def get_clbr_for_file(path):
try:
objects = clbr_modules[path]
except KeyError:
dir, filename = os.path.split(path)
base, ext = os.path.splitext(filename)
objects = pyclbr.readmodule_ex(base, [dir])
clbr_modules[path] = objects
return objects
# Our COM interfaces.
# Base class for a shell folder.
# All child classes use a simple PIDL of the form:
# "object_type\0object_name[\0extra ...]"
class ShellFolderBase:
_com_interfaces_ = [
shell.IID_IBrowserFrameOptions,
pythoncom.IID_IPersist,
shell.IID_IPersistFolder,
shell.IID_IShellFolder,
]
_public_methods_ = (
shellcon.IBrowserFrame_Methods
+ shellcon.IPersistFolder_Methods
+ shellcon.IShellFolder_Methods
)
def GetFrameOptions(self, mask):
# print("GetFrameOptions", self, mask)
return 0
def ParseDisplayName(self, hwnd, reserved, displayName, attr):
print("ParseDisplayName", displayName)
# return cchEaten, pidl, attr
def BindToStorage(self, pidl, bc, iid):
print("BTS", iid, IIDToInterfaceName(iid))
def BindToObject(self, pidl, bc, iid):
# We may be passed a set of relative PIDLs here - ie
# [pidl_of_dir, pidl_of_child_dir, pidl_of_file, pidl_of_function]
# But each of our PIDLs keeps the fully qualified name anyway - so
# just jump directly to the last.
final_pidl = pidl[-1]
typ, extra = final_pidl.split("\0", 1)
if typ == "directory":
klass = ShellFolderDirectory
elif typ == "file":
klass = ShellFolderFile
elif typ == "object":
klass = ShellFolderObject
else:
raise RuntimeError(f"What is {typ!r}")
ret = wrap(klass(extra), iid, useDispatcher=(debug > 0))
return ret
# A ShellFolder for an object with CHILDREN on the file system
# Note that this means our "File" folder is *not* a 'FileSystem' folder,
# as it's children (functions and classes) are not on the file system.
#
class ShellFolderFileSystem(ShellFolderBase):
def _GetFolderAndPIDLForPIDL(self, my_idl):
typ, name = my_idl[0].split("\0")
return GetFolderAndPIDLForPath(name)
# Interface methods
def CompareIDs(self, param, id1, id2):
if id1 < id2:
return -1
if id1 == id2:
return 0
return 1
def GetUIObjectOf(self, hwndOwner, pidls, iid, inout):
# delegate to the shell.
assert len(pidls) == 1, "oops - aren't expecting more than one!"
pidl = pidls[0]
folder, child_pidl = self._GetFolderAndPIDLForPIDL(pidl)
try:
inout, ret = folder.GetUIObjectOf(hwndOwner, [child_pidl], iid, inout, iid)
except pythoncom.com_error as exc:
raise COMException(hresult=exc.hresult)
return inout, ret
# return object of IID
def GetDisplayNameOf(self, pidl, flags):
# delegate to the shell.
folder, child_pidl = self._GetFolderAndPIDLForPIDL(pidl)
ret = folder.GetDisplayNameOf(child_pidl, flags)
return ret
def GetAttributesOf(self, pidls, attrFlags):
ret_flags = -1
for pidl in pidls:
pidl = pidl[0] # ??
typ, name = pidl.split("\0")
flags = shellcon.SHGFI_ATTRIBUTES
rc, info = shell.SHGetFileInfo(name, 0, flags)
hIcon, iIcon, dwAttr, name, typeName = info
# All our items, even files, have sub-items
extras = (
shellcon.SFGAO_HASSUBFOLDER
| shellcon.SFGAO_FOLDER
| shellcon.SFGAO_FILESYSANCESTOR
| shellcon.SFGAO_BROWSABLE
)
ret_flags &= dwAttr | extras
return ret_flags
class ShellFolderDirectory(ShellFolderFileSystem):
def __init__(self, path):
self.path = os.path.abspath(path)
def CreateViewObject(self, hwnd, iid):
# delegate to the shell.
folder, child_pidl = GetFolderAndPIDLForPath(self.path)
return folder.CreateViewObject(hwnd, iid)
def EnumObjects(self, hwndOwner, flags):
pidls = []
for fname in os.listdir(self.path):
fqn = os.path.join(self.path, fname)
if os.path.isdir(fqn):
type_name = "directory"
type_class = ShellFolderDirectory
else:
base, ext = os.path.splitext(fname)
if ext in [".py", ".pyw"]:
type_class = ShellFolderFile
type_name = "file"
else:
type_class = None
if type_class is not None:
pidls.append([type_name + "\0" + fqn])
return NewEnum(pidls, iid=shell.IID_IEnumIDList, useDispatcher=(debug > 0))
def GetDisplayNameOf(self, pidl, flags):
final_pidl = pidl[-1]
full_fname = final_pidl.split("\0")[-1]
return os.path.split(full_fname)[-1]
def GetAttributesOf(self, pidls, attrFlags):
return (
shellcon.SFGAO_HASSUBFOLDER
| shellcon.SFGAO_FOLDER
| shellcon.SFGAO_FILESYSANCESTOR
| shellcon.SFGAO_BROWSABLE
)
# As per comments above, even though this manages a file, it is *not* a
# ShellFolderFileSystem, as the children are not on the file system.
class ShellFolderFile(ShellFolderBase):
def __init__(self, path):
self.path = os.path.abspath(path)
def EnumObjects(self, hwndOwner, flags):
objects = get_clbr_for_file(self.path)
pidls = []
for name in objects:
pidls.append(["object\0" + self.path + "\0" + name])
return NewEnum(pidls, iid=shell.IID_IEnumIDList, useDispatcher=(debug > 0))
def GetAttributesOf(self, pidls, attrFlags):
ret_flags = -1
for pidl in pidls:
assert len(pidl) == 1, "Expecting relative pidls"
pidl = pidl[0]
typ, filename, obname = pidl.split("\0")
obs = get_clbr_for_file(filename)
ob = obs[obname]
flags = (
shellcon.SFGAO_BROWSABLE
| shellcon.SFGAO_FOLDER
| shellcon.SFGAO_FILESYSANCESTOR
)
if hasattr(ob, "methods"):
flags |= shellcon.SFGAO_HASSUBFOLDER
ret_flags &= flags
return ret_flags
def GetDisplayNameOf(self, pidl, flags):
assert len(pidl) == 1, "Expecting relative PIDL"
typ, fname, obname = pidl[0].split("\0")
fqname = os.path.splitext(fname)[0] + "." + obname
if flags & shellcon.SHGDN_INFOLDER:
ret = obname
else: # SHGDN_NORMAL is the default
ret = fqname
# No need to look at the SHGDN_FOR* modifiers.
return ret
def CreateViewObject(self, hwnd, iid):
return wrap(ScintillaShellView(hwnd, self.path), iid, useDispatcher=debug > 0)
# A ShellFolder for our Python objects
class ShellFolderObject(ShellFolderBase):
def __init__(self, details):
self.path, details = details.split("\0")
if details.find(".") > 0:
self.class_name, self.method_name = details.split(".")
else:
self.class_name = details
self.method_name = None
def CreateViewObject(self, hwnd, iid):
mod_objects = get_clbr_for_file(self.path)
object = mod_objects[self.class_name]
if self.method_name is None:
lineno = object.lineno
else:
lineno = object.methods[self.method_name]
return wrap(
ScintillaShellView(hwnd, self.path, lineno),
iid,
useDispatcher=debug > 0,
)
def EnumObjects(self, hwndOwner, flags):
assert self.method_name is None, "Should not be enuming methods!"
mod_objects = get_clbr_for_file(self.path)
my_objects = mod_objects[self.class_name]
pidls = []
for func_name in my_objects.methods:
pidl = ["object\0" + self.path + "\0" + self.class_name + "." + func_name]
pidls.append(pidl)
return NewEnum(pidls, iid=shell.IID_IEnumIDList, useDispatcher=(debug > 0))
def GetDisplayNameOf(self, pidl, flags):
assert len(pidl) == 1, "Expecting relative PIDL"
typ, fname, obname = pidl[0].split("\0")
class_name, method_name = obname.split(".")
fqname = os.path.splitext(fname)[0] + "." + obname
if flags & shellcon.SHGDN_INFOLDER:
ret = method_name
else: # SHGDN_NORMAL is the default
ret = fqname
# No need to look at the SHGDN_FOR* modifiers.
return ret
def GetAttributesOf(self, pidls, attrFlags):
ret_flags = -1
for pidl in pidls:
assert len(pidl) == 1, "Expecting relative pidls"
flags = (
shellcon.SFGAO_BROWSABLE
| shellcon.SFGAO_FOLDER
| shellcon.SFGAO_FILESYSANCESTOR
)
ret_flags &= flags
return ret_flags
# The "Root" folder of our namespace. As all children are directories,
# it is derived from ShellFolderFileSystem
# This is the only COM object actually registered and externally created.
class ShellFolderRoot(ShellFolderFileSystem):
_reg_progid_ = "Python.ShellExtension.Folder"
_reg_desc_ = "Python Path Shell Browser"
_reg_clsid_ = "{f6287035-3074-4cb5-a8a6-d3c80e206944}"
def GetClassID(self):
return self._reg_clsid_
def Initialize(self, pidl):
# This is the PIDL of us, as created by the shell. This is our
# top-level ID. All other items under us have PIDLs defined
# by us - see the notes at the top of the file.
# print("Initialize called with pidl={pidl!r}")
self.pidl = pidl
def CreateViewObject(self, hwnd, iid):
return wrap(FileSystemView(self, hwnd), iid, useDispatcher=debug > 0)
def EnumObjects(self, hwndOwner, flags):
items = [["directory\0" + p] for p in sys.path if os.path.isdir(p)]
return NewEnum(items, iid=shell.IID_IEnumIDList, useDispatcher=(debug > 0))
def GetDisplayNameOf(self, pidl, flags):
## return full path for sys.path dirs, since they don't appear under a parent folder
final_pidl = pidl[-1]
display_name = final_pidl.split("\0")[-1]
return display_name
# Simple shell view implementations
# Uses a builtin listview control to display simple lists of directories
# or filenames.
class FileSystemView:
_public_methods_ = shellcon.IShellView_Methods
_com_interfaces_ = [
pythoncom.IID_IOleWindow,
shell.IID_IShellView,
]
def __init__(self, folder, hwnd):
self.hwnd_parent = hwnd # provided by explorer.
self.hwnd = None # intermediate window for catching command notifications.
self.hwnd_child = None # our ListView
self.activate_state = None
self.hmenu = None
self.browser = None
self.folder = folder
self.children = None
# IOleWindow
def GetWindow(self):
return self.hwnd
def ContextSensitiveHelp(self, enter_mode):
raise COMException(hresult=winerror.E_NOTIMPL)
# IShellView
def CreateViewWindow(self, prev, settings, browser, rect):
print("FileSystemView.CreateViewWindow", prev, settings, browser, rect)
self.cur_foldersettings = settings
self.browser = browser
self._CreateMainWindow(prev, settings, browser, rect)
self._CreateChildWindow(prev)
# This isn't part of the sample, but the most convenient place to
# test/demonstrate how you can get an IShellBrowser from a HWND
# (but ONLY when you are in the same process as the IShellBrowser!)
# Obviously it is not necessary here - we already have the browser!
browser_ad = win32gui.SendMessage(self.hwnd_parent, win32con.WM_USER + 7, 0, 0)
browser_ob = pythoncom.ObjectFromAddress(browser_ad, shell.IID_IShellBrowser)
assert browser == browser_ob
# and make a call on the object to prove it doesn't die :)
assert browser.QueryActiveShellView() == browser_ob.QueryActiveShellView()
def _CreateMainWindow(self, prev, settings, browser, rect):
# Creates a parent window that hosts the view window. This window
# gets the control notifications etc sent from the child.
style = win32con.WS_CHILD | win32con.WS_VISIBLE #
wclass_name = "ShellViewDemo_DefView"
# Register the Window class.
wc = win32gui.WNDCLASS()
wc.hInstance = win32gui.dllhandle
wc.lpszClassName = wclass_name
wc.style = win32con.CS_VREDRAW | win32con.CS_HREDRAW
try:
win32gui.RegisterClass(wc)
except win32gui.error as details:
# Should only happen when this module is reloaded
if details[0] != winerror.ERROR_CLASS_ALREADY_EXISTS:
raise
message_map = {
win32con.WM_DESTROY: self.OnDestroy,
win32con.WM_COMMAND: self.OnCommand,
win32con.WM_NOTIFY: self.OnNotify,
win32con.WM_CONTEXTMENU: self.OnContextMenu,
win32con.WM_SIZE: self.OnSize,
}
self.hwnd = win32gui.CreateWindow(
wclass_name,
"",
style,
rect[0],
rect[1],
rect[2] - rect[0],
rect[3] - rect[1],
self.hwnd_parent,
0,
win32gui.dllhandle,
None,
)
win32gui.SetWindowLong(self.hwnd, win32con.GWL_WNDPROC, message_map)
print("View 's hwnd is", self.hwnd)
return self.hwnd
def _CreateChildWindow(self, prev):
# Creates the list view window.
assert self.hwnd_child is None, "already have a window"
assert self.cur_foldersettings is not None, "no settings"
style = (
win32con.WS_CHILD
| win32con.WS_VISIBLE
| win32con.WS_BORDER
| commctrl.LVS_SHAREIMAGELISTS
| commctrl.LVS_EDITLABELS
)
view_mode, view_flags = self.cur_foldersettings
if view_mode == shellcon.FVM_ICON:
style |= commctrl.LVS_ICON | commctrl.LVS_AUTOARRANGE
elif view_mode == shellcon.FVM_SMALLICON:
style |= commctrl.LVS_SMALLICON | commctrl.LVS_AUTOARRANGE
elif view_mode == shellcon.FVM_LIST:
style |= commctrl.LVS_LIST | commctrl.LVS_AUTOARRANGE
elif view_mode == shellcon.FVM_DETAILS:
style |= commctrl.LVS_REPORT | commctrl.LVS_AUTOARRANGE
else:
# XP 'thumbnails' etc
view_mode = shellcon.FVM_DETAILS
# Default to 'report'
style |= commctrl.LVS_REPORT | commctrl.LVS_AUTOARRANGE
for f_flag, l_flag in [
(shellcon.FWF_SINGLESEL, commctrl.LVS_SINGLESEL),
(shellcon.FWF_ALIGNLEFT, commctrl.LVS_ALIGNLEFT),
(shellcon.FWF_SHOWSELALWAYS, commctrl.LVS_SHOWSELALWAYS),
]:
if view_flags & f_flag:
style |= l_flag
self.hwnd_child = win32gui.CreateWindowEx(
win32con.WS_EX_CLIENTEDGE,
"SysListView32",
None,
style,
0,
0,
0,
0,
self.hwnd,
1000,
0,
None,
)
cr = win32gui.GetClientRect(self.hwnd)
win32gui.MoveWindow(self.hwnd_child, 0, 0, cr[2] - cr[0], cr[3] - cr[1], True)
# Setup the columns for the view.
lvc, extras = win32gui_struct.PackLVCOLUMN(
fmt=commctrl.LVCFMT_LEFT, subItem=1, text="Name", cx=300
)
win32gui.SendMessage(self.hwnd_child, commctrl.LVM_INSERTCOLUMN, 0, lvc)
lvc, extras = win32gui_struct.PackLVCOLUMN(
fmt=commctrl.LVCFMT_RIGHT, subItem=1, text="Exists", cx=50
)
win32gui.SendMessage(self.hwnd_child, commctrl.LVM_INSERTCOLUMN, 1, lvc)
# and fill it with the content
self.Refresh()
def GetCurrentInfo(self):
return self.cur_foldersettings
def UIActivate(self, activate_state):
print("OnActivate")
def _OnActivate(self, activate_state):
if self.activate_state == activate_state:
return
self._OnDeactivate() # restore menu's first, if necessary.
if activate_state != shellcon.SVUIA_DEACTIVATE:
assert self.hmenu is None, "Should have destroyed it!"
self.hmenu = win32gui.CreateMenu()
widths = 0, 0, 0, 0, 0, 0
# Ask explorer to add its standard items.
self.browser.InsertMenusSB(self.hmenu, widths)
# Merge with these standard items
self._MergeMenus(activate_state)
self.browser.SetMenuSB(self.hmenu, 0, self.hwnd)
self.activate_state = activate_state
def _OnDeactivate(self):
if self.browser is not None and self.hmenu is not None:
self.browser.SetMenuSB(0, 0, 0)
self.browser.RemoveMenusSB(self.hmenu)
win32gui.DestroyMenu(self.hmenu)
self.hmenu = None
self.hsubmenus = None
self.activate_state = shellcon.SVUIA_DEACTIVATE
def _MergeMenus(self, activate_state):
# Merge the operations we support into the top-level menus.
# NOTE: This function it *not* called each time the selection changes.
# SVUIA_ACTIVATE_FOCUS really means "have a selection?"
have_sel = activate_state == shellcon.SVUIA_ACTIVATE_FOCUS
# only do "file" menu here, and only 1 item on it!
mid = shellcon.FCIDM_MENU_FILE
# Get the hmenu for the menu
buf, extras = win32gui_struct.EmptyMENUITEMINFO(win32con.MIIM_SUBMENU)
win32gui.GetMenuItemInfo(self.hmenu, mid, False, buf)
data = win32gui_struct.UnpackMENUITEMINFO(buf)
submenu = data[3]
print("Do someting with the file menu!")
def Refresh(self):
stateMask = commctrl.LVIS_SELECTED | commctrl.LVIS_DROPHILITED
state = 0
self.children = []
# Enumerate and store the child PIDLs
for cid in self.folder.EnumObjects(self.hwnd, 0):
self.children.append(cid)
for row_index, data in enumerate(self.children):
assert len(data) == 1, "expecting just a child PIDL"
typ, path = data[0].split("\0")
desc = os.path.exists(path) and "Yes" or "No"
prop_vals = (path, desc)
# first col
data, extras = win32gui_struct.PackLVITEM(
item=row_index,
subItem=0,
text=prop_vals[0],
state=state,
stateMask=stateMask,
)
win32gui.SendMessage(
self.hwnd_child, commctrl.LVM_INSERTITEM, row_index, data
)
# rest of the cols.
col_index = 1
for prop_val in prop_vals[1:]:
data, extras = win32gui_struct.PackLVITEM(
item=row_index, subItem=col_index, text=prop_val
)
win32gui.SendMessage(self.hwnd_child, commctrl.LVM_SETITEM, 0, data)
col_index += 1
def SelectItem(self, pidl, flag):
# For the sake of brevity, we don't implement this yet.
# You would need to locate the index of the item in the shell-view
# with that PIDL, then ask the list-view to select it.
print("Please implement SelectItem for PIDL", pidl)
def GetItemObject(self, item_num, iid):
raise COMException(hresult=winerror.E_NOTIMPL)
def TranslateAccelerator(self, msg):
return winerror.S_FALSE
def DestroyViewWindow(self):
win32gui.DestroyWindow(self.hwnd)
self.hwnd = None
print("Destroyed view window")
# Message handlers.
def OnDestroy(self, hwnd, msg, wparam, lparam):
print("OnDestory")
def OnCommand(self, hwnd, msg, wparam, lparam):
print("OnCommand")
def OnNotify(self, hwnd, msg, wparam, lparam):
hwndFrom, idFrom, code = win32gui_struct.UnpackWMNOTIFY(lparam)
# print("OnNotify code=0x%x (0x%x, 0x%x)" % (code, wparam, lparam))
if code == commctrl.NM_SETFOCUS:
# Control got focus - Explorer may not know - tell it
if self.browser is not None:
self.browser.OnViewWindowActive(None)
# And do our menu thang
self._OnActivate(shellcon.SVUIA_ACTIVATE_FOCUS)
elif code == commctrl.NM_KILLFOCUS:
self._OnDeactivate()
elif code == commctrl.NM_DBLCLK:
# This DblClick implementation leaves a little to be desired :)
# It demonstrates some useful concepts, such as asking the
# folder for its context-menu and invoking a command from it.
# However, as our folder delegates IContextMenu to the shell
# itself, the end result is that the folder is opened in
# its "normal" place in Windows explorer rather than inside
# our shell-extension.
# Determine the selected items.
sel = []
n = -1
while 1:
n = win32gui.SendMessage(
self.hwnd_child, commctrl.LVM_GETNEXTITEM, n, commctrl.LVNI_SELECTED
)
if n == -1:
break
sel.append(self.children[n][-1:])
print("Selection is", sel)
hmenu = win32gui.CreateMenu()
try:
# Get the IContextMenu for the items.
inout, cm = self.folder.GetUIObjectOf(
self.hwnd_parent, sel, shell.IID_IContextMenu, 0
)
# As per 'Q179911', we need to determine if the default operation
# should be 'open' or 'explore'
flags = shellcon.CMF_DEFAULTONLY
try:
self.browser.GetControlWindow(shellcon.FCW_TREE)
flags |= shellcon.CMF_EXPLORE
except pythoncom.com_error:
pass
# *sob* - delegating to the shell does work - but lands us
# in the original location. Q179911 also shows that
# ShellExecuteEx should work - but I can't make it work as
# described (XP: function call succeeds, but another thread
# shows a dialog with text of E_INVALID_PARAM, and new
# Explorer window opens with desktop view. Vista: function
# call succeeds, but no window created at all.
# On Vista, I'd love to get an IExplorerBrowser interface
# from the shell, but a QI fails, and although the
# IShellBrowser does appear to support IServiceProvider, I
# still can't get it
if 0:
id_cmd_first = 1 # TrackPopupMenu makes it hard to use 0
cm.QueryContextMenu(hmenu, 0, id_cmd_first, -1, flags)
# Find the default item in the returned menu.
cmd = win32gui.GetMenuDefaultItem(hmenu, False, 0)
if cmd == -1:
print("Oops: _doDefaultActionFor found no default menu")
else:
ci = (
0,
self.hwnd_parent,
cmd - id_cmd_first,
None,
None,
0,
0,
0,
)
cm.InvokeCommand(ci)
else:
rv = shell.ShellExecuteEx(
hwnd=self.hwnd_parent,
nShow=win32con.SW_NORMAL,
lpClass="folder",
lpVerb="explore",
lpIDList=sel[0],
)
print("ShellExecuteEx returned", rv)
finally:
win32gui.DestroyMenu(hmenu)
def OnContextMenu(self, hwnd, msg, wparam, lparam):
# Get the selected items.
pidls = []
n = -1
while 1:
n = win32gui.SendMessage(
self.hwnd_child, commctrl.LVM_GETNEXTITEM, n, commctrl.LVNI_SELECTED
)
if n == -1:
break
pidls.append(self.children[n][-1:])
spt = win32api.GetCursorPos()
if not pidls:
print("Ignoring background click")
return
# Get the IContextMenu for the items.
inout, cm = self.folder.GetUIObjectOf(
self.hwnd_parent, pidls, shell.IID_IContextMenu, 0
)
hmenu = win32gui.CreatePopupMenu()
sel = None
# As per 'Q179911', we need to determine if the default operation
# should be 'open' or 'explore'
try:
flags = 0
try:
self.browser.GetControlWindow(shellcon.FCW_TREE)
flags |= shellcon.CMF_EXPLORE
except pythoncom.com_error:
pass
id_cmd_first = 1 # TrackPopupMenu makes it hard to use 0
cm.QueryContextMenu(hmenu, 0, id_cmd_first, -1, flags)
tpm_flags = (
win32con.TPM_LEFTALIGN
| win32con.TPM_RETURNCMD
| win32con.TPM_RIGHTBUTTON
)
sel = win32gui.TrackPopupMenu(
hmenu, tpm_flags, spt[0], spt[1], 0, self.hwnd, None
)
print("TrackPopupMenu returned", sel)
finally:
win32gui.DestroyMenu(hmenu)
if sel:
ci = 0, self.hwnd_parent, sel - id_cmd_first, None, None, 0, 0, 0
cm.InvokeCommand(ci)
def OnSize(self, hwnd, msg, wparam, lparam):
# print("OnSize", self.hwnd_child, win32api.LOWORD(lparam), win32api.HIWORD(lparam))
if self.hwnd_child is not None:
x = win32api.LOWORD(lparam)
y = win32api.HIWORD(lparam)
win32gui.MoveWindow(self.hwnd_child, 0, 0, x, y, False)
# This uses scintilla to display a filename, and optionally jump to a line
# number.
class ScintillaShellView:
_public_methods_ = shellcon.IShellView_Methods
_com_interfaces_ = [
pythoncom.IID_IOleWindow,
shell.IID_IShellView,
]
def __init__(self, hwnd, filename, lineno=None):
self.filename = filename
self.lineno = lineno
self.hwnd_parent = hwnd
self.hwnd = None
def _SendSci(self, msg, wparam=0, lparam=0):
return win32gui.SendMessage(self.hwnd, msg, wparam, lparam)
# IShellView
def CreateViewWindow(self, prev, settings, browser, rect):
print("ScintillaShellView.CreateViewWindow", prev, settings, browser, rect)
# Make sure scintilla.dll is loaded. If not, find it on sys.path
# (which it generally is for Pythonwin)
try:
win32api.GetModuleHandle("Scintilla.dll")
except win32api.error:
for p in sys.path:
fname = os.path.join(p, "Scintilla.dll")
if not os.path.isfile(fname):
fname = os.path.join(p, "Build", "Scintilla.dll")
if os.path.isfile(fname):
win32api.LoadLibrary(fname)
break
else:
raise RuntimeError("Can't find scintilla!")
style = (
win32con.WS_CHILD
| win32con.WS_VSCROLL
| win32con.WS_HSCROLL
| win32con.WS_CLIPCHILDREN
| win32con.WS_VISIBLE
)
self.hwnd = win32gui.CreateWindow(
"Scintilla",
"Scintilla",
style,
rect[0],
rect[1],
rect[2] - rect[0],
rect[3] - rect[1],
self.hwnd_parent,
1000,
0,
None,
)
message_map = {
win32con.WM_SIZE: self.OnSize,
}
# win32gui.SetWindowLong(self.hwnd, win32con.GWL_WNDPROC, message_map)
file_data = open(self.filename, "U").read()
self._SetupLexer()
self._SendSci(scintillacon.SCI_ADDTEXT, len(file_data), file_data)
if self.lineno is not None:
self._SendSci(scintillacon.SCI_GOTOLINE, self.lineno)
print("Scintilla's hwnd is", self.hwnd)
def _SetupLexer(self):
h = self.hwnd
styles = [
((0, 0, 200, 0, 0x808080), None, scintillacon.SCE_P_DEFAULT),
((0, 2, 200, 0, 0x008000), None, scintillacon.SCE_P_COMMENTLINE),
((0, 2, 200, 0, 0x808080), None, scintillacon.SCE_P_COMMENTBLOCK),
((0, 0, 200, 0, 0x808000), None, scintillacon.SCE_P_NUMBER),
((0, 0, 200, 0, 0x008080), None, scintillacon.SCE_P_STRING),
((0, 0, 200, 0, 0x008080), None, scintillacon.SCE_P_CHARACTER),
((0, 0, 200, 0, 0x008080), None, scintillacon.SCE_P_TRIPLE),
((0, 0, 200, 0, 0x008080), None, scintillacon.SCE_P_TRIPLEDOUBLE),
((0, 0, 200, 0, 0x000000), 0x008080, scintillacon.SCE_P_STRINGEOL),
((0, 1, 200, 0, 0x800000), None, scintillacon.SCE_P_WORD),
((0, 1, 200, 0, 0xFF0000), None, scintillacon.SCE_P_CLASSNAME),
((0, 1, 200, 0, 0x808000), None, scintillacon.SCE_P_DEFNAME),
((0, 0, 200, 0, 0x000000), None, scintillacon.SCE_P_OPERATOR),
((0, 0, 200, 0, 0x000000), None, scintillacon.SCE_P_IDENTIFIER),
]
self._SendSci(scintillacon.SCI_SETLEXER, scintillacon.SCLEX_PYTHON, 0)
self._SendSci(scintillacon.SCI_SETSTYLEBITS, 5)
baseFormat = (-402653169, 0, 200, 0, 0, 0, 49, "Courier New")
for f, bg, stylenum in styles:
self._SendSci(scintillacon.SCI_STYLESETFORE, stylenum, f[4])
self._SendSci(scintillacon.SCI_STYLESETFONT, stylenum, baseFormat[7])
if f[1] & 1:
self._SendSci(scintillacon.SCI_STYLESETBOLD, stylenum, 1)
else:
self._SendSci(scintillacon.SCI_STYLESETBOLD, stylenum, 0)
if f[1] & 2:
self._SendSci(scintillacon.SCI_STYLESETITALIC, stylenum, 1)
else:
self._SendSci(scintillacon.SCI_STYLESETITALIC, stylenum, 0)
self._SendSci(
scintillacon.SCI_STYLESETSIZE, stylenum, int(baseFormat[2] / 20)
)
if bg is not None:
self._SendSci(scintillacon.SCI_STYLESETBACK, stylenum, bg)
self._SendSci(
scintillacon.SCI_STYLESETEOLFILLED, stylenum, 1
) # Only needed for unclosed strings.
# IOleWindow
def GetWindow(self):
return self.hwnd
def UIActivate(self, activate_state):
print("OnActivate")
def DestroyViewWindow(self):
win32gui.DestroyWindow(self.hwnd)
self.hwnd = None
print("Destroyed scintilla window")
def TranslateAccelerator(self, msg):
return winerror.S_FALSE
def OnSize(self, hwnd, msg, wparam, lparam):
x = win32api.LOWORD(lparam)
y = win32api.HIWORD(lparam)
win32gui.MoveWindow(self.hwnd, 0, 0, x, y, False)
def DllRegisterServer():
import winreg
key = winreg.CreateKey(
winreg.HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\"
"Explorer\\Desktop\\Namespace\\" + ShellFolderRoot._reg_clsid_,
)
winreg.SetValueEx(key, None, 0, winreg.REG_SZ, ShellFolderRoot._reg_desc_)
# And special shell keys under our CLSID
key = winreg.CreateKey(
winreg.HKEY_CLASSES_ROOT,
"CLSID\\" + ShellFolderRoot._reg_clsid_ + "\\ShellFolder",
)
# 'Attributes' is an int stored as a binary! use struct
attr = (
shellcon.SFGAO_FOLDER | shellcon.SFGAO_HASSUBFOLDER | shellcon.SFGAO_BROWSABLE
)
import struct
s = struct.pack("i", attr)
winreg.SetValueEx(key, "Attributes", 0, winreg.REG_BINARY, s)
print(ShellFolderRoot._reg_desc_, "registration complete.")
def DllUnregisterServer():
import winreg
try:
key = winreg.DeleteKey(
winreg.HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\"
"Explorer\\Desktop\\Namespace\\" + ShellFolderRoot._reg_clsid_,
)
except OSError as details:
import errno
if details.errno != errno.ENOENT:
raise
print(ShellFolderRoot._reg_desc_, "unregistration complete.")
if __name__ == "__main__":
from win32com.server import register
register.UseCommandLine(
ShellFolderRoot,
debug=debug,
finalize_register=DllRegisterServer,
finalize_unregister=DllUnregisterServer,
)