WPF-由UI界面卡住问题延伸的C#多线程探索

本文介绍了解决WPF应用程序中UI界面卡顿的方法,包括使用Task和BackgroundWorker进行后台任务处理,以及自定义加载控件提升用户体验。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、问题的发现

在做项目的过程中,只要有延迟的代码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="&#xeb80;" 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);
    }
}

动图效果:

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

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值