[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 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 = 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 AnimationSet
在Started到Paused之间有一个平移的过渡,为了使位移根据元素自身的宽度决定我写了个RelativeOffsetBehavior,里面用到了UWP Community Toolkit 的 AnimationSet :
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. 参考
创建一个进度按钮
AnimationSet
Implicit Composition Animations
6. 源码
[UWP]创建一个ProgressControl的更多相关文章
- [UWP]创建一个进度按钮
1. 前言 最近想要一个进度按钮. 传统上UWP上处理进度可以这样实现,首先是XAML,包括一个ProgressBar和一个按钮: <StackPanel Orientation="H ...
- 如何用Unity创建一个的简单的HoloLens 3D程序
注:本文提到的代码示例下载地址>How to create a Hello World 3D holographic app with Unity 之前我们有讲过一次如何在HoloLens中创建 ...
- 如何在HoloLens中创建一个2D的Hello World程序
注:本文提及到的代码示例下载地址 > How to build an "Hello World" 2D app in HololLens. HoloLens 是微软的一款MR ...
- UWP 创建动画的极简方式 — LottieUWP
提到 UWP 中创建动画,第一个想到的大多都是 StoryBoard.因为 UWP 和 WPF 的界面都是基于 XAML 语言的,所以实现 StoryBoard 会非常方便. 来看一个简单的 Stor ...
- [UWP]实现一个轻量级的应用内消息通知控件
在UWP应用开发中,我们常常有向用户发送一些提示性消息的需求.这种时候我们一般会选择MessageDialog.ContentDialog或者ToastNotification来完成功能. 但是,我们 ...
- 【微软混合现实】开始使用Unity-第一章:创建一个新的项目
使用Unity开发App,第一步需要创建一个项目.项目具有一系列组织好文件夹,其中最重要的是你的附件文件夹(Assets folder).在这个文件夹中,存储了从其他工具中创建的数字内容,比如Maya ...
- 在一个空ASP.NET Web项目上创建一个ASP.NET Web API 2.0应用
由于ASP.NET Web API具有与ASP.NET MVC类似的编程方式,再加上目前市面上专门介绍ASP.NET Web API 的书籍少之又少(我们看到的相关内容往往是某本介绍ASP.NET M ...
- 用html5的canvas和JavaScript创建一个绘图程序
本文将引导你使用canvas和JavaScript创建一个简单的绘图程序. 创建canvas元素 首先准备容器Canvas元素,接下来所有的事情都会在JavaScript里面. <canvas ...
- 搭建QQ聊天通信的程序:(1)基于 networkcomms.net 创建一个WPF聊天客户端服务器应用程序 (1)
搭建QQ聊天通信的程序:(1)基于 networkcomms.net 创建一个WPF聊天客户端服务器应用程序 原文地址(英文):http://www.networkcomms.net/creating ...
随机推荐
- MyEclipse弹出提示窗口
MyEclipse弹出提示窗口 1.弹窗如下
- Linux显示文件和目录的详细资料
Linux显示文件和目录的详细资料 youhaidong@youhaidong-ThinkPad-Edge-E545:~$ ls -l 总用量 56 -rw-r--r-- 1 youhaidong y ...
- ASP.NET 页面双向静态化
而我们预期的结果应该如下图,实际只请求两次. 用301重定向可以解决该循环请求产生的问题. OK, let's begin. 本文的Demo和Source是基于上一篇的,如果下面的一些文件或文件夹没有 ...
- 命令行工具osql.exe使用
目标: 快速在21个库修改Test表的某条记录,这几个库都分别在不同的服务器上. 通常会想到,到每个库都执行一下语句不就好了吗?这个数据库切换来切换去,挺麻烦了,通过命令行工具osql.exe就可以快 ...
- BitMap位图与海量数据的理解
1. Bit Map算法简介 来自于<编程珠玑>.所谓的Bit-map就是用一个bit位来标记某个元素对应的Value, 而Key即是该元素.由于采用了Bit为单位来存储数据,因此在存储空 ...
- json 的循环输出
json不能用for-of循环,会报错 可以用for-in循环: var json = {'a':'apple','b':'banana','c':'orange','d':'pear'}; for( ...
- Bugku的web题目(多次)的解题
这道题目我弄了好久,最后问了朋友,然后在朋友的帮助下,将flag找到了 这里写一下解题方法,记录一下 一进到这道题,看到了php?id=1,就很熟悉,很有可能是一道sql注入的题目,肯定是要试一下最简 ...
- AndroidStudio Frameworks detected: Android framework is detected in the project Configure
出现这个问题应该是文件没有用正确的方式打开. 遇到这种情况,就要去检查下载的这个包的结构. 我的这个文件明显真正的是下面这个文件夹,如果把整个当做一个android文件打开会导致文件结 ...
- 【解高次同余方程】51nod1038 X^A Mod P
1038 X^A Mod P 基准时间限制:1 秒 空间限制:131072 KB 分值: 320 X^A mod P = B,其中P为质数.给出P和A B,求< P的所有X. 例如:P = 11 ...
- 记录使用微信小程序的NFC和蓝牙功能读取15693芯片的开发历程
开发目标: (1) 对于Android手机,直接通过微信小程序调用手机的NFC功能,对15693协议的芯片进行读写操作: (2)对于苹果手机(及没有NFC模块的手机),通过微信小程序的蓝牙功能连接到蓝 ...