原文:[UWP]创建一个ProgressControl

1. 前言

博客园终于新增了UWP的分类,我来为这个分类贡献第一篇博客吧。

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 StateChangingEventHandler 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 = 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 useTransitions){string progressState;switch (State){case ProgressState.Ready:progressState = ReadyStateName;break;case ProgressState.Started:progressState = StartedStateName;break;case ProgressState.Completed:progressState = CompletedStateName;break;case ProgressState.Faulted:progressState = FaultedStateName;break;case ProgressState.Paused:progressState = PausedStateName;break;default:progressState = ReadyStateName;break;}VisualStateManager.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();}
}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" /><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}" /></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="" />
</Style><Style TargetType="ContentControl"x:Key="FaultElementStyle"BasedOn="{StaticResource ContentElementStyle}"><Setter Property="Background"Value="MediumVioletRed" /><Setter Property="Content"Value="" />
</Style><Style TargetType="ContentControl"x:Key="PausedElementStyle"BasedOn="{StaticResource ContentElementStyle}"><Setter Property="Background"Value="CornflowerBlue" /><Setter Property="Content"Value="" />
</Style><Style TargetType="ContentControl"x:Key="CancelElementStyle"BasedOn="{StaticResource ContentElementStyle}"><Setter Property="Background"Value="OrangeRed" /><Setter Property="Content"Value="" />
</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

6. 源码

Progress-Control-Sample

[UWP]创建一个ProgressControl相关推荐

  1. [UWP]实现一个轻量级的应用内消息通知控件

    [UWP]实现一个轻量级的应用内消息通知控件 原文:[UWP]实现一个轻量级的应用内消息通知控件 在UWP应用开发中,我们常常有向用户发送一些提示性消息的需求.这种时候我们一般会选择MessageDi ...

  2. linux创建一个交换分区,如何创建linux交换分区

    匿名用户 1级 2017-03-26 回答 1.mkswap 把一个分区格式化成为swap交换区: [root@localhost]# mkswap /dev/sda6 注:创建此分区为swap 交换 ...

  3. 创建一个Scalar-valued Function函数来实现LastIndexOf

    昨天有帮助网友解决的个字符串截取的问题,<截取字符串中最后一个中文词语(MS SQL)>http://www.cnblogs.com/insus/p/7883606.html 虽然实现了, ...

  4. 如何创建一个基础jQuery插件

    如何创建一个基础插件 How to Create a Basic Plugin 有时你想使一块功能性的代码在你代码的任何地方有效.比如,也许你想调用jQuery对象的一个方法,对该对象进行一系列的操作 ...

  5. 只需三分钟!只需创建一个vuex.js文件,让你马上学会使用Vuex,尽管Vuex是个鸡肋!(扔掉store文件夹和里面的index、getters、actions、mutations等js文件吧!)

    前情提示:有一天,我要实现一个效果→点击某个按钮改变一个全局变量,并且要让绑定了该变量的所有位置异步渲染.我试过用一个全局的js文件存放该变量,该变量值虽然改变了,但是没有做到异步渲染.接着我用win ...

  6. 学习在Unity中创建一个动作RPG游戏

    游戏开发变得简单.使用Unity学习C#并创建您自己的动作角色扮演游戏! 你会学到什么 学习C#,一种现代通用的编程语言. 了解Unity中2D发展的能力. 发展强大的和可移植的解决问题的技能. 了解 ...

  7. Unity与C#创建一个3D平台游戏 Learn to Create a 3D Platformer Game with Unity C#

    游戏开发变得容易了.使用Unity学习C#并创建您自己的3D平台! 你会学到什么 学习现代通用编程语言C#. 了解Unity中3D开发的功能 发展强大的可转移的解决问题的技能 了解游戏开发过程 了解面 ...

  8. 学习用C#在Unity中创建一个2D Metroidvania游戏

    学习用C#在Unity中创建一个2D Metroidvania游戏 你会学到: 构建2D Unity游戏 用C#编程 玩家统计,水平提升,米尔和远程攻击 敌方人工智能系统 制定级别和级别选择 Lear ...

  9. 用Unity和Playmaker创建一个限时游戏 Creating a Time Limit game with Unity and Playmaker

    本课程结束时,您将拥有在Unity中使用Playmaker创建游戏的工具 你会学到: playmaker状态的基础以及它们如何与动作一起工作. 安装悬停车,可以在竞技场内行驶. 不同力度的射击地雷驱动 ...

最新文章

  1. 【转】statfs获得硬盘使用情况 模拟linux命令 df
  2. (二)生成深度伪造的方法
  3. 获取程序下基目录下的文件的
  4. 超级简单的jQuery纯手写五星评分效果
  5. 活到老,学到老!各大厂数据库技术解决方案来了
  6. 建设医疗人工智能的“四步曲”
  7. 计算机找不到链接打印机主机,添加打印机找不到任何端口,怎么办
  8. 手游实时阴影方案之Projector Shadow
  9. matlab的零极点分布图,matlab零极点分布图
  10. html 广告加载页面,JS广告代码延迟加载或是最后加载加快页面载入
  11. 新物种爆炸:认知升级时代的新商业思维
  12. [论文学习]TDN: An Integrated Representation Learning Model of Knowledge Graphs
  13. Jenkins+Pipline+Docker 自动部署SpringBoot项目流程
  14. 博客右下角的动态人物(live2d)看板娘
  15. 用CH340给STM32C8T6和野火stm32F103Mini板下载程序需要注意的三个问题
  16. SAP License:如何做好ERP系统的安全防护
  17. python openslide 查看并保存切片的略缩图,并将Image图片转换成Base64
  18. 辐射强度 BRDF概念定义明晰
  19. 数学建模系列-预测模型(四)马尔可夫预测
  20. TMD,被下属拒绝麻了...

热门文章

  1. Kali渗透测试——netdiscover
  2. Gradle在Android中的简单使用
  3. ERROR Could not find value for key log4j.appender.Console
  4. linux自动化作业安排工具cron anacron at batch (2)
  5. 共用软件现漏洞未修复,一年来美国数十个政府网站在推送色情广告
  6. 微软7月修复117个漏洞,其中9个为0day,2个是Pwn2Own 漏洞
  7. 接管任意微软账户并获$5万赏金的故事
  8. 几个大厂及 RCE漏洞二三事
  9. 快学scala-第七章 包和引入
  10. 《云服务器》与《传统服务器》的区别