昨天, 群里面有一個人問起: 要怎么讓"文件打開對話框"居中顯示, 有人說子類. 而我告訴他的方法是用鈎子函數OFNHookProc, 不知道這是不是所謂的子類?
相信看了我今天這篇文章以后, 要解決居中顯示的問題就是小菜一碟啦~
這個東西也並不是我今天才用, 很久以前做的串口調試助手(Com Monitor)上面也用到了這個功能.
下面來看一張被掛鈎了的GetOpenFileName的效果(來自QQ影音):

可以看到, "打開"對話框的右上角被QQ影音添加了一個按鈕, 用來管理常用文件夾, 這個按鍵放在這里是最適合不過了~
下面看看我將要說明的代碼實現的功能:

同樣可以看到, 我在文件類型下面, 增設了一欄, 叫做"Test".
不過她是怎么實現的,下面簡單介紹.
來看看GetOpenFileName(為簡單書寫,以后不再寫出GetSaveFileName)的參數OPENFILENAME中的某些成員:
typedef struct tagOFN { ... DWORD Flags; ... LPARAM lCustData; LPOFNHOOKPROC lpfnHook; LPCTSTR lpTemplateName; ... } OPENFILENAME, *LPOPENFILENAME;
DWORD Flags:
要實現鈎子效果,必須使能 OFN_EXPLORER + OFN_ENABLEHOOK
要像我上面那樣做的話, 還可以加上一個 OFN_ENABLETEMPLATE 標志, 該標志將程序中的一個對話框模板作為"打開"的一個子窗口.
LPARAM lCustData:
這個是傳遞給OFNHookProc鈎子過程的初始化參數,鈎子函數收到的初始化對話框消息就是通常的 WM_INITDIALOG, 其中的消息參數 LPARAM 就是
lCustData, 此時可用來進一步初始化對話框, 通過你的自定義初始化參數.
LPCTSTR lpTemplateName:
這個是所謂的對話框模板, 其實就是一個對話框資源而已, "打開"將其作為其窗體的部分顯示出來,用 MAKEINTRESOURCE 轉換成 LPCTSTR.
LPOFNHOOKPROC lpfnHook:
這里需要設定一個函數指針, 就是我們的子類化窗口消息處理過程.
下面來看看OFNHookProc:
原型:UINT_PTR CALLBACK OFNHookProc(HWND hdlg,UINT uiMsg,WPARAM wParam,LPARAM lParam);
這看起來和常規的消息過程一模一樣, 確實是這樣, 不過需要注意的地方是:
hdlg 是我們的子對話框的句柄, 並不是"打開"對話框的句柄, 我加的那些控件, 就是我的子窗口, 而我又作為"打開"的子窗口!
其實, 我們要處理的消息並不多, 比如, 我處理了 WM_SIZE,WM_INITDIALOG,WM_NOTIFY, ...
下面又來說說這幾個消息的處理:
WM_INITDIALOG:
參數:
1.這個消息是"打開"在初始化的時候發送的, 這時候我們需要做的就是初始化我們自己的對話框數據, 比如我上面在ComboBox中添加了幾條字符串
2.還有一個初始化參數, 來自 lParam, 通過 lCustData 傳送而來, 通過強制轉換轉換成我們需要的類型
3.由於我們的模板也是作為子窗口的, 所以, 這里可以在函數域定義一個全局的父窗口句柄變量, 並在這里初始化, 以便后續使用
返回:
WM_INITDIALOG 需要返回 0 以表示我們的初始化過程初始化成功, 不然函數調用失敗!
WM_NOTIFY:
參數:來自控件的WM_NOTIFY消息可能會很多, 不過, 我們只需要處理其中需要處理的
1.CDN_FILEOK
這個消息來自於當我們點擊"打開"按鈕時, 這時候, 我們可以判斷用戶的選擇是否合理並返回一個值告訴父窗口.
當你認為用戶的選擇合理時, 可以返回 0 來關閉對話框, 如果返回 非0, 對話框將不會被關閉!
2.CDN_SHAREVIOLATION
對此消息不熟, 不多作介紹( 如果哪位熟悉, 請幫忙補充到 評論 欄, 感謝 )
返回:由於我們處理的對話框消息, 所以, 消息的返回不能靠簡單地return解決, 而是使用函數:SetWindowLong/SetWindowLongPtr
1.SetWindowLong(..., DWL_MSGRESULT, ...);
2.SetDlgMsgResult(...);
上面兩種方法設置的是真正的返回值, 而對話框過程的返回值:
0 - 對話框在你處理該消息后繼續處理當前消息
1 - 對話框不再處理該消息
至於要怎么設置返回, 可以參看我提供的代碼
WM_SIZE:
參數:見MSDN, 不多說, 因為用不到
處理過程:
我所有的控件對齊都是在這里處理, 說起簡單也簡單, 說起復雜, 還是有點! (要是有個圖就能輕易說明問題啦~)
首先看一下窗口繼承情況:
第1層:"打開"對話框
/ \
第2層:原有的子窗口控件 + 我們的對話框模板
\
第3層: 我們的對話框模板的子窗口控件
正是因為如此, 所以處理控件坐標才會變得那么復雜(不過是通用的, 可以寫成一個宏或函數來搞定, 如果控件較多的話)
還有一點:對話框模板被放到"打開"上面的坐標, 我沒有找到資料明確說明, 我用GetWindowRect取得的很不規則(當然,還是矩形),也就是說:模板左邊距不為零, ...
好吧, 說說我上面(嚴格)對齊ComboBox的過程(上面的(文件過濾)叫ComboA, 下面的(我的)叫ComboB), 上面顯示文件名的叫 ComboC:
1.取得 ComboA 相對於"打開"的坐標,同時能獲得長寬: 通過 GetWindowRect
要怎么取得 ComboA 的句柄? 畢竟 ComboA 又不是我們的對話框, 我們沒辦法知道其標識符ID. 不過還好, M$ 在它的
頭文件里面告訴我們了它們的標識符ID(頭文件是dlgs.h, 被包含於Windows.h), 其中有如下:
chx1 只讀 CheckBox
cmb1 顯示 文件過濾 的 ComboBox
stc2 cmb1 左邊的標簽
cmb2 顯示當前驅動器或文件夾的 ComboBox
stc4 cmb2 左邊的標簽
cmb13 顯示當前選擇的文件的ComboBox(包含edt1)
edt1 顯示當前文件的文本框(可能為NULL)
stc3 cmb13 左邊的標簽
lst1 顯示當前驅動器或文件夾內容的列表框
stc1 lst1 左邊的標簽
IDOK 確定(OK)按鈕
IDCANCEL 取消(Cancel)按鈕
pshHelp 幫助命令按鈕
2.取得 ComboC 的坐標, 同上
這個是為了計算 ComboA 與 ComboC 的齊頂的間距, 來調整ComboB 和 ComboA 的間距
3.取得對話框模板(作為子窗口)相對於"打開"的坐標: 通過 GetWindowRect
注意對話框模板的父窗口是GetParent(hdlg)哦!
4.設定 ComboB 的坐標, 通過 SetWindowPos/MoveWindow
注意了, 如果只是簡單地根據 ComboA 來設定我們的 ComboB 那就錯了, 因為 ComboB 的父窗口是對話框模板!
如果對話框模板在"打開"上的坐標是(0,0)的話, 那么我們就可以輕松地設定為相同的坐標, 但如果不是的話, 就麻煩一點了.
我們需要計算相對坐標, 其實也不復雜, 只是看起來有點而已.
下面貼出我的對齊代碼, 不再詳述了:
HWND hCboCurFlt = GetDlgItem(hParent, cmb1); //過濾列表ComboBox HWND hStaticFlt = GetDlgItem(hParent,stc2); //過濾列表左側的Static HWND hCboFile = GetDlgItem(hParent,cmb13); //當前選擇的文件ComboBox RECT rcCboFlt,rcStaFlt,rcDlg,rcFile; int left,top,width,height; //這個矩形是"過濾ComboBox"和"過濾提示Static控件",我們根據它們來對齊控件 GetWindowRect(hCboCurFlt, &rcCboFlt); GetWindowRect(hStaticFlt,&rcStaFlt); //這個是"文件名(Edit)"-當前選擇的, 用來計算它和過濾的間距,以調整我的控件和過濾Combo的間距 GetWindowRect(hCboFile,&rcFile); //說實話,我並不清楚對話框模板的位置是怎樣的,反正左邊距不是0,不清楚 //所以,調整窗口的時候要相對移動坐標 GetWindowRect(hdlg,&rcDlg); //設定坐標, 注意, 這里的坐標不是相對於我們的對話框模板的,而是"打開/關閉",好好理解下 //什么時候能畫個圖示意下就好了 left = rcCboFlt.left-rcDlg.left; top = rcCboFlt.top-rcDlg.top+(rcCboFlt.top-rcFile.top); width = rcCboFlt.right-rcCboFlt.left; height = rcCboFlt.bottom-rcCboFlt.top; SetWindowPos(hCombo,0,left,top,width,height,SWP_NOZORDER); left = rcStaFlt.left-rcDlg.left; top = rcStaFlt.top-rcDlg.top+(rcCboFlt.top-rcFile.top); width = rcStaFlt.right-rcStaFlt.left; height = rcStaFlt.bottom-rcStaFlt.top; SetWindowPos(hStatic,0,left,top,width,height,SWP_NOZORDER);
說到這里已經差不多啦, 關於OFNHookProc的簡單介紹就到這里了.
示例源代碼:
#if _WIN32_WINNT<0x0502 #undef _WIN32_WINNT #define _WIN32_WINNT 0x0502 #endif #include <Windows.h> #include <WindowsX.h> #include "resource.h" UINT_PTR __stdcall OFNHookProc(HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam); BOOL get_file_name(char* title, char* filter,char* buffer) { OPENFILENAME ofn = {0}; *buffer = 0; ofn.lStructSize = sizeof(ofn); ofn.hInstance = GetModuleHandle(NULL); //允許 縮放窗口+資源管理器風格+文件必須存在+隱藏只讀文件+使能鈎子+使能模板 ofn.Flags = OFN_ENABLESIZING|OFN_EXPLORER|OFN_FILEMUSTEXIST|OFN_HIDEREADONLY|OFN_ENABLEHOOK|OFN_ENABLETEMPLATE; ofn.lpstrFilter = filter; ofn.lpstrFile = &buffer[0]; ofn.nMaxFile = MAX_PATH; ofn.lpstrTitle = title; ofn.lpfnHook = OFNHookProc; ofn.lpTemplateName = MAKEINTRESOURCE(IDD_DLG_TEMPLATE); return (BOOL)GetOpenFileName(&ofn); } /************************************************************************ 函數:OFNHookProc@16 功能:GetOpenFileName/GetSaveFileName的子類/鈎子處理過程 參數: hdlg - 該對話框模板句柄, 注意:不是"打開/保存"對話框的句柄 uiMsg,wParam,lParam - 同常規Windows消息參數 返回: 由於是對話框消息,所以返回值應使用SetWindowLong(...,DWL_MSGRESULT,...)來返回 分以下3種情況返回: 1.鈎子函數返回0:默認對話框函數繼續處理該消息 2.鈎子函數返回非0:默認對話框函數忽略該消息不再處理 3.(例外)對於CDN_SHAREVIOLATION 和 CDN_FILEOK(點擊"打開/保存"時觸發) 通知消 息時,鈎子過程應該明確返回一個非0值以指示鈎子過程已經使用SetWindowLong(...,DWL_MSGRESULT,...); 設置了一個非零的該消息的返回值, 默認對話框函數不再繼續處理 *************************************************************************/ UINT_PTR CALLBACK OFNHookProc(HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam) { static HWND hParent; //對話框模板的父窗口句柄 static HWND hCombo; //我增加的ComboBox的句柄 static HWND hStatic; //我增加的Static控件的句柄 if(uiMsg == WM_NOTIFY){ LPOFNOTIFY pofn = (LPOFNOTIFY)lParam; if(pofn->hdr.code == CDN_FILEOK){ //返回0-關閉對話框並返回 //返回!0-禁止關閉對話框 int iSel = ComboBox_GetCurSel(hCombo); if(iSel != 1){//未選擇"測試字符串二"時 MessageBox(hParent,"你必須選擇\"測試字符串二\"才能離開!",NULL,MB_ICONEXCLAMATION); SetWindowLong(hdlg,DWL_MSGRESULT,1); //或使用:SetDlgMsgResult(hdlg,1); return 1; } return 0; } }else if(uiMsg == WM_SIZE){ HWND hCboCurFlt = GetDlgItem(hParent, cmb1); //過慮列表ComboBox HWND hStaticFlt = GetDlgItem(hParent,stc2); //過慮列表左側的Static HWND hCboFile = GetDlgItem(hParent,cmb13); //當前選擇的文件ComboBox RECT rcCboFlt,rcStaFlt,rcDlg,rcFile; int left,top,width,height; //這個矩形是"過慮ComboBox"和"過慮提示Static控件",我們根據它們來對齊控件 GetWindowRect(hCboCurFlt, &rcCboFlt); GetWindowRect(hStaticFlt,&rcStaFlt); //這個是"文件名(Edit)"-當前選擇的, 用來計算它和過慮的間距,以調整我的控件和過慮Combo的間距 GetWindowRect(hCboFile,&rcFile); //說實話,我並不清楚對話框模板的位置是怎樣的,反正左邊距不是0,不清楚 //所以,調整窗口的時候要相對移動坐標 GetWindowRect(hdlg,&rcDlg); //設定坐標, 注意, 這里的坐標不是相對於我們的對話框模板的,而是"打開/關閉",好好理解下 //什么時候能畫個圖示意下就好了 left = rcCboFlt.left-rcDlg.left; top = rcCboFlt.top-rcDlg.top+(rcCboFlt.top-rcFile.top); width = rcCboFlt.right-rcCboFlt.left; height = rcCboFlt.bottom-rcCboFlt.top; SetWindowPos(hCombo,0,left,top,width,height,SWP_NOZORDER); left = rcStaFlt.left-rcDlg.left; top = rcStaFlt.top-rcDlg.top+(rcCboFlt.top-rcFile.top); width = rcStaFlt.right-rcStaFlt.left; height = rcStaFlt.bottom-rcStaFlt.top; SetWindowPos(hStatic,0,left,top,width,height,SWP_NOZORDER); return 0; }else if(uiMsg == WM_INITDIALOG){ HFONT hFontDialog = NULL; hParent = GetParent(hdlg); //初始化我們自己的句柄,方便下次使用 hCombo = GetDlgItem(hdlg, IDC_COMBO_TEST); hStatic = GetDlgItem(hdlg,IDC_STATIC_TEST); //初始化我們的控件的數據 ComboBox_AddString(hCombo, "測試字符串一"); ComboBox_AddString(hCombo, "測試字符串二"); ComboBox_AddString(hCombo, "女孩不哭,哈哈!"); ComboBox_SetCurSel(hCombo,0); //一般為了表現效果一致,需要設置一下字體,比如說VC6的默認字體 //System就相當的丑陋,如果不修改的話,受不了... //當然,最好是采用"打開"對話框的統一字體 //如果你當前也使用VC6,可以注釋掉看看,呃......... hFontDialog = (HFONT)SendMessage(hParent,WM_GETFONT,0,0); SendMessage(hCombo,WM_SETFONT,(WPARAM)hFontDialog,MAKELPARAM(TRUE,0)); SendMessage(hStatic,WM_SETFONT,(WPARAM)hFontDialog,MAKELPARAM(TRUE,0)); return 0; } UNREFERENCED_PARAMETER(wParam); return 0; } int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nShowCmd) { char buffer[MAX_PATH]; if(get_file_name("選擇一個文件","所有文件(*.*)\x00*.*\x00",buffer)){ MessageBox(NULL,buffer,"你選擇了文件",MB_ICONINFORMATION); }else{ MessageBox(NULL,"你沒有選擇文件或遇到了錯誤!",NULL,MB_ICONEXCLAMATION); } return 0; }
示例項目(VC6.0):http://files.cnblogs.com/nbsofer/ofnhook.7z
女孩不哭 @ 2013-07-09 21:59:02 @ http://www.cnblogs.com/nbsofer
