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

win32消息映射13-子类化和超类化相关推荐

  1. 实现 Win32 程序的消息映射宏(类似 MFC )

    对于消息映射宏,不用多说了,用过 MFC 的人都很清楚.但目前有不少程序由于各种原因并没有使用 MFC,所以本帖讨论一下如何在 Win32 程序中实现类似MFC的消息映射宏.其实 Windows 的头 ...

  2. Win32汇编---控件的超类化感想

    对于窗口的子类化相信大家并不陌生:基于某一个控件功能,用窗口子类化来实现我们想要的功能!由于控件的封装,我们无法对它进行直接操作修改,但是我们可以截获windows给控件过程发送的消息,从而达到控制控 ...

  3. MFC 教程【4_消息映射的实现】

    消息映射的实现 Windows消息概述 Windows应用程序的输入由Windows系统以消息的形式发送给应用程序的窗口.这些窗口通过窗口过程来接收和处理消息,然后把控制返还给Windows. 消息的 ...

  4. 转MFC消息映射梳理

    http://blog.csdn.net/phunxm/article/details/5640766 一.CWnd消息处理 一切从窗口(HWND)的创建说起,在MFC中,CWnd::CreateEx ...

  5. 【MFC系列-第9天】MFC消息映射机制的原理

    关注公号[逆向通信猿]更精彩!!! 第9天 MFC消息映射机制的原理 9.1 对话框常用的回调函数 a)窗口创建时的消息和虚函数包括:WM_CREATE,WM_INITDIALOG,和PreSubcl ...

  6. MFC消息映射与消息传递内幕

    MFC消息映射与消息传递内幕 Windows操作系统是以消息为基础,事件驱动的.作为程序员了解操作系统的消息传递机制是非常必要的.Microsoft的MFC有它自己的一套支持Windows操作系统消息 ...

  7. 【转】MFC消息映射详解(整理转载)

    消息:主要指由用户操作而向应用程序发出的信息,也包括操作系统内部产生的消息.例如,单击鼠标左按钮,windows将产WM_LBUTTONDOWN消息,而释放鼠标左按钮将产生WM_LBUTTONUP消息 ...

  8. MFC的消息映射有什么作用

    绝对以下这三个解释的比较简洁,特此做个记录!以感谢回答的这些人! MFC的消息映射有什么作用: Windows操作系统主要是有消息来处理的,每个程序都有自己的消息队列,并且这些消息是有优先级的,也就是 ...

  9. MFC消息映射与命令传递

    独酌逸醉(博客搬至http://www.perfect-is-shit.com/,本博不再更新!) Keep It Simple,Stupid! 博客园 首页 博问 闪存 联系 管理 随笔-63  文 ...

最新文章

  1. 不同的PCB混装方式及加工工艺
  2. 华为v9计算机在哪方面的应用,华为荣耀V9和华为Mate9区别在哪 哪款更好?
  3. Windows下安装BeautifulSoup
  4. 5 微信公众号开发 获取 access_token
  5. AI岗位秋招纪实:算法原理扎实才是王道,而不是调参
  6. dropdownlist ajax联动,asp.net省市三级联动的DropDownList+Ajax的三种框架(aspnet/Jquery/ExtJs)示例...
  7. 什么样的外链才是高质量的外链|网站优化
  8. 【Vue】—事件处理
  9. python经典实例-python经典实例
  10. 如何在Pages for Mac中添加页眉、页脚和页码?
  11. 【疾病分类】基于matlab LBP果实病害检测分类【含Matlab源码 1714期】
  12. 嵌套类nested class 和 抽象类abstract
  13. js 拖拽上传文件及文件夹
  14. Video Classification with Channel-Separated Convolutional Netwroks 论文阅读
  15. 图像语义分割-CVPR2020-CPNet:结合不同类别的上下文信息先验--场景语义分割--注意力机制--Context Prior for Scene Segmentation--关键创新点代码复现
  16. 渲染到纹理(Render To Texture, RTT)
  17. 非线性可视化(3)混沌系统
  18. AI生死劫,什么样的公司将被洪流吞噬?
  19. Android 手电筒的开启方法
  20. 《EXPLAINING AND HARNESSING ADVERSARIAL EXAMPLES》阅读笔记

热门文章

  1. cordova打包项目启动页面和图标的设置
  2. 解决泛微 Emobile7打卡定位OA
  3. Windows网络编程系列教程之四:Select模型
  4. Linux 入门基础(苏勇)课程笔记
  5. WUST-CTF 2020 WriteUp
  6. GitHub编程资源分享
  7. antd table 被内容撑开,设置columns宽度失效
  8. 使用ConnectifyInstaller软件模拟wifi热点,实现网络共享
  9. 大数据之Hive:hive中的cross join函数
  10. 一文详解特权访问管理(PAM)