Introduction
This article follows the first one in this series. The first article described the design considerations and the way to use the code for a set of custom gauge controls for the Windows Phone 7 platform. This article will dig deeper into the problem and will describe the implementation of the scale classes. If you haven't read the first article, please start with that one first as it will give you a better understanding of what this article is trying to present.
The articles in this series
Below, you can find a list of all the articles in this series:
The Scale base class
As you saw in the previous article, the Scale hierarchy has three classes: a base class (Scale
) that is used to hold the logic and properties that are common for every gauge, and two derived classes that implement specific functionality for linear and radial gauges.
The image below presents a diagram of all the common properties a gauge should have regardless of its shape.
These are all dependency properties. You can see that the Scale
has properties to hold the value interval (Minimum
and Maximum
), the indicators, the ranges, and some range properties (thickness, whether or not to use a default range, and whether or not to use the range’s colors to paint the ticks and labels that fall within that range in that particular color). The base class also has properties that are used to configure the label and tick templates.
There are a few things to discuss regarding some of these properties. The first property I’m going to talk about is the Indicators
property. This is used to expose the collection of indicators that the scale will have. The indicators will not be held directly inside the scale panel. Instead, I have defined a private Canvas
that holds the indicators. This canvas instance is then added to the Scale
’s Children
collection. The Indicators
property exposes the Canvas
' Children
property. This can be seen in the code below:
Canvas indicatorContainer;
public Scale()
{
indicatorContainer = new Canvas();
Children.Add(indicatorContainer);
}
public UIElementCollection Indicators
{
get { return indicatorContainer.Children; }
}
The next property is the Ranges
property. This property is used to hold all the optimal ranges the user decides to use. This property has the type ObservableCollection<GaugeRange>
. Each range has a maximum value (the offset) and a color. The type definition can be seen in the code below:
public class GaugeRange
{
public double Offset { get; set; }
public Color Color { get; set; }
}
A particular range will be drawn from where the previous one ends until the Offset
. The first range starts at 0. Another thing to mention here is that this collection only describes the ranges. In order to add ranges to our scale, we need to define the shapes and set the brushes according to the colors and add them to the Scale
’s Children
collection. There are two methods that are used to add and remove ranges from the scale. These are CreateRanges()
and ClearRanges()
. Since the range creation depends on the type of range, these two methods are abstract and need to be overridden in the derived classes. The declarations can be seen below:
protected abstract void CreateRanges();
protected abstract void ClearRanges();
The next set of properties I’m going to talk about are the tick and label customization properties. These are the properties that set the templates for the ticks and labels. These are plain DataTemplate properties, but the nice code to show here is the code that handles the lack of templates. The class has a couple of methods that are used to apply the templates. If the user specified the templates, the methods will apply those; if the user didn’t specify any templates, the methods apply implicit templates. These are obtained by loading a predefined XAML string. At the moment, this is hard coded. The two methods can be seen in the listing below:
protected virtual DataTemplate CreateDataTemplate(TickType tType)
{
string template = "";
if (tType == TickType.Label)
template = @"<TextBlock Text=""{Binding}"" FontSize=""13"" FontWeight=""Bold"" />";
else if (tType == TickType.Minor)
template = @"<Rectangle Fill=""Snow"" Width=""2"" Height=""5"" />";
else
template = @"<Rectangle Fill=""Snow"" Width=""2"" Height=""10"" />";
string xaml =
@"<DataTemplate
xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"">"+
template+@"</DataTemplate>";
DataTemplate dt = (DataTemplate)XamlReader.Load(xaml);
return dt;
}
private DataTemplate GetTickTemplate(TickType tType)
{
if (tType == TickType.Label)
{
if (LabelTemplate != null)
return LabelTemplate;
return CreateDataTemplate(tType);
}
else if (tType == TickType.Minor)
{
if (MinorTickTemplate != null)
return MinorTickTemplate;
return CreateDataTemplate(tType);
}
else
{
if (MajorTickTemplate != null)
return MajorTickTemplate;
return CreateDataTemplate(tType);
}
}
The CreateDataTemplate
method is marked as virtual
so that it can be overridden in the derived classes to provide different templates for linear and radial scales.
The last thing to talk about in this base class is the tick and label creation. The ticks and labels are held in two private variables. The definitions can be seen below:
List<Tick> labels=new List<Tick>();
List<Tick> ticks=new List<Tick>();
As you can see, the ticks and labels are represented by the Tick
class. This class is used to represent labels, minor ticks, and major ticks. The definition of this class can be seen below:
public enum TickType { Minor, Major, Label }
public class Tick:ContentPresenter
{
public double Value
{
get { return (double)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("ValueProperty", typeof(double),
typeof(Tick), new PropertyMetadata(0.0));
public TickType TickType
{
get { return (TickType)GetValue(TickTypeProperty); }
set { SetValue(TickTypeProperty, value); }
}
public static readonly DependencyProperty TickTypeProperty =
DependencyProperty.Register("TickType", typeof(TickType),
typeof(Tick), new PropertyMetadata(TickType.Minor));
}
These labels and ticks are created the first time in the constructor of the class and then destroyed and recreated every time tick related properties or the gauge size changes. The label creation method can be seen in the code below:
private void CreateLabels()
{
double max = Maximum;
double min = Minimum;
for (double v = min; v <= max; v += MajorTickStep)
{
Tick tick = new Tick() { Value = v, TickType = TickType.Label };
labels.Add(tick);
tick.ContentTemplate = GetTickTemplate(TickType.Label);
tick.Content = v;
Children.Insert(0, tick);
}
}
The labels will be created only for the major ticks. The function starts from the minimum value, and for every major tick, creates a label. It creates a tick instance, and sets the value, the type, and the template. As you can see, the template is assigned using the functions described above. At the end, the code sets the content and adds the label to the Scale
’s Children
collection. The function uses Insert
instead of Add
so that we can be sure the indicators are always on top of the other scale graphics.
The ticks are created in a similar fashion. The only difference is that an additional check is done to determine the minor ticks. The code for the tick creation method can be seen below:
private void CreateTicks()
{
double max = Maximum;
double min = Minimum;
int num = 0; double val = min; while (val <= max)
{
DataTemplate template = null;
Tick tick = new Tick();
tick.Value = val;
if (num % MinorTickStep == 0)
{
tick.TickType = TickType.Minor;
template = GetTickTemplate(TickType.Minor);
}
if (num % MajorTickStep == 0)
{
tick.TickType = TickType.Major;
template = GetTickTemplate(TickType.Major);
}
tick.ContentTemplate = template;
tick.Content = val;
ticks.Add(tick);
Children.Insert(0, tick);
val += MinorTickStep;
num += MinorTickStep;
}
}
You can see from the above method that, for a value that is a candidate for both a minor and a major tick, the major tick is chosen. There are also methods for removing the labels and ticks. These methods clear the private lists and also remove the ticks from the Scale
’s Children
collection. The definitions can be seen below:
private void ClearLabels()
{
for (int i = 0; i < labels.Count; i++)
{
Children.Remove(labels[i]);
}
labels.Clear();
}
private void ClearTicks()
{
for (int i = 0; i < ticks.Count; i++)
{
Children.Remove(ticks[i]);
}
ticks.Clear();
}
The last piece of code in this class that I haven’t talked about is the code that is used to arrange the labels, ticks, ranges, and indicators. Based on the scale type, all these elements will be arranged in a different way. Because of this, the specific arrange code will be implemented in the derived classes. Also, since this is a container, I should also override the ArrangeOverride
and MeasureOverride
methods. Since the measuring code depends on the scale type, I have overridden only the ArrangeOverride
method. The code for this method can be seen below:
protected override Size ArrangeOverride(Size finalSize)
{
ArrangeLabels(finalSize);
ArrangeTicks(finalSize);
ArrangeRanges(finalSize);
indicatorContainer.Arrange(new Rect(new Point(), finalSize));
ArrangeIndicators(finalSize);
return finalSize;
}
The three Arrange methods are abstract
and should be overridden in the derived scale classes. The declarations can be seen below:
protected abstract void ArrangeTicks(Size finalSize);
protected abstract void ArrangeLabels(Size finalSize);
protected abstract void ArrangeRanges(Size finalSize);
The linear scale
The properties specific to this type of scale can be seen in the image below:
These are dependency properties and are used to specify the orientation of the scale (horizontal or vertical) and the scale’s elements position (TopLeft
and BottomRight
).
The first methods I would like to talk about are the range creation and removal methods. The code for the range creation is presented below:
protected override void CreateRanges()
{
if (UseDefaultRange)
{
def.Fill = DefaultRangeBrush;
Children.Add(def);
}
if (Ranges == null)
return;
foreach (GaugeRange r in Ranges)
{
Rectangle rect = new Rectangle();
rect.Fill = new SolidColorBrush(r.Color);
ranges.Add(rect);
Children.Add(rect);
}
}
The first thing the method does is check if the UseDefaultRange
property is true
. If it is, it creates the default range, sets its brush, and adds it to the scale’s Children
collection. Then it iterates over the Ranges
collection, and for every range description, it creates a rectangle, sets the brush, and adds it to the scale’s Children
collection. Since this is the linear scale, the ranges are represented by using rectangles. The rectangles are also held in a private list. The method that removes the ranges is shown in the code below:
protected override void ClearRanges()
{
if (UseDefaultRange)
{
Children.Remove(def);
}
if (Ranges == null)
return;
for (int i = 0; i < ranges.Count; i++)
{
Children.Remove(ranges[i]);
}
ranges.Clear();
}
The method first removes the default range, removes the range rectangles from the scale’s Children
collection, and at the end, it clears the private list.
Another important method in this class is the MeasureOverride
method. This will be used to tell the framework how much space the linear scale will occupy. The space depends on the ticks, labels, ranges, and indicator sizes as well as on the scale’s orientation. The method can be seen in the listing below:
protected override Size MeasureOverride(Size availableSize)
{
foreach (Tick label in GetLabels())
label.Measure(availableSize);
foreach (Tick tick in GetTicks())
tick.Measure(availableSize);
foreach (Rectangle rect in ranges)
rect.Measure(availableSize);
if(Indicators!=null)
foreach (UIElement ind in Indicators)
ind.Measure(availableSize);
double width = 0;
double height = 0;
double lblMax=0, tickMax=0, indMax=0;
if (Orientation == Orientation.Horizontal)
{
lblMax = GetLabels().Max(p => p.DesiredSize.Height);
tickMax = GetTicks().Max(p => p.DesiredSize.Height);
if (Indicators != null && Indicators.Count > 0)
indMax = Indicators.Max(p => p.DesiredSize.Height);
height = 3 + lblMax + tickMax + indMax;
if(!double.IsInfinity(availableSize.Width))
width = availableSize.Width;
}
else
{
lblMax = GetLabels().Max(p => p.DesiredSize.Width);
tickMax = GetTicks().Max(p => p.DesiredSize.Width);
if (Indicators != null && Indicators.Count > 0)
indMax = Indicators.Max(p => p.DesiredSize.Width);
width = 3 + lblMax + tickMax + indMax;
if (!double.IsInfinity(availableSize.Height))
height = availableSize.Height;
}
return new Size(width, height);
}
The method first measures all of the panel’s children and then returns the desired size. As you can see, the size of the linear scale mostly depends on its orientation.
The last methods to talk about in the linear scale case are the three abstract methods that are used to arrange the tick's labels and ranges. The method used to arrange the labels can be seen in the listing below:
protected override void ArrangeLabels(Size finalSize)
{
var labels = GetLabels();
double maxLength = labels.Max(p => p.DesiredSize.Width)+1;
foreach (Tick tick in labels)
{
if (Orientation == Orientation.Horizontal)
{
double offset = GetSegmentOffset(finalSize.Width, tick.Value);
if (TickPlacement == LinearTickPlacement.TopLeft)
{
tick.Arrange(new Rect(new Point(offset-tick.DesiredSize.Width/2, 1),
tick.DesiredSize));
}
else
{
tick.Arrange(new Rect(new Point(offset - tick.DesiredSize.Width / 2,
finalSize.Height - tick.DesiredSize.Height - 1), tick.DesiredSize));
}
}
else
{
double offset = GetSegmentOffset(finalSize.Height, tick.Value);
if (TickPlacement == LinearTickPlacement.TopLeft)
{
tick.Arrange(new Rect(new Point(maxLength -
tick.DesiredSize.Width,finalSize.Height - offset -
tick.DesiredSize.Height / 2), tick.DesiredSize));
}
else
{
tick.Arrange(new Rect(new Point(finalSize.Width -
maxLength,finalSize.Height - offset -
tick.DesiredSize.Height / 2), tick.DesiredSize));
}
}
}
}
As you can see, the method first calculates the label offset and then it calls the Arrange
method on each label using the calculated offset and the label’s desired size. The offset is calculated by using the helper function below:
private double GetSegmentOffset(double length, double value)
{
double offset = length * (value - Minimum) / (Maximum - Minimum);
return offset;
}
The method used to arrange the ticks is similar. The only difference is the added offset to compensate for the label’s size. The code for this method can be seen below:
protected override void ArrangeTicks(Size finalSize)
{
var ticks = GetTicks();
foreach (Tick tick in ticks)
{
if (Orientation == Orientation.Horizontal)
{
double maxLength = ticks.Max(p => p.DesiredSize.Height);
double offset = GetSegmentOffset(finalSize.Width, tick.Value);
if (TickPlacement == LinearTickPlacement.TopLeft)
{
double yOff=GetLabels().Max(p=>p.DesiredSize.Height)+1;
tick.Arrange(new Rect(new Point(offset - tick.DesiredSize.Width / 2,
yOff + maxLength - tick.DesiredSize.Height), tick.DesiredSize));
}
else
{
double yOff = finalSize.Height - maxLength -
GetLabels().Max(p => p.DesiredSize.Height) - 2;
tick.Arrange(new Rect(new Point(offset -
tick.DesiredSize.Width / 2, yOff), tick.DesiredSize));
}
}
else
{
double maxLength = ticks.Max(p => p.DesiredSize.Width) +
GetLabels().Max(p => p.DesiredSize.Width) + 2;
double offset = GetSegmentOffset(finalSize.Height, tick.Value);
if (TickPlacement == LinearTickPlacement.TopLeft)
{
tick.Arrange(new Rect(new Point(maxLength - tick.DesiredSize.Width,
finalSize.Height- offset -
tick.DesiredSize.Height / 2), tick.DesiredSize));
}
else
{
tick.Arrange(new Rect(new Point(finalSize.Width - maxLength,
finalSize.Height- offset - tick.DesiredSize.Height / 2),
tick.DesiredSize));
}
}
}
}
The code used to arrange the ranges uses a similar technique to the previous two methods. This method first arranges the default range if it exists and then iterates over the rest of the ranges in order to arrange them as well. The interesting thing to notice about this function is that for the user defined ranges, the method calculates the optimal range dimension based on the previous range and does not simply start from 0. The code for this method can be seen below:
protected override void ArrangeRanges(Size finalSize)
{
if (UseDefaultRange)
{
if (Orientation == Orientation.Horizontal)
{
double yOff;
if (TickPlacement == LinearTickPlacement.TopLeft)
{
yOff = GetLabels().Max(p => p.DesiredSize.Height) +
GetTicks().Max(p => p.DesiredSize.Height) + 1;
def.Arrange(new Rect(new Point(0, yOff),
new Size(finalSize.Width, RangeThickness)));
}
else
{
yOff = GetLabels().Max(p => p.DesiredSize.Height) +
GetTicks().Max(p => p.DesiredSize.Height) + 3;
def.Arrange(new Rect(new Point(0, finalSize.Height - yOff -
RangeThickness), new Size(finalSize.Width, RangeThickness)));
}
}
else
{
double off = GetLabels().Max(p => p.DesiredSize.Width) +
GetTicks().Max(p => p.DesiredSize.Width) + 2;
if (TickPlacement == LinearTickPlacement.TopLeft)
{
def.Arrange(new Rect(new Point(off, 0),
new Size(RangeThickness, finalSize.Height)));
}
else
{
def.Arrange(new Rect(new Point(finalSize.Width - off - RangeThickness, 0),
new Size(RangeThickness, finalSize.Height)));
}
}
}
double posOffset = 0;
for (int i = 0; i < ranges.Count; i++)
{
Rectangle rect = ranges[i];
rect.Fill = new SolidColorBrush(Ranges[i].Color);
if (Orientation == Orientation.Horizontal)
{
double yOff;
if (TickPlacement == LinearTickPlacement.TopLeft)
{
yOff = GetLabels().Max(p => p.DesiredSize.Height) +
GetTicks().Max(p => p.DesiredSize.Height) + 1;
rect.Arrange(new Rect(new Point(posOffset, yOff),
new Size(GetSegmentOffset(finalSize.Width,
Ranges[i].Offset) - posOffset, RangeThickness)));
}
else
{
yOff = GetLabels().Max(p => p.DesiredSize.Height) +
GetTicks().Max(p => p.DesiredSize.Height) + 3;
rect.Arrange(new Rect(new Point(posOffset, finalSize.Height -
yOff - RangeThickness), new Size(GetSegmentOffset(finalSize.Width,
Ranges[i].Offset) - posOffset, RangeThickness)));
}
posOffset = GetSegmentOffset(finalSize.Width, Ranges[i].Offset);
}
else
{
double off = GetLabels().Max(p => p.DesiredSize.Width) +
GetTicks().Max(p => p.DesiredSize.Width) + 2;
double segLength = GetSegmentOffset(finalSize.Height, Ranges[i].Offset);
if (TickPlacement == LinearTickPlacement.TopLeft)
{
rect.Arrange(new Rect(new Point(off, finalSize.Height - segLength),
new Size(RangeThickness, segLength - posOffset)));
}
else
{
rect.Arrange(new Rect(new Point(finalSize.Width - off - RangeThickness,
finalSize.Height - segLength),
new Size(RangeThickness, segLength - posOffset)));
}
posOffset = segLength;
}
}
}
As you can see, before iterating over the range collection, the method initializes an offset variable. This variable will be used to calculate the correct start position of each range. This offset is updated after each iteration so that the next range starts where the previous one ended. After the horizontal and vertical offsets are calculated, the Arrange
method is called. The length of the current range will be calculated by subtracting the current offset from the offset of the current range. This effectively determines the optimal length and does not start every range from 0.
The images below present some screenshots with the LinearScale
. Each image presents different customizations.
The image above presents four LinearScale
instances. The first one uses the default settings. The second linear scale has a RangeThickness
of 5 and has templates for the labels and both tick types. The third scale has the TickPlacement
property set to LinearTickPlacement.BottomRight
, has two ranges, and has the RangeThickness
property set to 5. The last scale also has the TickPlacement
property set to LinearTickPlacement.BottomRight
. It also has templates for the minor and major ticks.
The image above presents the same four LinearScale
instances. The only difference is that this time the Orientation
property is set to Orientation.Vectical
.
The radial scale
The properties specific to radial scales can be seen in the image below:
These are all dependency properties. MinAngle
and MaxAngle
determine the start angle and the sweep of the scale. TickPlacement
determines whether the ticks should be drawn inward or outward. The RadialType
property refers to the circle subtypes we want for the scale. We can have a full circle scale, a semicircle scale, or a quadrant scale. This option can be mainly used to save space on the screen. For example, if the user chooses a 90 degree range, maybe he doesn’t want to waste the other three quadrants of the circle. To save space, he can specify that he wants a quadrant. This property will be used in conjunction with the min, max angle, and sweep direction properties. This property will have an effect on the position of the center of rotation inside the area of the scale and on the range. If the angles and sweep direction don’t match, the user can choose to change them to get what he wants. This is easier than implementing complicated logic to determine the quadrant in code and coercing the angle values.
The range creation and removal methods are similar to the ones implemented for the linear scale. The only difference is that instead of rectangles, the ranges for radial scales will be instances of the Path
class.
The MeasureOverride
method calls Measure
on all the container children and then it just returns the desired size. We will use all the available sizes and center the scale in that space. The definition can be seen below:
protected override Size MeasureOverride(Size availableSize)
{
double width = 0.0;
double height = 0.0;
if (!double.IsInfinity(availableSize.Width))
width = availableSize.Width;
if (!double.IsInfinity(availableSize.Height))
height = availableSize.Height;
Size size = new Size(width, height);
foreach (Tick label in GetLabels())
label.Measure(availableSize);
foreach (Tick tick in GetTicks())
tick.Measure(availableSize);
foreach (Path path in ranges)
path.Measure(availableSize);
if (Indicators != null)
foreach (UIElement ind in Indicators)
ind.Measure(availableSize);
return size;
}
The last methods to talk about are the three abstract methods used to arrange the labels, ticks, and ranges. These three methods make use of a helper class. I will first present these methods and then describe the helper class.
The code that arranges the labels in the radial scale can be seen below:
protected override void ArrangeLabels(Size finalSize)
{
double maxRad = RadialScaleHelper.GetRadius(RadialType, finalSize,
MinAngle, MaxAngle, SweepDirection);
Point center = RadialScaleHelper.GetCenterPosition(RadialType,
finalSize, MinAngle, MaxAngle, SweepDirection);
double x = center.X;
double y = center.Y;
double rad = maxRad;
if (TickPlacement == RadialTickPlacement.Inward)
{
rad = maxRad - RangeThickness - GetTicks().Max(p => p.DesiredSize.Height);
}
var labels = GetLabels();
for (int i = 0; i < labels.Count; i++)
{
PositionTick(labels[i], x, y, rad - labels[i].DesiredSize.Height / 2);
}
}
The code first retrieves the center of the scale and the maximum radius by using the helper class methods. After this, the actual radius is calculated based on the tick orientation. At the end, the code iterates over every label and positions it. This is done using the PositionTick
method. The definition of this method can be seen below:
private void PositionTick(Tick tick, double x, double y, double rad)
{
double tickW = tick.DesiredSize.Width;
double tickH = tick.DesiredSize.Height;
double angle = GetAngleFromValue(tick.Value);
if (SweepDirection == SweepDirection.Counterclockwise)
angle = -angle;
double px = x + (rad) * Math.Sin(angle * Math.PI / 180);
double py = y - (rad) * Math.Cos(angle * Math.PI / 180);
px -= tickW / 2;
py -= tickH / 2;
tick.Arrange(new Rect(new Point(px, py), tick.DesiredSize));
if ((EnableLabelRotation && tick.TickType==TickType.Label) ||
tick.TickType!=TickType.Label)
{
RotateTransform tr = new RotateTransform();
tr.Angle = angle;
tick.RenderTransformOrigin = new Point(0.5, 0.5);
tick.RenderTransform = tr;
}
}
The method calculates the tick (label or tick) position based on the center and radius by using the polar coordinate system formulas. The method then calls Arrange()
on the tick and then it sets the RenderTransform
in order to rotate the tick. You can see from the code above that the tick is only rotated if it is not a label or if it is a label and if the EnableLabelRotation
property is set to true.
The method that arranges the ticks can be seen in the listing below:
protected override void ArrangeTicks(Size finalSize)
{
double maxRad = RadialScaleHelper.GetRadius(RadialType, finalSize,
MinAngle, MaxAngle, SweepDirection);
Point center = RadialScaleHelper.GetCenterPosition(RadialType,
finalSize, MinAngle, MaxAngle, SweepDirection);
double x = center.X;
double y = center.Y;
var ticks = GetTicks();
var labels = GetLabels();
double rad = maxRad - labels.Max(p => p.DesiredSize.Height) -
ticks.Max(p => p.DesiredSize.Height) - 1;
if (TickPlacement == RadialTickPlacement.Inward)
{
rad = maxRad - RangeThickness;
}
for (int i = 0; i < ticks.Count; i++)
{
if (TickPlacement == RadialTickPlacement.Outward)
PositionTick(ticks[i], x, y, rad + ticks[i].DesiredSize.Height / 2);
else
PositionTick(ticks[i], x, y, rad - ticks[i].DesiredSize.Height / 2);
}
}
As you can see, the method is very similar and the tick placement method is called with a different radius based on the tick orientation property. The last method is the one that arranges the ranges. The first part of the ArrangeRanges()
method will be used to arrange the default range in case the UseDefaultRange
property is set to true
. This can be seen below:
double maxRad = RadialScaleHelper.GetRadius(RadialType, finalSize,
MinAngle, MaxAngle, SweepDirection);
Point center = RadialScaleHelper.GetCenterPosition(RadialType,
finalSize, MinAngle, MaxAngle, SweepDirection);
double x = center.X;
double y = center.Y;
double rad = maxRad;
if (TickPlacement == RadialTickPlacement.Outward)
{
rad = maxRad - GetLabels().Max(p => p.DesiredSize.Height) -
GetTicks().Max(p => p.DesiredSize.Height) - 1;
}
if (UseDefaultRange)
{
double min = MinAngle, max = MaxAngle;
if (SweepDirection == SweepDirection.Counterclockwise)
{
min = -min;
max = -max;
}
Geometry geom= RadialScaleHelper.CreateArcGeometry(min, max, rad,
RangeThickness, SweepDirection);
if (def.Data == null || def.Data.Bounds!=geom.Bounds)
def.Data = geom;
def.Arrange(new Rect(center, finalSize));
}
The code first gets the center and the maximum possible radius, then it sets the range shape by using the CreateArcGeometry()
method. In the end, the code calls Arrange()
on the default range by using the center point.
The code that arranges the rest of the ranges is similar. You can see this below:
double prevAngle = MinAngle;
if (SweepDirection == SweepDirection.Counterclockwise)
prevAngle = -prevAngle;
for (int i = 0; i < ranges.Count; i++)
{
Path range = ranges[i];
GaugeRange rng = Ranges[i];
double nextAngle = GetAngleFromValue(rng.Offset);
if (SweepDirection == SweepDirection.Counterclockwise)
nextAngle = -nextAngle;
range.Fill = new SolidColorBrush(rng.Color);
if (range.Data == null)
range.Data = RadialScaleHelper.CreateArcGeometry(prevAngle,
nextAngle, rad, RangeThickness, SweepDirection);
range.Arrange(new Rect(center, finalSize));
prevAngle = nextAngle;
}
Besides arranging the ranges, this code also sets the color of each range. In order to arrange these ranges, the code needs to get the end angles. The code starts at the minimum angle and then increments that after each range has been arranged.
The images below present some screenshots with the RadialScale
. Each image presents different customizations.
In the above image, the section on the left presents a radial scale with default settings. The section on the right has a single range set. The range thickness is 5, the range offset is 60.
In the above image, the section on the left presents a radial scale with the TickPlacement
property set to LinearTickPlacement.Inward
, the RangeThickness
property is 5, and the scale has two ranges. The section on the right shows a radial scale in which the label, minor tick, and major tick templates were changed.
The above image presents two gauges that have the SweepDirection
property set to SweepDirection.Counterclockwise
. In the section on the left, the MinAngle
is 0 and the MaxAngle
is 270. In the section on the right, the MinAngle
is 90, the MaxAngle
is 360, the UseDefaultRange
is false
, and we have a single range defined.
In this last image, we have two radial scales with more tick and label customizations.
The RadialScaleHelper class
This is a helper class that is used by the RadialScale
type to calculate the radial scale center position, the radial scale radius, and the range geometries. The class has three public methods. The first method returns the logical center of the scale. This is the point that will be used as the center of rotation. The second method returns the desired radius of the scale. This radius will depend on the minimum and maximum angles and also on the RadialType
setting. The last method will be used to obtain the range shapes.
The radial gauge can be a circle, a semi circle, or a quarter of a circle. In each situation, the center of rotation will differ. The GetCenterPosition()
method will be used to determine this center based on the radial type, the final size of the scale, the min and max angles, and the sweep direction. The center position cases can be seen better in the image below:
The definition of this method can be seen below:
public static Point GetCenterPosition(RadialType type, Size finalSize, double minAngle,
double maxAngle, SweepDirection sweepDir)
{
int q1 = GetQuadrant(minAngle);
int q2 = GetQuadrant(maxAngle);
if (sweepDir == SweepDirection.Counterclockwise)
{
q1++; q2++;
if (q1 > 4) q1 = 1;
if (q2 > 4) q2 = 1;
}
else
{
if (q1 % 2 == 0) q1 = 6 - q1;
if (q2 % 2 == 0) q2 = 6 - q2;
}
int diff = q2 - q1;
if (Math.Abs(diff) == 0)
{
if (type == RadialType.Quadrant)
{
return GetCenterForQuadrant(q2, finalSize);
}
else if (type == RadialType.Semicircle)
{
if (q1 == 1 || q1 == 2)
return new Point(finalSize.Width / 2, finalSize.Height);
else
return new Point(finalSize.Width / 2, 0);
}
else
{
return new Point(finalSize.Width / 2, finalSize.Height / 2);
}
}
else if (Math.Abs(diff) == 1 || (Math.Abs(diff)==3 && (maxAngle-minAngle)<=180))
{
if (type == RadialType.Quadrant || type == RadialType.Semicircle)
{
return GetCenterForSemicircle(q1, q2, finalSize);
}
else
{
return new Point(finalSize.Width / 2, finalSize.Height / 2);
}
}
else
{
return new Point(finalSize.Width / 2, finalSize.Height / 2);
}
}
Since the geometric quadrants don’t match up with the ones on the screen, the method first converts the screen quadrants to the geometric ones. After this, based on the difference, a different point is returned. The same checks are done to get the maximum possible radius in the GetRadius
method.
The last method is the method that builds the range geometries. Every range will have four segments. These can be seen in the image below:
The code for the method can be seen below:
public static Geometry CreateArcGeometry(double minAngle, double maxAngle,
double radius, int thickness, SweepDirection sweepDirection)
{
PathFigure figure = new PathFigure();
figure.IsClosed = true;
figure.StartPoint = new Point((radius - thickness) *
Math.Sin(minAngle * Math.PI / 180),
-(radius - thickness) * Math.Cos(minAngle * Math.PI / 180));
ArcSegment arc = new ArcSegment();
arc.Point = new Point((radius - thickness) * Math.Sin(maxAngle * Math.PI / 180),
-(radius - thickness) * Math.Cos(maxAngle * Math.PI / 180));
arc.Size = new Size(radius - thickness, radius - thickness);
arc.SweepDirection = sweepDirection;
if (Math.Abs(maxAngle - minAngle) > 180) arc.IsLargeArc = true;
figure.Segments.Add(arc);
LineSegment line = new LineSegment();
line.Point = new Point(radius * Math.Sin(maxAngle * Math.PI / 180),
-radius * Math.Cos(maxAngle * Math.PI / 180));
figure.Segments.Add(line);
arc = new ArcSegment();
arc.Point = new Point(radius * Math.Sin(minAngle * Math.PI / 180),
-radius * Math.Cos(minAngle * Math.PI / 180));
arc.Size = new Size(radius, radius);
arc.SweepDirection = SweepDirection.Counterclockwise;
if (sweepDirection == SweepDirection.Counterclockwise)
arc.SweepDirection = SweepDirection.Clockwise;
if (Math.Abs(maxAngle - minAngle) > 180) arc.IsLargeArc = true;
figure.Segments.Add(arc);
PathGeometry path = new PathGeometry();
path.Figures.Add(figure);
return path;
}
Final thoughts
That is all there is to scales implementation. I hope this second article sheds some more light on the implementation of the scales in this control library. Please check out the last article in this series to see how the indicators are implemented.
If you liked the article and found the code useful, please take a minute to post a comment and vote for the first article in the series.
History
- Created on March 19, 2011.
- Updated on March 21, 2011.
- Updated source code on March 23, 2011.
- Updated source code on March 30, 2011.
- Updated source code and sample on April 05, 2011.
- Updated source code on April 10, 2011.
- Updated source code on February 24, 2013.