逐步创建类似 Vista 的忙碌光标 Silverlight 控件
在本文中,您将学习如何一步一步创建一个类似 Silverlight 的 Vista 忙碌光标控件。

引言
在您的 Silverlight 应用程序中,您经常需要下载某些内容或花费一段时间执行某些操作。在此期间,您可能希望显示一个指示器,告诉用户您的应用程序正在忙碌。您可能希望为此创建一个可用于您应用程序的控件。在本文中,我将逐步展示如何创建一个类似 Vista 忙碌光标的控件。
第一部分:创建 Silverlight 应用
我们为什么要先创建一个 Silverlight 应用程序?原因是 Expression Blend 2 (带 SP1) 无法直接设计 Silverlight 控件的默认模板。因此,我们首先使用 Blend 创建一个应用程序来设计控件的视觉效果。
步骤 1.1:在 VS 2008 中创建 Silverlight 应用程序
打开 Visual Studio 2008,创建一个 Silverlight 应用程序。右键单击 Page.xaml 文件,选择“在 Expression Blend 中打开”,然后项目将在 Blend 2 中打开。
步骤 1.2:设计控件的视觉效果
在 Blend 中,Page.xaml 已打开。在“对象和时间轴”面板中,选择 LayoutRoot 元素。双击它会将其置于选中状态。

然后,从“工具箱”中选择一个 Grid 控件,双击将其添加到 LayoutRoot。在“属性”面板中,将 Grid 的 Width 和 Height 设置为 Auto,将 HorizontalAlignment 和 VerticalAlignment 设置为 Center。

在“对象和时间轴”面板中,双击刚刚添加的 Grid 会选中它。从“工具箱”面板中将一个 Ellipse 添加到其中。将 Width 和 Height 属性设置为 20,将 Fill 设置为 None,将 Stroke 设置为 GradientBrush,并将 StrokeThickness 设置为 6。将 Opacity 设置为 0。

控件的最终视觉效果如下所示:

步骤 1.3:创建 VisualStates
现在我们创建 VisualState。VisualState“表示控件处于特定状态时的视觉外观”(来自 MSDN)。我们的控件可以处于两种状态之一:“忙碌状态”(`BusyState`) 或“空闲状态”(`IdleState`)。在 BusyState 中,控件将可见并显示动画。在 IdleState 中,控件将隐藏。
在“状态”面板中,单击“添加状态组”按钮添加一个状态组。

将“VisualStateGroup”重命名为“BusyIdleStates”。单击“BusyIdleStates”右侧的“添加状态”按钮,添加两个 VisualState 并将其命名为“BusyState”和“IdleState”。

在“状态”面板中选择“BusyState”。然后,打开时间轴面板,在“对象和时间轴”面板中,选择 Ellipse 元素。然后,选择 Ellipse 元素的 Stroke 属性。在“工具箱”中,选择“画笔变换”工具,将时间轴移动到“0:00.300”,并使用“画笔变换”工具将 Stroke 画笔旋转 45 度。


选择“画笔变换”工具并使用它。


将时间轴移动到“0:00.600”,并使用“画笔变换”工具将画笔旋转 90 度。重复此步骤,直到旋转一整圈。
最后,选择 Opacity 属性,将时间轴移动到“0:00.000”,并将其设置为 100%。这将使控件可见。完成此步骤后,通过单击状态面板中的“Base”来关闭“BusyState”。
步骤 1.4:测试
将以下代码添加到 Page.xaml.cs 文件中。
public Page()
{
   InitializeComponent();
   // add a event handle to Loaded event
   this.Loaded += new RoutedEventHandler(Page_Loaded);
}
void Page_Loaded(object sender, RoutedEventArgs e)
{
    // Go to BusyState
   VisualStateManager.GoToState(this, "BusyState", false);
}
在 VS2008 中右键单击项目,选择“调试”,然后选择“运行新实例”。您将看到应用程序的效果。
第二部分:创建控件
步骤 2.1:我们控件的文件结构
在这一部分,我们将刚刚创建的应用程序转换为一个控件,这样您就可以轻松地在任何应用程序中使用此控件。首先,向当前解决方案添加一个“Silverlight 类库”项目,并将其命名为“WaitingIcon”。将 class1.cs 重命名为 WaitingIcon.cs,还将 class1 重命名为 WaitingIcon,并使其派生自 Control 类。向项目添加一个名为“themes”的文件夹,在该 themes 文件夹中添加一个新文本文件,并将其命名为“generic.xaml”。我们的默认控件模板已准备好在此处可用。

选择 Generic.xaml 文件并右键单击它。然后,选择“属性”菜单项。在“属性”窗口中,将“生成操作”设置为“资源”,并删除“自定义工具”框中的文本。

步骤 2.2:创建我们控件的默认控件模板
打开 Generic.xaml 文件,并将以下代码添加到该文件。
<ResourceDictionary  xmlns="http://schemas.microsoft.com/client/2007"
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                     xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows" 
                     xmlns:controls="clr-namespace:Cokkiy.Display">       
    <Style TargetType="controls:WaitingIcon">        
        <Setter Property="Template">
            <Setter.Value>
                <--Control Template for the WaitingIcon-->
                <ControlTemplate TargetType="controls:WaitingIcon">   
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>
打开我们在第一部分创建的 Page.xaml 文件,将“<Grid>”和“</Grid>”之间的所有代码复制到 Generic.xaml 文件中,并将其插入到 `<ControlTemplate TargetType="controls:WaitingIcon">` 之后。
<ControlTemplate TargetType="controls:WaitingIcon">                    
    <Grid>                        
        <Ellipse StrokeThickness="{TemplateBinding StrokeThickness}" 
           x:Name="ellipse" 
           Stroke="{TemplateBinding Background}" 
           Opacity="0">
        </Ellipse>
    </Grid>
</ControlTemplate>
在应用程序中,我们将 Ellipse StrokeThickness 直接设置为 6。但在我们的控件中,我们将其设置为 TemplateBinding,以便最终用户可以设置宽度。将 Stroke 属性更改为 TemplatingBinding,以便最终用户可以为我们的控件设置不同的 Brush。我们可以通过在 `<Style TargetType="controls:WaitingIcon">` 之后添加以下代码来为 Stroke 添加默认 Brush,并为 StrokeThickness 属性添加默认宽度。
<Style TargetType="controls:WaitingIcon">
   <Setter Property="StrokeThickness" Value="6"/>
   <Setter Property="Background">
      <Setter.Value>
         <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
             <GradientStop Color="#FF0A0E94" Offset="0.576"/>
             <GradientStop Color="#FF0FFF1B" Offset="1"/>
         </LinearGradientBrush>
      </Setter.Value>
   </Setter>
将“VisualStates”定义从 Page.xaml 复制到 Generic.xaml,并将其插入到 `<Grid>` 之后。
<Grid>
   <vsm:VisualStateManager.VisualStateGroups>
         <vsm:VisualStateGroup x:Name="BusyIdleStates">
               <vsm:VisualState x:Name="BusyState">
                    <Storyboard AutoReverse="False" 
                           RepeatBehavior="Forever">
                        <PointAnimationUsingKeyFrames BeginTime="00:00:00" 
                               Storyboard.TargetName="ellipse" 
                               Storyboard.TargetProperty=
                                "(Shape.Stroke).(LinearGradientBrush.StartPoint)">
                            <SplinePointKeyFrame KeyTime="00:00:00.25" 
                               Value="0.868,0.161"/>
                            <SplinePointKeyFrame KeyTime="00:00:00.5" 
                               Value="0.997,0.44"/>
                            <SplinePointKeyFrame KeyTime="00:00:00.75" 
                               Value="0.845,0.863"/>
                            <SplinePointKeyFrame KeyTime="00:00:01" 
                               Value="0.545,0.999"/>
                            <SplinePointKeyFrame KeyTime="00:00:01.2500000" 
                               Value="0.166,0.873"/>
                            <SplinePointKeyFrame KeyTime="00:00:01.5" 
                               Value="0.001,0.536"/>
                            <SplinePointKeyFrame KeyTime="00:00:01.7500000" 
                               Value="0.084,0.222"/>
                            <SplinePointKeyFrame KeyTime="00:00:02" 
                               Value="0.462,0.001"/>
                        </PointAnimationUsingKeyFrames>
                        <PointAnimationUsingKeyFrames BeginTime="00:00:00" 
                               Storyboard.TargetName="ellipse" 
                               Storyboard.TargetProperty=
                                 "(Shape.Stroke).(LinearGradientBrush.EndPoint)">
                             <SplinePointKeyFrame KeyTime="00:00:00.25" 
                                Value="0.132,0.839"/>
                             <SplinePointKeyFrame KeyTime="00:00:00.5" 
                                Value="0.003,0.56"/>
                             <SplinePointKeyFrame KeyTime="00:00:00.75" 
                                Value="0.155,0.137"/>
                             <SplinePointKeyFrame KeyTime="00:00:01" 
                                Value="0.455,0.001"/>
                             <SplinePointKeyFrame KeyTime="00:00:01.2500000" 
                                Value="0.834,0.127"/>
                             <SplinePointKeyFrame KeyTime="00:00:01.5" 
                                Value="0.999,0.464"/>
                             <SplinePointKeyFrame KeyTime="00:00:01.7500000" 
                                Value="0.916,0.778"/>
                             <SplinePointKeyFrame KeyTime="00:00:02" 
                                Value="0.538,0.999"/>
                        </PointAnimationUsingKeyFrames>
                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
                                 Storyboard.TargetName="ellipse"
                                 Storyboard.TargetProperty="Opacity">
                             <SplineDoubleKeyFrame KeyTime="00:00:00" 
                                 Value="1"/>
                        </DoubleAnimationUsingKeyFrames>
                    </Storyboard>
               </vsm:VisualState>
           <vsm:VisualState x:Name="IdleState"/>
       </vsm:VisualStateGroup>
   </vsm:VisualStateManager.VisualStateGroups>
步骤 2.3:为我们的控件编写代码
我们刚刚创建了控件的文件结构和控件模板。现在我们应该为其添加代码。在控件模板中,我们将 Ellipse 的 StrokeThickness 绑定到一个名为 StrokeThickness 的属性。因此,我们首先将 StrokeThickness 添加到我们的控件代码中。
#region StrokeThickness Property
/// <summary>
/// Gets or sets the width of the <see cref="WaitingIcon"/> stroke outline. 
/// </summary>
/// <value>The width of the <see cref="WaitingIcon"/>  outline, in pixels. 
/// The default value is 0. </value>
public double StrokeThickness
{
    get { return (double)GetValue(StrokeThicknessProperty); }
    set { SetValue(StrokeThicknessProperty, value); }
}
/// <summary>
/// Identifies the <see cref="StrokeThickness"/> dependency property. 
/// </summary>
public static readonly DependencyProperty StrokeThicknessProperty =
      DependencyProperty.Register("StrokeThickness", typeof(double), 
        typeof(WaitingIcon), new PropertyMetadata(6.0)); 
#endregion
创建此控件的目的是能够指示应用程序正在忙碌地执行某些操作,因此我们的控件应该有一个属性来指示它是否处于忙碌状态。
#region IsBusy Property
/// <summary>
/// Gets or sets a value indicating is busy or not
/// </summary>
/// <value>A value indicating whether the control is in busy state or not.
/// <para>The default value is <c>false</c>.</para></value>
public bool IsBusy
{
    get { return (bool)GetValue(IsBusyProperty); }
    set { SetValue(IsBusyProperty, value); }
}
/// <summary>
/// Identifies the <see cref="IsBusy"/> dependency property. 
/// </summary>
public static readonly DependencyProperty IsBusyProperty =
       DependencyProperty.Register("IsBusy", typeof(bool), 
          typeof(WaitingIcon),
            new PropertyMetadata(false, IsBusyPropertyChanged));
/// <summary>
/// The <see cref="IsBusy"/> property changed callback function.
/// </summary>
/// <param name="d">The <see cref="WaitingIcon"/>
/// control whosevsee cref="IsBusy"/> property changed.</param>
/// <param name="e">The DependencyPropertyChangedEventArgs
///     contains old and new value.</param>
private static void IsBusyPropertyChanged(DependencyObject d, 
                    DependencyPropertyChangedEventArgs e)
{
     WaitingIcon wi = d as WaitingIcon;
     wi.IsBusyChanged((bool)e.OldValue, (bool)e.NewValue);
} 
#endregion
当 IsBusy 属性设置为 true 时,我们的控件应该可见并显示我们创建的动画。我们只需转到“BusyState”。
/// <summary>
/// The <see cref="IsBusy "/> property changed.
/// </summary>
/// <param name="oldValue">The old value of the
///    <see cref="IsBusy"/> property.</param>
/// <param name="newValue">The new  value of the
///   <see cref="IsBusy"/> property.</param>
protected virtual void IsBusyChanged(bool oldValue, bool newValue)
{
    if (newValue)
    {
         VisualStateManager.GoToState(this, WaitingIcon.BusyStateName, false);
    }
    else
    {
         VisualStateManager.GoToState(this, WaitingIcon.IdleStateName, false);
    }
}
最后一步是将默认控件模板应用于我们的控件。在构造函数中,将 DefaultStyleKey 设置为我们控件的类型。
/// <summary>
/// Initialize a new instance of <see cref="WaitingIcon"/> class.
/// </summary>
public WaitingIcon()
{
    // The default style key
    this.DefaultStyleKey = typeof(WaitingIcon);
}
/// <summary>
/// Apply new template
/// </summary>
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    
    if (this.IsBusy)
    {
         // if set to busy in XAML, we must go to BusyState here
         VisualStateManager.GoToState(this, WaitingIcon.BusyStateName, false);
    }
}
编译我们刚刚创建的项目,我们的控件就可以使用了。
第三部分:使用控件
创建一个新的 Silverlight 应用程序,并为我们的控件程序集添加引用。然后,在 Page.xaml 文件中,将您的控件放在您想要的位置,并设置背景和描边厚度,或者直接使用默认设置。
<UserControl x:Class="WaitingTest.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:cdc="clr-namespace:Cokkiy.Display;assembly=Cokkiy.Display.WaitingIcon"
    Width="400" Height="300">
    <Grid x:Name="LayoutRoot" Background="#FF090808">
        <cdc:WaitingIcon Width="20" Height="20" IsBusy="True">
            <cdc:WaitingIcon.Background>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="#FF070B9C" Offset="0.57599997520446777"/>
                    <GradientStop Color="#FFFFFFFF" Offset="1"/>
                </LinearGradientBrush>
            </cdc:WaitingIcon.Background>
        </cdc:WaitingIcon>
   </Grid>
当您的应用程序处于忙碌状态时,将在代码中设置 IsBusy 属性。
关注点
您可能会注意到,在 IsBusyChanged 和 OnApplyTemplate 函数中,我都执行了相同的检查:检查 IsBusy 属性的值,并在设置为 true 时转到“BusyState”。原因是当您在 XAML 中将 IsBusy 设置为 true 时,IsBusyChanged 函数在模板应用之前被调用。此时,“BusyState”VisualState 根本不存在,什么都不会发生。因此,您需要重新检查,当模板应用后,如果值为 true,您应该在此处转到“BusyState”。




