拒绝卡顿——在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。可以根据实际需要进行相关修改。

时间: 2024-12-06 02:41:37

拒绝卡顿——在WPF中使用多线程更新UI的相关文章

CleanAOP实战系列--WPF中MVVM自动更新

CleanAOP实战系列--WPF中MVVM自动更新 作者: 立地 邮箱: [email protected] QQ: 511363759 CleanAOP介绍:https://github.com/Jarvin-Guan/CleanAOP 前言 讲起WPF,开发模式MVVM是必不可少的,使用MVVM模式以后可以在View中写界面,需要使用到的数据则使用绑定的方式写到标签中,那么控制权就放到了ViewModel中,那么有一个需求是每一个使用MVVM者都会有的,就是在后台改变ViewModel的属

在WPF中减少逻辑与UI元素的耦合

原文:在WPF中减少逻辑与UI元素的耦合             在WPF中减少逻辑与UI元素的耦合 周银辉 1,    避免在逻辑中引用界面元素,别把后台数据强加给UI  一个糟糕的案例 比如说主界面上有一个显示当前任务状态的标签label_TaskState,我们会时常更新该标签以便及时地将任务状态通知用户.那么很糟糕的一种假设是我们的代码中会到处充斥着这样的语句段this.label_TaskState .Content = this.GetStateDescription(TaskSta

Android多线程更新UI的方式

Android下,对于耗时的操作要放到子线程中,要不然会残生ANR,本次我们就来学习一下Android多线程更新UI的方式. 首先我们来认识一下anr: anr:application not reponse:应用程序无响应 主线程:UI线程 anr产生的原因:主线程需要做很多重要的事情,响应点击事件,更新ui,如果在主线程里面阻塞时间过久,应用程序就会无响应,为了避免应用程序出现anr,所有的耗时的操作,都应该放在子线程中执行. 认识了anr后,我们就来学习一下怎样在Android下开启多线程

IOS开发,子线程中是不能更新UI的

今天发现一个奇怪问题,当用NSNotification,在回调函数里面对tableview进行reloadData时,并不能更新UI,而且还会导致以后都更新不了.后来查了一些资料才发现,postNotification之后调用回调函数,相当于开了一个子线程,而子线程中是不能更新UI的.所以要想在notification的回调里面更新UI,必须用 dispatch_async(dispath_get_main_queue(),^{ [tableview reloadData]; }];

WPF中的多线程(一)

首先我们先来看一个例子,用来计算一定范围内的素数个数. XAML: <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto&quo

富客户端 wpf, Winform 多线程更新UI控件

前言 在富客户端的app中,如果在主线程中运行一些长时间的任务,那么应用程序的UI就不能正常相应.因为主线程要负责消息循环,相应鼠标等事件还有展现UI. 因此我们可以开启一个线程来格外处理需要长时间的任务,但在富客户端中只有主线程才能更新UI的控件. 解决方法 简单的来说,我们需要从其他的线程来更新UI线程的控件,需要将这个操作转交给UI线程(线程marshal). 方法1: 在底层的操作中,可以有以下的方法: WPF中,在element的Dispatcher类中调用BeginInvoke或者I

多线程更新UI的常用方法

开发Winform或者WPF相关GUI程序中,遇到执行耗时程序片段,并且需要在ui界面上实时展示状态信息的问题时,为了防止界面出现假死状态,会用到多线程技术,异步跨线程访问ui界面元素:下面总结下Winform和WPF中常用的几种异步跨线程访问ui界面的技术. Winform中常用技术 1.采用BackgroundWorker控件 public partial class Form1 : Form { private BackgroundWorker worker; public Form1()

一种WPF在后台线程更新UI界面的简便方法

WPF框架规定只有UI线程(主线程)可以更新界面,所有其他后台线程无法直接更新界面.幸好,WPF提供的SynchronizationContext类以及C#的Lambda表达式提供了一种方便的解决方法.以下是代码: public static SynchronizationContext s_SC = Synchronization.Current; //主窗口类的静态成员 在App类中: static Thread s_MainThread = Thread.CurrentThread; //

WPF 修改数据后更新UI

ObservableCollection<T> 只有项添加或删除才会更新UI 要想属性发生变动后立刻更新到UI,必须继承 INotifyPropertyChanged 接口,示例如下 public class SurfaceDetail: INotifyPropertyChanged { //不更新到界面的属性 public string name { get; set; } //以下是更新到界面的属性 private string _color; public string color {