自制WPF多线程加载框

在实际项目中,有些后台操作耗时较久,这时前台最好有一个动画加载框给用户提示。
该加载框的动画显示和后台操作需要同时进行,因此需要采用多线程的方式进行实现。

UI界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<Window x:Class="TestWPF.LoadingBar"
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:TestWPF"
mc:Ignorable="d"
ResizeMode="NoResize"
WindowStyle="None"
WindowStartupLocation="CenterScreen"
Background="#E0E0E0"
SizeToContent="WidthAndHeight"
MaxWidth="200">

<StackPanel Orientation="Vertical">
<StackPanel.Resources>
<Style x:Key="rec" TargetType="Rectangle">
<Setter Property="Width" Value="13"/>
<Setter Property="Height" Value="30"/>
<Setter Property="Margin" Value="4,0"/>
<Setter Property="Fill" Value="#f1404b"/>
</Style>
<PowerEase x:Key="powerEase" Power="3" EasingMode="EaseInOut"/>
</StackPanel.Resources>

<StackPanel.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever" Storyboard.TargetProperty="Height">
<DoubleAnimation Storyboard.TargetName="rec1" To="55" BeginTime="0:0:0.0" Duration="0:0:0.2" EasingFunction="{StaticResource powerEase}" AutoReverse="True"/>
<DoubleAnimation Storyboard.TargetName="rec2" To="55" BeginTime="0:0:0.1" Duration="0:0:0.2" EasingFunction="{StaticResource powerEase}" AutoReverse="True"/>
<DoubleAnimation Storyboard.TargetName="rec3" To="55" BeginTime="0:0:0.2" Duration="0:0:0.2" EasingFunction="{StaticResource powerEase}" AutoReverse="True"/>
<DoubleAnimation Storyboard.TargetName="rec4" To="55" BeginTime="0:0:0.3" Duration="0:0:0.2" EasingFunction="{StaticResource powerEase}" AutoReverse="True"/>
<DoubleAnimation Storyboard.TargetName="rec5" To="55" BeginTime="0:0:0.4" Duration="0:0:0.2" EasingFunction="{StaticResource powerEase}" AutoReverse="True"/>
<DoubleAnimation Storyboard.TargetName="rec6" To="55" BeginTime="0:0:0.5" Duration="0:0:0.2" EasingFunction="{StaticResource powerEase}" AutoReverse="True"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</StackPanel.Triggers>

<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Height="60">
<Rectangle x:Name="rec1" Style="{StaticResource rec}"/>
<Rectangle x:Name="rec2" Style="{StaticResource rec}"/>
<Rectangle x:Name="rec3" Style="{StaticResource rec}"/>
<Rectangle x:Name="rec4" Style="{StaticResource rec}"/>
<Rectangle x:Name="rec5" Style="{StaticResource rec}"/>
<Rectangle x:Name="rec6" Style="{StaticResource rec}"/>
</StackPanel>

<TextBlock Text="{Binding Prompt}"
FontWeight="Bold"
Margin="0,2"
TextWrapping="Wrap"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Grid.ColumnSpan="3"
Grid.Row="1"/>
</StackPanel>
</Window>

后台代码

后台代码是本窗体的重点,涉及到很多编程技巧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Windows;

namespace TestWPF {
public partial class LoadingBar : Window, INotifyPropertyChanged {
private LoadingBar() {
InitializeComponent();

// 不在任务栏上显示,防止用户手动关闭
ShowInTaskbar = false;
// 设置界面提示语
DataContext = this;
Topmost = true;
}

/// <summary>
/// 静态加载框实例
/// </summary>
private static LoadingBar instance = null;

/// <summary>
/// 防止多线程同时显示加载框,上锁
/// </summary>
private static object loadingBarLock = new object();

/// <summary>
/// 设置标识符,防止多次关闭加载框
/// 初始值设置为true,可以防止用户最开始执行CloseLoadingBar()方法出错
/// </summary>
private static bool isClosed = true;

#region 静态方法
public static void ShowLoadingbar(string prompt = "处理中...") {
isClosed = false;
Thread thread = new Thread(() => {
// 尝试获取锁
bool lockTaken = false;
Monitor.TryEnter(loadingBarLock, 1000, ref lockTaken);

// 获取锁成功,显示加载框
if (lockTaken) {
if (instance == null)
// 必须在此初始化,否则会报错
instance = new LoadingBar();
instance.Prompt = prompt;
instance.ShowDialog();
// 窗体关闭,释放锁
Monitor.Exit(loadingBarLock);
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
}

public static void SetPrompt(string prompt = "处理中...") {
if (instance != null)
instance.Prompt = prompt;
}

/// <summary>
/// 防止后台线程一直执行,调用此函数彻底关闭后台线程
/// </summary>
public static void CloseLoadingBar() {
if (isClosed) return;

// 加循环,保证执行CloseLoadingBar()完成后能成功关闭窗体(***)
while (true) {
if (instance != null && instance.IsVisible) {
instance.Dispatcher.Invoke(() => {
instance.Close();
instance = null;
isClosed = true;
});
return;
}
}
}
#endregion


#region 定义可通知UI的属性
private String _prompt;
public string Prompt {
get { return _prompt; }
set {
if (_prompt != value) {
_prompt = value;
OnPropertyChanged();
}
}
}

public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}

效果

  • LoadingBar.ShowLoadingbar()可以显示加载框,且可以传入参数,用来设置加载框的提示语。
  • LoadingBar.SetPrompt()可以设置提示语。
  • LoadingBar.CloseLoadingBar()关闭加载框。

特点

  • 加锁控制,防止多个线程同时显示进度框。
  • 可设置提示语并中途修改。
  • 稳定性较好,对于以下非常规调用依然有效。
1
2
3
4
5
6
7
8
9
10
private void btn01_Click(object sender, RoutedEventArgs e) {
LoadingBar.CloseLoadingBar();
LoadingBar.ShowLoadingbar();
LoadingBar.ShowLoadingbar();
LoadingBar.ShowLoadingbar();
Thread.Sleep(2000);
LoadingBar.CloseLoadingBar();
LoadingBar.CloseLoadingBar();
LoadingBar.CloseLoadingBar();
}

评论