12 子类化和超类化
回顾SDK窗口创建的过程。首先注册一个类,这里重要的是类名和窗口的回调函数,然后调用CreateWindowEx创建窗口,创建时必须指定类名,实际上也是指定了窗口的回调函数。
windows操作系统内置了一些标准控件,供程序员使用。这些标准控件控件,不需要注册类名,系统已经帮你注册好,直接拿过来用就是了。比如按钮的类名是“BUTTON”,编辑框的类名是“EDIT”……,CreateWindowEx的时候,直接把这类名传入,就能生成对应的控件。控件对应的窗口过程对各种windows消息会有正确的响应。一般情况下,都能满足基本的需求。但有时候,总会有个性化需求的存在,这时候该怎么办?重写控件有时候是很费时甚至是不可能的工作。大多数的时候,我们都不会改变控件的基本功能,windows给出的解决方案就是子类化和超类化。
什么是子类化?子类化就是用自己的窗口过程替换别人的窗口过程。一旦替换了别人的窗口过程,就能优先收到windows消息,从而能处理自己感兴趣的消息,不感兴趣的消息,仍给回原先的窗口过程处理。
子类化的实现很简单,比如,若想子类化BUTTON,可以这么做:
WNDPROC pSubclassOldButtonProc=0;
LRESULT CALLBACK MyButtonProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
// 处理自己感兴趣的消息……
return ::CallWindowProc(pSubclassOldButtonProc, hWnd, message, wParam, lParam);
}
创建button:
HWND hButton= ::CreateWindowEx(0, _T("BUTTON"),...);
pSubclassOldButtonProc= (WNDPROC)::SetWindowLong(hButton, GWL_WNDPROC, (DWORD)MyButtonProc);
上面是SDK的实现方式。pSubclassOldButtonProc是个全局变量,且和hButton相关联。若多个button需要子类化,这种实现方式是噩梦般的存在。
SetWindowLong函数是在CreateWindowEx后调用的,而CreateWindowEx的内部,会发送WM_NCCREATE、WM_CREATE等消息,由于窗口还没子类化,这些消息接收不到。若CreateWindowEx的时候能直接指定MyButtonProc,就不会有这个问题。但CreateWindowEx只能传入类名,不能传入窗口过程,类名是什么时候和窗口过程关联上的呢?RegisterClass的时候。所以重新注册一个类名“MYBUTTON”,指定窗口过程是MyButtonProc,就能解决刚才那个问题。
这种解决的方式就叫“超类化”。
在超类化之前,必须先取得原先的窗口过程:
WNDCLASSEX wc={0};
::GetClassInfoEx(hInstance, _T("BUTTON"), &wc);
pSubclassOldButtonProc= wc.lpfnWndProc;
然后,替换原先的窗口过程:
wc.lpszClassName= _T("MYBUTTON");
wc.lpfnWndProc= &MyButtonProc;
再重新注册:
RegisterClassEx(&wc);
就完成了超类化的动作。CreateWindowEx(0,_T("MYBUTTON"),...)会直接使用MyButtonProc()。
子类化只针对于某一个特定窗口,而超类化针对同一类名的所有窗口。子类化有些消息会捕捉不到,而超类化能捕获所有的消息。作为一个基础库,当然希望能捕获所有的消息。这里,提供一种接口,去简化超类化的实现。引入一个类:scwnd,所有超类化的窗口必须从scwnd继承。
class scwnd : public wndbase
{
// ...
};
对于超类化,我们感兴趣的只有两个字段:窗口过程和类名。
struct scinfo_t
{
WNDPROC old_proc;
ATOM class_name;
};
在wndproc加入一个新的窗口函数SCWndProc,用作超类化窗口的窗口过程。
class wndproc
{
// ...
public:
// ...
static LRESULT CALLBACK SCWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
};
实现超类化,用新的类名超类化旧的类名:
scinfo_t super_class(const TCHAR *old_name, const TCHAR *new_name)
{
WNDCLASSEX wc;
scinfo_t sc;
wc.cbSize = sizeof(wc);
BOOL result = ::GetClassInfoEx(*g_app, old_name, &wc);
if(!result)
throw std::exception("GetClassInfoEx");
sc.old_proc= wc.lpfnWndProc;
wc.lpfnWndProc=wndproc::SCWndProc;
wc.lpszClassName= new_name;
sc.class_name= ::RegisterClassEx(&wc);
if(!sc.class_name)
throw std::exception("RegisterClassEx");
return sc;
}
超类化一个class,就需要一个scinfo_t变量,若有n个class,就需要n个scinfo_t变量。一开始,我们无法预计多少个,用到的时候生成这变量,不用的时候这变量不存在,这方式是最好的。c++的template机制,用到的时候再编译,符合这期望。
class scwnd : public wndbase
{
scinfo_t m_sc;
public:
template<typename T>
explicit scwnd(const T &t)
{
static scinfo_t this_scinfo= super_class(t.old_name(), t.new_name());
m_sc= this_scinfo;
}
};
引入一个scwnd的template构造函数,这意味着引入一个类型T就增加一个static变量。没引入这static变量就不存在,符合我们的设计。T必须有old_name和new_name两个函数。
比如超类化windows的标准控件:BUTTON,可以这么做:
class button : public scwnd
{
struct superclass
{
const TCHAR * old_name()const { return _T("BUTTON"); }
const TCHAR * name_name()const { return _T("wabcbutton"); }
};
public:
button():scwnd(superclass()){}
};
若button的缺省的构造函数被调用了,就会超类化“BUTTON”,否则,这过程不会发生。superclass里的两个函数都可以inline化,看编译器的优化能力了。
scwnd的私有成员变量m_sc,用于窗口的创建和窗口过程的回调:
class scwnd : public wndbase
{
scinfo_t m_sc;
friend wndproc;
public:
template<typename T>
explicit scwnd(const T &t)
{
static scinfo_t this_scinfo= super_class(t.old_name(), t.new_name());
m_sc= this_scinfo;
}
virtual void before_create(CREATESTRUCT &cs)
{
// 指定类名
assert(m_sc.class_name);
cs.lpszClass = MAKEINTATOM(m_sc.class_name);
}
};
wndbase有create函数,用于创建窗口,在创建之前会调用before_create,允许派生类改变一些设置。这里指定创建的类名。
轮到SCWndProc的实现了:
LRESULT CALLBACK wndproc::SCWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
msg_struct msg={0};
// ...
if(msg.wnd)
{
WNDPROC pOldProc= static_cast<scwnd *>(msg.wnd)->m_sc.old_proc;
msg.cur_slot= msg.wnd->m_mapslot_head->next;
if(process(msg))
return msg.result;
return ::CallWindowProc(pOldProc, hWnd, message, wParam, lParam);
}
return ::DefWindowProc(hWnd, message, wParam, lParam);
}
前半部分的代码和WndProc的代码一样,后面的就有区别了。若获取到scwnd对象,一开始保存原先的窗口过程指针到pOldProc,这一步很重要,因为process返回后,msg.wnd可能不存在了,若没有预先保存而直接引用scwnd里面的m_sc,就有可能crash。
这里SCWndProc和scwnd是紧耦合的,必须确保super_class这个函数,只能由scwnd使用。所以,将super_class移入到scwnd。
class scwnd : public wndbase
{
static scinfo_t super_class(const TCHAR *old_name, const TCHAR *new_name);
// ...
};
而wndproc的3个public函数,按道理也应该全部private,但因为这个wndproc,是内部实现的方式,只要不暴露给外部,public也没有关系。
至此,超类化的代码已经完成。我们再举一个超类化编辑框的例子:
class editbox : public scwnd
{
struct superclass
{
const TCHAR * old_name()const { return _T("EDIT"); }
const TCHAR * name_name()const { return _T("wabcedit"); }
};
public:
editbox():scwnd(superclass()){}
};
其它的控件,比如“COMBOBOX”、WC_TREEVIEW、WC_TABCONTROL等等都是类似的处理。
完成了超类化,子类化的实现也很简单了,无非就是替换原来的窗口过程:
class scwnd : public wabc::wndbase
{
// ...
public:
// 子类化窗口的构造函数
scwnd(){ ::memset(&m_sc, 0, sizeof(m_sc)); }
// subclass window
WNDPROC attach(HWND hWnd)
{
assert(m_hWnd == 0);
assert(m_sc.old_proc == 0 && m_sc.class_name == 0);
m_hWnd = hWnd;
::SetWindowLongPtr(hWnd, GWL_USERDATA, (LONG)this);
m_sc.old_proc = (WNDPROC)SetWindowLongPtr(hWnd, GWL_WNDPROC, (LONG)&wndproc::SCWndProc);
return m_sc.old_proc;
}
void detach()
{
assert(m_hWnd);
assert(m_sc.old_proc && m_sc.class_name == 0);
if (m_hWnd)
{
SetWindowLongPtr(m_hWnd, GWL_WNDPROC, (LONG)m_sc.old_proc);
m_hWnd = 0;
m_sc.old_proc = 0;
}
}
}
attach()和detach()必须成对调用,由使用者保证,若调用了attach()而scwnd析构时候没有调用detach(),会导致意外发生。这里依然假设GWL_USERDATA没有被外面使用,一个更好的实现是采用thunk技术。
总结:
子类化和超类化都是二进制级别代码重用的方案。在不修改原先代码的前提下,对其改造。在源码级别,是不需要这么做的。wabc库,只注册了一个class,这class生成的窗口,功能可以是千变万化。windows操作系统内置一些标准控件,有时候需要定制。wabc库提供超类化的接口主要用作这种场合。
请点击这里下载'wabc'库的最终源码。