从WINDOS运行原理谈起学习WPF的几个要领

从WINDOS运行原理谈起学习WPF的几个要领

用户通过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对应什么呢?

  • 以消息为基础,以事件驱动之(Message based, event drive)
  • Windows程序的进行系依靠外部发生的事件来驱动,即程序不断等待(用一个While循环)等待任何可能的输入,然后做判断,然后再做适当处理。“输入”是由操作系统捕捉到之后,以消息形式(一种数据结构)进入程序之中

  • “输入”的分类:硬件所产生的消息(鼠标移动、键盘按下)放在系统队列(system queue)中,以及由Windows系统或其它Windows程序传送过来的消息,放在程序队列(application queue)中,以程序的角度看消息来自哪里并没有太大区别,反正程序调用一个GetMessage() API就取得一个消息,程序的生命靠它来推动,所有的GUI系统,包括UNIX的X Windows以及OS/2的Presentation Manger都像这样,是以消息为基础的事件驱动系统**。可想而知,每个Windows程序都应该有一个如下的循环:

MSG msg

while(GetMessage(&msg, NULL, NULL, NULL){

TranslateMessage(&msg);

DispatchMessage(&msg);

}

消息,MSG结构,其实是Windows内设的一种数据格式

/*Queued message structure*/

typedef struct tagMSG

{

    HWND    hwnd;

    UINT        message; //WM_XXX,,例如WM_MOUSEMOVE,WM_SIZE......

    WPARAM    wParam;

    LPARAM    lParam;

    DWORD    time;

    POINT    pt;

}


WPF的Window

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

无论使用GDI绘制,或者使用DirectX绘制,在操作系统来看,Window都是一块持有句柄的有效区域。

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

  • 接受并处理消息的主角就是窗口。
  • 每一个窗口都应该有一个函数负责处理消息,程序员必须负责设计这个所谓的“窗口函数”(windows procedure或称windows function),如果窗口获得一个消息,则这个窗口函数必须判断消息队列的类别,决定处理方式。

0. Windows程序开发流程:

Windows程序包含“程序代码”和“UI(User Interface用户接口)资源”两大部分,两部分最后以RC编译器整合为一个完整的EXE文件,所谓UI资源是功能菜单、对话框外貌、程序图标、光标形状等等。这些UI资源的实际内容(二进制代码)是借助各种工具产生的,并以各种扩展名的文件存在,如.ico, .bmp, .cur等等。程序员必须在一个所谓的资源描述文档(.rc)中描述它们,RC编译器(RC.exe)读取RC文件的描述后将所有UI资源文件集中作出一个.RES文件,再与程序代码结合在一起,这才是一个完整的Windows可执行文件。

在这里插入图片描述
需要什么函数库(.lib)

  • 并不只是.dll才是动态链接库(dynamic link library),事实上.exe、.dll、.fon、.mod、.drv、.ocx都是动态链接库

  • Windows程序调用的程序可以分为C Runtimes ,以及Windows API两大部分

  • 动态链接库是在执行时期才发生“链接”,但在生成.exe文件时期,链接器(Linker)仍然需要先为调用者准备一些适当信息,才能在执行时期顺利“跳”到DLL中执行,这些信息放在“import”函数库中,如GDI32.lib, User32.lib, Kernel32.lib, Comdlg32.lib, TH32.lib

需要什么头文件(.H)

  • 所有Windows程序都必须载入windows.h文件,如果还用到其它的系统dll,就得载入对应的头文件,如Commdlg.h, Mapi.h, Tapi.h等

1.一个具体的Windows程序

在这里插入图片描述

2. 程序进入点WinMain

  • main是一般C程序的进入点
int main(int argc, char *argv[], char *envp[])
  • WinMain则是Windows程序的进入点
int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hprevInstance, LPSTR lpCmdLine, int nCmdShow)
  • 当Windows的shell侦测到用户要执行一个程序(如双击),于是用加载器把此程序加载,然后调用C startup code,C startup code再调用WinMain,开始执行程序,WinMain的四个参数由操作系统传递进来

3. 窗口类之注册与窗口之诞生

在这里插入图片描述

  • 窗口的注册和产生由两个API完成,但是其属性必须先设定好,包括窗口的“外貌”和“行为”,如边框、颜色、标题、位置是窗口的外貌,而窗口接收消息后的反应就是其行为,具体地说就是指窗口函数本身。API RegisterClass需要一个大型数据结构WNDCLASS作为参数,API CreateWindow则另需要11个参数

  • 上述两个函数的名称设计有特别含义,注册窗口放在InitApplication()中表示只有第一个实例才会进入,至于这一进程是否是某个程序的第一个实例,可以由WinMain的参数hPrevInstance判断(此为旧参数,目前已抛弃,留着是为兼容,以后传入的值永远为0);产生窗口放在InitInstance()在表示任何实例都会进入,之所以特别提到自定义的两个函数InitApplication()和InitInstance()是因为MFC中有,后续章节会涉及到。

4. 消息循环

以消息为基础,以事件驱动之(Message based, event drive)

  • Windows程序的进行系依靠外部发生的事件来驱动,即程序不断等待(用一个While循环)等待任何可能的输入,然后做判断,然后再做适当处理。“输入”是由操作系统捕捉到之后,以消息形式(一种数据结构)进入程序之中

  • “输入”的分类:硬件所产生的消息(鼠标移动、键盘按下)放在系统队列(system queue)中,以及由Windows系统或其它Windows程序传送过来的消息,放在程序队列(application queue)中,以程序的角度看消息来自哪里并没有太大区别,反正程序调用一个GetMessage() API就取得一个消息,程序的生命靠它来推动,所有的GUI系统,包括UNIX的X Windows以及OS/2的Presentation Manger都像这样,是以消息为基础的事件驱动系统。可想而知,每个Windows程序都应该有一个如下的循环:

/*GetMessage()获取消息,收到消息才返回true,否则一直等待,当收到的消息是WM_QUIT时返回false,结束循环*/

while (GetMessage(&msg, NULL, 0, 0)) {

      TranslateMessage(&msg);//转换键盘消息

      DispatchMessage(&msg);//分派消息

  }


  • 其中***TranslateMessage()是为了将键盘消息转换,DispatchMessage()会将消息传给窗口函数去处理。没有指定函数名称却可以将消息传送过去岂不是玄?这是因为消息发生之时,操作系统已经根据当时状态,为它标明了所属窗口,而窗口所属的窗口类又已经明白标示了窗口函数(也就是wc.lpfnWndProc所指的函数),所以DispatchMessage()***自有脉络可寻,***DispatchMessage()***经过USER模块的协助,才把消息交到窗口函数手中。
  • 调用RegisterWindowClass注册窗口类,关联其中的窗口过程WndProc。
  • 调用CreateWindow创建窗口并显示。
  • (主线程)进入GetMessage循环,取得消息后调用DispatchMessage分发消息。

这里的GetMessage循环就是所谓的消息泵,它像水泵一样源源不断的从线程的消息队列中取得消息,然后调用DispatchMessage把消息分发到各个窗口,交给窗口的WndProc去处理。

int WINAPI _tWinMain(HINSTANCE hInstance,  HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,  int       nCmdShow)
{
    RegisterWindowClass(hInstance);                                          //1 

    HWND hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,        //2
        CW_USEDEFAULT, 0, CW_USEDEFAULT,z 0, NULL, NULL, hInstance, NULL);
    ShowWindow(hWnd, nCmdShow);

    MSG msg;                                                                     //3
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int) msg.wParam;
}
WORD RegisterWindowClass(HINSTANCE hInstance)
{
    WNDCLASSEX wcex;
    wcex.lpszClassName  = szWindowClass;
    wcex.lpfnWndProc    = WndProc;
    ...
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_PAINT:
    ...
}

用一副图来表示这个过程:
在这里插入图片描述

  1. 鼠标点击。
  2. 操作系统底层获知这次点击动作,根据点击位置遍历找到对应的Hwnd,构建一个Window消息MSG,把这个消息加入到创建该Hwnd线程的消息队列中去。
  3. 应用程序主线程处于GetMessage循环中,每次调用GetMessage获取一个消息,如果线程的消息队列为空,则线程会被挂起,直到线程消息队列存在消息线程会被重新激活。
  4. 调用DispatchMessage分发消息MSG,MSG持有一个Hwnd的字段,指明了消息应该发往的Hwnd,操作系统在第2步构建MSG时会设置这个值。
  5. 消息被发往Hwnd,操作系统回调该Hwnd对应的窗口过程WndProc,由WndProc来处理这个消息。

5. 窗口的生命中枢:窗口函数

  • 消息循环中的***DispatchMessage()***通过USER模块的协助把消息送到窗口函数,窗口函数通过swithch/case方式判断消息种类,以决定处置方式,窗口函数是被Windows系统调用的,此为回调函数(call back),意思是:代码在你的程序中,被Windows系统调用,这些函数虽然由你设计,但是永远不会也不该被你调用,它是为Windows系统准备的,所以窗口函数的接口必然是一致的:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

注意的是不论什么消息都必须被处理,所以switch/case指令中的default:处必须调用DefWindowProc(),这是Windows内部默认的消息处理函数。之所以设计成call back函数是因为很多情况除了你要处理消息,系统也必须处理,设计成call back系统就有机会处理了。

  • 窗口函数中的参数wParam和lParam的意义,因消息之不同而异

6.Modal对话框的激活与结束,靠的是DialogBox和EndDialog两个API函数

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

深入WPF的Window

作为外界和可视化数据之间的桥梁,Window具有对内和对外两层作用。

  1. 先说对内,Window内部可能会存在Button,ListBox等等控件,这些控件组成了一个对象树。树的子节点可能很多,但顶点只有一个,这个对象树是WPF的核心,Routed Event和Routed Command等都是依附于它的。抛开具体的对象树不说,我们要关注的是它的这种“众”字型的结构。如果你把这颗可视化数据组成的对象树想象成一个人的话,那么它的顶点就是它的头,我们对手臂和腿的操作只要对头喊话就可以了。换言之,对于WPF的Window,它对内最关心的就是找到对象树的头(RootVisual),然后通过头把操作传递下去。
  2. 从对外来看,操作系统关注的是注册Window的风格以及Rect。比如鼠标按键被按下时,按键消息被发送到系统的消息队列中,系统通过扫描所有注册窗口的Rect判断按键发生在哪个窗口中,再在适当的时机把按键消息从系统消息队列转移到创建窗口线程的消息队列中等待窗口处理。对于WPF的Window来说,同步这个Rect很重要,Window的UI是WPF的,但内部有个隐藏的使用CreateWindow创建的Win32-Window,当用户设置win.Width=60方法时要同步内部Window的Rect,反过来接收到WM_SIZE时也需要调用RootVisual去执行WPF的Measure、Arrange流程。

用一个草图来表示Window的消息处理过程:
  在这里插入图片描述

  • 系统将消息发给隐藏的Win32-Window,在DispatcherGetMessage并分发到对应的窗口过程处理函数WndProc。
  • WndProc里应是一个大的Switch-Case,用以处理不同的Window消息。按照消息的类别,WPF提供了不同的Manager来管理,这里的Manger并不是直接处理Window消息,并且并不是所有消息都经过WndProc再转到Manager的。
    比如说WM_KEYDOWN,Dispatcher调用GetMessage获得消息后, 调用了ComponentDispatcher的RaiseThreadMessage方法(关于ComponentDispatcher,可以参阅Nick的文章),最终由KeyboardDevice产生Keyboard.PreviewKeyDownEvent这个路由事件(Routed Event)。
  • 仍然以WM_KEYDOWN来说, InputManager找到这个Input发生的区域–Window,调用Window的RaiseEvent方法唤起Keyboard.PreviewKeyDownEvent这个路由事件。
  • 路由事件沿着对象树开始向下传递,方向是一去一回,由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的架构图,其中重要的两个是PresentationCoreMilCore。在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 TreeComposite Engine通过Message Transport来进行通信,Message Transport(消息传输)包括TransportChannel两部分。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来完成这些内部的处理。

WPF Dispatcher详解

在这里插入图片描述
 默认的WPF工程中中是找不到传统的Program.cs文件的,它的App.xaml文件的编译动作为ApplicationDefinition,编译后,编译器会自动生成App.g.cs文件,包含了Main函数。如下:

 

        [System.STAThreadAttribute()]
        [System.Diagnostics.DebuggerNonUserCodeAttribute()]
        public static void Main() 
    {
            WpfApplication3.App app = new WpfApplication3.App();
            app.InitializeComponent();
            app.Run();
        }
  

这里出现了Application类,按MSDN上的解释,“Application 是一个类,其中封装了 WPF 应用程序特有的功能,包括:应用程序生存期;应用程序范围的窗口、属性和资源管理;命令行参数和退出代码处理;导航”等。

调用app.Run()之后,按照前面Win32的步骤,应用程序应进入到一个GetMessage的消息泵之中,那么对WPF程序来说,这个消息泵是什么样的呢?又和Dispatcher有什么关系呢?
 
 Dispatcher的构造函数是私有的,调用Dispacher.CurrentDispatcher会获得当前线程的Dispatcher,Dispatcher内部持有一个静态的所有Dispatcher的List。因为构造函数私有,只能调用CurrentDispatcher来获得Dispatcher,可以保证对同一个线程,只能创建一个Dispatcher。

Dispatcher提供了一个Run函数,来启动消息泵,内部的核心代码是我们所熟悉的,如:

   while (frame.Continue)
        {
            if (!GetMessage(ref msg, IntPtr.Zero, 0, 0))
                break;
            TranslateAndDispatchMessage(ref msg);
        }

这里出现了一个Frame的概念,暂且不谈,来看看Dispatcher相对于传统的消息循环,有哪些改进的地方。

Dispatcher的新意

在Winform的消息循环中:

  1. 为了线程安全,调用Control的Invoke或者BeginInvoke方法可以在创建控件的线程上执行委托,方法的返回值分别为object和IAsyncResult。尽管可以使用IAsyncResultIsCompletedAsyncWaitHandle等方法来轮询或者等待委托的执行,但对于对任务的控制来讲,这个粒度是不够的,我们不能取消(Cancel)一个已经调用BeginInvoke的委托任务,也不能更换两个BeginInvoke的执行顺序。
  2. 更为友好的接口支持,Windows编程中,在窗口消息循环中加入Hook是常见的需求,Dispatcher提供了DispatcherHooks类,以Event的形式对外提供了OperationAborted,OperationCompleted,OperationPosted等事件。
      这里的Operation指的是DispatcherOperation,为了更好的控制消息循环,WPF引入了DispatcherOperation来封装Window消息,这个DispatcherOperation如下:

DispatcherOperation类看起来还是比较简单明了的,以属性的形式暴露了Result(结果),Status(状态),以及用事件来指出这个Operation何时结束或者取消。其中比较有意思的是Priority属性,从字面来看,它表示了DispatcherOperation的优先级,而且提供了get和set方法,也就是说,这个DispatcherOperation是可以在运行时更改优先级的。那么这个优先级是怎么回事,Dispatcher又是如何处理DispatcherOperation的呢,让我们深入DispatcherOperation,来看看它是如何被处理的。

public sealed class DispatcherOperation 
{ 
    public Dispatcher Dispatcher { get; } 
    public DispatcherPriority Priority { get; set; } 
    public object Result { get; } 
    public DispatcherOperationStatus Status { get; }
    public event EventHandler Aborted; 
    public event EventHandler Completed;
    public bool Abort(); 
    public DispatcherOperationStatus Wait(); 
    public DispatcherOperationStatus Wait(TimeSpan timeout); 
}

在这里插入图片描述

从三个方面来谈一下DispatcherOperation:

DispatcherOperation是如何被创建的。
DispatcherOperation是何时被执行的。
DispatcherOperation是怎样被执行的。

Dispatcher提供了BeginInvokeInvoke两个方法,其中BeginInvoke的返回值是DispatcherOperation,Invoke函数的内部调用了BeginInvoke,也就是说,DispatcherOperation就是在这两个函数中被创建出来的。我们可以调用这两个函数创建新的DO,WPF内部也调用了这两个函数,把Window消息转化为DispatcherOperation,用一副图表示如下:
  在这里插入图片描述

  1. 窗口过程WndProc接收到Window消息,调用Dispatcher的Invoke方法,创建一个DispatcherOperation。Dispatcher内部持有一个DispatcherOperation的队列,用来存放所有创建出来的DispatcherOperation。默认一个DO被创建出来后,会加入到这个队列中去。WndProc调用Invoke的时候比较特殊,他传递的优先级DispatcherPriority为Send,这是一个特殊的优先级,在Invoke时传递Send优先级WPF会直接执行这个DO,而不把它加入到队列中去。

  2. 用户也可以随时调用Invoke或者BeginInvoke方法加入新的DO,在DispatcherOperation处理的时候也可能会调用BeginInvoke加入新的DO。
      DO被加入到Dispatcher的队列中去,那么这个队列又是何时被处理呢?Dispatcher在创建的时候,创建了一个隐藏的Window,在DO加入到队列后,Dispatcher会向自己的隐藏Window发送一个自定义的Window消息(DispatcherProcessQueue)。当收到这个消息后,会按照优先级和队列顺序取出第一个DO并执行:

  3. 用户调用BeginInvoke。

  4. Dispatcher创建了一个DO,加入到DO队列中去,并向自己的隐藏窗口Post自定义消息(DispatcherProcessQueue)。

  5. 创建隐藏窗口时会Hook它的消息,当收到的消息为DispatcherProcessQueue时,按照优先级取出队列中的一个DO,并执行。
      每加入一个DO就会申请处理DO队列,在DO的优先级(DispatcherPriority)被改变的时候也会处理DO队列,DO在创建时声明了自己的优先级,这个优先级会影响到队列的处理顺序。

UI线程和Dispatcher

通常,WPF启动时具有两个线程,一个处理呈现(Render),另一个用于管理UI。这个管理UI的线程通常被称为UI线程。在WPF中,所有UI对象的基类为DispatcherObject,WPF在对所有DispatcherObject属性操作前进行了线程亲缘性校验,只有在创建UI对象的线程中才可以访问该UI对象。
在这里插入图片描述

  1. System.Object 类:大家都知道在.Net中所有类型的基类,DispatcherObject 就继承于它,所以它是WPF的基类。

  2. System.Windows.Threading.DispatcherObject 类:从图中看WPF 中的使用到的大部分控件与其他类大多是继承 DispatcherObject 类,它提供了用于处理并发和线程的基本构造。

  3. System.Windows.DependencyObject类:对WPF中的依赖项属性承载支持与 附加属性承载支持,表示参与 依赖项属性 系统的对象。

  4. System.Windows.Media.Visual类:为 WPF 中的呈现提供支持,其中包括命中测试、坐标转换和边界框计算等。

  5. System.Windows.UIElement 类:UIElement 是 WPF 核心级实现的基类,该类是 Windows Presentation Foundation (WPF) 中具有可视外观并可以处理基本输入的大多数对象的基类。

  6. System.Windows.FrameworkElement类:为 Windows Presentation Foundation (WPF) 元素提供 WPF 框架级属性集、事件集和方法集。此类表示附带的 WPF 框架级实现,它是基于由UIElement定义的 WPF 核心级 API 构建的。

  7. System.Windows.Controls.Control 类:表示 用户界面 (UI) 元素的基类,这些元素使用 ControlTemplate 来定义其外观。

  8. System.Windows.Controls.ContentControl类:表示没有任何类型的内容表示单个控件。

WPF的绝大部分的控件,还包括窗口本身都是继承自ContentControl的。

前面提到,由于Dispatcher构造函数私有,一个线程最多只能有一个Dispatcher。对UI线程来说,Dispatcher的主要作用就是对任务项(DispatcherOperation)进行排队。对UI对象来说,DispatcherObject有一个Dispatcher属性,可以获得创建该UI对象线程的Dispatcher。这种设计通过Dispatcher统一了UI对象的操作,从使用上隔离了UI对象和线程间的关系。

在WPF应用程序中***,Application.Current.Dispatcher是一个重要的属性。它允许开发者在WPF应用程序的主线程上执行操作,这对于确保UI响应性和避免假死(程序没有响应用户输入)非常关键。主线程负责接收输入、处理事件、绘制屏幕等任务。为了避免在主线程上执行耗时的操作,开发者可以使用Application.Current.Dispatcher.Invoke或者Application.Current.Dispatcher.InvokeAsync***方法,将需要在主线程上执行的代码块放入主线程的工作项队列中执行。

  • 主线程调度:用于在WPF应用程序的主线程上执行操作,确保UI线程的安全性。
  • UI响应性:允许开发者在主线程上执行操作,确保应用程序的UI响应及时,避免假死。
  • 线程关联特征:大部分WPF控件继承自 DispatcherObject,包括 Application对象,具有线程关联特征,只有在创建这些对象的线程上操作才是安全的。
  • 全局性:Application.Current.Dispatcher是全局的,对于当前应用程序的所有线程都是共享的,确保一致性和可靠性。
DispatcherObject

DispatcherObject是WPF中的一个基类,它允许对象在特定的线程上执行操作。在WPF中,大多数UI元素都继承自DispatcherObject,这使得它们具有线程关联特性。这意味着只有在创建UI元素的线程上操作这些元素才是安全的,这有助于确保UI的响应性和避免多线程冲突。DispatcherObject提供了Dispatcher属性,通过该属性可以获取与对象关联的Dispatcher实例,然后使用该Dispatcher实例来在对象关联的线程上执行操作,确保线程安全性。

如何保证UI线程操作安全的?

  • 线程亲缘性校验(Thread Affinity Check): DispatcherObject 在进行UI操作之前会校验当前线程是否为关联的UI线程。如果不是,它会将操作请求放入UI线程的消息队列中,确保在UI线程上执行。这样,即使在多线程环境下,UI线程上的操作也不会受到其他线程的干扰。
  • Dispatcher属性(Dispatcher Property):每个 DispatcherObject都有一个关联的 Dispatcher属性,该属性标识了UI线程。通过这个属性, DispatcherObject 可以将操作请求发送到关联的UI线程上执行。
  • VerifyAccess方法: DispatcherObject 类中提供了 VerifyAccess 方法,该方法用于在调用线程和 DispatcherObject 的 UI 线程之间验证线程亲缘性。通过调用此方法,可以确保当前线程是UI线程,从而保证操作的线程安全性。

Dispatcher的组成部分

  1. 消息队列(Message Queue):Dispatcher维护一个消息队列,其中包含需要在UI线程上执行的工作项。
  2. 消息循环(Message Loop):Dispatcher负责处理消息队列中的消息,按照优先级选择工作项并运行它们,直到队列为空。
  3. 优先级调度(Priority Scheduling):Dispatcher基于优先级选择工作项,并按照其优先级运行,确保高优先级的工作项优先执行。
  4. UI线程关联(UI Thread Affiliation):每个UI线程都有一个关联的Dispatcher对象,负责在UI线程上执行操作,确保UI元素的安全访问。
  5. 异步调度(Async Dispatching):Dispatcher提供异步调度的功能,例如 InvokeAsync方法,允许在UI线程上异步执行指定的操作。

Dispatcher是如何运行的?

1. UI线程管理:

Application.Current.Dispatcher是一个 Dispatcher对象,负责管理应用程序的UI线程。UI线程负责处理用户界面的绘制、事件响应和控件更新等任务。

2. 工作项队列:

Dispatcher 维护一个工作项队列,其中包含需要在UI线程上执行的工作项(通常是委托或操作)。
这些工作项按照加入队列的顺序执行,确保了操作的顺序性。

3. 跨线程访问:

非UI线程(例如后台线程) 需要访问UI元素时,它们不能直接进行操作,因为UI元素只能在UI线程上进行修改。此时,这些线程可以使用 Dispatcher.Invoke或 Dispatcher.BeginInvoke方法

  • Dispatcher.Invoke将操作推送到UI线程上执行,该方法是同步的,调用线程会被阻塞,直到操作执行完成。
  • Dispatcher.BeginInvoke将操作异步地推送到UI线程上执行,调用线程不会被阻塞。

4. 线程安全性:

通过使用 Dispatcher,WPF确保了UI元素的线程安全性。即使应用程序的其他部分在不同的线程上执行,UI元素的操作仍然受到 Dispatcher的保护,确保了应用程序的稳定性和可靠性。

5. 异步操作:

Dispatcher.InvokeAsync方法用于在UI线程上异步执行指定的操作,而不会阻塞调用线程。这使得在处理大量数据或执行耗时操作时,UI线程仍然保持响应性。

总结一下Dispatcher的工作原理,它在UI线程上启动一个循环,不断地从消息队列中取出消息,然后将消息分发到合适的UI元素上。这样,无论是用户交互、异步操作,还是其他UI相关的事件,都经过Dispatcher的调度,保证了UI的稳定和流畅。

1. 理解XAML相关窗体设计的原理。

逻辑树结构非常类似HTML,但更加麻烦。 理解Style类似于CSS,并可以通过随时更换资源字典以达到更换主题或者换肤的目的。
在这里插入图片描述

2. 触发器(Trigger)

最常用的是属性触发器和数据触发器。需要知道触发器主要是用于视觉交互的。属性触发器是控件本身的某个属性值发生改变,比如IsMouseOver=True的时候,会触发可视内容 比如背景色 发生变化。数据触发器是在数据模板(DataTemplete)中,当某个业务数据发生变化改变时,会触发可视内容发生变化。

3. 为了创建形态各异的界面,实现各种神奇的效果,需要学习WPF绘图。

  1. 使用图形,包括:直线,矩形,椭圆,贝塞尔曲线,Path(最强大的路径); 应用滤镜效果,Effect比较简单,但是导入和开发外部滤镜,一直没有研究。 使用变形。有平移,旋转,缩放,扭曲等基本变形,以及矩阵变形。(要注意的是:每种变形既可以放在呈现变形中,也可以放在布局变形中,需要区分二者的区别。呈现变形只是看到的样子变化了,实际位置和形状都没变。布局变形是真的变化,会在变形的同时不断对其他控件重新进行布局计算。)

5. 学习使用XAML创建简单的动画

尝试使用3类触发器触发动画的发生,使用VisualStatusManager来应用动画,如无必要,尽量避免通过写代码的方式创建动画;(扩展:使用Blend创建并组合出复杂的动画。)

5. 依赖属性和附加属性。

DP,也叫依赖属性,从名字来看,它首先是一个属性,依赖是一个形容词。就是在传统的属性上加了个“依赖”。

要学会如何自定义我们自己扩展的依赖属性和附加属性。所谓依赖属性,从功能上讲:就是一个普通的属性,附带了可以绑定到任意对象的其他属性上的功能。所谓绑定就是:一个值变化,另外一个值跟着变化的。这样可以省去大量的界面效果相关的后台代码,并使界面和业务代码分离成为可能。

public static readonly DependencyProperty IsSpinningProperty = DependencyProperty.Register("IsSpinning", typeof(bool)); 

public bool IsSpinning 
{ 
    get { return (bool)GetValue(IsSpinningProperty); } 
    set { SetValue(IsSpinningProperty, value); } 
} 

6. 模板。主要了解:ControlTemplete,DataTemplete

  • ControlTemplete 是用来重写现有控件的可视结构的,一般和依赖属性和附加属性结合,加上绑定,控件可以获得很好的扩展。
  • DataTemplete 主要用于定义数据对象的可视化结构的。

既然是数据对象,最好要有个数据类型,即在DataType中定义。模板在WPF起着巨大的作用。

控件模板可以很容易写出任意形态任意效果的外观,
数据模板使得View层和ViewModel层很好的分离。

请一定要注意,起初我对这样的概念不屑一顾,其实就是没明白什么意思。

 后来我才懂:就因为数据模板的存在,使得代码中几乎再也不用出现控件对象了.

7. MVVM设计模式

最方便学习此模式的是MVVMLight框架,可以直接在NuGet中下载。Model - View - ViewModel。不同于MVC,MVP等设计模式, MVVM最主要的特点是实现UI(View)和业务(Model)的分离。而ViewModel应该同时负责表现逻辑和业务逻辑。这在开发时尤其有用,另外可以同时快速创建设计用的ViewModel,以便设计阶段即可以模拟出完全真实的使用效果,因为View层对应的ViewModel可以很容易的切换。

ublic class NameObject : INotifyPropertyChanged 
    { 
        private string _name = "name1"; 
        public string Name 
        { 
            get 
            { 
                return _name; 
            } 
            set 
            { 
                _name = value; 
                NotifyPropertyChanged("Name"); 
            } 
        } 

        private void NotifyPropertyChanged(string name) 
        { 
            if (PropertyChanged != null) 
            { 
                PropertyChanged(this, new PropertyChangedEventArgs(name)); 
            } 
        } 

        public event PropertyChangedEventHandler PropertyChanged; 
    } 
 public class NameObjectViewModel : INotifyPropertyChanged 
    { 

        private readonly NameObject _model; 

        public NameObjectViewModel(NameObject model) 
        { 
            _model = model; 
            _model.PropertyChanged += new PropertyChangedEventHandler(_model_PropertyChanged); 
        } 

        void _model_PropertyChanged(object sender, PropertyChangedEventArgs e) 
        { 
            NotifyPropertyChanged(e.PropertyName); 
        } 

        public ICommand ChangeNameCommand 
        { 
            get 
            { 
                return new RelayCommand( 
                         new Action<object>((obj) => 
                        { 

                             Name = "name2"; 

                        }), 
                         new Predicate<object>((obj) => 
                        { 
                             return true; 
                        })); 
            } 
        } 

        public string Name 
        { 
            get 
            { 
                return _model.Name; 
            } 
            set 
            { 
                _model.Name = value; 
            } 
        } 

        private void NotifyPropertyChanged(string name) 
        { 
            if (PropertyChanged != null) 
            { 
                PropertyChanged(this, new PropertyChangedEventArgs(name)); 
            } 
        } 

        public event PropertyChangedEventHandler PropertyChanged; 
} 

public class RelayCommand : ICommand 
    { 
        readonly Action<object> _execute; 
        readonly Predicate<object> _canExecute; 

        public RelayCommand(Action<object> execute, Predicate<object> canExecute) 
        { 
            _execute = execute; 
            _canExecute = canExecute; 
        } 

        public bool CanExecute(object parameter) 
        { 
            return _canExecute == null ? true : _canExecute(parameter); 
        } 

        public event EventHandler CanExecuteChanged 
        { 
            add { CommandManager.RequerySuggested += value; } 
            remove { CommandManager.RequerySuggested -= value; } 
        } 

        public void Execute(object parameter) 
        { 
            _execute(parameter); 
        } 
    } 
    public partial class Window1 : Window 
    { 
        public Window1() 
        { 
            InitializeComponent(); 
            this.DataContext = new NameObjectViewModel(new NameObject()); 
        } 
    } 
<Window x:Class="WpfApplication7.Window1" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        Title="Window1" Height="300" Width="300"> 
    <Grid> 
        <TextBlock Margin="29,45,129,0" Name="textBlock1" Height="21" VerticalAlignment="Top" 
                   Text="{Binding Path=Name}"/> 
        <Button Height="23" Margin="76,0,128,46" Name="button1" VerticalAlignment="Bottom" 
                Command="{Binding Path=ChangeNameCommand}">Rename</Button> 
    </Grid> 
</Window

8. 可以附加在控件上的 行为 (Behavior)

行为,还有上面刚说的 触发器,EventToCommand,都是一种附加属性。这样就很好理解了。通常理解的附加属性,只是一一个数据类型的值,既然扩展成了一个行为这么复杂的数据类型,目的主要是为了实现各种行为效果的重用,然而行为的封装是最完整的。

9. 要反复深刻理解装饰器相关 Adorner, Decorator, AdornerDecorator

从简单开始,最好懂的是 Decorator,如果这个词感到陌生,那么Border就不陌生了,边框嘛,一个东西,外面套个边而已。然而Decorator是Border的基类而已。可以扩展它,然而我目前还没有遇到继承Decorator的应用场景。再说Adorner,这才是真正的装饰器,本身没有可视结构,它的存在就是要你继承它,并设计它的可视样式,如何继承,网上一大堆教程,最直观的理解就是:在控件自身上面,蒙了一层额外的装饰,用于交互性的提示与操作。 比如:光标,旋转钮,调整大小,表头的排序箭头 等等。需要什么就画什么,恩恩,这个很重要。

10. 路由事件和命令

如果想要写出来的WPF程序在复杂的界面中不会出现莫名其妙的问题,那么一定要弄懂路由事件和路由命令的概念。

所谓路由,有三种烂大街的方式:隧道,冒泡,直接。 顾名思义:隧道,标签从外向内响应事件。 冒泡,标签从内向外相应事件。

11.新手学好WPF编程并不难建议

1. 熟悉XAML,熟悉布局,熟悉基本控件,能够根据产品端提出的原型画出界面。——(入门)

2. 研究事件、Style、Template,提升自己的项目能力。——(可按Winform风格实现WPF)。

3. 熟悉MVVM,熟悉ICommand,学会使用MVVM框架实现程序。——(进入WPF味道)

4. 研究依赖属性、路由事件,学会写自定义控件。

5. 学会换肤,可以更新整个界面的样式(Light/Dark);学会一个ViewModel对应多个View。(学会写机制)

6. 接触开源框架MVVM Light等,研究其精髓,提升WPF掌控能力。

7. 提升对WPF技术点的取舍能力,向Presentation的本质深入。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是刘彦宏吖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值