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

用于带有标注的图像的自定义控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.44/5 (9投票s)

2003年10月7日

7分钟阅读

viewsIcon

166443

downloadIcon

961

一篇关于自定义控件的文章,用于显示带有注释功能的图像。

引言

在图像处理中,经常需要在图像上显示叠加图形,例如显示一个分割区域,但.NET本身并未提供此功能。我决定创建一个自定义控件,可以轻松地集成到其他项目中,并提供一些基本的图像功能,如缩放、平移以及注释的显示和创建。结合我稍后将介绍的图像处理库,这应该为任何处理图像处理的应用程序提供基础构建块。

注释类

我在这里发布的第一个尝试有点仓促,并不令人满意。事实上,对象层次结构不合乎逻辑,集合也不是类型安全的。此外,动画也不是无闪烁的。我决定使用一个清晰的面向对象方法来重构整个项目,并提出了以下方案:

  • 类 AnnotatedImage

    该控件用于保存绘制注释的位图,以及绘制该位图的属性和方法。这是要集成到其他项目中的控件。

    • ImageSize
    • DisplaySize
    • DisplayOffset
    • DisplayZoom
    • Bitmap
    • 注解
    • MaximumSize
    • PixelAveragingWindowSize
    • DrawErasableRectangle
    • DrawErasableLine
    • SaveImage
    • LoadImage
  • 类 Annotations

    包含所有注释代码及其所有集合的类。

    • 类 Container

      用于包含注释的类。

    • 类 ContainerCollection

      定义 Container 强类型集合的类。

    • 类 Annotation

      用于派生更具体的注释类的抽象类。

      • RegionAddMode
      • Color
      • Visible
      • 文本
      • Points
      • 区域
      • BorderRegion (Readonly)
      • AddPoint
      • RemovePoint
      • ClearPoints
    • 类 AnnotationCollection

      定义注释强类型集合的类。

    • 类 AnnotationRegion

      表示由构成区域的点组成的注释的类。

    • 类 PointBasedAnnotation

      用于派生更具体的基于点的注释类的抽象类。

      • Annotation 属性和方法
      • LinesVisible
      • PointsVisible
      • MaximumNumberOfPoints
    • 类 AnnotationLoose

      表示由不构成区域的散点组成的注释的类。

    • 类 AnnotationBorder

      表示由构成封闭轮廓的点组成并因此通过形成边框来定义区域的注释的类。

      • PointBasedAnnotation 属性和方法
      • AddRectangle

AnnotatedImageAnnotations 类旨在协同工作,并且它们之间相互引用。通过实例化 AnnotatedImage,您将获得以下层次结构:

  • AnnotatedImage
    • 注解
      • Containers (类型安全集合)
        • 容器
          • Annotations (类型安全集合)
            • 注释

绘制注释的所有代码都位于 Annotations 类中,除了两个用于 XOR 绘制的例程,它们存储在 AnnotatedImage 中。Annotations 类会拦截 AnnotatedImage 控件的 Paint 事件,以便重新绘制每个注释 Container 对象,而每个 Container 对象又会调用每个单独 Annotation 对象的 'Draw' 方法。AnnotatedImage 控件实现了双缓冲,以确保平滑绘制。

所有与注释相关的事件都在 Annotations 类中处理,例如交互式绘图;所有与图像相关的事件都在 AnnotatedImage 中处理,例如缩放。这意味着像鼠标单击这样的 Windows 事件将触发多个事件处理程序,这些处理程序将决定它们是否应对其进行处理。

使用代码

代码将编译成一个 DLL,可以将其添加到 Visual Studio 工具箱中,然后作为控件包含在任何应用程序中。在公开的所有事件中,Resize 是唯一必须处理的事件,因为正是通过此事件,父控件才能调整自身大小以适应自定义控件大小的变化,例如在缩放后。自定义控件的大小通常等于显示图像的大小,除非它超过 MaximumSize(对于非常大的图像或较大的 displayZoom,请不要忘记设置此属性!)。在这种情况下,仅显示图像的一部分,并且可以通过拖动图像(使用鼠标中键)来激活平移。此外,该控件响应鼠标滚轮以及 z 和 shift-z 进行缩放,w 和 shift-w 用于设置平均窗口大小,该大小可用于向用户提供有关光标周围区域颜色的反馈。此平均窗口也会在控件上绘制。

解释和注意事项

起初,我尝试从 PictureBox 类派生我的自定义控件,但我通过重写 Paint 方法未能绘制任何图形到可以分配给 PictureBox.Bitmap 的位图上。事实上,位图似乎是在我执行任何自定义绘图之后才绘制的(这很令人费解)。直接在位图上绘制当然不是一个选项,因为它会损坏位图。因此,我选择从 control 类派生(scrollablecontainer 效果也不好)。我尝试添加水平和垂直滚动条,但它们无法自行渲染,而且我找不到手动在控件上绘制它们的简单方法。所以,取而代之的是,我实现了通过鼠标中键拖动来实现平移,当图像无法完全适应自定义控件时,此功能就会激活。

注释是通过 3 个额外的类实现的:一个 Annotations 顶层对象,一个注释 Container 和一个 Annotation 对象。最后一个对象实际上是一个抽象类,真正可实例化的类是 AnnotationLooseAnnotationBorderAnnotationRegion。我避开了 GraphicsPath 对象,因为它不太适合我的目的,尽管它有时会在内部用于计算区域。每个 AnnotatedImage 图像控件都有一个 Annotations 对象,该对象可以包含多个 Container,每个 Container 又可以包含多个 Annotation 对象。这些对象可以是不同的子类型。请注意,这些对象不应与绘制在背景位图上的矢量图形对象混淆,它们是概念性的,只代表图像中的某些划分或区域。正是出于这个原因,这些注释没有内在的线宽,并且由 1 像素宽度的线表示,无论图像的缩放因子如何(因此我没有使用 .NET 中包含的坐标变换,因为它们也会变换线宽和字体。这也是 GraphicsPath 不适合的原因之一!)使用 3 个对象而不是 2 个的原因是,这样做可以将注释在逻辑上分组。此外,还可以合并 Container 中的注释来创建一个区域,该区域基于各个注释的 RegionAddMode 属性。这使得创建多个复杂区域变得容易,并且可以使用这些区域,例如用于进一步的图像处理。举个例子,我在工作中将其用于计算皮肤的临床图像的颜色差异,其中包含“正常皮肤”和“病变”容器。由于皮肤病变有时会出现斑驳的外观,即内部有正常皮肤的斑块,因此采用 2 个对象的方法将无法实现这一点。

可以通过将注释添加到注释 Container 来将其添加到图像,然后将此 Container 添加到顶层对象 Annotations

   Dim objAnnSet As Annotations.ContainerCollection = _
                            AnnotatedImage.Annotations.Containers
   Dim objAnnC1 As New Annotations.Container("Normal")

   objAnnSet.Add(objAnnC1)

   Dim objAnn As New Annotations.AnnotationBorder("Annotation 1", objColor)
   objAnnC1.Annotations.Add(objAnn)
   objAnn.PointsVisible = False

   'Set rectangle
   Dim objrect as new rectangle
   ...
   'Add to annotation
   objAnn.AddRectangle(objrect)

   'Draw it
   AnnotatedImage.Refresh()

与第一个版本不同,所有集合现在都是类型安全的,对于那些对此感兴趣的人,可以查看以下代码片段:

Public Class ContainerCollection
   Inherits NameObjectCollectionBase

   Public Sub New()
   End Sub 'New

   Default Public ReadOnly Property Item(ByVal key As String) As Container
     Get
       Return CType(Me.BaseGet(key), Container)
     End Get
   End Property

   Default Public ReadOnly Property Item(ByVal index As Integer) As Container
     Get
       Return CType(Me.BaseGet(index), Container)
     End Get
   End Property

   Public Sub Add(ByVal value As Container)
     'Key must be unique, so we remove existing entry
     If Me.KeyExists(value.Key) Then Me.BaseRemove(value.Key)
     Me.BaseAdd(value.Key, value)
   End Sub

   Public Function KeyExists(ByVal key As String) As Boolean
     If Me.Item(key) Is Nothing Then
       Return False
     Else
       Return True
     End If
   End Function

   Public Overloads Sub Remove(ByVal key As String)
     Me.BaseRemove(key)
   End Sub

   Public Overloads Sub Remove(ByVal index As Integer)
     Me.BaseRemoveAt(index)
   End Sub

   Public Sub Clear()
     Me.BaseClear()
   End Sub

   Public Shadows Function GetEnumerator() As ContainerCollectionEnumerator
     Return New ContainerCollectionEnumerator(Me)
   End Function

   Public Class ContainerCollectionEnumerator
     ' This overriding of IEnumerator allows for each to be used
     Implements IEnumerator

     Private objContainerCollection As ContainerCollection
     Private index As Integer = -1

     Public Sub New(ByVal objContainerCollection As ContainerCollection)
       Me.objContainerCollection = objContainerCollection
     End Sub

     Public ReadOnly Property Current() As Object Implements IEnumerator.Current
       Get
         Return CType(Me.objContainerCollection.Item(index), Container)
       End Get
     End Property

     Public Function MoveNext() As Boolean Implements IEnumerator.MoveNext
       If index < objContainerCollection.Count - 1 Then
         index += 1
         Return True
       Else
         Return False
       End If
     End Function

     Public Sub Reset() Implements IEnumerator.Reset
       index = -1
     End Sub
   End Class

End Class

为了提供平滑的重绘,通过设置 AnnotatedImage 控件的一些样式属性来激活了双缓冲。

setstyle(ControlStyles.UserPaint, True), 
     setstyle(ControlStyles.AllPaintingInWmPaint, 
     True),setstyle(ControlStyles.DoubleBuffer, True)

这里重要的点是,您必须在各个注释的绘图例程中使用 AnnotatedImage 绘制事件提供的 graphics 对象。事实上,使用双缓冲时,此对象实际上指向一个内存缓冲区,该缓冲区在绘制完成后选择性地复制到控件的客户端区域。这也意味着尝试无效控件的有限区域以加快重绘速度是不合适的,因为在绘制它们(并将它们添加到已绘制注释的边界框)之前,我们需要知道要绘制的注释的边界框。这很繁琐,并且会产生大量额外的代码,但带来的收益却很少或没有,因为双缓冲已经为我们完成了一部分工作,而且速度非常快。

评论、待办事项、错误

该组件已用于一个专门用于 sRGB 图像查看的用户控件中,该用户控件又用于一个使用校准的 sRGB 图像测量皮肤病变的应用程序中。您可以在 这里 找到它。到目前为止,与使用 VB6 和 leadtools 11 的先前版本相比,此应用程序非常稳定。

待办事项主要包括将注释序列化到文件(可能使用 XML),以及可能添加使用密码锁定注释的功能(在医疗环境中很重要)。

© . All rights reserved.