1.第一次改进
我们先解决私有数据的问题。
查阅MSDN的CreateWindow的帮助,我们发现,CreateWindow的最后一个参数是lpParam,这表明,这是一个用户数据。这个参数起什么作用?答案就在WM_NCCREATE和WM_CREATE消息中,当一个窗口接收到WM_NCCREATE或WM_CREATE消息时,lParam会指向一个CREATESTRUCT地址,而CREATESTRUCT有一个成员lpCreateParams,存放的就是CreateWindow时传入的lpParam值。利用CreateWindow的最后一个参数,就可以解决私有数据的问题:
class base_wnd
{
public:
virtual ~base_wnd(){}
HWND m_hWnd;
};
class MyWindow : public base_wnd
{
// 私有成员列表...
};
int WINAPI WinMain (...)
{
// ...
MyWindow aWin;
aWin.m_hWnd= ::CreateWindow(..., &aWin);
//...
}
在WM_NCCREATE或WM_CREATE消息中,我们把lParam强行转换成CREATESTRUCT指针,通过其成员lpCreateParams得到aWin的地址,也就是说,窗口句柄和私有成员已经关联起来了。
但其它消息怎么办?这时候,要借助于Windows提供的另外一个API:SetWindowLongPtr(),SetWindowLongPtr函数能往窗口存放一个私有int值,在这里,我们存放的是aWin的地址,一旦存放进去以后,我们就可以用GetWindowLongPtr把这个值再取回来。由于WM_NCCREATE是窗口收到的第一条消息,所以我们在WM_NCCREATE消息里把aWin的地址存进去,那么在以后的消息里就可以用GetWindowLongPtr把这个值再取回来,具体实现如下:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
base_wnd *p= 0;
if ( message == WM_NCCREATE )
{
CREATESTRUCT& cs= *reinterpret_cast<CREATESTRUCT*>(lParam);
p= reinterpret_cast<base_wnd *>(cs.lpCreateParams);
p->m_hWnd= hWnd;
::SetWindowLongPtr( hWnd, GWL_USERDATA, (LONG)p );// 注意这一句
}
else
p= reinterpret_cast<MyWindow *>( ::GetWindowLongPtr(hWnd, GWL_USERDATA) );//取回实际对象的指针
//...
}
这时候,由于窗口句柄和其所在的对象已经关联起来,很自然的,原先WndProc函数里的消息处理代码就要挪到base_wnd里面去,使其能方便的访问其私有成员,我们为base_wnd增加一个成员函数:msg_default;
class base_wnd
{
// ...
public:
virtual LRESULT msg_default(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
};
msg_default被声明成virtual,这说明派生类可以覆盖它。派生类可以处理自己感兴趣的消息,不感兴趣的就扔给基类去处理。msg_default函数的参数有4个,似乎多了一点,若base_wnd的派生类每一个都要调用其基类的msg_default,那每次参数都要压栈4次,我们可以定义一个结构,封装这些参数,使其只压栈一次。
struct msg_struct
{
HWND hwnd;
UINT message;
union
{
WPARAM wParam;
struct
{
WORD wParamLo;
WORD wParamHi;
};
};
union
{
LPARAM lParam;
struct
{
WORD lParamLo;
WORD lParamHi;
};
};
LRESULT result;
};
我们注意到,msg_struct有个成员result,用来保存消息处理的返回值,有个hWnd,是用来保存当前的窗口句柄,这两个成员好像没有存在的必要,但实际上,这两个变量不是多余,原因容后再述。
这样,msg_default函数就变成:
class base_wnd
{
// ...
public:
virtual void msg_default(msg_struct &msg)
{
msg.result= ::DefWindowProc ( msg.hwnd, msg.message, msg.wParam, msg.lParam );
}
};
而WndProc的实现就变成:
LRESULT CALLBACK WndProc(...)
{
// ...
if ( p != 0 )
{
msg_struct msg;
msg.hWnd= hWnd; msg.message= message;
msg.wParam= wParam; msg.lParam= lParam;
msg.result= 0;
p->msg_default( msg );
return msg.result;
}
return ::DefWindowProc( hWnd, message, wParam, lParam );
}
在这里,我们看到,WndProc的功能已经和最初的的不一样了,它现在负责的是窗口句柄和窗口对象的关联,至于消息如何处理,那丢给窗口对象操心,这样,WndProc无形中成了从过程式编程到面向对象编程的连接点。
既然窗口句柄已经和窗口对象关联上了,那什么时候撤销这个关联呢?答案在WM_NCDESTROY中:
LRESULT CALLBACK WndProc(...)
{
// ...
if ( p != 0 )
{
msg_struct msg;
msg.hWnd= hWnd; msg.message= message;
msg.wParam= wParam; msg.lParam= lParam;
msg.result= 0;
if ( message == WM_NCDESTROY )
{
p->m_hWnd= 0;
::SetWindowLongPtr( hWnd, GWL_USERDATA, 0 );
}
p->msg_default( msg );
return msg.result;
}
return ::DefWindowProc( hWnd, message, wParam, lParam );
}
这时候,可以看到msg_struct成员hWnd的作用了,由于base_wnd的m_hWnd在WM_NCDESTROY中,被提前设成0,若用m_hWnd替代msg_struct的hWnd去调用DefWindowProc,就会出现不是期望中的结果,可能有人会说,调用完msg_default再设成0不行吗?我想说行,但世事往往不如人意,在WndProc的设计里,WM_NCCREATE被认为是第一条接收的消息,而WM_NCDESTROY是被认为最后接收的消息,在这前提下,msg_default在WM_NCDESTROY消息里有可能把自己delete掉(base_wnd派生类有可能产生这种行为),这时候再设成0,就会出现内存访问违例的错误。
我们看看MyWindow的msg_default的实现:
void MyWindow::msg_default(msg_struct &msg)
{
PAINTSTRUCT ps;
HDC hdc;
switch (msg.message)
{
case WM_PAINT:
hdc = ::BeginPaint(msg.hWnd, &ps);
::TextOut( hdc, 0, 0, _T("Hello"), 5 );
::EndPaint(msg.hWnd, &ps);
break;
case WM_DESTROY:
::PostQuitMessage(0);
break;
default:
base_wnd::msg_default(msg);
}
}
这里的实现和原先WndProc的实现没有本质的区别,换句话说,一样可能需要一个庞大的switch case列表,但是在MyWindow::msg_default函数里,已经可以自由的访问MyWindow的私有成员,这是一个重大的改进。在我们讨论如何解决这switch case列表之前,要先谈谈这次改进的一些缺陷:
窗口句柄和窗口对象的关联是通过SetWindowLongPtr进行的,SetWindowLongPtr是个API函数,谁都可以调用这个API重设GWL_USERDATA的值,万一这个值被重设,那么关联就被破坏,由于本文只是示例代码,不考虑GWL_USERDATA被重设的情况,一种更好的实现是采用thunk技术,关于thunk技术在网上有很多文章,这里不在赘述。
请点击这里下载'wabc'库的最终源码。