开发评分条
.NET 的高级评分控件

引言
评分功能如今已变得非常流行。其背后的技术非常简单,即收集用户对内容的兴趣。RatingBar
是一款运用该技术并提供灵活 GUI 的控件。该控件可用于产品质量、产品外观、员工绩效等方面…… 专业的程序员不会倾向于使用 NumericUpDown
或 ComboBox
,而是会选择 RatingBar
。
在本文中,我们将学习如何开发 RatingBar
。
步骤 1:入门
首先,我们需要在一个新的 ClassLibrary
项目中添加一个名为 'RatingBar
' 的类,并添加对 'System.Windows.Forms
' 和 'System.Drawing
' 的引用。继承自 Control
对象。
步骤 2:构思
现在,我们会产生一些疑问:画什么?在哪里画?但别担心,我都有答案 ;)。
首先,我们需要绘制空的图标,但放在哪里?PictureBox
中?是的(为什么?后面再说!)。
因此,我们需要添加 PictureBox
并定义一个 Image
变量。分别命名为 'pb
' 和 'iconEmpty
'。
再次,有些令人困惑……将绘制多少个图标?嗯,我们通过定义一个 byte
变量让开发者决定。
我们将需要 PictureBox
的三个事件:MouseMove
、MouseLeave
和 MouseClick
。以及三个函数:一个用于绘制空图标,另一个用于更新图标,第三个用于更新 PictureBox
的大小。

将上述图像添加到资源中。
代码将如下所示
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:测试和绘制
运行应用程序,将鼠标悬停在评分图标上……耶,我们快成功了。是时候使用 MouseClick
和 MouseLeave
事件了。
在 '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
变量,它们保存新旧评分值,以及一个带有两个参数(即 NewRate
、OldRate
)的构造函数,并继承 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
变量并修改 MouseMove
、MouseLeave
和 MouseClick
事件。
bool readOnly = false;
bool rateOnce = false;
bool isVoted = false;
在 MouseMove
和 MouseLeave
中,在代码顶部添加此内容:
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
中的所有代码(除了 ReadOnly
和 ReadOnce
检查代码)并将其粘贴到我们的新函数中,并在 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
事件。我们知道该怎么做…… ;)