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

Developing Rating Bar

0.00/5 (No votes)
10 Mar 2009 1  
An advance rating control for .NET
showcase.png

Introduction

Rating function has become very popular these days. The technique behind it is very simple, collect the interest of users on the content. RatingBar is the one that uses that technique with flexible GUI. This control can be used for product quality, product appearance, employee performance and so on... A professional programmer will not prefer to use a NumericUpDown or a ComboBox, instead, a RatingBar.

In this article, we'll learn how to develop RatingBar.

Step 1: Getting Started

First we need to add a class 'RatingBar' in a new ClassLibrary project, add reference of 'System.Windows.Forms' and 'System.Drawing' as well. Inherit from Control object.

Step 2: Thinking

Now, questions arise, what to draw? where to draw? but no worries, I have the answers ;).

First of all, we need to draw empty icons but where? in PictureBox? YES (Why? later!)

Therefore, we need to add PictureBox as well as define an Image variable. Name it 'pb' and 'iconEmpty' respectively.

Again, something confusing...How many icons will be drawn? Well, we leave it to the developer by defining a byte variable.

We'll require three events of PictureBox, MouseMove, MouseLeave and MouseClick. And three functions, one to draw empty icons, another one to update the icon and the third one to update the picturebox size.

iconStarEmpty.png

Add the above image in resources.

The code will look like this:

byte iconsCount = 10;
Image iconEmpty;
PictureBox pb;

public RatingBar()
{
	pb = new PictureBox();
	pb.BackgroundImageLayout = ImageLayout.None;
	pb.MouseMove += new MouseEventHandler(pb_MouseMove);
	pb.MouseLeave += new EventHandler(pb_MouseLeave);
	pb.MouseClick += new MouseEventHandler(pb_MouseClick);
	pb.Cursor = Cursors.Hand;
	this.Controls.Add(pb);

	UpdateIcons();
	UpdateBarSize();

	#region --- Drawing Empty Icons ---
	Bitmap tb = new Bitmap(pb.Width, pb.Height);
	Graphics g = Graphics.FromImage(tb);
	DrawEmptyIcons(g, 0, iconsCount);
	g.Dispose();
	pb.BackgroundImage = tb;
	#endregion
}

void pb_MouseMove(object sender, MouseEventArgs e) { }
void pb_MouseLeave(object sender, EventArgs e) { }
void pb_MouseClick(object sender, MouseEventArgs e) { }
void DrawEmptyIcons(Graphics g, int x, byte count) { }
void UpdateIcons() { iconEmpty = Properties.Resources.iconStarEmpty; //Using the added 
					// empty icon image in Resources 
                   }
void UpdateBarSize() { pb.Size = new Size
		(iconEmpty.Width * iconsCount, iconEmpty.Height); }

Step 3: Drawing

First, we need to write 'DrawEmptyIcons' function.

for (byte a = 0; a < count; a++)
{
     g.DrawImage(iconEmpty, x, 0);
     x += iconEmpty.Width;
}

Step 4: Testing And Rectifying

Now, add 'WindowsApplication' project 'Test', right click on it and 'Set as StartUp Project'. Run once, therefore the RatingBar can appear in Toolbox. When done, add RatingBar in the form. Aha, we can see 10 empty icons but there is a problem. They are very close to each other. To solve this problem, we need to add a byte variable 'gap' (value 1 or 2) plus we have to modify two functions, 'DrawEmptyIcons' and 'UpdateBarSize'.

byte gap = 2;

In 'DrawEmptyIcons',  we are adding 'gap' in 'x' variable, like this:

x += gap + iconEmpty.Width;

In 'UpdateBarSize':

pb.Size = new Size((iconEmpty.Width * iconsCount) + (gap * (iconsCount - 1)), 
	iconEmpty.Height); // Last icon wont need gap, therefore we need to use -1

Time to test again and hmm... we got gap.

Step 5: Drawing

Now, we are going to draw half and full icons for 0.5 and 1.0 values respectively. The best place to draw them is in MouseMove event we added recently. Therefore, first add two more Image objects 'iconHalf' and 'iconFull'.

In this event, we'll save the rating value as well in a temporary variable.

void pb_MouseMove(object sender, MouseEventArgs e)
{
    Bitmap tb = new Bitmap(pb.Width, pb.Height);
    Graphics g = Graphics.FromImage(tb);
    int x = 0;
    float trate = 0;
    byte ticonsCount = iconsCount; // temporary variable to hold the 
			//iconsCount value, because we're decreasing it on each loop
    for (int a = 0; a < iconsCount; a++)
    {
        if (e.X > x && e.X <= x + iconEmpty.Width / 2)
        {
            g.DrawImage(iconHalf, x, 0, iconEmpty.Width, iconEmpty.Height); // Draw 
		//icons with the dimension of iconEmpty, 
		//so that they do not look odd
            x += gap + iconEmpty.Width;
            trate += 0.5f;
        }
        else if (e.X > x)
        {
            g.DrawImage(iconFull, x, 0, iconEmpty.Width, iconEmpty.Height); // Draw 
		//icons with the dimension of iconEmpty, 
		//so that they do not look odd
            x += gap + iconEmpty.Width;
            trate += 1.0f;
        }
        else
            break;
        ticonsCount--;
    }
    tempRateHolder = trate;
    DrawEmptyIcons(g, x, ticonsCount); // Draw empty icons if require
    g.Dispose();
    pb.BackgroundImage = tb;
}

Step 6: Testing And Drawing

Run the application, move mouse over the rating icons... yuppy, we're so near. It's time to use MouseClick and MouseLeave events.

In 'MouseClick', we just need to set actual rate from temporary rate.

rate = tempRateHolder;

In 'MouseLeave', we'll redraw icons from the value of rate.

void pb_MouseLeave(object sender, EventArgs e)
{
    Bitmap tb = new Bitmap(pb.Width, pb.Height);
    Graphics g = Graphics.FromImage(tb);
    int x = 0;
    byte ticonsCount = iconsCount;
    float trate = rate;
    while (trate > 0)
    {
        if (trate > 0.5f)
        {
            g.DrawImage(iconFull, x, 0, iconEmpty.Width, iconEmpty.Height); // Draw 
		//icons with the dimension of iconEmpty, so that they do not look odd 
            x += gap + iconEmpty.Width;
        }
        else if (trate == 0.5f)
        {
            g.DrawImage(iconHalf, x, 0, iconEmpty.Width, iconEmpty.Height); // Draw 
		//icons with the dimension of iconEmpty, so that they do not look odd
            x += gap + iconEmpty.Width;
        }
        else
            break;
        ticonsCount--;
        trate--;
    }
    DrawEmptyIcons(g, x, ticonsCount);
    g.Dispose();
    pb.BackgroundImage = tb;
}

Step 7: Testing And Enhancing

Run the application, whoa...we made it work. But there is something missing, the control isn't looking pro. We must enhance it by adding some more features. We're going to add these...

  • Raise event when rate changes
  • RatingBar alignment
  • Icons style
  • Read only and Rate once features
  • Properties

Sub Step 1 : Adding Event on Rate Change

We're required to declare a delegate above the RatingBar class. That will have 2 params as usual. One is sender and the other one is the type of RatingBarRateEventArgs. RatingBarRateEventArgs class contains 2 float variables, those hold new and old rate values plus a constructor with two params (i.e. NewRate, OldRate) plus inherits EventArgs.

public delegate void OnRateChanged(object sender, RatingBarRateEventArgs e);
public class RatingBarRateEventArgs : EventArgs
{
    public float NewRate;
    public float OldRate;
    public RatingBarRateEventArgs(float newRate, float oldRate)
    {
        NewRate = newRate;
        OldRate = oldRate;
    }
}

Next, declare Event in RatingBar class.

public event OnRateChanged RateChanged;

Now, we're going to modify 'MouseClick' like this:

void pb_MouseClick(object sender, MouseEventArgs e)
{
    float toldRate = rate;
    rate = tempRateHolder;

    if (RateChanged != null && toldRate != rate)
	RateChanged(this, new RatingBarRateEventArgs(rate, toldRate));
}

Sub Step 2: RatingBar Alignment

As we can see, the rating bar is always at the topleft (0, 0) of the control. But we can solve this by adding an alignment feature.

First, declare a ContentAlignment enum with MiddleCenter value. Name it 'alignment'.

Add a new method 'UpdateBarLocation' with the following code...

    if (alignment == ContentAlignment.TopLeft) { } // Leave it blank, 
	//Since we're calling this from Resize Event then we dont need to set
	//same point again and again
    else if (alignment == ContentAlignment.TopRight)
        pb.Location = new Point(this.Width - pb.Width, 0);
    else if (alignment == ContentAlignment.TopCenter)
        pb.Location = new Point(this.Width / 2 - pb.Width / 2, 0);
    else if (alignment == ContentAlignment.BottomLeft)
        pb.Location = new Point(0, this.Height - pb.Height);
    else if (alignment == ContentAlignment.BottomRight)
        pb.Location = new Point(this.Width - pb.Width, this.Height - pb.Height);
    else if (alignment == ContentAlignment.BottomCenter)
        pb.Location = 
		new Point(this.Width / 2 - pb.Width / 2, this.Height - pb.Height);
    else if (alignment == ContentAlignment.MiddleLeft)
        pb.Location = new Point(0, this.Height / 2 - pb.Height / 2);
    else if (alignment == ContentAlignment.MiddleRight)
        pb.Location = 
		new Point(this.Width - pb.Width, this.Height / 2 - pb.Height / 2);
    else if (alignment == ContentAlignment.MiddleCenter)
        pb.Location = new Point(this.Width / 2 - pb.Width / 2, 
				this.Height / 2 - pb.Height / 2);

Now, override 'OnResize' and write this code inside it:

    UpdateBarLocation();
    base.OnResize(e);

Next, we need to modify 'UpdateBarSize' method and add this at the end of the code to avoid strange effects after size change:

UpdateBarLocation();

Sub Step 3: Icons Styles

As we can notice, we are able to change icons' images. But the builtin is a single one. However, we can add some more inbuilt images.

To do that, first create a new enum above the RatingBar class. Name it 'IconStyle'.

public enum IconStyle
{
    Star,
    Heart,
    Misc
}

Now, declare it like this:

IconStyle iconStyle = IconStyle.Star;

Next, we have to modify 'UpdateIcons' method like this:

void UpdateIcons()
{
    if (iconStyle == IconStyle.Star)
    {
        iconEmpty = Properties.Resources.iconStarEmpty;
        iconFull = Properties.Resources.iconStarFull;
        iconHalf = Properties.Resources.iconStartHalf;
    }
    else if (iconStyle == IconStyle.Heart)
    {
        iconEmpty = Properties.Resources.iconHeartEmpty;
        iconFull = Properties.Resources.iconHeartFull;
        iconHalf = Properties.Resources.iconHeartHalf;
    }
    else if (iconStyle == IconStyle.Misc)
    {
        iconEmpty = Properties.Resources.iconMiscEmpty;
        iconFull = Properties.Resources.iconMiscFull;
        iconHalf = Properties.Resources.iconMiscHalf;
    }
}

NOTE: The images are added in Resources, you can get them from 'Resources' directory.

Sub Step 4: ReadOnly and RateOnce

Sometimes, we require this control to be ReadOnly. As well as it can be rate once. Therefore, to add these features, we have to declare 3 bool variables and modify MouseMove, MouseLeave and MouseClick events.

bool readOnly = false;
bool rateOnce = false;
bool isVoted = false;

In MouseMove and MouseLeave, add this on the top of code:

if (readOnly || (rateOnce && isVoted))
    return;

In MouseClick, add this just above this line >> if (RateChanged != null && toldRate != rate):

isVoted = true;
if (rateOnce)
    pb.Cursor = Cursors.Default;

Sub Step 5: Properties

We're going to add properties to this control so that it can be changed in design time as well as runtime.

public byte Gap
{
    get { return gap; }
    set { gap = value; }
}
public byte IconsCount
{
    get { return iconsCount; }
    set { if (value <= 10) { iconsCount = value; UpdateBarSize(); } }
}
[DefaultValue(typeof(ContentAlignment), "middlecenter")]
public ContentAlignment Alignment
{
    get { return alignment; }
    set
    {
        alignment = value;
        if (value == ContentAlignment.TopLeft)
            pb.Location = new Point(0, 0);
        else UpdateBarLocation();
    }
}

public Image IconEmpty
{
    get { return iconEmpty; }
    set { iconEmpty = value; }
}
public Image IconHalf
{
    get { return iconHalf; }
    set { iconHalf = value; }
}
public Image IconFull
{
    get { return iconFull; }
    set { iconFull = value; }
}

[DefaultValue(false)]
public bool ReadOnly
{
    get { return readOnly; }
    set { readOnly = value; if (value)pb.Cursor = Cursors.Default; 
				else pb.Cursor = Cursors.Hand; }
}
[DefaultValue(false)]
public bool RateOnce
{
    get { return rateOnce; }
    set { rateOnce = value; if (!value) pb.Cursor = Cursors.Hand; /* Set hand cursor, 
						if false is set from true*/ 
	}
}
[Browsable(false)]
public float Rate
{
    get { return rate; }
}

public Color BarBackColor
{
    get { return pb.BackColor; }
    set { pb.BackColor = value; }
}

[DefaultValue(typeof(IconStyle), "star")]
public IconStlye IconStyle
{
    get { return iconStyle; }
    set { iconStyle = value; UpdateIcons(); }
}

FAQ

Q. Why a child control (PictureBox) ?

A. Since we've added alignment functionality we had to use it. As this control has its own alignment option, then it won't depend on its parent control's alignment. Let's assume, we do not have PictureBox and the outer control size is (500, 200). Now, when we draw the image for control, it will be the size of (500, 200) but the actual we require is (iconsCount * iconWidth, iconHeight). This is quite useless performance on every mouse move and some other drawbacks...

Update

Sometimes we need to set the Rate value programmatically but since we created Rate property as Read-Only, we're unable to do that until we made it writable too. Therefore, we are going to add a new function 'DrawIcons'. Plus, select all the code inside MouseLeave except the ReadOnly and ReadOnce checking code and paste in our new function and call it in MouseLeave. It will look like this:

void DrawIcons()
{
    Bitmap tb = new Bitmap(pb.Width, pb.Height);
    Graphics g = Graphics.FromImage(tb);
    int x = 0;
    byte ticonsCount = iconsCount;
    float trate = rate;
    while (trate > 0)
    {
        if (trate > 0.5f)
        {
            g.DrawImage(iconFull, x, 0, iconEmpty.Width, iconEmpty.Height); // Draw 
		//icons with the dimension of iconEmpty, so that they do not look odd
            x += gap + iconEmpty.Width;
        }
        else if (trate == 0.5f)
        {
            g.DrawImage(iconHalf, x, 0, iconEmpty.Width, iconEmpty.Height); // Draw 
		//icons with the dimension of iconEmpty, so that they do not look odd
            x += gap + iconEmpty.Width;
        }
        else
            break;
        ticonsCount--;
        trate--;
    }
    DrawEmptyIcons(g, x, ticonsCount);

    g.Dispose();
    pb.BackgroundImage = tb;
}

void pb_MouseLeave(object sender, EventArgs e)
{
    if (readOnly || (rateOnce && isVoted))
        return;
    DrawIcons();
}

Also add the following code in Rate property:

set
{
    if (value >= 0 && value <= (float)iconsCount)
    {
         float toldRate = rate;
         rate = value;
         DrawIcons();
         OnRateChanged(new RatingBarRateEventArgs(value, toldRate));
    }
    else
         throw new ArgumentOutOfRangeException("Rate", "Value '" + 
		value + "' is not valid for 'Rate'. Value must be Non-negative 
		and less than or equals to '" + iconsCount + "'");
}

In the above code, OnRateChanged() called. We didn't add that earlier because we just required it on MouseClick. Now, add this and make it virtual so that it can be overridden.

protected virtual void OnRateChanged(RatingBarRateEventArgs e)
{
    if (RateChanged != null && e.NewRate != e.OldRate)
        RateChanged(this, e);
}

Now we have to modify the MouseClick event too. We know what to do...;)

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