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 Mou
seClick
. And three functions, one to draw empty icons, another one to update the icon and the third one to update the picturebox
size.
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...