首頁 > 軟體

在WPF中使用多執行緒更新UI

2022-06-22 18:02:34

有經驗的程式設計師們都知道:不能在UI執行緒上進行耗時操作,那樣會造成介面卡頓,如下就是一個簡單的範例:

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.Dispatcher.Invoke(new Action(()=> { }));
            this.Loaded += MainWindow_Loaded;
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            this.Content = new UserControl1();
        }
    }

    class UserControl1 : UserControl
    {
        TextBlock textBlock;

        public UserControl1()
        {
            textBlock = new TextBlock();
            this.Content = textBlock;

            this.Dispatcher.BeginInvoke(new Action(updateTime), null);
        }

        private async void updateTime()
        {
            while (true)
            {
                Thread.Sleep(900);            //模擬耗時操作

                textBlock.Text = DateTime.Now.ToString();
                await Task.Delay(100);
            }
        }
    }

當我們執行這個程式的時候,就會發現:由於主執行緒大部分的時間片被佔用,無法及時處理系統事件(如滑鼠,鍵盤等輸入),導致程式變得非常卡頓,連拖動視窗都變得不流暢;

如何解決這個問題呢,初學者可能想到的第一個方法就是新啟一個執行緒,線上程中執行更新:

    public UserControl1()
    {
        textBlock = new TextBlock();
        this.Content = textBlock;

        ThreadPool.QueueUserWorkItem(_ => updateTime());
    }

但很快就會發現此路不通,因為WPF不允許跨執行緒存取程式,此時我們會得到一個:"The calling thread cannot access this object because a different thread owns it."的InvalidOperationException異常

那麼該如何解決這一問題呢?通常的做法是把耗時的函數放線上程池執行,然後切回主執行緒更新UI顯示。前面的updateTime函數改寫如下:

    private async void updateTime()
    {
        while (true)
        {
            await Task.Run(() => Thread.Sleep(900));
            textBlock.Text = DateTime.Now.ToString();
            await Task.Delay(100);
        }
    }

這種方式能滿足我們的大部分需求。但是,有的操作是比較耗時間的。例如,在多視窗實時監控的時候,我們就需要同時多十來個螢幕每秒鐘各進行幾十次的重新整理,更新影象這個操作必須在UI執行緒上進行,並且它有非常耗時間,此時又會回到最開始的卡頓的情況。

看起來這個問題無法解決,實際上,WPF只是不允許跨執行緒存取程式,並非不允許多執行緒更新介面。我們大可以對每個視訊監控視窗單獨其一個獨立的執行緒,在那個執行緒中進行更新操作,此時就不會影響到主執行緒。MSDN上有篇文章介紹了詳細的操作:Multithreaded UI: HostVisual。用這種方式將原來的程式改寫如下:

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        HostVisual hostVisual = new HostVisual();

        UIElement content = new VisualHost(hostVisual);
        this.Content = content;

        Thread thread = new Thread(new ThreadStart(() =>
        {
            VisualTarget visualTarget = new VisualTarget(hostVisual);
            var control = new UserControl1();
            control.Arrange(new Rect(new Point(), content.RenderSize));
            visualTarget.RootVisual = control;

            System.Windows.Threading.Dispatcher.Run();

        }));

        thread.SetApartmentState(ApartmentState.STA);
        thread.IsBackground = true;
        thread.Start();
    }

    public class VisualHost : FrameworkElement
    {
        Visual child;

        public VisualHost(Visual child)
        {
            if (child == null)
                throw new ArgumentException("child");

            this.child = child;
            AddVisualChild(child);
        }

        protected override Visual GetVisualChild(int index)
        {
            return (index == 0) ? child : null;
        }

        protected override int VisualChildrenCount
        {
            get { return 1; }
        }
    }

這個裡面用來了兩個新的類:HostVisual、VisualTarget。以及自己寫的一個VisualHost。MSDN上相關的解釋,也不算難理解,這裡就不多介紹了。最後,再來重構一下程式碼,把在新執行緒中建立控制元件的方式改寫如下:

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        createChildInNewThread<UserControl1>(this);
    }

    void createChildInNewThread<T>(ContentControl container)
        where T : UIElement , new()
    {
        HostVisual hostVisual = new HostVisual();

        UIElement content = new VisualHost(hostVisual);
        container.Content = content;

        Thread thread = new Thread(new ThreadStart(() =>
        {
            VisualTarget visualTarget = new VisualTarget(hostVisual);

            var control = new T();
            control.Arrange(new Rect(new Point(), content.RenderSize));

            visualTarget.RootVisual = control;
            System.Windows.Threading.Dispatcher.Run();

        }));

        thread.SetApartmentState(ApartmentState.STA);
        thread.IsBackground = true;
        thread.Start();
    }

當然,我這個函數多了一些不必要的的限制:容器必須是ContentControl,子元素必須是UIElement。可以根據實際需要進行相關修改。這裡有一個完整的範例,也可以參考一下。

到此這篇關於WPF使用多執行緒更新UI的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com