带设计器支持的 SignalBar 控件





4.00/5 (5投票s)
带设计器支持的 PocketPC 版 SignalBar。
引言
俗话说“ necessity is the mother of invention”(需求是发明之母)。我写这篇文章并非要发明什么新东西,但我怀揣着创新的精神。开发 Pocket PC 应用程序时需要一个自定义的信号条,这激励我写了这样一个控件。
背景
不幸的是,在 .NET CF 中编写自定义控件不像在完整 .NET Framework 中那样直接。它没有提供“设计器支持” (DS)!幸运的是,有一些文章可以帮助您为 .NET CF 添加 DS。其中一篇是 Jim Wilson 写的:《为 .NET Compact Framework DateTimePicker 控件添加设计器支持》。因此,我强烈建议您阅读他的文章,作为理解我这篇文章的前提。所以,在这篇文章中,我将更多地关注实际控件的编写。然而,源代码将包含 DS 并可直接使用。
我认为编写自定义控件的核心在于能够将控件背后的逻辑与图形连接起来。要编写控件,许多程序员知道如何编写功能逻辑部分。但是,很少有人知道如何处理图形/UI 部分。因此,我将更多地关注绘图部分,而将功能部分的详细讨论留给读者自行查阅源代码。
使用控件
您可以下载 signalbar_demo.zip 文件。其中包含两个程序集:运行时和设计时。将运行时程序集 (SignalBarControl.dll) 提取到 \Program Files\Microsoft Visual Studio .NET 2003\CompactFrameworkSDK\v1.0.5000\Windows CE\,如果不存在,则创建一个名为 Designer 的子文件夹,并将设计时程序集 (SignalBarControl.Design.dll) 复制到其中。接下来,将其添加到您的 VS.NET 工具箱(视图 -> 工具箱 或 Ctrl+Alt+X)。您可以右键单击并选择“添加选项卡”来创建自己的控件选项卡。然后,通过选择“添加/删除项…”菜单项将控件程序集添加到新创建的选项卡中。然后浏览并找到 {Installation Directory}\Designer\ 文件夹中的 SignalBarControl.Design.dll 程序集,单击“确定”,然后您应该能够看到 SignalBar
控件并将其拖放(drag-and-drop)到您的窗体上。
要求
让我们从需求开始。我们需要编写一个彩色的信号条,它使用图形表示来指示定性值。颜色可以在设计时自定义。此外,我们将有固定的四个不同长度的条来指示信号强度。您可以进一步泛化设计,允许用户指定条的数量;但为了简化起见,我在这篇文章中不这样做。如果您的 PDA 运行的是最新的 Windows CE,您可能会发现顶部栏上有一个类似的控件。
您可以看到该控件的行为类似于进度条控件。您初始化 Minimum
和 Maximum
值,设置当前的 Value
,然后控件将根据您设置的这三个变量显示图形条表示。然而,在图形表示方面,这个信号条比进度条控件更专业。
因此,从概念设计角度来看,您可以派生 SignalBar
类自进度条控件,但自己进行绘制。不过,在这篇文章中,我宁愿派生 SignalBar
自 Control
类。首先,这样我就可以从头开始向您展示设计,包括定义其字段、属性和方法。其次,由于我们正在为 .NET CF 编写控件,我们将尽量避免因继承关系而产生的不必要的开销。
图 1:SignalBar 类图,继承与非继承版本
设计
我最喜欢的控件设计方式是将其绘制成一个“黑匣子”,输入和输出进出。对我来说,这总是奏效的,这样我就可以清晰地了解如何设计这样的控件。
我们之前已经指定了 SignalBar 将具有 Minimum
、Maximum
和(当前)Value
,以及四根条的单独颜色。正确初始化后,您应该会看到一个漂亮的信号条图像。
图 2:“黑匣子”信号条
与内置控件一样,您可以在 Visual Studio .NET (VS.NET) 属性窗口中编辑其属性。默认情况下,所有公共属性都会出现在属性窗口中。然而,对于 .NET CF,在属性出现在属性窗口之前需要做一些额外的工作。请参考 Wilson 的文章了解更多解释。
举个例子。为了将 Minimum
属性公开到 VS.NET 属性窗口,我们可以这样做:
protected int minimum;
#if NETCFDESIGNTIME
[System.ComponentModel.Browsable(true)]
[System.ComponentModel.Category("Design")]
[System.ComponentModel.DefaultValue(0)]
[System.ComponentModel.Description("The minimum value of the control.")]
#endif
public int Minimum
{
get
{
return minimum;
}
set
{
minimum = value;
Invalidate();
}
}
图 3:将 Minimum 属性公开到 VS.NET 属性窗口
set
部分的最后一行,Invalidate()
,会在用户更改最小值时调用绘制例程来重绘控件。您应该会在 VS.NET 属性窗口的“设计”类别下看到“可浏览”的 Minimum
属性,其下方有指定的描述,并且默认值为“0”,如下图所示。
图 4:VS.NET 属性窗口中的 Minimum 属性
然后,您可以将相同的过程应用于其他属性。
接下来,我将解释图形条的构建。首先,我们需要创建一个函数来绘制单个条,将条的索引和颜色作为参数,即 DrawBar(int index, Color color)
。创建 DrawBar
函数中最具挑战性的部分是动态计算矩形的大小。
图 5:条形设计
每个条的宽度计算为控件宽度的四分之一。然而,如果将空间精确地分成四份,控件看起来会太拥挤。通过一个简单的数学技巧,我们可以从每个条的左右矩形各减去 1 像素,这样我们就能看到条之间有一些空间。
确定每个条的高度有点棘手。这是因为使用的坐标系以左上角为原点,而不是屏幕的左下角。因此,当您指定 y = 0 且高度为 h 时,图形看起来不太对。它看起来是反转的。假设控件的高度为 H,计算出的条的高度为 h,为了解决这个问题,您需要通过 Inverse(y) = H – h 来反转垂直坐标。
代码
现在,我们已经明确了需求和规格,可以开始编写代码了。DrawBar
函数以确定条的宽度和高度开始。条的高度计算为控件高度的比例,即条的索引加 1 乘以控件高度除以条数。请记住,我们已经约定条的数量固定为 4,尽管您可以进一步泛化。如果您打算这样做,唯一需要修改的地方是如何为 n 个条指定颜色。
protected void DrawBar(int index, Color color)
{
int barWidth = this.Width / barcount;
int height = Convert.ToInt16(Height * (index + 1)/ barcount);
Rectangle r = new Rectangle(
(index * barWidth) + 1, Height-height,
barWidth-1, height);
SolidBrush br = new SolidBrush(color);
m_graphics.FillRectangle(br, r);
}
下一个函数负责处理 OnPaint
事件。当控件收到 OnPaint
事件时,就会执行此函数。我们可以通过调用 Invalidate()
来强制控件接收 OnPaint
事件。您可能想查阅 MSDN 库,了解 Invalidate()
、Refresh()
和 Update()
之间的区别。在我们的例子中,我使用 Invalidate()
调用来重绘控件。
为了避免闪烁,我们创建一个屏幕外位图 m_bmp
,通过调用 CreateMemoryBitmap()
。然后我们绘制条。控件的当前值 currentValue
,以及最小值和最大值决定了将绘制多少条。然后,通过调用 DrawBar
函数来绘制相应数量的条。该函数通过将内存位图blit(blitting)到屏幕上来完成其周期。
protected override void OnPaint(PaintEventArgs e)
{
// draw to memory bitmap
CreateMemoryBitmap();
// init background
m_graphics.Clear(this.BackColor);
// determine how many bars should be drawn
int nBar = (currentValue-minimum) * barcount / (maximum-minimum) ;
// draw bars
for(int i=0; i < nBar; i++)
{
DrawBar(i, GetColor(i));
}
// blit memory bitmap to screen
e.Graphics.DrawImage(m_bmp, 0, 0);
}
/// <summary>
/// Create offsceeen bitmap. This bitmap is used for double-buffering
/// to prevent flashing.
/// </summary>
private void CreateMemoryBitmap()
{
if (m_bmp == null || m_bmp.Width != this.Width
|| m_bmp.Height != this.Height)
{
// memory bitmap
m_bmp = new Bitmap(this.Width, this.Height);
m_graphics = Graphics.FromImage(m_bmp);
}
}
当控件大小调整时,我们希望更新图形,因此我们重写了 OnResize()
处理程序,以便当控件的大小改变时,它会通过再次调用 Invalidate()
函数来生成 OnPaint
消息,并使用更新的大小重绘控件。
protected override void OnResize(EventArgs e)
{
base.OnResize (e);
this.Invalidate();
}
结论
.NET CF 中缺少设计器支持并没有阻止我们创新。感谢那些撰写有关为 .NET CF 添加设计器支持的出色文章的人们。这使我能够编写一个自定义控件,该控件可以通过拖放轻松添加到我的项目中。我听说 .NET CF 2.0 将使我们摆脱这个麻烦。
历史
- 版本 1.0 -
SignalBar
控件的初始发布。