WPF: Animating objects along a path and connecting them with a line

by Olaf Rabbachin 14. September 2010 19:06

A while ago, I came about a question posted in the (German) WPF-Forum (here's the thread). There, the poster asked for a way to connect two animated objects (residing in a Canvas control) by means of a line. While what I posted there was more like a quickie, I thought it would be fun to elaborate a bit on this, especially with respect to exactly how the line is connecting both shapes (in the post, I only introduced a way to connect the line between both objects' center points) - more on that to come.

Furthermore, I explained how to animate an object or a shape along a pre-defined path bunches of times in the forums, so this might be a good place to share my thoughts and, from now on, link to this post then instead.

 

The Basics - a very simple example

So, given an arbitrary shape, how do we create an animation that moves it from here to there? Let's start out as simple as possible; the following example will show a Rectangle that continuously moves back and forth along a horizontal line.

Assuming that we already have a Canvas control and a Rectangle within, here's the list of additional ingredients we'll need:

  1. a TranslateTransform that is applied to the Rectangle's RenderTransform
  2. a Storyboard that targets the Rectangle and can be run, i.e., after the Rectangle has been loaded
  3. inside the Storyboard, a DoubleAnimationUsingPath - this allows to specify a Path as the source of the transform
  4. a PathGeometry that we assign to the aforementioned animation and which defines the path (and thus the coordinates) that we want our Rectangle to travel along

Here's the XAML for a Window that includes all the above ingredients (click the "1. ..."-button if you're running the sample project):

 

<Window x:Class="AO_01_Simplistic"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="A simplistic sample" 
        Height="300" Width="300">
   <Window.Resources>
      <!-- The Rectangle's animation (path) -->
      <Storyboard x:Key="sb_Rect">
         <!-- The X-axis animation -->
         <DoubleAnimationUsingPath
               Storyboard.TargetName="ttAnimationTarget_Rect"
               Storyboard.TargetProperty="X" 
               Source="X"
               Duration="0:0:01"
               RepeatBehavior="Forever" 
               AutoReverse="True">
            <DoubleAnimationUsingPath.PathGeometry>
               <PathGeometry Figures="M50,0 L300,0"/>
            </DoubleAnimationUsingPath.PathGeometry>
         </DoubleAnimationUsingPath>
      </Storyboard>
   </Window.Resources>

   <!-- The Viewbox will allow to easily scale the Canvas'es contents -->
   <Viewbox Stretch="Uniform" Margin="5">
      <Border BorderBrush="DarkGray" BorderThickness="1">
         <Canvas Name="cvs" Background="#FFF0F0F0"
                    Width="450" Height="260">
            <!-- The Rectangle itself ... -->
            <Rectangle x:Name="rct" 
                          Fill="#E0ADD8E6" Width="50" Height="50"
                          Canvas.Top="100"
                          Stroke="DarkBlue" StrokeThickness="2"
                          Panel.ZIndex="1">
               <Rectangle.RenderTransform>
                  <TranslateTransform x:Name="ttAnimationTarget_Rect" X="0" Y="0" />
               </Rectangle.RenderTransform>
               <Rectangle.Triggers>
                  <!-- Start the animation as soon as the rectangle has been created -->
                  <EventTrigger RoutedEvent="FrameworkElement.Loaded">
                     <BeginStoryboard Storyboard="{StaticResource sb_Rect}"/>
                  </EventTrigger>
               </Rectangle.Triggers>
            </Rectangle>
         </Canvas>
      </Border>
   </Viewbox>
</Window>

 

And here's a little video that shows the above Window in action (don't blame me - Vimeo requires that all video-uploads are at least 200KB large, hence the long video for something as silly as that):

Here's how it works: The Rectangle's TranslateTransform (ttAnimationTarget_Rect) allows us to influence the X/Y offset that we can thus animate. Since we named the TranslateTransform, we can use it within our Storyboard-resource; in the Storyboard, we animate the TranslateTransform's X-property by means of a DoubleAnimationUsingPath which contains the path that we'd like to be followed.

 

Defining a Path (resp. its draw commands) as a resource that can be used arbitrarily

The sample above is simplistic in several ways. For instance:

  • it only animates the X-axis
  • it doesn't display the path that is being followed

Let's provide a more complex path (if you need more info on the draw commands' markup coming up, please refer to the docs: Path Markup Syntax and Geometries How-to Topics). Here's a shape that consists of a bunch of cubic beziers, forming a goofy path that our Rectangle can follow:

 

<PathFigureCollection x:Key="RectanglePathFigureCollection">
   M 20 50 
   C0,100 80,50 100,120
   c50,200 40,-30 200,-10
   c60,10 125,-25 0,-25
   c-20,0 -100,-10 -150,-40
   c-50,-30 -110,-55 -130,5
</PathFigureCollection>

 

Two things are worth noting here, before we continue: Instead of defining a Path along with its Data-property containing the above shapes, I created a PathFigureCollection that keeps nothing but the plain draw commands. Why that? The next subjective will be that we a) change the animation so that it considers both the X- and Y-axis which requires to state the same draw commands twice (more on that later) and b) we also want to draw the path. So, if we don't want to specify our path several times, we probably want to specify a resource and then use that instead. However, if we want to draw our shape, we'll have to specify a Geometry as the Path's Data property, but for the DoubleAnimationUsingPath's PathGeometry, we'll need to specify our shape by means of the Figure property which is a collection of PathFigure objects. Despite the fact that if, in the XAML-tags, you specify your shape as a string for both, you won't succeed in doing so when using a resource instead. You think this is weird and/or confusing? Well, it probably is ... or maybe not, because - behind the hood, (internal) converters do the trick for you when you assign the figure directly as a string in the respective property. As soon as we define a resource, however, we'll have to do that in a strongly typed fashion.

The above PathFigureCollection brings both "worlds" down to a common denominator (at the cost of a bit more markup). If we now want to refer to our shape, we'll do so by referencing the above resource for a PathGeometry's Figures property/collection (fair enough) and, for out visualized Path, we'll need to explicitly create a PathGeometry as the Path's Data-property which will then again allow us to specify the Figures property/collection (making up for the "bit more markup").

Since this shape, amongst several other things, will be used for all subsquent Windows, I created a ResourceDictionary that you'll find in the sample solution's Resources.xaml file and which contains everything that would otherwise only pollute the Windows' markup.

 

Animating a more complex shape and showing the path that is being followed

Now that we got that settled, let's (finally) move on to animating our Rectangle along the more complex shape that we defined in the last paragraph. If you recall, we only animated the Rectangle's TranslateTransform's X-property as the parth only formed a horizontal line. If we need to move in both directions though, this animation needs to target both the X- and the Y-axis. While this doesn't sound like it'd be something all too obvious, it actually is, because we only need to supply a transform in both the X- and Y-directions. Et voilá - the DoubleAnimationUsingPath does all the rest for us. Here's the XAML for the Storyboard and DoubleAnimationUsingPath that we'll use to animate the Rectangle along our goofy shape (again, you'll find this in the sample solution's Resource.xaml file):

 

<!-- The Rectangle's animation (path) -->
<Storyboard x:Key="sb_Rect">
   <!-- The X-axis animation -->
   <DoubleAnimationUsingPath
               Storyboard.TargetName="ttAnimationTarget_Rect"
               Storyboard.TargetProperty="X" 
               Source="X"
               Duration="0:0:05"
               RepeatBehavior="Forever" 
               AutoReverse="False">
      <DoubleAnimationUsingPath.PathGeometry>
         <PathGeometry Figures="{StaticResource RectanglePathFigureCollection}"/>
      </DoubleAnimationUsingPath.PathGeometry>
   </DoubleAnimationUsingPath>

   <!-- The Y-axis animation -->
   <DoubleAnimationUsingPath
               Storyboard.TargetName="ttAnimationTarget_Rect"
               Storyboard.TargetProperty="Y" 
               Source="Y"
               Duration="0:0:05"
               RepeatBehavior="Forever" 
               AutoReverse="False">
      <DoubleAnimationUsingPath.PathGeometry>
         <PathGeometry Figures="{StaticResource RectanglePathFigureCollection}"/>
      </DoubleAnimationUsingPath.PathGeometry>
   </DoubleAnimationUsingPath>
</Storyboard>

 

If you compare the above to the animation we used in the first sample Window, there's only really two (major) differences:

  1. the Storyboard now contains two animations (one for the X-, one for the Y-axis)
  2. the PathGeometry is now referencing the PathFigureCollection-resource that we defined earlier

And if you compare the two animations themselves, you'll see that these differ only in the axis that is being assigned. Worth noting here is the fact that you'd of course get very funny results if your two animations' Duration-, RepeatBehavior- or AutoReverse-properties differ from each other, hence always make sure that both animations only differ in the TargetProperty and Source properties!

As for drawing the shape, this can now be accomplished by adding the following to the Canvas (some additional markup omitted for clarity):

 

<Path Stroke="#200F" StrokeThickness="1">
   <Path.Data>
      <PathGeometry Figures="{StaticResource RectanglePathFigureCollection}"/>
   </Path.Data>
</Path>

 

Here's the XAML of a Window - without the resources that you'll find in the sample solution's Resources.xaml file - that now makes the Rectangle follow the goofy shape and also allows to show/hide the shape itself (watch the video underneath it to see the Window in action or click the "2. ..."-button if you're running the sample project):

 

<Window x:Class="CS.AO_02_OneObject"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="01: A single object, following a pre-defined path" 
        Width="410" Height="300">
   <Grid>
      <Grid.RowDefinitions>
         <RowDefinition Height="*"/>
         <RowDefinition Height="Auto"/>
      </Grid.RowDefinitions>

      <!-- The Viewbox will allow to easily scale the Canvas'es contents -->
      <Viewbox Stretch="Uniform" Margin="5">
         <Border BorderBrush="DarkGray" BorderThickness="1">
            <Canvas Name="cvs" Background="#FFF0F0F0"
                    Width="450" Height="260">
               <!-- The Rectangle itself ... -->
               <Rectangle x:Name="rct" 
                          Fill="#E0ADD8E6" Width="50" Height="50"
                          Stroke="DarkBlue" StrokeThickness="2"
                          Panel.ZIndex="1">
                  <Rectangle.RenderTransform>
                     <TranslateTransform x:Name="ttAnimationTarget_Rect" X="0" Y="0" />
                  </Rectangle.RenderTransform>
                  <Rectangle.Triggers>
                     <!-- Start the animation as soon as the rectangle has been created -->
                     <EventTrigger RoutedEvent="FrameworkElement.Loaded">
                        <BeginStoryboard Storyboard="{StaticResource sb_Rect}"/>
                     </EventTrigger>
                  </Rectangle.Triggers>
               </Rectangle>

               <!-- ... and the path along which it travels -->
               <Path Stroke="#200F" StrokeThickness="1" 
                     Visibility="{Binding ElementName=chkShowPaths, Path=IsChecked,
                        Converter={StaticResource boolToVisibilityConverter}}">
                  <Path.Data>
                     <PathGeometry Figures="{StaticResource RectanglePathFigureCollection}"/>
                  </Path.Data>
               </Path>
            </Canvas>
         </Border>
      </Viewbox>

      <!-- This will allow to show/hide the path along which the shape travels. -->
      <CheckBox x:Name="chkShowPaths" 
                Content="Show Path"
                IsChecked="True"
                Margin="15,5"
                Grid.Row="1"/>
   </Grid>
</Window>

 

And here's a video showing the Window in action:

 

Connecting two objects with a line

Let's introduce a second shape that has its own path to travel along. In the next Window, I added an Ellipse. I also added the Path it travels along (also an ellipse). Both the Rectangle's and the Ellipse's Path overlap each other so we can see the shapes overlap each other at times. I'll skip the XAML for the Ellipse (it's all in the sample solution) as this is more or less the same as for the Rectangle - nothing new involved there.

What I'd rather like to focus on is a third shape that I'm adding: a Line that is continuously connecting both shapes. Due to WPF's binding mechanism, this shape does not require its own animation. Instead, its end-points are simply connected to a point on each the Rectangle and the Ellipse. That is, besides the Ellipse and its (travel-) Path, all we really need to add is a single tag. Here's the markup that defines the line:

 

<!-- The line connecting both objects -->
<Line StrokeThickness="1" Stroke="Red" Panel.ZIndex="2"
      X1="{Binding ElementName=rct, Path=RenderTransform.X}"
      Y1="{Binding ElementName=rct, Path=RenderTransform.Y}"
      X2="{Binding ElementName=el, Path=RenderTransform.X}"
      Y2="{Binding ElementName=el, Path=RenderTransform.Y}"/>

 

Pretty simple, huh? Here's a video of the Window in action (click the "3. ..."-button if you're running the sample project):

Well, while the line now neatly connects both shapes, it does so by using the top left corner - probably not all that neat really, especially with respect to the Ellipse where the line is simply connecting to a (virtual) rectangle that is made up by its Width and Height properties and thus ends outside of the shape. Also (and maybe not all too obvious), the line is utilizing the RenderTransform's current coordinates only; as a result, the line would be off if we would've placed the Rectangle or Ellipse somewhere else on the Canvas (i.e. by means of Canvas.Left/Canvas.Top).

Last but not least, you might have noticed that the two shapes, while traveling along the visualized Path, do so by means of their top left corner. This really is the behavior to be expected, but it would sure look better if the shapes's center would be (or at least seemed to be) on the current position/point of the Path.

 

Making the line's end-points stick to the center of the respective object

In order to have the line's end points connect to each shape's center and, in addition to the shape's TranslateTransform's X- and Y-properties that we used to bind our line to in the previous Window, we'll now need to also consider the shape's Width And Height which will allow us to calculate the offset required to point to the shape's center. While, in the last Window, we were able to directly bind to the TranslateTransform's X- and Y-properties, we will now have to create a MultiBinding. This in turn requires that we create a Converter that takes the bindings defined in the Window's XAML and returns either the center-point's X- or Y-offset.

Here's the converter (which is actually quite simple). Note that, even though I'm showing only C# code here, you'll find all the code in VB in the sample solution's VB-project.

 

/// <summary>
/// Takes a TranslateTransform + Width & Height and calculates/returns
/// the point in the center of the shape (either X or Y).
/// </summary>
public class CenterPointConverter
   : IMultiValueConverter
{
   /// <summary>
   /// Returns either the X- or the Y-axis of the center point/coordinate 
   /// of the passed object/shape.
   /// </summary>
   /// <param name="values">
   /// This array needs to always contain three values:
   ///
   ///   - the shape's TranslateTransform
   ///   - the shape's (Actual)Width
   ///   - the shape's (Actual)Height
   /// </param>
   /// <param name="targetType">(Irrelevant)</param>
   /// <param name="parameter">
   /// The axis-coordinate to return:
   ///   "True" for X
   ///   "False" for Y
   /// </param>
   /// <param name="culture">(Irrelevant)</param>
   /// <returns>
   /// The X- or Y-value of the requested axis'es center point.
   /// </returns>
   public object Convert(
         object[] values,
         Type targetType,
         object parameter,
         System.Globalization.CultureInfo culture
      )
   {
      //
      //Also, the parameter must 
      if (values == null || values.Length != 3) return null;

      TranslateTransform tt = (TranslateTransform)values[0];
      double dblWidth = (double)values[1];
      double dblHeight = (double)values[2];
      bool fGetX = bool.Parse(parameter.ToString());

      //Calc the center of the object
      Point pt = new Point(
            tt.X + dblWidth / 2,
            tt.Y + dblHeight / 2
         );

      //Return the requested axis
      return (fGetX ? pt.X : pt.Y);
   }

   //ConvertBack skipped for clarity (empty / not implemented)
}

 

The only thing worth noting here is the fact that, after the center-point has been calculated, the Convert-method's parameter argument is being used to specify whether to return the calculated point's X- or its Y-position. Why that? Because the Line shape requires that we address each position separately (X1,Y1 + X2, Y2). Here's the XAML for our Line shape that now uses the above converter instead:

 

<!-- 
   Centering both X1/Y1 and X2/Y2 so they're in the center of the 
   respective object 
-->
<Line StrokeThickness="1" Stroke="Red" Panel.ZIndex="2">
   <Line.X1>
      <MultiBinding Converter="{StaticResource centerPointConverter}"
                    ConverterParameter="True">
         <Binding ElementName="rct" Path="RenderTransform"/>
         <Binding ElementName="rct" Path="Width"/>
         <Binding ElementName="rct" Path="Height"/>
      </MultiBinding>
   </Line.X1>
   <Line.Y1>
      <MultiBinding Converter="{StaticResource centerPointConverter}"
                    ConverterParameter="False">
         <Binding ElementName="rct" Path="RenderTransform"/>
         <Binding ElementName="rct" Path="Width"/>
         <Binding ElementName="rct" Path="Height"/>
      </MultiBinding>
   </Line.Y1>
   <Line.X2>
      <MultiBinding Converter="{StaticResource centerPointConverter}"
                    ConverterParameter="True">
         <Binding ElementName="el" Path="RenderTransform"/>
         <Binding ElementName="el" Path="Width"/>
         <Binding ElementName="el" Path="Height"/>
      </MultiBinding>
   </Line.X2>
   <Line.Y2>
      <MultiBinding Converter="{StaticResource centerPointConverter}"
                    ConverterParameter="False">
         <Binding ElementName="el" Path="RenderTransform"/>
         <Binding ElementName="el" Path="Width"/>
         <Binding ElementName="el" Path="Height"/>
      </MultiBinding>
   </Line.Y2>
</Line>

 

Repositioning the visualized Path

Alright, we now achieved that the Line shape will always end in both the Rectangle's and the Ellipse's center. As I mentioned earlier, the DoubleAnimationUsingPath will continuously position our shapes at a given coordinate of the defined Path; since we target the TranslateTransform however, this will always be the respective shape's top-left corner. While we could, of course, introduce another converter that would take the TranslateTransform and again calculate the offset required to position our Rectangle and Ellipse with their center on the visualized path, let's rather shift the visualized Path instead. The technique here is just the same as the one we've been using to animate the Rectangle and Ellipse - we simply apply a TranslateTransform. But this time, we'll apply it to the Path's RenderTransform directly (we don't want this to be animated). Here's the Rectangle's Path that the Rectangle travels along (the one for the Ellipse only differs in the Element-binding and is otherwise identical):

 

<Path Stroke="#200F" StrokeThickness="1" 
      Visibility="{Binding ElementName=chkShowPaths, Path=IsChecked,
      Converter={StaticResource boolToVisibilityConverter}}">
   <Path.Data>
      <PathGeometry Figures="{StaticResource RectanglePathFigureCollection}"/>
   </Path.Data>
   <!--
      The following Transform will allow to draw the path based on the
      center of the Rectangle that follows it.
   -->
   <Path.RenderTransform>
      <TranslateTransform
         X="{Binding ElementName=rct, 
            Path=ActualWidth, 
            Converter={StaticResource divideBy2Converter}}"
         Y="{Binding ElementName=rct, 
            Path=ActualHeight, 
            Converter={StaticResource divideBy2Converter}}"/>
   </Path.RenderTransform>
</Path>

 

Again, the only thing worth noting here is the fact that another converter is required in order to divide the Width/Height by 2 to get the required offset for X/Y. Since the converter is so simple, it has been omitted here (you'll find it in the sample solutions Converters.cs/Converters.vb file). If you plan to use the DivideBy2Converter, please note that it doesn't consider any object displacement, i.e. the offset defined if your object is placed in a Canvas by means of the Canvas.Left/Canvas.Top attached properties as this would really require the use of a MultiBinding along with an IMultiValueConverter and thus have "polluted" the XAML.

Here's the next video of a Window in action which contains all of the above (click the "4. ..."-button if you're running the sample project):

 

Making the line stick to the perimeter center of the respective object

At this point, we actually have a nicer/cleaner version of the quick hack I posted in the forum-thread that I mentioned at the beginning, but not all too much that would go beyond (except for a few littleties). However, I thought I'd rather like the Line connect to the perimeters of both shapes. That is, there's two possible scenarios that I think could be used for i.e. charting or the such:

  • connecting two objects with the shortest line possible

and 

  • connecting between both object's center points, but with the line ending at the shape's perimeter.

Maybe I'll have another go in the future at the first alternative, but for the time being, I went for the latter one which imposed one of those little mathematical challenges that I like wrapping my head around.

Since, for the remainder of this section, we'll focus on the code required to calculate the perimeter points, here's a little excerpt of how the converter - that I'll introduce further down - needs to be used on the XAML-side. Note that, due to the verbosity, I'm showing only the Line's X1-part. The XAML for Y1, X2 and Y2 only differs in the ConverterParameter and/or in the ElementName-binding (reversing the order of appearance of the two shapes), so I guess it makes more sense to only post the relevant excerpt:

 

<Line.X1>
   <MultiBinding Converter="{StaticResource twoShapesPerimeterConverter}"
                 ConverterParameter="True">
      <Binding ElementName="rct"/>
      <Binding ElementName="rct" Path="RenderTransform"/>
      <Binding ElementName="el"/>
      <Binding ElementName="el" Path="RenderTransform"/>
      <Binding RelativeSource="{RelativeSource Self}"/>
   </MultiBinding>
</Line.X1>

 

As you can see in the XAML above, we now need to pass 5 parameters to the TwoShapesPerimeterConverter-converter. That is, we need each shape itself along with its RenderTransform plus the Line-shape. Since I'm passing a reference to each shape, you might ask yourself as to why the RenderTransform is being passed along. The reason is that the binding-mechanism (or rather INotifyPropertyChanged) is smart enough to only call into the converter if a relevant value changes. This "smartness" of course is necessary as, otherwise, the converter would be called over and over again, even if nothing changed that would justify the call in the first place. So, while that really is a good thing, it forces us to not only pass a reference to the shape in question (using which we could of course get at the RenderTransform), but also the RenderTransform. If we wouldn't, the converter wouldn't be called when it should.

Now that we sorted that out, let's look at the code. Before I do, let me once again point out that, even though I'm showing only C# code here, you'll find all the code in VB in the sample solution's VB-project. Also, I'll start with the main body of the Convert-method, skipping parts that I'll get to later:

 

public object Convert(
		object[] values,
		Type targetType,
		object parameter,
		System.Globalization.CultureInfo culture
	 )
  {
	 if (values == null || values.Length != 5) return null;

	 //First shape
	 FrameworkElement fe_O1 = (FrameworkElement)values[0];
	 TranslateTransform tt_O1 = (TranslateTransform)values[1];
	 double dblWidth_O1 = fe_O1.Width;
	 double dblHeight_O1 = fe_O1.Height;
	 //Second shape
	 FrameworkElement fe_O2 = (FrameworkElement)values[2];
	 TranslateTransform tt_O2 = (TranslateTransform)values[3];
	 double dblWidth_O2 = fe_O2.Width;
	 double dblHeight_O2 = fe_O2.Height;
	 //The connecting line
	 Line lne = (Line)values[4];
	 //Whether to return the X- or Y-axis
	 bool fGetX = bool.Parse(parameter.ToString());

	 double dblOffset_X_O1 = 0;
	 double dblOffset_Y_O1 = 0;
	 double dblOffset_X_O2 = 0;
	 double dblOffset_Y_O2 = 0;

	 //We're assuming that the control is part of a canvas, 
	 //so retrieve its position. 
	 //This will, however, only work if Canvas.Left/Canvas.Top 
	 //have been set, so check first.
	 if (!double.IsNaN(Canvas.GetLeft(fe_O1)))
		dblOffset_X_O1 += Canvas.GetLeft(fe_O1);
	 if (!double.IsNaN(Canvas.GetTop(fe_O1)))
		dblOffset_Y_O1 += Canvas.GetTop(fe_O1);

	 if (!double.IsNaN(Canvas.GetLeft(fe_O2)))
		dblOffset_X_O2 += Canvas.GetLeft(fe_O2);
	 if (!double.IsNaN(Canvas.GetTop(fe_O2)))
		dblOffset_Y_O2 += Canvas.GetTop(fe_O2);

	 //Calc the center of the first object
	 Point ptCenter_O1 = new Point(
		   dblOffset_X_O1 + tt_O1.X + dblWidth_O1 / 2,
		   dblOffset_Y_O1 + tt_O1.Y + dblHeight_O1 / 2
		);

	 //Calc the center of the second object
	 Point ptCenter_O2 = new Point(
		   dblOffset_X_O2 + tt_O2.X + dblWidth_O2 / 2,
		   dblOffset_Y_O2 + tt_O2.Y + dblHeight_O2 / 2
		);

	 //The point of which we'll return either X or Y.
	 //In case anything goes wrong, we'll simply return 
	 //a point that is negative and thus hopefully out of view.
	 Point ptPerimeterIntersection = new Point(-1000, -1000);

	 //Calculate the point on the first shape's perimeter,
	 //according to the type passed.
	 string strType = fe_O1.GetType().Name;
	 switch (strType)
	 {
		case "Rectangle":
		   [...]
		   break;

		case "Ellipse":
		   [...]
		   break;

		default:
		   //Oops.
		   throw new NotSupportedException("Unsupported type \"" + strType + "\".");
	 }

	 //Only show the line, if the resulting point is outside of the object, hide it
	 lne.Visibility = (ptPerimeterIntersection.X < 0 || ptPerimeterIntersection.Y < 0 ? Visibility.Hidden : Visibility.Visible);

	 //Return the requested axis
	 return (fGetX ? ptPerimeterIntersection.X : ptPerimeterIntersection.Y);
  }

 

There shouldn't anything overly surprising in the above: the converter first assures that the expected number of arguments has been passed, casts them into strongly typed variables, does a few calculations (to again get both shapes' the center-points) and then finally gets to its "heart" (more on that to come), i.e. the stuff represented by the [...]-placeholders. There's only three things worth noting here:

  1. Other than with the previously introduced converters, this one now considers a potentially existing offset applied to the shape by means of Canvas.Top/Canvas.Left.
  2. The switch-statement (Select Case in VB) is based on the name of the passed shape's type to determine what math is to be applied (see below for the math itself).
  3. The point that we return X or Y for defaults to a location outside of view, so that, if the respective shape's calculation doesn't return a valid value (which would be the case if both shapes overlap each other), the Line can be hidden.

As you can see, I only considered two general types of shapes: a Rectangle and an Ellipse. I'm not really planning on adding more shapes (this is purely academic really - I have no use for this at present), but this would be the place where you'd add the math for other types.

Before we peek at both shapes and the math required to get a point on their perimeter, let me send ahead that the center-points are still the basis for all calculations to follow. That is, first a virtual line will be considered that connects between both shapes' center points, then this line is "shortened" so that it ends on the center of the perimeter (or the center of the shape's stroke) of the first object that has been passed into the converter. This virtual line is actually the reason for the necessity of passing both shapes into the converter.

 

Calculating the perimeter-point for the Rectangle

For the Rectangle, the code in the converter is quite short, but the reason really is that it requires a) repetetive calls to identical code and b) contains generic code that could be used somewhere else, hence I put this into a separate class. FWIW, here's the code that makes up for the previous snippet's [...]-placeholder:

 

Rectangle rct = (Rectangle)fe_O1;
//Create a line that connects both objects' center points.
Line lneConnector = new Line()
{
  X1 = ptCenter_O1.X,
  Y1 = ptCenter_O1.Y,
  X2 = ptCenter_O2.X,
  Y2 = ptCenter_O2.Y
};
Rect r = new Rect()
{
  X = dblOffset_X_O1 + tt_O1.X,
  Y = dblOffset_Y_O1 + tt_O1.Y,
  Size = new Size(rct.ActualWidth, rct.ActualHeight)
};
//The calculations are quite verbose, hence I placed them in a helper-class.
//See the below method's definition for more information.
ptPerimeterIntersection = Helpers.RectAndLineIntersectionPoint(r, rct.StrokeThickness, lneConnector);
break;

 

So all we do here is to prepare the arguments that the RectAndLineIntersectionPoint() method expects. These consist of a rect-structure (which contains all information required about our Rectangle), the Rectangle's StrokeThickness (which we need in order to return a point that sits in the center of the stroke) and a Line that connects the Rectangle itself with our second end-point which really is the center of an arbitrary other shape (the Ellipse in our sample Window).

Here's the RectAndLineIntersectionPoint() method (minus the header comments):

 

public static Point RectAndLineIntersectionPoint(
	Rect rct, 
	double dblRectStrokeWidth, 
	Line lne
 )
{
 Point ptIntersection = new Point(-1000, -1000); //Default: outside of visible area
 double RO = dblRectStrokeWidth / 2; //Offset required due to the Rectangle's stroke-width 

 //We'll need to test the four sides of the Rectangle
 Line lneRect_Top = new Line() { X1 = rct.Left + RO, X2 = rct.Right - RO, Y1 = rct.Top + RO, Y2 = rct.Top + RO };
 Line lneRect_Bottom = new Line() { X1 = rct.Left + RO, X2 = rct.Right - RO, Y1 = rct.Bottom - RO, Y2 = rct.Bottom - RO };
 Line lneRect_Left = new Line() { X1 = rct.Left + RO, X2 = rct.Left + RO, Y1 = rct.Top + RO, Y2 = rct.Bottom - RO };
 Line lneRect_Right = new Line() { X1 = rct.Right - RO, X2 = rct.Right - RO, Y1 = rct.Top + RO, Y2 = rct.Bottom - RO };

 if (!Helpers.DoLinesIntersect(lneRect_Left, lne, ref ptIntersection))
	if (!Helpers.DoLinesIntersect(lneRect_Top, lne, ref ptIntersection))
	   if (!Helpers.DoLinesIntersect(lneRect_Right, lne, ref ptIntersection))
		  if (!Helpers.DoLinesIntersect(lneRect_Bottom, lne, ref ptIntersection))
			 return new Point(-1000, -1000);

 return ptIntersection;
}

 

Here's how it works: in order to find the side that the also passed line intersects, we'll have test each of the Rectangle's sides separately. For the order of the tests/sides, I opted to follow the same approach as with i.e. the Margin attached property does - starting on the left and then going on clockwise to the bottom, stopping the calculations as soon as the first intersection point has been found. I know I could've created the Line-objects only when required, too, but this is one of the occasions where I prefer readability over performance (I didn't do any perf tests to see what the cost really is). The separate Line objects being created, BTW, are solely for the purpose of considering the StrokeThickness and thus calculating the offset we need. That is, the rect-structure is simply reduced by half of the StrokeThickness on each side so that the DoLinesIntersect()-method can operate on the values required to return a point in the center of the stroke.

I won't discuss the code behind the DoLinesIntersect()-method as I simply took this method over from somebody else and converted it into a C# and VB-version. For more information, see the source code in the sample solution (you'll find the above method as well as the DoLinesIntersect()-method in Helpers.cs/Helpers.vb). You can also refer to the Paul Bourke's site where I took the math from: Intersection Point Of Two Lines (2 Dimensions). Thanks from here go out to Paul and the folks that kindly provided the source-code in C/C++ which I used for my C#/VB versions.

 

Calculating the perimeter-point for the Ellipse

So, here goes the last code-snippet which calculates the point on the Ellipse's perimeter that is being intersected by the line traveling between the Ellipse's and the other shape's center points:

 

Ellipse el = (Ellipse)fe_O1;
double dblEllipse_Radius_X = (el.ActualWidth - el.StrokeThickness) / 2;
double dblEllipse_Radius_Y = (el.ActualHeight - el.StrokeThickness) / 2;

//Create a vector for each a flat X- and one for X- and Y-axis - this will
//allow us to calculate the angle of a virtual line connecting the 
//center-points of both objects.
Vector v1 = new Vector(0, ptCenter_O1.Y - ptCenter_O2.Y);
Vector v2 = new Vector(ptCenter_O2.X - ptCenter_O1.X, v1.Y);

//If the second object is left of the ellipse, we'll need to return
//a point on the left half of the ellipse; if not, the perimeter-point 
//will be on its right half.
bool fIsOnLeftHalf = (ptCenter_O2.X < ptCenter_O1.X);

//Get the angle between both vectors
double dblAngle_GRAD = Vector.AngleBetween(v2, v1);

//The Vector.AngleBetween will return a value in degrees (and shifted 90° to the right
//due to the different coordinate-system that with 0° "in the east"), but we'll need 
//the value in radians for the calculations coming up, so convert the value accordingly.
double dblAngle_RAD = (Math.PI * (dblAngle_GRAD - 90d)) / 180d;

//In order to have this work not only for a circle, but also for an ellipse,
//will have to consider both radii (major/minor).
double theta = Math.Atan((dblEllipse_Radius_X / dblEllipse_Radius_Y) * Math.Tan(dblAngle_RAD));
double x = ptCenter_O1.X + dblEllipse_Radius_X * Math.Cos(theta) * (fIsOnLeftHalf ? -1 : 1);
double y = ptCenter_O1.Y + dblEllipse_Radius_Y * Math.Sin(theta) * (fIsOnLeftHalf ? -1 : 1);

//The above will, due to the usage of vectors, not consider whether or not the line is traveling
//through the perimeter, so test whether the calcuated point of the 2nd object lies within the 
//ellipse. Since, further down, we'll be testing as to whether the found point is located
//beyond the visible area (which is the perimeter-point's current value), only assign the
//found point if it is outside of a "virtual" (!) ellipsis defined with the center-point being
//the 2nd object's center - we cannot test with the "real" ellipsis itself since the point is
//ON but not OUTSIDE of the ellipse.
Point ptTemp = new Point(x, y);
if (!Helpers.ContainsPoint(ptCenter_O2, dblEllipse_Radius_X, ptTemp)
	 && !Helpers.ContainsPoint(ptCenter_O2, dblEllipse_Radius_Y, ptTemp))
  ptPerimeterIntersection = ptTemp;
break;

 

I sort of wondered as to why I couldn't find anything on the web for this (should really be something somebody else has done I thought). But well, that part was the most fun (and my personal challenge) anyway, so here's how I went about this.

Taking the Ellipse's radii (major and minor, that is), two Vectors are created that, in turn, allow us to simply utilize the Vector-class'es AngleBetween()-method to calculate the angle of the (virtual) line that connects our Ellipse with the other object (from center to center). Once we have the angle, we need to a) shift it counter-clockwise by 90° (due to .Net's coordinate system starting at 90°/"east") and then convert it back into radians (which is what we'll need for the trigonometry functions). Using the arctangent we can then calculate the X- and Y-coordinates of our point on the perimeter using the ubiquituous sinus/cosinus functions. Since the resulting point will always be on the right half of our Ellipse, a little check will simply multiply the result with -1 if it detects that we need to target the Ellipse's left half. The only thing left to do after the point has been found is to check as to whether our virtual line ends inside the ellipse (and is thus not intersecting its perimeter) in which case we don't want to show the line at all (which again we do by not setting the perimeter-point). To do the check, the code simply checks whether the calculated center-point of the second/other shape is residing within the Ellipse.

 

Wrapping it all up

Here's a video with a Window in action that again contains all of the above (click the "5. ..."-button if you're running the sample project):

 

The last word

As I said, this really was a nice way to waste my time - I don't have any scenario in which I think I could use the stuff introduced here (which is probably why it's been so much fun). That having been said, I'd sure like to know if you find a scenario for which this actually would be useful. Also, my math is problably not the best, so if you think that there would be an easier approach to do the calculations (or just a different one), make sure you drop a comment!

 

The sample solution

I’ve created a sample solution that contains everything discussed here, containing one project for each the C# and the VB versions.

Download: AnimatingObjectsAlongAPath.zip (54.47 kb)


Location: PostList

Tags: , , , , , ,

WPF (.net)

WPF: WindowHelpers - how to retrieve a window-reference by the window's name and others

by Olaf Rabbachin 16. February 2010 15:56

Today I have a quickie that I thought would be worth sharing. Note that, even though I only present C# code in this article, the sample solution (see the bottom for the download) again contains both a project for C# and VB.

 

The problem

In the past 6 months, I've spent quite a while or two in the WPF forums, crawling over other peoples' questions and learning by answering them. A very common approach to answering is to simply post a sample window that i.e. demonstrates the solution to the OP's question. In the very beginning, I tended to have a sample app with a Window1 which got loaded upon startup, replacing the XAML and/or code-behind everytime I created a sample window. That meant that, everytime I tried something new, the earlier version was either lost or had to be copied into another window. Alternatively, I sometimes named windows and then changed App.xaml to show the window I wanted.

Any of the aforementioned approaches includes either a) the loss of previous work, or b) additional work everytime you add something or need to show a different (older) window. I hence thought it should be fairly easy to simply make a main window, provide a list of windows found throughout the application and just open them on a click (or double-click).

 

Reflection to the rescue

I must say I just love Reflection - it provides a convenient (well, most of all times) approach to obtain information that just wasn't possible (or only with many hacks) in pre-.Net-times!

Here goes.

 

Getting a list of all windows

Here's all you need to do in order to get all windows that are part of the executing assemlby (aka the exe):

 

public static IEnumerable<string> WindowNames
{
   get
   {
      IEnumerable<string> ieWindowNames = null;
      Assembly asm = Assembly.GetExecutingAssembly();

      ieWindowNames =
         from types in asm.GetTypes()
         where types.BaseType.Name == "Window"
         orderby types.Name
         select types.Name;

      return ieWindowNames;
   }
}

 

The above returns a list (IEnumerable) that would allow for binding and, thanks to Linq, sorted alphabetically.

 

Displaying windows using their name

How about providing a convenient way of displaying a window when all you have is its name (i.e., taken from the previously introduced list)?

Again, this can be done with just a few lines:

 

public static bool? ShowWindowByName(string strWindowName, bool fShowDialog)
{
   if (string.IsNullOrEmpty(strWindowName)) return null;

   Assembly asm = Assembly.GetExecutingAssembly();
   string strFullyQualifiedName = asm.GetName().Name + "." + strWindowName;
   object obj = asm.CreateInstance(strFullyQualifiedName);

   Window win = obj as Window;
   if (win == null) return null;

   if (fShowDialog)
   {
      win.ShowDialog();
      return win.DialogResult;
   }
   else
   {
      win.Show();
      return null;
   }
}

 

The above method also allows you to show the window modally in which case the method will return the DialogResult after the window was closed.

 

WindowByName

The ShowWindowByName() method might actually do more than desired. If you only need a reference to a window by its name, here's another little helper method:

 

public static Window WindowByName(string strWindowName)
{
   if (string.IsNullOrEmpty(strWindowName)) return null;

   Assembly asm = Assembly.GetExecutingAssembly();
   string strFullyQualifiedName = asm.GetName().Name + "." + strWindowName;
   object obj = asm.CreateInstance(strFullyQualifiedName);

   if (obj != null)
      return obj as Window;
   else
      return null;
}

 

Closing all windows

If you're using VB, one way to close all open windows when your application exits is to specify this in the project's settings:

However, C# doesn't have a respective counterpart. And, anyway, I tend to rather have my own method for dealing with this, so here's another simple helper:

public static void CloseAllWindows()
{
   Application app = Application.Current;
   for (int intCounter = app.Windows.Count - 1; intCounter >= 0; intCounter--)
      app.Windows[intCounter].Close();
}
Due to the simplicity, it's not really all too worth sharing (must say, I feeling a bit ashamed to post something like this), but I thought the method simply belongs to the class. Embarassed

 

The sample solution

I've placed all of the above (basic documentation included - skipped in the article) into a simple class that you'll find in the solution (see the bottom for the download link). However, since I don't want to provide a bunch of windows just to allow to test things out, the projects' sample window only contains - besides the main window - a dummy second window to have at least two of them. Here's a screenshot:

The code-behind for the above is pretty short:

using System.Windows;
using System.Collections.Generic;
using System.Windows.Controls;

namespace CS
{
   public partial class Main : Window
   {
      public Main()
      {
         InitializeComponent();
         this.DataContext = this;
         this.Unloaded += new RoutedEventHandler(MainWindow_Unloaded);
      }

      //Allows to bind the ListBox to the window names in this assembly
      public IEnumerable<string> WindowNames { get { return Utils.WindowHelpers.WindowNames; } }

      //Show a window after an item in the ListBox has been double-clicked.
      private void lbWindows_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
      {
         if (lbWindows.SelectedItem != null)
         {
            Utils.WindowHelpers.ShowWindowByName(
                  lbWindows.SelectedItem.ToString(),
                  (bool)chkShowDialog.IsChecked
               );
            e.Handled = true;
         }
      }

      void MainWindow_Unloaded(object sender, RoutedEventArgs e)
      {
         Utils.WindowHelpers.CloseAllWindows();
      }
   }
}

But wait!

In the code above, you may (or may not) have noted that the selected window is shown when double-clicking a window rather than after a single click has been performed. I have no idea as to why there isn't any DoubleClick-event for the ListBox control, but luckily, it's not all too hard to provide a dummy one. I can't take credit for that as this is something I stumbled over in this StackOverflow thread, so thanks go out to Bob King. All you'll have to remember when going that way though is to set e.Handled = true to avoid the message from bubbling up.

 

The last word

I presently don't really see much "real world" usage scenarios for the class, but my forums-life has gotten a hell of a lot easier. Using the class, I can simply add windows to my test-solution as they come and then access/run them by selecting them from a list of my pre-defined main window. One problem that this might impose on you (it does on me) - the time it takes to build your solution will increase rapidly. My present solution has short of 100 windows ...

 

The sample solution

I’ve created a sample solution that contains everything discussed here.

Download: WindowByName.zip (27.38 kb)


Location: PostList

Tags: , , , , , ,

Utilities | Utilities | WPF (.net) | WPF (.net)

WPF: ColorHelper - how to retrieve the name of a given color or to retrieve a color by its name

by Olaf Rabbachin 5. February 2010 19:47

Today I stumbled over a thread in the WPF forum in which the OP was looking for a way to retrieve the (known) name for a given color.
That is, given a color such as #FFB22222 (or ARGB :: 255, 178, 32, 32), to retrieve "Firebrick". What I thought was darn simple actually isn't, because System.Windows.Media.Colors is a class and thus exposes all (known) colors as properties (as opposed to System.Drawing.KnownColor which is an enum). So there really isn't any other way than to use reflection to obtain a list of all colors. Here's a little helper class that returns the name of a color passed to it:

Retrieving the name of a given Color object

/// <summary>
/// Returns the known name of the color passed (if found), or an empty string.
/// </summary>
/// <param name="clr">The color whose name is to be returned.</param>
/// <returns>
/// The name of the passed color resp. an empty string if 
/// no matching known color could be found.
/// </returns>
public static string GetKnownColorName(Color clr)
{
   Color clrKnownColor;

   //Use reflection to get all known colors
   Type ColorType = typeof(System.Windows.Media.Colors);
   PropertyInfo[] arrPiColors = ColorType.GetProperties(BindingFlags.Public | BindingFlags.Static);

   //Iterate over all known colors, convert each to a <Color> and then compare
   //that color to the passed color.
   foreach (PropertyInfo pi in arrPiColors)
   {
      clrKnownColor = (Color)pi.GetValue(null, null);
      if (clrKnownColor == clr) return pi.Name;
   }

   return string.Empty;
}

If you call the above method with i.e. ...

string strColorName = GetKnownColorName(Color.FromArgb(255, 178, 32, 32));
MessageBox.Show(strColorName == "" ? "(None found)" : strColorName);

... the MessageBox will show "Firebrick".

 

Update to the above (Feb 13 2010)

I just updated the code for the method above after it became obvious that the GetHashCode() method is not returning unique values. I did a bit of searching in order to find out more about that, but couldn't find anything; the docs are no help at all with this respect or even when it comes to the reason for the existance of this method in the first place.

Actually, I don't have the slightest idea as to how the hashes couldn't be unique - colors resp. their ARGB values are uniqe in itself after all. However, the hashes for Colors.White and Colors.Blue are equal. Now if that isn't plain stupid! Lesson learned: never assume anything ...

Thanks to Pedro for catching this (see the comments).

 

Retrieving a list with all known colors (names + Color objects)

If you actually need a list with all color values and their name-counterparts (might make sense if you need this frequently), here's another little helper-method that will return all known colors along with the name of each color:

/// <summary>
/// Returns a list containing all known colors, each as a KeyValuePair with the name
/// as the key and the Color as the value.
/// </summary>
public static List<KeyValuePair<string, Color>> GetKnownColors()
{
   List<KeyValuePair<string, Color>> lst = new List<KeyValuePair<string, Color>>();
   Type ColorType = typeof(System.Windows.Media.Colors);
   PropertyInfo[] arrPiColors = ColorType.GetProperties(BindingFlags.Public | BindingFlags.Static);

   foreach (PropertyInfo pi in arrPiColors)
      lst.Add(new KeyValuePair<string, Color>(pi.Name, (Color)pi.GetValue(null, null)));
   return lst;
}

You could use the above in various ways, here's just one sample:

Color clr = Color.FromArgb(255, 178, 32, 32);
List> lstKnownColors = GetKnownColors();
string strColorName = (
   from c in lstKnownColors
   where
      c.Value.A == clr.A &&
      c.Value.R == clr.R &&
      c.Value.G == clr.G &&
      c.Value.B == clr.B
   select c.Key
   ).FirstOrDefault();

 

Retrieving a color via its name

Last but not least, if you need to go the opposite way, how about another simple helper method:

 

/// <summary>
/// Returns the Color which is represented by the name passed.
/// </summary>
/// <param name="strColorName">The name of the Color to retrieve.</param>
/// <returns>Nullable color - either the found Color or null.</returns>
public static Color? GetColorByName(string strColorName)
{
   Color? clrResult = null;
   try
   {
      object value = ColorConverter.ConvertFromString(strColorName);
      if (null != value) clrResult = (Color)value; 
   }
   catch (Exception) { }

   return clrResult;
}

 

Update to the above (Feb 15 2010)

I've just updated the GetColorByName method after Richard pointed out that it was prone to fail if an invalid name or an empty string were passed (see the comments). I've only resolved for a general purpose handler rather than just to catch FormatExceptions.

 

Wrapping it all up

To simplify things, I dropped the above into a class that you can simply drop into your projects.
Download it here:

C# version: ColorHelpers.cs (3.43 kb)

VB version: ColorHelpers.vb (3.48 kb)

 

Happy coding!


Location: PostList

Tags: , , , , , ,

Utilities | WPF (.net)

About

Hi and welcome to my blog!

I'm a developer from Germany, currently focusing on .Net and WPF.

More about me ...