IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope:Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Linux  >

GUI 应用程序移植,第 4 章:窗口子系统

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


肖 习攀 (xiaoxp@cn.ibm.com), 高级软件工程师, IBM
阎 小兵 (yanxb@cn.ibm.com), 高级开发经理, IBM
贾 迎乐 (jiayingl@cn.ibm.com), 高级软件工程师, IBM
龚 奕平 (gongyp@cn.ibm.com), 高级软件工程师, IBM

2007 年 10 月 18 日

窗口子系统是整个图形用户界面系统的核心--用户所有的交互操作无一不是围绕窗口进行的。Windows 和 Linux 操作系统都提供了各自的窗口系统,虽然它们具有一些类似的概念和特性,但从系统架构和实现方式上差别还是很大的,这种差别给在 Linux 上模拟 Windows 窗口系统的特性和行为带来了一定的困难。本章首先对 Windows 和 Linux 的窗口系统做比较,然后阐述模拟层的窗口类、窗口和窗口句柄、窗口过程、窗口绘图等是如何实现的。

引言

《GUI应用程序移植——在Linux上模拟Windows API的方法》一书介绍一种把Windows GUI应用程序移植到Linux的方法——API模拟方法。书中介绍了这种方法的设计思想,以及具体的实现过程。同时,充分探讨了GUI应用程序移植所必然面对的基于不同操作系统的编程模型的差异,揭示了两个系统有关用户界面交互和图形输出基本逻辑结构的一些鲜为人知的特性。我们推出了此书的第 14 章供大家在线浏览。

更多推荐书籍请访问 developerWorks 图书频道





回页首


4.1 Windows和Linux的窗口系统比较

图书信息 书名:《GUI应用程序移植》
作者:肖习攀、阎小兵、贾迎乐、龚奕平 等著
出版社:电子工业出版社
出版日期:2007 年 03 月
ISBN:978-7-121-03832-7
购买: 中国互动出版网dearbook

推荐章节:

更多推荐书籍,请访问 developerWorks 图书频道
如果您对本书有任何的建议、意见或者问题,欢迎与本书作者和 IBM 技术专家 交流

由于设计出发点的不同,Windows和Linux的窗口系统架构差异非常大,各有优缺点。本节将对两者进行分析和比较。

4.1.1 Windows的窗口系统

Windows窗口系统设计的侧重点是单一PC机上的用户体验。Windows窗口系统的架构如图4-1所示。


图4-1 Windows窗口系统的架构
图4-1  Windows窗口系统的架构

由图可知,Windows窗口系统由用户态的组件user32.dll和核心态的win32k.sys组成。

出于性能方面的考虑,Windows窗口系统使用系统中断的服务方式,而不是进程间通信的服务方式(进程间通信涉及到多次进程上下文切换,因而开销较大)。窗口系统的大部分实现被放置在核心态组件win32k.sys中,用户态的user32.dll提供了对win32k.sys中相应系统服务调用的封装。例如,当用户调用user32.dll中的CreateWindowEx函数创建一个窗口时,user32.dll通过中断将这个请求转发到核心态组件win32k.sys,在那里完成实际的窗口创建过程。为了进一步优化性能,Windows还将win32k.sys管理的对象以只读方式映射到用户态。这样,对于诸如GetWindowRect等简单而调用频繁的函数,user32.dll可以直接访问窗口数据而无需调用系统中断。另外,Windows窗口系统对请求的处理是同步的。例如,当MoveWindow或SetWindowPos函数返回时,指定窗口的大小和位置已经被改变了。这一点对程序员编程和调试来说是直接和方便的。

Windows窗口系统的另一个显著特点是在系统级别中提供内建的GUI模型,系统内置了诸如标题栏、按钮、滚动条、菜单、对话框这样的窗口部件。这样有利于程序员编程,用户界面也易于使用和掌握。Windows中的窗口管理器(Window Manager)也是系统内建的功能,其接口和实现散布在不同的系统函数中,如DefWindowProc函数就提供了大部分和窗口移动、改变大小等有关的支持。窗口管理器是系统内建的,所以用户无法简单地替换它。

4.1.2 Linux的窗口系统

Linux窗口系统的架构如图4-2所示。


图4-2 Linux窗口系统的架构
图4-2  Linux窗口系统的架构

Linux窗口系统是基于Unix世界流行的X窗口系统实现的。如第1章所述,X窗口系统自设计之初即强调硬件和网络的透明性,因而采用了基于客户机/服务器架构的系统设计。X客户端和X服务器可以位于一台计算机上(通常情况),也可以位于网络上的两台计算机上。这种客户机/服务器的设计提供了很好的网络能力和可扩展性,非常符合Unix/Linux系统的设计思想。但是客户机/服务器之间的通信开销降低了系统的性能(即便是在同一台计算机上,通过共享内存的方式进行进程间通信,与Windows的窗口系统相比仍然低效很多)。另外,X窗口系统对请求的处理是异步的。例如,当X客户端调用XMoveResizeWindow函数返回时,生成的请求通常并没有真正被X服务器处理,X客户端只有监听XWindowChanges等事件才能确认这个请求已经生效。这一点对程序员编程和调试来说不是很方便。

与Windows相比,Linux窗口系统在功能和实现上使用了清晰的模块化结构。X窗口系统自身并没有提供内建的GUI模型,只提供基本的窗口服务(大小、显示、绘图等),没有诸如标题栏、按钮、滚动条、菜单、对话框这样的窗口部件。大部分窗口部件在GTK/QT这样的工具包中实现,而标题栏等窗口管理相关的部件和功能则是由单独的窗口管理器进程实现的(窗口管理器也是一个标准的X程序)。熟悉Windows窗口系统的程序员对于Linux上窗口标题栏是由窗口管理器进程创建的另外一个窗口这一点可能很吃惊。这一模块化结构的优点是用户可以任意替换窗口部件工具包和窗口管理器,缺点则是不同窗口部件工具包和窗口管理器之间的差异不但会给用户带来使用上的不方便,而且也给程序员的编程带来不少的麻烦。当然,随着Linux窗口系统的演化和标准化,这种状况也在逐渐改进。





回页首


4.2 窗口类

和C++语言的类概念类似,窗口类是窗口对象的模板,描述了属于同一类窗口对象的公共性质和行为。在能够创建任何窗口之前必须有已经注册好的窗口类。系统初始化时会自动注册系统内建控件的窗口类,其中既包含提供给应用程序使用的窗口类,如按钮(Button)和编辑框(Edit),也包含系统内部使用的窗口类,如菜单(#32768)和对话框(#32770)。应用程序也可以调用RegisterClass或RegisterClassEx函数来注册自己的窗口类,调用UnregisterClass函数来取消已注册的窗口类。除此之外,应用程序还可以调用GetClassName和GetClassLong函数来获取窗口类的信息,调用SetClassLong函数来更改已注册窗口类的信息。

下面介绍模拟层中窗口类实现的两个主要部分:窗口类数据结构,以及窗口类的注册和撤销。

4.2.1 窗口类数据结构

模拟层使用一个链表结构来存储所有已注册的窗口类的信息。注册一个窗口类时,指定的窗口类的信息被添加到这个链表的末尾;创建窗口时,就在这个链表中查找指定的窗口类的信息。运行时刻,窗口类链表的示意图如图4-3所示。

其中,窗口链表节点使用WindowClass结构来表示,定义如下。


图4-3 窗口类链表
图4-3  窗口类链表


struct WindowClass {
    	WindowClass* pNext; // 单向链表
    	ATOM nAtom;
    	UINT style;
    	UINT nIdWndProc;
    	int cbClsExtra;
    	int cbWndExtra;
    	HINSTANCE hInstance;
    	WCHAR* pszMenuName;
    	HICON hIcon, hIconSm;
    	HCURSOR hCursor;
    	HBRUSH hbrBackground;
    	int cWindows;
    	// BYTE clsExtra[cbClsExtra]; // 变长结构
};

这是一个变长结构,其成员释义见表4-1。


表4-1 WindowClass结构成员
表4-1  WindowClass结构成员

在结构的最后面是这个窗口类的用户数据缓冲区,大小是cbClsExtra。没有为这个数据缓冲区单独分配内存,是为了避免二次分配引起的性能开销(cbClsExtra通常都很小,如4个字节等)。在分配WindowClass实例时,必须采取如下的方法:


WindowClass* pNewClass = (WindowClass*)malloc(sizeof(WindowClass) + cbClsExtra);

4.2.2 窗口类的注册和撤销

模拟层中,注册窗口类的RegisterClass和RegisterClassEx函数的流程算法如下。

  1. 调用AddAtom函数将传入的窗口类结构中的窗口类名称(lpszClassName成员)转换为类原子(ATOM)。
  2. 调用内部函数AllocWndProcId将传入的窗口类结构中的窗口过程(lpfnWndProc成员)转换为窗口过程ID。关于窗口过程ID请参见"窗口过程"一节。
  3. 检查窗口类是否已经注册。遍历窗口类链表,如果已有窗口类的类原子和要注册的窗口类的类原子(步骤1中获取的)相同,则执行错误处理并设置最后错误代码为ERROR_CLASS_ALREADY_EXISTS,然后返回FALSE。
  4. 分配一个新的WindowClass实例,并使用传入的窗口类信息及前面获取的类原子和类窗口过程ID初始化它。
  5. 将新分配的WindowClass实例添加到窗口类链表的末尾。

相应的,撤销窗口类的UnregisterClass函数的流程算法如下。

  1. 调用FindAtom函数将传入的窗口类名称转换为类原子(ATOM)。如果失败则设置最后错误代码为ERROR_CLASS_DOES_NOT_EXIST并返回FALSE。
  2. 在窗口类链表中查找符合条件的窗口类。如果找不到,则设置最后错误代码为ERROR_CLASS_DOES_NOT_EXIST并返回FALSE。
  3. 查看该WindowClass结构中的cWindows成员,如果不为0,即窗口类还有窗口实例,则此时尚不可以撤销该窗口类。于是设置最后错误代码为ERROR_CLASS_HAS_ WINDOWS并返回FALSE。
  4. 将该WindowClass结构从窗口类链表中移除,并释放该结构占用的资源。




回页首


4.3 窗口和窗口句柄

窗口和窗口句柄是窗口子系统最基本的数据结构。本节将阐述模拟层中窗口和窗口句柄的设计和实现。

4.3.1 窗口对象

模拟层中用Window结构来表示窗口对象,其主要成员如下面的代码所示。



struct Window {
    	HWND hWnd; // 自身的句柄
    	WindowClass* pWndClass; // 指向窗口类的指针
    	Window* pWndParent; // 指向父窗口的指针
    	Window* pWndOwner; // 指向拥有者的指针
    	Window* pWndChild; // 指向第一个子窗口的指针
    	Window* pWndNext; // 指向下一个兄弟窗口的指针
    	HMENU hMenuOrId; // 对顶层窗口是菜单句柄, 对子窗口是ID
    	HDC hDCPrivate; // 私有设备上下文(仅对CS_OWNDC类风格)
    	RECT rectWindow; // 窗口矩形
    	RECT rectClient; // 客户区矩形
    	UINT nIdWndProc; // 窗口过程ID
    	DWORD dwStyle, dwExStyle;
    	UINT nFlags;
    	LONG userData;
    	int cbWndExtra;
    	char* extra;
    	HRGN hRgnUpdate; // 窗口更新区域
    	LPWSTR pszText; // 窗口标题
    	MenuView* pMenuView;
    	ScrollBarInfo* psbis; // [0]: WS_HSCROLL, [1]: WS_VSCROLL
	DWORD nThreadId;

    	// 系统实现相关部分
    	GtkWidget* gtkWindow;
    	GdkWindow* window;
    	GdkWindow* client;
};

上述结构中的大部分都是平台无关的,即不依赖于GTK的。表4-2是这些成员的详细解释。


表4-2 Window结构成员平台无关部分
表4-2  Window结构成员平台无关部分

续表

Window结构中和系统平台相关的成员释义如表4-3所示。


表4-3 Window结构成员 平台相关部分
表4-3  Window结构成员 平台相关部分

Window结构是窗口相关函数实现的基础,在阅读本章下面各节内容时,请参考该结构定义。

4.3.2 窗口句柄

窗口句柄是窗口的标识。用窗口对象指针作为句柄是最简单的句柄实现方法,但不够健壮(例如,难以处理窗口对象指针释放后又马上被重新分配的情况),从而也就无法很好地实现IsWindow这样的函数。句柄最一般的实现是句柄表索引和其他一些信息的合并。

在Windows上,出于窗口句柄健壮性和Win16兼容性的考虑,将32位的窗口句柄划分为两部分,低16比特是句柄表的索引值,高16比特是句柄表项被分配的次数。例如,桌面窗口的句柄通常是0x00010014。在这里,低16比特的句柄表索引已经完全标识了窗口对象,16位应用程序也是使用这个值。高16比特是给32位应用程序做句柄冗余校验使用的,每次对应的句柄表项被重新分配时这个值都会加1。例如,0x00010014和0x00020014是两个不同的句柄值,系统只需将高16位的分配次数和句柄表中存储的分配次数进行比较,就可以容易地判断句柄是否合法了。

模拟层的实现要相对简单一些。模拟层设计了一个全局数组用来存储所有已分配的窗口对象指针,并设置了一个全局的分配索引用来跟踪当前最小的未分配的数组下标。创建窗口时,将该分配索引对应的数组项置为新建窗口对象的指针,并将该窗口的句柄置为该分配索引加窗口句柄基数。随后,分配索引被置为下一个未分配的数组下标。给定窗口句柄查找窗口对象指针时,只需要将窗口句柄值减去窗口句柄基数,即可得到窗口对象在全局窗口对象数组的下标,从而得到窗口对象的指针。销毁窗口时,要将对应的数组项置空。

模拟层内部的GetWindowPtr函数用来将一个窗口句柄转换为窗口对象指针,对于非法窗口句柄则返回空指针。GetWindowPtr函数的定义如下:


Window* GetWindowPtr(HWND hWnd);

4.3.3 窗口的创建和销毁

CreateWindow和CreateWindowEx函数用于创建窗口。其中前者只是对后者的简单封装而已(以0作为扩展风格调用后者)。因为内在繁杂的创建和初始化逻辑,CreateWindowEx毫无疑问是窗口函数中最复杂的函数之一。模拟层中,CreateWindowEx函数的流程算法如下:

  1. 首先对传入参数进行基本验证和处理。例如,如果调用者传入的width或height是负值的话,将视之为0。另外,会使用一个CREATESTRUCT结构保存传入的参数,这个结构在稍后发送WM_NCCREATE和WM_CREATE消息的时候会用到。
  2. 如果创建的是顶层窗口(具有WS_OVERLAPPED或WS_POPUP风格),则hWndParent参数指的是拥有者窗口(owner),而不是父窗口。CreateWindowEx函数会调用GetAncestor(hWndParent, GA_ROOT)来获得hWndParent参数对应的顶层窗口(因为调用者传入的hWndParent参数可能是子窗口)。
  3. 定位lpClassName参数指定的窗口类结构指针。lpClassName参数可以是窗口类名称字符串(如"Button"),也可以是RegisterClass函数返回的窗口类原子。如果找不到对应的窗口类结构指针,则设置最后错误代码为ERROR_CANNOT_FIND_WND_CLASS并返回NULL。
  4. 如果创建的是顶层窗口,而应用程序没有指定菜单句柄,则CreateWindowEx函数将查看窗口类结构是否定义了类菜单(即pszMenuName成员是否为空)。如果是,则调用LoadMenu函数加载窗口类菜单,并将它作为新创建窗口的菜单。
  5. 创建窗口所需的参数已经足够了。系统根据窗口类结构的相关信息分配一个新的Window对象和窗口句柄,使用前述参数初始化该Window对象,并将它加入到全局窗口树中。
  6. 创建相应的GDK窗口并初始化窗口属性。示例代码如下:
    
    Window* pWnd = ...;
    
    // 创建GdkWindow
    GdkWindowAttr attr;
    memset(&attr, 0, sizeof(attr));
    attr.window_type = (pWnd->pWndParent->hWnd) == GetDesktopWindow() ? 
        GDK_WINDOW_TOPLEVEL : GDK_WINDOW_CHILD);
    attr.event_mask = GDK_EXPOSURE_MASK | GDK_POINTER_MOTION_MASK | GDK_BUTTON
    _MOTION_MASK | GDK_BUTTON1_MOTION_MASK | GDK_BUTTON2_MOTION_MASK |
    GDK_BUTTON3_ MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
    | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_FOCUS_CHANGE_MASK |
    GDK_VISIBILITY_NOTIFY_MASK;
    attr.x = attr.y = 0;
    attr.width = attr.height = 1;
    attr.wclass = GDK_INPUT_OUTPUT;
    pWnd->window = gdk_window_new(pWnd->pWndParent->client, &attr,
    GDK_WA_X | 
    GDK_WA_Y);
    
    // 如果窗口不需要窗口管理器修饰(标题栏和边框),则设置相应属性
    if (((pWnd->dwStyle & WS_POPUP) && (pWnd->dwStyle & WS_CAPTION) != WS_CAPTION))
        gdk_window_set_decorations(pWnd->window, (GdkWMDecoration)0);
    
    // 设置是否在系统任务栏和窗口列表中显示
    if (!(dwExStyle & WS_EX_APPWINDOW) && ((dwExStyle & WS_EX_TOOLWINDOW) || 
        hWndOwner)) {
        gdk_window_set_skip_taskbar_hint(pWnd->window, TRUE); // taskbar
        gdk_window_set_skip_pager_hint(pWnd->window, TRUE); // Alt+Tab window
    }
    ...
    

  7. 如果创建的是顶层窗口(具有WS_OVERLAPPED或WS_POPUP风格),则窗口收到的第一条消息可能是WM_GETMINMAXINFO,而不是WM_NCCREATE。发送WM_ GETMINMAXINFO消息是为了决定窗口的实际大小。值得注意的是,当窗口过程收到WM_GETMINMAXINFO消息时,其rectWindow和rectClient都是空(未设置)。
  8. 计算相对于屏幕的窗口矩形--调用者传入的窗口位置是相对于父窗口的客户区的。然后调整窗口的rectWindow和rectClient值,将它们设置为计算的窗口矩形。底层的GDK窗口的位置和大小也要做相应调整。
  9. 使用一开始保存的CREATESTRUCT结构为参数,向窗口发送WM_NCCREATE消息。此时窗口的客户区矩形仍未计算。默认的窗口过程DefWindowProc会在这个消息里设置窗口标题等。如果窗口过程对WM_NCCREATE消息返回FALSE,则销毁窗口并返回NULL。
  10. 计算窗口的客户区矩形。这是通过给窗口发送WM_NCCALCSIZE消息达到的,消息的wParam是FALSE,lParam是前面计算的相对于屏幕的窗口矩形的指针。应用程序通常不处理WM_NCCALCSIZE消息,默认的窗口过程DefWindowProc会根据窗口风格(如WS_CAPTION)和扩展风格(如WS_EX_CLIENTEDGE)计算客户区矩形。系统在对窗口过程计算的客户区矩形进行验证之后会更新窗口的rectClient值。
  11. 使用一开始保存的CREATESTRUCT结构为参数,向窗口发送WM_CREATE消息。此时,必要的窗口初始化工作已经完成(只是窗口仍然是不可见的)。如果窗口过程对WM_CREATE消息返回 1,则销毁窗口并返回NULL。
  12. 如果创建的是子窗口,此时系统会给窗口发送WM_SIZE和WM_MOVE消息。否则,只是在窗口上设置WF_SENDSIZE标志,并在以后ShowWindow被初次调用时再给窗口发送WM_SIZE和WM_MOVE消息。微软设计这一行为的考虑可能是为了优化性能--对不可见窗口进行布局没有太大意义。
  13. 如果创建的是子窗口并且没有指定WS_EX_NOPARENTNOTIFY扩展风格,则CreateWindowEx函数会给父窗口发送WM_PARENTNOTIFY消息以通知有子窗口刚被创建。发送的WM_PARENTNOTIFY消息的wParam是MAKEWPARAM(WM_CREATE, pWnd->hMenuOrId),lParam是窗口的句柄。
  14. 在整个创建过程中,窗口一直是不可见的(即使创建时指定了WS_VISIBLE风格)。在返回前,如果指定了WS_VISIBLE风格,则CreateWindowEx函数会调用ShowWindow函数显示窗口--如果指定了WS_MAXIMIZE风格,则ShowWindow的参数为SW_ SHOWMAXIMIZED;如果指定了WS_MINIMIZE,则ShowWindow的参数为SW_ SHOWMINIMIZED,等等。

CreateWindowEx函数的流程示意图如图4-4所示。

模拟层中,DestroyWindow函数的流程如下:

  1. 验证传入的窗口句柄并取得窗口对象指针。

    图4-4 Create WindowEx函数的流程
    图4-4  Create WindowEx函数的流程

  2. 验证指定的窗口是否是当前线程创建的--DestroyWindow函数不能用于销毁其他线程创建的窗口。如果指定的窗口不是当前线程创建的,设置最后错误代码为ERROR_ACCESS_DENIED并返回FALSE。
  3. 如果窗口是子窗口(具有WS_CHILD风格),并且窗口没有WS_EX_ NOPARENTNOTIFY扩展风格,则调用SendMessage函数给它的父窗口发送WM_ PARENTNOTIFY消息。
  4. 销毁所有该窗口拥有的(owned)窗口(一个窗口可以拥有多个窗口)。
  5. 销毁窗口拥有的用户输入资源,包括鼠标捕获、键盘焦点和插入符等。
  6. 调用内部函数DestroyWindowCore递归销毁窗口及其子孙窗口。稍后将详细阐述DestroyWindowCore函数。
  7. 调用gdk_window_destroy函数销毁窗口拥有的GDK窗口。
  8. 如果窗口是可见的,则处理因窗口销毁导致的X服务器为其他曾被遮挡窗口产生的Expose事件。

内部函数DestroyWindowCore用于递归销毁窗口及其子孙窗口,声明如下:


void DestroyWindowCore(HWND hWnd);

它的流程算法如下:

  1. 调用SendMessage函数给窗口发送WM_DESTROY消息。
  2. 递归销毁所有子孙窗口。DestroyWindowCore函数会取得窗口的子窗口列表,然后对每一个子窗口,以子窗口句柄作为参数调用DestroyWindowCore函数。
  3. 调用SendMessage函数给窗口发送WM_NCDESTROY消息,这是窗口生命周期中收到的最后一条消息。
  4. 将窗口从窗口树上移除。
  5. 释放窗口占用的系统资源,如菜单和定时器。
  6. 销毁窗口对象和窗口句柄。

DestroyWindow函数的流程请参见图4-5。


图4-5 Destroy Window函数的流程
图4-5  Destroy Window函数的流程




回页首


4.4 窗口过程

Windows消息处理采用回调的机制(Don't call me, I'll call you),当有窗口消息(如绘图消息)需要处理时,系统就调用相应窗口的窗口过程。从面向对象的程序设计角度来看,窗口过程就是窗口对象的行为,不同的窗口过程就展现了不同的窗口对象行为。例如,Static控件和Button控件有不同的窗口过程,决定了它们有不同的外观和功能。窗口过程的定义形式如下(详细释义请参见MSDN):


LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM 
lParam);

当应用程序调用RegisterClass函数注册窗口类时,必须为窗口类提供一个相应的窗口过程。系统提供了内置的DefWindowProc函数可以处理大部分窗口消息,应用程序的窗口过程通常只是处理少数几个感兴趣的窗口消息,然后调用DefWindowProc函数处理其他的消息。在创建了一个窗口之后也可以调用SetWindowLong函数来动态地设置不同的窗口过程,这是实现窗口子类化的方法。

4.4.1 窗口过程的内部表示

由于兼容性的原因,Windows既支持ANSI窗口过程,也支持Unicode窗口过程,两者分别接受不同字符集编码的窗口消息(关于字符集的详细描述请参见"Unicode与国际化"一章)。如果应用程序调用RegisterClassA函数注册窗口类或者调用SetWindowLongA函数设置窗口过程,指定的窗口过程将只接受ANSI编码的窗口消息(如WM_SETTEXT)。如果应用程序调用RegisterClassW或SetWindowLongW函数,则指定的窗口过程将只接受Unicode编码的窗口消息。系统必须跟踪特定窗口过程是ANSI或Unicode的,并且在必要的时候自动对消息参数进行编码转换。例如,如果一个窗口过程只接受Unicode版本的消息,那么当调用SendMessageA函数发送ANSI编码的WM_SETTEXT消息时,系统就必须先将字符串参数(即lParam)转换为Unicode编码的字符串,再将转换后的Unicode字符串传递给该窗口过程。反之亦然。

由此可见,为了实现自动编码转换,在系统内部需要引用窗口过程的地方(如窗口类和窗口对象),不能直接使用函数地址,而必须使用句柄的方式间接引用。在模拟层中,窗口过程的内部表示是一个整数ID,这个ID表示了窗口过程在全局窗口过程表的入口。全局窗口过程表存储了所有调用RegisterClass或SetWindowLong函数时指定的窗口过程的信息(函数地址,是ANSI还是Unicode)。窗口过程内部表示的示意图如图4-6所示。


图4-6 窗口过程的内部表示
图4-6  窗口过程的内部表示

图中,系统预注册的Static窗口类的窗口过程ID是1,它有ANSI版本的窗口过程StaticWindowProcA和Unicode版本的窗口过程StaticWindowProcW(所有系统预定义的窗口类都是既有ANSI版本的窗口过程又有Unicode版本的窗口过程)。用户自定义的MyClass窗口类的窗口过程ID是32,它只有ANSI版本的窗口过程MyWndProc(所有用户自定义的窗口类都是只有ANSI版本的窗口过程或者Unicode版本的窗口过程)。

模拟层内部定义了AllocWndProcId函数,用来在全局窗口过程表中分配窗口过程ID,RegisterClass和SetWindowLong函数的实现会调用它。AllocWndProcId函数的声明如下:


UINT AllocWndProcId(WNDPROC pWndProcA, WNDPROC pWndProcW);

只有系统预定义的窗口类注册时才会传入ANSI和Unicode两个版本的窗口过程地址。AllocWndProcId函数执行时,会先在全局窗口过程表中查找指定的窗口过程地址,如果已经有了则直接返回该ID。否则,将在全局窗口过程表中添加新的入口并返回新入口的ID。

模拟层内部定义了GetRealWndProc函数用来查询指定窗口过程ID对应的实际窗口过程地址,CallWindowProc等函数的实现依赖于它。GetRealWndProc函数的声明如下:


BOOL GetRealWndProc(UINT nIdWndProc, 
OUT WNDPROC* ppWndProcA, OUT WNDPROC* ppWndProcW);

给定窗口过程ID,GetRealWndProc函数会在全局窗口过程表中查询对应的窗口过程入口,并返回ANSI和/或Unicode版本的窗口过程地址。只有系统预定义的窗口类有两个窗口过程地址。

4.4.2 CallWindowProc

如前所述,对于字符串消息,因为可能涉及到ANSI/Unicode编码转换,所以通常不能像调用其他C函数那样直接调用一个窗口过程。为此Windows提供了CallWindowProc函数用来调用一个窗口过程,它会在必要的时候对带字符串参数的消息做ANSI/Unicode转换。以CallWindowProcA函数为例,模拟层中它的流程算法如下:

  • 尝试以传入的lpPrevWndFunc参数作为窗口过程ID,调用GetRealWndProc函数得到ANSI和/或Unicode版本的窗口过程地址。
  • 如果GetRealWndProc函数调用失败,则lpPrevWndFunc参数不是合法的窗口过程ID。CallWindowProcA函数假设lpPrevWndFunc参数是实际的ANSI窗口过程地址并直接调用它。
  • 如果GetRealWndProc函数返回的ANSI窗口过程地址不为空,则CallWindowProcA函数直接调用它。否则,调用内部函数CallWindowProcA2W来进行ANSI到Unicode的编码转换,并调用Unicode版本的窗口过程。

CallWindowProcA2W函数的声明如下:


LRESULT CallWindowProcA2W(WNDPROC pWndProc, HWND hWnd, 
UINT message, WPARAM wParam, LPARAM lParam);

CallWindowProcA2W函数接受Unicode窗口过程和ANSI编码的窗口消息。它将对带字符串参数的消息进行ANSI到Unicode的编码转换并调用传入的Unicode窗口过程。对 于不带字符串参数的消息(如WM_COMMAND),CallWindowProcA2W函数将直接调用传入的Unicode窗口过程。

CallWindowProcA2W函数处理的窗口消息既包含一般的窗口消息,如WM_ NCCREATE和WM_GETTEXT消息,也包含系统内建控件的窗口消息,如列表框的LB_ADDSTRING消息和编辑框的EM_REPLACESEL消息。CallWindowProcA2W函数处理的部分消息如表4-4所示。


表4-4 CallWindowProcA2W函数处理的部分消息
表4-4  CallWindowProcA2W函数处理的部分消息

续表

其中,CallWindowProcA2W函数对WM_GETTEXT消息的处理代码示例如下:



// 获取Unicode编码的窗口文本的长度
int nLengthW = pWndProc(hWnd, WM_GETTEXTLENGTH, 0, 0);
if (nLengthW > wParam - 1)
    nLengthW = wParam - 1;
// 获取Unicode编码的窗口文本
WCHAR* pszW = new WCHAR[nLengthW + 1];
nLengthW = pWndProc(hWnd, WM_GETTEXT, nLengthW + 1, (LPARAM)pszW);
// 将Unicode编码的窗口文本转换为ANSI编码
result = WCSToMBS(pszW, nLengthW, /*out*/ (char*)lParam, wParam);
delete[] pszW;

可以看到,这个转换过程是相当低效的,但是也没有其他选择。最好的实践就是编写应用程序时总使用Unicode以避免ANSI/Unicode编码转换。

CallWindowProcW函数的处理与CallWindowProcA函数刚好相反,在此不再赘述。

4.4.3 DefWindowProc

DefWindowProc函数是Windows内置的窗口过程,为大部分窗口消息提供了默认的处理。应用程序窗口通常只是处理少数几个感兴趣的窗口消息,然后调用DefWindowProc函数处理其他的窗口消息。DefWindowProc函数对于窗口系统的重要作用不言而喻。模拟层中,DefWindowProc函数对一些常见窗口消息的默认处理见表4-5。


表4-5 DefWindowProc函数对常见窗口消息的默认处理
表4-5  DefWindowProc函数对常见窗口消息的默认处理

续表




回页首


4.5 窗口绘图

4.5.1 窗口关联的设备上下文

Windows通过GDI来进行图形的绘制--甚至是Windows窗口系统本身也是基于GDI实现的。设备上下文是通过GDI绘制图形的基本入口,在窗口上绘图就需要使用和窗口关联的设备上下文。关于设备上下文的详细讨论请参见前述章节。

Windows中,用来获取与窗口关联的设备上下文的函数有GetWindowDC, GetDC, GetDCEx和BeginPaint。其中,GetWindowDC函数返回窗口的设备上下文,通过这个设备上下文可以在整个窗口上作图(通常用来绘制自定义的标题栏等)。GetDC函数返回窗口客户区的设备上下文,通过它只能在窗口的客户区上作图。GetDCEx函数则具有更多的参数和标志,允许应用程序对返回的设备上下文的属性进行更多控制。可以想象,GetWindowDC和GetDC函数可以基于GetDCEx函数实现(稍后将看到这一点)。

模拟层中,GetDCEx函数的流程算法如下:

  1. 验证传入的窗口句柄并取得窗口对象指针。如果传入的窗口句柄是空,则使用桌面窗口。
  2. 如果创建参数指定了DCX_WINDOW标志,则获取的是窗口设备上下文(包含非客户区和客户区)。GetDCEx函数调用GDI内部函数创建一个和整个窗口对应的普通设备上下文。
  3. 否则,获取的是客户区设备上下文。如果创建参数没有指定DCX_CACHE并且窗口有私有设备上下文,则直接返回它;否则,调用GDI内部函数创建一个和窗口客户区对应的普通设备上下文。如果窗口有更新区域,并且创建参数指定了DCX_ INTERSECTUPDATE,则将窗口的更新区域设置到该设备上下文作为系统裁剪的一部分;如果创建参数指定了DCX_VALIDATE,则清除窗口的更新区域。

GetWindowDC函数的实现为调用GetDCEx函数并传入DCX_WINDOW标志作为创建参数,GetDC函数的实现为调用GetDCEx函数并视窗口风格传入DCX_CLIPCHILDREN和/或DCX_CLIPSIBLINGS标志作为创建参数。示例代码如下:



HDC GetWindowDC(HWND hWnd) {
    return GetDCEx(hWnd, (HRGN)NULL, DCX_WINDOW);
}

HDC GetDC(HWND hWnd) {
    DWORD dwStyle = GetWindowLong(hWnd, GWL_STYLE);
    DWORD dwFlags = 0;
    if ((dwStyle & WS_CLIPCHILDREN) != 0)
        dwFlags |= DCX_CLIPCHILDREN;
    if ((dwStyle & WS_CLIPSIBLINGS) != 0)
        dwFlags |= DCX_CLIPSIBLINGS;
    return GetDCEx(hWnd, (HRGN)NULL, dwFlags);
}

BeginPaint函数也可以用来获取和窗口关联的设备上下文,与前述几个函数不同的是,作为Windows绘图流程的一部分,它通常只在WM_PAINT的消息响应函数中调用。除了像前述几个函数那样返回一个客户区设备上下文之外,它还要做一些和绘图流程有关的簿记工作。模拟层中,BeginPaint函数的流程算法如下:

  1. 验证传入的窗口句柄并取得窗口对象指针。
  2. 如果窗口有WF_NCPAINT标志,则清除该标志,并调用SendMessage函数给窗口发送WM_NCPAINT消息以绘制非客户区。这样就保证了在视觉上是非客户区先于客户区绘制。
  3. 获取客户区设备上下文。如果窗口有私有设备上下文,则使用它,并将窗口的更新区域设置到该设备上下文作为系统裁剪的一部分;否则,调用GetDCEx函数获取一个普通设备上下文,调用时创建参数为DCX_INTERSECTUPDATE | DCX_VALIDATE(使用窗口的更新区域裁剪设备上下文,并清除窗口的更新区域),并视窗口风格加上DCX_CLIPCHILDREN和/或DCX_CLIPSIBLINGS标志。不论使用何种方式得到设备上下文,此时窗口的更新区域都被清除。
  4. 如果窗口有插入符并且插入符处于显示状态,则需要调用HideCaret函数隐藏它,以防止被应用程序的绘图操作擦除。被隐藏的插入符将在应用程序调用EndPaint函数时显示。
  5. 如果窗口有WF_ERASE标志,则清除该标志,并调用SendMessage函数给窗口发送WM_ERASEBKGND消息以擦除客户区背景。
  6. 填充PAINTSTRUCT结构。hdc成员为前面创建的客户区设备上下文,fErase成员为WM_ERASEBKGND消息返回值的反值,rcPaint为客户区设备上下文的裁剪框(使用逻辑坐标而非设备坐标)。
  7. 最后,返回客户区设备上下文。

相对应的,EndPaint函数的流程算法如下:

  1. 验证传入的窗口句柄并取得窗口对象指针。
  2. 调用ReleaseDC函数释放BeginPaint函数里分配的窗口客户区设备上下文。
  3. 如果传入的设备上下文是窗口的私有设备上下文,则ReleaseDC调用没有效果。此时EndPaint函数必须将该私有设备上下文的更新区域裁剪去除(在BeginPaint函数里设置的),以保证随后应用程序可以在该设备上下文上正常绘图。
  4. 如果在BeginPaint函数中隐藏了窗口的插入符,则在返回前调用ShowCaret函数重新显示插入符。

4.5.2 窗口更新区域和重绘

传统上,Windows使用共享显存的重绘模型。所有的应用程序窗口共享同一显存,系统并不保留应用程序窗口已经绘制的内容。当应用程序窗口部分或全部暴露时(例如,原来遮挡该窗口的其他窗口被移走),Windows将产生WM_PAINT消息并通知应用程序该窗口需要重绘。应用程序在消息循环中响应WM_PAINT消息并绘制窗口暴露的部分。典型的产生WM_PAINT消息的原因如下:

  • 窗口显示、隐藏、移动或者大小发生变化,使得相应窗口需要重绘。
  • 应用程序使用InvalidateRect、InvalidateRgn或RedrawWindow函数强制窗口重绘。
  • 应用程序使用ScrollWindow等函数滚动显示区域,使得相应暴露区域需要重绘。

Windows实现重绘机制的核心是为每个窗口保存一份重绘信息,包括更新区域(又称无效区域)和重绘标志(例如重绘时是否需要擦除背景)。窗口的更新区域表示窗口需要重绘的最小区域。当一个窗口的更新区域不为空时,这个窗口将在合适的时机收到WM_PAINT消息。每当窗口有区域需要重绘时,系统就将该区域加入到窗口的更新区域中。当应用程序响应WM_PAINT消息并调用BeginPaint函数时,窗口的更新区域被置空,表示窗口不再需要重绘。应用程序亦可调用ValidateRect等函数显式清除更新区域。另外,调用GetUpdateRect或GetUpdateRgn函数可以获取窗口的更新区域坐标。

模拟层中,Window结构的hRgnUpdate成员存储了窗口的更新区域,而nFlags成员有WF_ERASE和WF_NCPAINT标志用于重绘。下面介绍模拟层中重绘相关函数的设计和实现。

RedrawWindow函数是重绘的核心入口。不仅应用程序会调用它,其他重绘函数的实现依赖它,而且模拟层内部也会调用它。例如,当模拟层接收到X 窗口系统发送来的Expose事件时,会调用RedrawWindow函数将暴露区域加入窗口的更新区域,ShowWindow在显示或隐藏窗口时也会调用RedrawWindow添加或清除更新区域。RedrawWindow函数的流程算法如下:

  1. 验证传入的窗口句柄并取得窗口对象指针。
  2. 如果重绘参数指定了RDW_INVALIDATE或RDW_VALIDATE标志,则将窗口的指定区域置为无效或有效(可能包含子窗口)。在进行这一操作时窗口必须是可见的。应用程序可以使用lprcUpdate或hrgnUpdate参数来指定区域,如果hrgnUpdate参数不为空则直接使用它;否则,如果lprcUpdate参数不为空则调用CreateRectRgnIndirect创建和lprcUpdate参数对应的区域,如果lprcUpdate参数为空则使用窗口客户区的矩形。模拟层中,将窗口指定区域设置为无效的子函数为InvalidateWindowCore,将窗口指定区域设置为有效的子函数为ValidateWindowCore。稍后将详细阐述它们。
  3. 如果重绘参数指定了RDW_UPDATENOW或RDW_ERASENOW标志,则立即更新窗口(可能包含子窗口)。模拟层中完成这一更新过程的子函数为UpdateWindowCore。稍后将详细阐述它。

将窗口指定区域设置为无效的子函数InvalidateWindowCore的声明如下:


BOOL InvalidateWindowCore(Window* pWnd, HRGN hRgn, UINT nFlags);

它的流程算法如下:

  1. 将本窗口的指定区域置为无效。调用CombineRgn函数并传入RGN_OR标志将指定区域加入窗口的更新区域hRgnUpdate,如果重绘参数指定了RDW_ERASE标志则为窗口加上WF_ERASE标志;如果重绘参数指定了RDW_FRAME标志并且指定区域和窗口非客户区相交,则还要为窗口加上WF_NCPAINT标志。
  2. 如果重绘参数没有指定RDW_NOCHILDREN标志,而且重绘参数指定了RDW_ ALLCHILDREN标志或窗口没有WS_CLIPCHILDREN风格,则需要递归调用InvalidateWindowCore函数将子窗口的相应区域设置为无效。

将窗口指定区域设置为有效的子函数ValidateWindowCore的声明如下:


BOOL ValidateWindowCore(Window* pWnd, HRGN hRgn, UINT nFlags);

它的流程算法如下:

  1. 将本窗口的指定区域置为有效。调用CombineRgn函数并传入RGN_DIFF标志将指定区域从窗口的更新区域hRgnUpdate清除,如果重绘参数指定了RDW_NOERASE标志则为窗口清除WF_ERASE标志,如果重绘参数指定了RDW_NOFRAME标志则为窗口清除WF_NCPAINT标志。
  2. 如果重绘参数没有指定RDW_NOCHILDREN标志,而且重绘参数指定了RDW_ ALLCHILDREN标志或窗口没有WS_CLIPCHILDREN风格,则需要递归调用ValidateWindowCore函数将子窗口的相应区域设置为有效。

更新窗口子函数UpdateWindowCore的声明如下:


BOOL UpdateWindowCore(Window* pWnd, UINT nFlags);

它的流程算法如下:

  1. 如果重绘参数未指定RDW_NOANCESTORS标志(模拟层内部标志),并且窗口是可见的,则从父窗口开始向上遍历窗口树,将窗口矩形从所有祖先窗口更新区域中去除。这样就保证了本窗口及子窗口更新后,祖先窗口的更新不会将它们覆盖。
  2. 更新本窗口。如果窗口有WF_NCPAINT标志,则清除该标志,并调用SendMessage函数给窗口发送WM_NCPAINT消息以绘制非客户区。然后如果重绘参数指定RDW_ ERASENOW标志,则只擦除窗口背景。系统会查看窗口是否有WF_ERASE标志,如果有,则清除该标志,调用GetDCEx函数获取窗口的设备上下文(使用窗口的更新区域裁剪),并调用SendMessage函数给窗口发送WM_ERASEBKGND消息以擦除客户区背景。如果重绘参数没有指定RDW_ERASENOW,则仅调用SendMessage函数给窗口发送WM_PAINT消息(WM_ERASEBKGND消息将在应用程序的WM_PAINT消息处理中调用BeginPaint函数时发送)。
  3. 如果重绘参数未指定RDW_NOCHILDREN标志,则需要递归更新子窗口。先取得窗口的子窗口列表,如果列表中的每一个子窗口是可见的,则对该子窗口调用更新窗口子函数UpdateWindowCore,调用时重绘参数加上RDW_NOANCESTORS标志。

InvalidateRect和InvalidateRgn函数的实现只是简单调用RedrawWindow函数并传入RDW_INVALIDATE标志作为重绘参数(如果指定擦除背景,重绘参数还要加上RDW_ ERASE标志)。InvalidateRect函数的示例代码如下:



BOOL InvalidateRect(HWND hWnd, const RECT* lpRect, BOOL bErase) {
    UINT nFlags = (RDW_INVALIDATE | (bErase ? RDW_ERASE : 0));
    return RedrawWindow(hWnd, lpRect, (HRGN)NULL, nFlags);
}

ValidateRect和ValidateRgn函数的实现只是简单调用RedrawWindow函数并传入RDW_VALIDATE标志作为重绘参数。ValidateRect函数的示例代码如下:



BOOL ValidateRect(HWND hWnd, const RECT* lpRect) {
    return RedrawWindow(hWnd, lpRect, (HRGN)NULL, RDW_VALIDATE);
}

UpdateWindow函数的实现只是简单调用RedrawWindow函数并传入RDW_ UPDATENOW标志作为重绘参数。示例代码如下:



BOOL UpdateWindow(HWND hWnd) {
    return RedrawWindow(hWnd, (LPCRECT)NULL, (HRGN)NULL, RDW_UPDATENOW);
}





回页首


4.6 小结

窗口子系统是继GDI子系统之后,模拟层中最基础的一个部分。本章介绍了在GDK/X提供的接口的基础上构造基本的窗口子系统结构的过程,并重点探讨了最重要的CreateWindowEx函数的实现。本章较少提到GDK/X的内容,主要的逻辑都围绕在窗口子系统内部各对象间的关系。因为模拟层只有很少的一小部分直接依赖于GDK和X,复杂的逻辑是在模拟层的内部。这也为模拟层代码的可维护性、可扩展性、可移植性提供了保障。



作者简介

肖习攀,清华大学计算机科学与技术系硕士。IBM中国开发中心高级软件工程师。从2003年4月加入IBM中国开发中心至今,一直从事旨在提高企业生产效率的应用软件开发。多个项目开发组长。经历涉及Windows、Linux和Macintosh等平台,兴趣包括图形用户界面移植和企业协作式应用软件开发。


阎小兵,在中国科学院计算技术研究所获得硕士。1999年加入IBM中国公司。现任IBM中国开发中心高级开发经理。曾参与IBM CICS移植,IBM WebSphere Commerce移植,IBM Productivity Tools开发与项目管理等。目前主要工作领域是下一代企业级工作场所协作软件的开发。对跨平台(Windows, Linux, Macintosh)软件开发和移植,以及大型软件开发过程管理和项目管理有着丰富的经验。


贾迎乐,北京邮电大学信息工程学院硕士。目前是IBM中国软件开发中心高级软件工程师,从事IBM产品在Linux平台上的开发和移植工作。在C/C++编程语言,Windows和Linux图形用户界面应用程序的开发和移植等方面有深入的研究。


龚奕平,清华大学计算机科学与技术系学士,多伦多大学计算机科学系硕士。IBM中国开发中心高级软件工程师。现主要从事IBM产品的研究和开发工作。研究兴趣包括Windows应用程序跨平台移植,GUI开发,以及网络设备开发等。曾在国内外期刊杂志上发表多篇学术和技术文章。




对本文的评价

太差! (1)
需提高 (2)
一般;尚可 (3)
好文章 (4)
真棒!(5)

建议?




回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款