WaitSpin、ProgresPanel 和线程





5.00/5 (3投票s)
WPF 进度控件和比较相关的线程方法
引言
在我的 CBR 项目 CodePlex 上,我不得不研究新的任务 并行库,以便与旧的经典线程进行比较。这导致我开发了一个无限进度条控件和一个非阻塞或不具侵入性的进度 UI。
为了分享我的工作,您将在下面找到
- 如何编写 WaitSpin 控件并在 Expression Designer 中创建多个设计...
- 创建一个自定义 ItemsControl 作为具有可取消多进度项的滑动面板...
- 多线程:线程、任务和后台工作者...
屏幕截图
WaitSpin 控件
代码
它源自Control
类。它非常简单,定义了几个属性和方法。模板的要求是有一个 PART_LoadingAnimation
,它定义了一个用于动画控件的 storyboard。
/// <summary>
/// Enumeration for representing state of an animation.
/// </summary>
public enum AnimationState
{
/// <summary>
/// The animation is playing.
/// </summary>
Playing,
/// <summary>
/// The animation is paused.
/// </summary>
Paused,
/// <summary>
/// The animation is stopped.
/// </summary>
Stopped
}
/// <summary>
/// A control that shows a loading animation.
/// </summary>
public class WaitSpin : Control
{
#region --------------------CONSTRUCTORS--------------------
static WaitSpin()
{
FrameworkElement.DefaultStyleKeyProperty.OverrideMetadata(typeof(WaitSpin),
new FrameworkPropertyMetadata(typeof(WaitSpin)));
}
/// <summary>
/// LoadingAnimation constructor.
/// </summary>
public WaitSpin()
{
this.DefaultStyleKey = typeof(WaitSpin);
}
#endregion
#region --------------------DEPENDENCY PROPERTIES--------------------
#region -------------------- fill--------------------
/// <summary>
/// fill property.
/// </summary>
public static readonly DependencyProperty ShapeFillProperty =
DependencyProperty.Register("ShapeFill", typeof(Brush), typeof(WaitSpin), null);
/// <summary>
/// Gets or sets the fill.
/// </summary>
[System.ComponentModel.Category("Loading Animation Properties"), System.ComponentModel.Description("The fill for the shapes.")]
public Brush ShapeFill
{
get { return (Brush)GetValue(ShapeFillProperty); }
set { SetValue(ShapeFillProperty, value); }
}
#endregion
#region -------------------- stroke--------------------
/// <summary>
/// Ellipse stroke property.
/// </summary>
public static readonly DependencyProperty ShapeStrokeProperty =
DependencyProperty.Register("ShapeStroke", typeof(Brush), typeof(WaitSpin), null);
/// <summary>
/// Gets or sets the ellipse stroke.
/// </summary>
[System.ComponentModel.Category("Loading Animation Properties"), System.ComponentModel.Description("The stroke for the shapes.")]
public Brush ShapeStroke
{...
#endregion
#region --------------------Is playing--------------------
/// <summary>
/// Playing status
/// </summary>
public static readonly DependencyProperty IsPlayingProperty = DependencyProperty.Register("IsPlaying", typeof(bool), typeof(WaitSpin),
new FrameworkPropertyMetadata(new PropertyChangedCallback(OnIsPlayingChanged)));
/// <summary>
/// OnIsPlayingChanged callback
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnIsPlayingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(d))
return;
WaitSpin element = d as WaitSpin;
element.ChangePlayMode((bool)e.NewValue);
}
/// <summary>
/// IsPlaying
/// </summary>
[System.ComponentModel.Category("Loading Animation Properties"), System.ComponentModel.Description("Incates wheter is playing or not.")]
public bool IsPlaying
{...
#endregion
#region --------------------Associated element--------------------
/// <summary>
/// Associated element to disable when loading
/// </summary>
public static readonly DependencyProperty AssociatedElementProperty = DependencyProperty.Register("AssociatedElement", typeof(UIElement), typeof(WaitSpin), null);
/// <summary>
/// Gets or sets the associated element to disable when loading
/// </summary>
[System.ComponentModel.Category("Loading Animation Properties"), System.ComponentModel.Description("Associated element that will be disabled when playing.")]
public UIElement AssociatedElement
{...
#endregion
#region --------------------AutoPlay--------------------
/// <summary>
/// Gets or sets a value indicating whether the animation should play on load.
/// </summary>
public static readonly DependencyProperty AutoPlayProperty = DependencyProperty.Register("AutoPlay", typeof(bool), typeof(WaitSpin),
new FrameworkPropertyMetadata(new PropertyChangedCallback(OnAutoPlayChanged)));
private static void OnAutoPlayChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(d))
return;
WaitSpin element = d as WaitSpin;
element.ChangePlayMode((bool)e.NewValue);
}
/// <summary>
/// Gets or sets a value indicating whether the animation should play on load.
/// </summary>
[System.ComponentModel.Category("Loading Animation Properties"), System.ComponentModel.Description("The animation should play on load.")]
public bool AutoPlay
{...
#endregion
#endregion
#region --------------------PROPERTIES--------------------
...
/// <summary>
/// Gets the animation state,
/// </summary>
public AnimationState AnimationState
{
get { return this._animationState; }
}
#endregion
/// <summary>
/// Gets the parts out of the template.
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
//retreive the animation part
this._loadingAnimation = (Storyboard)this.GetTemplateChild("PART_LoadingAnimation");
if (this.AutoPlay)
Begin();
}
/// <summary>
/// Begins the loading animation.
/// </summary>
internal void ChangePlayMode( bool playing )
{
if (this._loadingAnimation == null) return;
if (playing)
{
if (this._animationState != AnimationState.Playing)
Begin();
}
else
{
if (this._animationState != AnimationState.Stopped)
Stop();
}
}
/// <summary>
/// Begins the loading animation.
/// </summary>
public void Begin()
{
if (this._loadingAnimation != null)
{
this._animationState = AnimationState.Playing;
this._loadingAnimation.Begin();
this.Visibility = System.Windows.Visibility.Visible;
if (AssociatedElement != null)
AssociatedElement.IsEnabled = false;
}
}
/// <summary>
/// Pauses the animation.
/// </summary>
public void Pause()
{
if (this._loadingAnimation != null)
{
this._animationState = AnimationState.Paused;
this._loadingAnimation.Pause();
}
}
/// <summary>
/// Resumes the animation.
/// </summary>
public void Resume()
{
if (this._loadingAnimation != null)
{
this._animationState = AnimationState.Playing;
this._loadingAnimation.Resume();
}
}
/// <summary>
/// Stops the animation.
/// </summary>
public void Stop()
{
if (this._loadingAnimation != null)
{
this._animationState = AnimationState.Stopped;
this._loadingAnimation.Stop();
this.Visibility = System.Windows.Visibility.Hidden;
if (AssociatedElement != null)
AssociatedElement.IsEnabled = true;
}
}
}
基本的 XAML 样式定义了 Visibility
、ShapeFill
和 ShapeStroke
属性。附加了一个简单的椭圆模板和一个用于调整形状透明度的 storyboard。
请注意,模板被一个绑定到控件尺寸的 Viewbox
包围——这是我发现的使其可调的最简单方法,因为 Canvas
控件简直是一场噩梦……椭圆属性通过模板绑定到 WaitSpin 控件的属性 Stroke="{TemplateBinding ShapeStroke}" Fill="{TemplateBinding ShapeFill}"
<Style x:Key="{x:Type local:WaitSpin}" TargetType="{x:Type local:WaitSpin}">
<Setter Property="Visibility" Value="Visible" />
<Setter Property="ShapeFill" Value="White" />
<Setter Property="ShapeStroke" Value="#00000000" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:WaitSpin}">
<Viewbox Width="{TemplateBinding Width}" Height="{TemplateBinding Height}">
<Canvas x:Name="Document" Width="100" Height="100" Clip="F1 M 0,0L 100,0L 100,100L 0,100L 0,0">
<Canvas.Resources>
<Storyboard.....
</Canvas.Resources>
<Ellipse x:Name="ellipse8" Width="15" Height="15" Canvas.Left="12" Canvas.Top="12" Stretch="Fill" StrokeThickness="1" StrokeLineJoin="Round" Stroke="{TemplateBinding ShapeStroke}" Fill="{TemplateBinding ShapeFill}" Opacity="0.66" />
.....
</Canvas>
</Viewbox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
创建设计
创建设计的最简单方法是结合使用 Expression Designer 和 Blend。我的模型基于一个 100x100 的文档。在 Designer 中,创建您想要的形状,然后使用以下选项导出它们:“选定对象”+(第 3 个)“XAML SL4 / CWPF 画布”+“始终命名”+“将分组对象放入容器”+“路径”+“转换为效果”
然后,将导出的文件中的画布内容复制到 WaitSpin 样式副本中,以替换椭圆。
如果您使用基本样式中预定义的透明度 storyboard,也要注意元素的顺序和名称,然后只需将元素重命名为“EllipseX”或更改每个 KeyFrames
的 TargetName
。
<Storyboard x:Key="PART_LoadingAnimation" x:Name="PART_LoadingAnimation" RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="ellipse1" Storyboard.TargetProperty="(UIElement.Opacity)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="1"/>
....
或者,在 Blend 中创建一个新的 storyboard,例如“ie”样式中定义的并且下面打印的旋转 storyboard - Visual Studio 在设计器中不喜欢它,但它有效。
<Storyboard x:Key="PART_LoadingAnimation" x:Name="PART_LoadingAnimation" RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)" Storyboard.TargetName="Document">
<EasingDoubleKeyFrame KeyTime="00:00:02" Value="360"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
注意:Expression 套件超出了范围……我一点也不精通它!
多进度面板
对于在线编程,通常会调用远程 Web 服务,而远程 Web 服务并不总是能快速响应。因此,您会启动一些异步方法并等待响应。想法是创建一个类似 Internet Explorer 中的下载面板,它小巧、不具侵入性、可取消,并且在这种情况下可以进行多进程处理。我也希望它符合 MVVM!
首先,我们需要选择基础控件并定义 ViewModel:ItemsControl
似乎满足了要求,然后让我们编写我们进程项的 ViewModel
。
public class ProcessItem : ViewModelBase
{
//init data
public bool UseTempo { get; set; }
public DateTime StartTime { get; set; }
public bool CanCancel { get; set; }
public bool ShowProgress { get; set; }
public bool ShowPercentage { get; set; }
private string _Title;
public string Title
{
get { return _Title; }
set
{
if (_Title != value)
{
_Title = value;
RaisePropertyChanged("Title");
}
}
}
private string _Message;
public string Message
{
get { return _Message; }
set
{
if (_Message != value)
{
_Message = value;
RaisePropertyChanged("Message");
}
}
}
public bool WaitForCancel { get; set; }
#region cancel command
private ICommand cancelCommand;
public ICommand CancelCommand
{
get
{
if (cancelCommand == null)
cancelCommand = new DelegateCommand(CancelCommandExecute, CancelCommandCanExecute);
return cancelCommand;
}
}
virtual public bool CancelCommandCanExecute()
{
return CanCancel;
}
virtual public void CancelCommandExecute()
{
WaitForCancel = true;
}
#endregion
}
要编写控件,请创建一个继承自 ItemsControl
的类。
public class ProcessPanel : ItemsControl
然后我们重写 OnApplyTemplate
以检索在模板中定义的打开和关闭动画。
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_CloseStoryboard = (Storyboard)this.GetTemplateChild("PART_CloseStoryboard");
_CloseAnim = _CloseStoryboard.Children[0] as DoubleAnimation;
_OpenStoryboard = (Storyboard)this.GetTemplateChild("PART_OpenStoryboard");
_OpenAnim = _OpenStoryboard.Children[0] as DoubleAnimation;
}
之后,我们只需订阅项目集合事件来展开或折叠面板,该面板将由我们的 storyboard 进行动画处理。请注意,动画周围的 StoryBoard
对象是修改 From
和 To
属性的唯一方法。否则,由于它们正在播放,您将遇到冻结对象的异常。
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
Console.WriteLine("play open : from " + _OpenAnim.From + "to" + _OpenAnim.To);
_OpenStoryboard.Begin();
_OpenAnim.From = _OpenAnim.To;
_OpenAnim.To += 35;
_CloseAnim.From = _OpenAnim.To;
_CloseAnim.To = _OpenAnim.From;
}...
我们的控件的样式如下:我定义了一个包含 storyboards 的 DockPanel
,然后是一个简单的网格,它有一个用于成型控件的边框和一个 ItemPresenter
,其中包含我的 ItemsControl
集合。ItemTemplate
被替换为简单的 DataTemplate
,它绑定到 ProcessItem
ViewModel。
<Style x:Key="{x:Type local:ProcessPanel}" TargetType="{x:Type local:ProcessPanel}">
<Setter Property="Visibility" Value="Visible" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ProcessPanel}">
<DockPanel x:Name="PART_Container" VerticalAlignment="Bottom" Panel.ZIndex="1000">
<DockPanel.Resources>
<Storyboard x:Name="PART_CloseStoryboard" x:Key="PART_CloseStoryboard">
<DoubleAnimation x:Name="CloseAnim"
Storyboard.TargetName="PART_Container"
Storyboard.TargetProperty="(Height)"
From="41"
To="0"
Duration="00:00:00.4000000" />
</Storyboard>
<Storyboard x:Name="PART_OpenStoryboard" x:Key="PART_OpenStoryboard">
...
</DockPanel.Resources>
<Grid Margin="0">
<Border x:Name="top" Grid.ColumnSpan="4" CornerRadius="7,7,0,0">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
...
</LinearGradientBrush>
</Border.Background>
</Border>
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Grid>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemsControl.ItemTemplate">
<Setter.Value>
<DataTemplate>
<Grid Margin="0" Height="35">
<Grid.ColumnDefinitions>
...
<TextBlock Grid.Column="0" VerticalAlignment="Center" Margin="5" Background="Transparent"
Text="{Binding Title}" TextWrapping="WrapWithOverflow" TextTrimming="WordEllipsis" />
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="5" Background="Transparent"
Text="{Binding Message}" TextWrapping="WrapWithOverflow" TextTrimming="WordEllipsis" />
<controls:WaitSpin Grid.Column="2" AutoPlay="True" Margin="6"></controls:WaitSpin>
<Button Grid.Column="3" Margin="6" Content="x" Command="{Binding CancelCommand}" />
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
线程、任务和 BackgroundWorker
解释
在我的 C.B.R. 项目中,我尝试了 TPL 以与我的线程实现进行比较……这太糟糕了……所以我选择深入研究并尝试比较这些方法。我选择实现一个“递归磁盘/文件夹解析”方法来查找图像。这使我们能够继续使用 WaitSpin
和 ProgressPanel
控件。
算法始终相同,基本如下 - 取决于方法:BtnClick => 创建一个 ProcessItem => 将其添加到集合 => 启动一个“进程线程”=> 完成后,删除 ProcessItem
线程方式
没什么特别的,但请注意,我使用 BeginInvoke
在应用程序线程上添加和删除项目。这是异步的,可能会导致错误。
private void btnThread_Click(object sender, RoutedEventArgs e)
{
ProcessItem pi = new ProcessItem()
{
Title = "btnThread_Click",
Message = "btnThread_Click",
CanCancel = true,
ShowProgress = true,
ShowPercentage = false,
Data = this.tbFolder.Text,
StartTime = DateTime.Now,
UseTempo = chkUseTempo.IsChecked.Value
};
Thread t = new Thread(new ParameterizedThreadStart(LaunchThread));
t.IsBackground = true;
t.Priority = ThreadPriority.Normal;
t.Start(pi);
}
private void LaunchThread(object param)
{
try
{
ProcessItem pi = param as ProcessItem;
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate
{
_list.Add(pi);
listBox1.Items.Add(pi.StartTime);
});
pi.Message = "Processing folders";
ProcessMethod(pi, pi.Data as string);
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate
{
_list.Remove(pi);
listBox1.Items.Add(DateTime.Now - pi.StartTime);
});
}
...}
internal void ProcessMethod(object param, string folder)
{
ProcessItem pi = param as ProcessItem;
...
pi.Message = "Processing folder " + directory.Name;
foreach (FileInfo file in directory.GetFiles("*.*"))
{
if (pi.WaitForCancel)
{
pi.Message = "canceled by the user";
return;
}
pi.Message = "Processing file " + file.Name;
if (file.Extension == ".jpg")
{
Application.Current.Dispatcher.Invoke(DispatcherPriority.Normal, (ThreadStart)delegate
{
listBox1.Items.Add(file.Name);
});
}
...
}
TPL - 第一次尝试...
想法是:哇!创建一些并行任务,看看有多快……但我忘了创建 Task
是 CPU 密集型的,而我的硬盘在这种情况下是瓶颈!想象一下,即使框架每秒可以创建 20000 个任务,我的硬盘也无法如此快速地读取文件!下面是第一次尝试。
public void ParallelProcessMethod(object param, string folder)
{
ProcessItem pi = param as ProcessItem;
try
{
DirectoryInfo directory = new DirectoryInfo(folder);
if (!directory.Exists)
...
Parallel.ForEach<FileInfo>(directory.GetFiles("*.*"), fi =>
{
...
Parallel.ForEach<DirectoryInfo>(directory.GetDirectories("*", SearchOption.TopDirectoryOnly), dir =>
{
...
ParallelProcessMethod(param, dir.FullName);
...
建议:切勿这样做!确保任务数量不会增长过多,并且处理时间足够长以弥补任务创建成本。
更新:这无疑是由于 Visual Studio 和调试模式!查看 CPU 和时间比较……
TPL - 第二次尝试...
删除 Parallel.ForEach
后一切都好多了!这是处理程序。我把添加和删除 ProcessItem
的代码放在这里,以利用 ContinueWith
方法。处理代码是相同的。
private void btnTask_Click(object sender, RoutedEventArgs e)
{
ProcessItem pi = new ProcessItem()
{
Title = "btnTask_Click",
Message = "btnTask_Click",
CanCancel = true,
ShowProgress = true,
ShowPercentage = false,
Data = this.tbFolder.Text,
StartTime = DateTime.Now,
UseTempo = chkUseTempo.IsChecked.Value
};
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate
{
_list.Add(pi);
listBox1.Items.Add(pi.StartTime);
});
Task tk = Task.Factory.StartNew(() =>
{
try
{
pi.Message = "Processing folders";
ParallelProcessMethod(pi, pi.Data as string);
}
catch (Exception err)
{
}
}).ContinueWith(ant =>
{
_list.Remove(pi);
listBox1.Items.Add(DateTime.Now - pi.StartTime);
//updates UI no problem as we are using correct SynchronizationContext
}, TaskScheduler.FromCurrentSynchronizationContext());
}
...
Background Worker 待完成
我以一种快速而经典的方式 实现了 BackgroundWorker
,如下所示。
private void btnWorker_Click(object sender, RoutedEventArgs e)
{
BackgroundWorker _Worker = null;
//init the background worker process
_Worker = new BackgroundWorker();
_Worker.WorkerReportsProgress = true;
_Worker.WorkerSupportsCancellation = true;
_Worker.DoWork += new DoWorkEventHandler(bw_DoBuildWork);
_Worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
_Worker.ProgressChanged += new ProgressChangedEventHandler(bw_ProgressChanged);
ProcessItem pi = new ProcessItem()
{
Title = "btnWorker_ClickbtnWorker_Click",
Message = "btnWorker_Click",
CanCancel = true,
ShowProgress = true,
ShowPercentage = false,
Data = this.tbFolder.Text,
StartTime = DateTime.Now,
UseTempo = chkUseTempo.IsChecked.Value
};
// Start the asynchronous operation.
_Worker.RunWorkerAsync(pi);
}
...
void bw_DoBuildWork(object sender, DoWorkEventArgs e)
{
ProcessItem pi = e.Argument as ProcessItem;
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate
{
_list.Add(pi);
listBox1.Items.Add(pi.StartTime);
});
BackProcessMethod(pi, pi.Data as string);
e.Result = pi;
}
internal void BackProcessMethod(object param, string folder)
{
ProcessItem pi = param as ProcessItem;
...
//the same as others
}
void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
...empty
}
void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
ProcessItem pi = e.Result as ProcessItem;
// First, handle the case where an exception was thrown.
if (e.Error != null)
{
}
else if (e.Cancelled)
{
// Next, handle the case where the user canceled the operation.
// Note that due to a race condition in the DoWork event handler, the Cancelled
// flag may not have been set, even though CancelAsync was called.
}
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate
{
_list.Remove(pi);
listBox1.Items.Add(DateTime.Now - pi.StartTime);
});
}
计时和 CPU
我的系统磁盘“C:”包含 125,241 个文件,23,087 个文件夹,总计 163 GB。
BackgroundWorker = 00:00:08s 1070。CPU 最多 50%,使用 4 核
带有 Parallel 的任务(第一次尝试) = 00:00:04s,CPU 非常高,达到 90%,但我们节省了 3 秒
任务 = 00:00:07s 3214。CPU 最多 50%,但在 4 核上使用较少
线程 = 00:00:07s 7414。CPU 最多 60%,活动与任务相同
总结一下...
在我看来,这个小小的尝试表明
- BackgroundWorker 方法的代码有点多,使用其进度处理程序显得有些过时且更僵硬
- 执行速度几乎相同,任务略有优势
- 相对于 Tasks 的 LINQ 风格,线程是更经典的书写方式
- Tasks 在更复杂的场景中肯定会显现出优势!
- 在 Visual Studio 外使用 release 模式进行比较!
顺便说一句,这更多的是一种时尚,而不是其他任何东西——这是为了让 TPL 的粉丝们过度反应 :-)
结论
本文是对您可以用于进一步开发的内容的非常快速的草稿。希望您会喜欢它……下一个改进是的用户交互后减小面板……但我遇到了一些麻烦……
历史
- v1.0
- 第一个版本:一切都在这里。我想添加一个自动隐藏功能,如果鼠标离开面板……
-
v2.0
- 第二个版本:更新了 CPU 和时间。关于第一次尝试的新建议,任务在 release 模式下似乎更好。