子类化GetOpenFileName/GetSaveFileName, 以及钩子函数OFNHookProc的使用的简要说明
昨天, 群里面有一个人问起: 要怎么让"文件打开对话框"居中显示, 有人说子类. 而我告诉他的方法是用钩子函数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):https://files.cnblogs.com/nbsofer/ofnhook.7z
女孩不哭 @ 2013-07-09 21:59:02 @ http://www.cnblogs.com/nbsofer