WPF的UI更新方式

2011年6月22日星期三

WPF的UI更新方式

緣由

在以往的 VB6,或者是 Windows Form 應用程式中,更新 UI的方式極為簡單,往往只是 Application.DoEvents 就可以更新。Windows Form 中更有 Invoke 與 BeginInvoke 可以彈性地使用。

那在 WPF 中,要如何更新 UI 的內容呢?

範例1:Bad Example

當然,要從一個不正確的範例開始。

Ex1Bad.xaml

?
1
2
3
4
5
6
7
8
9
< Window x:Class = "WpfApplication10.Ex1Bad"
         xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
         Title = "Ex1Bad" Height = "300" Width = "300" >
   < StackPanel >
     < Label Name = "lblMsg" Content = "Nothing" />
     < Button Content = "Start" Name = "btnStart" Click = "btnStart_Click" />
   </ StackPanel >
</ Window >

Ex1Bad.xaml.cs

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System.Threading;
using System.Windows;
 
namespace WpfApplication10
{
   public partial class Ex1Bad : Window
   {
     public Ex1Bad()
     {
       InitializeComponent();
     }
 
     private void btnStart_Click( object sender, RoutedEventArgs e)
     {
       lblMsg.Content = "Starting..." ;
       Thread.Sleep(3000); //執行長時間工作
       lblMsg.Content = "Doing..." ;
       Thread.Sleep(3000); //執行長時間工作
       lblMsg.Content = "Finished..." ;
     }
   }
}

這裡以 Thread.Sleep(3000)讓 UI Thread 睡個3秒鐘,來模擬長時間的工作。

這是個常見的程式碼,但卻是沒有用。在 Windows Form 的 API 中有 Application.DoEvents() 可以呼叫。WPF 中沒有類似的嗎?

範例2:使用Windows Form的 DoEvents

原來,WPF 中雖然沒有類似的 api 可以呼叫,但仍可以直接呼叫 Windows Form 的 Application.DoEvents.當然,需要引用 System.Windows.Forms.dll。

Ex2WinformDoEvents.xaml

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System.Threading;
using System.Windows;
using swf = System.Windows.Forms;
 
namespace WpfApplication10
{
   public partial class Ex2WinformDoEvents : Window
   {
     public Ex2WinformDoEvents()
     {
       InitializeComponent();
     }
 
     private void btnStart_Click( object sender, RoutedEventArgs e)
     {
       lblMsg.Content = "Starting..." ;
       swf.Application.DoEvents();
       Thread.Sleep(3000); //執行長時間工作
       lblMsg.Content = "Doing..." ;
       swf.Application.DoEvents();
       Thread.Sleep(3000); //執行長時間工作
       lblMsg.Content = "Finished..." ;
     }
   }
}

在更新UI後,呼叫 swf.Application.DoEvents(),就可以更新 UI 了。這樣的方式與之前的 VB6是一模一樣的手法。

範例3:WPF DoEvents

哦?WPF 沒有 DoEvents 可以使用,只能呼叫老前輩Windows Form 的 API 嗎?也不是。在 DispacherFrame 文章中就有sample.

Ex3WPFDoEvents.xaml.cs

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using System;
using System.Security.Permissions;
using System.Threading;
using System.Windows;
using System.Windows.Threading;
 
namespace WpfApplication10
{
   public partial class Ex3WPFDoEvents : Window
   {
     public Ex3WPFDoEvents()
     {
       InitializeComponent();
     }
 
     private void btnStart_Click( object sender, RoutedEventArgs e)
     {
       lblMsg.Content = "Starting..." ;
       DoEvents();
       Thread.Sleep(3000); //執行長時間工作
       lblMsg.Content = "Doing..." ;
       DoEvents();
       Thread.Sleep(3000); //執行長時間工作
       lblMsg.Content = "Finished..." ;
     }
 
     [SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
     public void DoEvents()
     {
       var frame = new DispatcherFrame();
       Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
           new DispatcherOperationCallback(ExitFrame), frame);
       Dispatcher.PushFrame(frame);
     }
 
     public object ExitFrame( object f)
     {
       ((DispatcherFrame)f).Continue = false ;
 
       return null ;
     }
 
     public static void DoEvents2()
     {
       Action action = delegate { };
       Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Input, action);
     }
   }
}

DoEvents() 與 DoEvents2() 的效果相同。

DoEvents is Evil

DoEvents 這麼好用,為什麼WPF還要發明 Dispatcher,或 Windows Form 的 BeginInvoke 這些 API  呢?

跑以下的程式碼看看吧。

?
1
2
3
4
5
6
7
8
private void btnEvil_Click( object sender, RoutedEventArgs e)
{
   for ( int i = 0; i < 10000000; i++)
   {
     System.Windows.Forms.Application.DoEvents();
   }
   MessageBox.Show( "Ok" );
}

執行時,記得打開工作管理員看看CPU 的負載,會持續飆高一斷時間。雖然 UI 沒有任何的更新,為何 CPU 會飆高呢?

DoEvent 的原理是execution loop,也就是以迴圈的方式來查詢是否有要更新的訊息(message)。一看到迴圈,各位看倌就知道是怎麼回事了吧。

範例3中的WPF 的 DoEvents 也是相同的道理。

範例4:BackgroundWorker

有沒有較正常的方式來更新 UI 呢?看一下Ex1Bad.xaml.cs的設計方式吧。更新lblMessage後執行一段工作,這基本上是同步的寫作方式。在 UI Thread 上執行工作,本來就會使得 UI 停頓,使用者感到不方變。

正確的方式,是使用BackgroundWorker來執行長時間的工作,並以非同步的方式更新在 UI Tread 上的UI內容。

Ex4BackgroundWorker.xaml.cs

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using System;
using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
 
namespace WpfApplication10
{
   public partial class Ex4BackgroundWorker : Window
   {
     public Ex4BackgroundWorker()
     {
       InitializeComponent();
     }
 
     private void btnStart_Click( object sender, RoutedEventArgs e)
     {
       ExecuteLongTimeWork(lblMsg, "Starting" );
       ExecuteLongTimeWork(lblMsg, "Doing" );
       ExecuteLongTimeWork(lblMsg, "Finished" );
     }
 
     private void ExecuteLongTimeWork(Label label, string message)
     {
       var backgroundWorker = new BackgroundWorker();
       backgroundWorker.DoWork += (s, o) => {
         Thread.Sleep(3000); //執行長時間工作
       };
 
       backgroundWorker.RunWorkerCompleted += (s, args) =>
                                                {
                                                  Dispatcher.BeginInvoke( new Action(() =>
                                                  {
                                                    label.Content = message;
                                                  }));
                                                };
       backgroundWorker.RunWorkerAsync();
     }
   }
}

BackgroundWorker 工作方式,是建立一個新的 Thread 來執行 DoWork 的event handler,執行完畢後,再執行 RunWorkerCompleted 的event handler。因此,我們需要的是在RunWorkerCompleted 的event handler 中更新的 UI。

更新 UI 時,又使用 Dispatcher 來更新 UI。基本上,Dispatcher 是一個 Queue 的概念,凡是使用 Dispatcher 來更新 UI,WPF 會照 DispatcherPriority 的優先順序來更新 UI。

結論

雖然只是小小的UI 更新方式,也是有不少的學問呢!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在WPF,界面元素是通过主线程更新的,因此在使用线程更新UI时必须将操作转移到主线程。这是因为WPF是基于Windows消息循环的,任何对UI更新必须在主线程上进行。 为了在后台线程更新UI,可以使用Dispatcher类的Invoke或BeginInvoke方法。这两个方法允许我们将任务调度到UI线程上执行。 Invoke方法会阻塞当前线程,直到UI线程执行完毕,而BeginInvoke方法则是异步执行,不会阻塞当前线程。使用这两个方法,可以将UI更新代码包装在委托,然后将其传递给Dispatcher对象,以便在UI线程上执行。 例如,以下是一个在后台线程更新UI的示例: ```csharp using System; using System.Threading; using System.Windows; using System.Windows.Controls; namespace WPFThreadUpdateUI { public partial class MainWindow : Window { private Thread workerThread; private TextBox textBox; public MainWindow() { InitializeComponent(); } private void StartButton_Click(object sender, RoutedEventArgs e) { workerThread = new Thread(WorkerThreadMethod); workerThread.Start(); } private void WorkerThreadMethod() { // 后台线程更新UI Dispatcher.Invoke(() => { textBox = new TextBox(); textBox.Text = "UI更新成功"; mainGrid.Children.Add(textBox); }); } } } ``` 在上述示例,当点击"Start"按钮时,会创建一个后台线程并执行WorkerThreadMethod方法。在方法,通过Dispatcher.Invoke方法将UI更新的代码封装在委托,然后传递给Dispatcher对象以在UI线程上执行。这样就可以在后台线程更新UI。 需要注意的是,当使用Dispatcher.Invoke或BeginInvoke方法更新UI时,一定要确保在访问UI元素之前检查调用线程与UI线程之间的关系,以避免线程冲突和UI更新问题。 ### 回答2: 在WPFWindows Presentation Foundation)UI更新通常需要在UI线程上执行。这是因为UI元素和控件只能在其创建线程上进行更新,如果在其他线程上尝试更新UI,将会引发异常。 要在线程更新UI,可以使用Dispatcher对象的Invoke或BeginInvoke方法。Dispatcher对象提供了对UI线程的访问,这样就可以在其他线程UI线程发送消息并更新UI。 示例代码如下: ```csharp // 在其他线程更新UI private void UpdateUI() { Dispatcher.Invoke(() => { // 在UI线程上执行更新操作 // 这里可以更新UI元素或控件的属性、内容等 }); } // 启动一个新线程,并在其更新UI private void StartUpdateThread() { Thread updateThread = new Thread(() => { // 执行其他任务... // 更新UI UpdateUI(); // 执行其他任务... }); updateThread.Start(); } ``` 在上述示例,我们首先创建了一个新的线程`updateThread`,然后在其调用`UpdateUI`方法,它通过`Dispatcher.Invoke()`方法将UI更新操作发送到UI线程。由于采用了Invoke方法,因此更新操作会等待UI线程完成操作后才会继续执行。 通过调用`StartUpdateThread`方法,我们可以在其他线程启动一个新的线程来更新UI。 总结而言,通过使用Dispatcher对象的Invoke或BeginInvoke方法,可以在WPF的其他线程更新UI。这确保了UI更新操作的线程安全性,并避免了潜在的异常情况。 ### 回答3: 在WPF,要更新UI,我们必须确保所有对UI元素的更改都发生在主线程上,这是因为WPFUI元素是线程的单元,只能由创建它的线程进行访问和修改。如果我们试图在非主线程上更新UI,可能会导致异常或者无法正确更新UI。 为了在线程之间更新UI,我们可以使用Dispatcher对象。Dispatcher是WPF应用程序的一个功能强大的工具,它允许我们在主线程上执行操作,包括更新UI元素。 我们可以通过调用Dispatcher对象的Invoke或BeginInvoke方法来跨线程更新UI。Invoke方法是同步的,会阻塞非主线程直到UI更新完成,而BeginInvoke方法是异步的,允许非主线程继续执行而无需等待UI更新完成。 例如,我们可以使用以下代码在非主线程上更新UI元素的内容: Dispatcher.Invoke(() => { // 在主线程上更新UI的代码 label1.Content = "更新后的内容"; }); 通过这种方式,我们可以安全地在WPF的任何线程上更新UI,而无需担心线程冲突或异常。然而,需要注意的是,频繁地在非主线程上更新UI可能会导致性能下降,因此我们应该尽量减少跨线程更新UI的需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值