• [UWP]创建一个进度按钮


    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="&#xE001;"
              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

  • 相关阅读:
    企业财务分析一头雾水?有了这个财务报表工具,问题一键解决
    解决wamp 3.0.6 访问路径出现 403 错误
    解决wamp 3.0.6 访问路径出现 403 错误
    解决wamp 3.0.6 访问路径出现 403 错误
    区间树
    区间树
    区间树
    区间树
    阿里云弹性裸金属服务器-神龙架构(X-Dragon)揭秘
    阿里云弹性裸金属服务器-神龙架构(X-Dragon)揭秘
  • 原文地址:https://www.cnblogs.com/dino623/p/ProgressButton.html
Copyright © 2020-2023  润新知