Click here to Skip to main content
Click here to Skip to main content

WPF: A Simple Yet Flexible Rating Control

, 23 Jul 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Sacha Barber
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)
 
- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence
 
Both of these at Sussex University UK.
 
Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
QuestionThankyou + small fixes Pinmemberavramik22-Jul-14 12:59 
AnswerRe: Thankyou + small fixes PinmvpSacha Barber23-Jul-14 0:37 
BugDoesn't display correct value in a listview gridview column PinmemberBerney14-Sep-13 23:37 
GeneralRe: Doesn't display correct value in a listview gridview column PinmemberBerney15-Sep-13 0:07 
GeneralRe: Doesn't display correct value in a listview gridview column PinmvpSacha Barber15-Sep-13 3:17 
GeneralMy vote of 3 PinmemberVipin ISPG2-Nov-12 2:21 
QuestionCool Control - One Question: Can the user click on it to give a new rating? Pinmembermahop5-Oct-12 11:08 
GeneralMy vote of 5 PinmvpMika Wendelius6-Sep-12 7:49 
GeneralThank You! Pinmember27djpip9-Jun-10 6:06 
GeneralRe: Thank You! PinmvpSacha Barber9-Jun-10 7:01 
GeneralJust ran into this PinmemberKarl Shifflett18-Apr-10 19:25 
GeneralRe: Just ran into this PinmvpSacha Barber18-Apr-10 23:53 
GeneralCool.. PinmemberRajesh Pillai29-Nov-09 1:49 
GeneralRe: Cool.. PinmvpSacha Barber29-Nov-09 6:37 
Generalexcellent PinmemberArash Partow27-Nov-09 17:18 
GeneralRe: excellent PinmvpSacha Barber27-Nov-09 22:10 
GeneralCoolio! PinmemberDaniel Vaughan27-Nov-09 4:40 
GeneralRe: Coolio! PinmvpSacha Barber27-Nov-09 5:46 
GeneralOh very nice. PinmvpPete O'Hanlon27-Nov-09 4:08 
GeneralRe: Oh very nice. PinmvpSacha Barber27-Nov-09 4:12 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.1411022.1 | Last Updated 23 Jul 2014
Article Copyright 2009 by Sacha Barber
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid