【问题标题】:Trouble separating data within buffer (WinAPI)在缓冲区中分离数据时遇到问题 (WinAPI)
【发布时间】:2017-08-06 01:49:58
【问题描述】:

我在网上找到了这段代码,它承诺将加载到缓冲区中的数据分开,我需要它,所以我可以在屏幕上单独显示每个 .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)\0*.bmp\0\0");
    ofn.nFilterIndex = 1;
    strcpy(szFilePath, "*.bmp");
    ofn.lpstrFile = (LPWSTR)szFilePath;
    //
    // Set lpstrFile[0] to '\0' so that GetOpenFileName does not 
    // use the contents of szFile to initialize itself.
    //
    ofn.lpstrFile[0] = '\0';
    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] != '\0')
    {
    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 是一个空指针;

我想我的问题是,如何有效地使用此功能来显示我的图像?

【问题讨论】:

  • 以允许的值传递pTest,换句话说,类似于int pTest; OpenBmpFile((char*)file, (char*)file2, &pTest, hWnd);
  • 谢谢,由于某种原因,即使我只打开一个 .bmp 图像,它也会显示“多项选择”。 :/ 我已经被这个问题困扰了一整天了,我真的厌倦了。如果只有 WinAPI 可以简化事情……尽管它毕竟是一种古老的语言。

标签: c++ c winapi buffer


【解决方案1】:

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

如果lpstrFile 指向后面的字符串"c:\dir1\dir2\file.ext",则该成员包含值13,表示"file.ext" 字符串的偏移量。如果用户选择了多个文件,nFileOffset 是第一个文件名的偏移量。

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

...如果设置了OFN_ALLOWMULTISELECT 标志并且用户选择了多个文件,则缓冲区包含当前目录,后跟所选文件的文件名。对于资源管理器样式的对话框,目录和文件名字符串以 NULL 分隔,最后一个文件名后有一个额外的 NULL 字符。

所以,判断用户是否选择了多个文件,到lpstrFile的末尾(即到第一个null),然后检查它后面是否有另一个null,如果没有,则用户选择了多个文件。

要构造每个文件的完整路径和文件名,请重复使用直到nFileOffset 的部分并连接每个文件名。返回时使用调试器检查ofn 以了解所有详细信息。

【讨论】:

  • 还应该提示OP代码中的szFilePath缓冲区对于多选结果来说太小了,如果缓冲区太小应该调用CommDlgExtendedError()来检查条件。
  • 如果您阅读OFN_ALLOWMULTISELECT标志的文档,如果选择了多个文件,则目录和文件名列表由NUL字符分隔,nFileOfset指向第一个文件名,所以nFileOffset-1 将指向目录后面的 NUL 字符。如果选择了单个文件,则该 NUL 字符不存在。
  • @Remy Lebeau,小修正:如果指定了OFN_ALLOWMULTISELECT,则始终会有一个终止的双空值,即使选择了单个文件也是如此。如果未指定标志,则始终只有一个文件名且没有双空。目录后面永远不会有空字符; nFileOffset 只是指向全路径和文件名中文件名的开头。
  • @PaulOgilvie "目录后面永远不会有 NULL 字符" - 是的,有。显然您没有仔细阅读文档:“对于资源管理器样式的对话框,目录和文件名字符串是 NULL 分隔的”。 OP正在显示资源管理器样式对话框,并且在选择多个文件后,目录后存在NUL字符。我测试了它。请参阅我的答案中的代码。
  • @PaulOgilvie:在您的回答中,“要确定用户是否选择了多个文件,请转到lpstrFile 的末尾(即第一个空),然后检查如果后面有另一个null,如果没有,那么用户选择了多个文件”是错误的。如果nFileOffset-1处有分隔符,则选择多个文件,否则选择一个文件。如果指定OFN_EXPLORER,则分隔符为空字符,否则分隔符为空格字符。这在文档中有明确的概述。
【解决方案2】:

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,使它更易于使用。它返回一个std::vector,其中包含用户选择的文件的绝对路径。如果用户单击“取消”,则该向量将为空。如果出现任何错误,它会引发 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;
}

【讨论】:

  • 虽然我很欣赏指出新界面,这是 C++,我想看看 C 版本。
【解决方案3】:

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

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

在选择多个文件时,尤其是具有长文件名的文件,结果字符数据可以容易地增长超出固定长度缓冲区的大小。正如OPENFILENAME documentation 所说:

lpstr文件
类型:LPTSTR

用于初始化文件名​​编辑控件的文件名。如果不需要初始化,此缓冲区的第一个字符必须为 NULL。当GetOpenFileNameGetSaveFileName 函数成功返回时,此缓冲区包含所选文件的驱动器标识符、路径、文件名和扩展名。

如果设置了OFN_ALLOWMULTISELECT 标志并且用户选择了多个文件,则缓冲区包含当前目录,后跟所选文件的文件名。对于资源管理器样式的对话框,目录和文件名字符串以 NULL 分隔,最后一个文件名后有一个额外的 NULL 字符。对于旧式对话框,字符串以空格分隔,函数使用短文件名作为带空格的文件名。您可以使用FindFirstFile 函数在长文件名和短文件名之间进行转换。如果用户只选择一个文件,lpstrFile 字符串在路径和文件名之间没有分隔符。

如果缓冲区太小,函数返回FALSECommDlgExtendedError 函数返回FNERR_BUFFERTOOSMALL。在这种情况下,lpstrFile 缓冲区的前两个字节包含所需的大小,以字节或字符为单位

nMaxFile
类型:DWORD

lpstrFile 指向的缓冲区的大小(以字符为单位)。缓冲区必须足够大以存储路径和文件名字符串或字符串,包括终止 NULL 字符。 如果缓冲区太小而无法包含文件信息,GetOpenFileNameGetSaveFileName 函数将返回 FALSE。缓冲区的长度应至少为 256 个字符

你没有考虑到这一点。 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)\0*.bmp\0\0";
    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 '\0' so that GetOpenFileName does not 
        // use the contents of lpstrFile to initialize itself.
        //
        ofn.lpstrFile[0] = '\0';

        // 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] != '\0')
    {
        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] = '\0';

        *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 != '\0');

    free(path);
    free(filenames);
}

更新:或者,为了简化返回文件名的使用,您可以执行更多类似的操作:

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)\0*.bmp\0\0";
    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 '\0' so that GetOpenFileName does not 
        // use the contents of lpstrFile to initialize itself.
        //
        ofn.lpstrFile[0] = '\0';

        // 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] != '\0')
    {
        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 != '\0')
        {
            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 != '\0')
        {
            strncpy(out, path, pathLen);
            out += pathLen;
            if (slashNeeded) *out++ = '\\';

            int filenameLen = strlen(filename);
            strncpy(out, filename, filenameLen);
            out += filenameLen;
            *out++ = '\0';

            filename += (filenameLen + 1);
        }

        *out = '\0';
    }

    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 != '\0');

    free(filenames);
}

【讨论】:

  • 嗨 Remy,非常感谢您提供的信息。我知道我无法摆脱像这样的类型转换。事情是,我对 WinAPI 很陌生,我不是它的语法的忠实粉丝,尽管我强迫自己用它创建一个应用程序。无论如何,你能不能看看我到目前为止的完整代码(这里:textuploader.com/dtchw)并告诉我应该如何使用它?因为目前我的加载位图函数不接受 char* 文件名,我不想做任何类型转换。
  • @FearlessHobbit 我给你的这段代码将目录和文件名分开,即使是单选。在调用LoadAndBlitBitmap() 之前,您的循环需要将它们重新组合在一起,例如使用PathCombine()。我选择不为每个报告的filename 加上相同的path 前缀以节省内存。不过,改变这种行为是微不足道的。
  • 嗯,好的,谢谢,那我会试着把它们结合起来,希望结合起来不会太难,我会让你知道事情的进展。非常感谢你的帮助。顺便说一下,我的程序背后的想法是制作一个使用多线程同时加载位图的应用程序。
  • 好吧,我觉得这样更好。 by if (insertSlash) *out++ = '\\'; 你的意思是把slashNeeded 作为表达式吗?
  • 哦,你认为你可以给我一个例子,说明如何使用 PathCombine() 加载图像吗?这会很有帮助。
猜你喜欢
  • 1970-01-01
  • 2019-03-16
  • 2020-09-16
  • 1970-01-01
  • 1970-01-01
  • 2018-07-11
  • 1970-01-01
  • 2020-09-07
  • 2011-06-09
相关资源
最近更新 更多