在之前的一篇关于对话框返回值的文章中,有人建议说可以使用另外一种不同的搞法:也即直接调用DefDlgProc这个API函数(就类似于窗口消息过程中调用默认的消息处理函数DefWindowProc一样,而不是直接返回TRUE或者FALSE。
实际上,我们准备试它两次。今天我会使用上述所说的方法,过几天,我还会演示另外一种完全不一样的做法。这两种做法中,都包含同一个的内部理念,而剩下的其他部分只是为了让这个理念能顺利跑起来的辅助结构而已。
这个所谓的第一种方法,实际上是使用了一种递归式调用手法,它尝试在对话框窗口过程中调用DefDlgProc,从而触发一种默认的消息处理流程。这种技巧需要设立一个标志,使用这个标志可以能够打破递归调用,从而退出无限循环。
因为通常你的对话框对象里已经有了一些数据成员了,所以,再添加一个数据成员应该不是什么大问题。
DefDlgProc会调用对话框窗口过程来判断主程序的意图。当你想让Windows执行默认的消息处理时,你就会递归地调用DefDlgProc:在DefDlgProc这个函数的内部,它会回过头来,调用你的对话框窗口过程来判断你是否会希望覆盖默认的处理流程。
通过检测这个递归调用并返回FALSE,对DefDlgProc的递归调用就会执行默认处理并返回它的处理结果。
现在,你得到了默认消息处理的结果,你可以在返回之前修改它,然后调用链返回到外层的DefDlgProc,它会将这个值作为消息处理的最终处理结果。
如果有人喜欢看图的话,可以看看下面这个图:
基于上面的这个流程图,你应该能够自己写出对应的代码来。下面是我写的一个版本,我姑且把它称作是一个”WndProc-Like”的对话框。
我将WLDlgProc声明为了虚方法,如果一个子类继承了WLDialogBox,则这个子类的窗口过程会被s_DlgProc调用,我们认为子类可能会在它们自己的窗口过程中执行自定义的行为。在基类的中,我们调用了DefDlgProcEx(这是个宏,来自windowsx.h头文件)来作为消息处理的默认方式(它的确做了许多脏活)。
是的,你想的没错,这个技法从1992年就已经被Microsoft公布出来了。如果你查看一下DefDlgProcEx这个宏的实现代码,就会发现,它会将检测递归调用的标志设置为TRUE,然后调用会触发递归调用的DefDlgProc。
本来我是想着再实现一份WLDefDlgProc的,它会调用DefDlgProcEx并且WLDlgProc会调用WLDefDlgProc。(在第一版中,我的确是这么做的)。但是,我最终没有选择这样做,我主要是不想有人跳过对基类WLDefDlgProc的调用。
如果你希望对消息进行默认的处理,则只需要将调用转发给基类的WLDefDlgProc即可。
s_DlgProc是所有Wndproc对话框实例的对话框窗口过程。当收到WM_INITDIALOG消息的时候,它会初始化自身,这样后面收到的消息就能准确地知道具体是哪个窗口实例负责处理此消息。
接下来,我们使用了CheckDlgRecursion这个宏(也是来自windowsx.h)。这个宏会检查递归调用标志,如果标志为TRUE,则它会将它设置为FALSE并立即返回FALSE,这样就可以打破递归调用循环了。
如果标志为FALSE,它会调用WLDlgProc这个方法(子类很有可能会重写这个方法),然后设置对话框窗口过程的返回值并返回。
SetDlgMsgResult这个宏也来自于windowsx.h:它保存返回值到DWLP_MSGRESULT中并返回TRUE。除非出现一些特殊的消息,在这种情况下,它会直接返回,而不设置DWLP_MSGRESULT。对64位应用开发者的温馨提醒:当前实现的这个宏里有一个Bug。表达式[(BOOL)(result)]应该改为[(INT_PTR)(result)],这样返回值的高32位就不会被截断了。
最后一个方法是DoModal,它会初始化递归调用标志并显示对话框。
下面是一个使用了上面这个类的一个例子:
为了演示返回一个自定义的返回值,我重写了WM_SETCURSOR消息处理函数,这样当鼠标位于标题栏时,会显示一个自定义的光标。对于演示目的来说,这个代码已经足够达到目的了。
我们通过调用__super::WLDlgProc来调用了默认的消息处理函数。在Visual C++扩展中,__super可以用来在子类中解析对基类的引用。这个关键字十分有用,因为它可以帮助开发者快速将调用转发给类的上一层基类。如果你希望将调动转发给基类的基类,则可以使用如下的技法:__super::__super::WLDlgProc
如果你使用的编译器不支持__super关键字,则可以使用如下的typedef来间接实现:
typedef WLDialogBox super;
然后使用super::WLDlgProc就可以调用基类方法了。
实际上,在VC开发团队将__super关键字添加到产品中之前,我就是这样做的,可能是习惯了吧。
递归调用标志真的有必要作为每个对话框示例的数据成员吗?可以将它设置为全局变量的吗?
递归调用标志不需要作为每个对话框示例的数据成员。只需要保证它的生命周期足够长,以至于可以及时检测到递归调用。
但是,将它设置为全局变量也不是个好主意,因为可能在对DefDlgProc的递归调用中会同时有两个线程。
所以,倒是可以考虑使用TLV(Thread Local Variable)来存储这个变量,如果你希望使用C语言而不是C++类来实现的话。