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

Star Rating Control With No Messy GDI Bits!

0.00/5 (No votes)
2 Feb 2011 1  
A non GDI version of a star rating control

Introduction

This article is about a simple, GDI free, star rating control.

Background

I was searching the web for a star rating control and the examples I found were very procedural in nature and had 're-invent the wheelitis'. Rather than reusing the current controls, they often took the do it yourself approach of creating the user drawn controls. I thought I would put my money where my mouth is and take a stab at producing what I think is a simpler, more object oriented and GDI free control.

Goals

  1. Use custom controls based on existing controls rather than user drawn controls.
    • Use a control derived from the Panel control to hold the stars.
    • Use a control derived from the PictureBox control to render individual stars.
  2. Take a more object oriented approach.
    • Encapsulate the star behavior in the Star class.
    • Encapsulate the rating behavior in the StarRatingControl class.
  3. Allow for N stars rather than the usual 5.
  4. Have a way to leave no stars selected. Most controls seem to start with no stars selected but once you select one, there's no way to select none again.
  5. Modify the star color based on how many stars are selected. I need this feature for a work project.

Using the Code

To use this code in your projects, simply add a control to your toolbox by right clicking on the toolbox and selecting 'Add/Remove Items...', browse to and select the StarRating.dll. Now drag and drop the control to your Win Form. That's it.

Change the number of stars through the StarCount property, the default is 5 stars. Change the images used through the OnImages property. Default images are provided so you only need to use this property if you want to use your own images.

To see the current value of the control, use the CurrentStarValue property.

There are two classes involved. The Star class which as the names suggests, is the class that encapsulates the behavior of a single Star, and the StarRatingControl class that holds a collection of Star objects.

Let's start with the Star class.
Note: The Star class shown below is incomplete. For clarity, I removed the code added by Visual Studio:

public class Star : PictureBox { 
    
    public int StarNumber { get { return _starNumber; } } 
    private int _starNumber; 
    public delegate void StarMouseEventDlg(int starNumber); 
    public event StarMouseEventDlg MouseOnStar; 
    public event StarMouseEventDlg MouseOffStar; 
    public event StarMouseEventDlg StarClicked; 
    internal Star(int starNumber, Bitmap offImage) 
    { 
        _starNumber = starNumber; 
         Size = new Size(offImage.Width,offImage.Height); 
         InitializeComponent(); 
    } 
    public void StarOn(Bitmap image) 
    { 
         Image = image; 
    } 
    public void StarOff(Bitmap image) 
    { 
             Image = image; 
    } 
    protected override void OnMouseEnter(EventArgs e) 
    { 
        if (MouseOnStar != null) 
            MouseOnStar(_starNumber); 
    } 
    protected override void OnMouseLeave(EventArgs e) 
    { 
        if (MouseOffStar != null) 
            MouseOffStar(_starNumber); 
    } 
    protected override void OnClick(EventArgs e) 
    { 
        if (StarClicked != null) 
            StarClicked(_starNumber); 
    }   
}

As you can see, the Star class is pretty simple. Its most important function is to alert the StarRatingControl class that one of the Star objects it contains has had the mouse moved onto it, the mouse moved off it or has been clicked on by the user. It accomplishes this by using the observer pattern of allowing interested parties (like the StarRatingControl) to sign up to be notified when certain events happen. The StarRatingControl creates the Star objects and as it does so, it attaches to the three Star events described above.

The Star control has two very simple methods called StarOn and StarOff - hopefully the method names are descriptive enough! The methods have the simple job of changing the image rendered by the Star object. These methods are actually identical now but I suspect they will diverge in future.

The Star class also contains the _starNumber field. This field gives the position of the Star within its container class (StarRatingControl). When the Star object is reporting mouse activity to the StarRatingControl, it passes this field to it so that the StarRatingControl can identify where within its star list the activity occurred.

The next class is the StarRatingControl class. Its job is to contain the Star objects and to orchestrate the correct rendering of each star. This class is a little more complex than the Star class. Some of that complexity arises from my need to have multi colored stars: Selecting a high number of stars results in the stars being colored green while selecting a low number of stars results in the stars being colored red. If this functionality is not required, you can simplify the class by removing the color specific logic.

Rather than looking at the entire class, let me take a more detailed look at its more significant methods.

There are a couple of methods that are not directly related to the StarController functionality but none the less perform some important functions. The LoadDefaultImages method simply adds a collection of default images embedded in the DLL as embedded resources. If you dislike the multi colored behavior, simply change this method to contain a Bitmap array of 1 image. The image that represents the on state of the star. Don't worry if you have 5 stars and only one image, the next method we are going to look at will help us out with that problem:

private void LoadDefaultImages() 
{ 
    if (_onImages != null) return; 
    _onImages = new Bitmap[] { 
          new Bitmap(this.GetType(),"StarOnRed.bmp"), 
          new Bitmap(this.GetType(),"StarOnRed.bmp"), 
          new Bitmap(this.GetType(),"StarOnBlue.bmp"), 
          new Bitmap(this.GetType(),"StarOnGreen.bmp"), 
          new Bitmap(this.GetType(),"StarOnGreen.bmp") }; 
    _offImage = new Bitmap(this.GetType(),"StarOff.bmp"); 
}

The EnsureEnoughImages method checks to see if we have enough images for the specified number of stars. If not, the control makes the assumption that when it runs out of star images, all the other stars will use the first image in the array:

private void EnsureEnoughImages() 
{ 
    if (_onImages == null) 
      throw new ArgumentNullException("OnImages",
                                 "Can not be null."); 
    Bitmap[] images = new Bitmap[_starCount]; 
    for (int x = 0; x < _starCount; x++) 
    { 
        if (x < _onImages.Length) 
            images[x] = _onImages[x]; 
        else 
            images[x] = _onImages[0]; 
    } 
    _onImages = images; 
}

The SetFlickerFreePainting method has some standard instructions for the control base class to tell it to put a little extra effort into how it's drawn. All courtesy of the .NET Framework. Basically the control is instructed to draw itself rather than leaving it to the OS and to also draw itself in such a way as to prevent flicker:

private void SetFlickerFreePainting() 
{ 
    SetStyle(ControlStyles.DoubleBuffer,true); 
    SetStyle(ControlStyles.AllPaintingInWmPaint,true); 
    SetStyle(ControlStyles.UserPaint,true); 
}

The AddTheStars method is where we instantiate the required number of Star objects and tie ourselves into their mouse events:

private void AddTheStars() 
{ 
    Star star = null; 
    this.Controls.Clear(); 
    for (int x = 0; x < _starCount; x++) 
    { 
        star = new Star(x + 1,_offImage); 
        star.Left = (star.Width*x); 
        star.MouseOnStar += 
            new Star.StarMouseEventDlg(Star_MouseOnStar); 
        star.MouseOffStar += 
            new Star.StarMouseEventDlg(Star_MouseOffStar); 
        star.StarClicked += 
            new Star.StarMouseEventDlg(star_StarClicked); 
        this.Controls.Add(star); 
    } 
}

The Star controls send messages back to the StarRatingControls using the following methods. When a star is clicked, it calls the Star_StarClicked method. The method simply sets the currentStarValue field to the starNumber of the Star object that sent the message. When the mouse leaves or enters a Star object, the Star_MouseOffStar and Star_MouseOnStar methods are called. All these methods do is make a call to the DrawCurrentState described below.

The only interesting point on the Star_StarClicked method is to note that if the currently selected Star object is clicked (_currentStarValue == starNumber), then the controls sets that no stars are selected:

private void Star_StarClicked(int starNumber) 
{ 
     _currentStarValue = 
         _currentStarValue == starNumber ? 0 : starNumber; 
} 

private void Star_MouseOffStar(int starNumber) 
{ 
    DrawCurrentState(_currentStarValue); 
} 
private void Star_MouseOnStar(int starNumber) 
{ 
    DrawCurrentState(starNumber); 
}

The DrawCurrentState responds to the mouse events on the Star objects and ensures that the right number of stars are rendered in the On state and in the Off state:

private void DrawCurrentState(int starNumber) 
{ 
    foreach (Star star in this.Controls) 
        { 
        if (star.StarNumber > starNumber) 
            star.StarOff(_offImage); 
        else 
            star.StarOn(_onImages[starNumber - 1]); 
        } 
}

Points of Interest

By default, the control uses multi colored images. This may not be everyone's taste (not even mine) but in my case, I was using the control on each row of a very long table and the color added the ability for the user to be able to understand the general rating of the item in question without counting the stars. You can change this default behavior by simply changing the images loaded in the LoadDefaultImages method of the StarRatingControl class.

I override the SetBoundsCore method of the Control class in the StarRatingControl class to force the control to be only as large as it needs to be to contain its stars. If you would prefer to allow the control to resize freely, remove this override and also the StarRatingControl's SetControlSize method.

To get the control to show no stars selected, simply click on the highest selected star. When you move the mouse off the control, no stars will be selected.

If you find this code useful, feel free to use it. No need to add any comments, links, etc. back to this article. I'm interested in feedback on how you think the coding could have been improved, particularly anything that you see would improve the object oriented-ness of it. Have fun!

Possible Improvements

Probably about a billion, but here are the most obvious ones:

  • Determine the number of stars from the size of the control. The bigger the developer makes the control, the more stars they get.
  • Enhance the control to show the average score so far by other users as well as the current user's score.
  • Allow for different sized images.
  • Add a databinding ability.
  • Hide all the public methods that are not required from the Panel and PictureBox base classes.
  • Allow the user to choose to display a textual description of the selected Star.
  • Implement some sort of formula to gradually change the star color as the number of stars selected increases rather than using the Bitmap array.
  • For those folks who like to play with GDI, perhaps change the OnImages property to a property that accepts an IStarProvder interface. Have the interface define a draw method, probably a couple of size methods and you could pass images and/or GDI draw routines.
  • Etc., etc.

History

  • 8th Jan, 2006 - Version 1.0
  • 1st February, 2011 - Updated source files

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