窗口服务器(或WSERV )几乎与每一个Symbian操作系统的每一部分协同工作,从核心到应用,只有唯一真正的例外是通信子系统。它的两个主要职责屏幕管理和事件管理。 WSERV接收内核的事件,传递给它的客户(这通常是应用程序)。它接受来自客户端的命令并因此更新屏幕。我的关于这两个关键职责的部分将构成本章的骨干。
WSERV在系统启动时启动,并在系统的整个运行期间持续运行。这是一个标准的系统的服务器,是一个从类CServer2 (或CPolicyServer从Symbian操作系统第9版起)派生的所谓CwindowServer类
在这一章我将还包括动画的DLL (Anim DLL ),这是WSERV的一个插件。我将讨论什么是动画DLL,如何建立,以及anim DLL如何与事件交互。为了说明这一点,我将开发一个简单的手写识别系统。
当然我会覆盖窗户的深入话题-不同类型的Windows,窗口类的客户端和服务器方面,如何窗口对象联系在一起(窗口树),窗户的不同的区域,不同的窗口效果,以及如何客户绘制到
窗口。但首先我会考虑WSERV为核心的事件处理程序。
在系统的启动期间,WSERV调用函数UserSvr::CaptureEventHook(),告知内核WSERV想成为内核的事件处理句柄。要实际接收到事件,WSERV需要调用函数UserSvr::RequestEvent (TrawEventBuf& aBuf , TrequestStatus& aStatus),传递一个活动对象CrawEventReceiver的请求状态进去,然后每当等待的内核事件发生时,这个活动对象的Run就会被调用。通过这种方式,内核将它的所有事件(例如数字化仪和键盘事件)传递给WSERV,WSERV将进一步处理这些事件。
WSERV不仅仅是一个简单的传递事件给它的客户的管道。它对事件做了很多处理,抛弃一些事件,代理他人一些事件,创造新的事件和决定向哪些用户端传送事件。在内核与WSERV之间传递的事件类型的集合和WSERV与它的客户端之间传递的事件类型的集合并不完全相同。内核不是事件的唯一来源,客户也可以生成,anim DLL和WSERV本身同样可以生成。
就像可以向客户端传递事件,WSERV也可以将事件传递给anim DLL。
在本节中,我将列出WSERV处理的各种不同类型的事件,并对每种类型给予简要介绍。我将同时描述从内核到WSERV的事件类型和从WSERV到客户端的事件类型,图11月1日概述事件在系统内传递的路径。这显示出三个不同的主题:内核, WSERV和WSERV的客户-以及它们之间的用分隔线代表的界限。这三个小方块代表事件在内部WSERV本身内部的处理。
图11.1 WSERV事件流程
虽然图11.1没有提供完整的图片,它应足以让您感受事件如何通过WSERV。
从内核到WSERV的事件列在类TRawEvent::Ttype类中。其中的好几个用于处理指针事件。在下表中,我仅仅列出了指针事件的典型集合。
原生事件
事件 |
目的 |
ENone |
一个占位值,并无实际用途 |
EPointerMove |
一个指针或触笔移动了位置。这可能是一个移动或推动事件。 |
EPointerSwitchOn |
数字化仪按下,这导致设备开机 |
EKeyDown |
键盘上的一个键被按下 |
EkeyUp |
键盘上的一个键被释放 |
ERedraw |
模拟器接收到一个Win32重绘事件 |
ESwitchOn |
设备刚刚开启电源 |
EActive |
模拟器窗口获得焦点 |
EInactive |
模拟器窗口失去焦点 |
EUpdateModifiers |
修饰键设置已经改变,由模拟器重新获得焦点的时候发送。 |
EButton1Down |
触笔或鼠标键1被按下 |
EButton1Up |
触笔或鼠标键1被释放 |
ESwitchOff |
设备将要关闭电源 |
ECaseOpen |
翻盖类型的手机已经被翻开 |
ECaseClose |
翻盖类型的手机已经合上 |
|
|
我将在本章后面讨论按键事件和指针事件后发生了哪些事情。下表展示了WSERV如何响应系统中的所有其他原生事件。
WSERV事件
事件 |
响应 |
ERedraw |
在模拟器上,有一个由屏幕驱动程序管理的位图,其中包含了当前显示的完整拷贝。WSERV调用函数:CFbsScreenDevice::Update(const Tregion& aRegion),传入全屏幕的区域,来从位图更新屏幕。 |
ESwitchOn |
停止键盘重复定时器。如果有设置则建立密码输入窗口。调用UserSvr::WsSwitchOnScreen()打开屏幕硬件电源。给请求这个通知的客户端发送事件。 |
EInactive |
停止按键自动重复 |
EUpdateModifiers |
复位所有修饰键的状态 |
ESwitchOff |
如果客户端将自己注册接收“switch-off”事件,则WSERV发送一个事件给那个客户端。否则它通过调用UserHal::SwitchOff关闭设备电源。 |
EKeyRepeat |
无 |
ECaseOpen |
与ESwitchOn相同,不同的是它不会打开屏幕硬件的电源 |
ECaseClose |
发送一个事件给注册了接收这个事件的客户端来处理关闭电源(如果不存在这样的客户端则不会关闭电源) |
|
|
|
|
在接下来的两个表中,我展示了WSERV传送到其客户的事件的列表。我将在本章后面部分详细按键和指针事件。第一个表中包含了WSERV发送到相关客户的事件:
非客户端注册的消息
事件 |
意义 |
EEventNull |
必须忽略 |
EEventKey |
一个字符事件 |
EEventKeyUp |
一个按键弹起事件 |
EEventKeyDown |
一个按键按下事件 |
EEventPointer |
一个弹起、按下、拖到或移动指针事件 |
EEventPointerEnter |
指针移进一个窗口 |
EEventPointerExit |
指针从一个特定的窗口移开 |
EEventPointerBufferReady |
一个包含指针拖到和移动的缓冲区已经准备好发送 |
EEventDragDrop |
一个特别类型的指针事件。这些事件可以被客户端请求,以便他们能通过拖放接收UI部件。 |
EEventFocusLost |
窗口失去焦点 |
EEventFocusGained |
窗口得到焦点 |
EEventPassword |
当一个密码输入窗口显示出来时,发送给密码窗口的拥有者。 |
EEventMessageReady |
一个消息已经从另外一个应用程序到达 |
EEventMarkInvalid |
仅内部使用,不会发送给客户 |
EEventKeyRepeat |
不会发送给客户,发送给键点击生成器 |
|
|
下表我展示了WSERV仅发送给注册接收它们的客户的事件-大部分这些事件可以被发送给超过一个客户端。
客户端注册的事件
事件 |
意义 |
EEventModifiersChanged |
一个或多个修饰键已经改变了它们的状态 |
EEventSwitchOn |
机器刚刚开机(这个事件在手机上不会产生,但是在一个比如5系列的Psion PDA上会生成) |
EEventWindowGroupsChanged |
一个窗口组被销毁或重命名时发生 |
EEventErrorMessage |
出错时发送,比如一个发生在WSERV中的内存溢出错误 |
EEventSwitchOff |
发送给客户端处理关闭电源 |
EEventKeySwitchOff |
关机键按下时发送给客户端处理关闭电源事件 |
EEventScreenDeviceChanged |
屏幕尺寸模式改变时发送 |
EEventFocusGroupChanged |
焦点窗口组改变时被发送 |
EEventCaseOpened |
翻盖手机打开盖时发送 |
EEventCaseClosed |
翻盖手机关闭盖时发送给客户端处理关闭电源事件 |
EEventWindowGroupListChanged |
当窗口组顺序有改变时发送 |
WSERV处理事件有多个阶段;有些对所有事件都是通用的,特别是WSERV为所有的客户端事件进行排队。其他的阶段对各种类型的消息是特定的。比如,指针事件和按键事件都需要特别的处理。
WSERV进行的第一个阶段是处理指针事件-这是为了后续对接收到指针事件的主要处理执行标准处理。在这点上,WSERV执行各种动作:
l 为基于触笔的模拟器过滤出Win32移动事件
l 内核发送给WSERV的坐标是相对于最后位置的相对坐标,将它们转换为虚拟指针光标需要的绝对坐标
l 对实际的设备(非模拟器),WSERV将坐标根据当前屏幕的方向进行旋转。(屏幕旋转允许屏幕坐标地址旋转角度相对于屏幕物理坐标设置为0,90,180,270度)。
l 将坐标根据当前的屏幕原点和放缩比例进行偏移
l 如果指针事件被限制在显示的一个子矩形中,WSERV将在坐标越过此矩形区域时限制它的坐标。
旋转在模拟器上不需要考虑并没有什么问题,由于位图已经旋转,因此,内核从Win32接收到的坐标已经旋转。
从Symbian OS 8.1开始,我们支持屏幕原点和放缩功能。通过使用屏幕原点和缩放,UI设计者可以容许不同的应用程序使用不同的坐标集解决屏幕像素。
在有些情况下,WSERV会关闭它的心跳定时器-这通常发生在客户端调用RwsSession::PrepareForSwitchOff()时,以便处理器能减低能耗以便节约电池电量。为了对来自内核的每个事件作出反应,WSERV将重新打开这个定时器(如果它已经关闭)。
接下来,WSERV将事件传递给任何将自己注册对事件感兴趣的anim DLL。Anim DLL通过调用函数MAnimGeneralFunctions::GetRawEvents(Etrue)。Anim DLL可以消费事件,这样WSERV就不会进行进一步的处理,要做到这一点,只需要从函数OfferRawEvent中返回ETrue,否则应返回EFalse。
从这里开始, WSERV用不同的方式对待不同类型的事件。在上面的WSERV事件表中,展示了所有除按键事件和指针事件外的所有事件。
有三个内核事件直接与按键相关:EKeyDown,EKeyUp和EUpdateModifiers。要处理这些按键事件,WSERV使用一个从CKeyTranslator派生的对象实例。这个对象理解字符映射和修饰键,它的主要用途是告诉WSERV一个特定的键按下时应该映射到哪个字符。比如,按下“a”键可能导致“a”或“A”,CKeyTranslator将分析shift键的状态并决定映射哪一个。
事件EUpdateModifiers被直接传递给CKeyTranslator对象。当模拟器窗口从另外的Windows应用程序获得焦点是,内核为模拟器生成这个事件。跟随事件传递的数据告诉我们所有修饰键的状态,并让模拟器得到当模拟器失去焦点时其他应用程序对修饰键的任何改变。
WSERV处理每一个按键的按下与弹起,因此:
l 如果日志打开,它会记录日志
l 它将把事件告诉键盘重复定时器对象
l 它将把事件传递给键盘翻译对象
l 它会检查任何修饰键变化
l 排队按键按下/弹起事件
l 执行进一步的处理创建字符事件(如果需要的话)
键盘重复定时器对象控制按键的自动重复。WSERV从内核仅接收到一次按下或弹起事件,无论按键被按下的时间有多长。如果一个按键按下映射到一个字符,WSERV启动一个定时器,定时器每流逝一个定时事件,WSERV就为客户端队列生成一个字符实例。如果客户端负责回显这个事件,则它将得到很多这个字符事件,定时器将生成除了第一个事件之外的所有事件。
当一个新的按键按下事件发生时,WSERV将通知定时器,这样它就会取消当前的重复-这是必要的,因为任何按键按下都可能改变当前定时器生成的字符。同样当发生按键弹起事件时,WSERV也会通知定时器,这样它就会停止那些来自于重复的按键弹起事件的字符事件。
随后,WSERV调用键盘翻译对象,使用下面的函数:
TBool TranslateKey(TUint aScanCode, TBool aKeyUp,
const CCaptureKeys &aCaptureKeys, TKeyData &aKeyData)
正如你看到的,WSERV传入键盘事件的扫描码,一个布尔值说明按键是按下还是弹起,以及当前按键捕获的列表。按键翻译对象返回一个TBool类型的值说明此按键是否映射到一个字符,按键翻译器也在TKeyData对象中返回如下的细节:
l 字符的代码
l 当前所有修饰键的状态
l 按键是否已经被捕获
如果按键被捕获,按键翻译器也返回:
l 一个句柄标识哪个窗口捕获这个对象
l 另外一个句柄WSERV用于它自己的捕获按键。
WSERV捕获的按键或热键是系统范围的。有些案件用于增加或减少对比度,切换或开关背光以及其他更多-你可以在枚举类型THotKey中找到完整的列表。
客户可以请求需要的事件,让他们知道当某些修饰键改变自己的状态时得到这个事件。在这种情况下, WSERV检查所有的客户端请求看看是否有要求的特殊信息变化是否已经发生。对于每一个这样的请求, WSERV将相关事件添加到有关客户端的事件队列中。
WSERV必须决定向哪个客户端发送按下或弹起按键的事件。通常选择当前拥有窗口焦点的客户端窗口-唯一的例外是,如果某一特定的客户要求捕获按下和弹起的某一个特定的按键。 WSERV还会给key click插件发送这个消息,这样它们就能对这个事件作出响应。
现在WSERV处理这些按键按下或弹起事件,翻译对象决定发起字符事件。这项处理牵涉到很多方面,我将在下一节描述。
WSERV执行字符事件处理的主要步骤包括:
l 调用按键点击插件(key click plug-in)
l 处理按键捕获(包括WSERV捕获的按键)
l 决定谁需要接收事件
l 检查事件是否有一个长程捕获(long capture)
l 检查重复定时是否需要启动
l 排队事件
首先, WSERV发送事件给按键单击(key click)插件,如果有的话。
按键翻译对象已经返回标志说按键事件是否应被捕获,所以WSERV检查这一点,并据此确定了事件目的地。如果按键未被抓获,则WSERV将事件发给当前的焦点窗口。如果事件被捕获,但WSERV目前显示的是密码窗口,则WSERV只发送事件给与密码窗口的窗口组相同的窗口组。
如果字符事件是一个WSERV的捕获键,则WSERV自己已经抓获。在这种情况下, WSERV将处理立即事件而不会发送到客户端。
如果按键是如下情况中的一种:
l 一个长捕获按键,或
l 当前没有重复的按键,并且此按键被容许自动可重复。
则重复定时器将启动。
长捕获是我们在Symbian OS v7.0中增加进来的一个特性。它容许一个长按得键可以被当做多次不同的快速敲击。The long-key-press event and the short-key-press event yielded by a single physical key press can differ both in their destination client and the actual character code generated. This feature allows a quick press of a number key on a phone to enter a number into the ‘‘dial number’’ dialogue, while a longer press of the same key could launch the contacts application and jump to first contact starting with a certain letter.
指针事件的处理比按键事件的处理更复杂。这是因为WSERV需要根据点击的确切位置来计算给哪个窗口发送这个事件。另外,指针抢夺和捕获也会影响到它。
通常的指针事件序列始于指针按下事件,随后是零个或多个指针拖动事件,并结束于指针弹起事件。所有这些事件到达屏幕上可见窗口的位置是可能的。然而,有有两个特性,客户可以使用它来更改此行为:抢夺和捕获。
如果一个窗口接收到一个按下事件后设置为抢夺此指针事件,则随后WSERV会将所有随后的事件都发送到这个窗口,即使它们实际上发生在另外的窗口上。如果一个窗口正在捕捉,并且按下事件发生在背后的一个窗口,那么正在进行捕获的窗口将收到这个事件。如果该窗口还执行抢夺,则还将收到后续的拖放事件和弹起事件。
在实践中,大多数窗口将抓住指针事件,还有一些窗口还将抓住他们。捕获允许对话框防止指针事件被发送到背后的窗口去。
WSERV在处理指针事件期间执行下面的主要步骤:
l 计算指针事件发生的实际的窗口
l 确定是否另一个窗口的抢夺或捕捉意味着需要获得该指针事件
l 如果当前窗口改变,排队进入和退出事件
l 通知key click插件关于指针事件的信息
l 对指针移动和拖动事件,检查窗口看它是否不需要这个事件
l 如果窗口已经请求这个事件,则将移动和和拖动事件存储在一个指针缓冲区中。
l 如果窗口有一个虚拟键盘,并且事件发生在其中一个虚拟按键上,将事件转换为一个按键事件
l 检查事件看它是否是一个双击事件,并检查是否是一个拖放事件。
WSERV通过递归方法分析窗口树来计算指针事件发生的实际窗口。从客户端窗口的顶层开始,查找用户点击位置的最前端的窗口,然后它继续检查这个窗口的每一个子窗口,直到没有找到新的子窗口或没有子窗口包含这个点。然后WSERV用同样的方式再次分析窗口,但是这次它检查每个窗口的捕获标志来看它是否捕获事件。
当WSERV增加了一个移动或拖曳事件到客户端事件队列,它会检查当前客户端事件队列的最后一个事件,如果这是一个除了坐标外其他信息都相同的事件,则WSERV仅仅将旧的事件用新的事件代替。这意味着,客户端不会得到非常细粒度触笔或鼠标移动的信息。对于大多数应用来说,这没有问题,实际上它是有益的,但对绘图应用并不理想。因此,对于这类应用, WSERV可以将从内核得到的所有事件保存在一个缓冲区中,客户端可以一次性获取到一整块这种数据。
WSERV使用客户端队列存储正等待传送到用户端的事件。每个客户端有三个不同的队列; 每个存储不同类型的事件:
l 重绘事件
l 优先键事件
l 所有其他的事件(主队列)
我们最初设计优先键队列的目的是支持OPL编程语言,并且这是目前使用它们的唯一应用程序。当一个OPL程序运行时,用户可以按下Ctrl+Esc键,这样会立即终止程序。这是因为这个键是通过优先键队列传送的,因此它可以越过所有其他的事件。
更笼统地说,我们有三个队列,使客户可以用不同的活动对象优先级处理不同的事件。一般情况下,客户端希望在已经排队的重绘事件前得到指针和按键事件,因此它将重绘事件队列的活动对象的优先级设置为低于主事件队列。
当WSERV有一个客户的事件,它就把事件放置到队列中并完成客户端提供给这个队列的请求状态,这样客户端知道,至少有一个事件在等待处理。但是,系统可能会很忙,可能会产生许多WSERV事件之前,客户还没有机会来处理事件。这意味着, WSERV必须处理队列溢出的问题。
我们设计这个队列是用一个单独的按键指示应用程序关闭;这意味着在这个队列中,我们对这个按键的多次按下并无兴趣。因此,队列只持有最近一次按键的状态,但有关的特殊地位-如果一个新的事件到来之前,旧的尚未交付,那么旧的事件将被覆盖。
重绘队列是一个数组,列出了所有目前需要重绘的客户端的窗口。数组按照由前向后的顺序排序,这样首先被重绘的窗口是最重要的一个。如果有足够的内存,现有的数组可以一直扩大下去-但每个窗口只能出现在这一次。
如果在任何时候数组不能扩展,则重绘队列设置一个标志说,数组尚未完成。当数组变成空并且标志被设置,WSERV扫描所有客户端的窗口找到一个需要重绘的。只有当它扫描了所有的窗口才会清除这个标志位。
WSERV采用了很多战术来避免或减少这个队列溢出的影响。然而,他们并非万无一失-它可能发生,在很极端的情况下,一个事件可能会丢失。但是,据我们所知,在实践中,这并没有表现出任何副作用!
该事件队列是一个全局的堆单元-整个系统只有一个事件队列。WSERV根据客户端的数量增加和减少这种单元的数量。队列的大小约48 +2 * (的客户数量)个条目,每个条目是40字节,这正是TWsEvent的大小。
堆单元分为段,每个客户分配一段。 WSERV也有自由改变大小的每个客户端段的大小,这会相应的增加或减少了其他客户的段的大小。一个特定的客户的段可以有2至32项。
如果WSERV需要排队一个事件,并且在这个客户端的分段中没有空间,则很明显,它将首先努力扩大客户的分段到其最大尺寸32项。(如果客户端的一节已经有32个条目,然后WSERV将试图清理该客户的队列-换句话说,它试图找到一个事件,这个事件可以被删除。)为此, WSERV首次尝试寻找其他客户,看它们是否有空间可以缩小它们的分段。如果失败,则WSERV将试图从其他客户的队列清理事件。当前焦点的客户端是最后被选择此作业- WSERV只会在当队列中没有任何其他队列的事件可以清除时才会清除当前焦点的客户端。如果清除失败的话事件将被丢弃。
为了从客户端队列清除事件, WSERV会尝试各种各样的战术,其中包括:
l 删去相关的一对弹起和按下指针事件。(如果弹起事件的尚未收到,则它将删除按下事件,并删除最近一次的匹配的弹起事件)
l 从非焦点客户端队列中删除键弹起或按下事件
l 从当前焦点的客户端队列删去配对的键按下和键弹起的事件(大多数应用程序忽略这些事件)。
l 合并两个修饰键变化事件,并删去其中之一
l 删除配对的失去焦点和获得焦点的事件
l 删除重复的Switch On事件
l 删除这些事件:按键事件,指针进入和退出的事件,拖放事件,指针缓冲区准备好事件和下面的指针事件:拖放,移动,按键重复和开关。
在这一节,我将开发一个简单的手写动画DLL。我不会尝试实现一个实际的字符识别,但是我将展示所有的周边框架,包括获得指针事件,在屏幕上绘制墨水并发送一个字符事件给应用程序。我的用意是解释的anim DLL ,特别是显示他们如何能够处理事件。
我们最初在Symbian操作系统第5版中为时钟设计了anim DLL。这时,他们提供了两个主要特点:
l 准确的时间信息。如果使用相对简单的CTimer ,比如,也能提供一个时钟,但是它的更新将慢于实际的时间。
l 用户代码可以执行与WSERV一样的线程绘制窗口的能力,从而避免造成的延误的IPC
窗口是Symbian操作系统用来控制存取屏幕的机制。它们默认是长方形的,并且可以互相重叠。他们有一个前向后顺序,这定义两个窗口哪个在前面覆盖另一个。应用程序可以创建和销毁窗口。窗口可以独立寻址,而且一个应用程序每次只能绘制到它的其中一个窗口上。典型的应用程序将有许多窗口。
窗口是重要的,因为它们允许不同的应用程序在同一时间可以绘制到屏幕的不同位置。此外,应用程序不必担心自己能被容许绘制到屏幕的哪一部分。应用程序只管将自己绘制到它的窗口上,且仅当该窗口是可见的它才会被显示在屏幕上。
以下各节中,我将涉及窗口树,WSERV浏览这个结构的方式,窗口类及其结构,窗口的属性,绘制窗口等更多内容。我也将涵盖直接屏幕访问( DSA) ,这也可以被描述为没有窗口的绘图。
图11.3显示了不同的窗口之间的关系。它是作为一棵倒立的树。这显示出在树的不同点上可能出现的不同的窗口类型。
图11.3展示了四种类型的窗口(虽然其中之一,组窗口,从来不会显示,根据前面对窗口的定义,因此不是一个窗口)。此图是一个对象图中的每一行只能包含一个特定类型的对象。不同类型的窗口如下所示:
l 根窗口。 WSERV创建这个窗口,它不能直接被任何客户端存取。它是用来作为窗口的起点结构,存在于系统的整个生命周期。在多屏幕设备中,每个屏幕都有一个根窗口
l 组窗口。这些窗口只能是根窗口的直接子节点。 WSERV的客户创造了他们,但他们没有任何相关的屏幕区域。它们提供给客户端一种方式,将客户端的一些窗口分组组织起来,以便它能够共同移动。
l 顶端客户端窗口。图中的第三排只有顶端客户端窗口。这些窗口可显示,因此用户端会至少需要其中的一个
l 客户端窗口。由图的所有后续行组成客户端窗口。客户可以没有客户端窗口,也可以有多层次的嵌套的客户端窗口。
图11.3
下表显示了代表这些窗口类型类,包括客户端和服务器端:
窗口类型 |
客户端类 |
服务器端类 |
根窗口 |
无 |
CWsRootWindow |
组窗口 |
RWindowGroup |
CWsGroupWindow |
顶级客户窗口 |
RWindowBase类的子类 |
CWsTopClientWindow |
客户窗口 |
RWindowBase类的子类 |
CWsClientWindow |
图11.3中包含了三种不同类型的箭头。这些代表指向对象,给出了窗口树的结构:
l 父指针。这些箭头向上指。所有窗户都有一个指向上面一行的一个父指针。(因为根窗口没有父对象,其指针将是NULL)。
l 子指针。这是指向左下方的对角箭头。这里的展示的是如果一个窗口有任何子窗口,则将有一个指针指向通常是最年长的子窗口。(默认情况下指针将指建立的第一个子窗口,但当然的子窗口的顺序可以改变,因此,这并非总是如此)。
l 兄弟指针。这些是那些指向右边的箭头。他们表明每个窗口都知道下一个与自己有相同父窗口的最年长的窗口。兄弟窗口将与自己有相同父窗口的子窗口组织成一个单向链表。(至于孩子指针,按照窗口被创建的顺序按最年长的到最年轻的顺序排列,但随后也有可能被改变。)
这些指针被定义在服务器端的类CWsWindowBase类中,这是所有服务端窗口类的基类,后面我会展示这点。你可以使用指针通过各种方式遍历窗口中的对象。例如,要获得最古老的父母兄弟姐妹,你可以使用iParent->iChild导航。
我们来看看一个更为复杂的使用这些指针的例子。以下功能当从一个窗口树中删除一个窗口是更新了这些指针:
当一个窗口被从窗口树中删除时,只有一个指针需要被更新。如果窗口是它的父窗口最年长的子窗口,则父窗口的子窗口指针将被更新。如果不是,则比它更年长的iSibling的指针需要被更新。在这两种情况下,指针都需要被更新为下一个最年轻的兄弟窗口。
有许多场合窗口树必须从前至后的穿越,例如:
l 当一个窗口被设置为不可见的窗口,这时,被这个窗口覆盖的窗口需要被暴露出来。
l
l 当一个窗口被设置为可见的,则本窗口背面的窗口将被覆盖。
有一个单一的算法做遍历,这个算法用在WSERV的许多地方,我将在这一节讨论它。
有两个规则,确定了窗口显示在屏幕上的前后顺序:
l 子窗口总是在父窗口的前面
l
l 如果两个窗口是同一个父窗口的子窗口,则年长的窗口位于年轻的窗口的前面。
第一条规则的结果,根窗口始终是最后面的窗口。根窗口的其中一项工作是在没有其他窗口可见时清除显示区域。它用于清除窗口的颜色可使用函数RWsSession:: SetBackgroundColor ()来设置。
遍历窗口树的机制被实现为一个单一的函数,CWindowBase::WalkWindowTree(),它使三个指针来以正确的顺序链接遍历所有的窗口。WalkWindowTree()以一个带虚函数的类作为参数,并在每次找到一个新的窗口时,调用这个虚函数,并将窗口作为一个参数传入。
这个函数的其中一个参数是TWalkWindowTreeBase& aWalkClass。图11.4展示了这个类的一些派生类和它的一个虚函数-DoIt()。
图11.4 在窗口树中行走
总之,有超过20个类基于各种不同的目的从TWalkWindowTree派生。一些操作只适用于可见的窗口,因此需要TWalkWindowTreeRegionBase。这个类的一个成员是一个区域,它被初始化为屏幕上的整个区域。当操作被应用到每个窗口时,这个窗口的可见部分被从这个区域中减去,当该区域成为空白时就不需要再继续扫描窗口。
图11.5展示了我们用于表示窗口的所有类,其中既包括服务器端也包括客户端。你可以看到两边的类结构并不是完全相同的。RWindowTreeNode和CWindowBase的目的都代表了一个在任何时候填充到窗口树中的一个节点。
图11.4 窗口的类结构
等价的windowgroup类是显而易见的。然而,其他部分的类结构情况不同。在客户端,类结构由每个类提供的绘图功能确定。在服务器端的类结构与窗口在窗口树中的地位是密切相关的。绘制行为的差异被一个插件对象决定,这是一个派生于CWsWindowRedraw的对象。
当我们从绘图的观点来考虑窗口时,有三种类型的窗口:空白窗口,备份位图窗口重绘窗口。在本章的后面部分,我将确切描述这几类窗口之间的差异。
根窗口,是CWsWindow的一个派生类对象,也有一个绘图对象插件。它的类型是“空白窗口”,因为它只是绘制一个纯色的背景,这在图中用虚线表现。