1. 前言

最近想要一个进度按钮。

传统上UWP上处理进度可以这样实现,首先是XAML,包括一个ProgressBar和一个按钮:

<StackPanel Orientation="Horizontal" Margin="0,30" >
<ProgressBar x:Name="ProgressBar" Maximum="1" Width="230"/>
<Button x:Name="Button" Content="Download"
Click="OnStartProgress" Margin="20,0,0,0"/>
</StackPanel>

然后是服务端,假设我有这样一个服务:

public class TestService
{
public event EventHandler<double> ProgressChanged; public async Task Start(bool throwException = false)
{
IsStarted = true;
try
{
ProgressChanged?.Invoke(this, _progress);
await Task.Delay(1000);
while (_progress < 1)
{
await Task.Delay(100);
_progress += 0.03;
ProgressChanged?.Invoke(this, _progress);
if (_progress > 0.7 && throwException)
throw new Exception("test"); if (IsPaused)
return;
} IsCompleted = true;
}
finally
{
IsStarted = false;
}
}
}

接下来就是用代码处理:

private async void OnStartProgress(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
Button.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
try
{
var uiSettings = new Windows.UI.ViewManagement.UISettings();
Windows.UI.Color color = uiSettings.GetColorValue(UIColorType.Accent);
var brush = new SolidColorBrush(color);
ProgressBar.Foreground = brush;
var testService = new TestService();
testService.ProgressChanged += (s, args) => { ProgressBar.Value = args; };
await testService.Start(ThrowExceptionElement.IsOn);
}
catch (Exception ex)
{
var brush = new SolidColorBrush(Colors.PaleVioletRed);
ProgressBar.Foreground = brush;
}
finally
{
Button.Visibility = Windows.UI.Xaml.Visibility.Visible;
} }

点击按钮开始进度,隐藏按钮;进度完成后重新显示按钮。运行效果如下:

出错的时候将ProgressBar的Foreground设置成红色。这里偷懒用代码处理,其实用VisualState处理会更好。效果如下:

基本上这样就够用了,Windows 10里通常也是几个按钮配合ProgressBar来实现进度的控制。但这样做XAML部分不能复用,同时管理Button和ProgressBar也比较复杂,在空间有局限的地方也不能使用。

结果还是自己做了个ProgressButton来用。

2. 成果

ProgressButton实现了上述UI的功能:

如上图所示,ProgressButton只在几种状态间转换:

  • Ready,普通的状态,因为“Normal”已经被“CommonStates”占用,用“Ready”还算比较合适。
  • Started,开始的状态(说不定“InProgress”比较合适)。
  • Completed,完成的状态。
  • Faulted,出错的状态。

本来还应该有Paused状态,但还没想好UI上应该怎么呈现,因为Paused状态下应该有Cancel和Restart两种动作(可以参考下图应用商店的下载页面),在一个按钮上不容易同时呈现这两种动作。而且暂时还不需要这个功能,这次就不实现了。

3. 实现

通常我建议先写完所有代码,再用Blend实现UI,这样会比在代码和UI间交错地工作更高效。

3.1 处理代码

ProgressButton 的基本代码如下(不包含依赖属性和const string等内容):

[TemplateVisualState(GroupName = ProgressStatesGroupName, Name = ReadyStateName)]
[TemplateVisualState(GroupName = ProgressStatesGroupName, Name = StartedStateName)]
[TemplateVisualState(GroupName = ProgressStatesGroupName, Name = CompletedStateName)]
[TemplateVisualState(GroupName = ProgressStatesGroupName, Name = FaultedStateName)]
public partial class ProgressButton : Button
{
public ProgressButton()
{
this.DefaultStyleKey = typeof(ProgressButton);
this.Click += OnClick;
} public ProgressState State
{
get { return (ProgressState)GetValue(StateProperty); }
set { SetValue(StateProperty, value); }
} public double Progress
{
get { return (double)GetValue(ProgressProperty); }
set { SetValue(ProgressProperty, value); }
} public event EventHandler StateChanged;
public event EventHandler<ProgressStateChangingEventArgs> StateChanging; protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
UpdateVisualStates(false);
} protected virtual void OnStateChanged(ProgressState oldValue, ProgressState newValue)
{
if (newValue == ProgressState.Ready)
Progress = 0; UpdateVisualStates(true); StateChanged?.Invoke(this, EventArgs.Empty);
} protected virtual void OnProgressChanged(double oldValue, double newValue)
{
if (newValue < 0)
Progress = 0; if (newValue > 1)
Progress = 1;
} private void OnClick(object sender, RoutedEventArgs e)
{
switch (State)
{
case ProgressState.Ready:
ChangeStateCore(ProgressState.Started);
break;
case ProgressState.Started:
ChangeStateCore(ProgressState.Ready);
break;
case ProgressState.Completed:
ChangeStateCore(ProgressState.Ready);
break;
case ProgressState.Faulted:
ChangeStateCore(ProgressState.Ready);
break;
}
} 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;
default:
progressState = ReadyStateName;
break;
}
VisualStateManager.GoToState(this, progressState, useTransitions);
} private void ChangeStateCore(ProgressState newstate)
{ var args = new ProgressStateChangingEventArgs(this.State, newstate);
if (args.OldValue == ProgressState.Started && args.NewValue == ProgressState.Ready)
args.Cancel = true; OnStateChanging(args);
StateChanging?.Invoke(this, args);
if (args.Cancel)
return; State = newstate;
} protected virtual void OnStateChanging(ProgressStateChangingEventArgs args)
{ } }

ProgressButton直接继承Button,并且包含如下功能:

  • 包含 public ProgressState Statepublic double Progress两个属性。
  • 使用TemplateVisualState声明了控件模板对应四种状态的VisualState。
  • 处理Click事件在各个状态之间切换,通过EventHandler StateChanged通知用户State改变的结果,并且使用EventHandler StateChanging为用户提供了控制这个过程的途径。

基本使用方式如下:

private async void OnStateChanged(object sender, EventArgs e)
{
switch (ProgressButton.State)
{
case ProgressState.Started:
try
{
var testService = new TestService();
testService.ProgressChanged += (s, args) => { ProgressButton.Progress = args; };
await testService.Start(ThrowExceptionElement.IsOn);
ProgressButton.State = ProgressState.Completed;
}
catch (Exception)
{
ProgressButton.State = ProgressState.Faulted;
}
break;
}
}

ProgressButton的代码量不多,功能上已满足我目前的需求。

3.2 处理UI

接来下处理UI,处理UI的原则是不要为了UI上的任何功能修改ProgressButton.cs,避免UI和代码间的耦合。

3.2.1 原理

如前所示,ProgressButton将一个矩形的按钮转变成圆形,再在圆形的边框上显示进度。这两个功能的实现方式在以前的文章中有介绍过。

实用的Shape指南中介绍了Rectangle的public System.Double RadiusX { get; set; }public System.Double RadiusY { get; set; }分别用于指定用于使矩形的角变圆的椭圆的x轴和 y轴半径。只要把Rectangle的宽高设成一致,RadiusX和RadiusY设成宽高的一半,Rectangle看上去就成了一个普通的Ellipse。下图展示了 RadiusX="50" RadiusY="20"的Rectangle的圆角和Width="100" Height="40"的Ellipse(x轴半径50,y轴半径20)基本重合在一起。:

用Shape做动画中介绍了怎么使用StrokeDashArray做进度提示动画:

理解及扩展Expander中介绍了怎么对StackPanel做拉伸动画,只是这次为了让内容可以变形将StackPanel换成Grid:

在ProgressButton的ControlTemplate中这三个功能都用Behavior做成动画了,同样在用Shape做动画介绍了怎么使用Behavior。(最近常常用Behavior,简直走火入魔。)

这么看来ProgressButton完全是以前介绍过的技术的组合应用,几乎没有新知识。

3.2.2 假装成普通Button

UWP的Button的ControlTemplate中只有一个ContentPresenter,边框、背景等都由这个ContentPresenter呈现。ProgressButton为了对边框和背景变形,移除了ContentPresenter的这部分内容,改为由一个Rectangle呈现:

<Rectangle x:Name="Rectangle"
StrokeThickness="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=BorderThickness,Converter={StaticResource BorderToStrokeThicknessConverter}}"
Stroke="{TemplateBinding BorderBrush}"
Fill="{TemplateBinding Background}">

由于Thickness BorderThicknessdouble StrokeThickness的类型不匹配,所以用BorderToStrokeThicknessConverter转换。

3.2.3 ControlTemplate结构

如上图所示,除了Rectangle,还另外添加了显示进度的Ellipse,显示Completed状态的CompletedElement和显示Faulted状态的FaultedElement。其实后面两个元素可以交由Rectangle处理,但我的Blend出了问题不能编辑ControlTemplate,ProgressButton所有动画都要手写,这样实现方式就有了很大限制,多了两个Element虽然结构变复杂,但控制它们只需要对Opacity做动画,还算比较轻松。反正Button只是小小的一块元素,就算结构再复杂对整体性能影响有限,我不会太介意这点复杂性。

3.2.4 FontIcon

<FontIcon Glyph=""
Foreground="White"
FontSize="{TemplateBinding FontSize}"
x:Name="CompletedIcon" />

CompletedElement和FaultedElement中的图标(√和×)使用了FontIcon,并且FontSize通过TemplateBinding绑定了FontSize,这样的好处是这两个图标的大小可以和按钮的字体保持一致。其实反正是矢量元素,用Path再配合ViewBox也可以达到同样效果,但只是简单图案的话使用FontIcon明显简洁方便多了。

3.2.5 DropShadowPanel

CompletedElement和FaultedElement里都用上了DropShadowPanel,这样UI上好看一点点。UWP中的Ellipse常常能看到锯齿,使用带圆角的元素时要注意这点,适当使用DropShadow能让锯齿看上去不那么明显,这是我常用的小技巧。在WPF中阴影效果对性能影响很大,而且应用阴影效果的元素尺寸越大对性能的影响就越大。但Silverlight以后性能影响就变小了,我没测试过UWP的情况,应该不会比Silverlight差吧。何况按钮的尺寸基本都不大,就算再怎么乱来对性能影响都有限。

3.2.6 VisualState

如果Blend没出错应该可以看到上图的所有状态。其中FocusStates基本上不会去处理。ProgressButton在Button的ControlTemplate基础上添加了ProgressStates。虽然ProgressButton中按钮的基本功能不是重点,但还是需要细心处理CommonStates的各种状态。

4. 其它

由于UWP的元素基本是矢量元素,ProgressButton也得益于这个优点,在狭窄空间也能表现得很好,配合StateChanged和StateChanging事件可以扩展更多的用法:

另外,虽然没有Paused状态,但配合ProgressBar和StateChanging事件,还是可以实现Paused-Restar的基本功能:

<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> <ContentControl Content="{Binding}"
Margin="5,0"
VerticalAlignment="Center" /> <local:ProgressButton Content="download"
x:Name="ProgressButton"
Margin="5,0"
Grid.Column="1"
HorizontalAlignment="Right"
StateChanged="OnCase3StateChanged"
StateChanging="OnCase3StateChanging"
/>
<ProgressBar Grid.ColumnSpan="2"
Maximum="1"
ShowPaused="{Binding ElementName=ProgressButton,Path=State,Converter={StaticResource ProgressStateToPausedConverter}}"
Value="{Binding ElementName=ProgressButton,Path=Progress}"
Style="{StaticResource ProgressBarStyle1}"
Foreground="#1D0490FF"
VerticalContentAlignment="Stretch"
VerticalAlignment="Stretch"
IsHitTestVisible="False"/>
</Grid>

5. 结语

做完后才有点后悔,其实ProgressButton不应该继承Button,既然不是Button好像也不应该命名为-Button。如果继承自ProgressBar的话可以直接使用它的Minimum和Maximum,Progress也不用限定在0到1之间。

由于UWP没有Resizing动画,ProgressButton改变宽度的动画实现得不算很好,从上面可以看到即使内容从'download'变成'open',ProgressButton的宽度还是'download'的宽度,这是ProgressButton的另一个遗憾。

顺便一提,虽然没有测试过但我想大部分代码可以兼容WPF。

6. 参考

How to Create a Circular Progress Button.htm

7. 源码

Progress Button Sample

[UWP]创建一个进度按钮的更多相关文章

  1. [UWP]创建一个ProgressControl

    1. 前言 博客园终于新增了UWP的分类,我来为这个分类贡献第一篇博客吧. UWP有很多问题,先不说生态的事情,表单.验证.输入.设计等等一堆基本问题缠身.但我觉得最应该首先解决的绝对是Blend,那 ...

  2. 创建一个jQuery UI的垂直进度条效果

    日期:2013-9-24  来源:GBin1.com 在线演示 缺省的jQuery UI只有水平的进度条效果,没有垂直的进度条效果,仅仅重新定义JQuery UI的CSS不能解决这个问题. 这里我们扩 ...

  3. 安卓入门 使用android创建一个项目 从启动activity中响应按钮事件 启动另一个activity 并传递参数

    启动android studio创建一个新项目 public void sendMessage(View view){ Intent intent=new Intent(this,DispalyMes ...

  4. xul 创建一个按钮

    MDN Mozilla 产品与私有技术 Mozilla 私有技术 XUL Toolbars 添加工具栏按钮 (定制工具栏) 添加工具栏按钮 (定制工具栏) 在本文章中 创建一个 overlay 在工具 ...

  5. uwp - 做一个相对炫酷的动画按钮/按钮动画

    原文:uwp - 做一个相对炫酷的动画按钮/按钮动画 看腻了系统自带的button animation何不尝试下自定义一个较为炫酷的动画顺便提升用户体验.效果图: 动画分为几个部分,分别是:内圆从中心 ...

  6. 创建一个apk:按钮-click-文字display,测试apk;安装在真机进行调试的方法

    问题引入: 怎么样在一个app做event事件?例如touch操作,滑动操作,和按键事件(back,home等) 回答1:device.touch(x,y) ---获取device对象,然后touch ...

  7. 5.从零开始创建一个QT窗口按钮

    如何创建一个QT项目 如何创建一个QT项目 1.创建新项目 2.配置选择 3.增加按钮 4.按钮和窗体的大小标签图标设置 5.信号与槽 6.自定义信号与槽 代码 1.创建新项目 点击文件->新建 ...

  8. 如何用Unity创建一个的简单的HoloLens 3D程序

    注:本文提到的代码示例下载地址>How to create a Hello World 3D holographic app with Unity 之前我们有讲过一次如何在HoloLens中创建 ...

  9. 如何在HoloLens中创建一个2D的Hello World程序

    注:本文提及到的代码示例下载地址 > How to build an "Hello World" 2D app in HololLens. HoloLens 是微软的一款MR ...

随机推荐

  1. jquery的2.0.3版本源码系列(3):96行-283行,给JQ对象,添加一些方法和属性

    jquery是面向对象的程序,面向对象就离不开方法和属性. 方法的简化 jQuery.fn=jQuery.prototype={ jquery: 版本 constructor: 修正指向问题 init ...

  2. jsp 使用Common-FileUpload组件文件上传及限制上传类型

    1.将commons-fileupload-1.3.3.jar复制到Web应用的lib文件夹下,在WebRoot目录下创建limit.jsp页面,在该页面中添加一个文件域的表单,设置类型为    mu ...

  3. mysql简单主从复制(一)

    MYSQL简单主从复制 master:172.25.44.1 slave:172.25.44.2 mysql5.7安装 master和slave均操作 准备rpm包:mysql-5.7.17-1.el ...

  4. Java基础---String类和基本数据类型包装类

    第一讲     String类 一.概述         String是字符串的类类型,用于描述字符串事物.字符串是一个特殊的对象.特殊之处就在于: Stings= new String();和Str ...

  5. wifi pineapple 外接USB无线网卡桥接外网

    0:选择USB网卡 在没有有线网络的情况下,可以外挂一个usb无线网卡来桥接上网,目前支持3070L.8187L芯片的网卡,反正linux系统都用这些芯片, 免的安装驱动, 我选择的是 WN-722N ...

  6. Spark Submit 脚本

    当我们需要命令行传递参数时候,将--class 写在前面,然后是jar 最后是参数 spark-submit --master yarn --num-executors 3 --executor-me ...

  7. 【Java数据结构学习笔记之二】Java数据结构与算法之栈(Stack)实现

      本篇是java数据结构与算法的第2篇,从本篇开始我们将来了解栈的设计与实现,以下是本篇的相关知识点: 栈的抽象数据类型 顺序栈的设计与实现 链式栈的设计与实现 栈的应用 栈的抽象数据类型   栈是 ...

  8. sqlserver 父子级查询(理念适应所有数据库)

    实现技术: 存储过程   ,零时表(3) 一句话说完 :把父级查询下来的子级ID 保存成零时表,并且将符合子级ID数据添加到另一张零时表. 同时清空数据时需要使用到一张零时表作为容器: alter P ...

  9. webservice Dome--一个webservice的简单小实例

    1.理解:webservice就是为了实现不同服务器上不同应用程序的之间的通讯 2.让我们一步一步的来做一个webservice的简单应用 1)新建一个空的web应用程序,在程序上右键,新建项目,选择 ...

  10. 团队作业2——需求分析&原型设计

    Deadline: 2017-4-14 22:00PM,以博客发表日期为准 评分基准: 按时交 - 有分,检查的项目包括后文的三个方面 需求分析 原型设计 编码规范 晚交 - 0分 迟交两周以上 - ...