How to get null terminated strings from a buffer?

Here’s a basic example that sets a buffer in the struct, with the field type set to POINTER(WCHAR) in order to avoid having to cast the buffer. Then after calling GetOpenFileNameW(), it simply converts the buffer to a string using a slice; strips off the trailing nulls; splits out the directory path and filenames; and returns the joined paths.

import os
import ctypes
from ctypes import wintypes

comdlg32 = ctypes.WinDLL('comdlg32', use_last_error=True)

OFN_ALLOWMULTISELECT = 0x00000200
OFN_PATHMUSTEXIST = 0x00000800
OFN_FILEMUSTEXIST = 0x00001000
OFN_EXPLORER = 0x00080000

FNERR_SUBCLASSFAILURE = 0x3001
FNERR_INVALIDFILENAME = 0x3002
FNERR_BUFFERTOOSMALL = 0x3003

UINT_PTR = wintypes.WPARAM
LPOFNHOOKPROC = ctypes.WINFUNCTYPE(UINT_PTR, wintypes.HWND, wintypes.UINT,
                    wintypes.WPARAM, wintypes.LPARAM)

class OPENFILENAMEW(ctypes.Structure):
    _fields_ = (
        ('lStructSize', wintypes.DWORD),
        ('hwndOwner', wintypes.HWND),
        ('hInstance', wintypes.HINSTANCE),
        ('lpstrFilter', wintypes.LPWSTR),
        ('lpstrCustomFilter', wintypes.LPWSTR),
        ('nMaxCustFilter', wintypes.DWORD),
        ('nFilterIndex', wintypes.DWORD),
        ('lpstrFile', ctypes.POINTER(wintypes.WCHAR)),
        ('nMaxFile', wintypes.DWORD),
        ('lpstrFileTitle', wintypes.LPWSTR),
        ('nMaxFileTitle', wintypes.DWORD),
        ('lpstrInitialDir', wintypes.LPCWSTR),
        ('lpstrTitle', wintypes.LPCWSTR),
        ('Flags', wintypes.DWORD),
        ('nFileOffset', wintypes.WORD),
        ('nFileExtension', wintypes.WORD),
        ('lpstrDefExt', wintypes.LPCWSTR),
        ('lCustData', wintypes.LPARAM),
        ('lpfnHook', LPOFNHOOKPROC),
        ('lpTemplateName', wintypes.LPCWSTR),
        ('pvReserved', wintypes.LPVOID),
        ('dwReserved', wintypes.DWORD),
        ('FlagsEx', wintypes.DWORD),
    )

LPOPENFILENAMEW = ctypes.POINTER(OPENFILENAMEW)

comdlg32.GetOpenFileNameW.argtypes = (LPOPENFILENAMEW,)

class ComDlgError(Exception):
    def __init__(self, error, function_name=''):
        self.error = error
        self.function_name = function_name

    def __str__(self):
        if self.function_name:
            return f'{self.function_name} [Error {self.error}]'
        return f'[Error {self.error}]'

def get_open_filename():
    ofn = OPENFILENAMEW()
    ofn.lStructSize = ctypes.sizeof(ofn)
    ofn.lpstrFilter = 'Text documents\0*.txt\0All files\0*.*\0\0'
    ofn.nFilterIndex = 1
    ofn.Flags = (OFN_EXPLORER | OFN_ALLOWMULTISELECT |
                 OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST)
    ofn.nMaxFile = 32768 + 256 * 100 + 1 # 1 long path + 100 base filenames
    buf = (ctypes.c_wchar * ofn.nMaxFile)()
    ofn.lpstrFile = buf
    if not comdlg32.GetOpenFileNameW(ctypes.byref(ofn)):
        raise ComDlgError(comdlg32.CommDlgExtendedError(), 'GetOpenFileNameW')
    # If a single file is selected, the full path is stored without a null
    # after the path of the directory. If multiple files are selected, the
    # path of the directory is terminated by a null, followed by the null-
    # terminated filenames. In either case, the first filename begins at
    # nFileOffset.
    s = buf[:].rstrip('\0')
    path = s[:ofn.nFileOffset].rstrip('\0')
    filenames = s[ofn.nFileOffset:].split('\0')
    return [os.path.join(path, f) for f in filenames] 

For example:

>>> get_open_filename()
['C:\\Program Files\\Python311\\LICENSE.txt', 'C:\\Program Files\\Python311\\NEWS.txt']
1 Like