WPF 自定义 BusyIndicator (忙指示) 控件

关于 Busy 控件,网上可以找到一大把,但是学习是学习,娱乐是娱乐,总不能总拿别人的东西“自娱愚人”,你会的,刻死在你脑子里,你不会的,那啥。。。我就不说了。

这个东西用到了 Adorner , 一开始总感觉到这个东西很神秘莫测,看别人讲的云里雾里,总之,不动手实现一遍,是永远不会理解的。

写这个东西,参考了网上的代码,如有雷同,笑一笑就行了,谢谢。

 

1,一切从 RegisterAttatched 开始。

写这个软件,基本都是基于CM的,Register 都很少用,偶然看到这个方法,费了我老长时间。

    <Grid ac:Busy.MaskType="Adorned"
          ac:Busy.Show="{Binding IsBusy, Mode=TwoWay}" 
          ac:Busy.Text="{Binding BusyText, Mode=TwoWay}">

如上所示,怎样写代码才能将 Busy.Show / Text / MaskType 做为附加属性添到 Grid 里?

看一下实现代码:

 1     public class Busy {
 2 
 3         #region Text
 4         public static readonly DependencyProperty TextProperty = DependencyProperty.RegisterAttached("Text", typeof(string), typeof(Busy), new PropertyMetadata(TextChanged));
 5 
 6         private static void TextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
 7             Update((FrameworkElement)d);
 8         }
 9 
10         public static void SetText(FrameworkElement target, string value) {
11             target.SetValue(TextProperty, value);
12         }
13 
14         public static string GetText(FrameworkElement target) {
15             return (string)target.GetValue(TextProperty);
16         }
17         #endregion
18 。。。
19 。。。

static / RegisterAttached / SetXXX / GetXXX 及 PropertyMetadata 都是不可忽略的部分。

 

2,注册 Adorner 

上面只是依赖属性,当各依赖属性变化的时候,都会执行 Update 方法,在 Update 方法里会去取 target 的 Adorner ,是实例化还是销毁都在 Update 方法里。

 1        private static void Update(FrameworkElement target) {
 2             var layer = AdornerLayer.GetAdornerLayer(target);
 3             if (layer == null) {
 4                 Dispatcher.CurrentDispatcher.BeginInvoke(new Action<FrameworkElement>(o => Update(o)), DispatcherPriority.Loaded, target);
 5                 return;
 6             }
 7 
 8             var text = GetText(target);
 9             var show = GetShow(target);
10             var maskType = GetMaskType(target);
11             var template = GetContentControlTemplate(target);
12 
13             var adorner = GetAdorner(target);
14 
15             if (show) {
16                 if (adorner == null) {
17                     adorner = new BusyIndicatorAdorner(target) {
18                         MaskType = maskType,
19                         ContentControlTemplate = template,
20                         Text = text
21                     };
22                     layer.Add(adorner);
23                     SetAdorner(target, adorner);
24                 } else {
25                     adorner.MaskType = maskType;
26                     adorner.ContentControlTemplate = template;
27                     adorner.Text = text;
28                 }
29                 //adorner.Visibility = Visibility.Visible;
30             } else {
31                 if (adorner != null) {
32                     layer.Remove(adorner);
33                     //如果不 Remove 并设置为 null, 在 使用AvalonDock的程序里,切换标签会使 adorner 的 Parent 丢失
34                     //如果设置为 null ,会在再一次显示的时候,重建
35                     //adorner.Visibility = Visibility.Collapsed;
36                     SetAdorner(target, null);
37                 }
38             }
39         }

显示时,将得到的 adorner 加到 AdornerLayer (Add) 里,不显示时,从 AdornerLayer 里移除它 (Remove)。

写这个件软,用了 AvalonDock 。如果不显示时,只是简单的将 adorner 设为不可见,在切换文档(标签)时,整个 Busy 都将不能正常工作,因为 adorer 还在,但是 adorner 的 Parent 却变为 null 了。不了解 AvalonDock 是怎样处理的,但是简单的修正方法是在不显示的时候,将 adorner 从 AdornerLayer 里移除,这样 在再一次显示的时候,就会重新实例一次。

 

3, Adorner

a, Adorner 的构造函数

1         public BusyIndicatorAdorner(FrameworkElement adornered)
2             : base(adornered) {
3 
4             this.Indicator = new BusyIndicator();
5             this.AddVisualChild(this.Indicator);
6         }

这里的 BusyIndicator 是一个自定义控件,由此可以看出整个过程如下:

将 Busy 以附加属性的方法附加到 FrameworkElement 上,当附加属性的值改变时,调用 Update 方法,Update 方法在将 Adorner 添加到 AdornerLayer 里, Adorner 负责实例化一个自定义控件,并将这个控件添加到该 Adorner 的可视树里,自定义控件负责这个 Busy 的外观。

b, 设置 adorner 的位置。

WPF 自定义 BusyIndicator (忙指示) 控件_第1张图片

WPF 自定义 BusyIndicator (忙指示) 控件_第2张图片

Busy 顾名思义,就是将某一部分界面设为不可操作。为了实现这个目的,我加了一个 MaskType : None, Adornered, Window.

为None 的时候,不显示遮罩层,UI还可以继续操作。

Window 即指将整个窗口加一个遮罩,这样速个窗口在忙的时候,都不能操作。

Adornered 只将遮罩添加到Busy 所在的 FrameworkElement 上。

为None 和 Adornered 都好办,不用调整 adorner 的位置。但是为 Window 的时候,就要调整 Adornered 的位置了,因为 Adorner 的起点默认为 AdorneredElement 的起点。

 1         public override GeneralTransform GetDesiredTransform(GeneralTransform transform) {
 2             if (this.MaskType == MaskTypes.Window) {
 3                 //变换Adorner 的起点
 4                 var w = GetParentWindow(this.AdornedElement);
 5                 var a = this.AdornedElement.TransformToAncestor(w);
 6                 var b = a.Transform(new Point(0, 0));
 7 
 8                 TransformGroup group = new TransformGroup();
 9                 var t = new TranslateTransform(-b.X, -b.Y + ((SystemParameters.ResizeFrameHorizontalBorderHeight * 2) + SystemParameters.WindowCaptionHeight));
10                 group.Children.Add(t);
11 
12                 group.Children.Add(transform as Transform);
13                 return group;
14             } else {
15                 return base.GetDesiredTransform(transform);
16             }
17         }

这里,先获取 AdorneredElement 在其所在窗口上的位置(起点),在跟据这个点做位移(这里一并排除标题栏的高度,这就就不会出现不能关闭窗口的情况了)。

 

C, 设置 Adorner 的大小

 1         protected override Size MeasureOverride(Size constraint) {
 2             Size s = Size.Empty;
 3             switch (this.MaskType) {
 4                 case MaskTypes.None:
 5                 case MaskTypes.Adorned:
 6                     this.AdornedElement.Measure(constraint);
 7                     var ele = this.AdornedElement as FrameworkElement;
 8                     s = new Size(ele.ActualWidth, ele.ActualHeight);
 9                     //s = this.AdornedElement.DesiredSize;
10                     break;
11                 case MaskTypes.Window:
12                     var w = GetParentWindow(this.AdornedElement);
13                     s = new Size(w.ActualWidth, w.ActualHeight - ((SystemParameters.ResizeFrameHorizontalBorderHeight * 2) + SystemParameters.WindowCaptionHeight));
14                     break;
15             }
16             return s;
17         }

这个没什么好说的,就是计算 Adorner 所需的显示区域的大小。

 

4,界面未按照设想及时更新

在做一个需要长时间的操作之前,我希望将这个 Busy 显示出来,于是我将 Busy 设为 Show, 在操作完成时,将它关闭。想的很好。

但是:现实是在这个操作完成后,Busy 才显示,接着又关闭!即使用了 Dispatcher 也一样!Dispatcher 被阻止了,我相信可以用多线程来解决这个问题,但是简单的操作,着实没有必要用多线程来做!

搜了一下,有个叫 DispatcherFrame 的东西。

 1     public static class DispatcherHelper {
 2 
 3         public static void DoEvents() {
 4             DispatcherFrame frame = new DispatcherFrame();
 5             Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
 6                 new DispatcherOperationCallback(ExitFrame), frame);
 7             Dispatcher.PushFrame(frame);
 8         }
 9 
10         private static object ExitFrame(object f) {
11             ((DispatcherFrame)f).Continue = false;
12 
13             return null;
14         }
15 
16     }

 

 

 1         public void Search(QueryEx<Order> ex) {
 2 
 3             this.IsBusy = true;
 4             this.NotifyOfPropertyChange(() => this.IsBusy);
 5 
 6             DispatcherHelper.DoEvents();
 7 ...
 8 ...
 9 需要长时间的操作
10 ...
11 ...

 

 

好了,一切看似完美了。但是在有 AvalongDock 的应用了,当切换文档(标签)的时候,这个Busy 还是会消失不见,比较了一下,貌似是因为 adorner 的 Parent 被置为 null 引起的,如何解决,待探索。

附上源码:

Busy.cs

  1 using System;
  2 using System.Collections.Generic;
  3 using System.Linq;
  4 using System.Text;
  5 using System.Threading.Tasks;
  6 using System.Windows;
  7 using System.Windows.Controls;
  8 using System.Windows.Documents;
  9 using System.Windows.Threading;
 10 
 11 namespace AsNum.WPF.Controls {
 12 
 13     public enum MaskTypes {
 14         None,
 15         Adorned,
 16         Window
 17     }
 18     public class Busy {
 19 
 20         #region Text
 21         public static readonly DependencyProperty TextProperty = DependencyProperty.RegisterAttached("Text", typeof(string), typeof(Busy), new PropertyMetadata(TextChanged));
 22 
 23         private static void TextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
 24             Update((FrameworkElement)d);
 25         }
 26 
 27         public static void SetText(FrameworkElement target, string value) {
 28             target.SetValue(TextProperty, value);
 29         }
 30 
 31         public static string GetText(FrameworkElement target) {
 32             return (string)target.GetValue(TextProperty);
 33         }
 34         #endregion
 35 
 36         #region MaskType
 37         public static readonly DependencyProperty MaskTypeProperty = DependencyProperty.RegisterAttached("MaskType", typeof(MaskTypes), typeof(Busy), new PropertyMetadata(MaskTypes.Adorned, MaskTypeChanged));
 38 
 39         private static void MaskTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
 40             Update((FrameworkElement)d);
 41         }
 42 
 43         public static void SetMaskType(FrameworkElement target, MaskTypes type) {
 44             target.SetValue(MaskTypeProperty, type);
 45         }
 46 
 47         public static MaskTypes GetMaskType(FrameworkElement target) {
 48             return (MaskTypes)target.GetValue(MaskTypeProperty);
 49         }
 50         #endregion
 51 
 52         #region Show
 53         public static readonly DependencyProperty ShowProperty = DependencyProperty.RegisterAttached("Show", typeof(bool), typeof(Busy), new PropertyMetadata(false, ShowChanged));
 54         private static void ShowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
 55             Update((FrameworkElement)d);
 56         }
 57 
 58         public static void SetShow(FrameworkElement target, bool value) {
 59             target.SetValue(ShowProperty, value);
 60         }
 61 
 62         public static bool GetShow(FrameworkElement target) {
 63             return (bool)target.GetValue(ShowProperty);
 64         }
 65         #endregion
 66 
 67         #region adorner
 68         public static readonly DependencyProperty AdornerProperty = DependencyProperty.RegisterAttached("Adorner", typeof(BusyIndicatorAdorner), typeof(Busy));
 69 
 70         public static void SetAdorner(FrameworkElement target, BusyIndicatorAdorner adorner) {
 71             target.SetValue(AdornerProperty, adorner);
 72         }
 73 
 74         public static BusyIndicatorAdorner GetAdorner(FrameworkElement target) {
 75             return (BusyIndicatorAdorner)target.GetValue(AdornerProperty);
 76         }
 77 
 78         #endregion
 79 
 80         #region
 81         public static readonly DependencyProperty ContentControlTemplateProperty = DependencyProperty.Register("ContentControlTemplate", typeof(ControlTemplate), typeof(Busy));
 82 
 83         public static ControlTemplate GetContentControlTemplate(FrameworkElement target) {
 84             return (ControlTemplate)target.GetValue(ContentControlTemplateProperty);
 85         }
 86 
 87         public static void SetContentControlTemplate(FrameworkElement target, ControlTemplate ctrl) {
 88             target.SetValue(ContentControlTemplateProperty, ctrl);
 89         }
 90 
 91         #endregion
 92 
 93         private static void Update(FrameworkElement target) {
 94             var layer = AdornerLayer.GetAdornerLayer(target);
 95             if (layer == null) {
 96                 Dispatcher.CurrentDispatcher.BeginInvoke(new Action<FrameworkElement>(o => Update(o)), DispatcherPriority.Loaded, target);
 97                 return;
 98             }
 99 
100             var text = GetText(target);
101             var show = GetShow(target);
102             var maskType = GetMaskType(target);
103             var template = GetContentControlTemplate(target);
104 
105             var adorner = GetAdorner(target);
106 
107             if (show) {
108                 if (adorner == null) {
109                     adorner = new BusyIndicatorAdorner(target) {
110                         MaskType = maskType,
111                         ContentControlTemplate = template,
112                         Text = text
113                     };
114                     layer.Add(adorner);
115                     SetAdorner(target, adorner);
116                 } else {
117                     adorner.MaskType = maskType;
118                     adorner.ContentControlTemplate = template;
119                     adorner.Text = text;
120                 }
121                 //adorner.Visibility = Visibility.Visible;
122             } else {
123                 if (adorner != null) {
124                     layer.Remove(adorner);
125                     //如果不 Remove 并设置为 null, 在 使用AvalonDock的程序里,切换标签会使 adorner 的 Parent 丢失
126                     //如果设置为 null ,会在再一次显示的时候,重建
127                     //adorner.Visibility = Visibility.Collapsed;
128                     SetAdorner(target, null);
129                 }
130             }
131         }
132     }
133 }
View Code

 

BusyAdorner.cs

  1 using System;
  2 using System.Collections.Generic;
  3 using System.Linq;
  4 using System.Text;
  5 using System.Threading.Tasks;
  6 using System.Windows;
  7 using System.Windows.Controls;
  8 using System.Windows.Documents;
  9 using System.Windows.Media;
 10 
 11 namespace AsNum.WPF.Controls {
 12     public class BusyIndicatorAdorner : Adorner {
 13 
 14         #region maskType
 15         private MaskTypes maskType;
 16         public MaskTypes MaskType {
 17             get {
 18                 return this.maskType;
 19             }
 20             set {
 21                 this.maskType = value;
 22                 this.Indicator.MaskType = value;
 23             }
 24         }
 25         #endregion
 26 
 27         #region Text
 28         private string text;
 29         public string Text {
 30             get {
 31                 return this.text;
 32             }
 33             set {
 34                 this.text = value;
 35                 this.Indicator.Text = value;
 36             }
 37         }
 38         #endregion
 39 
 40         #region contentControlTemplate
 41         private ControlTemplate contentControlTemplate;
 42         public ControlTemplate ContentControlTemplate {
 43             get {
 44                 return this.contentControlTemplate;
 45             }
 46             set {
 47                 this.contentControlTemplate = value;
 48                 this.Indicator.ContentControlTemplate = value;
 49             }
 50         }
 51         #endregion
 52 
 53         private BusyIndicator Indicator;
 54 
 55         protected override int VisualChildrenCount {
 56             get {
 57                 return 1;
 58             }
 59         }
 60 
 61         protected override System.Windows.Media.Visual GetVisualChild(int index) {
 62             return this.Indicator;
 63         }
 64 
 65         public BusyIndicatorAdorner(FrameworkElement adornered)
 66             : base(adornered) {
 67 
 68             this.Indicator = new BusyIndicator();
 69             this.AddVisualChild(this.Indicator);
 70         }
 71 
 72         protected override Size MeasureOverride(Size constraint) {
 73             Size s = Size.Empty;
 74             switch (this.MaskType) {
 75                 case MaskTypes.None:
 76                 case MaskTypes.Adorned:
 77                     this.AdornedElement.Measure(constraint);
 78                     var ele = this.AdornedElement as FrameworkElement;
 79                     s = new Size(ele.ActualWidth, ele.ActualHeight);
 80                     //s = this.AdornedElement.DesiredSize;
 81                     break;
 82                 case MaskTypes.Window:
 83                     var w = GetParentWindow(this.AdornedElement);
 84                     s = new Size(w.ActualWidth, w.ActualHeight - ((SystemParameters.ResizeFrameHorizontalBorderHeight * 2) + SystemParameters.WindowCaptionHeight));
 85                     break;
 86             }
 87             return s;
 88         }
 89 
 90         protected override Size ArrangeOverride(Size finalSize) {
 91             this.Indicator.Measure(finalSize);
 92             this.Indicator.Arrange(new Rect(finalSize));
 93             return finalSize;
 94         }
 95 
 96         public override GeneralTransform GetDesiredTransform(GeneralTransform transform) {
 97             if (this.MaskType == MaskTypes.Window) {
 98                 //变换Adorner 的起点
 99                 var w = GetParentWindow(this.AdornedElement);
100                 var a = this.AdornedElement.TransformToAncestor(w);
101                 var b = a.Transform(new Point(0, 0));
102 
103                 TransformGroup group = new TransformGroup();
104                 var t = new TranslateTransform(-b.X, -b.Y + ((SystemParameters.ResizeFrameHorizontalBorderHeight * 2) + SystemParameters.WindowCaptionHeight));
105                 group.Children.Add(t);
106 
107                 group.Children.Add(transform as Transform);
108                 return group;
109             } else {
110                 return base.GetDesiredTransform(transform);
111             }
112         }
113 
114         public static Window GetParentWindow(DependencyObject child) {
115             DependencyObject parentObject = VisualTreeHelper.GetParent(child);
116 
117             if (parentObject == null) {
118                 return null;
119             }
120 
121             Window parent = parentObject as Window;
122             if (parent != null) {
123                 return parent;
124             } else {
125                 return GetParentWindow(parentObject);
126             }
127         }
128     }
129 }
View Code

 

BusyIndicator.cs

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 using System.Windows;
 7 using System.Windows.Controls;
 8 using System.Windows.Data;
 9 using System.Windows.Documents;
10 using System.Windows.Input;
11 using System.Windows.Media;
12 using System.Windows.Media.Imaging;
13 using System.Windows.Navigation;
14 using System.Windows.Shapes;
15 
16 namespace AsNum.WPF.Controls {
17 
18     [TemplatePart(Name = "CNT", Type = typeof(Control))]
19     [TemplatePart(Name = "MASK", Type = typeof(Border))]
20     public class BusyIndicator : ContentControl {
21 
22         public static DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(BusyIndicator), new PropertyMetadata("Waiting..."));
23         public static DependencyProperty MaskTypeProperty = DependencyProperty.Register("MaskType", typeof(MaskTypes), typeof(BusyIndicator), new PropertyMetadata(MaskTypes.Adorned));
24         public static DependencyProperty ContentControlTemplateProperty = DependencyProperty.Register("ContentControlTemplate", typeof(ControlTemplate), typeof(BusyIndicator));
25 
26         public string Text {
27             get {
28                 return (string)this.GetValue(TextProperty);
29             }
30             set {
31                 this.SetValue(TextProperty, value);
32             }
33         }
34 
35         public MaskTypes MaskType {
36             get {
37                 return (MaskTypes)this.GetValue(MaskTypeProperty);
38             }
39             set {
40                 this.SetValue(MaskTypeProperty, value);
41             }
42         }
43 
44         public ControlTemplate ContentControlTemplate {
45             get {
46                 return (ControlTemplate)this.GetValue(ContentControlTemplateProperty);
47             }
48             set {
49                 this.SetValue(ContentControlTemplateProperty, value);
50             }
51         }
52 
53         static BusyIndicator() {
54             DefaultStyleKeyProperty.OverrideMetadata(typeof(BusyIndicator), new FrameworkPropertyMetadata(typeof(BusyIndicator)));
55         }
56 
57         public BusyIndicator() {
58             this.DataContext = this;
59         }
60         public override void OnApplyTemplate() {
61             var mask = this.Template.FindName("MASK", this) as Border;
62             mask.Visibility = this.MaskType != MaskTypes.None ? Visibility.Visible : Visibility.Collapsed;
63             if (this.ContentControlTemplate != null) {
64                 var tp = this.Template.FindName("CNT", this) as Control;
65                 tp.Template = this.ContentControlTemplate;
66             }
67         }
68     }
69 }
View Code

 

Busy.xaml

 1 <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 2                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 3                     xmlns:local="clr-namespace:AsNum.WPF.Controls">
 4 
 5 
 6 
 7     <ControlTemplate x:Key="DEFAULTTEMPLATE">
 8         <Grid>
 9             <Border BorderBrush="Black" BorderThickness="1" CornerRadius="10" Margin="-1">
10                 <Border.Effect>
11                     <BlurEffect />
12                 </Border.Effect>
13             </Border>
14             <Border Background="#FF10a8ab" CornerRadius="15" Padding="10, 5" Width="auto" Height="auto">
15                 <TextBlock Text="{Binding Text}" FontSize="16" FontWeight="Bold" Foreground="White" Width="auto" HorizontalAlignment="Center" Height="auto" />
16             </Border>
17         </Grid>
18     </ControlTemplate>
19 
20     <Style TargetType="{x:Type local:BusyIndicator}">
21         <Setter Property="Template">
22             <Setter.Value>
23                 <ControlTemplate TargetType="{x:Type local:BusyIndicator}">
24                     <Grid>
25                         <Border x:Name="MASK" Background="#66000000">
26                             <!--<Border.Triggers>
27                                 <EventTrigger RoutedEvent="Border.IsVisibleChanged">
28                                     <BeginStoryboard>
29                                         <Storyboard>
30                                             <DoubleAnimation To="0" Duration="0:0:1"
31                                                              Storyboard.TargetProperty="Opacity" />
32                                         </Storyboard>
33                                     </BeginStoryboard>
34                                 </EventTrigger>
35                             </Border.Triggers>-->
36                         </Border>
37                         <Control x:Name="CNT" Template="{StaticResource DEFAULTTEMPLATE}" HorizontalAlignment="Center" VerticalAlignment="Center" />
38                     </Grid>
39                 </ControlTemplate>
40             </Setter.Value>
41         </Setter>
42     </Style>
43 
44 </ResourceDictionary>
View Code

 

谢绝不加出处的转载!

 

你可能感兴趣的:(WPF)