一、问题的发现
在做项目的过程中,只要有延迟的代码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));
/// <summary>
/// 是否启用
/// </summary>
public bool IsActive
{
get { return (bool)GetValue(IsActiveProperty); }
set { SetValue(IsActiveProperty, value); }
}
static BusyBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(BusyBox), new FrameworkPropertyMetadata(typeof(BusyBox)));
}
}
新建资源字典,给控件添加样式:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestWPF.CustomControl">
<Style TargetType="{x:Type local:BusyBox}">
<!--<Setter Property="Foreground" Value="{StaticResource TextForeground}"></Setter>-->
<Setter Property="Width" Value="32"></Setter>
<Setter Property="Height" Value="32"></Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:BusyBox}">
<Grid VerticalAlignment="Center" HorizontalAlignment="Center" >
<Viewbox Stretch="Uniform" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="" x:Name="FIcon" FontSize="36" FontFamily="/icon/#iconfont" RenderTransformOrigin="0.5,0.5"
Foreground="{TemplateBinding Foreground}">
<TextBlock.RenderTransform>
<RotateTransform x:Name="TransFIcon" Angle="0"/>
</TextBlock.RenderTransform>
</TextBlock>
</Viewbox>
</Grid>
<ControlTemplate.Triggers>
<!--激活状态-->
<Trigger Property="IsActive" Value="true">
<Setter Property="Visibility" Value="Visible" TargetName="FIcon"/>
<Trigger.EnterActions>
<BeginStoryboard >
<Storyboard >
<DoubleAnimation RepeatBehavior="Forever" Storyboard.TargetName="TransFIcon"
Storyboard.TargetProperty="Angle" To="360" Duration="0:0:2.5"/>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard >
<Storyboard >
<DoubleAnimation RepeatBehavior="Forever" Storyboard.TargetName="TransFIcon"
Storyboard.TargetProperty="Angle" To="0" Duration="0"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
<!--非激活状态-->
<Trigger Property="IsActive" Value="false">
<Setter Property="Visibility" Value="Collapsed" TargetName="FIcon"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
添加到界面中:
<local:BusyBox x:Name="busyTest" Width="30" Height="30" Foreground="White" Margin="5"/>
回到案例中,将加载控件应用到案例中:
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);
}
}
动图效果:
至此,已经非常接近最终的需求效果了。