提问人:WDpad159 提问时间:11/5/2023 最后编辑:WDpad159 更新时间:11/8/2023 访问量:102
如何使用 C# 和 WPF 为圆形进度条创建选框并仅填充进度条的灰色区域
How to create a Marquee for Circular Progress Bar and fill only the gray area of bar using C# and WPF
问:
我创建了一个自定义的圆形进度条,如下所示:
使用以下代码:
<UserControl x:Class="WpfApp1.SpinnerProgressBar"
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:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Viewbox>
<Canvas Width="100" Height="100">
<!-- Base Spinner -->
<Path Stroke="LightGray" StrokeThickness="3" Fill="LightGray"
Data="M 0 100 a 100,100 0 1 1 200,0
a 100,100 0 1 1 -200,0
M 30 100 a 70,70 0 1 1 140,0
a 70,70 0 1 1 -140,0" RenderTransformOrigin="0.5,0.5" />
<!-- Loader Spinner "M 0 100 a 100,100 0 0 1 100,-100 v 30 a 70,70 0 0 0 -70,70" -->
<Path Fill="Gold" Data="M 0 100 a 100,100 0 0 1 100,-100 v 30
a 70,70 0 0 0 -70,70" RenderTransformOrigin="1,1" >
<Path.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform Angle="0"/>
<TranslateTransform/>
</TransformGroup>
</Path.RenderTransform>
</Path>
</Canvas>
</Viewbox>
</UserControl>
通过模仿跑马灯行为,我只需要绑定到(如果我没记错的话)。但是,当有成功连接到数据库的情况以及仍在尝试连接时使用选框时,我被困在如何创建完整的进度条上。我怎样才能完成完整的进度条部分?如果有教程/视频可以解释如何做到这一点,我将不胜感激。<RotateTransform Angle="0"/>
编辑1:多亏了@EldHasp我做了以下操作,我尝试了以下代码:
SpinnerProgressBar.xaml
<UserControl x:Class="SpinnerProgress.SpinnerProgressBar"
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:SpinnerProgress"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
xmlns:rpb="clr-namespace:SpinnerProgress.spinnerprogressbar">
<!-- Spinner Design -->
<!-- Use the text document about the path commands in Data. -->
<Viewbox>
<Grid>
<!-- Arc Movement requirements: (Size, (RotatingAngle, isLargeArc, SweepDirection), Coordinates[End-points]) -->
<Canvas Width="100" Height="100">
<Path Stroke="LightGray" StrokeThickness="3" Fill="LightGray" Panel.ZIndex="2"
Data="M 0 100 a 100,100 0 1 1 200,0
a 100,100 0 1 1 -200,0
M 30 100 a 70,70 0 1 1 140,0
a 70,70 0 1 1 -140,0" RenderTransformOrigin="0.5,0.5" />
<!-- Arc Movement requirements: (Size, (RotatingAngle, isLargeArc, SweepDirection), Coordinates[End-points]) -->
<!-- Loader Spinner "M 0 100 a 100,100 0 0 1 100,-100 v 30 a 70,70 0 0 0 -70,70" -->
<Path x:Name="progressPath" Fill="Gold" RenderTransformOrigin="1,1">
<Path.Data>
<MultiBinding Converter="{x:Static local:ProgressBarToGeometryConverter.Instance}">
<Binding Path="Value" RelativeSource="{RelativeSource AncestorType=local:SpinnerProgressBar}"/>
<Binding Path="Maximum" RelativeSource="{RelativeSource AncestorType=local:SpinnerProgressBar}"/>
<Binding Path="Minimum" RelativeSource="{RelativeSource AncestorType=local:SpinnerProgressBar}"/>
</MultiBinding>
</Path.Data>
</Path>
</Canvas>
</Grid>
</Viewbox>
</UserControl>
SpinnerProgressBar 背后的代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SpinnerProgress
{
/// <summary>
/// Interaction logic for SpinnerProgressBar.xaml
/// </summary>
public partial class SpinnerProgressBar : UserControl
{
public SpinnerProgressBar()
{
InitializeComponent();
}
private void StartMarqueeAnimation()
{
// Create a double animation to rotate the Path (marquee effect)
var rotationAnimation = new DoubleAnimation
{
To = 360, // Rotate a full circle (360 degrees)
Duration = TimeSpan.FromSeconds(1), // Adjust the duration as needed
RepeatBehavior = RepeatBehavior.Forever // Keep repeating the animation
};
progressPath.RenderTransform = new RotateTransform(); // Add a RotateTransform
progressPath.RenderTransformOrigin = new Point(1, 1); // Set the rotation origin to the center
// Begin the rotation animation
progressPath.RenderTransform.BeginAnimation(RotateTransform.AngleProperty, rotationAnimation);
}
}
}
MainWindow.xaml
<Window x:Class="SpinnerProgress.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:SpinnerProgress"
xmlns:spb="clr-namespace:SpinnerProgress.spinnerprogressbar"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:5"
To="10"
Storyboard.TargetName="Spinner"
Storyboard.TargetProperty="Value"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Window.Triggers>
<Grid>
<spb:SpinnerProgressBar x:Name="Spinner" Maximum="10" Minimum="-10" Value="-10" Height="100" Width="100" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</Window>
转换器.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Markup;
using System.Windows.Media;
using static System.Math;
namespace SpinnerProgress.spinnerprogressbar
{
public class SpinnerProgressBar : RangeBase
{
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(double), typeof(SpinnerProgressBar), new PropertyMetadata(0.0));
public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register("Maximum", typeof(double), typeof(SpinnerProgressBar), new PropertyMetadata(0.0));
public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register("Minimum", typeof(double), typeof(SpinnerProgressBar), new PropertyMetadata(0.0));
public double Value
{
get { return (double)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public double Maximum
{
get { return (double)GetValue(MaximumProperty); }
set { SetValue(MaximumProperty, value); }
}
public double Minimum
{
get { return (double)GetValue(MinimumProperty); }
set { SetValue(MinimumProperty, value); }
}
static SpinnerProgressBar()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SpinnerProgressBar), new FrameworkPropertyMetadata(typeof(SpinnerProgressBar)));
}
}
[ValueConversion(typeof(SpinnerProgressBar), typeof(Geometry))]
public class ProgressBarToGeometryConverter : IMultiValueConverter
{
private static readonly double valPI = 2 * PI;
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
double min = (double)values[0];
double max = (double)values[1];
double value = (double)values[2];
double angle = (value - min) / (max - min);
angle *= valPI;
if (!double.IsNormal(angle))
return DependencyProperty.UnsetValue;
double cos = Cos(angle);
double sin = Sin(angle);
double x1 = 100 * cos + 100;
double y1 = 100 * sin + 100;
double x2 = 70 * cos + 100;
double y2 = 70 * sin + 100;
StreamGeometry sg = new();
sg.FillRule = FillRule.EvenOdd;
using StreamGeometryContext sgc = sg.Open();
if (value >= max)
{
sgc.BeginFigure(new Point(200, 100), true, true);
sgc.ArcTo(new Point(0, 100), new Size(100, 100), 0, false, SweepDirection.Clockwise, true, false);
sgc.ArcTo(new Point(200, 100), new Size(100, 100), 0, false, SweepDirection.Clockwise, true, false);
sgc.LineTo(new Point(170, 100), true, false);
sgc.ArcTo(new Point(30, 100), new Size(070, 070), 0, false, SweepDirection.Counterclockwise, true, false);
sgc.ArcTo(new Point(170, 100), new Size(070, 070), 0, false, SweepDirection.Counterclockwise, true, false);
}
else
if (angle < PI)
{
sgc.BeginFigure(new Point(200, 100), true, true);
sgc.ArcTo(new Point(x1, y1), new Size(100, 100), 0, false, SweepDirection.Clockwise, true, false);
sgc.LineTo(new Point(x2, y2), true, false);
sgc.ArcTo(new Point(170, 100), new Size(070, 070), 0, false, SweepDirection.Counterclockwise, true, false);
}
else
{
sgc.BeginFigure(new Point(200, 100), true, true);
sgc.ArcTo(new Point(x1, y1), new Size(100, 100), 0, true, SweepDirection.Clockwise, true, false);
sgc.LineTo(new Point(x2, y2), true, false);
sgc.ArcTo(new Point(170, 100), new Size(070, 070), 0, true, SweepDirection.Counterclockwise, true, false);
}
return sg;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public ProgressBarToGeometryConverter() { }
public static ProgressBarToGeometryConverter Instance { get; } = new();
}
[MarkupExtensionReturnType(typeof(ProgressBarToGeometryConverter))]
public class ProgressBarToGeometryExtension : MarkupExtension
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return ProgressBarToGeometryConverter.Instance;
}
}
}
它似乎不起作用。我正在考虑将选框添加到代码中。
答:
为选框效果制作旋转动画非常简单,只需对属性进行动画处理即可。RenderTransform.RotateTranform
你可以像这样实现它。这是 的声明,对旋转有一些额外的效果,以及 on selected 的触发示例。Storyboard
IsMouseOver
Path
<UserControl.Resources>
<Storyboard x:Key="SpinStoryboardSpinning" RepeatBehavior="Forever">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(Path.RenderTransform).(RotateTransform.Angle)">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="90"/>
<EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="180"/>
<EasingDoubleKeyFrame KeyTime="0:0:1" Value="450">
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase EasingMode="EaseInOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</UserControl.Resources>
<Path Fill="Gold"
Data="M 0 100 a 100,100 0 0 1 100,-100 v 30 a 70,70 0 0 0 -70,70"
RenderTransformOrigin="1,1">
<Path.RenderTransform>
<RotateTransform Angle="90"/>
</Path.RenderTransform>
<Path.Style>
<Style TargetType="{x:Type Path}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource SpinStoryboardSpinning}"/>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
但是为了创建一个完整的进度条,这是一项更复杂的任务。
首先,您需要控制创建的,即在需要时停止和启动它们。同样对于它本身,我建议从控件派生开始,并根据需要设置其样式。Storyboards
ProgressBar
ProgressBar
好的开始是首先创建标准,但样式基于 MSDN https://learn.microsoft.com/en-us/dotnet/desktop/wpf/controls/progressbar-styles-and-templates?view=netframeworkdesktop-4.8 中的此示例ProgressBar
评论
圆形 ProgressBar 的示例。
自定义控制和转换器:
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Markup;
using System.Windows.Media;
using static System.Math;
namespace Core2023.RoundProgressBar
{
public class RoundProgressBar : RangeBase
{
static RoundProgressBar()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(RoundProgressBar), new FrameworkPropertyMetadata(typeof(RoundProgressBar)));
}
}
[ValueConversion(typeof(ProgressBar), typeof(Geometry))]
public class ProgressBarToGeometryConverter : IMultiValueConverter
{
private static readonly double valPI = 2 * PI;
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
double min = (double)values[0];
double max = (double)values[1];
double value = (double)values[2];
double angle = (value - min) / (max - min);
angle *= valPI;
if (!double.IsNormal(angle))
return DependencyProperty.UnsetValue;
double cos = Cos(angle);
double sin = Sin(angle);
double x1 = 100 * cos + 100;
double y1 = 100 * sin + 100;
double x2 = 70 * cos + 100;
double y2 = 70 * sin + 100;
string data;
if (angle < PI)
{
data = @$"
M200,100
A100,100 0 0 1 {x1.ToString(culture)},{y1.ToString(culture)}
L{x2.ToString(culture)},{y2.ToString(culture)}
A070,070 0 0 0 170,100
z";
}
else
{
data = @$"
M200,100
A100,100 0 1 1 {x1.ToString(culture)},{y1.ToString(culture)}
L{x2.ToString(culture)},{y2.ToString(culture)}
A070,070 0 1 0 170,100
z";
}
return Geometry.Parse(data);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private ProgressBarToGeometryConverter() { }
public static ProgressBarToGeometryConverter Instance { get; } = new();
}
[MarkupExtensionReturnType(typeof(ProgressBarToGeometryConverter))]
public class ProgressBarToGeometryExtension : MarkupExtension
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return ProgressBarToGeometryConverter.Instance;
}
}
}
主题 Generic.xaml:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Core2023"
xmlns:rpb="clr-namespace:Core2023.RoundProgressBar">
<Style TargetType="{x:Type rpb:RoundProgressBar}">
<Setter Property="Background" Value="LightGray"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type rpb:RoundProgressBar}">
<ControlTemplate.Resources>
<Geometry x:Key="round">
M 0 100 a 100,100 0 1 1 200,0
a 100,100 0 1 1 -200,0
M 30 100 a 70,70 0 1 1 140,0
a 70,70 0 1 1 -140,0</Geometry>
</ControlTemplate.Resources>
<Grid>
<!-- Base Spinner -->
<Path Fill="{TemplateBinding Background}"
Data="{StaticResource round}"/>
<Path Stroke="{TemplateBinding BorderBrush}" StrokeThickness="3"
Data="{StaticResource round}" Panel.ZIndex="2"/>
<!-- Loader Spinner "M 0 100 a 100,100 0 0 1 100,-100 v 30 a 70,70 0 0 0 -70,70" -->
<Path Fill="Gold">
<Path.Data>
<MultiBinding Converter="{rpb:ProgressBarToGeometry}">
<Binding RelativeSource="{RelativeSource AncestorType=rpb:RoundProgressBar}" Path="Minimum"/>
<Binding RelativeSource="{RelativeSource AncestorType=rpb:RoundProgressBar}" Path="Maximum"/>
<Binding RelativeSource="{RelativeSource AncestorType=rpb:RoundProgressBar}" Path="Value"/>
</MultiBinding>
</Path.Data>
</Path>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
带有 Value 属性的使用和动画示例的窗口:
<Window x:Class="Core2023.RoundProgressBar.RoundProgressBarWindow"
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:Core2023.RoundProgressBar"
mc:Ignorable="d"
Title="RoundProgressBarWindow" Height="450" Width="800">
<Window.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:5"
To="10"
Storyboard.TargetName="rpb"
Storyboard.TargetProperty="Value"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Window.Triggers>
<Grid>
<local:RoundProgressBar x:Name="rpb"
Maximum="10" Minimum="-10" Value="-10"
BorderBrush="Aqua"/>
</Grid>
</Window>
Value 属性可以绑定到 ViewModel,也可以以任何其他方式进行设置。进度将正确显示。
附言这是使用 StreamGeometry 的转换器的改进版本。
[ValueConversion(typeof(ProgressBar), typeof(Geometry))]
public class ProgressBarToGeometryConverter : IMultiValueConverter
{
private static readonly double valPI = 2 * PI;
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
double min = (double)values[0];
double max = (double)values[1];
double value = (double)values[2];
double angle = (value - min) / (max - min);
angle *= valPI;
if (!double.IsNormal(angle))
return DependencyProperty.UnsetValue;
double cos = Cos(angle);
double sin = Sin(angle);
double x1 = 100 * cos + 100;
double y1 = 100 * sin + 100;
double x2 = 70 * cos + 100;
double y2 = 70 * sin + 100;
StreamGeometry sg = new();
sg.FillRule = FillRule.EvenOdd;
using StreamGeometryContext sgc = sg.Open();
if (value >= max)
{
sgc.BeginFigure(new Point(200, 100), true, true);
sgc.ArcTo(new Point(0, 100), new Size(100, 100), 0, false, SweepDirection.Clockwise, true, false);
sgc.ArcTo(new Point(200, 100), new Size(100, 100), 0, false, SweepDirection.Clockwise, true, false);
sgc.LineTo(new Point(170, 100), true, false);
sgc.ArcTo(new Point(30, 100), new Size(070, 070), 0, false, SweepDirection.Counterclockwise, true, false);
sgc.ArcTo(new Point(170, 100), new Size(070, 070), 0, false, SweepDirection.Counterclockwise, true, false);
}
else
if (angle < PI)
{
sgc.BeginFigure(new Point(200, 100), true, true);
sgc.ArcTo(new Point(x1, y1), new Size(100, 100), 0, false, SweepDirection.Clockwise, true, false);
sgc.LineTo(new Point(x2, y2), true, false);
sgc.ArcTo(new Point(170, 100), new Size(070, 070), 0, false, SweepDirection.Counterclockwise, true, false);
}
else
{
sgc.BeginFigure(new Point(200, 100), true, true);
sgc.ArcTo(new Point(x1, y1), new Size(100, 100), 0, true, SweepDirection.Clockwise, true, false);
sgc.LineTo(new Point(x2, y2), true, false);
sgc.ArcTo(new Point(170, 100), new Size(070, 070), 0, true, SweepDirection.Counterclockwise, true, false);
}
return sg;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private ProgressBarToGeometryConverter() { }
public static ProgressBarToGeometryConverter Instance { get; } = new();
}
“编辑 1”中的错误
- “class SpinnerProgressBar : UserControl” - 这个类是多余的。删除它。
- 在类“class SpinnerProgressBar : RangeBase”中,无需声明 Minimum、Maxmimum 和 Value 属性。这些属性已在基类“RangeBase”中声明。
- 类“SpinnerProgressBar : RangeBase”是一个自定义控件,您需要为其声明一个主题。默认主题是文件“Themes/Generic.xaml”。
您可以从此处下载完整的工作项目:https://drive.google.com/file/d/1lUpKDHz0QQ_NoFEXeXeWBcBMVgj8WZhW/view?usp=drive_link
评论