Я пытаюсь создать динамический круговой индикатор выполнения, который можно использовать, например, в дереве во время загрузки TreeItem.
К сожалению, элемент управления работает не совсем гладко.

Это 16x16, 16x32, 32x16 и 32x32 при размере 400%.В начале GIF-файла он немного запаздывает из-за инструмента захвата.Как вы можете видеть, круги кружатся немного, это то, что я хочу удалить.
Это мой пользовательский элемент управления:
Code-Behind:
/// <summary>
/// Interaction logic for CircularProgressBar.xaml
/// </summary>
public partial class CircularProgressBar
{
public static readonly DependencyProperty DeferedVisibilityProperty = DependencyProperty.Register(nameof(DeferedVisibility), typeof(bool),
typeof(CircularProgressBar), new PropertyMetadata
{
PropertyChangedCallback = OnDeferedVisibilityChanged,
DefaultValue = false
});
private readonly (Ellipse, int)[] _circlesWithOffset;
private Stopwatch _stopwatch;
public CircularProgressBar()
{
InitializeComponent();
DefaultStyleKey = typeof(CircularProgressBar);
_circlesWithOffset = new[] {(C0, 0), (C1, 1), (C2, 2), (C3, 3), (C4, 4), (C5, 5), (C6, 6), (C7, 7), (C8, 8)};
}
#region Animation
private void Start()
{
//Mouse.OverrideCursor = Cursors.Wait;
if(_stopwatch == null)
_stopwatch = new Stopwatch();
_stopwatch.Start();
CompositionTarget.Rendering += CompositionTarget_Rendering;
}
private void Stop()
{
//Mouse.OverrideCursor = Cursors.Arrow;
_stopwatch.Stop();
CompositionTarget.Rendering += CompositionTarget_Rendering;
}
private void CompositionTarget_Rendering(object sender, EventArgs e)
{
_circlesWithOffset.ToList().ForEach(x => SetCircle(x.Item1, x.Item2));
}
private void SetCircle(Ellipse circle, int offset)
{
var posOnCircle = _stopwatch.Elapsed.TotalSeconds * Math.PI - Math.PI / 5 * offset;
var halfWidth = (Width - circle.Width) / 2;
var halfHeight = (Height - circle.Height) / 2;
circle.SetValue(Canvas.LeftProperty, halfWidth + Math.Sin(posOnCircle) * halfWidth);
circle.SetValue(Canvas.TopProperty, halfHeight + -Math.Cos(posOnCircle) * halfHeight);
}
private void HandleUnloaded(object sender, RoutedEventArgs e)
{
Stop();
}
private void HandleVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var isVisible = (bool)e.NewValue;
if(isVisible)
Start();
else
Stop();
}
#endregion Animation
#region Visibility
public bool DeferedVisibility
{
get => (bool)GetValue(DeferedVisibilityProperty);
set => SetValue(DeferedVisibilityProperty, value);
}
[Obsolete("Please use DeferedVisibility")]
public new Visibility Visibility
{
get => base.Visibility;
set => base.Visibility = value;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
OnDeferedVisibilityChanged();
}
private static void OnDeferedVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((CircularProgressBar)d).OnDeferedVisibilityChanged();
}
private void OnDeferedVisibilityChanged()
{
if(DeferedVisibility)
{
VisualStateManager.GoToState(this, "Visible", true);
#pragma warning disable 618
Visibility = Visibility.Visible;
#pragma warning restore 618
} else
{
VisualStateManager.GoToState(this, "Collapsed", true);
#pragma warning disable 618
Visibility = Visibility.Collapsed;
#pragma warning restore 618
}
}
#endregion Visibility
}
XAML:
<UserControl x:Class="MyProject.Views.Controls.Util.CircularProgressBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:util="clr-namespace:MyProject.Views.Controls.Util"
Background="Transparent"
IsVisibleChanged="HandleVisibleChanged">
<UserControl.Resources>
<util:PercentageValueConverter x:Key="PercentageValueConverter"
Scaling="0.2" />
</UserControl.Resources>
<Grid x:Name="LayoutRoot"
Background="Transparent"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Canvas RenderTransformOrigin="0.5, 0.5"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Unloaded="HandleUnloaded">
<Ellipse x:Name="C0"
SnapsToDevicePixels="False"
Width="{Binding Width,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Height="{Binding Height,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Stretch="Fill"
Fill="Black"
Opacity="0.9" />
<Ellipse x:Name="C1"
SnapsToDevicePixels="False"
Width="{Binding Width,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Height="{Binding Height,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Stretch="Fill"
Fill="Black"
Opacity="0.8" />
<Ellipse x:Name="C2"
SnapsToDevicePixels="False"
Width="{Binding Width,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Height="{Binding Height,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Stretch="Fill"
Fill="Black"
Opacity="0.7" />
<Ellipse x:Name="C3"
SnapsToDevicePixels="False"
Width="{Binding Width,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Height="{Binding Height,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Stretch="Fill"
Fill="Black"
Opacity="0.6" />
<Ellipse x:Name="C4"
SnapsToDevicePixels="False"
Width="{Binding Width,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Height="{Binding Height,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Stretch="Fill"
Fill="Black"
Opacity="0.5" />
<Ellipse x:Name="C5"
SnapsToDevicePixels="False"
Width="{Binding Width,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Height="{Binding Height,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Stretch="Fill"
Fill="Black"
Opacity="0.4" />
<Ellipse x:Name="C6"
SnapsToDevicePixels="False"
Width="{Binding Width,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Height="{Binding Height,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Stretch="Fill"
Fill="Black"
Opacity="0.3" />
<Ellipse x:Name="C7"
SnapsToDevicePixels="False"
Width="{Binding Width,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Height="{Binding Height,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Stretch="Fill"
Fill="Black"
Opacity="0.2" />
<Ellipse x:Name="C8"
SnapsToDevicePixels="False"
Width="{Binding Width,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Height="{Binding Height,
Mode=OneWay,
Converter={StaticResource PercentageValueConverter},
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Stretch="Fill"
Fill="Black"
Opacity="0.1" />
</Canvas>
</Grid>
</UserControl>
Преобразователь предназначен только для установки диаметра окружностей на 20% от размера элемента управления.
Это способ использования элемента управления в любом месте
<util:CircularProgressBar Grid.Row="1"
DeferedVisibility="True"
Width="32"
Height="32" />
Как вы можете видеть, положение кругов обновляется событием CompositionTarget.Rendering.
Я уже пытался установить для SnapsToDevicePixels значение false, но это ничего не изменило.Для расчета позиции используется двойное значение, поэтому нет ошибок округления.