Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF: A Simple Yet Flexible Rating Control

0.00/5 (No votes)
23 Jul 2014 2  
Nice little rating control for WPF

Introduction

I was at work the other day and one of my work colleagues asked me how to create a Rating control (you know the ones with the stars). I talked him through how to do it, but whilst doing so I thought I might have a go at that if I get a spare hour or two. I found some time to give it a go, and have come up with what I think is a pretty flexible RatingControl for WPF.

You are able to alter the following attributes:

  • Overall background color
  • Star foreground color
  • Star outline color 
  • Number of stars
  • Current value

All of these properties are DependencyProperty values, so they are fully bindable. This is all wrapped up in a very simple and easy to use UserControl called RatingsControl, Here is what the resulting RatingsControl looks like in a demo window:

What I like about my implementation compared to the more typical implementations you see out there is that mine deals with fractions of Stars. What I mean is that it is possible to provide a value such as 7.5 and the 0.5 will actually only fill 1/2 a star.

Here is all you have to do to use it in XAML:

<local:RatingsControl x:Name="ratings0" 
                  Value="2.6"
                  NumberOfStars="4"
                  BackgroundColor="White"
                  StarForegroundColor="Blue"
                  StarOutlineColor="Black"
                  Margin="5" 
                  HorizontalAlignment="Left"/>

See that is pretty easy, isn't it.

How It Works

So I'll just talk you through how it works.

There are actually 2 controls that make this happen.

RatingsControl

There is a top level RatingsControl that you set values on. Based on those values, the RatingsControl works out how many stars have been requested (this is dictated by the NumberOfStars DP). There is also DP value coercing to ensure that the NumberOfStars DP value does not exceed the RatingsControl.Minimum and RatingsControl.Maximum DP property values, which I have set to 0 and 10 respectively.

The code to do this is as follows:

/// <summary>
/// NumberOfStars Dependency Property
/// </summary>
public static readonly DependencyProperty NumberOfStarsProperty =
    DependencyProperty.Register("NumberOfStars", typeof(Int32), typeof(RatingsControl),
        new FrameworkPropertyMetadata((Int32)5,
            new PropertyChangedCallback(OnNumberOfStarsChanged),
            new CoerceValueCallback(CoerceNumberOfStarsValue)));

/// <summary>
/// Gets or sets the NumberOfStars property.  
/// </summary>
public Int32 NumberOfStars
{
    get { return (Int32)GetValue(NumberOfStarsProperty); }
    set { SetValue(NumberOfStarsProperty, value); }
}

/// <summary>
/// Handles changes to the NumberOfStars property.
/// </summary>
private static void OnNumberOfStarsChanged(DependencyObject d, 
    DependencyPropertyChangedEventArgs e)
{
    d.CoerceValue(MinimumProperty);
    d.CoerceValue(MaximumProperty);
    RatingsControl ratingsControl = (RatingsControl)d;
    SetupStars(ratingsControl);
}

/// <summary>
/// Coerces the NumberOfStars value.
/// </summary>
private static object CoerceNumberOfStarsValue(DependencyObject d, object value)
{
    RatingsControl ratingsControl = (RatingsControl)d;
    Int32 current = (Int32)value;
    if (current < ratingsControl.Minimum) current = ratingsControl.Minimum;
    if (current > ratingsControl.Maximum) current = ratingsControl.Maximum;
    return current;
}

What happens is that when either the RatingsControl.Value or the RatingsControl.NumberOfStars DP values change, the following logic is run which creates the correct number of StarControl, and sets their actual Value based on a share of the overall RatingsControl.Value.

/// <summary>
/// Sets up stars when Value or NumberOfStars properties change
/// Will only show up to the number of stars requested (up to Maximum)
/// so if Value > NumberOfStars * 1, then Value is clipped to maximum
/// number of full stars
/// </summary>
/// <param name="ratingsControl"></param>
private static void SetupStars(RatingsControl ratingsControl)
{
    Decimal localValue = ratingsControl.Value;

    ratingsControl.spStars.Children.Clear();
    for (int i = 0; i < ratingsControl.NumberOfStars; i++)
    {
        StarControl star = new StarControl();
        star.BackgroundColor = ratingsControl.BackgroundColor;
        star.StarForegroundColor = ratingsControl.StarForegroundColor;
        star.StarOutlineColor = ratingsControl.StarOutlineColor;
        if (localValue > 1)
            star.Value = 1.0m;
        else if (localValue > 0)
        {
            star.Value = localValue;
        }
        else
        {
            star.Value = 0.0m;
        }

        localValue -= 1.0m;
        ratingsControl.spStars.Children.Insert(i,star);
    }
}

As can be seen from this code, this is where each StarControl gets created and is assigned a Value. For example, if the overall RatingsControl.Value was 7.5 and the RatingsControl.NumberOfStars was 8, we would loop through creating 8 StarControls, where the first 7 StarControls would get their Value DP to 1.0 all except the last one which would get its Value DP to 0.5.

A lot of the other DPs previously mentioned such as BackgroundColor/StarForegroundColor/StarOutlineColor DPs are simply used to set the corresponding DP values on any created StarControl which can also be seen above.

So how do the StarControls work and how to they render partial stars?

StarControl

A StarControl represents a single star within the overall RatingsControl, and each StarControl has the following DPs:

  • BackgroundColor (set via RatingsControl)
  • StarForegroundColor (set via RatingsControl)
  • StarOutlineColor (set via RatingsControl)
  • Value (set via RatingsControl, but is coerced between the StarControl.Minimum and StarControl.Maximum DP values, which are set at 0.0 and 1.0 respectively)
  • Minimum used to coerce StarControl.Value if it is out of acceptable range
  • Maximum used to coerce StarControl.Value if it is out of acceptable range

The XAML that described what a StarControl looks like is as follows:

<UserControl x:Class="StarRatingsControl.StarControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="Auto" Width="Auto">
    <Grid x:Name="gdStar">

        <Path Name="starForeground" Fill="Gray" Stroke="Transparent" StrokeThickness="1"
              Data="M 5,0 L 4,4 L 0,4 L 3,7 L 2,11 L 5,9 L 6,
			9 L 9,11 L 8,7 L 11,4 L 7,4 L 6,0"/>

        <Rectangle x:Name="mask" Margin="0"/>

        <Path Name="starOutline" Fill="Transparent" 
		Stroke="Transparent" StrokeThickness="1"
              Data="M 5,0 L 4,4 L 0,4 L 3,7 L 2,11 L 5,9 L 6,
			9 L 9,11 L 8,7 L 11,4 L 7,4 L 6,0"/>

    </Grid>
</UserControl>

So again getting back to how the StarControl is able to render fractions of stars, well there are 2 tricks in use here:

Trick 1: Clipping

In the constructor of the StarControl, there is a Clip geometry set up which prevents the StarControl from rendering anything that would appear outside of this clipped geometry.

public StarControl()
{
    this.DataContext = this;
    InitializeComponent();

    gdStar.Width = STAR_SIZE;
    gdStar.Height = STAR_SIZE;
    gdStar.Clip = new RectangleGeometry
    {
        Rect = new Rect(0, 0, STAR_SIZE, STAR_SIZE)
    };

    mask.Width = STAR_SIZE;
    mask.Height = STAR_SIZE;
}

Trick 2: Moving Mask

There is actually a Rectangle (I call this Mask) that is between the star background Path and the star outline path, this Rectangle (Mask) is the same Color as the background, and has its Margin adjusted to be moved the correct place to give the illusion of a partially filled star. And then the outline is drawn on top of the moved Rectangle.

This figure explains this a bit better:

And here is the code that deals with this in the StarControl:

/// <summary>
/// Handles changes to the Value property.
/// </summary>
private static void OnValueChanged(DependencyObject d, 
    DependencyPropertyChangedEventArgs e)
{
    d.CoerceValue(MinimumProperty);
    d.CoerceValue(MaximumProperty);
    StarControl starControl = (StarControl)d;
    if (starControl.Value == 0.0m)
    {
        starControl.starForeground.Fill = Brushes.Gray;
    }
    else
    {
        starControl.starForeground.Fill = starControl.StarForegroundColor;
    }

    Int32 marginLeftOffset = (Int32)(starControl.Value * (Decimal)STAR_SIZE);
    starControl.mask.Margin = new Thickness(marginLeftOffset, 0, 0, 0);
    starControl.InvalidateArrange();
    starControl.InvalidateMeasure();
    starControl.InvalidateVisual();

}

That's It... Hope You Liked It

Anyway there you go, hope you liked it. I know this is a very small article, but I am hoping it may be useful to someone.

Thanks

As always votes / comments are welcome.

History

  • 27th November, 2009: Initial post

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here