C++程序操作文件對話框的方法

在C++程序中有時需要通過系統的文件對話框去操作文件或者文件夾,我們有必要熟練掌握操作文件對話框的細節,去更好的為我們軟件服務。本文我們就來講述一下C++在操作文件夾對話框的相關細節,給大傢借鑒和參考。

1、調用GetOpenFileName接口啟動打開文件對話框

當我們需要使用文件對話框打開一個文件時,我們可以調用系統API函數GetOpenFileName來啟動系統的打開文件對話框。調用該API函數時需要傳入OPENFILENAME結構體對象,通過該結構體給打開文件對話框設置一些屬性參數。

1.1、OPENFILENAME結構體說明

結構體OPENFILENAME的定義有兩個版本,一個Unicode版本的OPENFILENAMEW,一個ANSI版本的OPENFILENAMEA,這個地方我們以Unicode版本的OPENFILENAMEW來說明,該結構體的定義如下:

typedef struct tagOFNW {
   DWORDlStructSize;
   HWND hwndOwner;
   HINSTANCEhInstance;
   LPCWSTR  lpstrFilter;
   LPWSTR   lpstrCustomFilter;
   DWORDnMaxCustFilter;
   DWORDnFilterIndex;
   LPWSTR   lpstrFile;
   DWORDnMaxFile;
   LPWSTR   lpstrFileTitle;
   DWORDnMaxFileTitle;
   LPCWSTR  lpstrInitialDir;
   LPCWSTR  lpstrTitle;
   DWORDFlags;
   WORD nFileOffset;
   WORD nFileExtension;
   LPCWSTR  lpstrDefExt;
   LPARAM   lCustData;
   LPOFNHOOKPROC lpfnHook;
   LPCWSTR  lpTemplateName;
#ifdef _MAC
   LPEDITMENU   lpEditInfo;
   LPCSTR   lpstrPrompt;
#endif
#if (_WIN32_WINNT >= 0x0500)
   void *pvReserved;
   DWORDdwReserved;
   DWORDFlagsEx;
#endif // (_WIN32_WINNT >= 0x0500)
} OPENFILENAMEW, *LPOPENFILENAMEW;

這個地方我們講一下幾個我們使用到的字段:

  • lStructSize字段[in]:用來存放當前結構體的長度。
  • hwndOwner字段[in]:用來指定文件對話框的父窗口。
  • lpstrFilter字段[in]:用來設置選擇的文件類型,如果要選擇所有文件,則設置“All Files(*.*)\0*.*\0\0”;如果選擇的是常用的圖片文件,則可以設置“圖片文件(*.bmp;*.jpg;*.jpeg;*.gif;*.png)\0*.bmp;*.jpg;*.jpeg;*.gif;*.png\0\0”。
  • lpstrFile字段[out]:該字段用來傳出用戶選擇的文件信息,存放選擇的文件信息的buffer是需要外部申請內存的,然後將buffer地址賦值給該字段,然後在GetOpenFileName調用完成後通過該字段指定的buffer輸出用戶選擇的文件信息。
  • nMaxFile字段[in]:用來指定設置給lpstrFile字段對應的buffer長度的。
  • lpstrInitialDir字段[in]:用來指定打開文件對話框時的初始顯示路徑。
  • Flags字段[in]:用來設定文件對話框支持的屬性,比如支持多選的OFN_ALLOWMULTISELECT、選擇的文件必須存在OFN_FILEMUSTEXIST等。

1.2、設置支持文件多選,控制選擇文件的個數上限

要支持文件多選,需要給OPENFILENAME結構體的Flags字段添加OFN_ALLOWMULTISELECT選項。如果要控制選擇文件的個數上限,比如我們在IM聊天框中限制用戶插入圖片的個數(比如我們此處限制為5個圖片文件):

我可以在給OPENFILENAME結構體參數設置lpstrFilter字段指定接收用戶選擇信息時指定buffer的長度最多存放5張圖片的路徑。

如果選擇多個文件時的文件信息超過buffer的長度,則GetOpenFileName會返回失敗,此時可以調用CommDlgExtendedError獲取錯誤碼,如果錯誤碼是FNERR_BUFFERTOOSMALL,則表示buffer太小存不下選擇的多個圖片文件信息,此時可以彈出提示框,相關代碼如下:

// 設置最多選擇5個文件的buffer長度
int nBufLen = 5*(MAX_PATH+1) + 1;
TCHAR* pBuf = new TCHAR[nBufLen];
memset( pBuf, 0, nBufLen*sizeof(TCHAR) );
 
OPENFILENAME ofn;
memset( &ofn, 0 ,sizeof(ofn) );
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = m_hParentWnd;
ofn.lpstrFile = pBuf;
ofn.nMaxFile = nBufLen;
ofn.lpstrFilter = _T("All Files(*.*)\0*.*\0\0");
ofn.lpstrFileTitle = NULL;
ofn.nMaxFileTitle = 0;
ofn.lpstrInitialDir = NULL;
ofn.Flags = OFN_EXPLORER|OFN_ALLOWMULTISELECT|OFN_FILEMUSTEXIST|OFN_HIDEREADONLY;
// 打開文件打開對話框
BOOL bRet = ::GetOpenFileName( &ofn );
if ( !bRet )
{
    // 上面的ofn.lpstrFile buffer的長度設置存放3個圖片路徑,選擇較多文件時
    // 會超出給定的buffer長度,所以此處要判斷一下這樣的情況,並給出提示5
    DWORD dwErr = CommDlgExtendedError();
    if ( dwErr == FNERR_BUFFERTOOSMALL ) 
    {
        // FNERR_BUFFERTOOSMALL - The buffer pointed to by the lpstrFile member 
        // of the OPENFILENAME structure is too small
        ::MessageBox( NULL, _T("最多隻允許選擇5個文件,請重新選擇"), _T("提示"), MB_OK );
    }
 
    delete []pBuf;
    return;
}
 
// 再檢查一下用戶選擇的文件個數,檢測是否達到上限
int nPicTotal = 0;
UIPOSITION pos = (UIPOSITION)ofn.lpstrFile;
while ( pos != NULL )
{
    // 獲取用戶選擇的每個文件的路徑,此處可以將文件的路徑保存下來
    strPicPath = GetNextPathName( pos, ofn );
    ++nPicTotal;
}
 
if ( nPicTotal > 5 )
{
    ::MessageBox( NULL, _T("最多隻允許選擇5個文件,請重新選擇"), _T("提示"), MB_OK );
    delete []pBuf;
    return;
}

當然,我們僅僅通過FNERR_BUFFERTOOSMALL錯誤判斷文件個數超限瞭,是不夠的。可能用戶選擇瞭6個文件時,總的buffer不會超限,所以還要在解析出來用戶選擇的每個文件後計算一下選擇的文件個數。

1.3、從OPENFILENAME結構體的lpstrFile字段解析出用戶選擇的文件的完整路徑

上面我們講瞭,用戶在選擇完多個文件後,點擊確定,API函數GetOpenFileName返回,傳給該API函數的OPENFILENAME結構體對象參數的lpstrFile字段中存放這用戶選擇的文件信息,比如我選擇瞭3個文件(docker.png、linux.png、running.png),lpstrFile中存放的數據格式會是:

C:\Users\Administrator\Desktop\CSDN文章圖集\docker.png\0linux.png\0running.png

因為這段字串中包含\0,所以我們不能把lpstrFile當成字符串來處理。我們專門封裝瞭接口GetNextPathName,該接口負責將每個文件的完整路徑解析:

// 主要用於文件打開對話框選中多個文件時,解析出多個文件名(絕對路徑),從MFC庫中的CFileDialog類的GetNextPathName函數中剝離出來的
CUIString GetNextPathName( UIPOSITION& pos, OPENFILENAME& ofn ) 
{
    BOOL bExplorer = ofn.Flags & OFN_EXPLORER;
        TCHAR chDelimiter;
    if ( bExplorer )
    {
        chDelimiter = '\0';
    }
    else
    {
        // For old-style dialog boxes, the strings are space separated and the function uses 
        // short file names for file names with spaces. 
        chDelimiter = ' '; 
    }
 
    LPTSTR lpsz = (LPTSTR)pos;
    if ( lpsz == ofn.lpstrFile ) // first time
    {
        if ( (ofn.Flags & OFN_ALLOWMULTISELECT) == 0 )
        {
            pos = NULL;
            return ofn.lpstrFile;
        }
 
        // find char pos after first Delimiter
        while( *lpsz != chDelimiter && *lpsz != '\0' )
        lpsz = _tcsinc( lpsz );
        lpsz = _tcsinc( lpsz );
 
        // if single selection then return only selection
        if ( *lpsz == 0 )
        {
            pos = NULL;
            return ofn.lpstrFile;
        }
    }
 
    CString strPath = ofn.lpstrFile;
    if ( !bExplorer )
    {
        LPTSTR lpszPath = ofn.lpstrFile;
        while( *lpszPath != chDelimiter )
            lpszPath = _tcsinc(lpszPath);
        strPath = strPath.Left( lpszPath - ofn.lpstrFile );
    }
 
    LPTSTR lpszFileName = lpsz;
    CString strFileName = lpsz;
 
    // find char pos at next Delimiter
    while( *lpsz != chDelimiter && *lpsz != '\0' )
        lpsz = _tcsinc(lpsz);
 
    if ( !bExplorer && *lpsz == '\0' )
        pos = NULL;
    else
    {
        if ( !bExplorer )
            strFileName = strFileName.Left( lpsz - lpszFileName );
 
            lpsz = _tcsinc( lpsz );
        if ( *lpsz == '\0' ) // if double terminated then done
            pos = NULL;
        else
            pos = (UIPOSITION)lpsz;
     }
 
    // only add '\\' if it is needed
    if ( !strPath.IsEmpty() )
    {
        // check for last back-slash or forward slash (handles DBCS)
        LPCTSTR lpsz = _tcsrchr( strPath, '\\' );
        if ( lpsz == NULL )
            lpsz = _tcsrchr( strPath, '/' );
        // if it is also the last character, then we don't need an extra
        if ( lpsz != NULL &&
        (lpsz - (LPCTSTR)strPath) == strPath.GetLength()-1 )
        {
            ASSERT( *lpsz == '\\' || *lpsz == '/' );
            return strPath + strFileName;
        }
    }
 
    return strPath + '\\' + strFileName;
}

我們在外部循環調用GetNextPathName函數即可:

UIPOSITION pos = (UIPOSITION)ofn.lpstrFile;
while ( pos != NULL )
{
    // 獲取用戶選擇的每個文件的路徑,此處可以將文件的路徑保存下來
    strPicPath = GetNextPathName( pos, ofn );
    ++nPicTotal;
}

2、調用GetSaveFileName接口啟動保存文件對話框

當我們需要使用保存文件對話框保存一個文件時,我們可以調用系統API函數GetSaveFileName來啟動系統的保存文件對話框。調用該函數時也是傳入OPENFILENAME結構體對象的,通過該結構體給打開文件對話框設置一些屬性參數。

對於GetSaveFileName接口,OPENFILENAME結構體中的lpstrFile字段的含義是不同的,此處用來設置保存文件的默認名字的。調用函數的示例代碼如下:

TCHAR szFile[3*MAX_PATH] = { 0 };
_tcscpy( szFile, strFileName );
OPENFILENAME ofn;
memset( &ofn, 0 ,sizeof(ofn) );
ofn.lStructSize = sizeof(OPENFILENAME); 
ofn.hwndOwner = NULL;
ofn.lpstrFilter = _T("All Files(*.*)|0*.*||"); 
ofn.lpstrFile = szFile; 
ofn.nMaxFile = sizeof(szFile)/sizeof(TCHAR); 
ofn.lpstrDefExt = (LPCTSTR)strExt;
ofn.lpstrInitialDir = NULL; 
ofn.Flags = OFN_EXPLORER|OFN_PATHMUSTEXIST|OFN_HIDEREADONLY|OFN_NOCHANGEDIR|OFN_OVERWRITEPROMPT|OFN_ENABLEHOOK; 
ofn.lpfnHook = FileSaveAs_OFNHookProc; // 設置回調函數,添加額外的處理機制if ( ::GetSaveFileName( &ofn ) )
{
    // 用戶最終輸入的文件名的完整路徑
    CString strPathName = ofn.lpstrFile;
}

在上述代碼中,我們添加瞭OFN_OVERWRITEPROMPT屬性,用來指定覆蓋同名文件時給出提示;我們還添加瞭OFN_ENABLEHOOK屬性,用來指定設置回調函數,在函數中添加額外的處理機制。在本例中,我們的HOOK回調函數如下:

// 文件對話框回調函數
UINT_PTR CALLBACK FileSaveAs_OFNHookProc(
HWND hdlg,  // handle to child dialog box
UINT uiMsg, // message identifier
WPARAM wParam,  // message parameter
LPARAM lParam   // message parameter
)
{
    if( uiMsg == WM_NOTIFY) 
    {
        LPOFNOTIFY pofn = (LPOFNOTIFY)lParam;
        if( pofn->hdr.code == CDN_FILEOK )
        {
            HWND hFileDlg = ::GetParent( hdlg );
 
            // 獲取用戶選擇保存路徑
            TCHAR achCurPath[MAX_PATH*3] = { 0 };
            int nRet = ::SendMessage( hFileDlg, CDM_GETFOLDERPATH,                        sizeof(achCurPath)/sizeof(TCHAR),
            (LPARAM)achCurPath );
            if ( nRet < 0 )
            {
            return 0;
            }
 
            // 判斷當前程序對選擇的目錄是否有寫權限
            BOOL bCanWrite = CanAccessFile( achCurPath, GENERIC_WRITE );
            if ( !bCanWrite )
            {
                ::MessageBox( hFileDlg, STRING_HAVE_NO_RIGHT_SAVE_FILE, STRING_TIP,         MB_OK|MB_ICONWARNING );
 
                ::SetWindowLong( hdlg, DWL_MSGRESULT, 1 );
                return 1;
            }
        }
    }
 
    return 0;
}

在HOOK回調函數中我們攔截瞭點擊確定的消息,判斷一下用戶當前選擇的路徑有沒有權限保存文件。因為在Win7及以上系統中,如果程序沒有管理員權限,是沒有權利向C:\Windows、C:\Program Files等系統關鍵路徑中寫入文件的。

判斷程序是否對某個路徑有寫權限,我們封裝瞭專門的判斷函數,如下所示:

// 將要檢測的權限GENERIC_XXXXXX傳遞給dwGenericAccessMask,可檢測對
// 文件或者文件夾的權限
BOOL CanAccessFile( CString strPath, DWORD dwGenericAccessMask )  
{  
    CString strLog;
    strLog.Format( _T("[CanAccessFile] strPath: %s, dwGenericAccessMask: %d"), strPath,     dwGenericAccessMask );
    WriteUpdateLog( strLog );
 
    DWORD dwSize = 0;  
    PSECURITY_DESCRIPTOR psd = NULL;  
    SECURITY_INFORMATION si = OWNER_SECURITY_INFORMATION | 
    GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION;
 
    WriteLog( _T("[CanAccessFile] GetFileSecurity - NULL") );
 
    // 獲取文件權限信息結構體大小  
    BOOL bRet = GetFileSecurity( strPath, si, psd, 0, &dwSize );  
    if ( bRet || GetLastError() != ERROR_INSUFFICIENT_BUFFER )  
    {  
        strLog.Format( _T("[CanAccessFile] GetFileSecurity - NULL failed, GetLastError: %d"), GetLastError() );
        WriteLog( strLog );
        return FALSE;  
    }  
 
 
    char* pBuf = new char[dwSize];
    ZeroMemory( pBuf, dwSize );
    psd = (PSECURITY_DESCRIPTOR)pBuf;  
 
    strLog.Format( _T("[CanAccessFile] GetFileSecurity - dwSize: %d"), dwSize );
    WriteLog( strLog );
 
    // 獲取文件權限信息結構體大小  
    bRet = GetFileSecurity( strPath, si, psd, dwSize, &dwSize );  
    if ( !bRet )  
    {  
        strLog.Format( _T("[CanAccessFile] GetFileSecurity - dwSize failed, GetLastError: %d"), GetLastError() );
        WriteLog( strLog );
        delete []pBuf;
        return FALSE;  
    }  
 
    WriteLog( _T("[CanAccessFile] OpenProcessToken") );
 
    HANDLE hToken = NULL;  
    if ( !OpenProcessToken( GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken ) )  
    {  
        strLog.Format( _T("[CanAccessFile] OpenProcessToken failed, GetLastError: %d"),     GetLastError() );
        WriteLog( strLog );
        delete []pBuf;
        return FALSE;  
    }  
 
    WriteLog( _T("[CanAccessFile] DuplicateToken") );
 
    // 模擬令牌  
    HANDLE hImpersonatedToken = NULL;  
    if( !DuplicateToken( hToken, SecurityImpersonation, &hImpersonatedToken ) )  
    {  
        strLog.Format( _T("[CanAccessFile] DuplicateToken failed, GetLastError: %d"),     GetLastError() );
        WriteLog( strLog );
 
        delete []pBuf;
        CloseHandle( hToken );
        return FALSE;  
    }  
 
    WriteLog( _T("[CanAccessFile] MapGenericMask") );
  
    // 在檢測是否有某個權限時,將GENERIC_WRITE等值傳給本函數的第二個參數dwGenericAccessMask
    // GENERIC_WRITE等參數在調用CreateFile創建文件時會使用到,下面調用MapGenericMask將
    // GENERIC_WRITE等轉換成FILE_GENERIC_WRITE等
    // 將GENERIC_XXXXXX轉換成FILE_GENERIC_XXXXXX
    GENERIC_MAPPING genMap;
    genMap.GenericRead = FILE_GENERIC_READ;  
    genMap.GenericWrite = FILE_GENERIC_WRITE;  
    genMap.GenericExecute = FILE_GENERIC_EXECUTE;  
    genMap.GenericAll = FILE_ALL_ACCESS;  
    MapGenericMask( &dwGenericAccessMask, &genMap );  
 
    WriteLog( _T("[CanAccessFile] AccessCheck") );
 
    // 調用AccessCheck來檢測是否有指定的權限
    PRIVILEGE_SET privileges = { 0 };  
    DWORD dwGrantedAccess = 0;  
    DWORD privLength = sizeof(privileges);  
    BOOL bGrantedAccess = FALSE;  
    if( AccessCheck( psd, hImpersonatedToken, dwGenericAccessMask, 
&genMap, &privileges, &privLength, &dwGrantedAccess, &bGrantedAccess ) )  
    {
        strLog.Format( _T("[CanAccessFile] AccessCheck succeed, dwGenericAccessMask: %d,     dwGrantedAccess: %d, bGrantedAccess: %d, "), 
dwGenericAccessMask, dwGrantedAccess, bGrantedAccess );
        WriteLog( strLog );
 
        if ( bGrantedAccess )
        {
            if ( dwGenericAccessMask == dwGrantedAccess )
            {
                bGrantedAccess = TRUE;
            }
            else
            {
                bGrantedAccess = FALSE;
            }
        }
        else
        {
            bGrantedAccess = FALSE;
        }
    }
    else
    {
        strLog.Format( _T("[CanAccessFile] AccessCheck failed, GetLastError: %d"), GetLastError() );
        WriteLog( strLog );
 
        bGrantedAccess = FALSE;
    }
 
    strLog.Format( _T("[CanAccessFile] bGrantedAccess: %d"), bGrantedAccess );
    WriteLog( strLog );
 
    delete []pBuf;
    CloseHandle( hImpersonatedToken );
    CloseHandle( hToken );
    return bGrantedAccess;  
} 

另外,我們可以在HOOK回調函數中調整文件對話框相對於父窗口的彈出位置,攔截WM_INITDIALOG消息,代碼如下:

// 通過該回調函數移動文件保存對話框的位置,解決win32 文件對話框默認顯示在屏幕左上方的問題
UINT_PTR CALLBACK OFNHookProc(
HWND hdlg,  // handle to child dialog box
UINT uiMsg, // message identifier
WPARAM wParam,  // message parameter
LPARAM lParam   // message parameter
)
{
    if ( uiMsg == WM_INITDIALOG )
    {
        HWND hFileDlg = ::GetParent( hdlg );
        HWND hParent = ::GetParent( hFileDlg );
 
        RECT rcParent;
        ::GetWindowRect( hParent, &rcParent );
 
        RECT rcFileDlg;
        ::GetWindowRect( hFileDlg, &rcFileDlg );
 
        int nFileDlgWdith = rcFileDlg.right - rcFileDlg.left;
        int nFileDlgHeight = rcFileDlg.bottom - rcFileDlg.top;
 
        RECT rcDst;
        rcDst.left = rcParent.left + ( (rcParent.right - rcParent.left) - nFileDlgWdith )/2;
        rcDst.right = rcDst.left + nFileDlgWdith;
        rcDst.bottom = rcParent.bottom + 1;
        rcDst.top = rcDst.bottom - nFileDlgHeight;
 
        // 下面保證文件對話框處於屏幕可視區域內
        if ( rcDst.left < 0 )
        {
            rcDst.left = 0;
            rcDst.right = rcDst.left + nFileDlgWdith;
        }
 
        if ( rcDst.top < 0 )
        {
            rcDst.top = 0;
            rcDst.bottom = rcDst.top + nFileDlgHeight;
        }
 
        int nXScreen = ::GetSystemMetrics( SM_CXSCREEN );
        int nYScreen = ::GetSystemMetrics( SM_CYSCREEN );
 
        if ( rcDst.right > nXScreen )
        {
            rcDst.right = nXScreen;
            rcDst.left = rcDst.right - nFileDlgWdith;
        }
 
        if ( rcDst.bottom > nYScreen )
        {
            rcDst.bottom = nYScreen;
            rcDst.top = rcDst.bottom - nFileDlgHeight;
        }
 
        // 重新設置文件對話框位置
        ::SetWindowPos( hFileDlg, NULL, rcDst.left, rcDst.top, 0, 0, SWP_NOSIZE | SWP_NOZORDER );
 
        return 1;
    }
 
    return 0;
} 

3、調用SHBrowseForFolder接口打開瀏覽文件夾對話框

當我們在IM聊天框中發起文件傳輸時,我們要發送的是一個文件夾,可能就需要使用到系統的瀏覽文件夾對話框來選擇要發送的文件夾。當我們在接收到多個文件時,可以選擇全部另存為,此時也需要使用系統的瀏覽文件夾對話框來選擇要另存為的路徑。

我們調用SHBrowseForFolder接口即可打開瀏覽文件夾對話框,相關代碼如下所示:

// 打開目錄選擇對話框
::CoInitialize( NULL );
 
LPMALLOC lpMalloc = NULL;
TCHAR  pchSelDir[MAX_PATH] = {0};
pchSelDir[0] = _T('\0');
if( E_FAIL == ::SHGetMalloc(&lpMalloc) )
{
::CoUninitialize();
return;
}
 
BROWSEINFO browseInfo;
browseInfo.hwndOwner  = m_hParentWnd;
browseInfo.pidlRoot   = NULL; 
browseInfo.pszDisplayName = NULL;
browseInfo.lpszTitle  = STRING_SEL_FOLDER_TO_SEND;   
browseInfo.ulFlags= BIF_RETURNONLYFSDIRS;  
browseInfo.lpfn   = NULL;   
browseInfo.lParam = 0;
 
// 打開瀏覽文件夾對話框
LPITEMIDLIST lpItemIDList = NULL;
if ( ( lpItemIDList = ::SHBrowseForFolder(&browseInfo) ) != NULL )
{
    // 獲取用戶選擇的文件夾
    ::SHGetPathFromIDList( lpItemIDList, pchSelDir );
 
    lpMalloc->Free( lpItemIDList );
    lpMalloc->Release();  
}
else
{
    lpMalloc->Free( lpItemIDList );
    lpMalloc->Release(); 
    ::CoUninitialize();
    return;
}
 
::CoUninitialize();

4、最後

到此這篇關於C++如何操作文件對話框的文章就介紹到這瞭,更多相關C++文件對話框內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: