win32消息映射11-消息反射

10 消息反射

在MFC里,有个消息反射的机制,就是窗口A发送消息给窗口B,窗口B再把这个消息回送到窗口A去,让窗口A自己处理。有时候这种用法很有用,能将窗口B的一些代码剥离到窗口A去,提高了代码的重用性。下面以WM_COMMAN消息为例,讲解如何实现消息反射。假设有如下场景,窗口A是个windows的标准button,它的ID是IDOK,当它被点击的时候,会发送一个BN_CLICKED的WM_COMMAND消息到它parent B去,窗口B收到这消息,把这消息回送给A。

class A : public wabc::button
{
    WABC_DECLARE_MSG_MAP()
public:
    A();

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

class B : public wabc::wndbase
{
    WABC_DECLARE_MSG_MAP()
    A m_ok_button;
public:
    B()
    {
        WABC_BEGIN_MSG_MAP(B)
            BN_ON_CLICK(IDOK, &B::on_ok)
        WABC_END_MSG_MAP()
    }

    bool on_ok(wabc::msg_command &msg)
    {
        ::SendMessage(m_ok_button.m_hWnd, msg.message, msg.wParam, msg.lParam);
        return true;
    }
};

这里忽略A和B的创建过程。在B::on_ok里,给出了一个最容易想到的实现方式,实现方式的优劣先不管它,重点是看A如何做消息映射。

若A的ID永远都是IDOK,这么做是可行的:

A::A()
{
    WABC_BEGIN_MSG_MAP(A)
        BN_ON_CLICK(IDOK, &A::on_clicked)
    WABC_END_MSG_MAP()
}

但不可能这么设计,A的ID是由使用者决定,自身不能做这个假设。由于ID是不确定的,但映射A::on_clicked的时候,又必须是个常量。仔细分析WM_COMMAND的wParam和lParam参数,发现控件ID不可能为0。所以,可以引入一条规范:若反射WM_COMAND消息,反射回去的ID必须为0。

A::A()
{
    WABC_BEGIN_MSG_MAP(A)
        BN_ON_CLICK(0, &A::on_clicked)
    WABC_END_MSG_MAP()
}

这样,B::on_ok的实现必须改了:

bool B::on_ok(wabc::msg_command &msg)
{
    ::SendMessage(m_ok_button.m_hWnd, msg.message, MAKEWPARAM(0,msg.wParamHi), msg.lParam);
    return true;
}

BN_CLICKED的值是0,id是0,依照前面的设计,这意味着映射整个WM_COMMAND消息。但用户不可能再重新映射一个WM_COMMAND消息,也就是:

A::A()
{
    WABC_BEGIN_MSG_MAP(A)
        BN_ON_CLICK(0, &A::on_clicked)
        WABC_ON_COMMAND(0, 0, &A::on_command)
    WABC_END_MSG_MAP()
}

在WABC_END_MSG_MAP里,debug版本会检查消息映射是否排序。检查到WABC_ON_COMMAND的时候,会发现和前面的消息映射值相等,这时候就会报错。也就是说,BN_ON_CLICK和WABC_ON_COMMAND只能存在一个。若两个功能都需要,怎么办?只能在映射的函数里面判断了:

bool A::on_clicked(wabc::msg_command &msg)
{
    if(msg.hSender == m_hWnd)
    {
        // 反射消息
    }
    else
    {
        // 映射了整个WM_COMMAND消息
    }
}

99%的情况下,不会同时存在这两种情况。第一:反射只会存在于控件中;第二:BN_CLICKED消息是单击消息,由按钮触发。按钮需要映射整个WM_COMMAND消息吗?很明显,看不到这需求。但从逻辑上,这情形会发生。发生了,也有解决的方法,尽管这解决的方法有些隐晦。

只有BN_CLICKED有这问题,因为它的值是0,非0值的通知码(notification code)不会有这问题。

现在已经有了第一个实现的版本。这个版本的最大弊端在于B::on_ok这个函数,在这实现里面,必须了解WM_COMMAND消息,必须了解WM_COMMAND反射的规范,且这些都要暴露给使用者。一不小心就会反射失败了,失败了若不了解原理还难找原因。所以,这是个不成熟的方案。顺应原先的消息映射框架,我们若可以这么使用就方便多了:

B:::B()
{
    WABC_BEGIN_MSG_MAP(B)
        WABC_REFLECT_COMMAND(IDOK, BN_CLICKED)
    WABC_END_MSG_MAP()
}

WABC_REFLECT_COMMAND的意思是将控件ID为IDOK的BN_CLICKED消息反射回去。这里不需要B::on_ok函数,看起来清爽好多。WABC_REFLECT_COMMAND实际上定义一个msgmap_t结构,很自然的,原先B::on_ok的代码会转移到这结构的on_map函数中。

struct map_command : msgmap_t
{
	typedef msg_command msg_type;

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

	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_REFLECT_COMMAND(id, code) \
	{ MAKEWPARAM(id, code), WM_COMMAND, 0, 0, &wabc::map_command::reflect },

bool map_command::reflect(const msgmap_t &a, void * _this, msg_struct &msg)
{
	if(msg.lParam)
	{
		::SendMessage(HWND(msg.lParam), msg.message, MAKEWPARAM(0,msg.wParamHi), msg.lParam);
		return ???;
	}
	return false;
}

原以为,事情会就此结束,但是SendMessage结束后,应该返回true还是false?依照设计,找到了映射函数,就应该返回true,没找到就应该返回false,这样可以继续遍历下一个mapslot节点。SendMessage的返回值无法告之这点。

办法总比问题多。问题能避免就避免,不能避免就只能解决它。为此,引入一个新的消息用作消息映射:

enum{ WM_WTL=WM_USER, WM_WABC_USER };

这里规定,外面不能使用WM_WTL的消息值,一旦使用了,会有意想不到的后果。使用者自定义消息应该从WM_WABC_USER开始。

WM_WTL的引入,是无可奈何之事,对话框内TAB键的处理也需要一个自定义消息。好在,自始至终,只需要一个消息。

WM_WTL的wParam和lParam定义如下:

若wParam >= 65536,wParam指向的是一个msg_struct的地址,lParam指向一个全局函数的地址;
否则,lParam的含义根据wParam的值不同而不同。

由此

class wndproc
{
    // ...
    static bool process_WM_COMMAND(msg_struct &msg);
    static bool process_WM_WTL(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_NOTIFY, &wndproc::process_WM_NOTIFY,
        WM_COMMAND, &wndproc::process_WM_COMMAND,
        WM_WTL, &wndproc::process_WM_WTL,
    };
    // ...
}

bool wndproc::process_WM_WTL(msg_struct &msg)
{
    typedef bool(*fun_t)(msg_struct &);
    if (msg.wParam >= 64 * 1024)
    {
        fun_t fun = reinterpret_cast<fun_t>(msg.lParam);

        msg_struct &other = *reinterpret_cast<msg_struct *>(msg.wParam);
        msg.message = other.message;
        msg.wParam = other.wParam;
        msg.lParam = other.lParam;
        if (fun(msg))
        {
            other.result = msg.result;
            msg.result = 1;
        }

        // 由于是自定义内部消息,所以这消息已经被处理了
        return true;
    }
    else
        return process_WM(msg, msg.wParam);
}

在这里我们要特别注意msg.result=1这代码,=1了,就说明对应的函数执行过了。other一个字段一个字段的赋值给msg,也是必要,因为它们对应不同的mapslot链表。

而process_WM_COMMAND,也要增加反射的代码:

bool wndproc::process_WM_COMMAND(msg_struct &msg)
{
    // 若是消息反射,不需要验证控件ID
    if (HWND(msg.lParam) == msg.hWnd)
        return process_WM(msg, MAKEWPARAM(0, msg.wParamHi));

    // 正常流程
    return process_WM(msg, msg.wParam);
}

现在,反射的问题算是真正解决了:

bool map_command::reflect(const msgmap_t &a, void * _this, msg_struct &msg)
{
    if(msg.lParam)
    {
        const LRESULT ret = ::SendMessage(HWND(msg.lParam), WM_WTL, WPARAM(&msg), LPARAM(&wndproc::process_WM_COMMAND));
        if( ret == 1)
            return true;
    }
    return false;
}

把A B的代码整理一次:

class A : public wabc::button
{
    WABC_DECLARE_MSG_MAP()
public:
    A()
    {
        WABC_BEGIN_MSG_MAP(A)
            BN_ON_CLICK(0, &A::on_clicked)
        WABC_END_MSG_MAP()
    }

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

class B : public wabc::wndbase
{
    WABC_DECLARE_MSG_MAP()
    A m_ok_button;
public:
    B()
    {
        WABC_BEGIN_MSG_MAP(B)
            WABC_REFLECT_COMMAND(IDOK, BN_CLICKED)
        WABC_END_MSG_MAP()
    }
};

这看起来是不是很容易使用:)

WM_NOTIFY的消息反射也是一样的道理:

struct map_notify : msgmap_t
{
	typedef msg_notify msg_type;

	static bool reflect(const msgmap_t &a, void * _this, msg_struct &);
};

#define WABC_REFLECT_NOTIFY(ctrlid, code) \
	{ code, WM_NOTIFY, ctrlid, 0, &wabc::map_notify::reflect },

bool map_notify::reflect(const msgmap_t &a, void * _this, msg_struct &msg)
{
	NMHDR &nh = *reinterpret_cast<NMHDR *>(msg.lParam);
	const LRESULT ret = ::SendMessage(nh.hwndFrom, WM_WTL, WPARAM(&msg), LPARAM(&wndproc::process_WM_NOTIFY));
	return (ret == 1) ? true : false;
}

bool wndproc::process_WM_NOTIFY(msg_struct &msg)
{
	NMHDR &nh = *reinterpret_cast<NMHDR *>(msg.lParam);
	// 若是消息反射,不需要验证控件ID
	if (nh.hwndFrom == msg.hWnd)
		return process_WM(msg, nh.code);

	// ...
}

同理还有WM_DRAWITEM,反射的时候不需要验证控件ID,这里不再赘述。


总结:

一次反射,从map_reflect_XXX::on_map => wndproc::process_WM_WTL => wndproc::process_XXX => wndproc::process_WM,这过程有些曲折,从问题的产生到问题的解决,每一步有时候走得轻松,有时候却走得缓慢,当问题解决的时候,回过头看,却也就觉得不过如此。解决的方法不重要,重要的是在这过程中形成的思维方式。MFC实现反射的方式是用到了继承,继承的方式,派生类和基类是紧耦合的,不管派生类需要不需要这功能,都会强行存在。而组合的方式,则比继承灵活得多,当前的实现,若应用程序没用到消息反射的功能,相关的代码根本不会编译到里面去,所以,能用组合的时候不要用继承。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值