6. 窗口的创建和注销
在Delphi里,有个FormCreate和FormDestroy事件。FormCreate事件在Form创建之后发生,FormDestroy事件在Form销毁之前发生。我们在这里也引入类似的功能,但是和Delphi的这两个事件有所差别。
这两个事件很有用,FormCreate相当于是整个窗口生命周期的入口,而FormDestroy,是整个窗口生命周期的出口。可以简单的理解,一进入FormCreate,这时候窗口句柄是有效的,一离开FormDestroy,窗口句柄变得无效了。多数情况,在FormCreate内会创建子窗口,而在FormDestroy内,做一些和窗口相关的资源清理动作。
窗口创建成功的时候,操作系统会给窗口发送WM_NCCREATE和WM_CREATE消息,而窗口要被注销的时候,操作系统会发送WM_DESTROY和WM_NCDESTROY消息,很明显,FormCreate和FormDestroy事件一定和其中的两个消息相关联。WM_NCCREATE可以理解成窗口接收的第一个消息,WM_NCDESTROY是窗口接收的最后一个消息。WM_NCCREATE消息是窗口非客户区之后发送,由于客户区这时候还没创建,这和FormCreate的原意不符,同理,WM_NCDESTROY发生的时候,窗口客户区被注销了,这仍然和FormDestroy的原意不符。所以FormCreate只能对应WM_CREATE,而FormDestroy对应WM_DESTROY。
在wabc库里,WM_CREATE消息相当于c++中的构造函数,而WM_DESTROY则相当于c++中的析构函数,也就是说,消息的发生起始于WM_CREATE消息,终止于WM_DESTROY消息,若映射了发生在WM_CREATE之前的消息,或WM_DESTROY之后的消息,必须确保知道自己在做什么。
在c++的构造函数里,若存在继承,构造函数是从基类往派生类执行。同理,在调用WM_CREATE消息的映射函数,也要依照这顺序调用,即最先映射的先调用。而WM_DESTROY消息则相反,最后映射的最先调用。在前面说继承的时候讲到,基类派生类映射同一个消息,优先权放在派生类,这和WM_DESTROY的调用顺序吻合。而对于WM_CREATE消息,则需要特殊处理。一种简单的实现是:
LRESULT CALLBACK WndProc(...)
{
typedef std::pair<const msgmap_t *, const msgmap_t *> pair_type;
wndbase *p= 0;
// ...
if(p)
{
if(msg.message == WM_CREATE)
process_WM_CREATE(...);
pair_type pr;
// ...
}
return ::DefWindowProc( hWnd, message, wParam, lParam );
}
这种实现的思想,是将某个消息特例化,当只特例化一两个消息的时候,这种实现方式还是可取的,若要特例化很多个消息的时候,这种实现方式就很丑陋了。在将来,我们还要特例化WM_COMMAND,WM_NOTIFY等消息的处理。所以这里的实现有必要优化。
WndProc渐渐复杂起来了,我们需要重构里面的代码,使之有更好的可维护性和可阅读性。引入一个类:wndproc:
struct wndproc
{
static LRESULT CALLBACK WndProc(...);
static bool process_WM_CREATE(wndbase &wnd, msg_struct &msg);
static bool process_WM(wndbase &wnd, msg_struct &msg);
};
当只有一两个消息需要特殊化处理的时候,用顺序查找效率高且实现简单,但有多个消息需要特殊处理,用二分查找是不二之选:
LRESULT CALLBACK wndproc::WndProc(...)
{
struct special_msg
{
typedef bool (*fun_t)(wndbase &, msg_struct &);
UINT message;
fun_t fun;
};
static special_msg items[] = {
WM_CREATE, &wndproc::process_WM_CREATE,
};
// ...
if(p)
{
const size_t nSize = countof(items);
size_t first = 0, last = nSize, n = nSize, m, tmp;
#ifdef _DEBUG
for (m = 1; m < countof(items); ++m)
{
assert(items[m - 1].message < items[m].message);
}
#endif
while (0 < n)
{
m = n / 2;
tmp = first + m;
if (items[tmp].message < msg.message)
{
first = tmp + 1;
n -= m + 1;
}
else if (msg.message < items[tmp].message)
n = m;
else
{
if(items[tmp].fun(*p, msg))
return msg.result;
else
return ::DefWindowProc( hWnd, message, wParam, lParam );
}
}
if(process_WM(*p, msg))
return msg.result;
}
return ::DefWindowProc( hWnd, message, wParam, lParam );
};
在wndproc::WndProc里,先看看是不是需要特殊处理的消息,若是,调用相关的函数,若不是,调用wndproc::process_WM,走回正常流程。
要注意里面的#ifdef _DEBUG;因为里面用到二分查找,在debug版本下检查items数组是否有序,Release版本就没必要检查了。
bool wndproc::process_WM_CREATE(wndbase &wnd, msg_struct &msg)
{
typedef std::pair<const msgmap_t *, const msgmap_t *> pair_type;
pair_type pr;
msgmap_t v;
v.message = msg.message;
mapslot_node *pSlot= p->m_mapslot_head.prior;
for(;pSlot!= &p->m_mapslot_head;pSlot= pSlot->prior)
{
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;
v.invoke(v, slot.wnd, msg );
}
}
return true;
}
WM_CREATE在wabc库里被认为是窗口初始化的消息,前面讲继承的时候说过,mapslot链表的节点可能分布于各个对象中。各个对象之间的关系可能是低耦合,这些对象有可能需要一个初始化的时机,这时机就落在了WM_CREATE消息上。从而就出现了一个需求,若对象映射了WM_CREATE消息,那么这映射函数一定会执行。也就是说,每个mapslot链表节点的WM_CREATE映射函数,一定要执行。所以process_WM_CREATE里并没有判断v.invoke的返回值。
而WM_DESTROY也是同理,每个映射的WM_DESTROY函数也一定会被调用,为了和process_WM配合,修改map_destroy的on_map函数,使之永远返回false:
struct map_destroy : msgmap_t
{
// ...
static bool on_map(const msgmap_t &a, void * x, msg_struct &msg)
{
// ...
return false;
}
}
#define WABC_ON_CREATE(f) \
{ WM_CHAR, map_destroy::map_fun_addr<map_class>(f), &map_destroy::on_map },
#define WABC_ON_DESTROY(f) \
{ WM_CHAR, map_destroy::map_fun_addr<map_class>(f), &map_destroy::on_map },
窗口怎么创建和销毁呢?若直接用api的方式,必须确保和wndproc打交道,若不如此,这套映射机制根本不会起到任何作用。一个好的设计,应该将可能出错的地方封装起来。回顾SDK创建窗口的方式:1.以某一类名注册一个class;2.以这个类名创建窗口。
引入一个application类,用作全局。
application *g_app = 0;
static ATOM register_class(HINSTANCE hInstance,const wchar_t *lpClassName= L"wabc")
{
WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS,
&wndproc::WndProc, 0, 0, hInstance, NULL, ::LoadCursor(0, IDC_ARROW), // NULL
(HBRUSH)(COLOR_WINDOW + 1), NULL, lpClassName, NULL };
const ATOM atom = RegisterClassEx(&wc);
return atom;
}
class application
{
HINSTANCE m_hInstance;
public:
const ATOM defclass;
explicit application(HINSTANCE hInstance,const wchar_t *lpClassName= L"wabc"):m_hInstance(hInstance)
,defclass(register_class(hInstance, lpClassName)
{
g_app= this;
}
operator HINSTANCE()const { return m_hInstance; }
};
在application构造函数里,注册一个窗口class,确保这个窗口class的回调函数是wndproc::WndProc。然后,一个全局变量g_app指向这个对象。在整个app生命周期中,只应该存在一个application对象且要确保这个对象在整个app生命周期中都存在。
在wndbase加入创建的代码:
class wndbase : public basewnd
{
public:
// ...
virtual void before_create(CREATESTRUCT &cs)
{
cs.lpszClass = MAKEINTATOM(g_app->defclass);
}
HWND create(const wchar_t *lpszCaption, DWORD nStyle= WS_VISIBLE|WS_OVERLAPPEDWINDOW, DWORD dwStyleEx = 0,
const RECT *rt = 0, HMENU hMenu= 0);
HWND create(HWND hParent, DWORD nStyle, DWORD dwStyleEx = 0,
const wchar_t *lpszCaption = 0, size_t id = 0, const RECT *rt = 0);
HWND create(const CREATESTRUCT &cs)
{
assert(m_hWnd == 0);
return ::CreateWindowEx(
cs.dwExStyle,
cs.lpszClass,
cs.lpszName,
cs.style,
cs.x,
cs.y,
cs.cx,
cs.cy,
cs.hwndParent,
cs.hMenu,
cs.hInstance,
this
);
}
void destroy()
{
if (m_hWnd)
{
::DestroyWindow(m_hWnd);
m_hWnd = 0;
}
}
};
引入一个虚函数before_create,允许派生类在创建之前有机会改变创建的参数。
destroy()的函数很简单,和直接调用api差不多。
第一个create,是创建一个没有parent的窗口。第二个,创建有parent的窗口,第三个,完全按照CREATESTRUCT的参数创建,里面不做任何改动。
HWND wndbase::create(const wchar_t *lpszCaption, DWORD nStyle, DWORD dwStyleEx,
const RECT *rt, HMENU hMenu)
{
CREATESTRUCT cs={ 0 };
cs.hInstance = *g_app;
before_create(cs);
cs.style |= nStyle;
cs.dwExStyle |= dwStyleEx;
cs.lpszName = lpszCaption;
cs.hMenu = hMenu;
if (rt)
{
cs.x = rt->left;
cs.y = rt->top;
cs.cx = rt->right - rt->left;
cs.cy = rt->bottom - rt->top;
}
else
{
cs.cx = cs.x = CW_USEDEFAULT;
cs.cy = cs.y = CW_USEDEFAULT;
}
return create(cs);
}
HWND wndbase::create(HWND hParent, DWORD nStyle, DWORD dwStyleEx,
const wchar_t *lpszCaption, size_t id, const RECT *rt)
{
CREATESTRUCT cs={ 0 };
cs.hInstance = *g_app;
// 为id取一个默认值,若为0,恐怕会在WM_NOTIFY的消息处理中带来麻烦
// 见 process_WM_NOTIFY 的实现
if (nStyle & WS_CHILD)
cs.hMenu = id != 0 ? HMENU(id) : HMENU(this);
else
cs.hMenu = HMENU(id);
before_create(cs);
if (rt)
{
cs.x = rt->left;
cs.y = rt->top;
cs.cx = rt->right - rt->left;
cs.cy = rt->bottom - rt->top;
}
cs.hwndParent = hParent;
cs.style |= nStyle;
cs.dwExStyle |= dwStyleEx;
cs.lpszName = lpszCaption;
return create(cs);
}
这两个函数的实现都很简单,里面重要的一点是什么时候调用before_create()。在before_create()里,有可能改变cs某些成员的值,但改变后,有可能被create的相关参数所替换。这里的考虑是:以create的参数优先。毕竟,create有着最直观的语义,也易于理解。before_create一般情况下不应该使用,它存在的意义在于一些特殊的场合。
现在,我们能用至今所做的代码,去完成一个最基本的sdk程序。(上面的代码,全部封装在命名空间wabc里,上面为了简化说明,忽略掉这命名空间)
.h文件
class HelloWnd : public wabc::wndbase
{
WABC_DECLARE_MSG_MAP()
public:
typedef HelloWnd self;
typedef wabc::wndbase inherited;
HelloWnd();
virtual ~HelloWnd(){}
bool on_destroy()
{
::PostQuitMessage(0);
return true;
}
bool on_paint(HDC hdc, const RECT &rtClip, wabc::msg_paint &)
{
::TextOut( hdc, 0, 0, _T("Hello"), 5 );
return true;
}
};
.cpp文件
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
wabc::application app(hInstance);
HelloWnd wnd;
wnd.create(_T("Hello"));
MSG msg;
while ( ::GetMessage(&msg, NULL, 0, 0))
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
return int(msg.wParam);
}
HelloWnd::HelloWnd()
{
WABC_BEGIN_MSG_MAP(self)
WABC_ON_DESTROY(&self::on_destroy)
WABC_ON_PAINT(&self::on_paint)
WABC_END_MSG_MAP()
}
消息循环的代码,很难用统一的方式去封装,有时候用GetMessage,有时候用PeekMessage,还有一些形形色色的需求。封装不了东西,干脆不封装,不封装也不会造成出错。
对于WM_CREATE和WM_DESTROY消息,其on_map函数要永远返回false,而对于WM_PAINT,要永远返回true。其它的消息,由消息的映射函数决定。
总结:
做任何设计,边界问题最为烦人。反而中间的过程相对轻松,在边界上,要考虑的问题很多,如何做出正确的抉择,和人对设计的认知、经验有着密切的关系。经验是从错误中来,一个成熟的库,能避免大部分的错误,但有些错误做不到绝对的避免。在知道原理的前提下去使用这些库,能避免一些惊诧的错误。一个库,封装得太多,难于理解,封装得太少,又达不到封装的初衷,占用的内存、运行的效率、代码的稳定和可维护性,这里面方方面面的权衡,很考验凡人的智慧:)
请点击这里下载'wabc'库的最终源码。