翻译|其它|编辑:郝浩|2007-04-10 14:34:16.000|阅读 1420 次
概述:
# 界面/图表报表/文档/IDE等千款热门软控件火热销售中 >>
好主意不会过时。很久以前,Paul DiLascia 在《Microsoft Systems Journal》中演示了一个漂亮的技巧:在图片上显示悬浮的上下文相关工具提示。当用户在图片上移动鼠标时,工具提示控件更新了工具提示的文本,显示出所指向的人物的姓名。
工具提示控件如何做到如此智能化,以至能够支持非矩形区域,例如,丝毫不差地辨别出图像中人物的自然形态?图像中的热点并非新技术,但这通常是通过常用的、易于描述的形状(例如矩形、圆形,或许还有多边形)来实现的。
Paul 解决这一问题的方法十分高明:加载图像的两个副本,即原始图像和热点映射。原始照片显示在一个图片框组件中,而映射图像则被隐藏起来,用于将原始图像的每个像素都映射为一种特定的颜色。除了用颜色填充每个热点区域外,映射图像与原始图像几乎一模一样。这样,每个热点区域便与一种唯一的颜色相对应,而每种唯一的颜色都可以链接到一个工具提示文本。在工具提示弹出之前,鼠标下面的像素将映射到热点映射中的相应像素。图像的隐藏副本中的颜色被映射到热点列表中,最后相关的文本弹出。
在本月这期“最先进的技术”中,我将把 Paul 的技巧应用于标准的 Windows® Forms PictureBox 控件中,以便当用户单击特定区域时,会显示上下文相关工具提示,并引发适当的事件。这样,您可以使用现实中的图片轻松实现可单击的映射,并毫不费力地随意进行缩放。
技巧揭秘
图 1 中左侧的照片是向用户显示的;右侧的照片则在内部用于快速确定所显示的图像上任何被单击的像素是否属于热点区域。在本照片中热点区域显示为红色、石灰绿色、青色、洋红色和黄色。我有意选择了一张现实中的图片来帮助解说。而您使用几乎任何图像都可以创建出内部带上下文相关区域的映射。
图 1 用户看到的图像和带热点的底层图像
让我们简要考察一下一种常见的情况。您的用户想单击一幅世界地图,以选择一片特定的区域并查看销售数字。如何检测到对各个区域的单击?您可以将地图划分为若干个由一系列点构成的多边形。获得了区域后,Microsoft® .NET Framework 中的图形 API 即可以告诉您一个点是否属于该区域。这是 ASP.NET 应用程序中的常用方法,在这种程序中,ImageMap 控件使其实现起来相对地不费什么力。然而,如果最终的位图大小发生了某种变化,您就会遇到麻烦。当位图大小发生变化时,您需要重新计算所有区域的所有点。应用缩放系数可能会有所帮助,但如果有更灵活的解决方案则更为理想。
使用第二个图像有两个关键性的好处。首先,您可以更细致地控制每个区域的边界。其次,区域的编辑工作微不足道 — 只需使用简单的图形编辑器(如 Microsoft Paint)给适当的区域着色(请参阅图 2)。
图 2 世界地图上定义的着色热点
使用两个图像所消耗的额外资源可以忽略不计,尤其是在 Windows Forms 应用程序环境中。但在 ASP.NET 中,由于加载双图像而导致的内存消耗是个较大的问题。幸运的是,可将解决方案构建为对图像只进行一次缓存,并且可在多个请求和用户间共享。
Windows Forms PictureBox 控件可以用多种不同的格式显示从不同的来源加载的图像,包括以前加载的 Image 对象、位于已知 URL 的远程图像或驻留在磁盘上的图像。在 .NET Framework 2.0 中,对 PictureBox 进行了进一步的强化,可以在控件加载主图像的过程中显示一个临时的图片,并在所选图像不可用时显示一个出错图像。
在 .NET Framework 1.x 中,当用户单击图像时,控件会引发 Click 事件,但所做的只是发送有关用户所进行的操作的通知;不会提供鼠标位置等其他信息。该事件的事件处理程序会收到基本的 EventArgs:
Sub PictureBox1_Click(ByVal sender As Object, _
ByVal e As EventArgs) _
Handles PictureBox1.Click
...
End Sub
在 Windows Forms 2.0 中,您可以利用新的 MouseClick 事件,该事件为您提供单击发生时的鼠标位置:
Sub PictureBox1_MouseClick(ByVal sender As Object, _
ByVal e As MouseEventArgs) _
Handles PictureBox1.MouseClick
...
End Sub
同样,如果要根据鼠标下面的那部分图像来显示上下文相关工具提示,您可以从宿主窗体内处理 MouseMove 事件,并处理位置及确定采取什么行动。
构建更丰富的 PictureBox 控件
我在本专栏文章中要实现的 PictureBox 控件将应对这两个问题。它接受热点区域集合,如果用户单击任何这类区域或在上面移动,则会引发工作区事件。总体编程接口模仿 ASP.NET 2.0 中的 ImageMap 控件的编程接口。其主要差别是,PictureBox 控件基于颜色而非点来定义热点区域。新的 PictureBox 控件派生自 Windows.Forms.PictureBox 内置控件,具有两个基本属性:HotSpots 和 MapImage(请参阅图 3)。
MapImage 属性为 Image 类型,表示所显示的图片的附带图像,其中的热点区域使用众所周知的调色板进行绘制。Image 包装在一个 Bitmap 对象(标准的 GDI+ 对象)中,该对象公开获取特定像素颜色的方法。
请看图 2 中的地图。PictureBox 的 Image 属性将绑定到最上层的图像;新的 MapImage 属性将与底层的图像相关联。这两个图像必须符合几点要求。首先,图像大小必须相同。其次,附带的图像必须使用无丢失的格式(例如 BMP,或许还有 PNG)来保存。应避免使用 JPEG 图像,因为这种图像在压缩中使用近似颜色。如果热点区域中某些像素的颜色被改动,则整个点检测机制便会被破坏。对于 GIF 文件,只要您用来标记热点区域的颜色不超出调色板的范围,则使用这种文件不会有问题。否则,便会与 JPEG 图像一样有使用近似颜色的风险。所显示图像的格式不限。
Windows Forms PictureBox 控件具有 SizeMode 属性,用于指示图像的显示方式。该属性的有效值来自 PictureBoxSizeMode 枚举。默认值为 Normal,表示图像从控件的左上角开始呈现,并且图像中任何超出 PictureBox 的区域的部分都将被剪切掉。而另一方面,StretchImage 值则使所显示的图像根据 PictureBox 的大小来伸缩。鉴于扩展的 PictureBox 控件的预期行为,这会引起严重问题:附带的图像也应该按照同样的比例来调整大小。可以使用 GDI+ 类以编程的方式实现这一点。例如,您可以使用 Image 类上的 GetThumbnailImage 方法,或者更好的做法是,使用 Graphics 类上的 DrawImage 方法。要计算比例,可以将 PictureBox 的大小与图像的大小加以比较。需要注意的是,在此情况下有充分的理由需要避免使用 GetThumbnailImage。与原始图像相比,它的保真度可能很低,而在某些罕见的情况下,它实际上与原图大相径庭。虽然出现这种情况的可能性很小,但判别起来十分困难。如果大小模式不是 Normal,则示例 PictureBox 控件不会提供其他功能。如果这不能满足您的需要,您可以增加示例代码,该代码可以从 MSDN®Magazine 网站下载。
要即刻发现 SizeMode 属性中任何值的变化,并实现其他功能,您需要处理控件中的 SizeModeChanged 事件:
Protected Overrides Sub OnSizeModeChanged(ByVal e As EventArgs)
MyBase.OnSizeModeChanged(e)
m_beSmart = (Me.SizeMode = PictureBoxSizeMode.Normal)
End Sub
一个内部私有成员将会跟踪 PictureBox 的工作模式。如果 SizeMode 属性不是 Normal,则该成员(即前面的代码片段中的 m_beSmart 变量)会返回 false。
定义热点区域
HotSpots 是新的 PictureBox 控件的第二个关键属性。它是自定义类型的集合,如图 4 所示。
HotSpots 属于 HotSpotElementCollection 类型,该类型是取自 HotSpotElement 类型所构成的 Collection (Of T) 类型的泛型类型。HotSpotElement 类定义映射图像中有意义的颜色区域。在此环境中,热点区域用唯一的数字 ID、RGB 颜色、标题和说明来标识。颜色用于对区域进行唯一标识。如果您希望捕获用户的鼠标单击操作,则数字 ID 提供一个数值来对工作区进行测试,并确定采取什么行动。可能仅颜色信息便足以唯一标识所单击的区域;从这一角度看,ID 信息是冗余的。然而,在具有丰富的数据绑定的情况下,如果您想将映射上的区域与其在后端数据库中的 ID 相关联,则 ID 信息可能比颜色更为有用。因此,颜色信息没有与区域信息结合。
标题和说明也扮演了另外一种角色:它们可用于上下文相关工具提示,当用户在所显示的图片上移动鼠标时,即会弹出这些提示。
您可以从 Visual Studio® 2005 设计器中以编程或声明的方式填充 HotSpots 属性。令人高兴的是,Visual Studio 2005 可以自动识别集合属性并将其绑定到内置的集合编辑器(请参阅图 5)。
图 5 编辑 Hotspots 集合属性
默认的集合编辑器将添加集合成员,并使其与相应成员的 ToString 方法所产生的任何文本一起显示。在最右侧的网格中,您可以看到该成员类型的所有属性。每个属性都和在 Visual Studio 2005 的父属性网格中一样,都是可编辑的。
如果您熟悉 ASP.NET 控件开发,您就会知道,通过设计器进行的更改不会保存在内含代码文件 designer.vb 或 designer.cs 中。如果不相信,您可以自己试试。将新的 PictureBox 控件放在一个窗体上,并填充 HotSpots 集合。完成后,保存并启动该窗体。PictureBox 控件的行为显示集合是空的,并且 designer.vb 文件或该窗体没有保存热点元素。这是怎么回事?
问题的答案是,您需要为 HotSpots 集合属性指定特定的设计器序列化策略。一般而言,Windows Forms 和 ASP.NET 自定义控件中的任何集合属性都需要进行这一操作。集合属性中的以下属性可解决这一问题:
<DesignerSerializationVisibility( _
DesignerSerializationVisibility.Content)>
如果没有该属性,在设计时对集合进行的任何更改都不会保存在 Windows Forms 的 designer.vb 文件中或 ASP.NET 的 ASPX 源页面中。
HotSpotElementCollection 类型继承自泛型 Collection 类型,并通过两个发现程序方法(ContainsColor 和 FindHotSpot)对其进行了扩展。两者都以颜色作为其输入,并根据其发现,分别返回布尔值或 HotSpotElement 对象。(请看图 4。)FindHotSpot 还会特别返回与指定颜色相匹配的 HotSpotElement(如果有)。
此外,HotSpotElementCollection 类替换了 InsertItem 和 SetItem 两种方法,以确保颜色不会与集合中的某项内容发生冲突。
现在已经具备了对图像热点区域的全面支持,接下来让我们为宿主窗体实现两个事件。我将定义 HotSpotFound 和 HotSpotClicked 两个事件。鼠标在热点上移动时即会引发 HotSpotFound,而用户单击热点时则会引发 HotSpotClicked。
添加必要的事件处理
HotSpotFound 事件通过 EventHandler 类型的新的泛型版本来声明,具体如下:
Public Event HotSpotFound As EventHandler(Of HotSpotEventArgs)
您在前面的图 4 中已经看到了 HotSpotEventArgs 数据结构。该结构通过 HotSpot 和 CancelTooltip 两个属性对 EventArgs 基类进行了基本扩展。HotSpot 属性引用已发现或单击的热点区域。CancelTooltip 属性指示是否应在鼠标移动时取消相应的工具提示。该属性将就热点显示的工具提示对工作区代码发出最终的指令。通过处理该事件,工作区窗体可以通过编程方式更改甚至取消工具提示。
HotSpotClicked 事件的架构与此类似。
Public Event HotSpotClicked As EventHandler(Of HotSpotEventArgs)
不过,在此例中,当事件处理程序返回时,PictureBox 控件不使用 CancelTooltip 属性。无论您在事件处理程序中给该属性指定了什么值,PictureBox 控件都会干脆地加以忽略。
在实例化时,PictureBox 控件会为 MouseClick 事件注册其自己的处理程序,如图 3 所示。内部处理程序 OnClickInternal 如图 6 所示。
当单击 PictureBox 控件的工作区中的一点时,该控件会从事件数据结构收到该点的工作区坐标(即 MouseEventArgs 类)。
下一步是找出指定位置的像素的颜色。需要注意的是,即使用户单击图像边界以外的区域也会引发内部 MouseClick 事件。您需要捕捉这些单击,并且在不调用 Bitmap 上 GetPixel 的情况下返回。如果您用图像大小以外的坐标来调用 GetPixel,则会引发异常。GetPixel 是 Bitmap 类上的一个方法,它返回特定位置的像素的颜色。然后,您可以选取该颜色,并查找与该颜色相关联的区域。如果找到任何区域,则会返回相应的 HotSpotElement 对象:
Dim elem As HotSpotElement = HotSpots.FindHotSpot(clr)
您可以在热点集合上使用 FindHotSpot 方法来检索与所找到的区域有关的信息。
但是,如果您的图像所自然包含的像素的颜色与某一热点颜色相同,该怎么办?如果用户恰好将鼠标移动到了该像素上,则会发生冲突。为避免这一问题,您最好能为热点区域选择图像的其他部分不使用的颜色。由于可供使用的颜色超过 1600 万种,因此,找到一种不常见的颜色尽管可能,但并非易事。不过,鉴于带颜色的像素在实际图像中的分布方式,尽管可能在某些部分的某一像素中发现您的颜色,但也许不会构成问题。
更好的解决方案可以采取为图像的所有未进行区域定义的部分保留一种颜色(或许为白色或透明)的做法。这样,除热点区域外,您的颜色可统一应用于所有底层图像中。保留的颜色可以作为公共属性进行公开,以便开发人员可以自行选用。
概括
PictureBox 控件允许您定义所显示的图像中的热点区域。在图像的副本(映射图像)上,每个区域都用一种不同的颜色(可能是图像其余部分未使用的颜色)进行绘制。将 PictureBox 控件放在窗体上之后,您可以设置映射图像并填充热点集合。热点集合向控件指示图像中使用的“热点”颜色。默认情况下,用户单击热点区域会引发 HotSpotClicked 事件。以下是一个典型的事件处理程序:
Sub PictureBox1_HotSpotClicked(ByVal sender As Object, _
ByVal e As HotSpotEventArgs) _
Handles PictureBox1.HotSpotClicked
MessageBox.Show(e.HotSpot.Description)
End Sub
如果启用 PictureBox 对鼠标移动的捕获功能,则它会为 MouseMove 事件注册一个内部处理程序。引发该事件后,控件将提供工具提示。工具提示的标题和文本通常反映热点元素中设置的值。不过,可以在工作区处理程序中更改这些参数。如前所述,也可以取消工具提示:
RaiseEvent HotSpotFound(Me, args)
If Not args.CancelTooltip Then
m_tooltip.ToolTipTitle = args.HotSpot.Title
m_tooltip.SetToolTip(Me, args.HotSpot.Description)
Else
HideTooltip()
End If
图 7 为操作中的控件及其显示的标准工具提示。工具提示的标题可通过编程方式更改,热点区域的说明也显示在窗体的状态条上:
Sub PictureBox1_HotSpotFound(ByVal sender As Object, _
ByVal e As HotSpotEventArgs) _
Handles PictureBox1.HotSpotFound
Info.Text = e.HotSpot.Description
e.HotSpot.Title = "EMEA"
End Sub
图 7 地图上显示的上下文相关工具提示
数据绑定支持
内置的 PictureBox 控件具有一个名为 Image 的可绑定属性。由于添加了 HotSpots 集合,因此需要更强的数据绑定形式。例如,如果可以从数据库填充集合,则是最理想的,因为,在一种颜色或一个说明在某个时候发生更改的情况下,这会为开发人员省去编辑源文件的时间。您可能需要添加一个 DataSource 属性,并将其内容映射到 HotSpots 集合,或将 HotSpots 集合转换为可以通过编程方式设置为对象的读/写属性。如果您选择传统的 DataSource 属性,您还需要定义一些映射程序属性,以指示绑定的数据源上的哪些字段映射到热点元素的可绑定属性。例如,您可能想让 DataDescriptionField、DataTitleField、DataColorField、DataValueField 属性将数据源上的字段映射到热点元素。对于 DataColorField,您负责编写一个转换算法,以便根据绑定的数据源中存储的字符串或数字生成一种 .NET Framework 颜色类型。这可以通过使用 System.Drawing 命名空间中的 ColorConverter 类而轻松实现。
PictureBox 控件的可绑定 Image 属性用 Bindable 属性来标记,并被添加到 DataBindings 集合中。您可能想将该行为也扩展到 MapImage 属性。为此,您可以在控件的源代码中将 Bindable 属性添加到 MapImage 属性:
<Bindable(True)> _
Public Property MapImage() As Image
...
End Property
一个 ASP.NET ImageMap 控件
我讨论的是 Windows Forms 应用程序环境中的上下文相关图片框控件;但也可以很方便地从 Image 或 ImageMap 控件构建一个类似的 ASP.NET 控件。
在 ASP.NET 中,ImageMap 控件提供的行为与本专栏文章中通篇说明的行为类似,并定义了一系列 HotSpot 区域。当用户单击一个热点时,控件会回发或导航到指定的 URL。ASP.NET 框架带有一组预定义的 HotSpot 对象,包括 CircleHotSpot、RectangleHotSpot 和 PolygonHotSpot 类。另外,您可以从抽象的 HotSpot 类中进行派生,以定义自己的自定义热点对象;在与此相类似的其他情况下,您会发现这些自定义热点对象很有用。
请将您的疑问和意见通过 cutting@microsoft.com 发送给 Dino。
Dino Esposito 是 Solid Quality Learning 的顾问,并且是《Programming Microsoft ASP.NET 2.0》(Microsoft Press, 2005)(英文)的作者。Dino 居住在意大利,经常就世界范围的行业事件发表评论。可通过 cutting@microsoft.com 或者加入博客 weblogs.asp.net/despos 与 Dino 联系。
本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com