开发平台
- windows10
- python3.8
- pywin32
背景知识
图标覆盖概念: Windows中的IconOverlay 之争
widows实现: How to overlay an icon over existing shell objects
python实现: Add my own icon overlays
SHChangeNotify:SHChangeNotify
现有文章存在的问题
在Add my own icon overlays作者提供了python下实现图标覆盖功能的代码,但是存在两个问题
- 没有说明多个图标时如何实现不同条件下不同覆盖图标的判断与现实
- 没有说明图如何动态的实现图标覆盖切换与刷新,比如当前文件资源器中图标叠加状态是同步中,如何用应用代码控制图标覆盖改为同步完成,并且主动刷新用户文件资源浏览器中的图标显示而不需要用户手动刷新
解决方案
多个图标覆盖实现条件判断
创建多个IconOverlay类并同时注册,1个IconOverlay类对应1个图标
注意多个IconOverlay类要有不同的 reg_clsid 属性,然后在 GetOverlayInfo 方法中返回不同的图标,再在 IsMemberOf 方法中根据不同条件返回是否显示图标
我的实现方式是在 IsMemberOf 方法中查询sqlite数据库的一个文件状态表,根据表中文件路径和对应的文件状态,判断为文件显示哪种图标
import os
import winreg
import sqlite3
import winerror
import win32con
// 注意这里的引入路径,旧版的pywin32路径不一样
from win32comext.shell import shell, shellcon
class IconOverlayOk:
"""
图标覆盖组件
windows shell下的IShellIconOverlayIdentifier接口用于实现图标覆盖功能
用python实现功能需要以下步骤
1. 使用pywin32,编写python class实现windows com组件
2. 将组件注册到windows系统
3. 在注册表中添加图标覆盖组件的项
4. 重启explorer.exe
当windows文件资源管理器浏览文件时
1. explorer会为每一个当前要显示的文件调用IShellIconOverlayIdentifier的IsMemberOf方法判断是否需要对文件做处理
2. 如果IsMemberOf返回winerror.S_OK,系统会继续调用GetOverlayInfo方法获取要叠加的图标文件
3. 操作系统负责将GetOverlayInfo返回的图标叠加到文件原有图标上
_reg_remove_keys_: 卸载组件时,需要同时删除的注册表项,这里在最后的注册表键名中添加空格前缀,可以保证图标覆盖组件靠前的优先级,避免与其它图标覆盖程序如oneDriver、坚果云等竞争失败导致图标覆盖不能显示
"""
_reg_clsid_ = '{EA258179-A91D-45F9-A237-FC92A5290423}'
_reg_progid_ = 'SUN.PythonPackagesOverlayHandlerOK'
_reg_desc_ = 'Icon Overlay Handler to indicate Python packages'
_public_methods_ = ['GetOverlayInfo', 'GetPriority', 'IsMemberOf']
_com_interfaces_ = [shell.IID_IShellIconOverlayIdentifier]
_reg_remove_keys_ = [(r'Software\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\ PyPackageOverlay1',
win32con.HKEY_LOCAL_MACHINE)]
def GetOverlayInfo(self):
"""
获取要叠加的图标文件位置
从注册表获取图标文件路径iconPath
:return:
"""
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'Software\Dachengyun')
iconPath, index = winreg.QueryValueEx(key, 'iconPath')
key.Close()
return os.path.join(iconPath, 'Ok.ico'), 0, shellcon.ISIOI_ICONFILE
def GetPriority(self):
return 1
def IsMemberOf(self, fname, attributes):
"""
判断是否对文件进行图标覆盖
从注册表读取同步文件夹路径syncPath、sqlite数据库文件路径dbPath
如果当前是syncPath下的文件,则到db查询文件同步状态
根据文件同步状态判断是否需要图标覆盖
:param fname:
:param attributes:
:return:
"""
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'Software\Dachengyun')
syncPath, index = winreg.QueryValueEx(key, 'syncPath')
dbPath, index = winreg.QueryValueEx(key, 'dbPath')
key.Close()
if fname.startswith(syncPath):
filePath = fname[len(os.path.dirname(syncPath)):]
conn = sqlite3.connect(dbPath)
c = conn.cursor()
c.execute('SELECT state FROM file_sync_state WHERE path=:filePath', {'filePath': filePath})
r = c.fetchone()
c.close()
conn.close()
if r is not None and r[0] == 1:
return winerror.S_OK
return winerror.E_FAIL
class IconOverlaySync:
_reg_clsid_ = '{EA258179-A92D-45F9-A237-FC92A5290423}'
_reg_progid_ = 'SUN.PythonPackagesOverlayHandlerSync'
_reg_desc_ = 'Icon Overlay Handler to indicate Python packages'
_public_methods_ = ['GetOverlayInfo', 'GetPriority', 'IsMemberOf']
_com_interfaces_ = [shell.IID_IShellIconOverlayIdentifier]
_reg_remove_keys_ = [(r'Software\Microsoft\Windows\CurrentVersion\Explorer\ShellIconOverlayIdentifiers\ PyPackageOverlay2',
win32con.HKEY_LOCAL_MACHINE)]
def GetOverlayInfo(self):
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'Software\Dachengyun')
iconPath, index = winreg.QueryValueEx(key, 'iconPath')
key.Close()
return os.path.join(iconPath, 'Sync.ico'), 0, shellcon.ISIOI_ICONFILE
def GetPriority(self):
return 2
def IsMemberOf(self, fname, attributes):
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'Software\Dachengyun')
syncPath, index = winreg.QueryValueEx(key, 'syncPath')
dbPath, index = winreg.QueryValueEx(key, 'dbPath')
key.Close()
if fname.startswith(syncPath):
filePath = fname[len(os.path.dirname(syncPath)):]
conn = sqlite3.connect(dbPath)
c = conn.cursor()
c.execute('SELECT state FROM file_sync_state WHERE path=:filePath',
{'filePath': filePath})
r = c.fetchone()
c.close()
conn.close()
if r is not None and r[0] == 2:
return winerror.S_OK
return winerror.E_FAIL
动态图标切换与刷新
图标的动态切换只需要在外部应用代码中修改sqlite数据库中文件状态表数据即可
但是这只能实现图标覆盖的切换,不能实现动态刷新;就是说当用户在文件资源管理器中浏览文件时,如果当前图标覆盖文件正在显示,即便我们再数据库中更改了文件状态,图标覆盖也不会动态刷新,必须要手动刷新文件资源管理器或者退出文件夹重新进入,才能看到更改后的覆盖图标
解决办法是使用 SHChangeNotify 来通知操作系统文件已经变更,使操作系统做一个主动的刷新
// 这里的引入要注意,旧版的pywin32引入路径好像不一样,容易出错
from win32comext.shell import shell, shellcon
// syncPath是要刷新的文件绝对路径
shell.SHChangeNotify(shellcon.SHCNE_ATTRIBUTES,
shellcon.SHCNF_PATH | shellcon.SHCNF_FLUSHNOWAIT,
bytes(syncPath, 'gbk'), None)
组件注册卸载与explorer.exe重启
IconOverlay类编写完需要注册到windows系统,代码如下
import os
import win32api
import win32con
from win32com.server import register
import iconOverlay
// 多个图标需要复制以下代码,修改类名后注册多个类
register.UseCommandLine(iconOverlay.IconOverlayOk)
win32api.RegSetValue(win32api.RegCreateKey(iconOverlay.IconOverlayOk._reg_remove_keys_[0][1],iconOverlay.IconOverlayOk._reg_remove_keys_[0][0]),
None, win32con.REG_SZ,
iconOverlay.IconOverlayOk._reg_clsid_)
register.UseCommandLine(iconOverlay.IconOverlaySync)
win32api.RegSetValue(win32api.RegCreateKey(iconOverlay.IconOverlaySync._reg_remove_keys_[0][1],iconOverlay.IconOverlaySync._reg_remove_keys_[0][0]),
None, win32con.REG_SZ,
iconOverlay.IconOverlaySync._reg_clsid_)
// 组件注册完成后需要重启explorer.exe使组件生效
os.system('taskkill /f /im explorer.exe & start explorer.exe')
组件卸载
import win32com.server.register
win32com.server.register.UnregisterClasses(iconOverlay.IconOverlayOk)
win32com.server.register.UnregisterClasses(iconOverlay.IconOverlaySync)
os.system('taskkill /f /im explorer.exe & start explorer.exe')