C# WPF 中沿轮廓带有进度条的矩形按钮

Rectangular button with a progressbar along the contour in C# WPF

提问人:Bagomot 提问时间:10/23/2023 最后编辑:ClemensBagomot 更新时间:10/26/2023 访问量:85

问:

我需要在 C# 6.0 WPF 中创建一个带有圆角的矩形按钮。此按钮应具有进度条,而不是顺时针填充的框架(从顶部边框的中间开始)。

我尝试了很多方法来做到这一点,甚至使用 Path 制作了一个稍微可行的版本,但没有我需要的圆角。

请告诉我如何使用 xaml 标记完成此操作以及如何管理进度。

下面是一个带有圆角的常规按钮:

<UserControl x:Class="Example.ProgressButton"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:Example"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid HorizontalAlignment="Center" VerticalAlignment="Center">
        <Button x:Name="button" Width="150" Height="60" Content="Click me" Background="LightGray" BorderBrush="Transparent">
            <Button.Style>
                <Style TargetType="{x:Type Button}">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="{x:Type Button}">
                                <Border CornerRadius="10" Background="{TemplateBinding Background}" BorderBrush="Green" BorderThickness="2">
                                    <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                                </Border>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </Button.Style>
        </Button>
    </Grid>
</UserControl>

最初,框架不应可见(按钮的大小不应随框架或不带框架而改变);单击后,进度条将从上边框的中间顺时针方向开始填充。填充 100% 后,按钮应如图所示:button

此外,即使在圆角上,进度条的填充也应该平滑。

我根本不知道如何做到这一点。请帮帮我。

更新:我可能没有很好地解释我的问题,我会试着澄清。我需要将边框本身制作为进度条(作为元素或动画,没关系),它应该填充指定时间,如图所示: load_button

C WPF XAML 进度条 C#-6.0

评论

0赞 Bagomot 10/23/2023
我还要解释一下,从本质上讲,这不一定是进度条。这大概可以通过动画来完成,主要是它工作 1 次,持续时间可以调整。
0赞 Demon 10/23/2023
为什么不简单地将依赖属性添加到动画或进度按钮中,并使动画依赖于它
0赞 Bagomot 10/23/2023
这可能是需要做的,但我无法弄清楚如何使动画本身具有所需的形状。

答:

1赞 Sinatr 10/23/2023 #1

它结合了几个问题,包括:

我快速地将一些东西放在一起,让你开始。仍有工作要做;)

demo

Xaml:

<Window x:Class="WpfApp3.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:WpfApp3"
        mc:Ignorable="d"
        Title="MainWindow"
        Height="450"
        Width="800">
    <Window.Resources>

        <local:AngleToPointConverter x:Key="angleToPointConverter" />
        <local:AngleToIsLargeConverter x:Key="angleToIsLargeConverter" />

        <Style TargetType="Button">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Grid>
                            <Viewbox Stretch="Fill"
                                     ClipToBounds="True">
                                <Viewbox.Clip>
                                    <RectangleGeometry RadiusX="10"
                                                       RadiusY="10"
                                                       Rect="0,0,200,100" />
                                </Viewbox.Clip>
                                <Path Stroke="LightGray"
                                      StrokeThickness="100"
                                      Width="100"
                                      Height="100">
                                    <Path.Data>
                                        <PathGeometry>
                                            <PathFigure StartPoint="50,0">
                                                <ArcSegment RotationAngle="0"
                                                            SweepDirection="Clockwise"
                                                            Size="50,50"
                                                            Point="{Binding DataContext.Angle, Converter={StaticResource angleToPointConverter}, RelativeSource={RelativeSource FindAncestor, AncestorType=Button}}"
                                                            IsLargeArc="{Binding DataContext.Angle, Converter={StaticResource angleToIsLargeConverter}, RelativeSource={RelativeSource FindAncestor, AncestorType=Button}}">
                                                </ArcSegment>
                                            </PathFigure>
                                        </PathGeometry>
                                    </Path.Data>
                                </Path>
                            </Viewbox>
                            <Border CornerRadius="10"
                                    Background="Transparent"
                                    BorderBrush="Green"
                                    BorderThickness="2">
                                <ContentPresenter HorizontalAlignment="Center"
                                                  VerticalAlignment="Center" />
                            </Border>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Window.DataContext>
        <local:ViewModel />
    </Window.DataContext>
    <Grid>
        <TextBlock Text="{Binding Progress}"
                   VerticalAlignment="Top" />
        <Button Width="200"
                Height="100"
                Content="Click me"
                Command="{Binding ClickCommand}" />
    </Grid>
</Window>

Cs:

namespace WpfApp3
{
    public class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;

        public DelegateCommand ClickCommand { get; }

        public double Progress { get; set; }
        public double Angle => Progress / 100 * 360;

        public ViewModel()
        {
            var busy = false;

            ClickCommand = new(async o =>
            {
                busy = true;
                ClickCommand!.Update();

                for (int i = 1; i <= 100; i++)
                {
                    SetProgress(i);
                    await Task.Delay(20);
                }

                SetProgress(0);

                busy = false;
                ClickCommand.Update();

            }, o => !busy);
        }

        void SetProgress(double value)
        {
            Progress = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Progress)));
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Angle)));
        }
    }

    public class DelegateCommand : ICommand
    {
        public event EventHandler? CanExecuteChanged;

        readonly Action<object?> execute;
        readonly Predicate<object?> canExecute;

        public DelegateCommand(Action<object?> execute, Predicate<object?> canExecute)
        {
            this.execute = execute;
            this.canExecute = canExecute;
        }

        public void Update() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);

        public bool CanExecute(object? parameter) => canExecute(parameter);

        public void Execute(object? parameter) => execute(parameter);
    }

    class AngleToPointConverter : IValueConverter
    {

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            double angle = (double)value;
            double radius = 50;
            double piang = angle * Math.PI / 180;

            double px = Math.Sin(piang) * radius + radius;
            double py = -Math.Cos(piang) * radius + radius;

            return new Point(px, py);
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

    class AngleToIsLargeConverter : IValueConverter
    {

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            double angle = (double)value;

            return angle > 180;
        }

        public object ConvertBack(object value, Type targetTypes, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

如果需要帮助,

border only

然后只需进行以下更改:

  • 设置路径Stroke="Green"
  • 设置边框和Background="{TemplateBinding Background}"BorderBrush="Transparent"

这样,按钮就会对动画路径的内部部分进行省略,在更改时只留下外线可见。

评论

0赞 Bagomot 10/23/2023
这不完全是我所做的,但无论如何非常感谢!这可能会对我有所帮助。我的任务是使边框本身成为进度条,也就是说,最初边框是完全透明的,并随着时间的推移顺时针填充。但是,我无法在底层制作一个矩形并填充它,因为按钮背景将是半透明的。
1赞 Clemens 10/26/2023 #2

您可以创建一个派生控件,该控件在其边框顶部绘制描边几何图形。描边的长度可以通过用于绘制边框几何体的 a 的虚线数组来控制。BorderPen

该控件声明两个附加属性,以及 0 到 1 范围内的双精度值。ProgressBrushProgressValue

下面的示例仅使用 的分量 和 的分量 ,因此它不支持不规则的边框粗细或拐角半径。LeftBorderThicknessTopLeftCornerRadius

public class ProgressBorder : Border
{
    public static readonly DependencyProperty ProgressBrushProperty = DependencyProperty.Register(
        nameof(ProgressBrush), typeof(Brush), typeof(ProgressBorder),
        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));

    public static readonly DependencyProperty ProgressValueProperty = DependencyProperty.Register(
        nameof(ProgressValue), typeof(double), typeof(ProgressBorder),
        new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsRender));

    public Brush ProgressBrush
    {
        get => (Brush)GetValue(ProgressBrushProperty);
        set => SetValue(ProgressBrushProperty, value);
    }

    public double ProgressValue
    {
        get => (double)GetValue(ProgressValueProperty);
        set => SetValue(ProgressValueProperty, value);
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);

        var w = RenderSize.Width;
        var h = RenderSize.Height;
        var t = BorderThickness.Left;
        var d = t / 2;
        var r = Math.Max(0, Math.Min(CornerRadius.TopLeft, Math.Min(w / 2 - t, h / 2 - t)));
        var geometry = new StreamGeometry();

        using (var dc = geometry.Open())
        {
            dc.BeginFigure(new Point(w / 2, d), true, true);

            dc.LineTo(new Point(w - d - r, d), true, true);
            dc.ArcTo(new Point(w - d, d + r), new Size(r, r), 0, false, SweepDirection.Clockwise, true, true);

            dc.LineTo(new Point(w - d, h - d - r), true, true);
            dc.ArcTo(new Point(w - d - r, h - d), new Size(r, r), 0, false, SweepDirection.Clockwise, true, true);

            dc.LineTo(new Point(d + r, h - d), true, true);
            dc.ArcTo(new Point(d, h - d - r), new Size(r, r), 0, false, SweepDirection.Clockwise, true, true);

            dc.LineTo(new Point(d, d + r), true, true);
            dc.ArcTo(new Point(d + r, d), new Size(r, r), 0, false, SweepDirection.Clockwise, true, true);

            dc.LineTo(new Point(w / 2, d), true, true);
        }

        var length = (2 * w + 2 * h + 4 * ((0.5 * Math.PI - 2) * r - t)) / t;
        var dashes = new double[] { ProgressValue * length, (1 - ProgressValue) * length };
        var pen = new Pen
        {
            Brush = ProgressBrush,
            Thickness = t,
            StartLineCap = PenLineCap.Flat,
            EndLineCap = PenLineCap.Flat,
            DashCap = PenLineCap.Flat,
            DashStyle = new DashStyle(dashes, 0),
            LineJoin = PenLineJoin.Round
        };

        drawingContext.DrawGeometry(null, pen, geometry);
    }
}

XAML 中的用法示例:

<local:ProgressBorder
    Background="AliceBlue"
    BorderBrush="LightGray"
    ProgressBrush="Red"
    BorderThickness="10"
    CornerRadius="10,10,10,10">
    <TextBlock Text="Hello" Margin="10"/>
    <local:ProgressBorder.Triggers>
        <EventTrigger RoutedEvent="Loaded">
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation
                       Storyboard.TargetProperty="ProgressValue"
                       To="1.0" Duration="0:0:3"/>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </local:ProgressBorder.Triggers>
</local:ProgressBorder>

评论

0赞 Bagomot 10/27/2023
是的,谢谢!这正是我所需要的。我不明白为什么我没有立即想到继承 Border 类。您的版本结构更简单,更方便。
0赞 Clemens 10/27/2023
最重要的是,它精确地绘制了边框的轮廓,笔画端始终垂直于笔画方向,这在笔画粗细越大时更加明显。