创建一个ProgressControl

. 前言

  
  UWP有很多问题,先不说生态的事情,表单、验证、输入、设计等等一堆基本问题缠身。但我觉得最应该首先解决的绝对是Blend,那个随随便便就崩溃、报错、比Silverlight时代还差、不能用的Blend For Visal Studio。不过无论Blend怎么坏都不能让我写漂亮控件的心屈服,毕竟写了这么多年XAML,只靠Visual Studio勉勉强强还是可以写样式的,这篇文章介绍的控件就几乎全靠Visual Studio写了全部样式(其实VisalStudio的设计器也一直报错)。
  
  在之前写的文章 创建一个进度按钮 中我实现了一个ProgressButton,它主要有以下几个功能:
  
  有Ready、Started、Completed、Faulted四种状态;
  
  从Ready状态切换到Started状态按钮会从方形变成圆形;
  
  在Started状态下使用Ellipse配合StrokeDashArray显示进度;
  
  完成后可切换到Completed状态;
  
  出错后可切换到Faulted状态;
  
  运行效果如下:
  
  无论是实现过程还是结果都很有趣,但还是有几个问题:
  
  没有Paused状态;
  
  Progress限定在0到1之间,其实应该参考ProgressBar可以Minimum和Maximum;
  
  除了可以点击这点好像和Button关系不大,所以也不应该命名为-Button;
  
  因为以上理由决定做个新的控件。
  
  2. 改进的结果
  
  新控件名就叫ProgressControl---因为无奈真的想不到叫什么名字了。运行效果如下:
  
  它有Ready、Started、Completed、Faulted和Paused五个状态。其中Paused即暂停状态,在Started状态点击控件将可进入Paused状态,并且显示CancelButton,这时候点击CancelButton将回到Ready状态;当然点击继续的图标就回到Started状态。
  
  3. 实现
  
  由于ProgressControl的Control Template已经十分复杂,所以将它拆分成两个部分:
  
  ProgressStateIndicator,主要用于显示各种状态,功能和以前的ProgressButton相似,还是直接继承自Button;
  
  CancellButton,外观上模仿progressStateIdicator,在Paused状态下显示;
  
  懒得为它命名的Ellipse,用于在Started状态下显示进度;
  
  ProgressControl由以上三部分组成,Ready状态(默认状态)下只显示ProgressStateIndicator,点击ProgressStateIndicator触发EventHandler StateChanging和EventHandler StateChanged事件并转换状态;Started状态下同时显示Ellipse;Paused状态下隐藏Ellipse并显示CancelButton。
  
  3.1处理代码
  
  和之前强调的一样,先完成代码部分再完成UI部分会比较高效。而且UI部分怎么呈现、怎么做动画都是它的事,代码部分完成后就可以甩手不管由得XAML去折腾了。
  
  首先完成ProgressStateIndicator,继承Button,提供一个public ProgressState State { get; set; }属性,并在State改变时改变VisualState。它的功能仅此而已,之所以把它独立出来是因为清楚知道它的ControlTemplate比较复杂,如果不把它独立出来ProgressControl的ControlTemplate就复杂到没法维护了。代码如下:
  
  [TemplateVisualState(GroupName = ProgressStatesGroupName, Name = ReadyStateName)]
  
  [TemplateVisualState(GroupName = ProgressStatesGroupName, Name = StartedStateName)]
  
  [TemplateVisualState(GroupName = ProgressStatesGroupName, Name = CompletedStateName)]
  
  [TemplateVisualState(GroupName = ProgressStatesGroupName, Name = FaultedStateName)]
  
  [TemplateVisualState(GroupName = ProgressStatesGroupName, Name = PausedStateName)]
  
  public partial class ProgressStateIndicator : Button
  
  {
  
  public ProgressStateIndicator()
  
  {
  
  this.DefaultStyleKey = typeof(ProgressStateIndicator);
  
  }
  
  /// <summary>
  
  /// 获取或设置State的值
  
  /// </summary>
  
  public ProgressState State
  
  {
  
  get { return (ProgressState)GetValue(StateProperty); }
  
  set { SetValue(StateProperty, value); }
  
  }
  
  /// <summary>
  
  /// 标识 State 依赖属性。
  
  /// </summary>
  
  public static readonly DependencyProperty StateProperty =
  
  DependencyProperty.Register("State", typeof(ProgressState), typeof(ProgressStateIndicator), new PropertyMetadata(ProgressState.Ready, OnStateChanged));
  
  private static void OnStateChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
  
  {
  
  ProgressStateIndicator target = www.365soke.cn  obj as ProgressStateIndicator;
  
  ProgressState oldValue = (ProgressState)args.OldValue;
  
  ProgressState newValue = (ProgressState)args.NewValue;
  
  if (oldValue != newValue)
  
  target.OnStateChanged(oldValue, newValue);
  
  }
  
  protected override void OnApplyTemplate()
  
  {
  
  base.OnApplyTemplate();
  
  UpdateVisualStates(false);
  
  }
  
  protected virtual void OnStateChanged(ProgressState oldValue, ProgressState newValue)
  
  {
  
  UpdateVisualStates(true);
  
  }
  
  private void UpdateVisualStates(bool www.2636666.cn  useTransitions)
  
  {
  
  string progressState;
  
  switch (State)
  
  {
  
  case ProgressState.Ready:
  
  progressState = ReadyStateName;
  
  break;
  
  case ProgressState.Started:
  
  progressState = StartedStateName;
  
  break;
  
  case ProgressState.www.dashuju178.com Completed:
  
  progressState = CompletedStateName;
  
  break;
  
  case ProgressState.Faulted:
  
  progressState = FaultedStateName;
  
  break;
  
  case ProgressState.Paused:
  
  progressState = PausedStateName;
  
  break;
  
  default:
  
  progressState = ReadyStateName;
  
  break;
  
  }
  
  VisualStateManager.www.huarenyl.cn GoToState(this, progressState, useTransitions);
  
  }
  
  }
  
  代码是很普通的模板化控件的做法,记住OnApplyTemplate()中的UpdateVisualStates(false)参数一定要是False。
  
  接下来完成ProgressControl。ProgressControl继承RangeBase,只是为了可以使用它的Maximum、Minimum和Value三个属性。为了可以显示内容模仿ContentControl实现了Content属性,因为不是直接继承ContentControl,所以要为控件添加[ContentProperty(Name = nameof(Content))]Attribute。模仿ContentControl的部分代码可见 了解模板化控件(2):模仿ContentControl 。
  
  ProgressCotrol也提供了public ProgressState State { get; set; }属性,这部分和ProgressStateIndicator基本一致。
  
  最后是两个TemplatePart:ProgressStateIndicator和CancelButton。点击这两个控件触发状态改变的事件并改变VisualState:
  
  protected override void OnApplyTemplate()
  
  {
  
  base.OnApplyTemplate();
  
  _progressStateIndicator = GetTemplateChild(ProgressStateIndicatorName) as ProgressStateIndicator;
  
  if (_progressStateIndicator != null)
  
  _progressStateIndicator.Click += OnGoToNextState;
  
  _cancelButton = GetTemplateChild(CancelButtonName) as Button;
  
  if (_cancelButton != null)
  
  _cancelButton.Click += OnCancel;
  
  UpdateVisualStates(false);
  
  }
  
  private void OnGoToNextState(object sender, RoutedEventArgs e)
  
  {
  
  switch (State)
  
  {
  
  case ProgressState.Ready:
  
  ChangeStateCore(ProgressState.Started);
  
  break;
  
  case ProgressState.Started:
  
  ChangeStateCore(ProgressState.Paused);
  
  break;
  
  case ProgressState.Completed:
  
  ChangeStateCore(ProgressState.Ready);
  
  break;
  
  case ProgressState.Faulted:
  
  ChangeStateCore(ProgressState.Ready);
  
  break;
  
  case ProgressState.Paused:
  
  ChangeStateCore(ProgressState.Started);
  
  break;
  
  default:
  
  throw new ArgumentOutOfRangeException(www.taohuayuan178.com);
  
  }
  
  }
  
  private void OnCancel(object sender, RoutedEventArgs e)
  
  {
  
  if (ChangeStateCore(ProgressState.Ready))
  
  Cancelled?.Invoke(this, EventArgs.Empty);
  
  }
  
  private bool ChangeStateCore(ProgressState newstate)
  
  {
  
  var args = new ProgressStateEventArgs(State, newstate);
  
  OnStateChanging(args);
  
  StateChanging?.Invoke(this, args);
  
  if (args.Cancel)
  
  return false;
  
  State = newstate;
  
  return true;
  
  }
  
  至于Value属性不需要任何处理,只是给UI提供可绑定的属性就够了。
  
  3.2 处理UI
  
  大部分UI部分用到的技术都在上一篇文章 创建一个进度按钮 介绍过了,这次只做了一些改进。
  
  3.2.1 ContentControlStyle
  
  <Style TargetType="ContentControl"
  
  x:Key="ContentElementStyle">
  
  <Setter Property="Foreground"
  
  Value="White" www.douniu178.com/>
  
  <Setter Property="Template">
  
  <Setter.Value>
  
  <ControlTemplate TargetType="ContentControl">
  
  <Grid Margin="0"
  
  HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
  
  VerticalAlignment="{TemplateBinding VerticalAlignment}">
  
  <control:DropShadowPanel OffsetX="0"
  
  OffsetY="0"
  
  BlurRadius="5"
  
  ShadowOpacity="0.3"
  
  VerticalContentAlignment="Stretch"
  
  HorizontalContentAlignment="Stretch">
  
  <Ellipse x:Name="CompletedRectangle"
  
  Fill="{TemplateBinding Background}" www.thd178.com/>
  
  </control:DropShadowPanel>
  
  <FontIcon Glyph="{TemplateBinding Content}"
  
  Foreground="{TemplateBinding Foreground}"
  
  FontSize="{TemplateBinding FontSize}"
  
  VerticalAlignment="Center"
  
  HorizontalAlignment="Center"
  
  x:Name="CompletedIcon" />
  
  </Grid>
  
  </ControlTemplate>
  
  </Setter.Value>
  
  </Setter>
  
  </Style>
  
  <Style TargetType="ContentControl"
  
  x:Key="CompltedElementStyle"
  
  BasedOn="{StaticResource ContentElementStyle}">
  
  <Setter Property="Background"
  
  Value="LightSeaGreen" />
  
  <Setter Property="Content"
  
  Value="&#xE001;" />
  
  </Style>
  
  <Style TargetType="ContentControl"
  
  x:Key="FaultElementStyle"
  
  BasedOn="{StaticResource ContentElementStyle}">
  
  <Setter Property="Background"
  
  Value="MediumVioletRed" />
  
  <Setter Property="Content"
  
  Value="&#xE10A;" />
  
  </Style>
  
  <Style TargetType="ContentControl"
  
  x:Key="PausedElementStyle"
  
  BasedOn="{StaticResource ContentElementStyle}">
  
  <Setter Property="Background"
  
  Value="CornflowerBlue" />
  
  <Setter Property="Content"
  
  Value="&#xE768;" />
  
  </Style>
  
  <Style TargetType="ContentControl"
  
  x:Key="CancelElementStyle"
  
  BasedOn="{StaticResource ContentElementStyle}">
  
  <Setter Property="Background"
  
  Value="OrangeRed" />
  
  <Setter Property="Content"
  
  Value="&#xE10A;" />
  
  </Style>
  
  之前的ProgressButton中ControlTemplate有些复杂,这次用于Started、Completed和Faulted等状态下显示的元素都使用样式并统一了它们的ContentTemplete,大大简化了ProgressStateIndicator的ControlTemplate。
  
  3.2.2 Animation​Set
  
  在Started到Paused之间有一个平移的过渡,为了使位移根据元素自身的宽度决定我写了个RelativeOffsetBehavior,里面用到了UWP Community Toolkit 的 Animation​Set :
  
  if (AssociatedObject != null)
  
  {
  
  var offsetX = (float)(AssociatedObject.ActualWidth * OffsetX);
  
  var offsetY = (float)(AssociatedObject.ActualHeight * OffsetY);
  
  var animationSet = AssociatedObject.Offset(offsetX, offsetY, duration: 0, easingType: EasingType.Default);
  
  animationSet?.Start();
  
  }
  
  3.2.3 Implicit Composition Animations
  
  由于有些动画是重复的,例如显示进度的Ellipse从Ready到Started及从Paused到Started都是从Collapsed变到Visible,并且Opacity从0到1。为了减轻VisualTransition的负担,在VisualTransition中只改变Ellipse的Visibility,Opacity的动画使用了UWP Community Toolkit 的 Implicit Composition Animations :
  
  <animations:Implicit.HideAnimations>
  
  <animations:ScalarAnimation Target="Opacity"
  
  Duration="0:0:1"
  
  To="0.0"/>
  
  </animations:Implicit.HideAnimations>
  
  <animations:Implicit.ShowAnimations>
  
  <animations:OpacityAnimation Duration="0:0:3"
  
  From="0"
  
  To="1.0" />
  
  </animations:Implicit.ShowAnimations>
  
  这段XML即当Ellipse的Visibility值改变时调用的动画。
  
  4. 结语
  
  ProgressControl已经很复杂了,只是这个控件XAML就多达800行,还有一些Behavior配合。如果可以使用Blend的话可能可以减少一些XAML,而且精力都放在XAML上,可能还有考虑不周的地方。
  
  除了使用UWP Community Toolkit的部分基本上移植到WPF,而UWP Community Toolkit的部分应该也可以使用其它方法代替。
  
  5. 参考
  
  创建一个进度按钮
  
  Animation​Set
  
  Implicit Composition Animations
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值