win32消息映射3

来源:互联网 发布:wpf编程宝典c 2012 编辑:程序博客网 时间:2024/05/16 12:41

2.第二次改进。

现在想想,如何解决switch case的问题。switch case的作用,是将某个消息分配到某段处理代码上,这个问题可以抽象成,如何让某个消息和某个函数关联,一旦关联,我们将能编写如下代码:

class MyWindow : public base_wnd
{
public:
    void on_paint( HDC hdc, const RECT & rtPaint )
    {
        ::TextOut( hdc, 0, 0, _T("Hello"), 5 );
    }

    void on_destroy()
    {
        ::PostQuitMessage(0);
    }
};

观察on_paint和on_destroy的参数,会发现两者完全不一样,这说明,每个消息处理函数的接口可能都不一样(一样的意思是参数的个数、类型相同且返回值的类型都相同),但windows发送消息时,都带有WPARAM和LPARAM参数,这又好像暗示着似乎可以用统一的方法来处理,如何从统一走向分化,这是值得思考的问题。

在解决这个问题之前,我们简单的回顾成员函数指针概念。众所周知,指针的本质就是地址,函数指针就是函数地址,成员函数指针就是指一个类成员函数的地址,但调用时有一点特别,它需要一个“this”值,也就是说要关联到某个对象,举个简单例子:

 typedef void (MyWindow::*on_paint_t)(HDC,const RECT &);

这个typedef意思是,定义一个MyWindow成员函数指针类型,这个指针将会指向MyWindow的某个函数,其返回值是void,参数有2个,分别是HDC类型和const RECt &类型。

 MyWindow aWin;
 HDC hdc;
 RECT rtPaint;

 on_paint_t fun= &MyWindow::on_paint; // fun指向MyWindow的on_paint地址

 // 初始化hdc和rtPaint...
 (aWin.*fun)(hdc, rtPaint);// 实际上是aWin.on_paint(hdc,rtPaint)

使用成员函数指针时,必须有一个对象和其相关联,换句话说,若知道一个对象的地址和一个成员函数的地址,就可以调用这个对象的成员函数了,举个高级一点的例子,首先定义一个类:

class X
{
    public:
     void on_paint( HDC hdc, const RECT & rtPaint );
};

然后,

 HDC hdc;
 RECT rtPaint;
 // 初始化hdc和rtPaint...

 X a;
 a.on_paint(hdc, rtPaint); // 直接调用X::on_paint函数

 MyWindow &r= reinterpret_cast<MyWindow &>(a);  // r指向a
 on_paint_t fun= reinterpret_cast<on_paint_t>(&X::on_paint); // fun指向X::on_paint的地址

 (r.*fun)(hdc, rtPaint); // 实际上是a.on_paint(hdc, rtPaint);

你若初次接触这样的代码,会大吃一惊,这样也可以!为什么可以,原因有3:
 1. r的地址和a的地址一样的;
 2. fun指向的是X::on_paint函数的地址;
 3. X::on_paint和MyWindow::on_paint的函数声明是一样的(返回值类型一样,参数类型和个数一样,参数入栈方式一样)。

上面的3个原因又可以归结为一句话,因为编译器生成的汇编代码是一样的。

有了上面成员函数指针的基础后,我们可以继续刚才的话题。因为每个消息都有WPARAM和LPARAM参数,很明显,这就暗示着必须有一个基类的存在,

struct msg_handler_base
{
     UINT message; // message标记当前的对象是对应哪个消息
     explicit msg_handler_base( UINT n ) : message(n){}
     virtual ~msg_handler_base(){}

     virtual bool do_it( msg_struct &msg )const =0;
};

do_it函数的功能是处理这个消息,若函数里面已经处理了这个消息,返回true,否则返回false,在一般情况下,都是返回true。我们看看对于WM_PAINT消息,do_it的实现。

struct msg_map_base
{
     struct paint : msg_handler_base
     {
          template< typename T >
          struct function
          {
               typedef void ( T::*type )( HDC, const RECT & );
          };

          function< X >::type fun; // fun的类型是 void (X::*)( HDC, const RECT & )

          template< typename T >
          explicit paint( void ( T::*pfn )(HDC, const RECT &) ) 
           : msg_handler_base(WM_PAINT)
          { ::memcpy( &fun, &pfn, sizeof(fun) ); }

          virtual bool do_it( msg_struct &msg )const
          {
               PAINTSTRUCT ps;
               HDC hdc= ::BeginPaint(msg.hWnd, &ps);

               X &x= ???; // 如何为x赋值?

               (x::*fun)(hdc, ps.rcPaint);
               ::EndPaint(msg.hWnd, ps);

               return true;
          }
     };
};

paint为什么要作为msg_map_base的嵌套类?paint里面为什么又要定义一个template structure 'function'?这些原因容后再述。

观察paint的构造函数,它要求传入某一个类的成员函数地址,这个成员函数的类型要求没有返回值,且只有两个参数,一个是HDC,另外一个const RECT &,而成员变量fun,正是用来保存这成员函数的地址。看到这可能有人有疑问,那对象的地址呢,不保存了吗?我们先看看,若保存了对象的地址,会有什么弊端:

若保存了对象的地址,势必造成一个对象对应一个paint实例,也就是说,当对象被构造时,势必要构造一个paint实例,若有n个消息呢,将会构造 n个XXX实例,这将造成运行时的开销。而且,在大多数情况下,n个消息保存着都是同一对象的地址,这将造成内存的浪费。另外,考虑多个对象的情况,多个对象对应多个paint实例,但这么多paint实例保存的却是同一个成员函数地址,这同样也造成了内存的浪费,所以,在paint里,无论如何是不能保存对象的地址(但毫无疑问,若保存了对象的地址,会使这部分的设计简单一点,也容易理解一点)。


但我们的确需要对象的地址,对象的地址从何而来?在paint内部是没有办法了,只能依靠于外部。我们为msg_handler_base的do_it函数增加一个参数,x:

struct msg_handler_base
{
     // ...
     virtual bool do_it( X &x, msg_struct &msg )const =0;
};

那paint的do_it函数就变成:

virtual bool msg_map_base::paint::do_it( X &x, msg_struct &msg )const
{
     // ...
     (x::*fun)(hdc, ps.rcPaint);
     // ...
}

现在暂时抛开do_it函数,看看如何构造一个paint实例。注意,同一类型的多个对象只能共同享有一个paint实例,这就暗示着,paint实例是这个对象的static成员,或者,我们将访问范围缩小一点,限制它只能是这个对象某个成员函数的static局部变量:

class MyWindow
{
     void enable_msg_map();
public:
     MyWindow() { enable_msg_map(); }
     // ...
};

void MyWindow::enable_msg_map()
{
     typedef MyWindow self;

     static msg_handler_base * entries[]= {
          new msg_map_base::paint( &self::on_paint );
     };
     //...
}

因为entries是static变量,C++会保证,它只会被初始化一次,也就是说,在MyWindow里,只会拥有一个paint实例。

由于paint是new出来的,那它如何被delete?这时候,就牵涉到msg_handler_base派生类的整体设计思想,我们人为的规定,msg_handler_base派生类的对象只能new出来,不能在堆栈上分配,在这前提下,我们修改msg_handler_base构造函数的实现。 

msg_handler_base::msg_handler_base( UINT n ):message(n)
{
     class manager : public std::vector< msg_handler_base * >
     {
     public:
      ~manager()
      {
           iterator first= begin();
           iterator last= end();
           for( ; first != last; ++first )
            delete *first;
      }
     };
     static manager g_manager;
     g_manager.push_back(this);
};

我们在msg_handler_base构造函数内部构造一个静态局部变量g_manager,每构造一个msg_handler_base的实例,就把实例的地址保存起来,当g_manager析构时,msg_handler_base的实例就会被delete掉。

manager是从vector公有派生,但vector是不希望被继承的(它的析构函数不是virtual),在某种情况下,这会带来错误,但由于manager是局部类(Local Class),它使用的目的和使用的方式都很单一,且这样代码看起来会清爽很多,所以manager将就这样设计。

我们现在回过头重新审视一下msg_handler_base的设计,就会发现有些美中不足。我们是人为的规定,msg_handler_base派生类只能new出来,但由于msg_handler_base派生类是对外接口的一部分,将不可避免的有人会在堆栈上构造msg_handler_base派生类对象,编译器无法防止这种情况的发生,而一旦出现这样的情况,在g_manager被析构时立刻会出错。而在另一方面,在MyWindow::enable_msg_map里面,却要和paint实例直接打交道,这无形中会增加使用者的记忆负担和初学者的出错机会。如何改善呢,这就轮到了下一个类的出场,

template< typename W >
struct msg_map : msg_map_base
{
     struct handler_base{}; // 为什么要定义handler_base?,原因容后再述。

     typedef const handler_base * msg_type;

     template< typename M >
      static msg_type on_message( typename M::function< W >::type pfn )
     {
          typedef typename M msg_handler;
          return (msg_type)new msg_handler(pfn); // 为什么要做强行转换,原因容后再述
     }
};

而MyWindow::enable_msg_map的代码会变成:

void MyWindow::enable_msg_map()
{
     typedef MyWindow self;
     typedef msg_map< self > msg_map;

     static msg_map::msg_type entries[]= {
          msg_map::on_message< msg_map::paint >(&self::on_paint );
     };
     //...
}

可读性和原先的相比,感觉如何?是不是感觉十分直观?当有paint消息到时,调用on_paint函数。这一小小的改动,使用者就不需要和paint直接打交道了,又为以后的维护和修改打下了伏笔,真是一举三得。

同样的,我们为WM_DESTROY消息增加一个消息处理函数:

struct msg_map_base
{
     struct paint : msg_handler_base
     {
      // ...
     };

     struct destroy : msg_handler_base
     {
          template< typename T >
          struct function
          {
               typedef void ( T::*type )( void );
          };

          function< X >::type fun;

          template< typename T >
          explicit paint( void ( T::*pfn )(void) ) 
           : msg_handler_base(WM_DESTROY)
          { ::memcpy( &fun, &pfn, sizeof(invoke) ); }

          virtual bool do_it( X &x, msg_struct &msg )const
          {
               (x::*fun)();
               return true;
          }
     };
};

而MyWindow的enable_msg_map函数则变成:

void MyWindow::enable_msg_map()
{
     typedef MyWindow self;
     typedef msg_map< self > msg_map;

     static msg_map::msg_type entries[]= {
          msg_map::on_message< msg_map::paint >(&self::on_paint );
          msg_map::on_message< msg_map::destroy >(&self::on_destroy );
     };
     //...
}

现在,我们已经解决成员函数的地址问题,但对象地址的问题还没有解决。
先观看上面的entries数组。在entries数组里所有对象成员函数指针(on_paint和on_destroy),实际上都对应着同一个“this”值,我们可以定义一个结构,来表达这种关系:

struct msg_value
{
     X *m_x; // 对象的地址
     const msg_handler_base ** m_handler; // 对象的消息处理函数,这些消息处理函数,对应着同一个对象地址m_x
     size_t m_size_of_handler;
};

在base_wnd里,添加与消息映射有关的变量和函数:

class base_wnd
{
     // ...
public:
     msg_value m_message;

     template< typename W >
      typename msg_map<W>::msg_type * 
      map_msg( W *p, typename msg_map<W>::msg_type *entries, size_type n )
     {
          m_message.m_x= reinterpret_cast<X *>(p);
          m_message.m_handler= reinterpret_cast<const msg_handler_base ** >(entries);
          m_message.m_size_of_handler= n;

          return entries + n; // 为什么要有返回值,容后再述
     }
};

在MyWindow的enable_msg_map里,把消息处理函数映射上去:

void MyWindow::enable_msg_map()
{
     // ...
     map_msg( this, entries, sizeof(entries)/sizeof(entries[0]) );
}

现在只剩下最后一步,就是如何分派这些消息处理函数,这个责任,当仁不让的让WndProc来承担:

LRESULT CALLBACK WndProc(...)
{
     // ...
     if ( p != 0 )
     {
          // ...

          const msg_value &v= p->m_message;
          for( size_t j= 0; j < v.m_size_of_handler; ++j )
          {
               const msg_handler_base &mhb= *v.m_handler[j];
               if ( mhb.message != msg.message )
                    continue;
               if ( mhb.do_it( *v.m_x , msg ) )
                    return msg.result;
          }

          p->msg_default( msg );
          return msg.result;
     }
     return ::DefWindowProc( hWnd, message, wParam, lParam );
}

在WndProc里,不断的循环遍历对象的entries(v.m_handler),若消息类型相等,立刻调用消息处理函数。

在这里,消息映射的核心设计思想已经介绍完了,但远远还没有达到完善的地步,比如,如何处理WM_COMMAND消息、如何处理WM_NOTIFY消息等等。在讨论这些更为高级的话题之前,我们必须对上面的设计做一个总结;

 1. base_wnd,这是作为所有窗口的基类,它为WndProc的实现提供了一个统一的接口;
 2. msg_handler_base的派生类,这些派生类的作用是负责把消息的WPARAM和LPARAM参数转化成更容易理解的对外接口;
 3. WndProc,它作用有2,一是负责窗口句柄和窗口对象的关联与撤销,二是负责分派消息,而消息分派的实现,是以base_wnd和msg_handler_base为基础;
 4. msg_map,它存在的目的是为使用者提供一个良好的消息映射接口,限制对象成员函数必须来自同一对象类型,这将避免使用者犯一些危险的错误。

性能。从上面的实现可以看出,一个消息要到达最终的处理函数,中间必须经过一次虚函数的转发,在转发之前,还要先经过消息的配对。这就说明,消息的最终处理函数,其实相当于是个双虚拟函数。而消息映射的初始化过程,实际上是12个字节(msg_value)的赋值过程。从空间和时间上看,这些开销都是及其低廉。这说明,这消息映射机制的设计实现,性价比十分高:)


Q: msg_map为什么是个template structure?
A: msg_map必须保证,on_message所接受的成员函数指针,必须来自同一对象类型。若不能做这个保证,可能会出现十分难以调试的错误,想想,某一类型的对象,调用不同类型的成员函数,会出现什么情况?

Q: paint为什么要作为msg_map_base的嵌套类?
A: 这增加了用户使用paint时的直观性,且paint定义在msg_map_base里面,减小了名字重复的可能性。

Q: paint里面为什么又要定义一个template structure 'function'
A: 决定msg_map::on_message的参数的类型,on_message并不知道需要什么样的函数原型,这是由typename M决定的,不同的M对应的成员函数原型并不一样,一旦函数不匹配,编译器立刻会报错。在MyWindow::enable_msg_map里,W是MyWindow,M是paint,

M::function< W >::type就是paint::function< MyWindow >::type,也就是void ( MyWindow::*type )( HDC, const RECT & ),这函数类型的推导,都是由编译器在背后进行。

Q: 在msg_map里为什么要定义类handler_base?
A: 定义handler_base是为了定义msg_type,我们首先要明白,msg_map<A>::msg_type和msg_map<B>::msg_type(假设A和B都是class)的类型是不一样的,这样,若我们在MyWindow::enable_msg_map里面写入如下代码:

 void MyWindow::enable_msg_map()
 {
  A a;
  // ...
  map_msg( &a, entries, sizeof(entries)/sizeof(entries[0]) );
 }

 编译器立刻会抱怨,不能生成对应的map_msg函数,为什么,原因在于第二个参数,因为W的类型为A,所以第二个参数的类型要求是msg_map<A>::msg_type *,但现在提供的却是msg_map<MyWindow>::msg_type *!entries希望的对象类型是MyWindow,却提供了一个不相干的A对象,若编译器不抱怨,这将是一个十分危险的错误:一个对象,调用的不是其类型的成员函数。

 msg_map::handler_base的存在,从另外一个侧面说明,msg_map和base_wnd::map_msg函数是紧耦合的。