提问人:Bagomot 提问时间:10/23/2023 最后编辑:ClemensBagomot 更新时间:10/26/2023 访问量:85
C# WPF 中沿轮廓带有进度条的矩形按钮
Rectangular button with a progressbar along the contour in C# WPF
问:
我需要在 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% 后,按钮应如图所示:
此外,即使在圆角上,进度条的填充也应该平滑。
我根本不知道如何做到这一点。请帮帮我。
更新:我可能没有很好地解释我的问题,我会试着澄清。我需要将边框本身制作为进度条(作为元素或动画,没关系),它应该填充指定时间,如图所示: load_button
答:
1赞
Sinatr
10/23/2023
#1
它结合了几个问题,包括:
我快速地将一些东西放在一起,让你开始。仍有工作要做;)
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();
}
}
}
如果需要帮助,
然后只需进行以下更改:
- 设置路径
Stroke="Green"
- 设置边框和
Background="{TemplateBinding Background}"
BorderBrush="Transparent"
这样,按钮就会对动画路径的内部部分进行省略,在更改时只留下外线可见。
评论
0赞
Bagomot
10/23/2023
这不完全是我所做的,但无论如何非常感谢!这可能会对我有所帮助。我的任务是使边框本身成为进度条,也就是说,最初边框是完全透明的,并随着时间的推移顺时针填充。但是,我无法在底层制作一个矩形并填充它,因为按钮背景将是半透明的。
1赞
Clemens
10/26/2023
#2
您可以创建一个派生控件,该控件在其边框顶部绘制描边几何图形。描边的长度可以通过用于绘制边框几何体的 a 的虚线数组来控制。Border
Pen
该控件声明两个附加属性,以及 0 到 1 范围内的双精度值。ProgressBrush
ProgressValue
下面的示例仅使用 的分量 和 的分量 ,因此它不支持不规则的边框粗细或拐角半径。Left
BorderThickness
TopLeft
CornerRadius
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
最重要的是,它精确地绘制了边框的轮廓,笔画端始终垂直于笔画方向,这在笔画粗细越大时更加明显。
评论