Dynamically Creating Path Data in Silverlight 2

Please follow and share!
Twitter
Facebook
LinkedIn
RSS

Recently I started playing around with the creation of a Silverlight application of C#; leaving my comfort zone of doing Silverlight development solely in IronRuby.  The project that I decided to start is an application that is similar to the famous "Wheel of Lunch", which will essentially query the Windows Live Search phonebook web service for any number of criteria (I am not limiting it to places to eat, you can choose to search hardware stores if you’d like), and have it populate what I am now calling the "Wheel of Choice" with the search results, it will then allow the user to spin the wheel, and the app will provide the user with their choice.

My adventure with Silverlight paths started as I was trying to figure out how to render the wheel.  I wanted to keep this Silverlight user control as flexible as possible, so that I could easily use it in other projects.  The requirements that I put forth were:

  • the wheel size must be customizable
  • the wheel must be able to be split into any number of sections
  • the sections of the wheel must be rendered to be of equal size
  • each section must be labeled with text passed in to the control(to represent a choice)

It was necessary to generate each section of the wheel separately, so that each section could be it’s own element.  The Path element is the most flexible solution, it’s Data attribute essentially contains instructions on where to draw random lines and curves on the canvas.  Looking at Path data can be daunting, it looks like a bunch of letters and numbers concatenated together, and to the blind eye, it really does not tell you a whole lot (example: "M 10100 C 10300 300,-200 250100z"). 

In doing a little research I found out that the data string is made up of a Path sub-language, I find it similar to the old Turtle graphics programs, only it’s definitely made for grown-ups.  A reference to the Path syntax is found here.  While I completely understand that you can make up a path’s data by using a bunch of geometry elements,  I wanted more of a challenge so I decided to dynamically build the paths for each section using the Path syntax.

The control was created with two public properties:

public double Radius { get; set; }
public List<string> SectionLabels { get; set; }

Radius takes care of the overall size of the wheel, and SectionLabel defines the tool tip to set on each section (as well as defines the number of sections needed on the wheel).  Some private properties are also used to be used globally throughout the user control:

private int _sectionCount { get; set; }
private double _theta { get; set; }
private Color[] _colors = { Colors.Red, Colors.Green, Colors.Blue, Colors.Yellow, Colors.Orange };

_sectionCount places the total number of sections in a value type, just so that it’s not necessary to keep pulling the SectionLabels.Count property. _theta is the angle that is used to identify endpoints on the edge of the wheel.  _colors is an array used to assign a color to each section created.  Setting of these values and the calculation of _theta is as follows:

//calculates the number of sections to render as well as the theta value used to calculate the 
//endpoints on the circle to render lines and arcs.
private void Calculate()
{
_sectionCount = SectionLabels.Count;
if (_sectionCount <= 0)
_sectionCount = 1;
_theta = 2 * Math.PI / _sectionCount; 
}

As specified before, _theta is used to find the evenly-spaced endpoints around the circle which will make up the sections.  The method to calculate these endpoints is as follows (where section is simply the index of the section being rendered):

private Point calculateCircleEndPoint(int section)
{
double x = (Radius * (Math.Cos(section * _theta))) + Radius; 
double y = (Radius * (Math.Sin(section * _theta)))+ Radius;
return new Point(x, y);
}

Putting it all together, the RenderWheel() public method takes care of calculating the endpoints as well as defining the Path.  The first lesson learned is that you cannot directly assign the generated path data string to a Path’s data attribute, it must be of type Geometry.  To get around this issue, the XAML for the path is instead generated with the string value in the data attribute, then cast back into a Path element and then added to the visual root of the user control. Do not forget to include the namespaces in this generated XAML.  The well-commented listing of the RenderWheel method is as follows:

public void RenderWheel()
{
Calculate();
 
//a point in the center of the circle
Point circleCenter = new Point(Radius, Radius);
 
//array to hold evenly spaced endpoints on the circle
Point[] circlePoints = new Point[_sectionCount];
 
//calculate the evenly-spaced points on the edge of the circle
for (int i = 0; i < _sectionCount; i++)
{
circlePoints[i] = calculateCircleEndPoint(i);
}
 
int colorIndex = 0;
 
//build out the path representing each segment
for (int i = 0; i < _sectionCount; i++)
{
StringBuilder pathData = new StringBuilder();
//move to circle center
pathData.Append("M ");
pathData.Append(circleCenter.ToString());
//draw line to edge of circle, to the first calculated endpoint
pathData.Append(" L ");
pathData.Append(circlePoints[i].ToString());
//draw an arc 45 degrees to the next circle end point 
//(or the first calculated point if it is the last drawn segment)
pathData.Append(" A ");
pathData.Append(circleCenter.ToString());
pathData.Append(" 45 0 1 ");
int check = i + 1;
if (check == _sectionCount)
check = 0;
pathData.Append(circlePoints[check].ToString());
//close the section shape (renders line back to the center point)
pathData.Append(" Z ");
 
//Cannot add string path directly to Data property, so it must be injected, also inserts tool tip xaml
string sectionTip = "<ToolTipService.ToolTip><ToolTip Content=\"" + SectionLabels[i] + "\"/></ToolTipService.ToolTip>";
string nsPath = "<Path xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" Data=\"";
Path section = (Path)XamlReader.Load(nsPath + pathData.ToString() + "\" x:Name=\"sect"+ i +"\">"+ sectionTip +"</Path>"); 
section.Stroke = new SolidColorBrush(Colors.Black);
section.StrokeThickness = 0.3;
 
//color the newly created section
section.Fill = new SolidColorBrush(_colors[colorIndex]);
if (colorIndex + 1 == _colors.Length)
colorIndex = 0;
else
colorIndex++;

//add filled path
this.splitWheelRoot.Children.Add(section);
}
 
}

For sake of completeness, here is the XAML of the user control (only an empty canvas):

<UserControl x:Class="LiveSearch.SplitWheel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
>
<Canvas x:Name="splitWheelRoot">

 
</Canvas>
</UserControl>

The complete code-behind for this control can be found here . The final result (though not very pretty) looks like this:

wheel

Please follow and share!
Twitter
Facebook
LinkedIn
RSS