Reputation: 792
I'm trying to build a generic loading control. I have the dummy control all set up with the gradient and masking, but I'm finding when I actually run it in a window it appears to rotate slightly off kilter. If you drop the below code into a user control, then drop that control into a window you should see the behavior I'm describing. I defined RenderTransformOrigin, so I'm a little confused as to why it's still not centering the rotation to the middle of the ellipse.
<UserControl x:Class="SpinningGradient.LoadingControl"
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:SpinningGradient"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="300">
<Grid>
<Ellipse Stretch="Uniform"
RenderTransformOrigin=".5,.5">
<Ellipse.RenderTransform>
<RotateTransform x:Name="noFreeze" />
</Ellipse.RenderTransform>
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Ellipse.RenderTransform).(RotateTransform.Angle)"
By="10"
To="360"
Duration="0:0:1"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Ellipse.Triggers>
<Ellipse.Fill>
<RadialGradientBrush RadiusX="0.5"
RadiusY="0.5">
<RadialGradientBrush.GradientOrigin>
<Point X=".9"
Y=".9" />
</RadialGradientBrush.GradientOrigin>
<RadialGradientBrush.Center>
<Point X="0.5"
Y="0.5" />
</RadialGradientBrush.Center>
<RadialGradientBrush.GradientStops>
<GradientStop Color="Blue"
Offset="1" />
<GradientStop Color="Red"
Offset="-.5" />
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</Ellipse.Fill>
<Ellipse.OpacityMask>
<DrawingBrush>
<DrawingBrush.Drawing>
<GeometryDrawing>
<GeometryDrawing.Brush>
<SolidColorBrush Color="Black" />
</GeometryDrawing.Brush>
<GeometryDrawing.Geometry>
<GeometryGroup FillRule="EvenOdd">
<RectangleGeometry Rect="0,0,100,100" />
<EllipseGeometry RadiusX="30"
RadiusY="30"
Center="50,50" />
</GeometryGroup>
</GeometryDrawing.Geometry>
<GeometryDrawing.Pen>
<Pen Thickness="0"
Brush="Black" />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</Ellipse.OpacityMask>
</Ellipse>
</Grid>
</UserControl>
Upvotes: 0
Views: 763
Reputation: 10349
The reason for that is that even though your Ellipse
looks like a circle, it is still stretching to the size of its container (or, more precisely, to the size of the LoadingControl
). So, unless the said LoadingControl
is contained within a perfect square, the Ellipse
's actual center point is going to be offset from the center point of the visible circle, either horizontally or vertically. And around that actual center point (rather than the apparent center point) the Ellipse
is rotated.
@Clemens already gave you a handful of options to remedy this situation in the comments section, but let me quickly go over them:
Width
and Height
of your Ellipse
to the same value - this is limiting since you have to hard-code the size of your controlWidth
to the ActualHeight
of the Ellipse
(or the other way around) - this gives you some extent of dynamic behavior, but things go bad if the target dimension constraint is smaller than the source dimension constraint (e.g. if you bind Width
to ActualHeight
, things go bad when your control is contained within a rectangle with its width smaller than its height - the Ellipse
is clipped)Ellipse
in a square container - or more precisely, within a container that would always arrange it in a squareI strongly recommend the last approach, since it gives you full flexibility in sizing your control. Now I don't think there's a component shipped with WPF that could accomplish this task, but it is fairly simple to devise one of your own, and I guarantee from my experience, that it'll become a useful tool in your toolbox. Here's an example implementation, which is based on a Decorator
:
public class SquareDecorator : Decorator
{
protected override Size ArrangeOverride(Size arrangeSize)
{
if (Child != null)
{
var paddingX = 0d;
var paddingY = 0d;
if (arrangeSize.Width > arrangeSize.Height)
paddingX = (arrangeSize.Width - arrangeSize.Height) / 2;
else
paddingY = (arrangeSize.Height - arrangeSize.Width) / 2;
var rect = new Rect
{
Location = new Point(paddingX, paddingY),
Width = arrangeSize.Width - 2 * paddingX,
Height = arrangeSize.Height - 2 * paddingY,
};
Child.Arrange(rect);
}
return arrangeSize;
}
protected override Size MeasureOverride(Size constraint)
{
var desiredSize = new Size();
if (Child != null)
{
Child.Measure(constraint);
var max = Math.Min(constraint.Width, constraint.Height);
var desired = Math.Max(Child.DesiredSize.Width, Child.DesiredSize.Height);
desiredSize.Width = desiredSize.Height = Math.Min(desired, max);
}
return desiredSize;
}
}
Then you only need to slightly modify your control:
<UserControl (...)>
<local:SquareDecorator>
<Ellipse (...) />
</local:SquareDecorator>
</UserControl>
Upvotes: 0
Reputation: 128061
The UserControl below creates a very similar or the same visual result with a lot less XAML. The ratio between StrokeThickness and RadiusX/RadiusY determines the relative stroke width.
<UserControl ...>
<Viewbox>
<Path StrokeThickness="1" Stretch="Uniform" RenderTransformOrigin="0.5,0.5">
<Path.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="RenderTransform.Angle"
To="360" Duration="0:0:1" RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Path.Triggers>
<Path.RenderTransform>
<RotateTransform/>
</Path.RenderTransform>
<Path.Data>
<EllipseGeometry RadiusX="2" RadiusY="2"/>
</Path.Data>
<Path.Stroke>
<RadialGradientBrush GradientOrigin="0.9,0.9">
<RadialGradientBrush.GradientStops>
<GradientStop Color="Blue" Offset="1" />
<GradientStop Color="Red" Offset="-0.5" />
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</Path.Stroke>
</Path>
</Viewbox>
</UserControl>
Upvotes: 2