逐步创建类似 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”。