win32消息映射13-子类化和超类化

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值