代码改变世界

一站式WPF--Window(一)

2009-11-30 14:18 by 周永恒, ... 阅读, ... 评论, 收藏, 编辑

用户通过Window与 Windows Presentation Foundation (WPF) 独立应用程序进行交互。Window的主要用途是承载可视化数据并使用户可以与数据进行交互的内容。独立 WPF 应用程序使用 Window 类来提供它们自己的窗口。

  这段是MSDN上对Window的描述,虽然翻译的不是那么地道,也可以看出Window的两大功能:一,承载可视化数据。二,使用户可以与可视化数据进行交互。

  在正式研究Window的功能之前,先来看一下,什么是Window?

什么是Window

  Window是Windows操作系统的核心,从表现上来说,Windows就是由许许多多的Window组成的,那么具体什么叫Window呢?

  通常意义上讲,我们所谓的Window是最外面的Window,也就是有着关闭、最小化的主Window。在Window编程中,调用CreateWindow来创建Window,通过设置dwStyle来指定样式,比如设置LBS_OWNERDRAWFIXED可以创建ListBox,设置BS_PUSHBUTTON可以创建Button等。CreateWindow的返回值就是窗口的句柄,从这个意义上来讲,在Win32世界中,万物皆Window,只是表现形式不同,那么WPF的Window对应什么呢?

WPF的Window

  WPF中的Window继承于ContentControl,内部可以承载一个Content,当然,借助于ItemsControl或Panel,Content也可以向下添加多个对象。这些对象都是WPF中的对象,也就是要承载的可视化数据。那么用户与可视化数据间的交互是怎么完成的呢?

  无论使用GDI绘制,或者使用DirectX绘制,在操作系统来看,Window都是一块持有句柄的有效区域。所有对该区域的操作,都会通过句柄来发送到Window对应的消息处理函数。也就是说,对外来看,WPF的Window依然是传统Win32的Window,对内它又把消息转化为Routed Event或者Command等来处理。关于这层处理和消息转化,要深入WPF的Window来谈起。

深入WPF的Window

  作为外界和可视化数据之间的桥梁,Window具有对内和对外两层作用。先说对内,Window内部可能会存在Button,ListBox等等控件,这些控件组成了一个对象树。树的子节点可能很多,但顶点只有一个,这个对象树是WPF的核心,Routed Event和Routed Command等都是依附于它的。抛开具体的对象树不说,我们要关注的是它的这种“众”字型的结构。如果你把这颗可视化数据组成的对象树想象成一个人的话,那么它的顶点就是它的头,我们对手臂和腿的操作只要对头喊话就可以了。换言之,对于WPF的Window

,它对内最关心的就是找到对象树的头(RootVisual),然后通过头把操作传递下去。

  从对外来看,操作系统关注的是注册Window的风格以及Rect。比如鼠标按键被按下时,按键消息被发送到系统的消息队列中,系统通过扫描所有注册窗口的Rect判断按键发生在哪个窗口中,再在适当的时机把按键消息从系统消息队列转移到创建窗口线程的消息队列中等待窗口处理。对于WPF的Window来说,同步这个Rect很重要,Window的UI是WPF的,但内部有个隐藏的使用CreateWindow创建的Win32-Window,当用户设置win.Width=60方法时要同步内部Window的Rect,反过来接收到WM_SIZE时也需要调用RootVisual去执行WPF的Measure、Arrange流程。

  用一个草图来表示Window的消息处理过程:

 

  1. 系统将消息发给隐藏的Win32-Window,在Dispatcher中GetMessage并分发到对应的窗口过程处理函数WndProc。
  2. WndProc里应是一个大的Switch-Case,用以处理不同的Window消息。按照消息的类别,WPF提供了不同的Manager来管理,这里的Manger并不是直接处理Window消息,并且并不是所有消息都经过WndProc再转到Manager的。
    比如说WM_KEYDOWN,Dispatcher调用GetMessage获得消息后, 调用了ComponentDispatcher的RaiseThreadMessage方法(关于ComponentDispatcher,可以参阅Nick的文章),最终由KeyboardDevice产生Keyboard.PreviewKeyDownEvent这个路由事件(Routed Event)。
  3. 仍然以WM_KEYDOWN来说, InputManager找到这个Input发生的区域--Window,调用Window的RaiseEvent方法唤起Keyboard.PreviewKeyDownEvent这个路由事件。
  4. 路由事件沿着对象树开始向下传递,方向是一去一回,由PreviewKeyDown到KeyDown。在这个传递过程中,相应的路由事件也被唤起,比如说如果此时焦点在Button上,当传递到Button时还会唤起Button的ClickEvent事件等。

  这些Manager,其中像ContentLayoutManager,本身是Internal的,仅仅是在Measure和Arrange的内部使用,这里只是表示消息经由分类后最终由这些Manager来管理。这个过程比较有意思的是Input,简单的来谈一谈它。

Input

  路由事件是WPF处理Input的核心,简略的说就是有一去一回从PreivewKeyDown到KeyDown这个过程,PreviewKeyDown的方向是从父到子,KeyDown的方向是从子到父。这个处理的过程不是本篇文章要谈的,重点是如何把一个简单的WM_KEYDOWN消息转化为PreivewKeyDown和KeyDown这两个路由事件。

  从图中可以看出,InputManager负责处理Input,一个Input,可能来自不同的设备--Mouse,Keyboard等等。InputManager要关注的地方有二:一,这个Input会转化成什么路由事件。二,这个Input作用在哪个UIElement上。第一个转化是由InputDevice来做的,这个InputDevice,具体有MouseDevice、KeyboardDevice等等。它会根据Window消息来生成对应的路由事件,然后把这些信息报告给InputManager。InputManager再根据这些信息找到作用的UIElement,然后唤起路由事件。

  说过了Input,重点来看Presentation,所谓Windows Presentation Foundation,显示一定是它的重点。

Presentation

  在前面中,介绍到了需要被显示的可视化数据,在WPF中是以对象树(确切说是Visual Tree)来组织的。那么它又是如何被画出来的呢?从对象树到真正Render之间又发生了什么呢?

 

  图例是WPF的架构图,其中重要的两个是PresentationCore和MilCore。在PresentationCore中,定义了Visual类,这个是WPF显示的核心,所有可以被显示的对象都直接或间接继承自Visual。当然,这里的Visual Tree就指Visual组成的树。Milcore(MIL -- Media Integration Layer),非托管代码,负责WPF和DirectX之间的通信,它主要由两部分组成:一,Composition Engine。二,Render Engine。前者负责创建Composition Tree,后者负责把Composition Tree转换成DirectX可以识别的Triangle并通知DirectX进行Render。

  简单说一下Render的流程:

  1. Visual被添加到Visual Tree上。
  2. Visual Tree和Composite Engine通过Message Transport来进行通信,Message Transport包括Transport和Channel两部分。Transport定义了传输的细节,Channel作用在Transport上,用来建立一个双向的通信管道。这里,当Visual Tree被修改后,把被修改的Viusal数据通过Channel发送给Composition Engine。
  3. Composition Engine接收到Visual数据后,创建对应的Composition Node,并加入到Composition Tree中去。
  4. Composition Engine通知Render Engine开始绘制,Composition Tree中的节点是Rectangle,Ellipse等,DirectX不能识别这些数据,Render Engine要把这些数据转化为DirectX可以识别的三角形,这个过程叫做Tessellate。
  5. Render Engine通知DirectX开始绘制(Render),DirectX在经过驱动(WDDM或者XPDM)通知显卡开始绘制像素到屏幕。

  在第一篇文章中,介绍了WPF的线程模型,WPF中线程一分为二,有UI线程和Render线程。UI线程是托管代码,管理Visual  Tree,用于处理输入,事件等。Render线程是非托管代码,在MIL中,仅用于绘制,把从UI线程传入的Visual数据转化并添加到Composition Tree进行绘制。在这个过程中,Render线程是被动的,它等待着UI线程向它传输数据并下达命令,也会把操作的结果(绘制完成,错误)等通过Channel报告给UI线程。

  这里要说说Viusal数据,也就是如何把Visual转化为Composition Node,在Avalon世界中,UCE(Unified Composition Engine)负责处理这层转化。当然,对UCE来说,它是不能识别WPF对象的,这种不能识别,就是说直接拿一个WPF的Line,它是不知道如何转化为相应Composition Node的,必须要WPF对象进行自描述,告诉UCE它对应什么Composition类型。UCE提供了IResource接口,这个接口定义了可以通过Channel传递到UCE的一系列方法。WPF的Visual实现了这一接口,Visual子类重写了其中的AddRefOnChannel方法并注明了其对应的Composition类型,比如说LineGeometry设置了它的类型是DUCE.Resource.Type_LINEGEOMETRY。UCE通过这些信息,就可以把传递过来的Visual数据转化为相应的Composition Node了。

  这里说到了UCE,每个WPF进程都有自己的UCE,并且在Avalon(Window Vista/Window 7)中,负责绘制桌面的DWM(Desktop Window Manager)也有它的UCE(也叫DUCE)。为了提供透明效果,桌面上的显示需要进行混合,DWM也是使用Composition Tree来管理窗口的,用两幅图来描述一下UCE的处理过程:

 

 

  最终,DWM经过混合后得到了桌面最后的透明效果。

  当然,整个过程不必细究,在WPF编程中也很少需要从UCE这个角度来考虑问题,只是帮助朋友们捋清一下思路,更好的理解WPF。讲过了这些底层的处理,把思路回归到Window上来,来看看Window是如何对这些进行整合的。

Inside Window

  前面提到,Window内部有一个隐藏的Win32-Window,用于接收消息,在WPF中,使用HwndSource来封装这个隐藏Window。那么从Visual Tree到Window之间又发生了什么呢?

  从Visual Tree来看,像提线木偶一样,控制它的头(顶点)就可以随意玩弄它。WPF提供了CompositionTarget以及PresentationSource来完成这些内部的处理,关于具体的流程,那么,就下篇吧。 ^_^

 

 

作者:周永恒 
出处:http://www.cnblogs.com/Zhouyongh  

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。