使用附加 ViewModel 的圆形 ProgressBar 样式





0/5 (0投票)
如何重新模板化 Silverlight ProgressBar 控件以渲染圆形进度指示器
这篇博文介绍了如何重新模板化 Silverlight ProgressBar 控件以渲染圆形进度指示器。这种方法使用了一个附加的 ViewModel 来绕过 ProgressBar 设计中的一些限制。
这篇博文描述了为 ProgressBar
创建以下炫酷样式(以及对“外观无关”控件工作方式的一点抱怨!)。
如果您厌倦了旋转,请点击暂停按钮!
引言
几天前,我在 Stack Overflow 上回答了一个问题,该问题是 如何创建圆形样式的进度条?(原文如此)。
我的回答是,大多数人似乎都同意,要实现这一点,您必须“从头开始”创建自己的控件。我很欣慰我的答案被采纳,但同时又有点不高兴,因为这应该是正确的答案。毕竟,Silverlight / WPF 赋予您创建“外观无关”控件的能力,而圆形进度条不就是普通进度条的另一种外观或“皮肤”吗?
ProgressBar 有什么问题?
如果您查看 有关 ProgressBar 的样式/模板化的文档,您会发现该控件期望模板包含两个元素:ProgressBarTrack
和 ProgressBarIndicator
。
当应用模板时,ProgressBar
在 OnApplyTemplate
中会查找具有给定名称的元素,以更新 UI 的视觉状态。您可以使用 Reflector(快速,趁它还免费!)来查看 ProgressBar.SetProgressBarIndicatorLength
方法中这些元素的**状态**是如何更新的。
private void SetProgressBarIndicatorLength()
{
double minimum = base.Minimum;
double maximum = base.Maximum;
double num3 = base.Value;
if ((this.ElementTrack != null) && (this.ElementIndicator != null))
{
FrameworkElement parent = VisualTreeHelper.GetParent
(this.ElementIndicator) as FrameworkElement;
if (parent != null)
{
double num4 = this.ElementIndicator.Margin.Left +
this.ElementIndicator.Margin.Right;
Border border = parent as Border;
if (border != null)
{
num4 += border.Padding.Left + border.Padding.Right;
}
else
{
Control control = parent as Control;
if (control != null)
{
num4 += control.Padding.Left + control.Padding.Right;
}
}
double num5 = (this.IsIndeterminate ||
(maximum == minimum)) ? 1.0 : ((num3 - minimum) / (maximum - minimum));
double num6 = Math.Max((double) 0.0, (double) (parent.ActualWidth - num4));
this.ElementIndicator.Width = num5 * num6;
}
}
}
从上面的代码可以看出,ElementTrack
和 ElementIndicator
元素(模板中两个命名的元素)的各种属性正在被**编程方式**更新。这基本上将 ProgressBar
的重新模板化能力限制在“指示器”元素的宽度是其父元素的某个比例的情况。这很不“外观无关”!
那么,从头开始创建自己的圆形进度指示器有什么不好?首先,涉及面向对象的设计原则和重用问题。其次,在我看来,更重要的是它如何影响皮肤化。模板允许您通过应用一组新的样式来彻底改变您的 UI,例如查看 Silverlight Toolkit 主题。样式可以更改元素的任何属性的值(包括其模板),但不能更改类本身!因此,如果您将圆形进度条创建为新控件,您就不能通过应用主题来将其与标准的 ProgressBar
互换。
附加 ViewModel
好了,抱怨结束。是时候解决问题了!
几个月前,我写了一篇关于如何使用附加 ViewModel 创建完全“外观无关”控件的博文。这种方法的基本概念是,控件本身不应包含与特定模板或“外观”紧密耦合的任何逻辑。这种逻辑仍然是必需的,但而是通过附加 ViewModel 的方式引入到模板中的。
通常,控件模板内的元素继承与控件相同的 DataContext
,即您已绑定到 UI 的任何业务对象或 ViewModel。通过附加 ViewModel 的方法,一个 ViewModel 被附加到模板的根元素。附加后,该 ViewModel 会获得对 ProgressBar
的引用,以便调整其属性,使其更容易渲染圆形指示器,并将其本身设置为子元素的 DataContext
。
ViewModel 在 XAML 中按如下方式附加,结果是模板内任何元素的 DataContext
现在都是 ViewModel。
<ControlTemplate TargetType="ProgressBar">
<Grid x:Name="LayoutRoot">
<!-- attach the view model -->
<local:CircularProgressBarViewModel.Attach>
<local:CircularProgressBarViewModel/>
</local:CircularProgressBarViewModel.Attach>
<!-- the rest of the template now has
CircularProgressBarViewModel as the DataContext -->
</Grid>
</ControlTemplate>
成为附加的
下面是 Attach
属性的更改处理程序。总而言之,在附加时,ViewModel 会将自己设置为其已附加到的元素的 DataContext
。然后,它处理 Loaded
事件,该事件在 UI 完全构建时触发,以便使用 Linq to VisualTree 来定位 ProgressBar
。
/// <summary>
/// Change handler for the Attach property
/// </summary>
private static void OnAttachChanged
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// set the view model as the DataContext for the rest of the template
FrameworkElement targetElement = d as FrameworkElement;
CircularProgressBarViewModel viewModel = e.NewValue as CircularProgressBarViewModel;
targetElement.DataContext = viewModel;
// handle the loaded event
targetElement.Loaded += new RoutedEventHandler(Element_Loaded);
}
/// <summary>
/// Handle the Loaded event of the element to which this view model is attached
/// in order to enable the attached
/// view model to bind to properties of the parent element
/// </summary>
static void Element_Loaded(object sender, RoutedEventArgs e)
{
FrameworkElement targetElement = sender as FrameworkElement;
CircularProgressBarViewModel attachedModel = GetAttach(targetElement);
// find the ProgressBar and associated it with the view model
var progressBar = targetElement.Ancestors<ProgressBar>().Single() as ProgressBar;
attachedModel.SetProgressBar(progressBar);
}
一旦 ViewModel 与进度条关联,它就可以计算有助于创建圆形模板的属性,例如用于表示特定进度值的角度。
/// <summary>
/// Add handlers for the updates on various properties of the ProgressBar
/// </summary>
private void SetProgressBar(ProgressBar progressBar)
{
_progressBar = progressBar;
_progressBar.SizeChanged += (s, e) => ComputeViewModelProperties();
RegisterForNotification("Value", progressBar, (d,e) => ComputeViewModelProperties());
RegisterForNotification("Maximum", progressBar, (d, e) => ComputeViewModelProperties());
RegisterForNotification("Minimum", progressBar, (d, e) => ComputeViewModelProperties());
ComputeViewModelProperties();
}
/// Add a handler for a DP change
/// see: http://amazedsaint.blogspot.com/2009/12/silverlight-listening-to-dependency.html
private void RegisterForNotification
(string propertyName, FrameworkElement element, PropertyChangedCallback callback)
{
//Bind to a dependency property
Binding b = new Binding(propertyName) { Source = element };
var prop = System.Windows.DependencyProperty.RegisterAttached(
"ListenAttached" + propertyName,
typeof(object),
typeof(UserControl),
new PropertyMetadata(callback));
element.SetBinding(prop, b);
}
感谢 Anoop 发布了一个 nice and simple 的方法来注册依赖属性的更改通知(DP 不实现 INotifyPropertyChanged
模式真让人头疼!)。
每次进度条上的某个属性发生更改时,以下方法都会更新附加 ViewModel 公开的几个 CLR 属性。
/// <summary>
/// Re-computes the various properties that the elements in the template bind to.
/// </summary>
protected virtual void ComputeViewModelProperties()
{
if (_progressBar == null)
return;
Angle = (_progressBar.Value - _progressBar.Minimum) * 360 /
(_progressBar.Maximum - _progressBar.Minimum);
CentreX = _progressBar.ActualWidth / 2;
CentreY = _progressBar.ActualHeight / 2;
Radius = Math.Min(CentreX, CentreY);
Diameter = Radius * 2;
InnerRadius = Radius * HoleSizeFactor;
Percent = Angle / 360;
}
下面是此博文顶部看到的其中一个已设置样式的进度条的完整 XAML。在这里,您可以看到模板内的各种 UI 元素如何绑定到附加的 ViewModel。
<Style TargetType="ProgressBar" x:Key="PieProgressBarStyle">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ProgressBar">
<Grid x:Name="LayoutRoot">
<!-- attach the view model -->
<local:CircularProgressBarViewModel.Attach>
<local:CircularProgressBarViewModel HoleSizeFactor="0.75"/>
</local:CircularProgressBarViewModel.Attach>
<!-- a circular outline -->
<Ellipse Width="{Binding Diameter}" Height="{Binding Diameter}"
HorizontalAlignment="Center" VerticalAlignment="Center"
Stroke="LightGray" Fill="Transparent"
StrokeThickness="0.3">
</Ellipse>
<!-- a pie-piece that indicates the progress -->
<local:PiePiece CentreX="{Binding CentreX}" CentreY="{Binding CentreY}"
RotationAngle="0" WedgeAngle="{Binding Angle}"
Radius="{Binding Radius}" Fill="LightBlue"/>
<!-- progress as a percent -->
<Grid util:GridUtils.RowDefinitions="*,3.5*,*"
util:GridUtils.ColumnDefinitions="*,3.5*,*">
<TextBlock Text="{Binding Percent, StringFormat=0%}"
Foreground="DarkBlue"
FontWeight="Bold" FontSize="20"
Grid.Row="1" Grid.Column="1"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
(模板使用了一个 PiePiece
,这是我几年前从我创建的 PieChart 控件借用的一个控件,以及 简化的 Grid 语法。)
我们现在有了一个圆形的 ProgressBar
!……
分段进度条
为了好玩,我扩展了附加 ViewModel,以允许轻松创建渲染为离散分段的圆形进度条。附加到模板的 SegmentedProgressBarViewModel
公开一个对象集合,允许通过 ItemsControl
创建分段指示器。有关完整详细信息,请下载博文源代码。
<Style TargetType="ProgressBar" x:Key="SegmentedProgressBarStyle">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ProgressBar">
<Grid x:Name="LayoutRoot">
<!-- attach the view model -->
<local:CircularProgressBarViewModel.Attach>
<local:SegmentedProgressBarViewModel HoleSizeFactor="0.7"/>
</local:CircularProgressBarViewModel.Attach>
<!-- render the segments -->
<ItemsControl ItemsSource="{Binding Segments}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<!-- a grey segment -->
<local:PiePiece CentreX="{Binding Parent.CentreX}"
CentreY="{Binding Parent.CentreY}"
RotationAngle="{Binding StartAngle}"
WedgeAngle="{Binding WedgeAngle}"
Radius="{Binding Parent.Radius}"
InnerRadius="{Binding Parent.InnerRadius}"
Fill="LightGray" Stroke="White" Opacity="0.5"/>
<!-- a blue segment, with an Opacity bound to the view model -->
<local:PiePiece CentreX="{Binding Parent.CentreX}"
CentreY="{Binding Parent.CentreY}"
RotationAngle="{Binding StartAngle}"
WedgeAngle="{Binding WedgeAngle}"
Radius="{Binding Parent.Radius}"
InnerRadius="{Binding Parent.InnerRadius}"
Fill="DarkBlue" Stroke="White" Opacity="{Binding Opacity}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
上面的标记生成了以下样式:
此博文的源代码包含其他几种样式,包括一个从 Pete Brown 关于饼图样式的博文借用的“玻璃”效果。
源代码
您可以在此处下载此博文的完整源代码。
此致,
Colin E.