win32消息映射7-窗口的创建和注销

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值