Marisa
Marisa

Reputation: 792

Rotating Ellipse Around Center

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

Answers (2)

Grx70
Grx70

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:

  • Set Width and Height of your Ellipse to the same value - this is limiting since you have to hard-code the size of your control
  • Bind the Width 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)
  • Put the Ellipse in a square container - or more precisely, within a container that would always arrange it in a square

I 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

Clemens
Clemens

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

Related Questions