5.继承
在basewnd里,定义了一个mapslot,由basewnd的派生类使用。当有一个类,比如A,继承自basewnd,且A没有派生类了,这一机制会运行得很好,但若A也有派生类,且A的派生类也有消息映射的需求,这一机制就不能满足需求。若A的派生类也有派生类呢……?
这说明,只有一个mapslot已经不能满足我们的需求。现在的需求是必须有n(n>=1)个mapslot:每当一个class需要映射消息,都需要有一个自己的mapslot。当有多个mapslot时,就需要定义一个数据结构,能把这些mapslot组织起来,供WndProc访问。很明显的,有两种数据结构能满足这需求:数组和链表。若使用数组,由于数组的大小不能固定,这会导致做消息映射的时候,会有动态分配的行为。若用链表,动态分配可以避免。由于mapslot不会有很多个,所以,链表比数组合适。
链表又分为两种:单向链表和双向链表。对于mapslot,只会有前端或者末端插入的需求。若没有链表中间断开的需求,单向链表是一个很好的选择。但在后面的章节我们会看到,中间断开的需求是存在的。所以我们采用双向链表这一数据结构。
struct mapslot_node
{
msgslot_node *next, *prior;
mapslot_node()
{
next = prior = this;
}
~mapslot_node()
{
prior->next= next;
next->prior= prior;
}
// 插入到链表头
void link(msgslot_node *h)
{
assert(h);
assert(this != h);
next= h->next;
h->next->prior= this;
prior= h;
h->next= this;
}
void unlink()
{
prior->next= next;
next->prior= prior;
next = prior = this;
}
};
这时,我们就得到了第二个版本的msgmap_t:
struct mapslot : mapslot_node
{
// ...
};
注意,mapslot从mapslot_node派生,但mapslot_node没有虚函数,这违背了继承的设计初衷。若引入虚函数,编译器会自动增加了一个vtable成员变量。这没有必要。一个更为正确的做法是:
struct msgmap_t
{
mapslot_node node;
// ...
};
但这样,会增加很多强行转换的代码,权衡再三,还是采用继承的语法。利用继承语法的便利去使用msgmap_t。
在win32 SDK编程里,有很多api和HWND有关,一旦创建了窗口,就可以通过HWND去获知窗口的属性和改变窗口的行为。我们将这些api封装在basewnd,而将窗口的创建、销毁放在其派生类wndbase中。
class basewnd
{
public:
HWND m_hWnd;
basewnd():m_hWnd(0){}
virtual ~basewnd(){}
// ...
};
class wndbase : public basewnd
{
public:
mapslot_node m_mapslot_head;
wndbase(){}
virtual ~wndbase(){}
template<typename W>
void map_msg(msgslot *slot, W *map_to, const msgmap_t *entries, size_t n)
{
slot->assign(map_to, entries, n);
slot->link(&m_mapslot_head);
}
// ...
};
由于每个class做消息映射时,都有自己私有的mapslot,所以,wndbase::map_msg增加一个参数:slot。同样的,宏WABC_END_MSG_MAP()也要做相应的改变:
#define WABC_END_MSG_MAP() \
}; \
(*this).map_msg<map_class>(???, this,entries, sizeof(entries)/sizeof(entries[0])); }
这时候,麻烦来了,宏WABC_END_MSG_MAP怎么知道这私有mapslot的变量名呢?往WABC_END_MSG_MAP加入一个参数是个解决的方法,但一个更好的方法是指定一个固定的变量名。姑且把这个变量名称命名为m_mapslot_wabc。
#define WABC_END_MSG_MAP() \
}; \
(*this).map_msg<map_class>(&m_mapslot_wabc, this,entries, sizeof(entries)/sizeof(entries[0])); }
我们再引入一个宏,用作m_mapslot_wabc的声明:
#define WABC_DECLARE_MSG_MAP() private: wabc::msgslot m_msgslot_wabc;
前面的例子也要做相应的改变:
class MyWindow : public wndbase
{
WABC_DECLARE_MSG_MAP()
public:
// ...
void enable_msg_map()
{
// 代码不变
}
};
WABC_DECLARE_MSG_MAP里,那一个private是否是必须?考虑MyWindow也派生一个类
class YourWindow : public MyWindow
{
WABC_DECLARE_MSG_MAP()
public:
// ...
void enable_msg_map()
{
WABC_BEGIN_MSG_MAP(YourWindow)
// ...
WABC_END_MSG_MAP()
}
};
若m_msgslot_wabc不是私有的变量,一编译的时候,编译器立刻会抱怨,有两个m_msgslot_wabc,不知道该使用哪一个。但若改成private,编译器立刻就知道了。若YourWindow没有WABC_DECLARE_MSG_MAP,编译器会给出一个错误,YourWindow不能访问MyWindow的private变量。
现在轮到了WndProc的改动。m_mapslot_head是一个mapslot链表的根结点。m_mapslot_head.next指向第一个mapslot,m_mapslot_head.prior指向最后一个。在做插入动作的时候,最后插入的节点总是在链表头。在YourWindow里,首先执行的是基类(MyWindow)的enable_msg_map(),然后才执行YourWindow的enable_msg_map(),也就是说YourWindow处于第一个节点的位置,而MyWindow随后。假设YourWindow和MyWindow都映射了同一个windows消息,那么,先执行哪一个呢?由于YourWindow知道MyWindow,而MyWindow不知道谁会继承它,我们人为规定。YourWindow的优先级高。
LRESULT CALLBACK WndProc(...)
{
typedef std::pair<const msgmap_t *, const msgmap_t *> pair_type;
wndbase *p= 0;
// ...
if ( p != 0 )
{
// ...
pair_type pr;
msgmap_t v;
v.message = msg.message;
mapslot_node *pSlot= p->m_mapslot_head.next;
for(;pSlot!= &p->m_mapslot_head;pSlot= pSlot->next)
{
const msgslot &slot= static_cast<const msgslot &>(*pSlot);
pr = std::equal_range(slot.entries, slot.entries + slot.entries_count, v);
if (pr.first != pr.second)
{
const msgmap_t &v= *pr.first;
if ( v.invoke(v, slot.wnd, msg ) )
return msg.result;
}
}
}
return ::DefWindowProc( hWnd, message, wParam, lParam );
}
这里,可以看到映射函数返回值的意义了,若返回true,这说明这消息已经处理完毕,若返回值是false,这说明消息没有处理,抛给上一层(也就是基类)处理,若没有上一层了,那么就调用msg_default(...)。多数情况下,映射函数的返回值都应该是true。
对于当前WM_PAINT消息,其映射的函数应该永远返回true。在进入WM_PAINT消息映射函数之前,调用了BeginPaint,之后调用EndPaint,若返回false,其基类若也映射了WM_PAINT,会导致BeginPaint和EndPaint会再调用一次,这会导致未知的后果。所以,必须强制WM_PAINT消息返回true:
struct map_paint : msgmap_t
{
// ...
static bool on_map(const msgmap_t &a, void * x, msg_struct &msg)
{
// ...
(o.*f)(ps.hdc, ps.rcPaint, reinterpret_cast<msg_paint &>(msg));
return true;
}
};
诚然,若派生类和基类都映射了同一消息,在派生类的映射函数里,直接调用基类的映射函数,从而就可以永远的返回true。但是,若基类的映射函数是private,这么做就不行了。引入返回值,可以解决这个问题,也解耦了派生类和基类的紧密关系,毕竟,下一个mapslot,不一定会定义在基类当中!:)看下面的例子。
一个窗口创建了一个button,它想拦截这个button的按键消息,这该怎么做呢?
首先要明白,button的按键消息,是发送给button自身,它起始的根源,是来自于button自身的WndProc,在它自身的WndProc里,循环遍历button自己的mapslot链表。我们所要做的,是在这个窗口里,定义一个mapslot,添加到button的mapslot链表里。
这里的需求,和原先的需求不太一样了。原来是一个class一个mapslot,现在变成了一个class对应n个mapslot(n>=1),由此,改变WABC_DECLARE_MSG_MAP的实现:
#define WABC_DECLARE_MSG_MAPEX(N) private: wabc::msgslot m_msgslot_wabc[N];
#define WABC_DECLARE_MSG_MAP() WABC_DECLARE_MSG_MAPEX(1)
WABC_DECLARE_MSG_MAP定义一个长度为1的m_msgslot_wabc数组,但若需要多个,由使用者在WABC_DECLARE_MSG_MAPEX的参数里指定。
class MyWindow : public wndbase
{
WABC_DECLARE_MSG_MAPEX(2)
button m_button;
public:
bool on_button_keydown(msg_keydown &);
// ...
};
这里定义了两个mapslot,第一个用作于MyWindow自身,第二个用作m_button。
原先WABC_END_MSG_MAP的声明如下:
#define WABC_END_MSG_MAP() \
}; \
(*this).map_msg<map_class>(&m_mapslot_wabc, this,entries, sizeof(entries)/sizeof(entries[0])); }
这里,要细说里面的代码。(*this).map_msg这意思是,消息发生的起始根源,是来自于(*this)。map_msg的第一个参数,说明的映射的节点;而map_msg第二个参数的this,是说明,这是消息映射函数的对象。从上面的例子来说,就是:
m_button.map_msg<map_class>(m_msgslot_wabc[1], MyWindow,...);
由此,我们得到了WABC_END_MSG_MAP的第二个版本:
#define WABC_END_MSG_MAPEX(mapTo, mapFrom, slotIndex) \
}; \
enum{ slot_count= sizeof(m_msgslot_wabc)/sizeof(m_msgslot_wabc[0]) }; \
__wabc_static_assert(slotIndex < slot_count); \
(mapTo).map_msg<map_class>(m_msgslot_wabc + (slotIndex), &(mapFrom), entries, countof(entries)); }
#define WABC_END_MSG_MAP() WABC_END_MSG_MAPEX(*this, *this, 0)
WABC_END_MSG_MAPEX里的__wabc_static_assert和BOOST库里面的static_assert类似,断言传入的索引号不会超过数组的长度,这是编译期的断言。
这样,上面的需求就能实现了:
void MyWindow::enable_msg_map()
{
WABC_BEGIN_MSG_MAP(MyWindow)
WABC_ON_DESTROY(&MyWindow::on_destroy)
WABC_ON_PAINT(&MyWindow::on_paint)
WABC_END_MSG_MAP()
WABC_BEGIN_MSG_MAP(MyWindow)
WABC_ON_KEYDOWN(&MyWindow::on_button_keydown)
WABC_END_MSG_MAPEX(m_button, *this, 1)
}
这里WABC_END_MSG_MAPEX的意思是,将m_button的消息,映射到*this对象里,使用映射槽1。由于映射槽的索引号需要手动指定,这里可能会出错。若使用映射槽的索引号有重复,编译器无法给出错误或者警告,这将会出现映射失败的结果。在使用WABC_END_MSG_MAPEX时,一定要确保索引值不能重复!
总结:
在继承的情形下,产生了mapslot链表的需求,在实现mapslot链表后却发现,这套机制不仅在“继承”下可用,在“组合”的方式也可以正常运作。至此,这套消息映射机制开始体现出它的威力,它不仅仅是简化了SDK消息映射的编程,也简化了各个窗口对象之间消息关联的复杂性。从实现的角度上,并没有增加多少代码,但在灵活性上,却获益良多。在深入理解消息映射原理的基础上,我们可以在运行时,动态替换消息映射的实现。比如,若窗口存在状态机的行为,在某一状态下,需要这行为,而在另外的状态下,则是另外的行为。这可以将每个状态下的行为定义成一个entries,在相应状态下,映射上去。
请点击这里下载'wabc'库的最终源码。
本文介绍了一种基于链表实现的消息映射机制,用于处理多级继承和组合中的消息映射需求。通过定义mapslot链表,实现了不同类及其派生类间的灵活消息映射,增强了代码的可扩展性和复用性。

1497

被折叠的 条评论
为什么被折叠?



