【MFC】BROWSEINFO設定路徑,支援記憶上次路徑

Cain Xcy發表於2020-11-04

01、目錄

02、BROWSEINFO結構

BROWSEINFOW 結構是一種顯示檔案或者資料夾路徑的結構體。作為SHBrowseForFolder()函式的引數。

首先看下BROWSEINFO的原型(來自微軟官方文件):

typedef struct _browseinfoW {
  HWND              hwndOwner;
  PCIDLIST_ABSOLUTE pidlRoot;
  LPWSTR            pszDisplayName;
  LPCWSTR           lpszTitle;
  UINT              ulFlags;
  BFFCALLBACK       lpfn;
  LPARAM            lParam;
  int               iImage;
} BROWSEINFOW, *PBROWSEINFOW, *LPBROWSEINFOW;

引數介紹:

  1. hwndOwner:父視窗控制程式碼,通常通過AfxGetMainWnd()->GetSafeHwnd()函式獲取安全的控制程式碼。
  2. pidlRoot:顯示的檔案目錄對話方塊的根(Root),一般設定為NULL
  3. pszDisplayName:儲存被選取的資料夾路徑的緩衝區。
  4. lpszTitle:顯示位於對話方塊左上部的標題。
  5. ulFlags:指定對話方塊的外觀和功能的標誌。(下面會聊此引數的作用)
  6. lpfn:處理事件的回撥函式,一般設定為NULL
  7. lparam:應用程式傳給回撥函式的引數,一般設定為NULL
  8. iImage:資料夾對話方塊的圖片索引,一般設定為0。

說明:

一般而言父視窗控制程式碼(hwndOwner)和根(pidlRoot)設定為Null就可以了,pszDisplayName設定一塊MAX_PATH大小的緩衝區,跟顯示相關的引數就是對話方塊提示標題(lpszTitle)、對話方塊樣式(ulFlags)、設定對話方塊的預設路徑的操作(lpfnlParam)以及對話方塊工作列上顯示的圖示(iImage)。

由於返回值LPITEMIDLIST是一個指向ITEMIDLIST的指標,這個ITEMIDLIST涉及到Windows Shell中關於管理諸如檔案、網路上的計算機、控制皮膚程式、回收站等等物件的知識點,Windows Shell為了識別具體的每一個物件,就使用了ITEMID來唯一識別和區分,而ITEMIDLIST就是一個完整的物件路徑。顯然這個函式可以用來瀏覽非檔案物件,比如區域網內的電腦等等,在這裡這個LPITEMIDLIST返回的物件路徑是一個資料夾的路徑,Windows提供了一個函式BOOL SHGetPathFromIDList(LPCITEMIDLIST pidl, LPSTR pszPath)來實現從物件路徑轉化為資料夾路徑。

03、淺談Windows回撥函式

MFC的回撥函式機制,沒記錯前面應該聊過(不管有沒有聊過,這裡再說說)。

MFC中應該有兩類回撥函式:一類是源自C的傳統回撥函式,此類回撥函式若非定義為全域性函式,而定義在類中的話,要新增static約束,常見的有EnumXXX();一類是訊息響應函式,通過成員函式指標實現回撥。

設想一種情況,基類A觸發某事件E後,回撥某定義好的函式F進行事件處理(MFC中表現為訊息響應函式)。繼承於類A的子類B和C,可能對於E有不同的處理方式,於是需要對基類A的函式F進行改寫。自然而然的,我們想到將F定義為以virtual修飾的虛擬函式。

回撥函式的簡單定義就是你定義的由Windows來呼叫。以下兩個函式摘自《Programming Windows with MFC》,這裡暫且不管函式的具體作用,在FillListBox中有一個API函式,它呼叫的回撥函式是EnumFontFamProc,回撥函式的宣告形式一般都是相對固定的,具體可以參考MSDN。

static int CALLBACK EnumFontFamProc (ENUMLOGFONT* lpelf,NEWTEXTMETRIC* lpntm, int nFontType, LPARAM lParam);
 
void CMainWindow::FillListBox () 
{
    m_wndListBox.ResetContent ();
    CClientDC dc (this);
    ::EnumFontFamilies ((HDC) dc, NULL, (FONTENUMPROC) EnumFontFamProc,(LPARAM) this); 
}
 
int CALLBACK CMainWindow::EnumFontFamProc (ENUMLOGFONT* lpelf,NEWTEXTMETRIC* lpntm, int nFontType, LPARAM lParam)
{
    CMainWindow* pWnd = (CMainWindow*) lParam;
    if ((pWnd->m_wndCheckBox.GetCheck () == BST_UNCHECKED) || (nFontType & TRUETYPE_FONTTYPE))
        pWnd->m_wndListBox.AddString (lpelf->elfLogFont.lfFaceName);
 
    return 1;
}

請注意這裡的函式EnumFontFamilies中的最後一個引數傳遞的是this即該CMainWindow物件的指標,為什麼要這樣呢,可以看到EnumFontFamProc的宣告是static,在C++中static函式是不能呼叫非static成員的,所以這裡傳遞一個this就不是很奇怪了。

但是為什麼要將該函式宣告為static呢,這就要歸咎於C++的特殊性了,眾所周知C++編譯器在編譯的時候都會在物件中新增一個this指標,在成員函式呼叫中又會附加一個引數儲存this指標,但是Windows的回撥函式有嚴格的定義就是必須按照引數列表傳遞的引數,加了this指標後引數列表就會與Windows期望的引數列表不一致了,因此這裡將其宣告為static

(static成員函式不會傳遞this指標,這點說起來總是知道,但是真正用時總是忘了,唉)。

另外在Windows中使用callback函式很常見,恰好許多支援回撥函式的API函式都像這裡的EnumFontFamilies一樣支援自定義的LPARAM引數,剛好可以傳遞this,如果使用的API函式不支援這樣的自定義的LPARAM引數,就需要其他的方法了,一種比較簡單的方法是將this複製為global變數使得回撥函式可以使用。

04、 BROWSEINFO回撥函式

BROWSEINFO結構的回撥函式如果不在本類寫的話,預設是跟隨自afxshellmanager.h中,如下所示:

static int CALLBACK BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lparam, LPARAM lpData);

我們則要重新在本類寫此回撥,使用本類的回撥函式。

舉例:

//.h中宣告靜態成員方法
static int CALLBACK BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lparam, LPARAM lpData);
//.cpp中寫實現
int XXX::BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lparam, LPARAM lpData)
{
	if(uMsg == BFFM_INITIALIZED)
	{
		CTreeCtrl treePath;
		HTREEITEM hItemSel;
		::SendMessage(hwnd, BFFM_SETSELECTION, TRUE, lpData); //傳送訊息給父視窗
		//下面的0x3741是一塊沒人用的地址,我試過,如果地址有人用就會錯,這裡找塊兒空地址即可
		treePath.SubclassWindow(::GetDlgItem(hwnd,0x3741));
		hItemSel = treePath.GetSelectedItem(); //選擇當前選擇的節點
		treePath.Expand(hItemSel, TVE_COLLAPSE); //樹擴充套件,具體幹啥,我也不太清楚
		treePath.UnsubclassWindow();  //視窗子類化,為了擷取Windows的訊息
	}
	
	return 0;
}

瞭解了回撥函式,接下來就看兩個例子,熟悉下本節的內容吧!

05、應用例項

功能: 建立一個應用程式,使點選按鈕可以生成對話方塊,提供選擇資料夾/檔案的功能。
支援: 如果顯示路徑控制元件支援儲存、載入功能,則此程式可以實現記憶上次選擇資料夾/檔案路徑的功能,如果不支援,就只能在不關軟體的前提下支援此功能。

int CCreatePipeDlg::BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lparam, LPARAM lpData)
{
	if(uMsg == BFFM_INITIALIZED)
	{
		CTreeCtrl treePath;
		HTREEITEM hItemSel;
		::SendMessage(hwnd, BFFM_SETSELECTION, TRUE, lpData);
		treePath.SubclassWindow(::GetDlgItem(hwnd,0x3741));
		hItemSel = treePath.GetSelectedItem(); 
		treePath.Expand(hItemSel, TVE_COLLAPSE);
		treePath.UnsubclassWindow(); 
	}
	
	return 0;
}

//按鈕的訊息處理函式
void CCreatePipeDlg::OnBnClickedBtnLoad()
{
	//TODO:在此新增控制元件通知處理程式程式碼
	BROWSEINFO bi;
	char szPath[MAX_PATH];
	LPITEMIDLIST pList = NULL;
	ZeroMemory(szPath, MAX_PATH);

	//獲取當前路徑
	GetDlgItemText(ID_EDIT_PATH, (LPWSTR)szPath, MAX_PATH);
	//配置路徑對話方塊
	memset(&bi, 0, sizeof(BROWSEINFO));

	bi.hwndOwner = AfxGetMainWnd()->GetSafeHwnd();
	bi.pidlRoot = NULL;
	bi.pszDisplayName = (LPWSTR)szPath;
	//這裡切記不能用:(LPWSTR)"選擇資料夾",不然會出現亂碼
	bi.lpszTitle = _T("選擇資料夾");
	bi.ulFlags = BIF_EDITBOX;
	bi.lpfn = BrowseCallbackProc;
	bi.lparam = (LPWSTR)szPath;
	bi.iImage = 0;

	//彈出選擇資料夾/檔案對話方塊
	if((pList = SHBrowseForFolder(&bi)) != NULL)
	{
		if(SHGetPathFromIDList(pList, (LPWSTR)szPath))
		{
			//設定路徑
			SetDlgItemText(IDC_EDIT_PATH, (LPWSTR)szPath);
		}
	}
}

下面優化一下上面的程式碼,改變一下引數即可實現支援新建資料夾功能!

int CCreatePipeDlg::BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lparam, LPARAM lpData)
{
	if(uMsg == BFFM_INITIALIZED)
	{
		CTreeCtrl treePath;
		HTREEITEM hItemSel;
		::SendMessage(hwnd, BFFM_SETSELECTION, TRUE, lpData);
		treePath.SubclassWindow(::GetDlgItem(hwnd,0x3741));
		hItemSel = treePath.GetSelectedItem(); 
		treePath.Expand(hItemSel, TVE_COLLAPSE);
		treePath.UnsubclassWindow(); 
	}
	
	return 0;
}

//按鈕的訊息處理函式
void CCreatePipeDlg::OnBnClickedBtnLoad()
{
	//TODO:在此新增控制元件通知處理程式程式碼
	BROWSEINFO bi;
	char szPath[MAX_PATH];
	LPITEMIDLIST pList = NULL;
	ZeroMemory(szPath, MAX_PATH);

	//獲取當前路徑
	GetDlgItemText(ID_EDIT_PATH, (LPWSTR)szPath, MAX_PATH);
	//配置路徑對話方塊
	memset(&bi, 0, sizeof(BROWSEINFO));

	bi.hwndOwner = AfxGetMainWnd()->GetSafeHwnd();
	bi.pidlRoot = NULL;
	bi.pszDisplayName = (LPWSTR)szPath;
	//這裡切記不能用:(LPWSTR)"選擇資料夾",不然會出現亂碼
	bi.lpszTitle = _T("選擇資料夾");
	bi.ulFlags = BIF_EDITBOX | BIF_NEWDIALOGSTYLE; //此處增加BIF_NEWDIALOGSTYLE即可
	bi.lpfn = BrowseCallbackProc;
	bi.lparam = (LPWSTR)szPath;
	bi.iImage = 0;

	//彈出選擇資料夾/檔案對話方塊
	if((pList = SHBrowseForFolder(&bi)) != NULL)
	{
		if(SHGetPathFromIDList(pList, (LPWSTR)szPath))
		{
			//設定路徑
			SetDlgItemText(IDC_EDIT_PATH, (LPWSTR)szPath);
		}
	}
}

如果使用的地方多,可以將BIF_EDITBOX | BIF_NEWDIALOGSTYLE定義為全域性巨集,如下:

#define NEWFILE_MEMROY BIF_EDITBOX | BIF_NEWDIALOGSTYLE  //全域性巨集

然後程式中:

bi.ulFlags = NEWFILE_MEMROY BIF_EDITBOX;

06、小結

弄清楚每個引數的意義,然後才能按需求設定初值。先太盲目了,只知道套別人的,根本就沒有理解。

如果連每一行在幹什麼都不知道,就知道copy程式碼的話,變一下花樣也許就失去了方向。
此次感觸最深莫過於此,希望以後引以為鑑,努力修煉!

版權宣告:轉載請註明出處,謝謝!

相關文章