How to get null terminated strings from a buffer?

Hi all,
I am writing a gui library in Python with ctypes. So far so good. Currently I am writing the common dialog boxes like file open, file save and folder browser. Windows will allow us to open more than one file with OFN_ALLOWMULTISELECT flag. But if we use this flag, the lpstrFile member of the OPENFILENAMEW struct behaves quite differently. The lpstrFile member is an array with 260 wchar character length. The documentation says this ;

If the user selects more than one file, the lpstrFile buffer returns the path to the current directory
followed by the file names of the selected files. 
The nFileOffset member is the offset, in bytes or characters, 
to the first file name, and the nFileExtension member is not used. 
For Explorer-style dialog boxes, the directory and file name strings 
are NULL separated, with an extra NULL character after the last file name. 

So this is what i am using for D programming language.

void extractFileNames(wchar[] buff, int startPos) {
        int offset = startPos;
        for (int i = startPos; i < MAX_PATH; i++) {
            wchar wc = buff[i];
            if (wc == '\u0000') {
                wchar[] slice = buff[offset..i];
                offset = i + 1;
                this.mSelFiles ~= slice.to!string; // Adding sliced file name to array.
                if (buff[offset] == '\u0000') break;
            }
        }
    }

I am using Explorer-style dialog box. So I have a buffer which should contain the directory path and all selected file names separated by null character. But buffer.value only returns the directory path. Is the rest of the data missing because of the null character after the directory path? So how to retrieve the data after the first null character?

Do you have a small example that shows what you can so far?

Once you have ctypes returning the array of 260 word (520 bytes) buffer
to python you can decode it with buf.decode(‘utf-16’) and then look for ‘\0’
in the unicode string. I would be tempted to use

parts = buf.decode('utf-16').split('\0')
cur_dir = parts[0]
assert parts[-1] == ''
all_files = parts[1:-1]
1 Like

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

Hi,
Thanks for the reply. I tried the decode function. But python says AttributeError: 'c_wchar_Array_260' object has no attribute 'decode'. So this is my code.

# Parameters
# 1. obj - may be a FileOpenDialog class or FileSaveDialog class.
# 2. isOpen - bool
# 3. hwnd - HWND (A window handle)
def _showDialogHelper(obj, isOpen, hwnd):
    ofn = OPENFILENAMEW()
    ofn.hwndOwner = hwnd
    buffer = create_unicode_buffer(MAX_PATH)
    idBuff = None if obj._initDir == "" else cast(create_unicode_buffer(obj._initDir), c_wchar_p)
    ofn.lStructSize = sizeof(OPENFILENAMEW)
    ofn.lpstrFilter = cast(create_unicode_buffer(obj._filter), c_wchar_p)
    ofn.lpstrFile = cast(buffer, c_wchar_p)
    ofn.lpstrInitialDir = idBuff
    ofn.lpstrTitle = obj._title
    ofn.nMaxFile = MAX_PATH
    ofn.nMaxFileTitle = MAX_PATH
    ofn.lpstrDefExt = '\u0000'
    retVal = 0
    if isOpen:
        ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST
        if obj._multiSel: ofn.Flags |= OFN_ALLOWMULTISELECT | OFN_EXPLORER
        if obj._showHidden: ofn.Flags |= OFN_FORCESHOWHIDDEN
        retVal = GetOpenFileName(byref(ofn))
        if retVal > 0 and obj._multiSel:
            parts = buffer.decode('utf-16').split('\0')
            print(parts)
    else:
        ofn.Flags = OFN_PATHMUSTEXIST | OFN_OVERWRITEPROMPT
        retVal = GetSaveFileName(byref(ofn))

    if retVal != 0:
        obj._fileNameStart = ofn.nFileOffset
        obj._extStart = ofn.nFileExtension
        obj._selPath = buffer.value
        return True
    return False

Hi, Thanks for the reply. Let me try your suggestion. And yeah, your code looks great. I think there is some valuable points I can learn from that.

Wow !! This worked like a charm. Thank you once again.

So I ended up on this.

def _extractFileNames(self, buff, startPos):
        parts = buff[:].rstrip('\0')
        dirPath = parts[:startPos].rstrip('\0')
        names = parts[startPos:].split('\0')
        for name in names:
            self._fNames.append(f"{dirPath}\{name}")

You should use a larger buffer. The open dialog can return a long path to a directory, which can have up to 32767 characters, plus each filename can have up to 255 characters.

1 Like

Yes. I changed that to something similar in your code. I thought it should be only 260 chars. My bad.