WPF

您所在的位置:网站首页 unity下载卡住不下 WPF

WPF

2023-08-11 09:36| 来源: 网络整理| 查看: 265

一、问题的发现

在做项目的过程中,只要有延迟的代码UI界面就会卡住,比如各种网络连接和Thread睡眠等。虽然这样的代码并不多,但整个系统做下来出现这样问题的交互还真不少,对于多线程小白的我来说并不懂的如何去解决这样的问题,只能任由其交互体验差。

但最近实在是忍无可忍,便花了几天的时间来解决这个问题,以下是解决该问题的方法和从UI界面卡住的问题延伸的多线程探索,其中包括自定义加载控件的制作并且应用到实际界面中,也就是在界面“卡住”期间启用加载控件,Dispatcher的使用,BackgroundWorker类的使用,同步和异步等。

注:本文将大量使用到委托和Lambda表达式。

二、UI界面卡住的解决方法

当代码中出现Thread.Sleep(4000);这样的让程序睡眠的代码时,程序就会等待睡眠后再运行下一行代码,在WPF中,也就意味着UI界面会被卡住不能进行其他操作,这是关于UI主线程之类的知识点,这里不做过多扩展。解决方法就是将睡眠代码写到另一个线程中,让UI线程得以脱身。

这里用到Task类,参考以下代码:

Task task = new Task(() => { Thread.Sleep(4000); MessageBox.Show("延迟4秒"); }); task.Start();

或者可以用Task的静态属性Factory直接进行异步调用:

Task.Factory.StartNew(() => { Thread.Sleep(4000); MessageBox.Show("延迟4秒"); });

以上也就基本解决了我们原先的问题。但我想把提示的消息窗口换成TextBox,这样可以在线程进行时提示一个“加载中”,或者其他什么信息时,又出现了“调用线程无法访问此对象,因为另一个线程拥有该对象。”的异常:

原因也很明显,TextBox是UI主线程下创建的实例,另一个线程不能直接访问并赋值。那有没有间接访问的方法呢,当然有。

三、Dispatcher的使用

Dispatcher是WPF管理控件线程的方式,应用到这里就可以实现“间接”的在另一个线程中访问TextBox并赋值,参考以下代码:

Task.Factory.StartNew(() => { this.Dispatcher.Invoke(new Action(() => { txtTestThread.Text = "另一个线程开启中"; })); Thread.Sleep(4000); this.Dispatcher.Invoke(new Action(() => { txtTestThread.Text = "线程结束,延迟4秒"; })); });

效果动图:

Dispatcher有两个方法,Invoke和BeginInvoke,网上的说法是:Invoke是同步调用,直到UI线程实际执行完该委托它才返回,而BeginInvoke是异步调用,会立即返回。光说概念是不会懂的,下面用代码来说明两个方法的区别:

//BeginInvoke方法和Invoke方法的区别 //控制台会等到4秒后才输出test new Thread(() => { Application.Current.Dispatcher.Invoke(new Action(() => { Thread.Sleep(4000); }), null); Console.WriteLine("test"); }).Start(); //控制台在一开始就输出test new Thread(() => { Application.Current.Dispatcher.BeginInvoke(new Action(() => { Thread.Sleep(4000); }), null); Console.WriteLine("test"); }).Start();

很明显就可以看出两个方法的区别,Invoke会在委托中的Sleep执行完后才输出test,所以它是同步的。而BeginInvoke不管委托中会不会Sleep都直接执行下一条语句,也就是输出test,所以它是异步的。但在我们这个给TextBox赋值的案例中,Dispatcher中并没有延迟的代码,所以不管是Invoke还是BeginInvoke效果都一样。

回到我们的案例中,仔细看其实可以发现这段是由三部分组成:

Task.Factory.StartNew(() => { //第一部分 this.Dispatcher.Invoke(new Action(() => { txtTestThread.Text = "另一个线程开启中"; })); //第二部分,这里模拟延迟操作 Thread.Sleep(4000); //第三部分 this.Dispatcher.Invoke(new Action(() => { txtTestThread.Text = "线程结束,延迟4秒"; })); });

每一部分都是一个独立的方法,特别是第二部分,一般来说也不会是一句Sleep,而是一段有延迟的代码,所以这里用方法调用会比较好。

来到这里基本也没啥问题了,但我想让延迟的秒数展示在TextBox中,TextBox像读秒一样显示延迟了多少时间,这个该怎么解决呢。这里就要引进另一个知识点了:BackgroundWorker类。

四、BackgroundWorker类的使用

BackgroundWorker类允许在单独的线程上执行某个可能导致用户界面(UI)停止响应的耗时操作,并且想要一个响应式的UI来反应当前耗时操作的进度。是不是跟我当前的需求一模一样,通过BackgroundWorker类来改进案例中代码:

BackgroundWorker bgMeet; private void btnTestThread_Click(object sender, RoutedEventArgs e) { bgMeet = new BackgroundWorker(); bgMeet.WorkerReportsProgress = true; bgMeet.DoWork += new DoWorkEventHandler(bgMeet_DoWork); bgMeet.ProgressChanged += new ProgressChangedEventHandler(bgMeet_ProgressChanged); bgMeet.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bgMeet_RunWorkerCompleted); bgMeet.RunWorkerAsync(); } void bgMeet_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { this.Dispatcher.Invoke(new Action(() => { this.txtTestThread.Text = "完成"; })); } void bgMeet_ProgressChanged(object sender, ProgressChangedEventArgs e) { this.Dispatcher.Invoke(new Action(() => { this.txtTestThread.Text = e.ProgressPercentage.ToString(); })); } void bgMeet_DoWork(object sender, DoWorkEventArgs e) { GetData(); } public void GetData() { for (int i = 0; i < 11; i++) { bgMeet.ReportProgress(i); Thread.Sleep(1000); } }

将Task理解成BackgroundWorker,原案例代码中第一部分理解成bgMeet_DoWork事件(这里为空),第二部分理解成GetData方法,第三部分理解成bgMeet_RunWorkerCompleted事件,中间添加一个监听数据改变的bgMeet_ProgressChanged事件就完成我们想要的需求。

以下是动图效果:

到这里我并不满意,我想要有一个加载控件,在调用线程的这段时间同时启动加载控件,并跟随线程结束而结束。

五、自定义加载控件

网络上关于自定义加载控件的代码有很多,我找了一个比较简单,只需对一个图标进行一个角度上的动画就可以实现加载的动画效果。

新建用户控件,取名BusyBox,用户控件代码:

public partial class BusyBox : UserControl { public BusyBox() { InitializeComponent(); } public static readonly DependencyProperty IsActiveProperty = DependencyProperty.Register("IsActive", typeof(bool), typeof(BusyBox), new PropertyMetadata(false)); /// /// 是否启用 /// public bool IsActive { get { return (bool)GetValue(IsActiveProperty); } set { SetValue(IsActiveProperty, value); } } static BusyBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(BusyBox), new FrameworkPropertyMetadata(typeof(BusyBox))); } }

新建资源字典,给控件添加样式:

添加到界面中:

回到案例中,将加载控件应用到案例中:

BackgroundWorker bgMeet; private void btnTestThread_Click(object sender, RoutedEventArgs e) { bgMeet = new BackgroundWorker(); bgMeet.WorkerReportsProgress = true; bgMeet.DoWork += new DoWorkEventHandler(bgMeet_DoWork); bgMeet.ProgressChanged += new ProgressChangedEventHandler(bgMeet_ProgressChanged); bgMeet.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bgMeet_RunWorkerCompleted); bgMeet.RunWorkerAsync(); } void bgMeet_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { busyTest.IsActive = false; this.Dispatcher.Invoke(new Action(() => { this.txtTestThread.Text = "完成"; })); } void bgMeet_ProgressChanged(object sender, ProgressChangedEventArgs e) { this.Dispatcher.Invoke(new Action(() => { this.txtTestThread.Text = e.ProgressPercentage.ToString(); })); } void bgMeet_DoWork(object sender, DoWorkEventArgs e) { this.Dispatcher.Invoke(new Action(() => { busyTest.IsActive = true; })); GetData(); } public void GetData() { for (int i = 0; i < 11; i++) { bgMeet.ReportProgress(i); Thread.Sleep(1000); } }

动图效果:

至此,已经非常接近最终的需求效果了。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3