wpf DoEvents
如果在執行一段卡UI的程式碼,這時如何讓UI響應。如果存在程式碼需要獲得依賴屬性,那麼程式碼就需要在UI執行緒執行,但是這時就會卡UI,為了讓UI響應,所以就需要使用DoEvents
來讓UI響應。 首先需要知道,DoEvents
是在 WinForm 有的,在 WPF 沒有這個函式,但是可以自己寫出來。
先做一個例子讓大家知道DoEvents
<Window x:Class="ZuindmMbx.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ZuindmMbx"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ListView ItemsSource="{Binding KatudefZubpobryk}">
<ListView.ItemTemplate>
<DataTemplate>
< TextBlock Text="{Binding}"></TextBlock>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button Content="確定" HorizontalAlignment="Left" Margin="424,292,0,0" VerticalAlignment="Top" Width="75" Click="Button_OnClick"/>
</Grid>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
public ObservableCollection<string> KatudefZubpobryk { get; set; } = new ObservableCollection<string>();
private void Button_OnClick(object sender, RoutedEventArgs e)
{
for (int i = 0; i < 10; i++)
{
Foo(10);
KatudefZubpobryk.Add(i.ToString());
}
}
private void Foo(int n)
{
for (int i = 0; i < n; i++)
{
Foo(n - 1);
}
}
}
這時點選確定可以看到,需要等待一些時間才可以響應介面
如果加上了 DoEvents 就可以看到下圖的效果
用法
在呆磨的程式做一些修改,請看程式碼
private void Button_OnClick(object sender, RoutedEventArgs e)
{
for (int i = 0; i < 10; i++)
{
Foo(10);
KatudefZubpobryk.Add(i.ToString());
DoEvents();
}
}
public static void DoEvents()
{
DispatcherFrame frame = new DispatcherFrame();
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(ExitFrame), frame);
Dispatcher.PushFrame(frame);
}
private static Object ExitFrame(Object state)
{
((DispatcherFrame) state).Continue = false;
return null;
}
所以只需要在迴圈加上程式碼就可以了。可以複製下面的兩個方法到需要使用讓UI響應的地方,在需要的地方呼叫,使用的方法很簡單。
建議在下面的地方使用:
- 後臺操作比較耗時,未完全載入也能正常使用
- 效能已經沒有辦法優化
- 效能沒有時間優化,可作為臨時性方案
- DoEvents建議一定是在主執行緒上使用
原理
請看一下底層的PushFrameImpl
下面的程式碼有刪減
會導致UI重繪的訊息:0xC25A及0xC262 所以傳送這個訊息就可以讓UI響應
存在的坑
這裡的坑是 PushFrame 的坑,關於他的原理,請看 https://walterlv.github.io/post/dotnet/2017/09/26/dispatcher-push-frame.html
如果點選確定按鈕之後,再次點選確定按鈕,那麼就會出現很多個重複的數。如果使用這個方法,那麼需要禁用確定按鈕,小心使用者多次點選。
在使用方法的時候拖動視窗,可能讓視窗卡死。
復現步驟:
修改上面呆磨程式碼,加上OnLoaded
,裡面使用Dispatcher.Invoke
或DoEvents
,然後執行拖動視窗,這時視窗卡死
public MainWindow()
{
InitializeComponent();
DataContext = this;
Loaded += OnLoaded;
}
private async void OnLoaded(object sender, RoutedEventArgs e)
{
await Task.Delay(2000);
Dispatcher.Invoke(() => { }, DispatcherPriority.Background);
}
但是這時使用 Alt+Tab 到其他視窗,然後回來,可以看到視窗正常
實際上嘗試改變視窗大小也會讓視窗卡死,請看WPF application intermittently hangs when using Dispatcher.Invoke and/or Dispatcher.PushFrame while user is resizing or draging window
OnLoad 上其他坑
我必須說,不僅是 OnLoad 會出現這些坑,在很多情況也會,但是我還不知道條件。
請把await Task.Delay(2000)
換為Foo(10);
進行一些計算,這時在軟體啟動的時候,嘗試拖動視窗,可以看到視窗是沒有顯示內容,但是滑鼠放開的時候,就可以看到介面顯示。
private void OnLoaded(object sender, RoutedEventArgs e)
{
Foo(10);
Dispatcher.Invoke(() =>
{
}, DispatcherPriority.Background);
}
接著把Invoke
換為DoEvents
,結果相同,在啟動拖動視窗,視窗沒有內容。
使用 DispatcherTimer 出現視窗凍結
下面的程式碼是建立一個 time 不停在裡面使用Dispatcher.Invoke
public MainWindow()
{
InitializeComponent();
DataContext = this;
Loaded += OnLoaded;
DispatcherTimer time = new DispatcherTimer();
time.Interval = new TimeSpan(0, 0, 1);
time.Tick += Time_Tick;
time.Start();
}
private void Time_Tick(object sender, EventArgs e)
{
Foo(10);
Dispatcher.Invoke(() => { }, DispatcherPriority.Background);
}
這時拖動視窗會出現凍結,和上面一樣。
實際把上面程式碼的運算去掉也會凍住,但是我嘗試10次,有2次在放開的時候才凍住。
推薦方法
實際上垃圾wr是不是要讓開發者去寫這樣的方法?實際上垃圾wr已經做了這個東西,但是沒有直接告訴開發者,請嘗試使用下面的程式碼代替上面呆磨
private void Button_OnClick(object sender, RoutedEventArgs e)
{
for (int i = 0; i < 10; i++)
{
Foo(10);
KatudefZubpobryk.Add(i.ToString());
Dispatcher.Invoke(() => { }, DispatcherPriority.Background);
}
}
關鍵就是Dispatcher.Invoke(() => { }, DispatcherPriority.Background);
,這句程式碼就是在主執行緒插入一個Background
因為優先順序,所以這時就可以讓UI處理其他的輸入
但是直接使用Dispatcher.Invoke
程式碼太長,是不是可以使用比較簡單的?實際上還是有的,請看程式碼。
private async void Button_OnClick(object sender, RoutedEventArgs e)
{
for (int i = 0; i < 10; i++)
{
Foo(10);
KatudefZubpobryk.Add(i.ToString());
await System.Windows.Threading.Dispatcher.Yield();
}
}
實際上System.Windows.Threading.Dispatcher.Yield
這個方法的實現和Dispatcher.Invoke(() => { }, DispatcherPriority.Background
一點也不同,他使用的是 async 以及其他我還不知道怎麼說的科技。
最後的方法是在UI主執行緒執行的函式上新增async
和直接使用Dispatcher.Yield
就可以在迴圈中讓UI響應。不會在迴圈中讓UI卡住。
建議使用最後的方法,因為這個方法可以解決坑,而且使用簡單
實際上,使用了上面無論哪個方法都不會讓介面一直都響應,如果頁面有一個迴圈的動畫,就可以看到動畫播放實際上有些卡,下面寫一個呆磨就可以知道。在上面的介面新增下面的程式碼,不停做動畫。
<Grid>
<Grid.Triggers>
<EventTrigger RoutedEvent="Grid.Loaded">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever">
<DoubleAnimation Storyboard.TargetName="T" Storyboard.TargetProperty="Angle" From="0" To="360" Duration="0:0:1"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
<Grid x:Name="G" Background="#565656" Width="200" Height="200"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid.RenderTransform>
<RotateTransform x:Name="T" CenterX="100" CenterY="100" Angle="0"></RotateTransform>
</Grid.RenderTransform>
</Grid>
</Grid>
這時點選按鈕,可以看到動畫有些卡,點選視窗拖動就可以看到動畫正常。
本文會經常更新,請閱讀原文: https://lindexi.gitee.io/lindexi/post/wpf-DoEvents.html ,以避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗。
本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名林德熙(包含連結: https://lindexi.gitee.io ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。如有任何疑問,請 與我聯絡 。