Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / WPF

WPF: A Simple Yet Flexible Rating Control

Rate me:
Please Sign up or sign in to vote.
4.96/5 (46 votes)
22 Jul 2014CPOL4 min read 104.6K   3.2K   74   29
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:

XML
<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:

C#
/// <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.

C#
/// <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:

XML
<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.

C#
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:

C#
/// <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)


Written By
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 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • 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

 
PraiseGood work, still simple to use in .Net 5 Pin
Mr Bean 201324-Mar-21 4:53
Mr Bean 201324-Mar-21 4:53 
QuestionEasy way to add "Star Background" Pin
ChristianKern972-Aug-20 3:26
ChristianKern972-Aug-20 3:26 
PraiseAdded some options Pin
Toxaris14-Jul-16 1:06
Toxaris14-Jul-16 1:06 
GeneralRe: Added some options Pin
Sacha Barber14-Jul-16 3:28
Sacha Barber14-Jul-16 3:28 
Questionits is not accepting decimal values Pin
Sandeep_sni14-Dec-15 3:58
Sandeep_sni14-Dec-15 3:58 
GeneralMy vote of 5 Pin
Whisortates26-Oct-15 0:00
Whisortates26-Oct-15 0:00 
QuestionIn touch Screen application. Pin
Dnyati11-Apr-15 1:48
Dnyati11-Apr-15 1:48 
QuestionVery Good! Pin
andreowik26-Mar-15 3:19
andreowik26-Mar-15 3:19 
QuestionThankyou + small fixes Pin
avramik22-Jul-14 11:59
avramik22-Jul-14 11:59 
AnswerRe: Thankyou + small fixes Pin
Sacha Barber22-Jul-14 23:37
Sacha Barber22-Jul-14 23:37 
BugDoesn't display correct value in a listview gridview column Pin
Berney14-Sep-13 22:37
Berney14-Sep-13 22:37 
GeneralRe: Doesn't display correct value in a listview gridview column Pin
Berney14-Sep-13 23:07
Berney14-Sep-13 23:07 
GeneralRe: Doesn't display correct value in a listview gridview column Pin
Sacha Barber15-Sep-13 2:17
Sacha Barber15-Sep-13 2:17 
GeneralMy vote of 3 Pin
Vipin J S_02-Nov-12 1:21
Vipin J S_02-Nov-12 1:21 
GeneralRe: My vote of 3 Pin
8an058-Mar-15 2:08
8an058-Mar-15 2:08 
Change Minimum variables to decimal and set default Minimum value to 0.5
QuestionCool Control - One Question: Can the user click on it to give a new rating? Pin
mahop5-Oct-12 10:08
mahop5-Oct-12 10:08 
GeneralMy vote of 5 Pin
Wendelius6-Sep-12 6:49
mentorWendelius6-Sep-12 6:49 
GeneralThank You! Pin
27djpip9-Jun-10 5:06
27djpip9-Jun-10 5:06 
GeneralRe: Thank You! Pin
Sacha Barber9-Jun-10 6:01
Sacha Barber9-Jun-10 6:01 
GeneralJust ran into this Pin
User 27100918-Apr-10 18:25
User 27100918-Apr-10 18:25 
GeneralRe: Just ran into this Pin
Sacha Barber18-Apr-10 22:53
Sacha Barber18-Apr-10 22:53 
GeneralCool.. Pin
Rajesh Pillai29-Nov-09 0:49
Rajesh Pillai29-Nov-09 0:49 
GeneralRe: Cool.. Pin
Sacha Barber29-Nov-09 5:37
Sacha Barber29-Nov-09 5:37 
Generalexcellent Pin
Arash Partow27-Nov-09 16:18
Arash Partow27-Nov-09 16:18 
GeneralRe: excellent Pin
Sacha Barber27-Nov-09 21:10
Sacha Barber27-Nov-09 21:10 

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

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