无法在缓冲区内分离数据 (WinAPI)

Trouble separating data within buffer (WinAPI)

我在网上找到这段代码,它承诺将加载到缓冲区中的数据分开,我需要它,这样我就可以在屏幕上单独显示每个 .bmp 图像。

BOOL OpenBmpFile(char* filePath, char* fileName, int* offset, HWND hwnd)
{
    OPENFILENAME ofn;            
    char szFileName[256];    
    char szFilePath[256];
    BOOL FileOK;         

    memset(&ofn, 0, sizeof(ofn));
    ofn.lStructSize = sizeof(OPENFILENAME);
    ofn.hwndOwner = hwnd;
    ofn.lpstrFilter = TEXT("Bitmap Files (*.bmp)[=10=]*.bmp[=10=][=10=]");
    ofn.nFilterIndex = 1;
    strcpy(szFilePath, "*.bmp");
    ofn.lpstrFile = (LPWSTR)szFilePath;
    //
    // Set lpstrFile[0] to '[=10=]' so that GetOpenFileName does not 
    // use the contents of szFile to initialize itself.
    //
    ofn.lpstrFile[0] = '[=10=]';
    ofn.nMaxFile = sizeof(szFilePath);
    ofn.lpstrFileTitle = (LPWSTR)szFileName;
    ofn.nMaxFileTitle = sizeof(szFileName);
    ofn.lpstrTitle = TEXT("Open BMP File");
    ofn.Flags = OFN_SHOWHELP | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_LONGNAMES | OFN_ALLOWMULTISELECT | OFN_EXPLORER;


    // show the common dialog "Open BMP File"
    FileOK = GetOpenFileName(&ofn);

    // if cancel, exit
    if (!FileOK)
        return FALSE;

    // else store the selected filename
    strcpy(fileName, szFileName);
    //I use this because strcpy stops after the first NULL
    memcpy(filePath, szFilePath, sizeof(szFilePath));
    *offset = ofn.nFileOffset;

    if(szFilePath[ofn.nFileOffset-1] != '[=10=]')
    {
    MessageBox(hwnd,L"Single Selection",L"Open Debug 1",MB_OK);
    }
    else
    {
    MessageBox(hwnd,L"Multiple Selection",L"Open Debug 2",MB_OK);
    }

    return TRUE;
}

但是,每次我使用以下行调用此函数时都会导致错误:

OpenBmpFile((char*)file, (char*)file2, pTest, hWnd);

错误:pTest 是 nullptr;

我想我的问题是,我怎样才能有效地使用这个功能来显示我的图像?

nFileOffsetlpstrFile开头到文件名的偏移量:

if lpstrFile points to the following string, "c:\dir1\dir2\file.ext", this member contains the value 13 to indicate the offset of the "file.ext" string. If the user selects more than one file, nFileOffset is the offset to the first file name.

据此,szFilePath[ofn.nFileOffset-1]将指向\lpStrFile 定义为:

...If the OFN_ALLOWMULTISELECT flag is set and the user selects multiple files, the buffer contains the current directory followed by the file names of the selected files. For Explorer-style dialog boxes, the directory and file name strings are NULL separated, with an extra NULL character after the last file name.

因此,要确定用户是否选择了多个文件,请转到 lpstrFile 的末尾(即到第一个 null),然后检查它后面是否还有另一个 null,如果没有, 然后用户选择了多个文件。

要构造每个文件的完整路径和文件名,重复使用 nFileOffset 之前的部分并连接每个文件名。使用调试器在 return 上检查 ofn 以了解所有详细信息。

Microsoft 建议使用更现代的 Common Item Dialog API(自 Windows Vista 起可用)而不是 GetOpenFileName().

有了这个 API 你就没有了分割文件名缓冲区的麻烦,并且如果提供的缓冲区太小则处理错误情况(这需要你调用 API再次使用更大的缓冲区)。在您的代码中,缓冲区 (szFilePath) 对于多选结果来说太小,因此如果用户选择多个文件,您很快就会 运行 进入此错误状态。

我将首先提供一个简短的示例,仅显示使用 IFileOpenDialog 进行多项选择的 API 调用的顺序。

简短示例(向下滚动查看完整代码)

为简洁起见,以下代码根本没有错误处理!您应该检查每个 COM API 调用的 HRESULT 是否失败,我将在后面的完整示例中显示。

// Prepare the file open dialog.
CComPtr<IFileOpenDialog> dlg;
dlg.CoCreateInstance( CLSID_FileOpenDialog );

dlg->SetOptions( fos | FOS_ALLOWMULTISELECT );

// Show the file open dialog.
dlg->Show( hwndOwner );
if( hr == S_OK ) // If user clicked OK button...
{
    CComPtr<IShellItemArray> items;
    dlg->GetResults( &items );

    DWORD numItems = 0;
    items->GetCount( &numItems );

    // Loop over all files selected by the user.
    for( DWORD i = 0; i < numItems; ++i )
    {
        CComPtr<IShellItem> item;
        items->GetItemAt( i, &item );

        CComHeapPtr<WCHAR> path;
        item->GetDisplayName( SIGDN_FILESYSPATH, &path );

        std::wcout << std::wstring( path ) << std::endl;
    }
}

带有错误处理的完整示例

下面的代码展示了如何使用 IFileOpenDialog 以及如何弥合 C 风格错误报告 (HRESULT) 和 C++ 方法(异常)之间的差距。

首先我们定义一个函数ShowFileOpenDialog(),将IFileOpenDialog包装起来,方便使用。它 returns 一个 std::vector 与用户选择的文件的绝对路径。如果用户点击 "cancel" 向量将为空。如果出现任何错误,它会抛出一个 std::system_error 异常。

#include <atlbase.h>
#include <atlcom.h>     // CComHeapPtr
#include <atlcomcli.h>  // CComPtr
#include <Shobjidl.h>   // IFileOpenDialog
#include <system_error>
#include <vector>
#include <string>

void ThrowOnFail( HRESULT hr, char const* reason )
{
    if( FAILED(hr) )
        throw std::system_error( hr, std::system_category(), std::string("Could not ") + reason );
}

std::vector<std::wstring> ShowFileOpenDialog( 
    HWND hwndOwner, const std::vector<COMDLG_FILTERSPEC>& filter = {},
    FILEOPENDIALOGOPTIONS options = 0 )
{
    // Using CComPtr to automatically call IFileOpenDialog::Release() when scope ends.
    CComPtr<IFileOpenDialog> dlg;
    ThrowOnFail( dlg.CoCreateInstance( CLSID_FileOpenDialog ), "instanciate IFileOpenDialog" );

    if( !filter.empty() )
        ThrowOnFail( dlg->SetFileTypes( filter.size(), filter.data() ), "set filetypes filter" );

    ThrowOnFail( dlg->SetOptions( options ), "set options" );

    HRESULT hr = dlg->Show( hwndOwner );
    if( hr == HRESULT_FROM_WIN32(ERROR_CANCELLED) )
        return {};
    ThrowOnFail( hr, "show IFileOpenDialog");

    CComPtr<IShellItemArray> items;
    ThrowOnFail( dlg->GetResults( &items ), "get results" );

    DWORD numItems = 0;
    ThrowOnFail( items->GetCount( &numItems ), "get result count" );

    std::vector<std::wstring> result;
    result.reserve( numItems );

    for( DWORD i = 0; i < numItems; ++i )
    {
        CComPtr<IShellItem> item;
        ThrowOnFail( items->GetItemAt( i, &item ), "get result item" );

        // Using CComHeapPtr to automatically call ::CoTaskMemFree() when scope ends.
        CComHeapPtr<WCHAR> path;
        ThrowOnFail( item->GetDisplayName( SIGDN_FILESYSPATH, &path ), "get result item display name" );

        // Construct std::wstring directly in the vector.
        result.emplace_back( path );
    }

    return result;
}

如何调用这段代码并处理异常:

#include <iostream>

int main()
{
    ::CoInitialize(0);  // call once at application startup

    try 
    {
        HWND hwnd = nullptr;  // In a GUI app, specify handle of parent window instead.
        auto paths = ShowFileOpenDialog( hwnd, {{ L"Bitmap Files (*.bmp)", L"*.bmp" }},
                                         FOS_ALLOWMULTISELECT );
        if( paths.empty() )
        {
            // Cancel button clicked.
            std::cout << "No file(s) selected.\n";
        }
        else
        {
            // OK button clicked.
            for( const auto& path : paths )
                std::wcout << path << L"\n";
        }
    }
    catch( std::system_error& e )
    {
        std::cout 
            << "Could not show 'file open dialog'."
            << "\n\nCause: " << e.what()
            << "\nError code: " << e.code() << "\n";
    }

    ::CoUninitialize();  // match call of ::CoInitialize()
    return 0;
}

您犯的最大错误是将 ANSI 和 Unicode 混合在一起。您正在使用 char[] 缓冲区并将它们类型转换为 LPWSTR 指针,以便将它们分配给 OPENFILENAME 字段。由于您使用的是 API 的 TCHAR 版本,这意味着您的项目是针对 Unicode 而不是 ANSI 编译的。因此 API 需要 Unicode 缓冲区,并将输出 Unicode 字符串。这也意味着您告诉 API 两次 您分配的缓冲区 space 可用于接收字符,因为您正在设置 ofn.nMaxFileofn.nMaxFileTitle 字段以 byte 计数,而不是 character 计数。所以你会导致缓冲区溢出。

您不能仅仅将 8 位缓冲区类型转换为 16 位数据类型。您必须首先为缓冲区使用正确的数据类型,并摆脱类型转换。在这种情况下,这意味着使用 WCHAR/wchar_t(或至少 TCHAR)缓冲区而不是 char 缓冲区。但是,由于您在函数参数中使用 char,因此您应该使用 API 的 ANSI 版本而不是 TCHAR/Unicode 版本。

选择多个文件时,尤其是文件名较长的文件,生成的字符数据很容易增长到超出固定长度缓冲区的大小。正如 OPENFILENAME documentation 所述:

lpstrFile
Type: LPTSTR

The file name used to initialize the File Name edit control. The first character of this buffer must be NULL if initialization is not necessary. When the GetOpenFileName or GetSaveFileName function returns successfully, this buffer contains the drive designator, path, file name, and extension of the selected file.

If the OFN_ALLOWMULTISELECT flag is set and the user selects multiple files, the buffer contains the current directory followed by the file names of the selected files. For Explorer-style dialog boxes, the directory and file name strings are NULL separated, with an extra NULL character after the last file name. For old-style dialog boxes, the strings are space separated and the function uses short file names for file names with spaces. You can use the FindFirstFile function to convert between long and short file names. If the user selects only one file, the lpstrFile string does not have a separator between the path and file name.

If the buffer is too small, the function returns FALSE and the CommDlgExtendedError function returns FNERR_BUFFERTOOSMALL. In this case, the first two bytes of the lpstrFile buffer contain the required size, in bytes or characters.

nMaxFile
Type: DWORD

The size, in characters, of the buffer pointed to by lpstrFile. The buffer must be large enough to store the path and file name string or strings, including the terminating NULL character. The GetOpenFileName and GetSaveFileName functions return FALSE if the buffer is too small to contain the file information. The buffer should be at least 256 characters long.

你没有考虑到这一点。 256(最好使用 260,又名 MAX_PATH)可以选择单个文件,但可能不适用于选择多个文件。如果 GetOpenFileName()FNERR_BUFFERTOOSMALL 而失败,您将不得不重新分配缓冲区并再次调用 GetOpenFileName()

话虽如此,请尝试更像这样的事情:

BOOL OpenBmpFiles(char **filePath, char** fileNames, HWND hwnd)
{
    *filePath = NULL;
    *fileNames = NULL;

    size_t iMaxFileSize = MAX_PATH;
    char *lpFileBuffer = (char*) malloc(iMaxFileSize);
    if (!lpFileBuffer)
        return FALSE;

    char szFileTitle[MAX_PATH];
    BOOL bResult = FALSE;

    OPENFILENAMEA ofn;            
    memset(&ofn, 0, sizeof(ofn));
    ofn.lStructSize = sizeof(ofn);
    ofn.hwndOwner = hwnd;
    ofn.lpstrFilter = "Bitmap Files (*.bmp)[=10=]*.bmp[=10=][=10=]";
    ofn.nFilterIndex = 1;
    ofn.lpstrFile = lpFileBuffer;
    ofn.nMaxFile = iMaxFileSize;
    ofn.lpstrFileTitle = szFileTitle;
    ofn.nMaxFileTitle = MAX_PATH;
    ofn.lpstrTitle = "Open BMP File";
    ofn.Flags = OFN_SHOWHELP | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_LONGNAMES | OFN_ALLOWMULTISELECT | OFN_EXPLORER;

    do
    {
        //
        // Set lpstrFile[0] to '[=10=]' so that GetOpenFileName does not 
        // use the contents of lpstrFile to initialize itself.
        //
        ofn.lpstrFile[0] = '[=10=]';

        // show the common dialog "Open BMP File"
        if (GetOpenFileNameA(&ofn))
            break;

        // if cancel, exit           
        if (CommDlgExtendedError() != FNERR_BUFFERTOOSMALL)
            goto cleanup;

        // reallocate the buffer and try again
        iMaxFileSize = * (WORD*) lpFileBuffer;
        char *lpNewFileBuffer = (char*) realloc(lpFileBuffer, iMaxFileSize);
        if (!lpNewFileBuffer)
            goto cleanup;

        lpFileBuffer = lpNewFileBuffer;

        ofn.lpstrFile = lpFileBuffer;
        ofn.nMaxFile = iMaxFileSize;
    }
    while (true);

    if (lpFileBuffer[ofn.nFileOffset-1] != '[=10=]')
    {
        MessageBox(hwnd, TEXT("Single Selection"), TEXT("Open Debug 1"), MB_OK);

        // copy the single filename and make sure it is double-null terminated

        size_t len = strlen(&lpFileBuffer[ofn.nFileOffset]) + 2;

        *fileNames = (char*) malloc(len);
        if (!*fileNames)
            goto cleanup;

        strncpy(*fileNames, &lpFileBuffer[ofn.nFileOffset], len);

        // copy the directory path and make sure it is null terminated

        lpFileBuffer[ofn.nFileOffset] = '[=10=]';

        *filePath = strdup(lpFileBuffer);
        if (!*filePath)
        {
            free(*fileNames);
            *fileNames = NULL;
            goto cleanup;
        }
    }
    else
    {
        MessageBox(hwnd, TEXT("Multiple Selection"), TEXT("Open Debug 2"), MB_OK);

        // copy the directory path, it is already null terminated

        *filePath = strdup(lpFileBuffer);
        if (!*filePath)
            goto cleanup;

        // copy the multiple filenames, they are already double-null terminated

        size_t len = (ofn.nMaxFile - ofn.nFileOffset);

        *fileNames = (char*) malloc(len);
        if (!*fileNames)
        {
            free(*filePath);
            *filePath = NULL;
            goto cleanup;
        }

        // have to use memcpy() since the filenames are null-separated
        memcpy(*fileNames, &lpFileBuffer[ofn.nFileOffset], len);
    }

    bResult = TRUE;

cleanup:
    free(lpFileBuffer);
    return bResult;
}

那么你可以这样使用它:

char *path, *filenames;
if (OpenBmpFiles(&path, &filenames, hwnd))
{
    char *filename = filenames;
    do
    {
        // use path + filename as needed...
        /*
        char *fullpath = (char*) malloc(strlen(path)+strlen(filename)+1);
        PathCombineA(fullpath, path, filename);
        doSomethingWith(fullpath);
        free(fullpath);
        */

        filename += (strlen(filename) + 1);
    }
    while (*filename != '[=11=]');

    free(path);
    free(filenames);
}

UPDATE:或者,为了简化返回文件名的使用,您可以做更多类似这样的事情:

BOOL OpenBmpFiles(char** fileNames, HWND hwnd)
{
    *fileNames = NULL;

    size_t iMaxFileSize = MAX_PATH;
    char *lpFileBuffer = (char*) malloc(iMaxFileSize);
    if (!lpFileBuffer)
        return FALSE;

    char szFileTitle[MAX_PATH];
    BOOL bResult = FALSE;

    OPENFILENAMEA ofn;            
    memset(&ofn, 0, sizeof(ofn));
    ofn.lStructSize = sizeof(ofn);
    ofn.hwndOwner = hwnd;
    ofn.lpstrFilter = "Bitmap Files (*.bmp)[=12=]*.bmp[=12=][=12=]";
    ofn.nFilterIndex = 1;
    ofn.lpstrFile = lpFileBuffer;
    ofn.nMaxFile = iMaxFileSize;
    ofn.lpstrFileTitle = szFileTitle;
    ofn.nMaxFileTitle = MAX_PATH;
    ofn.lpstrTitle = "Open BMP File";
    ofn.Flags = OFN_SHOWHELP | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_LONGNAMES | OFN_ALLOWMULTISELECT | OFN_EXPLORER;

    do
    {
        //
        // Set lpstrFile[0] to '[=12=]' so that GetOpenFileName does not 
        // use the contents of lpstrFile to initialize itself.
        //
        ofn.lpstrFile[0] = '[=12=]';

        // show the common dialog "Open BMP File"
        if (GetOpenFileNameA(&ofn))
            break;

        // if cancel, exit           
        if (CommDlgExtendedError() != FNERR_BUFFERTOOSMALL)
            goto cleanup;

        // reallocate the buffer and try again
        iMaxFileSize = * (WORD*) lpFileBuffer;
        char *lpNewFileBuffer = (char*) realloc(lpFileBuffer, iMaxFileSize);
        if (!lpNewFileBuffer)
            goto cleanup;

        lpFileBuffer = lpNewFileBuffer;

        ofn.lpstrFile = lpFileBuffer;
        ofn.nMaxFile = iMaxFileSize;
    }
    while (true);

    if (lpFileBuffer[ofn.nFileOffset-1] != '[=12=]')
    {
        MessageBox(hwnd, TEXT("Single Selection"), TEXT("Open Debug 1"), MB_OK);

        // copy the single filename and make sure it is double-null terminated

        size_t len = strlen(lpFileBuffer) + 2;

        *fileNames = (char*) malloc(len);
        if (!*fileNames)
            goto cleanup;

        strncpy(*fileNames, lpFileBuffer, len);
    }
    else
    {
        MessageBox(hwnd, TEXT("Multiple Selection"), TEXT("Open Debug 2"), MB_OK);

        // calculate the output buffer size

        char *path = lpFileBuffer;
        size_t pathLen = strlen(path);
        bool slashNeeded = ((path[pathLen-1] != '\') && (path[pathLen-1] != '/'));
        size_t len = 1;

        char *filename = &lpFileBuffer[ofn.nFileOffset];
        while (*filename != '[=12=]')
        {
            int filenameLen = strlen(filename);
            len += (pathLen + filenameLen + 1);
            if (slashNeeded) ++len;
            filename += (filenameLen + 1);
        }

        // copy the filenames and make sure they are double-null terminated

        *fileNames = (char*) malloc(len);
        if (!*fileNames)
            goto cleanup;

        char *out = *fileNames;

        filename = &lpFileBuffer[ofn.nFileOffset];
        while (*filename != '[=12=]')
        {
            strncpy(out, path, pathLen);
            out += pathLen;
            if (slashNeeded) *out++ = '\';

            int filenameLen = strlen(filename);
            strncpy(out, filename, filenameLen);
            out += filenameLen;
            *out++ = '[=12=]';

            filename += (filenameLen + 1);
        }

        *out = '[=12=]';
    }

    bResult = TRUE;

cleanup:
    free(lpFileBuffer);
    return bResult;
}

char *filenames;
if (OpenBmpFiles(&filenames, hwnd))
{
    char *filename = filenames;
    do
    {
        // use filename as needed...
        /*
        doSomethingWith(filename);
        */

        filename += (strlen(filename) + 1);
    }
    while (*filename != '[=13=]');

    free(filenames);
}