WPF学习与分享之二:DispatcherObject与WPF线程模型(下)

WPF学习与分享之二:DispatcherObject与WPF线程模型(下)
2008-08-28 21:50

 

在上一篇中我们分析了Win32和WinForm编写GUI应用程序会面对的主要问题。总结下来最重要的就是:如何高效的从Worker线程中更新界面。所以首先让我们看看WPF中是如何达到这个目的的。

DispatcherObject的使用

DispatcherObject类有两个成员方法,CheckAccess和VerifyAccess。CheckAccess功能和WinForm中Control.InvokeRequired属性相同,当调用线程与对象的创建线程不是同一个实例时,返回False。VerifyAccess则是抛出异常。

DispatcherObject还有一个Dispatcher的属性,返回控件的创建Dispatcher。通过其Invoke方法,我们可以将界面更新的操作Marshal到控件的创建线程。如下例:

 

1 TextBox textbox = new TextBox();
2 if ( ! textbox.CheckAccess)
3 {
4    textbox.Dispatcher.Invoke(DispatcherPriority.Send, delegate { });
5 }

CheckAccess()的实现方法类似于上篇的最后一个例子,通过比较两个线程实例,因而速度很快,只需要几个IL指令。继承DispatcherObject的类,在内部都调用VerifyAccess来做判断,因而当非创建线程调用时,就会抛出异常。这样的好处是我们可以较早的发现代码的问题,不像WinForm那样不确定的发生。

好了,下面我们来看看真正做事情的Dispatcher类,在这之前让我们再回顾一个Win32的消息循环。

Win32 消息循环

在Win32中,消息Pump的建立是通过循环的调用GetMessage,当收到WM_Quit类型的消息是,退出。而我们对窗口或控件属性的修改,如颜色,位置都实际都是调用PostMessage或SendMessage。当调用线程与窗口的Owner线程是同一个,则SendMessage直接调用窗口函数;如不同则将消息放置在窗口对应的消息队列中,由GetMessage取出并进行处理。WinForm实际是建立在Win32的基础上,所以当我们写lable.Text = "Hello World"时,实际是调用了PostMessage等方法。

下面我们看看Win32消息队列的特点和限制:

  1. 任劳任怨的从消息队列中拿出消息,并调用对应的窗口处理函数
  2. 当消息Post进队列后就无法控制了
  3. 消息的处理支持有限的优先级,但都由OS控制,用户代码不能控制和改变优先级
  4. Application.DoEvents只有在处理完消息队列中所有消息后才返回,当然Win32没有这个限制,因为Message Pump就是用户创建的
  5. 可以添加钩子,偷偷的干些坏事

WPF中的Dispatcher的主要功能类似于Win32中消息队列,当并不是替换消息循环,而是建立在消息循环之上。首先,让我们看看Dispatcher的结构:

线程相关的Dispatcher的创建

当我们在一个线程上第一次创建WPF对象时,DispatcherObject构造函数会调用Dispatcher.CurrentDispatcher赋值给私有变量_dispatcher。Dispatcher.CurrentDispatcher通过Thread.CurrentThread来找到该线程对应的Dispatcher实例,如不存在则创建一个。Dispatcher的构造函数,首先创建了一个具有11个级优先级(DispatcherPriority)的优先队列PriorityQueue,这个优先队列用来保存不同优先级的操作。然后调用Win32 API RegisterClassEx注册窗口处理函数,并调用CreateWindowEx创建一个不可见的窗口。

Dispatcher有两个静态方法用来建立消息循环,定义如下:

1 public sealed class Dispatcher
2 {
3    public static void Run()
4    {
5      PushFrame( new DispatcherFrame());
6    }
7    public static void PushFrame(DispatcherFrame frame) {…}
8 }

尽管是静态方法,实际上是调用当前线程对应的Dispatcher实例的方法。Application.Run()内部实际就是调用了Dispatcher.Run(),进而调用Dispatcher.PushFrame(…)。当我们创建一个WPF项目时,VS自动生成的App.xaml,编译后的代码为

1 public static void Main()
2 {
3    WpfHello.App app = new WpfHello.App();
4    app.InitializeComponent();
5    app.Run();
6 }

当调用PushFrame()或Run()后,内部开始循环调用Win32 GetMessage方法,而建立消息循环。如下面的例子中,在VS缺省的WPF项目中,创建了一个新的线程,在该线程中创建一个新的WPF窗口,并启动消息循环:

1 public partial class App : Application
2 {
3     protected override void OnStartup(StartupEventArgs e)
4     {
5        // Create a new thread
6        Thread monitorThread = new Thread(QueueMonitor.ThreadMain);
7        monitorThread.SetApartmentState(ApartmentState.STA);
8        monitorThread.Start( this );
9
10        base .OnStartup(e);
11     }
12 }
13
14 class QueueMonitor
15 {
16     public static void ThreadMain(Object obj)
17     {
18        QueueMonitor monitor = new QueueMonitor(obj as Application);
19     }
20
21     private Application m_target;
22     private DispatcherFrame     m_frame;
23     private MonitorWindow       m_window;
24     public QueueMonitor(Application target)
25     {
26        m_target = target;
27
28        m_frame = new DispatcherFrame();
29        m_window = new MonitorWindow();
30
31        m_window.Closed += MonitorWindow_Closed;
32        m_window.Visibility = Visibility.Visible;
33
34        Dispatcher.Run();
35        // Dispatcher.PushFrame(m_frame);
36     }
37
38     void MonitorWindow_Closed( object sender, EventArgs e)
39     {
40        Dispatcher.ExitAllFrames();
41        // m_frame.Continue = false;
42     }
43 }
消息循环的控制

上个例子中,我们也可以使用PushFrame方法。相比于Run(),我们可以通过PushFrame的参数DispatcherFrame对象来控制什么时候退出消息循环。如需退出当前层的循环,只需将对应的DispatcherFrame.Continue赋值false。我们可以嵌套的调用PushFrame,这点上类似Application.DoEvents,更具灵活性,但我们仍不能只要求处理特定优先级的消息。

另外可以调用Dispatcher.ExitAllFrames方法退出当前线程的所有消息循环。Dispatcher还提供一个方法DisableProcessing,该方法可以暂停消息循环处理消息。下面是这个方法的使用方法:

1 using (Dispatcher.DisableProcessing())
2 {
3    Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Render, delegate { });
4 }

该方法返回的DispatcherProcessingDisabled对象实现了IDisposable接口,在Dispose中恢复消息循环。该方法也可以嵌套的调用。

下面我们来看看如何投递消息,及Dispatcher如何处理的。

Dispatcher优先队列

Dispatcher的Invoke和BeginInvoke方法,用来向Dispatcher的优先队列放置任务,前者是同步方法,后者是异步。这两个方法,第二个参数是个delegate对象,代表要执行的任务。第一个参数定义任务的优先级,具体定义可以看DispatcherPriority枚举类型。总的来说,我们可以划分为两类,Background和Foreground,另一个比较特殊的是DispatcherPriority.Send。

当在Invoke中指定Send时,且CheckAccess为真,则Invoke直接调用delegate执行任务。如果不是,则调用BeginInvoke,并等待结果,或超时。

BeginInvoke执行时,首先将该任务封装成DispatcherOperation对象,并放置在对应的优先队列中。然后判断是Background还是Foreground,如果是Foreground,则调用PostMessage往Win32消息队列中投递一条消息,然后返回。如果是Background,则检查是否有Timer,如没有则创建一个Timer,Timer会不断循环的投递消息到Win32消息队列,来触发消息的处理。当有Foreground消息是,则删掉Timer。通过这种方式,系统在空闲的时候可以处理Background消息。

当有Win32消息投递到Win32队列时,注册的窗口函数被执行,从优先队列中取出一个DispatcherOperation来执行。完成后,则投递新的Win32消息来触发下次执行,或等待Timer消息。

BeginInvoke的返回值则为DispatcherOperation对象,通过她我们可以取消,等待,或者调整该任务的优先级。在后面的系列中,我们在具体看不同Foreground优先级的使用。

优先队列钩子Hook

与Win32类似,我们也可以对消息的处理添加Hook,可以添加下面的Event Handler:

1 Dispatcher.CurrentDispatcher.Hooks.OperationAborted += new DispatcherHookEventHandler(Hooks_OperationAborted);
2 Dispatcher.CurrentDispatcher.Hooks.OperationCompleted += ;
3 Dispatcher.CurrentDispatcher.Hooks.OperationPosted += ;
4 Dispatcher.CurrentDispatcher.Hooks.OperationPriorityChanged +=

Event Handler的参数中,我们可以获得对应的Dispatcher和DispatcherOperation对象。通过这种办法,我们可以过滤,查询或改变任务的优先级等操作。

DispatcherTimer

DispatcherTimer类似WinForm中的Timer。我们可以在构造函数中指定优先级,Dispatcher实例等等。

总结

总的来说,与Win32比较,如果给WPF中的线程和消息循环的机制打分的话,我觉得可以打4分的高分。WPF解决了Win32中没有优先级,跨线程调用性能的问题,友好的编程接口。如果说不足的话,如果PushFrame可以支持优先级,对Reentrancy的问题,可能能更好的控制;另外,DispatcherOperation无法获得名字,如我们要开发一个队列监视程序的话很不方便。这些我认为可以扣0.5分,那另外0.5分是什么呢?

最后,如果我们看这些类的话,全部都在Windowsbase.dll中,也就是说System.Windows.Threading中的类,如DispatcherObject,Dispatcher也可以用于其他系统中。我们甚至可以利用这些在我们的系统中。

转自:http://www.cnblogs.com/Nullnoid/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值