天天看點

拒絕卡頓——在WPF中使用多線程更新UI

有經驗的程式員們都知道:不能在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);

async

void updateTime()

            while (true)

            {

                Thread.Sleep(900);            //模拟耗時操作

                textBlock.Text = DateTime.Now.ToString();

                await

Task.Delay(100);

            }

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

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

    public UserControl1()

        textBlock = new

        this.Content = textBlock;

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

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

拒絕卡頓——在WPF中使用多線程更新UI

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

    private

        while (true)

            await

Task.Run(() => Thread.Sleep(900));

            textBlock.Text = DateTime.Now.ToString();

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

        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

            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;

int VisualChildrenCount

            get { return 1; }

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

        createChildInNewThread<UserControl1>(this);

    void createChildInNewThread<T>(ContentControl container)

        where

T : UIElement , new()

        container.Content = content;

T();