首页 > 代码库 > 拒绝卡顿——在WPF中使用多线程更新UI

拒绝卡顿——在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);
????????}

????????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