最近在做Windows 開發過程中遇到一個問題,需要打開文件對話框和另存爲對話框,獲取所選文件的路徑信息,這部分很容易就實現了,可當另存爲文件時如何獲得格式不同文件的擴展名卻遇到了一點問題,經過一番資料搜索和官方文檔查閱,遂解決。先說心得,windows API 相關的問題還是要直接查詢微軟官方文檔來的快,上面解釋的很清楚,不要被英文和那些奇奇怪怪的定義嚇到,自己動手實現幾次就明白是怎麼回事了。東拉西扯,進入正題,
Windows 打開和另存爲對話框如下所示,
(1) 打開對話框
(2)另存爲對話框
這裏用一個例子說明幾個概念,文件目錄,文件路徑,文件名,如下圖所示,在F盤Demo文件夾下有一個名爲"Test"的txt文件
文件Test.txt的文件名是“Test”
文件目錄是 “F:\Demo";
文件路徑是:“F:\Demo\Test.txt";
文件擴展名是:“.txt”
這打開“打開”對話框以及打開“另存爲對話框”這兩個兩個窗口涉及到的API分別爲
GetOpenFileName()
GetSaveFileName()
微軟官方的文檔的建議是這兩個API已經過時了,建議使用 IFileOpenDialog 或者IFileSaveDialog,這個不是本文的主題,有興趣的朋友可以去官網查詢新方法的用法,此處暫且不表。
以GetOpenFileName() 爲例,做以解釋。官方文檔給出的函數定義是
BOOL GetOpenFileNameA(
LPOPENFILENAMEA Arg1
);
參數 Arg1 類型 LPOPENFILENAME
(小提示,如果覺得官方文檔大段的英文看起來很費勁,可以用chrome瀏覽器自帶的翻譯功能,鼠標右鍵有翻譯選項,翻譯的結果也八九不離十)關於類型LPOPENFILENAME 文檔給出的解釋是:
A pointer to an OPENFILENAME structure that contains information used to initialize the dialog box. When GetSaveFileName returns, this structure contains information about the user’s file selection.(一個指向OPENFILENAME結構的指針,該結構包含用於初始化對話框的信息。當GetSaveFileName返回時,此結構包含有關用戶文件選擇的信息。)
因此我們知道GetOpenFileNameA的參數我們需要傳入的是一個指向 OPENFILENAME 結構的指針,馬不停蹄的我們要立即看看這個OPENFILENAME 結構的內容是什麼,因爲只有知道他的結構我們才能知道如何初始化我們的結構體指針。如果經常使用Windows API 你就會發現這是很常見的模式。 不同的開發人員有不同的需要,Windows 把框架丟給你,讓你自己定義,比如我們打開的窗口的標題,窗口的模態,保存文件類型的數目,是否允許多選,等等。這也是我們初始化這個結構體的意義所在。
點進OPENFILENAME 的鏈接,我們看到如下結構體定義
typedef struct tagOFNA {
DWORD lStructSize;
HWND hwndOwner;
HINSTANCE hInstance;
LPCSTR lpstrFilter;
LPSTR lpstrCustomFilter;
DWORD nMaxCustFilter;
DWORD nFilterIndex;
LPSTR lpstrFile;
DWORD nMaxFile;
LPSTR lpstrFileTitle;
DWORD nMaxFileTitle;
LPCSTR lpstrInitialDir;
LPCSTR lpstrTitle;
DWORD Flags;
WORD nFileOffset;
WORD nFileExtension;
LPCSTR lpstrDefExt;
LPARAM lCustData;
LPOFNHOOKPROC lpfnHook;
LPCSTR lpTemplateName;
LPEDITMENU lpEditInfo;
LPCSTR lpstrPrompt;
void *pvReserved;
DWORD dwReserved;
DWORD FlagsEx;
} OPENFILENAMEA, *LPOPENFILENAMEA;
文檔裏面每一項都解釋的很清楚,這裏就不做一一解讀了。挑其中幾個用的比較多的解釋一下。
lStructSize:結構的長度,以字節爲單位。此參數使用 sizeof (OPENFILENAME)。
hwndOwner:擁有該對話框的窗口的句柄。這個成員可以是任何有效的窗口句柄。如果對話框裏有沒有所有者,它可以爲 NULL。
lpstrFilter:篩選器,這個關鍵字很常用,他決定着對話框的文件類型。這個關鍵字通常由兩對字符串構成,每一對中的第一個字符串是一個顯示字符串,描述了該篩選器 (例如,“文本文件”),和第二個字符串指定篩選器模式 (例如,"*.TXT")。例如
ofn.lpstrFiler = TEXT("所有文件\0*.*\0\0");
我們打開的對話框保存類型如下所示,
如果我們想要在一個條目內有多個格式選擇,以分號分割,例如我們只想打開txt或者doc文件,以此類推;
ofn.lpstrFiler = TEXT("所有文件\0*.txt;*.doc*\0\0");
如果我們想顯示多個條目,實現如下圖效果,
則繼續添加篩選器即可,例如
ofn.lpstrFilter = TEXT("所有文件\0*.*\0文本 文件(*.txt)\0*.txt*\0圖片(*.jpg)\0*.jpg*\0\0");
nFilterIndex:在文件類型控件中當前選定的篩選器的索引。 如果你有3條篩選器,這個值指定爲2, 則打開對話框時默認顯示的文件類型是第二條篩選器。 這個值還有一個重要的應用就是如果當我們設置了對話框回調函數時,這個值保存着用戶的篩選器選擇,有了他我們便可以知道用戶選擇文件的對應的擴展名。(在另存爲對話框)
nMaxFile:以字符數計算,lpstrFile 所指向的緩衝區的大小。緩衝區必須足夠大以存儲的路徑和文件名稱字符串或字符串,包括終止 NULL 字符。緩衝區應至少 256 個字符長。
lpstrInitialDir:初始目錄。留空由系統選擇,但選擇的初始目錄的算法在不同系統上各不相同。
lpstrTitle:要放在對話框的標題欄中的字符串。如果此成員爲 NULL,則系統使用默認標題 。
lpstrFile:用來初始化文件名稱的文件名稱的編輯控件。
當使用 GetOpenFileName時, 如過只允許選擇單個文件,該緩衝區包含驅動器符、 路徑、 文件名稱和所選文件的擴展名, 如果設置可以多選文件,則lpstrFile 包含當前選擇文件的目錄,後面跟着對應所選的多個文件的文件名已經擴展名。
而當使用GetSaveFileName時,lpstrFile 只包含當前文件目錄以及文件名,並沒有包含文件擴展名,這是和打開文件對話框不同的地方,如果我們希望獲取用戶選擇的對應文件的文件擴展名,需要通過設置lpfnHook 回調函數來獲取這一參數,後面會說到。
Flags: 設置對話框風格標誌位。如果希望實現多選文件,則可以設置標誌位爲
ofn.Flags = OFN_EXPLORER |OFN_ALLOWMULTISELECT;
一定要有OFN_EXPLOER, 否則窗口風格會變。下面上代碼
int main()
{
OPENFILENAME ofn = { 0 };
TCHAR strFilename[MAX_PATH] = { 0 }; //用於接收文件名
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.lpstrFile = strFilename;
ofn.lpstrFilter =TEXT("Cpp(*.cpp)\0*.cpp*\0文本(*.txt)\0*.txt*\0圖片(*.jpg)\0*.jpg*\0\0");
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_EXPLORER|OFN_HIDEREADONLY |OFN_NOCHANGEDIR |OFN_PATHMUSTEXIST| OFN_ALLOWMULTISELECT;
//ofn.lpfnHook = (LPOFNHOOKPROC)SaveAsHookPrc;
TCHAR * pszFileName;
CString szDirectory;
CString szFileName;
CStringArray szMultiFliePath; //如果允許多選,該array中記錄着所有被選中的文件路徑。
if (GetOpenFileName(&ofn))
{
pszFileName = ofn.lpstrFile;
szDirectory = pszFileName;
//如果只允許單選, 緩衝區ofn.lpstrFile包含完整的文件路徑(包括文件名以及擴展名)
//如果允許多選,緩衝區ofn.lpstrFile指針包含內容分爲兩個部分,第一部分是選擇文件所在的目錄,第二部分是選擇的所有文件名,每個文件名以“\0”作爲分隔;
//緩衝區中的最後一個字符串以兩個空字符終止,因此可以通過移動指針來判斷是否包含多個文件
pszFileName = pszFileName + szDirectory.GetLength() + 1;
while (*pszFileName)
{
szFileName = pszFileName;
szMultiFliePath.Add(szDirectory + pszFileName);
pszFileName = pszFileName + szFileName.GetLength() + 1;
}
}
return true;
}
“另存爲”對話框的使用方法與“打開”對話框的使用方法類似,這裏要重點說明的是如何獲取另存爲對話框的文件擴展名。因爲使用GetSaveFileName() 如果用戶沒有鍵入文件的擴展名,ofn對象的文件緩衝區並不包含文件的擴展名,
如下圖所示
如果用戶不手動添加文件擴展名“.txt”,ofn.lpstrFile 文件緩衝區只能得到不包含文件擴展名的文件路徑。
如果我們想實現自動根據用戶選擇的文件擴展來添加擴展名,可以通過添加回調函數到ofn.lpfnHook的方法來獲取用戶的選擇的nFilterIndex. 其中nFilterIndex的值與 ofn.lpstrFilter 設定的順序一樣。例如,
ofn.lpstrFilter =TEXT("Cpp(*.cpp)\0*.cpp*\0文本(*.txt)\0*.txt*\0圖片(*.jpg)\0*.jpg*\0\0");
當用戶選擇第一項(Cpp)時,nFilterIndex爲1,選擇第二項(文本)時,nFilterIndex =2, 以此類推。
當用戶選擇了不同的文件名擴展,windows會講用戶選擇響應一個CDN_TYPECHANGE消息,而該消息包含在WM_NOTIFY中,同時CDN_TYPECHANGE消息包含一個指向OFNOTIFY的結構指針,該結構指針裏包含了我們所需要的對象LPOPENFILENAMEA 指針。代碼如下
設置nFlag 爲
ofn.Flags = OFN_EXPLORER| OFN_ALLOWMULTISELECT|OFN_ENABLEHOOK;
同時添加回調函數:CbGetFileExtension();
#include <afx.h>
#include <windows.h>
#include <commdlg.h>
#include <iostream>
#include <string>
#include <cstring>
using namespace std;
static int g_nFilterIndex(0); // 記錄用戶選擇的index.
void GetFilterInde(OFNOTIFY* pFile)
{
switch (pFile->lpOFN->nFilterIndex)
{
case 1:
g_nFilterIndex = 1;
break;
case 2:
g_nFilterIndex = 2;
break;
case 3:
g_nFilterIndex = 3;
break;
default:
break;
}
}
UINT_PTR CALLBACK CbGetFileExtension(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_NOTIFY:
{ NMHDR *pHdr = (NMHDR*)lParam;
switch (pHdr->code)
{
case CDN_TYPECHANGE:
GetFilterInde((OFNOTIFY*)pHdr);
break;
default:
break;
}
break;
}
default:
break;
}
return 0;
}
int main()
{
OPENFILENAME ofn = { 0 };
TCHAR strFilename[MAX_PATH] = { 0 };
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.lpstrFile = strFilename;
ofn.lpstrFilter = TEXT("Cpp(*.cpp)\0*.cpp*\0文本(*.txt)\0*.txt*\0圖片(*.jpg)\0*.jpg*\0\0");
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_EXPLORER | OFN_PATHMUSTEXIST |OFN_ENABLEHOOK;
ofn.lpfnHook = &CbGetFileExtension; //添加回調函數
CString szFilePath;
CStringArray szMultiFliePath;
if (GetSaveFileName(&ofn))
{
szFilePath = ofn.lpstrFile;
switch (g_nFilterIndex)
{
case 1:
szFilePath = szFilePath + ".cpp";
case 2:
szFilePath = szFilePath + ".txt";
break;
case 3:
szFilePath = szFilePath + ".jpg";
break;
default:
break;
}
}
return true;
}