XAML and WPF - or "I'm seeing stars"

Warning - This post just describes some fun I've been having learning about 2D graphics in WPF. There's certainly nothing very clever going on here, but its been pointed out to me by a prominent Australian blogger that its not the quality but the quantity of blog posts that's important :-p

But more seriously if you do want to read something of substance check out Paul's post regarding why he doesn't believe the Visual Studio 2008 Form Designer (Cider) will help developer productivity.

So anyway - about those stars...

I'm having a hard time at the moment really diving into a serious bit of WPF programming. It seems I'm constantly being tripped by the question of "should I do this in XAML, or should I do this in C#?". Now traditionally (under WinForms) when writing a custom control I'd just start with a blank class file, determine the most appropriate Control/Component base class to derive from and start coding properties, events and any required rendering.

Now each time I've tried that approach with WPF - I end up realising pretty early on that its the wrong approach. Everything apart from the very specific properties and events are already there - or are provided by attached properties/events. The rendering is much easier to do in XAML too - in fact if done property the rendering is almost completely divorced from the control definition anyway and ends up in a theme based or Generic.xaml file.

Ok - so that's good right? Well it sounds right - but I think I'm just having trouble coming to grips with it. The stumbling block I'm having at the moment with custom controls is realising when to step out of the XAML and into fleshing out the real logic. Trouble is with custom controls most of the logic is related to the UI!

As an example - I recently wanted to create some simple graphics by having some spinning stars. So my first reaction was to just jump straight into XAML and code up a filled polygon path using the System.Windows.Shapes.Polygon.

<Polygon Points="30,0 35,20 55,25 35,30 30,50 25,30 5,25 25,20" Fill="Gold" Stroke="Black"
StrokeThickness="2"/> <Polygon Points="30,0 35,10 45,15 35,20 30,30 25,20 15,15 25,10" Fill="Gold" Stroke="Black"
StrokeThickness="2"> <Polygon.LayoutTransform> <RotateTransform Angle="45"/> </Polygon.LayoutTransform> </Polygon>

SimpleXAMLStar 

Ok - but that's a pretty lame star... and I want lots of them right - so I should create a custom control inheriting from Shape? Whilst I'm at it add a property that lets me configure the number of points too.

public class Star : Shape
{
    // Using a DependencyProperty as the backing store for NumberOfPoints.  
public static readonly DependencyProperty NumberOfPointsProperty = DependencyProperty.Register("NumberOfPoints", typeof(int), typeof(Shape), new UIPropertyMetadata(5)); public int NumberOfPoints { get { return (int)GetValue(NumberOfPointsProperty); } set { SetValue(NumberOfPointsProperty, value); } } protected override Geometry DefiningGeometry { get { return VisualContainer.CreateStarGeometry(NumberOfPoints); } } }

Creating the geometry for an n-pointed star could be done a heap of ways. Mine was the easiest to visualize but certainly not very elegant. Just create a triangle for each prong and keep rotating for as many as required. Then use the GetOutlinedPathGeometry to get the enclosing path.

public static Geometry CreateStarGeometry(int numberOfPoints)
{
    GeometryGroup group = new GeometryGroup();
    group.FillRule = FillRule.Nonzero;
    Geometry triangle = PathGeometry.Parse("M 0,-30 L 10,10 -10,10 0,-30");
    group.Children.Add(triangle);

    double deltaAngle = 360 / numberOfPoints;
    double currentAngle = 0;
    for (int index = 1; index < numberOfPoints; index++)
    {
        currentAngle += deltaAngle;
        triangle = triangle.CloneCurrentValue();
        triangle.Transform = new RotateTransform(currentAngle, 0, 0);
        group.Children.Add(triangle);
    }

    Geometry outlinePath = group.GetOutlinedPathGeometry();
    return outlinePath;
}

Now I've got a Shape they're much easier to re-use - so much so we may as well even add some animation.

<control:Star NumberOfPoints="5" Width="60" Height="60" Stroke="Black" Fill="Gold" StrokeThickness="2"
Opacity="0.5"> <control:Star.Triggers> <EventTrigger RoutedEvent="control:Star.MouseEnter"> <EventTrigger.Actions> <BeginStoryboard> <Storyboard TargetProperty="Angle"> <DoubleAnimation Storyboard.TargetName="starRotation" From="0" To="72"
Duration="0:0:1" AccelerationRatio="0.3" DecelerationRatio="0.3"/> <DoubleAnimation Storyboard.TargetProperty="Opacity" From="0.5" To="1.0"
Duration="0:0:0.5" AutoReverse="True"/> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> </control:Star.Triggers> <control:Star.RenderTransform> <RotateTransform x:Name="starRotation" Angle ="0"/> </control:Star.RenderTransform> </control:Star> <control:Star NumberOfPoints="6" Width="60" Height="60" Stroke="Black" Fill="Orange"
StrokeThickness="2"/> <control:Star NumberOfPoints="7" Width="60" Height="60" Stroke="Black" Fill="Red"
StrokeThickness="2"/>

ShapeStars

So I think I'm getting the hang of it. But hold on, according to the book if I'm going to have heaps of these things then I shouldn't be using Shapes - I should be using DrawingVisuals within a container.

public class VisualContainer : Canvas
{
    private List<Visual> visuals = new List<Visual>();

    public VisualContainer()
    {
        DrawingVisual star = CreateStar(5);

        visuals.Add(star);
        
        foreach (Visual visual in visuals)
        {
            AddVisualChild(visual);
            AddLogicalChild(visual);
        }
    }

    public static DrawingVisual CreateStar(int numberOfPoints)
    {
        DrawingVisual star = new DrawingVisual();

        using (DrawingContext drawingContext = star.RenderOpen())
        {
            Geometry outlinePath = CreateStarGeometry(numberOfPoints);
            drawingContext.DrawGeometry(Brushes.Gold, new Pen(Brushes.Black, 2), outlinePath);
        }

        return star;
    }

    public static Geometry CreateStarGeometry(int numberOfPoints)
    {         ...  as above ...
    }

    protected override int VisualChildrenCount
    {
        get { return visuals.Count; }
    }

    protected override Visual GetVisualChild(int index)
    {
        if (index < 0 || index >= visuals.Count)
            throw new ArgumentOutOfRangeException("index");

        return visuals[index];
    }

    public void AddStar()
    {
        Visual visual = CreateStar(5);
        visuals.Add(visual);
        PositionVisuals();
        AddVisualChild(visual);
        AddLogicalChild(visual);
    }

    private void PositionVisuals()
    {
        if (visuals.Count == 1)
            ((DrawingVisual)visuals[0]).Offset = new Vector(Width / 2, Height / 2);
        else
        {
            double angle = 0;
            double deltaAngle = Math.PI * 2 / visuals.Count;
            double radius = Width / 2;
            foreach (DrawingVisual visual in visuals)
            {
                visual.Offset = new Vector(Width / 2 + Math.Cos(angle) * radius, 
Height / 2 + Math.Sin(angle) * radius); angle += deltaAngle; } } } }

So that gives me a container that I can create heaps of stars in and only have one UIElement - the stars themselves are the apparently much lighter weight DrawingVisual instances. The cool thing with these is that Visual Hit Testing actually allows me to respond to click events on individual stars.

<control:VisualContainer Width="150" Height="150" MouseLeftButtonUp="VisualContainer_MouseLeftButtonUp" 
                                     RenderTransformOrigin="0.5,0.5"> <control:VisualContainer.RenderTransform> <RotateTransform x:Name="rotateTransform" Angle="0" /> </control:VisualContainer.RenderTransform> </control:VisualContainer> <Button HorizontalAlignment="Center">Rotate <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <EventTrigger.Actions> <BeginStoryboard> <Storyboard TargetProperty="Angle"> <DoubleAnimation Storyboard.TargetName="rotateTransform" From="0" To="360"
                                                    Duration="0:0:5"/> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> </Button.Triggers> </Button>

Of course I couldn't resist rotating the whole thing too

RotatingDrawingVisuals

 

What did you think of this article?




Trackbacks
  • Trackbacks are closed for this post.
Comments

Leave a comment

Comments are closed.