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']