win32消息映射6-继承

本文介绍了一种基于链表实现的消息映射机制,用于处理多级继承和组合中的消息映射需求。通过定义mapslot链表,实现了不同类及其派生类间的灵活消息映射,增强了代码的可扩展性和复用性。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值