Click here to Skip to main content
Email Password   helpLost your password?

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:

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:

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

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralCool..
Rajesh Pillai
1:49 29 Nov '09  
Thanks!!!

Enjoy Life,
Rajesh Pillai
http://geekswithblogs.net/rajeshpillai/Default.aspx

GeneralRe: Cool..
Sacha Barber
6:37 29 Nov '09  
no problem

Sacha Barber
  • Microsoft Visual C# MVP 2008/2009
  • Codeproject MVP 2008/2009
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

My Blog : sachabarber.net

Generalexcellent
Arash Partow
17:18 27 Nov '09  
Though i have one quick suggestion, instead of simply blanking out vertically a percentage, why not assume each tip section to represent 20% of a value and simple not fill as many tips as required, eg: for a 75% of a star fill-in 4 tips etc.... Big Grin
GeneralRe: excellent
Sacha Barber
22:10 27 Nov '09  
Would look a little weird I think...Maybe. Smile

Sacha Barber
  • Microsoft Visual C# MVP 2008/2009
  • Codeproject MVP 2008/2009
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

My Blog : sachabarber.net

GeneralCoolio!
Daniel Vaughan
4:40 27 Nov '09  
Very nice Sacha.
I wonder if it's the scale combined with antialiasing that makes the tops of the stars appear slightly cut off. I guess placing them in a view box, and making them a bit bigger would fix that.

I am sure this will come in handy sometime. Nice one!
Have a 5.

Cheers,
Daniel

Daniel Vaughan
Follow me on Twitter
Blog: DanielVaughan.Orpius.com
Open Source Projects: Calcium SDK, Clog
Organization: Outcoder of PebbleAge

GeneralRe: Coolio!
Sacha Barber
5:46 27 Nov '09  
Yeah not sure really.

Sacha Barber
  • Microsoft Visual C# MVP 2008/2009
  • Codeproject MVP 2008/2009
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

My Blog : sachabarber.net

GeneralOh very nice.
Pete O'Hanlon
4:08 27 Nov '09  
It's these little throwaway snippets you toss out there that simply stagger me mate. +5

"WPF has many lovers. It's a veritable porn star!" - Josh Smith

As Braveheart once said, "You can take our freedom but you'll never take our Hobnobs!" - Martin Hughes.


My blog | My articles | MoXAML PowerToys | Onyx



GeneralRe: Oh very nice.
Sacha Barber
4:12 27 Nov '09  
They are throw away but they is fun. Thanks Pete

Sacha Barber
  • Microsoft Visual C# MVP 2008/2009
  • Codeproject MVP 2008/2009
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

My Blog : sachabarber.net


Last Updated 27 Nov 2009 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010