win32消息映射2-第一次改进

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'库的最终源码。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值