65.9K
CodeProject 正在变化。 阅读更多。
Home

使用附加 ViewModel 的圆形 ProgressBar 样式

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2011年5月24日

CPOL

5分钟阅读

viewsIcon

22247

如何重新模板化 Silverlight ProgressBar 控件以渲染圆形进度指示器

这篇博文介绍了如何重新模板化 Silverlight ProgressBar 控件以渲染圆形进度指示器。这种方法使用了一个附加的 ViewModel 来绕过 ProgressBar 设计中的一些限制。

这篇博文描述了为 ProgressBar 创建以下炫酷样式(以及对“外观无关”控件工作方式的一点抱怨!)。

在此处查看演示。

如果您厌倦了旋转,请点击暂停按钮!

引言

几天前,我在 Stack Overflow 上回答了一个问题,该问题是 如何创建圆形样式的进度条?(原文如此)。

我的回答是,大多数人似乎都同意,要实现这一点,您必须“从头开始”创建自己的控件。我很欣慰我的答案被采纳,但同时又有点不高兴,因为这应该是正确的答案。毕竟,Silverlight / WPF 赋予您创建“外观无关”控件的能力,而圆形进度条不就是普通进度条的另一种外观或“皮肤”吗?

ProgressBar 有什么问题?

如果您查看 有关 ProgressBar 的样式/模板化的文档,您会发现该控件期望模板包含两个元素:ProgressBarTrackProgressBarIndicator

当应用模板时,ProgressBarOnApplyTemplate 中会查找具有给定名称的元素,以更新 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;
        }
    }
}

从上面的代码可以看出,ElementTrackElementIndicator 元素(模板中两个命名的元素)的各种属性正在被**编程方式**更新。这基本上将 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.

© . All rights reserved.