如何在Python中调用SHGetKnownFolderPath?
我写了一个简单的例子,想要通过“艰难的方式”来计算Windows的桌面文件夹路径(使用 SHGetKnownFolderPath
),但我得到的结果是一个成功的错误代码,而输出的缓冲区通过 .result
属性只显示了 b'C'
。我到底哪里出错了呢?
我的代码做了以下几件事:
- 根据微软的规范,把想要的GUID转换成了一个叫
_GUID
结构格式 的东西。 - 分配了一个
result_ptr = c_char_p()
,这个指针一开始是空的,但之后会被结果的指针覆盖。 - 调用了
SHGetKnownFolderPath
,传入想要的GUID结构,没有设置标志,针对当前用户,并通过引用传递我们的result_ptr
,这样它的值就可以被覆盖。 - 如果
SHGetKnownFolderPath
表示成功,就用.value
来获取result_ptr
的值。
我得到的结果只有一个字符,但我以为 c_char_p
应该是指向以空字符结尾的字符串的指针。
是Windows在我的指针里写了一个错误的字符串,还是我读取它的值时搞错了,或者我在构建这个函数时犯了其他错误?
import contextlib
import ctypes
import ctypes.wintypes
import functools
import os
import pathlib
import types
import uuid
try:
wintypes_GUID = ctypes.wintypes.GUID
except AttributeError:
class wintypes_GUID(ctypes.Structure):
# https://learn.microsoft.com/en-us/windows/win32/api/guiddef/ns-guiddef-guid
# https://github.com/enthought/comtypes/blob/1.3.1/comtypes/GUID.py
_fields_ = [
('Data1', ctypes.c_ulong),
('Data2', ctypes.c_ushort),
('Data3', ctypes.c_ushort),
('Data4', ctypes.c_ubyte * 8)
]
@classmethod
def _from_uuid(cls, u):
u = uuid.UUID(u)
u_str = f'{{{u!s}}}'
result = wintypes_GUID()
errno = ctypes.oledll.ole32.CLSIDFromString(u_str, ctypes.byref(result))
if errno == 0:
return result
else:
raise RuntimeError(f'CLSIDFromString returned error code {errno}')
DESKTOP_UUID = 'B4BFCC3A-DB2C-424C-B029-7FE99A87C641'
def get_known_folder(uuid):
# FIXME this doesn't work, seemingly returning just b'C' no matter what
result_ptr = ctypes.c_char_p()
with _freeing(ctypes.oledll.ole32.CoTaskMemFree, result_ptr):
errno = ctypes.windll.shell32.SHGetKnownFolderPath(
ctypes.pointer(wintypes_GUID._from_uuid(uuid)),
0,
None,
ctypes.byref(result_ptr)
)
if errno == 0:
result = result_ptr.value
if len(result) < 2:
import warnings
warnings.warn(f'result_ptr.value == {result!r}')
return pathlib.Path(os.fsdecode(result))
else:
raise RuntimeError(f'Shell32.SHGetKnownFolderPath returned error code {errno}')
@contextlib.contextmanager
def _freeing(freefunc, obj):
try:
yield obj
finally:
freefunc(obj)
assert get_known_folder(DESKTOP_UUID) ==\
pathlib.Path('~/Desktop').expanduser(),\
f'Result: {get_known_folder(DESKTOP_UUID)!r}; expcected: {pathlib.Path("~/Desktop").expanduser()!r}'
2 个回答
主要问题是输出参数是 PWSTR*
,而传入的对象是 PSTR
。使用正确的类型后,pathlib
和 fsdecode
的部分就不需要了。
这个返回的单个字母其实给了我们一个线索。宽字符串是用 UTF-16 编码的,所以你得到的结果是 'C\x00:\x00\...'
,而不是 'C:...'
。而且,获取 c_char_p
的 .value
时会在第一个空字符处停止。使用 c_wchar_p
时,ctypes
会从 .value
返回解码后的 UTF-16 的 Python str
。
为每个通过 ctypes
调用的函数指定 .argtypes
和 .restype
是个好习惯,这样可以检查传入的参数是否正确,这样就能发现问题了。.errcheck
也很有用,可以让函数在出错时抛出异常。
下面是一个完整指定且可工作的示例:
import ctypes as ct
import ctypes.wintypes as w
class HResultError(Exception):
pass
def hresultcheck(result, func, args):
if result != S_OK:
raise HResultError(f'{result} (0x{result & 0xFFFFFFFF:08X})')
return None
class GUID(ct.Structure):
# https://learn.microsoft.com/en-us/windows/win32/api/guiddef/ns-guiddef-guid
# https://github.com/enthought/comtypes/blob/1.3.1/comtypes/GUID.py
_fields_ = [('Data1', ct.c_ulong),
('Data2', ct.c_ushort),
('Data3', ct.c_ushort),
('Data4', ct.c_ubyte * 8)]
@classmethod
def from_uuid(cls, u):
guid = GUID()
CLSIDFromString(u, ct.byref(guid))
return guid
# Definitions from Windows headers not included in ctypes.wintypes.
KNOWNFOLDERID = GUID
REFKNOWNFOLDERID = ct.POINTER(KNOWNFOLDERID)
PWSTR = w.LPWSTR
HRESULT = w.LONG
CLSID = GUID
LPCLSID = ct.POINTER(CLSID)
S_OK = 0
shell32 = ct.WinDLL('shell32', use_last_error=True)
ole32 = ct.WinDLL('ole32', use_last_error=True)
# Explicit argument and return types matching MSDN docs,
# and automatic return type checking.
SHGetKnownFolderPath = shell32.SHGetKnownFolderPath
SHGetKnownFolderPath.argtypes = REFKNOWNFOLDERID, w.DWORD, w.HANDLE, ct.POINTER(PWSTR)
SHGetKnownFolderPath.restype = HRESULT
SHGetKnownFolderPath.errcheck = hresultcheck
CLSIDFromString = ole32.CLSIDFromString
CLSIDFromString.argtypes = w.LPCOLESTR, LPCLSID
CLSIDFromString.restype = HRESULT
CLSIDFromString.errcheck = hresultcheck
CoTaskMemFree = ole32.CoTaskMemFree
CoTaskMemFree.argtypes = w.LPVOID,
CoTaskMemFree.restype = None
DESKTOP_UUID = GUID.from_uuid('{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}')
def get_known_folder(guid):
path = PWSTR() # bug was here. C equivalent is "wchar_t*" but was "char*".
try:
SHGetKnownFolderPath(ct.byref(guid), 0, None, ct.byref(path))
return path.value
finally:
CoTaskMemFree(path)
print(get_known_folder(DESKTOP_UUID))
输出:
C:\Users\Mark\Desktop
根据 [MS.Learn]: SHGetKnownFolderPath 函数 (shlobj_core.h) 的说明(强调是我自己的):
[out] ppszPath
类型: PWSTR*
当这个方法返回时,包含一个指向以空字符结尾的 Unicode 字符串的 指针的地址
这个函数会返回一个 宽字符(016位)字符串,类型是 wchar_t*,或者是 [Python.Docs]: class ctypes.c_wchar_p。
想了解更多细节,可以查看 [SO]: 将 utf-16 字符串传递给 Windows 函数 (@CristiFati 的回答)。
所以,你需要在 get_known_folder 的开头做的唯一更改是:
result_ptr = ctypes.c_wchar_p() # :)
其他重要方面:
查看 [SO]: 从 Python 通过 ctypes 调用的 C 函数返回错误值 (@CristiFati 的回答),这是在使用 CTypes(调用函数)时常见的陷阱。在这个案例中也是个问题。
如果你不执着于使用 CTypes 来解决这个问题(虽然这样做是个不错的练习),你可以使用 [GitHub]: mhammond/pywin32 - Python for Windows (pywin32) 扩展,这是一个针对 WinAPI 的 Python 封装。文档(正在进行中)可以在 [GitHub.MHammond]: Python for Win32 扩展帮助 找到(或者在 [ME.TimGolden]: Python for Win32 扩展帮助)。
由于它包含很多样板代码,你需要写的代码会少很多:[cfati@CFATI-5510-0:e:\Work\Dev\StackExchange\StackOverflow\q078097730]> python Python 3.10.11 (tags/v3.10.11:7d4cc5a, Apr 5 2023, 00:38:17) [MSC v.1929 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> >>> import pathlib >>> from win32comext.shell import shell >>> >>> >>> print(str(pathlib.Path("~/Desktop").expanduser())) C:\Users\cfati\Desktop >>> >>> print(shell.SHGetKnownFolderPath("{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}")) C:\Users\cfati\Desktop >>>