1. 程式人生 > >wpf DoEvents

wpf DoEvents

原文: wpf DoEvents

如果在執行一段卡UI的程式碼,這時如何讓UI響應。如果存在程式碼需要獲得依賴屬性,那麼程式碼就需要在UI執行緒執行,但是這時就會卡UI,為了讓UI響應,所以就需要使用DoEvents來讓UI響應。 首先需要知道,DoEvents是在 WinForm 有的,在 WPF 沒有這個函式,但是可以自己寫出來。

目錄
  1. 用法
  2. 原理
  3. 存在的坑
    1. OnLoad 上其他坑
    2. 使用 DispatcherTimer 出現視窗凍結
  4. 推薦方法

先做一個例子讓大家知道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.InvokeDoEvents,然後執行拖動視窗,這時視窗卡死

        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 ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。如有任何疑問,請 與我聯絡