11 对话框
对话框是一种特殊的窗口,它起源于所见即所得(WYSIWYG)的设计思想。当使用api创建窗口的时候,界面的布局存在于程序员的脑海中,只有等代码完成了,能运行了,才能看到效果。有没有一种方式方式,能在设计的时候就能看到效果,不用等到运行的时候呢?在win32 SDK编程里,只有对话框能做到这一点。对初学者而言,对话框好像减少了入门的难度,但实际上,难度并没有减少,一个问题的解决又引入了另外一个问题。在界面的布局上,的确做到了所见即所得,但如何把界面上的控件和代码里的变量相关联,这里就八仙过海,各显神通。MFC里引入了DDX的概念,把控件和资源相关联。对于初学者而言,DDX是个很抽象的概念,只知道要这么做,但背后的原理,要等到对SDK编程有了一定的了解才能有所体会。掌握win32编程,SDK是绕不过去的坎。
对话框也是窗口,也是用CreateWindowEx api创建的,但它的特殊性在于,是操作系统帮你创建,不需要手动创建(这当然包括对话框上控件的创建)。对话框的窗口过程由操作系统接管,这样,对话框就会有一些默认的系统行为,比如,TAB键的处理、模式对话框消息循环的处理等等。我们若用CreateWindowEx创建窗口,也能模拟对话框的这些行为,但若有操作系统支持,又何必自己模拟呢?有时候,顺势而为能省很多事情。
这篇文章,只讲对话框里面消息映射的部分,其它部分,有机会再详细阐述。
先讲模式对话框。模式对话框,是指对话框创建之后,对话框窗口一直处于“活动”(active)状态,除非它里面又创建了模式对话框。
模式对话框用DialogBoxParam创建:
INT_PTR DialogBoxParam(HINSTANCE hInstance,
LPCTSTR lpTemplateName,
HWND hWndParent,
DLGPROC lpDialogFunc,
LPARAM dwInitParam
);
第一个参数,传入的是lpTemplateName所在的HINSTANCE;
第二个参数,是对话框的资源id;
第三个参数,对话框的父窗口,若指定了父窗口,在对话框退出之前,父窗口不会响应鼠标消息;
第四个参数,对话框的回调函数。对话框的消息,通过这个回调函数告之;
第五个参数,和CreateWindowEx最后一个参数意义一样。在CreateWindowEx里,这参数的值能在WM_NCCREATE和WM_CREATE消息取回,而对于对话框,是在WM_INITDIALOG消息里获得。对话框里没有WM_CREATE消息吗?有,但被操作系统接管了。在“窗口的创建和注销”里说过,初始化是很重要的,若外面能接管对话框的WM_CREATE消息,对话框就有可能丧失初始化的机会。所以,对于对话框来说,WM_INITDIALOG消息是被认为第一个被接收的消息。
一个简单的使用DialogBoxParam创建模式对话框的例子:
int CALLBACK DlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_DIAOLOG),0,&DlgProc, 0);
由于对话框的回调函数和窗口的回调函数原型不一样,所以要重新定义对话框的回调函数。
class wndproc
{
// ...
bool process(msg_struct &msg);
public:
// ...
static int CALLBACK DlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
};
再定义一个dialog类:
int CALLBACK wndproc::DlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
dialog *p;
if(message == WM_INITDIALOG)
{
p= reinterpret_cast< dialog *>(lParam);
::SetWindowLongPtr(hWnd, GWL_USERDATA, (LONG)p);
}
else if((p= reinterpret_cast<dialog *>(::GetWindowLongPtr(hWnd, GWL_USERDATA))==0)
return FALSE;
msg_struct msg={0};
msg.hWnd = m_hWnd;
msg.message = message;
msg.wParam = wParam;
msg.lParam = lParam;
msg.wnd = p;
msg.cur_slot = p->m_msgslot_head.next;
const BOOL b= process(msg);
if(b)
{
::SetWindowLongPtr(hWnd, DWL_MSGRESULT, msg.result);
return TRUE;
}
return FALSE;
}
上面的代码都很简单,就不用再一一细说了。不过仍然有2点需要注意:
1. WM_INITDIALOG消息的处理。WM_INITDIALOG和WM_CREATE一样的,由于WM_CREATE消息没办法获得,所以,对于对话框来说,WM_INITDIALOG是第一条接收到的消息,它的作用就相当于WM_CREATE的作用,所以,在wndproc::process里,必须映射WM_INITDIALOG到process_WM_CREATE中去。
2.before_create对于对话框来说是没意义,因为对话框的窗口过程被操作系统隐藏了。
在上面的代码能看到DWL_MSGRESULT存在的意义。对话框的回调函数的原型,除返回值外,和窗口的回调函数一模一样。DlgProc是在对话框的窗口过程里调用的,对话框的窗口过程,必须知道DlgProc有没有处理了这个消息,若处理了,还要知道处理的结果(msg.result)。而窗口过程,直接返回的是结果。当初设计的时候返回值的格式没有统一好,造成了DWL_MSGRESULT的存在。
如何结束模式对话框?调用api EndDialog:
class dialog : public wndbase
{
// ...
public:
int show_modal(HINSTANCE hInstance, size_t nTemplateName, HWND hParent = 0);
void end_dialog(int nModalResult)
{
BOOL b= ::EndDialog(m_hWnd, nModalResult);
}
};
EndDialog需要传入两个参数,第一个是对话框的句柄,第二个模式对话框的返回值。假设某个模式对话框会有个“确定”和“取消”的按钮(当然,也可以有其它按钮),在对话框结束的时候,如何知道它是按“确定”结束的,还是按“取消”结束的呢?就通过这个返回值来判断。这个返回值,实际上也是DialogBoxParam api的返回值,从DialogBoxParam到EndDialog感觉好像很遥远,但是,只有调用EndDialog后,才会退出DialogBoxParam函数,由此可以推断出,在DialogBoxParam内部,也有一个类似于GetMessage之类的消息循环,EndDialog起到的作用是类似于PostQuitMessage,使消息循环结束。
非模式对话框由另外一个api创建:
HWND CreateDialogParam(HINSTANCE hInstance,
LPCTSTR lpTemplateName,
HWND hWndParent,
DLGPROC lpDialogFunc,
LPARAM dwInitParam
);
这api的参数,和DialogBoxParam一模一样,这里就不再复述。CreateDialogParam并没有自身的消息循环,创建完对话框,就直接返回了。由此,可以知道模式和非模式的区别。
class dialog : public wndbase
{
public:
// ...
BOOL create(HINSTANCE hInstance, size_t nTemplateName, HWND hParent = 0)
{
return CreateDialogBoxParam(hInstance, MAKEINTRESOURCE(nTemplateName),hParent,
&wndproc::DlgProc, LPARAM(this));
}
};
把CreateDialogBoxParam封装在create函数里,在wndbase也有create函数,dialog的create函数,会隐藏掉wndbase的create函数。一个dialog对象,调用create函数是调用dialog::create而不是wndbase::create,即使它的参数能和wndbase::create里的参数匹配。这符合设计的期望。但由于dialog是wndbase的派生类,若一个wndbase指针指向dialog对象,通过这个指针调用create就不是我们期望的了。前面说过,dialog的确是窗口,所以从wndbase派生没有问题,但它是一种特殊的窗口,创建的过程被操作系统接管了。很难平衡这里的设计,当前暂且这样设计。
总结:
对话框在某些场合是很有用的,它方便了布局的设计,简化了标准化控件的创建过程。
请点击这里下载'wabc'库的最终源码。