WPF 的 BackgroundWorker

最近在做 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 = "连接失败!";
    }
}

完!

参考文献

  1. Multi-threading with the BackgroundWorker.

你可能感兴趣的:(WPF 的 BackgroundWorker)