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
- 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.
- Take a more object oriented approach.
- Encapsulate the star behavior in the
Star
class.
- Encapsulate the rating behavior in the
StarRatingControl
class.
- Allow for N stars rather than the usual 5.
- 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.
- 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