[WPF] 製作進度環 (3) 完成控制項

  • 1004
  • 0

幾何繪圖的部分差不多理解後,這一篇就要來完成整個控制項的設計。

Arc class 一些必要的修改

前面的繪圖其實有幾個缺點,其一是我們設定的依賴屬性是角度,老實說這麼做也沒錯,但是套上控制項的時候由於是進度,控制項對外的屬性應該會設定為百分比值。如果 Arc 用角度,就難免要寫個值轉換器來做百分比值和角度的轉換,我這人通常嫌麻煩,所以直接在 Arc 上就直接設定百分比值的依賴屬性。

第二個問題是比較嚴重的,前面的設計在動態改變屬性值的時候並不會重新渲染,所以要在中繼資料設定一個 FrameworkPropertyMetadataOptions.AffectsRender 的列舉旗標,讓屬性值變更時會重新渲染畫面。

依此要領,先移除 EndAngleProperty 相關程式碼,補上 PercentProperty :

public static readonly DependencyProperty PercentProperty =
     DependencyProperty.Register(nameof(Percent), typeof(double), typeof(Arc), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender, null, new CoerceValueCallback(OnPercentChanged)));

private static object OnPercentChanged(DependencyObject d, object baseValue)
{
    var value = (double)baseValue;
    if (value < 0) { value = 0; }
    if (value > 100) { value = 100; }
    return value;
}

public double Percent 
{
    get => (double)GetValue(PercentProperty);
    set => SetValue(PercentProperty, value);
}

當然有一些相關程式碼也要異動:

private bool IsLargeArc()
{
    return Percent > 50;
}

private double EndAngleToPolarAngle()
{
    var endAngle = 360.0 / 100.0 * Percent;
    if (endAngle > 359.9 ) { endAngle = 359.9; }
    var result = 90.0 - endAngle;            
    if (result < 0) { result += 360.0; }           
    return result;
}
設計控制項

這個控制項的範本使用 Arc class 來表達進度,最基礎的依賴屬性需求大概就是 Percent、Stroke 和 StrokeThickness,如果有其他的需求也可以自己補上。其中還會放置一個 ContentPresenter,可以讓使用者自由擺些甚麼其他的東西在裡面:

public class ProgressCircle : ContentControl
{
    public static readonly DependencyProperty PercentProperty =
         DependencyProperty.Register(nameof(Percent), typeof(double), typeof(ProgressCircle), new PropertyMetadata(0.0));      

    public double Percent
    {
        get => (double)GetValue(PercentProperty);
        set => SetValue(PercentProperty, value);
    }

    public static readonly DependencyProperty StrokeProperty =
        DependencyProperty.Register(nameof(Stroke), typeof(Brush), typeof(ProgressCircle), new PropertyMetadata(new SolidColorBrush(Colors.Black)));

    public Brush Stroke
    {
        get => (Brush)GetValue(StrokeProperty);
        set => SetValue(StrokeProperty, value);
    }

    public static readonly DependencyProperty StrokeThicknessProperty =
        DependencyProperty.Register(nameof(StrokeThickness), typeof(double), typeof(ProgressCircle), new PropertyMetadata(1.0));

    public double StrokeThickness
    {
        get => (double)GetValue(StrokeThicknessProperty);
        set => SetValue(StrokeThicknessProperty, value);
    }

    static ProgressCircle()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(ProgressCircle), new FrameworkPropertyMetadata(typeof(ProgressCircle)));
    }
}
<Style TargetType="{x:Type local:ProgressCircle}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:ProgressCircle}">
                <Grid>                        
                    <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                        <local:Arc Percent="{TemplateBinding Percent}"
                               Stroke="{TemplateBinding Stroke}"
                               StrokeThickness="{TemplateBinding StrokeThickness}"/>
                    </Border>
                    <ContentPresenter HorizontalAlignment="Center" 
                                      VerticalAlignment="Center" 
                                      Margin="{TemplateBinding Padding}"/>
                </Grid>                   
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

展示用的程式碼很簡單:

<Window x:Class="ProgressCircleSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ProgressCircleSample"
        xmlns:p="clr-namespace:ProgressCircleLibrary;assembly=ProgressCircleLibrary"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <StackPanel Orientation="Vertical" >
        <p:ProgressCircle Percent="{Binding Percent}" Stroke="Brown" StrokeThickness="10" 
                          Width="100" Height="100" Background="AliceBlue"
                          Content="{Binding Percent}"/>
        <Button Margin="12" Content="Test" Command="{Binding TestProgress}"/>
        <Button Margin="12" Content="Rest" Command="{Binding Reset}"/>
    </StackPanel>
</Window>
public class MainViewModel : NotifyPropertyBase
{

    private double percent;
    public double Percent
    {
        get => percent;
        set => SetProperty(ref percent, value);
    }

    public ICommand Reset
    {
        get
        {
            return new RelayCommand((x) => Percent = 0);
        }
    }

    public ICommand TestProgress
    {
        get
        {
            return new RelayCommand(async (x) =>
           {
               for(int i = 0; i < 101; i++)
               {
                   Percent = i;
                   await Task.Delay(100);
               }
           });
        }
    }
}
成果展示

完整的範例程式碼在此