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

C# Star Rating Control

0.00/5 (No votes)
20 Dec 2004 2  
A C# star rating control.

C# Star Rating Control Screenshot

Introduction

This is a control similar to one used on the Netflix movie rental website, and one used by the iTunes and Microsoft Media Player to rate the songs in the song library.

Background

There is an implementation of such a control in C++, which can be found here: StarControl.

This article and code were influenced by a recently published MSDN magazine article by Duncan Mackenzie, which is available here: Creating a Five-Star Rating Control. His implementation is in Visual Basic .NET.

This control is implemented in C#.

The Code

  • Create a Class Library project.
  • Inherit from System.Windows.Forms.Control.
  • Add a project Reference to the System.Windows.Forms.dll.

This is not done automatically, and can be done by right clicking on "References" in the Solution Explorer and choosing the wanted DLL.

Constructor

public StarRatingControl()
{
    SetStyle(ControlStyles.AllPaintingInWmPaint, true);
    SetStyle(ControlStyles.UserPaint, true);
    SetStyle(ControlStyles.DoubleBuffer, true);
    SetStyle(ControlStyles.ResizeRedraw, true);

    Width = 120;
    Height = 18;

    m_starAreas = new Rectangle[StarCount];
}
 >>> SetStyle(ControlStyles.AllPaintingInWmPaint, true);

This lowers the flickering during background painting by telling the control to ignore the WM_ERASEBKGND message. This only works however when the ControlStyles.UserPaint control style is set.

 >>> SetStyle(ControlStyle.UserPaint, true);

This indicates that the control is going to paint itself.

 >>> SetStyle(ControlStyles.DoubleBuffer, true);

This indicates that the control will use double buffering to draw itself. This will also reduce flicker. ControlStyle.UserPaint and ControlStyle.AllPaintInWmPaint should be set in conjunction with this bit.

 >>> SetStyle(ControlStyles.ResizeRedraw, true);

This indicates that the control is to redraw itself upon being resized.

LeftMargin, RightMargin, TopMargin, and BottomMargin are properties exposing the following private data members:

private int m_leftMargin = 2;
private int m_rightMargin = 2;
private int m_topMargin = 2;
private int m_bottomMargin = 2;

The public properties are mostly of the form:

public int LeftMargin
{
    get
    {
        return m_leftMargin;
    }
    set
    {
        if ( m_leftMargin != value )
        {
            m_leftMargin = value;
            Invalidate();
        }
    }
} 

Please notice that setting the property forces a redraw of the control if the value of the margin changes. This logic also holds for some of the other properties dealing with modifiable visual properties of the control, such as the property exposing the number of stars to draw.

protected override void OnPaint(PaintEventArgs pe)
{    
    pe.Graphics.Clear(BackColor);

    int starWidth =(Width -(LeftMargin + RightMargin + 
                            (StarSpacing *(StarCount - 1))))/StarCount;
    int starHeight = (Height - (TopMargin + BottomMargin));

    Rectangle drawArea = 
            new Rectangle(LeftMargin, TopMargin, starWidth, starHeight);

    for ( int i = 0 ; i < StarCount; ++i )
    {
        m_starAreas[i].X = drawArea.X - StarSpacing / 2;
        m_starAreas[i].Y = drawArea.Y;
        m_starAreas[i].Width = drawArea.Width + StarSpacing / 2;
        m_starAreas[i].Height = drawArea.Height;

        DrawStar ( pe.Graphics, drawArea, i );
        
        drawArea.X += drawArea.Width + StarSpacing;
    }

    base.OnPaint ( pe );
}

All of the drawing is done in this method. For the implementation of this method, we will need to add a reference to System.Drawing.dll.

 >>> pe.Graphics.Clear(this.BackColor)

This erases the background.

int starWidth =(Width -(LeftMargin + RightMargin + 
                      (m_starSpacing *(m_starCount -1))))/ m_starCount;
int starHeight = (Height - (TopMargin + BottomMargin));

This calculates the width and height of the stars given the control width and height and the inner margins.

Then we draw each of the stars.

protected void DrawStar ( Graphics g, Rectangle rect, int starAreaIndex )
{    
    Brush fillBrush;
    Pen outlinePen = new Pen ( OutlineColor, OutlineThickness );
    
    if ( m_hovering  &&   m_hoverStar > starAreaIndex )
    {
        fillBrush = new LinearGradientBrush(rect,
               HoverColor, BackColor, LinearGradientMode.ForwardDiagonal); 
    }
    else if ( (!m_hovering) &&     m_selectedStar > starAreaIndex )
    {
        fillBrush = new LinearGradientBrush(rect,
               SelectedColor, BackColor, LinearGradientMode.ForwardDiagonal);
    }
    else
    {
        fillBrush = new SolidBrush ( BackColor );
    }
    
    PointF[] p = new PointF[10];
    p[0].X = rect.X + (rect.Width / 2);
    p[0].Y = rect.Y;
    p[1].X = rect.X + (42 * rect.Width / 64);
    p[1].Y = rect.Y + (19 * rect.Height / 64);
    p[2].X = rect.X + rect.Width;
    p[2].Y = rect.Y + (22 * rect.Height / 64);
    p[3].X = rect.X + (48 * rect.Width / 64);
    p[3].Y = rect.Y + (38 * rect.Height / 64);
    p[4].X = rect.X + (52 * rect.Width / 64);
    p[4].Y = rect.Y + rect.Height;
    p[5].X = rect.X + (rect.Width / 2);
    p[5].Y = rect.Y + (52 * rect.Height / 64);
    p[6].X = rect.X + (12 * rect.Width / 64);
    p[6].Y = rect.Y + rect.Height;
    p[7].X = rect.X + rect.Width / 4;
    p[7].Y = rect.Y + (38 * rect.Height / 64);
    p[8].X = rect.X;
    p[8].Y = rect.Y + (22 * rect.Height / 64);
    p[9].X = rect.X + (22 * rect.Width / 64);
    p[9].Y = rect.Y + (19 * rect.Height / 64);

    g.FillPolygon ( fillBrush, p );
    g.DrawPolygon ( outlinePen, p );
}

The areas in which the stars are to be drawn are contained in an array of System::Drawing::Rectangle objects. This array is of the same size as the number of stars and is resized whenever the count of stars changes through the StarCount property. Each of the stars is filled with a gradient brush. This is an arbitrary decision, but one which I hope would yield more pleasing results than a solid color would. The 10 points of each star are then calculated. I found it easiest to implement the star by dividing the drawing area into 64 logical partitions and placing the horizontal and vertical points as the offset into the 64 segment space. Finally, we fill the polygon specified by these ten points with the forward-diagonal, gradient brush, and outline the polygon with the outline pen. Lastly, we implement the event handlers dealing with various mouse functions. When the mouse enters the control, we set the hovering flag to true and force a repaint of the control:

protected override void OnMouseEnter ( System.EventArgs ea )
{
    m_hovering = true;
    Invalidate();
    base.OnMouseEnter ( ea );
}

When the mouse leaves the control, we clear that flag and once again force a repaint of the control:

protected override void OnMouseLeave ( System.EventArgs ea )
{
    m_hovering = false;
    Invalidate();
    base.OnMouseLeave ( ea );
}

Whenever the mouse moves, we check each of the stars in order to see over which, if any, the mouse is hovering. We record this information and force a repaint of the control.

protected override void OnMouseMove ( MouseEventArgs args )
{
    for ( int i = 0 ; i < StarCount ; ++i )
    {
        if ( m_starAreas[i].Contains(args.X, args.Y) )
        {
            m_hoverStar = i + 1;
            Invalidate();
            break;
        }
    }

    base.OnMouseMove ( args );
}

The logic is similar whenever a user clicks on the control. We iterate over each of the stars to see which, if any, was clicked on, and record this information. We then force a repaint of the control.

Invalidate() eventually turns into a call to OnPaint which calls DrawStar(). DrawStar(), in turn, will check our recorded hover and click information to figure out how to render the control.

Testing and using the control

Any project using the control must first add a reference just like we added a reference to the System.Drawing.dll. Add "usingRatingControls" to the using declarations.

The sample application does little else than depend on the default properties provided by the control, which include 5 stars with a dark gray outline, a yellow hover color, and a royal blue selected color. The control also has a default width and height of 120 and 18 pixels respectively. I found that this ratio produced the most pleasing looking image. The height and width are, of course, modifiable, and your stars can thus look as tall or as wide as you please.

Really, the only mildly-interesting piece of code in the sample application is in the constructor. The following is the piece of code, in its entirety:

public MainForm()
{
    InitializeComponent();

    m_starRatingControl.Top = 45;
    m_starRatingControl.Left = 85;

    Controls.Add ( m_starRatingControl );
}

Possible Improvements

The control could benefit from more publicly exposed properties such as fill brush style, so that forward-diagonal gradient is not the only implementation.

If speed optimization is a priority over size, one can rewrite the OnPaint() method so that it uses cached pre-calculated points to draw each of the stars instead of calculating the points on the fly. This would necessitate a few more data members to store the cached data, but would greatly improve the rendering speed of the control. This will probably be one of my own improvements of this control.

If you are really ambitious, you can follow the example of the MSDN magazine article and implement functionality to render a user-supplied image rather than a star. This would, of course, necessitate a name change, as it would no longer technically be a *star* rating control.

Please send me any comments/questions/suggestions. I will strive to implement any valid suggestions for improvements and answer all of your questions.

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