本文将用一个进度条控件作为例子来介绍WPF的多线程,进度条的例子可以较全面的让我们认识WPF多线程的特点。
当用户在我们的应用程序下载东西或者加载大量数据的时候,不可避免需要用户等待一段较长的时间,这时候,我们需要一个进度条来实时反映进度给用户,避免用户以为程序死机而进行的一系列蜜汁操作。
那么,先让我们上一个进度条吧。
XAML代码:
<Grid Margin="100,0">
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition Height="50" />
Grid.RowDefinitions>
<ProgressBar
Name="ProgressBar"
Grid.Row="0"
Width="580"
Height="30"
Maximum="100"
Minimum="0" />
<DockPanel Grid.Row="1" LastChildFill="False">
<Button
Width="100"
Height="30"
Click="Download_OnClick"
Content="Download"
DockPanel.Dock="Left" />
DockPanel>
Grid>
有多种方法可以控制进度条的进度,但本文中主要介绍具有代表性的4种。用来介绍如何在WPF中使用多线程。
后台代码:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
///
/// Download按钮点击事件
///
///
///
private void Download_OnClick(object sender, RoutedEventArgs e)
{
for (int i = 1; i <= 100; i++)
{
ProgressBar.Value = i;
Thread.Sleep(100);
}
}
}
这种方法实现进度条显然是失败的,因为在UI线程下执行,方法运行时UI线程卡死,只有当方法运行结束后才会反映到画面。
所以,你会看到进度条从0突然变成100,看不到中间过程。
使用UI线程会导致程序卡死,大家很容易想到,那就用线程控制呗。这样就不会影响UI线程了。
没错,此时只能使用多线程来实现,因此就有了下面这段代码。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
///
/// Download按钮点击事件
///
///
///
private void Download_OnClick(object sender, RoutedEventArgs e)
{
Task task = new Task(TaskMethod);
task.Start();
}
private void TaskMethod()
{
for (int i = 1; i <= 100; i++)
{
ProgressBar.Value = i;
Thread.Sleep(50);
}
}
}
使用上面这段代码无疑会运行异常导致程序结束,因为在新线程种并没有进度条的对象,强行调用线程中找不到的对象会导致报错。因此,不要试图在Task线程中访问应用程序对象。
想要在线程中操作应用程序对象,可以使用当前应用程序的Dispatcher
对象。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
///
/// Download按钮点击事件
///
///
///
private void Download_OnClick(object sender, RoutedEventArgs e)
{
Task task = new Task(TaskMethod);
task.Start();
}
private void TaskMethod()
{
for (int i = 1; i <= 100; i++)
{
Thread.Sleep(50);
Dispatcher.BeginInvoke((ThreadStart)delegate
{
ProgressBar.Value = i;
}, DispatcherPriority.Normal);
}
}
}
获取当前程序的Dispatcher后,使用 BeginInvoke(DispatcherPriority, Delegate)
执行异步方法,在异步方法中会调用委托来实现在其他线程中调用应用程序对象。
该方法有两个参数:
DispatcherPriority
:用于表示该线程的优先级,一般不怎么用,优先级高的先执行。
Delegate
:委托,BeginInvoke()方法会将传入的方法安排为调度程序的任务。之后调度程序会执行这个方法。
使用Dispatcher当然是合适的做法,成功的实时表示进度条的进度。我们也学会了如何在线程中调用应用程序对象。
不过,以上三种方法都弱爆了。实际上,使用BackgroundWorker组件可以近乎完美的匹配进度条的功能。
该属性设为True时,才会触发进度更新方法。
该属性设为True时,才会触发取消方法。
将需要异步执行的方法,添加进DoWork
中,BackgroundWorker
工作时会触发该事件。
将进度更新时执行的方法添加进ProgressChanged
中,当WorkerReportsProgress = True
时,进度更新时会触发该事件。
将工作结束时的反馈方法添加进RunWorkerCompleted
中,工作结束时会触发该事件。
object argument
:传入一个对象,使其可以在异步线程中使用。
调用该方法开始执行异步操作。
int percentProgress
:传入一个数值表示当前进度
调用该方法触发RunWorkerCompleted
事件。
代码如下:
为了方便使用,可以把BackgroundWorker
作为资源放进窗口的Resources
里。
<Window
x:Class="ThreadDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:componentModel="clr-namespace:System.ComponentModel;assembly=System"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ThreadDemo"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="300"
mc:Ignorable="d">
<Window.Resources>
<componentModel:BackgroundWorker
x:Name="Worker"
x:Key="Worker"
DoWork="Worker_OnDoWork"
ProgressChanged="Worker_OnProgressChanged"
RunWorkerCompleted="Worker_OnRunWorkerCompleted"
WorkerReportsProgress="True"
WorkerSupportsCancellation="True" />
Window.Resources>
<Grid Margin="100,0">
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition Height="50" />
Grid.RowDefinitions>
<ProgressBar
Name="ProgressBar"
Grid.Row="0"
Width="580"
Height="30"
Maximum="100"
Minimum="0" />
<DockPanel Grid.Row="1" LastChildFill="False">
<Button
Width="100"
Height="30"
Click="Download_OnClick"
Content="Download"
DockPanel.Dock="Left" />
<Button
Width="100"
Height="30"
Click="Cancel_OnClick"
Content="Cancel"
DockPanel.Dock="Right" />
DockPanel>
Grid>
Window>
public partial class MainWindow : Window
{
private BackgroundWorker worker;
public MainWindow()
{
InitializeComponent();
worker = (BackgroundWorker)FindResource("Worker");
}
///
/// Download按钮点击事件
///
///
///
private void Download_OnClick(object sender, RoutedEventArgs e)
{
worker?.RunWorkerAsync(ProgressBar);
}
///
/// Cancel按钮点击事件
///
///
///
private void Cancel_OnClick(object sender, RoutedEventArgs e)
{
worker?.CancelAsync();
}
///
/// 线程工作方法
///
///
///
private void Worker_OnDoWork(object sender, DoWorkEventArgs e)
{
for (int i = 1; i <= 100; i++)
{
if (worker.CancellationPending)
{
e.Cancel = true;
return;
}
worker.ReportProgress(i);
Thread.Sleep(100);
}
}
///
/// 进度改变方法
///
///
///
private void Worker_OnProgressChanged(object sender, ProgressChangedEventArgs e)
{
ProgressBar.Value = e.ProgressPercentage;
}
///
/// 工作完成方法
///
///
///
private void Worker_OnRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
MessageBox.Show("已取消");
return;
}
MessageBox.Show("下载完成");
}
}
如代码所示,
本例中我们在构造方法里初始化BackgroundWorker
对象。
在Download按钮点击事件中借助BackgroundWorker的RunWorkerAsync(object argument)
方法运行异步线程。
注意:Dowork中添加的方法将会在异步线程中执行。因此切记在Dowork事件中不能调用应用程序对象。
不过需要在Dowork
事件中调用的对象可以作为参数传入。以实现在线程中调用。
在Dowork
事件中可以随时调用BackgroundWorker
的ReportProgress(int percentProgress)
方法来反馈进度到窗体。
该方法被调用时会触发Worker_OnProgressChanged()
方法,实时更新窗口反馈进度给用户,因为该方法处于UI线程中,所以可以任意调用应用程序对象。
DoWork
事件执行结束后会触发RunWorkerCompleted
事件,表示线程执行结束,在该方法中可以使用RunWorkerCompletedEventArgs
参数判断线程执行状态是正常结束或者被取消等等,来决定如何更新窗体。
调用BackgroundWorker
的CancelAsync()
方法可以随时结束线程。
虽说是结束线程,但其实他并不会自动结束,而是将BackgroundWorker的CancellationPending
属性设置为True
,标记线程为取消状态。
开发者应在DoWork
方法中自行判断CancellationPending
属性来决定是否要结束线程。需要注意的是,设为取消状态后依然会触发RunWorkerCompleted
事件,
需要将DoWorkEventArgs
的Cancel
属性设置为True
,将状态传递到CancellationPending
事件中,最终在结束方法里判断该属性已决定如何反馈结果到窗体。
以上,使用BackgroundWorker可以十分方便的实现进度条。当然除此之外,可以使用该类实现任何异步方法,同时反馈进度以及随时取消。
对你有帮助吗?点个赞吧~