win32消息映射10-处理WM_COMMAND和WM_NOTIFY消息

9 处理WM_COMMAND和WM_NOTIFY消息

在标准的windows消息里,存在两类消息:1.是操作系统发送给窗口的消息;2.是窗口之间用作通讯的消息。我们前面举例的消息,都是windows操作系统发送给窗口的消息,但窗口之间通讯的消息,却一直没有举例,原因在于,窗口之间通讯的消息,其格式有些复杂。若一开始就将这些复杂性引入到介绍消息映射的原理上,这会加大对原理理解的难度。万丈高楼平地起,一开始的入门,总是越简单越好,简单,才能去掉旁支末节,使人直指核心之处。在前面的基础上,引入WM_COMMAND,不会给理解加入难度,反而有种水到渠成的感觉。

WM_COMMAND里wParam和lParam的意义,请查阅MSDN,里面有正式的说明。这里,以笔者的理解来阐述wParam和lParam。

WM_COMMAND消息用作窗口之间的通讯,也就是窗口A发送消息给窗口B,告诉窗口B,窗口A发生了什么。

窗口A的类型有3种:1.菜单(Menu);2.加速键(Accelerator);3.控件(Control)。既然有3种类型,就肯定需要有个区分的标记,wParamHi就是这个标记,若wParamHi为0,说明A是菜单;为1,说明A是加速键;其它,A就是控件。若A是控件,这个值的取值范围是[3,65534],既然有这么大的取值范围,肯定就要拓展它的意义,不然就浪费了。微软把这个值拓展为控件的通知码(notification code),比如BN_CLICKED、EN_CHANGE、EN_UPDATE等等。窗口B收到这个消息,就知道这控件被单击了(BN_CLICKED),或者这控件的内容被改变了(EN_CHANGE)……

菜单和加速键都不止一项,控件也不止一个,怎么辨别呢?这就是wParamLo起的作用了,它取值范围是(0,65535],若wParamHi是菜单,wParamLo就表明是哪个菜单项被单击,若是加速键,就表明哪个加速键被按了。若是控件的通知码,就代表来自哪个控件。

按道理,有了wParamHi和wParamLo就够了,但还有个lParam,总要发挥一些作用,控件也有窗口句柄,干脆就把控件的窗口句柄保存在lParam里。

若窗口A是控件,wParamLo的取值范围是(0,65535],总共有65535个,我们假设,一个窗口内的所有控件,不会超过这个数目。若超过了,不应该使用WM_COMMAND消息。

对于菜单,WM_COMAND消息只能获知它的单击消息,而BN_CLICKED的值是0,恰好是菜单的类型值!这是特意设置成这样。有时候,一个控件对应着某一个菜单项,若它们的id(wParamLo)一样,做一次消息映射就够了。而lParam的值,能区别是来自于菜单还是控件。

理解了wParam和lParam,就可以定义msg_struct的变异体:

struct msg_command : msg_node
{
    HWND    hWnd;

    UINT    message;

    union
    {
        struct
        {
            WORD    ctrlid;
            WORD    code;
        };
        WPARAM wParam;
    };

    union
    {
        HWND    hSender;
        LPARAM lParam;
    };

    LRESULT result;
};

struct map_command : msgmap_t
{
    typedef msg_command msg_type;

    template<typename T>
    static inline size_t map_fun_addr(bool (T::*f)(msg_type &))
    {
        __wabc_static_assert(sizeof(msg_struct) == sizeof(msg_type));
        return *reinterpret_cast<size_t *>(&f);
    }
};

原先是,只根据message的值来查找对应的消息映射函数,引入WM_COMMAND后,这么做是不行了。若只匹配WM_COMMAND消息,那么在对应的映射函数,将出现switch case列表,来查找匹配wParam,又回到了最初sdk编程窗口回调函数所出现的情况。现在我们不单纯要匹配message,还要匹配wParm。

struct msgmap_t
{
    WPARAM wParam;
    UINT message;

    // ...
    bool operator<(const msgmap_t &rhs)const
    {
        typedef unsigned __int64 uint64;
        __wabc_static_assert((sizeof(WPARAM)+sizeof(message)) == sizeof(uint64));
        const uint64 &v1 = *reinterpret_cast<const uint64 *>(this);
        const uint64 &v2 = *reinterpret_cast<const uint64 *>(&rhs);
        return v1 < v2;
    }
};

对于原先的消息,wParam是0:

#define WABC_ON_DESTROY(f) \
    { 0, WM_DESTROY, map_destroy::map_fun_addr<map_class>(f),    &map_destroy::on_message },

#define WABC_ON_PAINT(f) \
    { 0, WM_PAINT, map_paint::map_fun_addr<map_class>(f), &map_paint::on_message },

而对于WM_COMMAND消息,wParam需要作为参数由外部传入进来:

#define WABC_ON_COMMAND(id, code, f) \
    { MAKEWPARAM(id,code), WM_COMMAND, wabc::map_command::map_fun_addr<map_class>(f), &wabc::map_size::on_map },

对于菜单消息,可以另外提供一个宏,方便使用:

#define BN_ON_CLICK(id, f) WABC_ON_COMMAND(id, BN_CLICKED, f)

其它控件消息如EN_CHANGE、EN_UPDATE等,也可以做类似的简化。

数据结构改变了,其相关的算法也要随之改变。

class wndproc
{
    // ...
    static bool process_WM(msg_struct &msg, WPARAM wParam=0);
    // ...
};

bool wndproc::process_WM(msg_struct &msg, WPARAM wParam)
{
    // ...
    msgmap_t v;
    v.message= msg.message;
    v.wParam= wParam;
    // ...
}

为process_WM增加一个参数wParam,并给一个默认值,查找消息映射函数的时候,把v.wParam赋上值,这样,所做的改动不影响原先的功能。然后可以添加process_WM_COMMAND函数了

class wndproc
{
    static bool process_WM_COMMAND(msg_struct &msg);
    // ...
    static bool process(msg_struct &msg);
};

bool wndproc::process(msg_struct &msg)
{
    // ...
    static special_msg items[] = {
        WM_CREATE, &wndproc::process_WM_CREATE,
        WM_DESTROY, &wndproc::process_WM_DESTROY,
        WM_COMMAND, &wndproc::process_WM_COMMAND,
    };
    // ...
}

bool wndproc::process_WM_COMMAND(msg_struct &msg)
{
    return process_WM(msg, msg.wParam);
}

映射WM_COMMAND消息很简单,依然像以前一样,这里给出一个小例子,其中ID_TEST1是一个resource id:

class A : public wabc::wndbase
{
    WABC_DECLARE_MSG_MAP()
public:
    A()
    {
        WABC_BEGIN_MSG_MAP(A)
            BN_ON_CLICK(ID_TEST1, &A::test1)
        WABC_END_MSG_MAP()
    }

    bool on_test1(wabc::msg_command &msg);
}

现在要回过头审视一下WM_COMMAND的消息的设计。某一特定的菜单项消息,现在可以直接映射了,但用户有没有需要自己直接映射WM_COMMAND消息的需求?这需求很少,但依然存在的,比如在一些文本编辑器的软件里,总有一项“打开最近使用的文件”的菜单项,这菜单项下的子菜单,是最近打开的文件名,也许有0个,也许有9个,不管多少个,若这些id是连续的,那么实现起来会轻松好多。假设有20个最近打开的文件,难道需要做20次映射吗?尽管这也是一种实现的方案,但毕竟是一种丑陋的实现方案。若用户自己能接管WM_COMMAND消息,不管多少个,也就是一条if语句判断的事情。

由此,映射WM_COMMAND消息有两种需求,1.映射某一菜单项;2.给用户接管WM_COMMAND消息的接口。如何实现呢?分析WM_COMMAND消息的wParam参数,会发现,wParam不可能为0。所以,可以人为规定,若wParam为0,意味着用户要直接映射WM_COMMAND消息。若用户映射了某一菜单项,又同时接管了WM_COMMAND消息,这里就有一个优先级的问题,可以人为规定,映射菜单项的优先级高。

实现只能放在wndproc::process_WM中:

void wndproc::process_WM(msg_struct &msg, WPARAM wParam)
{
    msgmap_t v;
    v.message= msg.message;
    v.wParam= wParam;

    mapslot_ptr<mapslot_node> head(wnd.m_mapslot_head);
    while(msg.cur_slot != head.m_p)
    {
        mapslot &slot= static_cast<msgslot &>(*msg.cur_slot);
        pr = std::equal_range(slot.entries, slot.entries + slot.entries_count, v);
        if (pr.first == pr.second)
        {
            // 若wParam已经为0,不用再查找了
            if(wParam == 0)
            {
                msg.cur_slot= slot.next;
                continue;
            }
            
            // 再查找一次
            v.wParam=0;
            pr = std::equal_range(slot.entries, slot.entries + slot.entries_count, v);
            v.wParam = wParam;
            if (pr.first == pr.second)
            {
                msg.cur_slot= slot.next;
                continue;
            }
        }

        msg_guard guard(&slot);
        const msgmap_t &v= *pr.first;
        if (v.invoke(v, slot.wnd, msg))
            return true;
        msg.cur_slot= slot.next;
    }
    return false;
}

process_WM所做的改动很简单,无非就是增加了再重新查找一次的代码。下面是使用的一个小例子:

class A : public wabc::wndbase
{
    WABC_DECLARE_MSG_MAP()
public:
    A()
    {
        WABC_BEGIN_MSG_MAP(A)
            WABC_ON_COMMAND(0,0,&A::on_command)
            BN_ON_CLICK(ID_TEST1, &A::test1)
        WABC_END_MSG_MAP()
    }

    bool on_test1(wabc::msg_command &msg);

    bool on_command(wabc::msg_command &msg)
    {
        if(msg.code == 0 && msg.ctrlid >= ID_FIRST && msg.ctrlid <ID_LAST)
        {
            // ...
            return true;
        }
        return false;
    }
}

若ID_TEST1在[ID_FIRST,ID_LAST)里,依然会优先调用on_test1,不会进入on_command里。

是不是多加一个wParam就可以解决所有的问题了?不是的,WM_NOTIFY消息拓展了WM_COMMAND消息,在WM_NOTIFY消息里,通知码(notification code)由WM_COMMAND消息里的16位整数,扩展成32位整数,控件id也从16位整数扩展到32位。一个wParam已经不能容纳通知码和控件id,必须多引入一个整型变量,最终的msgmap_t定义如下:

struct msgmap_t
{
    WPARAM    wParam;
    UINT    message;

    DWORD    ctrlid;

    size_t    on_message;    // the address of map function

    bool(*invoke)(const msgmap_t &a, void * _this, msg_struct &);

    bool operator<(const msgmap_t &rhs)const
    {
        typedef unsigned __int64 uint64;
        __wabc_static_assert((sizeof(WPARAM)+sizeof(message)) == sizeof(uint64));
        const uint64 &v1 = *reinterpret_cast<const uint64 *>(this);
        const uint64 &v2 = *reinterpret_cast<const uint64 *>(&rhs);
        return v1 < v2;
    }
};

注意比较的代码,依然是wParam和message参与,里面并没有ctrlid。这是因为,我们要特列化WM_NOTIFY的process_WM实现,现在的cpu都支持64位的整数,但支持128位整数的cpu就很少了。在运行时,WM_NOTIFY消息并不会经常发生,所以我们要特例化一个较慢版本的process_WM。

从MSDN上对WM_NOTIFY wParam和lParam的描述,msg_notify定义如下:

struct msg_notify : msg_node
{
    HWND    hWnd;

    UINT    message;

    WPARAM     ctrlid;
    union
    {
        LPNMHDR    pnmh;
        LPARAM lParam;
    };

    LRESULT result;
};

struct map_notify : msgmap_t
{
    typedef msg_notify msg_type;

    template<typename T>
    static inline size_t map_fun_addr(bool (T::*f)(msg_type &))
    {
        __wabc_static_assert(sizeof(msg_struct) == sizeof(msg_type));
        return *reinterpret_cast<size_t *>(&f);
    }
};

#define WABC_ON_NOTIFY(ctrlid, code, f) \
    { code, WM_NOTIFY, ctrlid, wabc::map_notify::map_fun_addr<map_class>(f), &wabc::map_size::on_map },

先定义一个比较WM_NOTIFY消息的函数:

static inline bool compare_by_notify(const msgmap_t &lhs, const msgmap_t &rhs)
{
    typedef unsigned __int64 uint64;
    const uint64 &v1 = *reinterpret_cast<const uint64 *>(&lhs);
    const uint64 &v2 = *reinterpret_cast<const uint64 *>(&rhs);
    if (v1 == v2)
        return lhs.ctrlid < rhs.ctrlid;
    return v1 < v2;
}

class wndproc
{
    static bool process_WM_NOTIFY(msg_struct &msg);
    // ...
};

bool wndproc::process(msg_struct &msg)
{
    // ...
    static special_msg items[] = {
        WM_CREATE, &wndproc::process_WM_CREATE,
        WM_NOTIFY, &wndproc::process_WM_NOTIFY,
        WM_COMMAND, &wndproc::process_WM_COMMAND,
    };
    // ...
}

现在,我们可以把process_WM的代码搬到process_WM_NOTIFY来,再做一些比较代码的改动:
 

void wndproc::process_WM_NOTIFY(msg_struct &msg, WPARAM wParam)
{
    NMHDR &nh = *reinterpret_cast<NMHDR *>(msg.lParam);
    msgmap_t v;
    v.message= msg.message;
    v.wParam = nh.code;

    assert(nh.idFrom);
    v.ctrlid = nh.idFrom;

    mapslot_ptr<mapslot_node> head(wnd.m_mapslot_head);
    while(msg.cur_slot != head.m_p)
    {
        msgslot &slot= static_cast<msgslot &>(*pSlot);
        pr = std::equal_range(slot.entries, slot.entries + slot.entries_count, v, &compare_by_notify);
        if (pr.first == pr.second)
        {
            // 再查找一次
            v.wParam= 0;
            v.ctrlid= 0;

            pr = std::equal_range(slot.entries, slot.entries + slot.entries_count, v);

            v.wParam = nh.code;
            v.ctrlid = nh.idFrom;

            if (pr.first == pr.second)
            {
                msg.cur_slot= slot.next;
                continue;
            }
        }

        mapslot_ptr<mapslot> guard(msg, slot_head);
        const msgmap_t &v= *pr.first;
        if (v.invoke(v, slot.wnd, msg))
            return true;
        msg.cur_slot= slot.next;
    }
    return false;
}

从上面的代码我们能看到,对于WM_NOTIFY消息,都要查找两次,第一次是查找特定的WM_NOTIFY消息,第二次是给与用户一个处理WM_NOTIFY消息的机会。

大多数情况下,用户都不会自己处理WM_NOTIFY消息,也就是说,第二次的查找大多数情况下都是不必要的,当前slot.entries_count是个32位整数,实际上用16位整数也可以,剩下的16位可以用做这些特殊处理消息的标记位,若是1,表明这个mapslot没有必要进行第二次查找。这就为第二次查找做了一个优化,限于篇幅,不再赘述。

用户需不需要自己处理WM_NOTIFY消息?这个问题笔者自己也很疑惑。从逻辑的角度,是有这可能,但从笔者多年的项目经验上看,却从没遇到过这需求。感觉把WM_NOTIFY消息第二次查找去掉也可以。但从设计上来说,保持一致性似乎是更好的选择。

不单纯只有WM_COMMAND和WM_NOTITY消息需要这么处理,WM_SYSCOMMAND、WM_TIMER也有类似的需求。由于设计原理都是类似,没必要一一列举了。

最后一步,必须保证消息映射函数的有序性:

void mapslot::assign(void *map_to, const msgmap_t *entries1, size_t n)
{
    // ...
#ifdef _DEBUG
    for (size_t i = 1; i < entries_count; ++i)
        assert(compare_by_notify(entries[i - 1], entries[i]));
#endif
}

总结:

从匹配一个整数,到匹配两个整数,再到三个,那有没有四个呢?也许有,但目前还没发现。没发现就先不考虑。从设计上,感觉16位的id和16位的通知码足够了,但既然WM_NOTIFY被设计成这样,就必须遵循它的设计。另外,special_msg的数目是固定的,并没有给使用者拓展的接口。若出现了类似special_msg的需求,可以使用WM_COMMAND或者WM_NOTIFY,毕竟,这两个消息并不是只能用在windows的标准控件上,自己编写的控件也是可以用。

请点击这里下载'wabc'库的最终源码。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值