如何在Python中调用SHGetKnownFolderPath?

1 投票
2 回答
51 浏览
提问于 2025-04-14 18:36

我写了一个简单的例子,想要通过“艰难的方式”来计算Windows的桌面文件夹路径(使用 SHGetKnownFolderPath),但我得到的结果是一个成功的错误代码,而输出的缓冲区通过 .result 属性只显示了 b'C'。我到底哪里出错了呢?

我的代码做了以下几件事:

  1. 根据微软的规范,把想要的GUID转换成了一个叫 _GUID 结构格式 的东西。
  2. 分配了一个 result_ptr = c_char_p(),这个指针一开始是空的,但之后会被结果的指针覆盖。
  3. 调用了 SHGetKnownFolderPath,传入想要的GUID结构,没有设置标志,针对当前用户,并通过引用传递我们的 result_ptr,这样它的值就可以被覆盖。
  4. 如果 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 个回答

1

主要问题是输出参数是 PWSTR*,而传入的对象是 PSTR。使用正确的类型后,pathlibfsdecode 的部分就不需要了。

这个返回的单个字母其实给了我们一个线索。宽字符串是用 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
1

根据 [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()  # :)

其他重要方面:

撰写回答