Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.Windows
Imports ClassesYVDH.Annotations
Public Class AnnotatedImage
'Class that permits rendering of an image and overlayed annotations
'It will size itself and parent controls must implement the Resize event
'to adjust their size accordingly ...
'No IO is provided, just set the internal bitmap.
'This class does not do any computations or anything, it just does the drawing.
Inherits System.Windows.Forms.Control
'Class variables, mirrored by a property
Private objBitmap As Bitmap, oDefaultCursor As Cursor
Private objMaximimumSize As New Size(800, 600), iWindowSize As Integer
Private objMousePos As New Point(0, 0), bDirty As Boolean, bBusy As Boolean
Private sDisplayZoom As Single, ptDisplayOffset As Point
Private WithEvents objAnnotations As Annotations
'Public events
Public Event DisplayZoomChanged(ByVal sDisplayZoom As Single)
Public Event PixelAveragingWindowSizeChanged(ByVal iWindowSize As Integer)
Public Event BitmapChanged(ByVal objBitmap As Bitmap)
'Public events handled in the Annotations class, and passed on too!
Public Event AnnotationCreationDone(ByVal objBitmap As Bitmap, ByVal objAnnotation As Annotation)
Public Event AnnotationCreationCanceled(ByVal objAnnotation As Annotation)
Public Event AnnotationCreationPointAdded(ByVal objAnnotation As Annotation)
Public Event AnnotationCreationPointRemoved(ByVal objAnnotation As Annotation)
Public Event AnnotationCreationMouseMove(ByVal e As System.Windows.Forms.MouseEventArgs, ByVal objAnnotation As Annotation)
Public Event AnnotationClick(ByVal e As System.Windows.Forms.MouseEventArgs, ByVal objAnnotation As Annotation)
Public Event AnnotationHover(ByVal e As System.Windows.Forms.MouseEventArgs, ByVal objAnnotation As Annotation)
#Region " Component Designer generated code "
Public Sub New()
MyBase.New()
objAnnotations = New Annotations(Me)
' This call is required by the Component Designer.
InitializeComponent()
setstyle(ControlStyles.UserPaint, True)
setstyle(ControlStyles.AllPaintingInWmPaint, True)
setstyle(ControlStyles.DoubleBuffer, True)
End Sub
'Control overrides dispose to clean up the component list.
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
If disposing Then
If Not (components Is Nothing) Then
components.Dispose()
End If
End If
MyBase.Dispose(disposing)
End Sub
Protected Overrides Sub Finalize()
Me.Bitmap = Nothing
End Sub
'Required by the Control Designer
Private components As System.ComponentModel.IContainer
' NOTE: The following procedure is required by the Component Designer
' It can be modified using the Component Designer. Do not modify it
' using the code editor.
<System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
'
'AnnotatedImage
'
End Sub
#End Region
#Region "Properties"
Public Property Busy() As Boolean
Get
Return bBusy
End Get
Set(ByVal Value As Boolean)
bBusy = Value
If bBusy Then
Cursor = System.Windows.Forms.Cursors.WaitCursor
Else
Cursor = System.Windows.Forms.Cursors.Arrow
End If
End Set
End Property
Public ReadOnly Property ImageSize() As Size
Get
Return New Size(Me.Bitmap.Width, Me.Bitmap.Height)
End Get
End Property
Public ReadOnly Property DisplaySize() As Size
Get
Dim objBitmap As Bitmap = Me.Bitmap
If Not objBitmap Is Nothing Then
Dim sZoom As Single = Me.DisplayZoom
Return New Size(objBitmap.Width * sZoom, objBitmap.Height * sZoom)
Else
Return New Size(0, 0)
End If
End Get
End Property
Public ReadOnly Property Annotations() As Annotations
Get
Return Me.objAnnotations
End Get
End Property
Public Property MaximimumSize() As Size
Get
Return Me.objMaximimumSize
End Get
Set(ByVal Value As Size)
Me.objMaximimumSize = Value
End Set
End Property
Public Property DisplayOffset() As Point
Get
Return Me.ptDisplayOffset
End Get
Set(ByVal Value As Point)
Debug.WriteLine("Set display offset at " & Value.ToString(), "AnnotatedImage.DisplayOffset")
If Value.X < 0 Then Value.X = 0
If Value.Y < 0 Then Value.Y = 0
Dim iDeltaX As Integer = CInt((DisplaySize.Width - Me.Width) / Me.DisplayZoom)
Dim iDeltaY As Integer = CInt((DisplaySize.Height - Me.Height) / Me.DisplayZoom)
If Value.X > iDeltaX Then Value.X = iDeltaX
If Value.Y > iDeltaY Then Value.Y = iDeltaY
Me.ptDisplayOffset = Value
Me.Refresh()
End Set
End Property
Public Property DisplayZoom() As Single
Get
Return Me.sDisplayZoom
End Get
Set(ByVal Value As Single)
If Math.Abs(Value - 1.0) < 0.001 Then Value = 1.0
Dim objOffsetScale As Single = Value / Me.sDisplayZoom
Me.sDisplayZoom = Value
Debug.WriteLine("Set display zoom at " & Value, "AnnotatedImage.DisplayZoom")
SizeControl()
'We have to check if we have to reset or scale the display offset
'It is actually setting the offset that triggers the redraw!
If DisplayImageBiggerThanCanvas() = False Then
Me.DisplayOffset = New Point(0, 0)
Else
Dim objOffset As Point = Me.DisplayOffset
objOffset.X *= objOffsetScale
objOffset.Y *= objOffsetScale
Me.DisplayOffset = objOffset
End If
RaiseEvent DisplayZoomChanged(Value)
End Set
End Property
Public Property Bitmap() As Bitmap
Get
Return Me.objBitmap
End Get
Set(ByVal Value As Bitmap)
Me.objBitmap = Value
'Clear all annotations
Me.Annotations.AnnotationContainers.Clear()
'Causes redraw
DisplayZoom = ComputeInitialDisplayZoom()
RaiseEvent BitmapChanged(Value)
End Set
End Property
Public Property PixelAveragingWindowSize() As Integer
Get
Return Me.iWindowSize
End Get
Set(ByVal Value As Integer)
Me.iWindowSize = Value
RaiseEvent PixelAveragingWindowSizeChanged(Value)
End Set
End Property
Private Property Dirty() As Boolean
'This indicates if XOR drawing has occured since last refresh, which is necessary for
'the routines drawing XOR (erasable) rectangles and lines...
Get
Return Me.bDirty
End Get
Set(ByVal Value As Boolean)
Me.bDirty = Value
End Set
End Property
#End Region
#Region "Public methods"
Public Sub DrawErasableRectangle(ByVal objRect As Rectangle)
'Draw an eraseble rectangle.
'Input is in client coordinates, static old rectangle is in screen coordinates!
'Checks that we do not draw outside client area
Static objOldRect As Rectangle
'Take a middle of the road color
Dim objColor As New Color
objColor = Color.Gray
'Only erase if required, e.g. not when control has been refreshed ...
If Me.Dirty = True And objOldRect.Width > 0 Then
ControlPaint.DrawReversibleFrame(objOldRect, objColor, FrameStyle.Dashed)
End If
'Make sure we are not outside the client area
objRect.Intersect(New Rectangle(0, 0, Me.Width, Me.Height))
objOldRect = Me.RectangleToScreen(objRect)
ControlPaint.DrawReversibleFrame(objOldRect, objColor, FrameStyle.Dashed)
Me.Dirty = True
End Sub
Public Sub DrawErasableLine(ByVal objPt1 As Point, ByVal objPt2 As Point)
'Draw an eraseble line.
'Pts are in client coordinates
'static old Pts are in screen coordinates!
Static objOldPt1 As Point
Static objOldPt2 As Point
'Take a middle of the road color
Dim objColor As New Color, objTempPt2 As Point
objColor = Color.Gray
'Avoid trying to erase a rectangle if canvas has just been redrawn!
If Me.Dirty Then
ControlPaint.DrawReversibleLine(objOldPt1, objOldPt2, objColor)
End If
'Convert to screen coordinates
objOldPt1 = Me.PointToScreen(objPt1)
objOldPt2 = Me.PointToScreen(objPt2)
ControlPaint.DrawReversibleLine(objOldPt1, objOldPt2, objColor)
Me.Dirty = True
End Sub
Public Overloads Sub SaveImage(ByVal strFileName As String, ByVal imgFormat As System.Drawing.Imaging.ImageFormat)
Me.Bitmap.Save(strFileName, imgFormat)
End Sub
Public Overloads Function LoadImage(ByVal strFileName As String) As Boolean
' Sets up an image object to be displayed.
Dim objBitmap As Bitmap = Bitmap.FromFile(strFileName), bSucces As Boolean = False
If objBitmap.PixelFormat = Drawing.Imaging.PixelFormat.Format24bppRgb Then
'Create an in-memory copy, and assign to custom control
Dim objMemoryBitmap As New Bitmap(objBitmap.Width, objBitmap.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb)
Dim g As Graphics = Graphics.FromImage(objMemoryBitmap)
g.DrawImage(objBitmap, New Rectangle(0, 0, objBitmap.Width, objBitmap.Height))
objMemoryBitmap.SetResolution(objBitmap.HorizontalResolution, objBitmap.VerticalResolution)
Me.Bitmap = objMemoryBitmap
bSucces = True
Else
MessageBox.Show("Error loading image: only 24-bit images are supported!", "sRGB Tool", MessageBoxButtons.OK, MessageBoxIcon.Error)
End If
'Dispose of original bitmap
objBitmap.Dispose()
'return if successful
Return bSucces
End Function
#End Region
#Region "Private methods"
Private Function DisplayImageBiggerThanCanvas() As Boolean
If Me.MaximimumSize.Width < Me.DisplaySize.Width Or Me.MaximimumSize.Height < Me.DisplaySize.Height Then
Return True
Else
Return False
End If
End Function
Private Function ComputeInitialDisplayZoom() As Single
'Compute the best startDisplayZoom. Alos set the scrollbars to 0
If Not Me.Bitmap Is Nothing Then
Dim sHorDisplayZoom As Single = MaximimumSize.Height / CSng(objBitmap.Height())
Dim sVertDisplayZoom As Single = MaximimumSize.Width / CSng(objBitmap.Width())
Return Math.Min(1, Math.Min(sHorDisplayZoom, sVertDisplayZoom))
Else
Return 1
End If
End Function
Private Sub SizeControl()
'We take care of sizing ourselves, based on the DisplayZoom
'We cannot exceed the screensize MaximimumSize
'Setting size in one call causes less events in parent!
Dim objBitmap = Me.Bitmap, objSize As New Size(800, 600)
If Not objBitmap Is Nothing Then
'Compute size of control
Dim iImageWidth As Integer = objBitmap.Width() * Me.DisplayZoom
Dim iImageHeight As Integer = objBitmap.Height() * Me.DisplayZoom
If iImageWidth > Me.MaximimumSize.Width Then
objSize.Width = Me.MaximimumSize.Width
Else
objSize.Width = iImageWidth
End If
If iImageHeight > Me.MaximimumSize.Height Then
objSize.Height = Me.MaximimumSize.Height
Else
objSize.Height = iImageHeight
End If
End If
Debug.WriteLine("Control size set to " & objSize.ToString(), "AnnotatedImage.SizeControl")
Me.Size = objSize
End Sub
Private Sub CheckAndAdjustBitmapRectangle(ByRef objRect As Rectangle)
If objRect.X < 0 Then
objRect.Width += objRect.X
objRect.X = 0
End If
If objRect.Y < 0 Then
objRect.Height += objRect.Y
objRect.Y = 0
End If
Dim fZoom As Single = Me.DisplayZoom
Dim objBitmap As Bitmap = Me.Bitmap
If objRect.Bottom >= (objBitmap.Height * fZoom) Then
objRect.Height = CInt(Math.Round((objBitmap.Height - objRect.Y - 1) * fZoom))
End If
If (objRect.Right >= (objBitmap.Width * fZoom)) Then
objRect.Width = CInt(Math.Round((objBitmap.Width - objRect.X - 1) * fZoom))
End If
End Sub
#End Region
#Region "Form event handlers"
Protected Overrides Sub OnPaint(ByVal pe As System.Windows.Forms.PaintEventArgs)
'Add your custom paint code here
If Not Me.Bitmap Is Nothing Then
'Paint the bitmap onto the control, taking the clipractangle into account
Dim sZoom As Single = Me.DisplayZoom
Dim srcRect As Rectangle = New Rectangle(Me.DisplayOffset.X + pe.ClipRectangle.X / sZoom, _
Me.DisplayOffset.Y + pe.ClipRectangle.Y / sZoom, _
Math.Ceiling(pe.ClipRectangle.Width / Me.DisplayZoom), _
Math.Ceiling(pe.ClipRectangle.Height / Me.DisplayZoom))
Debug.WriteLine("Redrawing bitmap in rectangle (" _
& srcRect.Left & "," & srcRect.Top & ")-(" _
& srcRect.Right & "," & srcRect.Bottom & ")", "AnnotatedImage.Paint")
pe.Graphics.DrawImage(Me.Bitmap, pe.ClipRectangle, srcRect, GraphicsUnit.Pixel)
End If
' This trigers the Paint event so that annotations get drawn!
MyBase.OnPaint(pe)
'Canvas is no longer dirty after full redraw
Me.Dirty = False
End Sub
Private Sub AnnotatedImage_KeyDown(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles MyBase.KeyDown
'Make sure we do not intercept modifiers that are in use by the annotations class!
Debug.WriteLine("Pressed key " & e.KeyCode, "AnnotatedImage.KeyDown")
If e.Shift Then
Select Case e.KeyCode
Case Keys.Z
'Zoom in
Me.DisplayZoom = Me.DisplayZoom * 1.25
Case Keys.W
'Decrease averaging window size
If PixelAveragingWindowSize > 1 Then
PixelAveragingWindowSize = PixelAveragingWindowSize - 2
End If
End Select
ElseIf e.Alt Then
ElseIf e.Control Then
Else
Select Case e.KeyCode
Case Keys.Z
'Zoom out
Me.DisplayZoom = Me.DisplayZoom / 1.25
Case Keys.W
'Increase averaging window size
PixelAveragingWindowSize = PixelAveragingWindowSize + 2
Case Keys.F5
'Cause refresh of client area
Me.Refresh()
End Select
End If
End Sub
Private Sub AnnotatedImage_MouseDown(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseDown
'One can always move the image. This is always with the middle mouse button
Debug.WriteLine("Mouse button " & e.Button & " at " & e.X & "," & e.Y, "AnnotatedImage.MouseDown")
If e.Button = MouseButtons.Right Then
If DisplayImageBiggerThanCanvas() Then
Me.Cursor = System.Windows.Forms.Cursors.SizeAll
objMousePos.X = e.X
objMousePos.Y = e.Y
End If
End If
End Sub
Private Sub AnnotatedImage_MouseWheel(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseWheel
Debug.WriteLine("Mouse wheel Delta " & e.Delta, "AnnotatedImage.MouseWheel")
If e.Delta > 0 Then
Me.DisplayZoom = Me.DisplayZoom / 1.25
Else
Me.DisplayZoom = Me.DisplayZoom * 1.25
End If
End Sub
Private Sub AnnotatedImage_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseMove
If Me.Bitmap Is Nothing Then Exit Sub
If e.Button = MouseButtons.Right Then
If DisplayImageBiggerThanCanvas() Then
'Adjust DisplayOffset
Dim objCurrentOffset As Point = Me.DisplayOffset
Me.DisplayOffset = New Point(objCurrentOffset.X + (objMousePos.X - e.X) / Me.DisplayZoom, _
objCurrentOffset.Y + (objMousePos.Y - e.Y) / Me.DisplayZoom)
objMousePos.X = e.X
objMousePos.Y = e.Y
End If
End If
End Sub
#End Region
#Region "Annotations events"
'These events are mostly passed on as is
Private Sub objAnnotations_AnnotationClick(ByVal e As System.Windows.Forms.MouseEventArgs, ByVal objAnnotation As ClassesYVDH.Annotations.Annotation) Handles objAnnotations.AnnotationClick
Debug.WriteLine("AnnotatedImage.AnnotationClick")
RaiseEvent AnnotationClick(e, objAnnotation)
End Sub
Private Sub objAnnotations_AnnotationHover(ByVal e As System.Windows.Forms.MouseEventArgs, ByVal objAnnotation As ClassesYVDH.Annotations.Annotation) Handles objAnnotations.AnnotationHover
RaiseEvent AnnotationHover(e, objAnnotation)
End Sub
'Annotation creation
Private Sub objAnnotations_AnnotationCreationCanceled(ByVal objAnnotation As ClassesYVDH.Annotations.Annotation) Handles objAnnotations.AnnotationCreationCanceled
Debug.WriteLine("AnnotatedImage.AnnotationCreationCanceled")
RaiseEvent AnnotationCreationCanceled(objAnnotation)
End Sub
Private Sub objAnnotations_AnnotationCreationDone(ByVal objAnnotation As ClassesYVDH.Annotations.Annotation) Handles objAnnotations.AnnotationCreationDone
Debug.WriteLine("AnnotatedImage.AnnotationCreationDone")
RaiseEvent AnnotationCreationDone(Me.Bitmap, objAnnotation)
End Sub
Private Sub objAnnotations_AnnotationCreationMouseMove(ByVal e As System.Windows.Forms.MouseEventArgs, ByVal objAnnotation As ClassesYVDH.Annotations.Annotation) Handles objAnnotations.AnnotationCreationMouseMove
RaiseEvent AnnotationCreationMouseMove(e, objAnnotation)
End Sub
Private Sub objAnnotations_AnnotationCreationPointAdded(ByVal objAnnotation As ClassesYVDH.Annotations.Annotation) Handles objAnnotations.AnnotationCreationPointAdded
Debug.WriteLine("AnnotatedImage.AnnotationCreationPointAdded")
RaiseEvent AnnotationCreationPointAdded(objAnnotation)
End Sub
Private Sub objAnnotations_AnnotationCreationPointRemoved(ByVal objAnnotation As ClassesYVDH.Annotations.Annotation) Handles objAnnotations.AnnotationCreationPointRemoved
Debug.WriteLine("AnnotatedImage.AnnotationCreationPointRemoved")
RaiseEvent AnnotationCreationPointRemoved(objAnnotation)
End Sub
#End Region
End Class