65.9K
CodeProject 正在变化。 阅读更多。
Home

开发评分条

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.56/5 (23投票s)

2009 年 3 月 1 日

CPOL

5分钟阅读

viewsIcon

49310

downloadIcon

1086

.NET 的高级评分控件

showcase.png

引言

评分功能如今已变得非常流行。其背后的技术非常简单,即收集用户对内容的兴趣。RatingBar 是一款运用该技术并提供灵活 GUI 的控件。该控件可用于产品质量、产品外观、员工绩效等方面…… 专业的程序员不会倾向于使用 NumericUpDownComboBox,而是会选择 RatingBar

在本文中,我们将学习如何开发 RatingBar

步骤 1:入门

首先,我们需要在一个新的 ClassLibrary 项目中添加一个名为 'RatingBar' 的类,并添加对 'System.Windows.Forms' 和 'System.Drawing' 的引用。继承自 Control 对象。

步骤 2:构思

现在,我们会产生一些疑问:画什么?在哪里画?但别担心,我都有答案 ;)。

首先,我们需要绘制空的图标,但放在哪里?PictureBox 中?是的(为什么?后面再说!)。

因此,我们需要添加 PictureBox 并定义一个 Image 变量。分别命名为 'pb' 和 'iconEmpty'。

再次,有些令人困惑……将绘制多少个图标?嗯,我们通过定义一个 byte 变量让开发者决定。

我们将需要 PictureBox 的三个事件:MouseMoveMouseLeaveMouseClick。以及三个函数:一个用于绘制空图标,另一个用于更新图标,第三个用于更新 PictureBox 的大小。

iconStarEmpty.png

将上述图像添加到资源中。

代码将如下所示

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); }

步骤 3:绘制

首先,我们需要编写 'DrawEmptyIcons' 函数。

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

步骤 4:测试和纠正

现在,添加一个名为 'Test' 的 'WindowsApplication' 项目,右键单击它并选择 '设置为启动项目'。运行一次,以便 RatingBar 可以出现在工具箱中。完成后,将 RatingBar 添加到窗体中。啊哈,我们看到了 10 个空图标,但有一个问题。它们彼此靠得太近了。为了解决这个问题,我们需要添加一个 byte 变量 'gap'(值为 1 或 2),并且我们必须修改两个函数:'DrawEmptyIcons' 和 'UpdateBarSize'。

byte gap = 2;

在 'DrawEmptyIcons' 中,我们在 'x' 变量中添加了 'gap',如下所示:

x += gap + iconEmpty.Width;

在 'UpdateBarSize' 中:

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

是时候再次测试了,嗯……我们得到了间距。

步骤 5:绘制

现在,我们将为 0.5 和 1.0 的值分别绘制半满和全满图标。绘制它们的最合适位置是我们最近添加的 MouseMove 事件。因此,首先再添加两个 Image 对象:'iconHalf' 和 'iconFull'。

在此事件中,我们还将评分值保存在一个临时变量中。

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;
}

步骤 6:测试和绘制

运行应用程序,将鼠标悬停在评分图标上……耶,我们快成功了。是时候使用 MouseClickMouseLeave 事件了。

在 'MouseClick' 中,我们只需将实际评分设置为临时评分。

rate = tempRateHolder;

在 'MouseLeave' 中,我们将根据评分值重绘图标。

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;
}

步骤 7:测试和增强

运行应用程序,哇……我们做到了!但还有些东西缺失,这个控件看起来不够专业。我们必须通过添加更多功能来增强它。我们将添加这些功能……

  • 评分更改时引发事件
  • RatingBar 对齐方式
  • 图标样式
  • 只读和一次性评分功能
  • 属性

子步骤 1:在评分更改时添加事件

我们需要在 RatingBar 类上方声明一个委托。它将像往常一样带有 2 个参数。一个是发送者,另一个是 RatingBarRateEventArgs 类型。RatingBarRateEventArgs 类包含两个 float 变量,它们保存新旧评分值,以及一个带有两个参数(即 NewRateOldRate)的构造函数,并继承 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;
    }
}

接下来,在 RatingBar 类中声明 Event

public event OnRateChanged RateChanged;

现在,我们将修改 'MouseClick' 如下:

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

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

子步骤 2:RatingBar 对齐方式

如我们所见,评分条始终位于控件的左上角 (0, 0)。但我们可以通过添加对齐功能来解决此问题。

首先,声明一个 ContentAlignment enum,其值为 MiddleCenter。并将其命名为 'alignment'。

添加一个新方法 'UpdateBarLocation',其中包含以下代码……

    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);

现在,重写 'OnResize' 并在其中写入此代码:

    UpdateBarLocation();
    base.OnResize(e);

接下来,我们需要修改 'UpdateBarSize' 方法,并在代码末尾添加此代码,以避免在大小更改后出现奇怪的效果:

UpdateBarLocation();

子步骤 3:图标样式

正如我们所注意到的,我们能够更改图标的图像。但内置的只有一个。但是,我们可以添加更多内置图像。

为此,首先在 RatingBar 类上方创建一个新的 enum。并将其命名为 'IconStyle'。

public enum IconStyle
{
    Star,
    Heart,
    Misc
}

现在,按如下方式声明它:

IconStyle iconStyle = IconStyle.Star;

接下来,我们必须修改 'UpdateIcons' 方法,如下所示:

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;
    }
}

注意:图像已添加到资源中,您可以从 'Resources' 目录获取它们。

子步骤 4:只读和一次性评分

有时,我们需要此控件为 ReadOnly。并且它也可以是一次性评分。因此,为了添加这些功能,我们必须声明 3 个 bool 变量并修改 MouseMoveMouseLeaveMouseClick 事件。

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

MouseMoveMouseLeave 中,在代码顶部添加此内容:

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

MouseClick 中,在此行之前添加此内容 >> if (RateChanged != null && toldRate != rate)

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

子步骤 5:属性

我们将向此控件添加属性,以便可以在设计时和运行时对其进行更改。

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(); }
}

常见问题解答

问:为什么使用子控件 (PictureBox)?

答:因为我们添加了对齐功能,所以必须使用它。由于此控件有自己的对齐选项,因此它不会依赖于其父控件的对齐方式。假设我们没有 PictureBox,而外部控件的大小是 (500, 200)。现在,当我们为控件绘制图像时,它的大小将是 (500, 200),但我们实际需要的大小是 (iconsCount * iconWidth, iconHeight)。这在每次鼠标移动时都会导致相当无用的性能和其他一些缺点……

更新

有时我们需要以编程方式设置 Rate 值,但由于我们将 Rate 属性创建为只读的,因此我们无法做到这一点,除非我们也使其可写。因此,我们将添加一个新函数 'DrawIcons'。另外,选择 MouseLeave 中的所有代码(除了 ReadOnlyReadOnce 检查代码)并将其粘贴到我们的新函数中,并在 MouseLeave 中调用它。它看起来会像这样:

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();
}

还将以下代码添加到 Rate 属性中:

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 + "'");
}

在上面的代码中,调用了 OnRateChanged()。我们之前没有添加它,因为我们只在 MouseClick 时才需要它。现在,添加它并使其成为虚拟的,以便可以重写它。

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

现在我们还需要修改 MouseClick 事件。我们知道该怎么做…… ;)

© . All rights reserved.