Monday, August 23, 2010

Redial – A Dial Custom Control

In the last post I described a Dial User Control and promised I would turn it into a Custom Control. Re-doing it as a viewless custom control means it can be visually redesigned without having to write any behavior code.

Here is the control with a couple of different styles, with some of the dials bound to an items source. I also added an animation for the dial positions being added and removed. You can see this by changing the value of the big dial in the bottom left corner; its value sets the MaxValue of the top left dial.

You can grab the source here.

There are a couple of features in this control that weren’t in the User Control:

  • The ItemsSource property can be bound to a data source for the dial position items. If you use this then don’t use MaxValue since that is automatically determined from the data source.
  • If you don’t use an items source, the positions are auto-generated using the StartValue and MaxValue properties.
  • I added very basic support for the mouse wheel, but it needs improvement.
  • I changed the behavior of the RotationAngle property to take into account any initial rotation of the list box path, and also the span of the path.
  • You can click and drag around the dial positions to set the value

A Simple Style


The template to the left shows the entire template for the black speaker-style dials in the top right of the sample application above. This is a good starting point for creating styles for the dial since it is very simple.

There are three key elements to note:

  1. The “itemPath” element is the ellipse that defines where the dial positions end up.
  2. The “DialList” element is the path list box that uses the itemPath. The Dial control won’t work without this. Any style for the Dial control must include a PathListBox control called “DialList”.
  3. The “Knob” grid contains the visual elements that are rotated – more about this below.

The remaining elements are not doing anything interesting other than adding to the visual appearance of the knob.

In order to get the knob to rotate, I have bound it to the RotationAngle property:

   1: <Grid x:Name="knob" Margin="10" RenderTransformOrigin="0.5,0.5">
   2:     <Grid.RenderTransform>
   3:         <CompositeTransform Rotation="{Binding RotationAngle, RelativeSource={RelativeSource TemplatedParent}}"/>
   4:     </Grid.RenderTransform>
   6:     ...
   8: </Grid>

The RotationAngle property calculates the correct position by looking at the DialList PathListBox, determining the rotation of it, and the span. You may have noticed that I used the “RelativeSource” syntax inside a regular Binding instead of using the TemplateBinding syntax. I did this out of habit, since elsewhere I use property converters on bindings in the other templates, and the TemplateBinding class does not support converters.

Here is an example that uses a converter:

   1: <LinearGradientBrush EndPoint="0.5,1" MappingMode="RelativeToBoundingBox" StartPoint="0.5,0">
   2:     <LinearGradientBrush.RelativeTransform>
   3:         <CompositeTransform CenterY="0.5" CenterX="0.5" 
   4:             Rotation="{Binding RotationAngle, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource negativeConverter}}"/>
   5:     </LinearGradientBrush.RelativeTransform>
   6:     <GradientStop Color="Black" Offset="1"/>
   7:     <GradientStop Color="#FF3B3B3B"/>
   8: </LinearGradientBrush>

Feel free to use this control in your own projects.