Silverlight 2.0 组件开发
本文基于我们在一个项目中积累的 Silverlight 经验,希望分享一些知识。
使用 WPF 创建组件
Silverlight 是一个面向 Web 解决方案的前景平台。微软一直在积极开发它,试图取代 Macromedia Flash 的现有地位。互联网上充斥着大量基于 Silverlight 的令人印象深刻的示例。然而,不幸的是,很难具体说明其中涉及的工作量以及可能遇到的陷阱。作为我们实验的对象,我们选择了一个用于设置特定数值范围的控件。稍后您将了解我们最终的成果,以及我们遇到的困难和陷阱。
因此,让我们来明确任务。该控件应提供以下功能:
- 从可能的数值范围内选择一个值
- 显示选定的值
- 显示可能数值的范围
- 吸引人的图形设计。
例如,该控件可能看起来像这样
主要类
我们需要能够定义当前值,由 `Value` 属性负责。此外,我们还需要知道最大值、最小值和步长 - `MinValue`、`MaxValue`、`Step`。最好能有一个选项来调整刻度开始和结束的位置,这将由角度来设置 - `StartAngle`、`SweepAngle`。为了设置边框、刻度和其他项,我们将使用模板。
首先,我们创建基本的 `Indicator` 类,包含最少必需的功能:`MinValue`、`MaxValue`、`Step`。我们继承自 `Control` 类。然后,我们创建 `CircularIndicator` 类,继承自 `Indicator` 类,并在其中定义以下属性:`StartAngle`、`SweepAngle`、`PointerAngle`。`PointerAngle` 将用于绑定。这里我们还将定义鼠标事件的处理程序;我们需要响应鼠标点击、移动以改变指针的位置。
在这里,我们应该指出,在 Silverlight 2.0 中,无法将 `OnMouseEnter`、`OnMouseMove` 等方法定义为类成员。相反,我们必须订阅相应的事件,这有点不寻常,但并不会造成任何不便。
计算 `PointerAngle` 的公式相对简单
StartAngle + Value * SweepAngle / (MaxValue - MinValue)
基本上就这些了,我们只需要添加更改属性的事件。但是,这里有一个技巧。问题在于,与 WPF 不同,Silverlight 2.0 中的 `Dependency` 属性只能指向 4 个参数,这显然不够。缺少设置元数据或回调函数(例如,在本例中检查属性很有用,`CoerceValueCallback`)的选项。
创建用于刻度显示的面板
让我们转向视觉部分。第一个遇到的问题是如何显示刻度。很明显,可以创建一个面板,并在其中放置 15 个矩形作为刻度线,以一定角度排列。然而,这并不是一个吸引人的解决方案。更简单优雅的方法是定义一个继承自 `Panel` 类的类,称之为 `CircularElementPanel`,通过它来定义数量、元素模板、初始和最终角度以及圆的半径。这将允许自动创建元素。显然,该类也适用于创建刻度的标题,只需添加 `StartValue` 和 `EndValue`。然而,这并不那么容易,因为我们还必须在模板中绑定某些内容来显示值。
我们是这样做的
首先,我们创建了 `DataField` 类。
public class DataField
{
private object value = null;
public object ElementValue
{
get { return this.value; }
set { this.value = value; }
}
public DataField(object value)
{
this.value = value;
}
}
在创建单独的元素时,`DataField` 对象将被分配给其 `DataContext` 属性。下面提供了一个示例代码
private static void RecreateElements(CircularElementsPanel panel)
{
DataTemplate elementTemplate = panel.ElementsTemplate;
if (elementTemplate == null) return;
double value = panel.StartValue;
double valueStep = (panel.EndValue - panel.StartValue) / (panel.ElementsCount - 1);
panel.Children.Clear();
for (int i = 0; i < panel.ElementsCount; i++)
{
UIElement element = (UIElement)panel.ElementsTemplate.LoadContent();
if (element is FrameworkElement)
{
((FrameworkElement)element).DataContext = new DataField(value + i * valueStep);
}
panel.Children.Add(element);
}
}
然后在元素的模板中,可以创建如下所示的绑定
<c:CircularElementsPanel.ElementsTemplate>
<DataTemplate>
<TextBlock Width=”50? TextAlignment=”Center” Text=”{Binding ElementValue}”/>
</DataTemplate>
</c:CircularElementsPanel.ElementsTemplate>
这些元素会沿着圆周排列吗?为了实现这种效果,我们需要在我们的面板上预先定义 `ArrangeOverride` 和 `MeasureOverride` 方法。我们对其进行了简化——使用了 CodeProject 的 `PolarPanel`,它预定义了这些方法并从中继承。尽管它是为 WPF 编写的,但没有问题,唯一需要重新加工的是属性注册。然而,我们应该指出,该面板还包含两个附加属性——`angle` 和 `radius`。`Radius` 将被设置为可能的最大值(考虑到最终面板大小),而 `angle` 将在 `RecreateElements` 方法中定义。考虑到最新的更改,该函数如下所示
private static void RecreateElements(CircularElementsPanel panel)
{
DataTemplate elementTemplate = panel.ElementsTemplate;
if (elementTemplate == null) return;
double angle = panel.StartAngle;
double angleStep = panel.SweepAngle / (panel.ElementsCount - 1);
double value = panel.StartValue;
double valueStep = (panel.EndValue - panel.StartValue) / (panel.ElementsCount - 1);
panel.Children.Clear();
for (int i = 0; i < panel.ElementsCount; i++)
{
UIElement element = (UIElement)panel.ElementsTemplate.LoadContent();
SetRadius(element, panel.ElementsRadius);
SetAngle(element, angle + i * angleStep);
if (element is FrameworkElement)
{
((FrameworkElement)element).DataContext = new DataField(value + i * valueStep);
}
panel.Children.Add(element);
}
}
最后,让我们创建另一个类——`CircularGauge` 或 `Gauge`。在这个类中,只有一个属性——旋钮转动的角度。更改属性时,我们应用 `RotateTransform`。
创建控件模板
以下是 `CircularIndicator` 的完整模板。这应该有助于解决稍后描述的问题。
<ControlTemplate x:Key=”Indicator” TargetType =”w:CircularIndicator”>
<Grid x:Name=”LayoutRoot” Margin=”0?>
<Rectangle RadiusX=”5? RadiusY=”5? Margin=”0? Stroke=”Black”>
<Rectangle.Fill>
<LinearGradientBrush StartPoint=”0,0? EndPoint=”0,1?>
<LinearGradientBrush.GradientStops>
<GradientStop Color=”Lavender” Offset=”0?/>
<GradientStop Color=”Gray” Offset=”0.1?/>
<GradientStop Color=”LightGray” Offset=”0.5?/>
<GradientStop Color=”Gray” Offset=”0.9?/>
<GradientStop Color=”Lavender” Offset=”1?/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<c:CircularElementsPanel x:Name=”ScaleLabels”
StartValue=”{TemplateBinding MinValue}”
EndValue=”{TemplateBinding MaxValue}” Margin=”50?
StartAngle=”{TemplateBinding StartAngle}”
SweepAngle=”{TemplateBinding SweepAngle}”
ElementsCount=”{TemplateBinding ValuesCount}”
RotateElements=”False”>
<c:CircularElementsPanel.ElementsTemplate>
<DataTemplate>
<TextBlock Width=”50? TextAlignment=”Center”
Text=”{Binding ElementValue}”/>
</DataTemplate>
</c:CircularElementsPanel.ElementsTemplate>
</c:CircularElementsPanel>
<c:CircularElementsPanel StartValue=”{TemplateBinding MinValue}”
EndValue=”{TemplateBinding MaxValue}”
Margin=”80? StartAngle=”{TemplateBinding StartAngle}”
SweepAngle=”{TemplateBinding SweepAngle}”
ElementsCount=”{TemplateBinding ValuesCount}”
RotateElements=”True”>
<c:CircularElementsPanel.ElementsTemplate>
<DataTemplate>
<Rectangle Fill=”Black” Width=”5? Height=”2?/>
</DataTemplate>
</c:CircularElementsPanel.ElementsTemplate>
</c:CircularElementsPanel>
<w:CircularGauge Margin=”90? Template=”{StaticResource Gauge}”
Angle=”{TemplateBinding PointerAngle}”/>
</Grid>
</ControlTemplate>
首先看到的是带有渐变填充的背景层,然后是刻度和它的标题,最后是仪表盘。请注意刻度线的数量绑定到 `ValuesCount` 属性。这可以轻松计算
((MaxValue - MinValue) / Step) + 1;
绑定表达式的赋值
将标题和刻度线的数量与 `ValuesCount` 绑定会很好,但是没有这样的可能性。老实说,我们仍然不敢相信,我们在搜索设置 `a*b` 类型绑定表达式的解决方案上花费的时间没有取得任何结果。
我们想提请您注意的第二项是 `Gauge`。你可能会问,这是做什么用的?实际上,它的模板由两个椭圆组成。为什么不在模板中创建 `RotateTransform` 并将角度值绑定到 `PointerAngle` 属性值?然而,我们不知何故做不到。尽管 `Angle = “{TemplateBinding PointerAngle}”` 表达式在编译时不会报错,但也没有带来任何积极的结果。为什么?这仍然是个谜。
因此,这就是创建 `CircularGauge` 类的原因。还值得注意的是,在 WPF 中,我们可以使用 Binding 的 `RelativeSource` 属性轻松解决此任务,但在 Silverlight 2.0 中它不存在。我们希望它能在后续版本中出现,因为没有它,很难创建实质性的东西。
此外,`RelativeSource` 的存在也将允许绕过设置绑定表达式的问题。`Converter` 可以通过某个对象参数化。该对象将定义操作和其中一个操作数;第二个操作数将由绑定字段定义。不幸的是,`TemplateBinding` 中没有 `Converter` 属性,因此无法通过它来解决问题。
动画
最后,第三个方面是动画。如果在 WPF 中,我们可以在控件模板中创建一组触发器,例如在鼠标悬停时,然后从那里开始动画(`Storyboard`),但这里有另一种方法。借助 `TemplatePart` 属性,我们定义了应包含在类控件模板中的元素的名称和类型。在这里,我们可以设置用于动画的 `Storyboard` 名称以及资源中存储 `Storyboard` 的元素的名称。
[TemplatePart(Name = “RootElement”,
Type = typeof(FrameworkElement))] [TemplatePart(Name = “Normal State”,
Type = typeof(Storyboard))]
此外,在构造函数中,我们订阅 `MouseEnter` 和 `MouseLeave` 事件,并创建以下处理程序
void CircularGauge_MouseLeave(object sender, MouseEventArgs e)
{
FrameworkElement panel = this.GetTemplateChild(“RootElement”) as FrameworkElement;
(panel.Resources[“MouseOver State”] as Storyboard).Stop();
}
void CircularGauge_MouseEnter(object sender, MouseEventArgs e)
{
FrameworkElement panel = this.GetTemplateChild(“RootElement”) as FrameworkElement;
(panel.Resources[“MouseOver State”] as Storyboard).Begin();
}
结果是,当鼠标指向控件时,动画开始;当鼠标移开时,动画停止。动画在我们的示例中是在 `Gauge` 上实现的。那么 `Storyboard` 看起来是这样的
<Storyboard x:Key=’MouseOver State’>
<ColorAnimation Storyboard.TargetName=’Stop’ Storyboard.TargetProperty=’Color’
To=’White’ Duration=”0:0:0.5? AutoReverse=’False’/>
</Storyboard>
摘要
还应该注意到,尽管在 Silverlight 开发过程中存在所有的小故障和困难,但这无疑是用户友好且外观精美的 Web 界面交互的一大进步。当然,这项技术仍在发展中,但开发者的工具尚未调试完善(几乎所有错误都会收到相同的消息,并且经常陷入无限循环),并且与 WPF 相比,自定义选项很少或几乎没有(模板中的触发器、绑定、动画)。然而,为 Web 创建管理元素变得更加容易,此外,Silverlight 技术在编程和图形设计之间划清了界限。
历史
- 2008 年 4 月 25 日:首次发布