最近在做 WPF 显示和控制网络摄像机的任务。一个重要的小问题是窗口显示与后台处理的同步问题。例如,在登录(或切换状态、播放视频等)的时候,如果这么写代码:
private void BtnLogin_Click(object sender, RoutedEventArgs e)
{
bool isConnected = Login(m_IP, m_Port);
if (isConnected)
{
MessageBox.Show("登录成功!");
}
else
{
MessageBox.Show("登录失败!");
}
}
那么在登录状态返回之前,窗口将一直无法响应用户操作(如移动等)。这是因为状态显示与后台处理置于同一个线程中,这显然是不合理的。
所以,更合理的做法应该是将窗口的控制与显示,和后台数据的处理,放在两个不同的线程中异步处理。WPF 的 BackgroundWorker 可以十分简洁地实现这一点。关于 BackgroundWorker,文章 Multi-threading with the BackgroundWorker 介绍得非常好,本文基于它做一些笔录。
假设我们要在后台统计 1~10000 之间所有能被 42 整除的数字的个数,并在每次找到一个时,实时地在前端显示出来。
首先在代码开头处引用:
using System.ComponentModel;
并在事件响应函数(如 Button_Click)的主体部分加入:
BackgroundWorker worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
worker.DoWork += worker_DoWork;
worker.ProgressChanged += worker_ProgressChanged;
worker.RunWorkerCompleted += worker_RunWorkerCompleted;
worker.RunWorkerAsync(10000);
也就是对 worker 对象添加了三个事件处理函数:DoWork, ProgressChanged 和 RunWorkerCompleted。DoWork 用于后台数据处理的主要过程,并在必要时候汇报进度(ReportProgress);ProgressChanged 用于在 DoWork 汇报进度时决定做什么;RunWorkerCompleted 用于在 DoWork 结束时执行后续的处理或显示等工作。
MainWindow.xaml:
<Window x:Class="MTTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:MTTest" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525">
<DockPanel Margin="10">
<DockPanel DockPanel.Dock="Top">
<Button Name="btnDoSynchronousCalculation" Click="btnDoSynchronousCalculation_Click" DockPanel.Dock="Left" HorizontalAlignment="Left">
Synchronous (same thread)</Button>
<Button Name="btnDoAsynchronousCalculation" Click="btnDoAsynchronousCalculation_Click" DockPanel.Dock="Right" HorizontalAlignment="Right">
Asynchronous (worker thread)</Button>
</DockPanel>
<ProgressBar DockPanel.Dock="Bottom" Height="18" Name="pbCalculationProgress" />
<ListBox Name="lbResults" Margin="0,10" />
</DockPanel>
</Window>
MainWindow.cs:
using System;
using System.Windows;
using System.ComponentModel;
namespace MTTest
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void btnDoSynchronousCalculation_Click(object sender, RoutedEventArgs e)
{
int max = 10000;
pbCalculationProgress.Value = 0;
lbResults.Items.Clear();
int result = 0;
for (int i = 0; i < max; i++)
{
if (i % 42 == 0)
{
lbResults.Items.Add(i);
result++;
}
System.Threading.Thread.Sleep(1);
pbCalculationProgress.Value = Convert.ToInt32(((double)i / max) * 100);
}
MessageBox.Show("Numbers between 0 and 10000 divisible by 7: " + result);
}
private void btnDoAsynchronousCalculation_Click(object sender, RoutedEventArgs e)
{
pbCalculationProgress.Value = 0;
lbResults.Items.Clear();
BackgroundWorker worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
worker.DoWork += worker_DoWork; // background computing
worker.ProgressChanged += worker_ProgressChanged; // progress reporting
worker.RunWorkerCompleted += worker_RunWorkerCompleted; // computation completed
worker.RunWorkerAsync(10000);
}
void worker_DoWork(object sender, DoWorkEventArgs e)
{
int max = (int)e.Argument;
int result = 0;
for (int i = 0; i < max; i++)
{
int progressPercentage = Convert.ToInt32(((double)i / max) * 100);
if (i % 42 == 0)
{
result++;
(sender as BackgroundWorker).ReportProgress(progressPercentage, i);
}
else
(sender as BackgroundWorker).ReportProgress(progressPercentage);
System.Threading.Thread.Sleep(1);
}
e.Result = result;
}
void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
pbCalculationProgress.Value = e.ProgressPercentage;
if (e.UserState != null)
lbResults.Items.Add(e.UserState);
}
void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
MessageBox.Show("Numbers between 0 and 10000 divisible by 7: " + e.Result);
}
}
}
基于 BackgroundWorker,文章开头的问题就可以通过如下方式解决:
private void BtnLogin_Click(object sender, RoutedEventArgs e)
{
string param = m_IP + ":" + m_Port;
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += Login_DoWork;
worker.RunWorkerCompleted += Login_RunWorkerCompleted;
worker.RunWorkerAsync(param);
LabelLoginState.Content = "正在连接...";
private void Connect_DoWork(object sender, DoWorkEventArgs e)
{
string[] param = ((string)e.Argument).Split(':');
string ip = param[0];
string port = param[1];
m_IsConnected = Login(ip, port);
}
private void Connect_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (m_IsConnected)
{
LblConnectSate.Content = "已连接!";
}
else
{
LblConnectSate.Content = "连接失败!";
}
}
完!