lundi 29 septembre 2014

Pure xaml star rating control

One of the best feature in wpf/xaml is ability to completely redesign a built-in control without any line of code. Most of the time you will just define a custom theme for a selection of useful controls. But it becomes more interesting when "hacking" an existing control to make something new.

In my day work I had to make a star rating control and I thought it would be interesting to reuse the slider control for that purpose. I also wanted it to be pure xaml. I use a shape proposed by Kiran Kumar which I slightly modified.
Here is a visual (nothing really surprising though):
If you want to try, simply copy/paste the following piece of xaml to your favorite xaml editor (I used Kaxaml, which is a great tool):

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
<Page
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Background="#2d2d20">
  <Page.Resources>
    <SolidColorBrush x:Key="NoStar" Color="#29ffffff" />
    <SolidColorBrush x:Key="Star" Color="Yellow" />

    <Style
      x:Key="StarRatingShapeStyle"
      TargetType="Polygon">
      <Setter Property="Width" Value="12" />
      <Setter Property="Height" Value="12" />
      <Setter Property="Fill" Value="{StaticResource NoStar}" />
      <Setter Property="Points" Value="9,2 11,7 17,7 12,10 14,15 9,12 4,15 6,10 1,7 7,7" />
      <Setter Property="Stretch" Value="Fill" />
      <Setter Property="Stroke" Value="White" />
      <Setter Property="StrokeLineJoin" Value="Round" />
      <Setter Property="StrokeThickness" Value=".5" />
      <Setter Property="IsHitTestVisible" Value="False" />
    </Style>

    <Style x:Key="RatingSliderRepeatButtonStyle" TargetType="{x:Type RepeatButton}">
      <Setter Property="Padding" Value="0" />
      <Setter Property="Margin" Value="0" />
      <Setter Property="Background" Value="Transparent" />
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate>
            <Border
              Background="{TemplateBinding Background}"
              BorderThickness="0"
              />
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>

    <Style x:Key="RatingSliderThumbStyle" TargetType="{x:Type Thumb}">
      <Setter Property="OverridesDefaultStyle" Value="true" />
      <Setter Property="Focusable" Value="False" />
      <Setter Property="Padding" Value="0" />
      <Setter Property="Margin" Value="0" />
      <Setter Property="Background" Value="Transparent" />
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="{x:Type Thumb}">
            <Border
              Background="{TemplateBinding Background}"
              BorderThickness="0"
              />
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>

    <Style
      x:Key="StarRatingSliderStyle"
      TargetType="Slider">
      <Setter Property="Minimum" Value="0" />
      <Setter Property="Maximum" Value="5" />
      <Setter Property="SmallChange" Value="1" />
      <Setter Property="LargeChange" Value="1" />
      <Setter Property="TickFrequency" Value="1" />
      <Setter Property="IsSnapToTickEnabled" Value="True" />
      <Setter Property="IsMoveToPointEnabled" Value="True" />
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate
            TargetType="Slider">
            <Grid>
              <Track
                x:Name="PART_Track"
                >
                <Track.DecreaseRepeatButton>
                  <RepeatButton
                    Style="{StaticResource RatingSliderRepeatButtonStyle}"
                    Command="{x:Static Slider.DecreaseLarge}"
                    />
                </Track.DecreaseRepeatButton>
                <Track.IncreaseRepeatButton>
                  <RepeatButton
                    Style="{StaticResource RatingSliderRepeatButtonStyle}"
                    Command="{x:Static Slider.IncreaseLarge}"
                    />
                </Track.IncreaseRepeatButton>
                <Track.Thumb>
                  <Thumb
                    x:Name="PART_Thumb"
                    Style="{StaticResource RatingSliderThumbStyle}"
                    />
                </Track.Thumb>
              </Track>

              <StackPanel
                Orientation="{TemplateBinding Orientation}">
                <Polygon
                  x:Name="S5"
                  Style="{StaticResource StarRatingShapeStyle}"
                  />
                <Polygon
                  x:Name="S4"
                  Style="{StaticResource StarRatingShapeStyle}"
                  />
                <Polygon
                  x:Name="S3"
                  Style="{StaticResource StarRatingShapeStyle}"
                  />
                <Polygon
                  x:Name="S2"
                  Style="{StaticResource StarRatingShapeStyle}"
                  />
                  <Polygon
                  x:Name="S1"
                  Style="{StaticResource StarRatingShapeStyle}"
                  />
              </StackPanel>
            </Grid>

            <ControlTemplate.Triggers>
              <Trigger Property="Value" Value="1">
                <Setter TargetName="S1" Property="Fill" Value="{StaticResource Star}"/>
                <Setter TargetName="S2" Property="Fill" Value="{StaticResource NoStar}"/>
                <Setter TargetName="S3" Property="Fill" Value="{StaticResource NoStar}"/>
                <Setter TargetName="S4" Property="Fill" Value="{StaticResource NoStar}"/>
                <Setter TargetName="S5" Property="Fill" Value="{StaticResource NoStar}"/>
              </Trigger>
              <Trigger Property="Value" Value="2">
                <Setter TargetName="S1" Property="Fill" Value="{StaticResource Star}"/>
                <Setter TargetName="S2" Property="Fill" Value="{StaticResource Star}"/>
                <Setter TargetName="S3" Property="Fill" Value="{StaticResource NoStar}"/>
                <Setter TargetName="S4" Property="Fill" Value="{StaticResource NoStar}"/>
                <Setter TargetName="S5" Property="Fill" Value="{StaticResource NoStar}"/>
              </Trigger>
              <Trigger Property="Value" Value="3">
                <Setter TargetName="S1" Property="Fill" Value="{StaticResource Star}"/>
                <Setter TargetName="S2" Property="Fill" Value="{StaticResource Star}"/>
                <Setter TargetName="S3" Property="Fill" Value="{StaticResource Star}"/>
                <Setter TargetName="S4" Property="Fill" Value="{StaticResource NoStar}"/>
                <Setter TargetName="S5" Property="Fill" Value="{StaticResource NoStar}"/>
              </Trigger>
              <Trigger Property="Value" Value="4">
                <Setter TargetName="S1" Property="Fill" Value="{StaticResource Star}"/>
                <Setter TargetName="S2" Property="Fill" Value="{StaticResource Star}"/>
                <Setter TargetName="S3" Property="Fill" Value="{StaticResource Star}"/>
                <Setter TargetName="S4" Property="Fill" Value="{StaticResource Star}"/>
                <Setter TargetName="S5" Property="Fill" Value="{StaticResource NoStar}"/>
              </Trigger>
              <Trigger Property="Value" Value="5">
                <Setter TargetName="S1" Property="Fill" Value="{StaticResource Star}"/>
                <Setter TargetName="S2" Property="Fill" Value="{StaticResource Star}"/>
                <Setter TargetName="S3" Property="Fill" Value="{StaticResource Star}"/>
                <Setter TargetName="S4" Property="Fill" Value="{StaticResource Star}"/>
                <Setter TargetName="S5" Property="Fill" Value="{StaticResource Star}"/>
              </Trigger>
            </ControlTemplate.Triggers>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
  </Page.Resources>
  <StackPanel>
    <Slider
      x:Name="Ranking"
      Orientation="Vertical"
      HorizontalAlignment="Center"
      Background="Transparent"
      Style="{StaticResource StarRatingSliderStyle}"
      />
    <Slider
      x:Name="Test"
      Orientation="Horizontal"
      Minimum="0"
      Maximum="5"
      Width="200"
      Height="200"
      IsSnapToTickEnabled="True"
      TickFrequency="1"
      Value="{Binding Value, ElementName=Ranking}"
      />
  </StackPanel>
</Page>

This xaml implementation is not completely usable because of two small issues:

  • Depending on where you click on a star (above or below the middle) you got different behaviors. But it could be easily solved with a converter that would ceil or floor the value.
  • Horizontal and vertical orientation is handled, but in horizontal orientation the order is reversed. I guess I should not use the Stackpanel orientation but rather a LayoutTransform.

Aucun commentaire:

Enregistrer un commentaire