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