Overview


When posting images online, either in forums or in a presentation such as this, sometimes you want to highlight or draw the reader’s attention to part of the image. Recently I’ve needed to do this more often which led me to write this. It’s a balloonTip designer which saves the partially transparent balloonTip image to a file on your desktop (it’s always 001.png, so it’s easy to find in Explorer).




Figure 1.
Designer



Using the Designer tools, you can change the text, backcolor, and text color. The four corner buttons allow you to change the corner the arrow is attached to, and the sizing handles in the PictureBox allow some degree of resizing.
When you’ve designed your balloonTip, pressing F3 saves it to your desktop. To paste the transparent balloonTip into your screenshot in Paint you first need to set Selection Optionsà'Transparent Selection' and then use 'Paste From' and navigate to 001.png (see Figure 2) 





Figure 2.
Paste From (file)
 


The Code


These are the API functions and the Constant used to catch the F3 key being pressed.
clipRect is the Rectangle  used when copying the image from the screen.


Private Declare Function RegisterHotKey Lib "user32" (ByVal hwnd As IntPtr, _
    ByVal id As Integer, ByVal fsModifiers As Integer, ByVal vk As Integer) _
    As Integer

Private
Declare Function UnregisterHotKey Lib "user32" (ByVal hwnd As IntPtr, _
    ByVal id As Integer) As Integer 
Private Const WM_HOTKEY As Integer = &H312 
Dim clipRect As Rectangle

 


In the Form’ Load event, the Button images are set, the ComboBoxes are loaded, and the HotKey is setup.
 


Private Sub Form1_FormClosed(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosedEventArgs) Handles Me.FormClosed
    UnregisterHotKey(Me.Handle, 0)
End Sub
 
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    Dim img As New Bitmap("1.png")
    img.MakeTransparent(Color.Red)
    RadioButton3.Image = img
    Dim img2 As Bitmap = DirectCast(img.Clone, Bitmap)
    img2.RotateFlip(RotateFlipType.Rotate90FlipNone)
    RadioButton1.Image = img2
    Dim img3 As Bitmap = DirectCast(img.Clone, Bitmap)
    img3.RotateFlip(RotateFlipType.Rotate180FlipNone)
    RadioButton2.Image = img3
    Dim img4 As Bitmap = DirectCast(img.Clone, Bitmap)
    img4.RotateFlip(RotateFlipType.Rotate270FlipNone)
    RadioButton4.Image = img4
    ComboBox2.DataSource = New String() {"Red", "LimeGreen", "LightBlue", "Yellow"}
    ComboBox3.DataSource = New String() {"Black", "White"}
    RegisterHotKey(Me.Handle, 0, 0, Keys.F3)
    topSizer.Location = New Point(312, 137)
End Sub


 

These are the RadioButton Checked handlers, which cause a repaint and subsequently move the balloonTip pointer.

 
Private Sub RadioButton1_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles RadioButton1.CheckedChanged
    If RadioButton1.Checked Then
        pointerSizer.Location = New Point(110, 90)
        pointerSizer.BringToFront()
        PictureBox1.Invalidate()
    End If
End Sub
 
Private Sub RadioButton3_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles RadioButton3.CheckedChanged
    If RadioButton3.Checked Then
        pointerSizer.Location = New Point(110, 370)
        pointerSizer.BringToFront()
        PictureBox1.Invalidate()
    End If
End Sub

Private Sub RadioButton2_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles RadioButton2.CheckedChanged
    If RadioButton2.Checked Then
        pointerSizer.Location = New Point(587, 90)
        pointerSizer.BringToFront()
        PictureBox1.Invalidate()
    End If
End Sub

Private Sub RadioButton4_CheckedChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles RadioButton4.CheckedChanged
    If RadioButton4.Checked Then
        pointerSizer.Location = New Point(587, 370)
        pointerSizer.BringToFront()
        PictureBox1.Invalidate()
    End If
End Sub 




These two events cause a repaint when changed, changing the backcolor and the caption.
 
Private Sub ComboBox2_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ComboBox2.SelectedIndexChanged, ComboBox3.SelectedIndexChanged
    PictureBox1.Invalidate()
End Sub

Private Sub TextBox1_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TextBox1.TextChanged
    PictureBox1.Invalidate()
End Sub


 

The sizer handles around the design time balloonTip are pictureboxes. This is the code that enables resizing the balloonTip by dragging. 


Dim
cursorX, CursorY As Integer 
Private Sub H_Sizer_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles leftSizer.MouseMove, rightSizer.MouseMove
    Dim s As PictureBox = DirectCast(sender, PictureBox)
    If s.Capture Then
        ' Move the control according to mouse movement
        s.Left = (s.Left + e.X) - cursorX
        PictureBox1.Invalidate()
    End If
End Sub
 
Private Sub V_Sizer_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles bottomSizer.MouseMove, topSizer.MouseMove
    Dim s As PictureBox = DirectCast(sender, PictureBox)
    If s.Capture Then
        ' Move the control according to mouse movement
        s.Top = (s.Top + e.Y) - CursorY
        PictureBox1.Invalidate()
    End If
End Sub
 
Private Sub Sizers_MouseUp(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles rightSizer.MouseUp, leftSizer.MouseUp, topSizer.MouseUp, bottomSizer.MouseUp
    Cursor.Clip = Nothing
End Sub

Private Sub Sizers_Move(ByVal sender As Object, ByVal e As System.EventArgs) Handles leftSizer.Move, rightSizer.Move, topSizer.Move, bottomSizer.Move
    Dim s As PictureBox = DirectCast(sender, PictureBox)
    If s Is leftSizer OrElse s Is rightSizer Then
        bottomSizer.Left = leftSizer.Right + (((rightSizer.Left - leftSizer.Right) - bottomSizer.Width) \ 2)
        topSizer.Left = bottomSizer.Left
        bottomSizer.BringToFront()
        topSizer.BringToFront()
    ElseIf s Is topSizer OrElse s Is bottomSizer Then
        leftSizer.Top = topSizer.Bottom + (((bottomSizer.Top - topSizer.Bottom) - leftSizer.Height) \ 2)
        rightSizer.Top = leftSizer.Top
        leftSizer.BringToFront()
        rightSizer.BringToFront()
    End If
End Sub

Private Sub pointerSizer_MouseDown(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles pointerSizer.MouseDown
    cursorX = e.X
    CursorY = e.Y
End Sub
 
Private Sub pointerSizer_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles pointerSizer.MouseMove
    If pointerSizer.Capture Then
        ' Move the control according to mouse movement
        pointerSizer.Left = (pointerSizer.Left + e.X) - cursorX
        pointerSizer.Top = (pointerSizer.Top + e.Y) - CursorY
        PictureBox1.Invalidate()
    End If
End Sub


 

The Cursor.Clip property is used to restrict the sizer drag movement (except the pointerSizer which has more freedom of movement).

 
Private Sub leftSizer_MouseDown(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles leftSizer.MouseDown
    Cursor.Clip = New Rectangle(Me.Left + 115, leftSizer.PointToScreen(e.Location).Y, 130, 1)
    cursorX = e.X
End Sub
 
Private Sub rightSizer_MouseDown(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles rightSizer.MouseDown
    Cursor.Clip = New Rectangle(Me.Left + 450, rightSizer.PointToScreen(e.Location).Y, 130, 1)
    cursorX = e.X
End Sub
 
Private Sub topSizer_MouseDown(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles topSizer.MouseDown
    Cursor.Clip = New Rectangle(topSizer.PointToScreen(e.Location).X, Me.Top + 110, 1, 100)
    CursorY = e.Y
End Sub
 
Private Sub bottomSizer_MouseDown(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles bottomSizer.MouseDown
    Cursor.Clip = New Rectangle(topSizer.PointToScreen(e.Location).X, Me.Top + 320, 1, 100)
    CursorY = e.Y
End Sub

 


The PictureBox Paint event is fairly simple, calling two functions, both of which return a Drawing2D.GraphicsPath, then filling the areas returned with a solid color, before drawing the balloonTip caption centred in the balloonTip.

 
Private Sub PictureBox1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles PictureBox1.Paint
    e.Graphics.FillPath(New SolidBrush(Color.FromName(ComboBox2.Text)), pointer(leftSizer.Right - PictureBox1.Left + 12, topSizer.Bottom - PictureBox1.Top + 12, rightSizer.Left - leftSizer.Right - 24, _
                                                           bottomSizer.Top - topSizer.Bottom - 24))
    e.Graphics.FillPath(New SolidBrush(Color.FromName(ComboBox2.Text)), RoundedRectangle(leftSizer.Right - PictureBox1.Left + 12, topSizer.Bottom - PictureBox1.Top + 12, rightSizer.Left - leftSizer.Right - 24, _                                                            bottomSizer.Top - topSizer.Bottom - 24, 50))
    Dim boundingRectangle As New Rectangle(leftSizer.Right - PictureBox1.Left + 24, _
                                               topSizer.Bottom - PictureBox1.Top + 24, _
                                               rightSizer.Left - leftSizer.Right - 48, _
                                               bottomSizer.Top - topSizer.Bottom - 48) 
    Dim sf As New StringFormat
    sf.LineAlignment = StringAlignment.Center
    sf.Alignment = StringAlignment.Center 
    e.Graphics.DrawString(TextBox1.Text, TextBox1.Font, New SolidBrush(Color.FromName(ComboBox3.Text)),boundingRectangle, sf)
End Sub










These are the two GraphicsPath functions, pointer and RoundedRectangle.
 
Public Function pointer(ByVal m_intxAxis As Integer, _
                                ByVal m_intyAxis As Integer, _
                                ByVal m_intWidth As Integer, _
                                ByVal m_intHeight As Integer) As Drawing2D.GraphicsPath 
    Dim gp As New Drawing2D.GraphicsPath 
    Dim BaseRect As New RectangleF(m_intxAxis, m_intyAxis, m_intWidth, m_intHeight) 
    Select Case True
        Case RadioButton1.Checked 'topleft
            gp.AddPolygon(New Point() {New Point(pointerSizer.Right - PictureBox1.Left, pointerSizer.Bottom - PictureBox1.Top), New Point(CInt(BaseRect.Left + 20), CInt(BaseRect.Top + 10)), New Point(CInt(BaseRect.Left + 10), CInt(BaseRect.Top + 40)), New Point(pointerSizer.Right - PictureBox1.Left, pointerSizer.Bottom - PictureBox1.Top)})
        Case RadioButton2.Checked 'topright
            gp.AddPolygon(New Point() {New Point(pointerSizer.Left - PictureBox1.Left, pointerSizer.Bottom - PictureBox1.Top), New Point(CInt(BaseRect.Right - 20), CInt(BaseRect.Top + 10)), New Point(CInt(BaseRect.Right - 10), CInt(BaseRect.Top + 40)), New Point(pointerSizer.Left - PictureBox1.Left, pointerSizer.Bottom - PictureBox1.Top)})
        Case RadioButton3.Checked 'bottomleft
            gp.AddPolygon(New Point() {New Point(pointerSizer.Right - PictureBox1.Left, pointerSizer.Top - PictureBox1.Top), New Point(CInt(BaseRect.Left + 20), CInt(BaseRect.Bottom - 10)), New Point(CInt(BaseRect.Left + 10), CInt(BaseRect.Bottom - 40)), New Point(pointerSizer.Right - PictureBox1.Left, pointerSizer.Top - PictureBox1.Top)})
        Case RadioButton4.Checked 'bottomright
            gp.AddPolygon(New Point() {New Point(pointerSizer.Left - PictureBox1.Left, pointerSizer.Top - PictureBox1.Top), New Point(CInt(BaseRect.Right - 20), CInt(BaseRect.Bottom - 10)), New Point(CInt(BaseRect.Right - 10), CInt(BaseRect.Bottom - 40)), New Point(pointerSizer.Left - PictureBox1.Left, pointerSizer.Top - PictureBox1.Top)})
    End Select 
    Return gp 
End Function
 
Public Function RoundedRectangle(ByVal m_intxAxis As Integer, _
                                ByVal m_intyAxis As Integer, _
                                ByVal m_intWidth As Integer, _
                                ByVal m_intHeight As Integer, _
                                ByVal m_diameter As Integer) As Drawing2D.GraphicsPath 
    Dim gp As New Drawing2D.GraphicsPath 
    Dim BaseRect As New RectangleF(m_intxAxis, m_intyAxis, m_intWidth, m_intHeight)
    Dim ArcRect As New RectangleF(BaseRect.Location, New SizeF(m_diameter, m_diameter))
    'top left Arc
    gp.AddArc(ArcRect, 180, 90)
    gp.AddLine(m_intxAxis + CInt(m_diameter / 2), m_intyAxis, _
                             m_intxAxis + m_intWidth - CInt(m_diameter / 2), m_intyAxis) 
    ' top right arc
    ArcRect.X = BaseRect.Right - m_diameter
    gp.AddArc(ArcRect, 270, 90)
    gp.AddLine(m_intxAxis + m_intWidth, m_intyAxis + CInt(m_diameter / 2), _
                             m_intxAxis + m_intWidth, m_intyAxis + m_intHeight - CInt(m_diameter / 2)) 
    ' bottom right arc
    ArcRect.Y = BaseRect.Bottom - m_diameter
    gp.AddArc(ArcRect, 0, 90)
    gp.AddLine(m_intxAxis + CInt(m_diameter / 2), m_intyAxis + m_intHeight, _
                             m_intxAxis + m_intWidth - CInt(m_diameter / 2), m_intyAxis + m_intHeight) 
    ' bottom left arc
    ArcRect.X = BaseRect.Left
    gp.AddArc(ArcRect, 90, 90)
    gp.AddLine(m_intxAxis, m_intyAxis + CInt(m_diameter / 2), _
                         m_intxAxis, m_intyAxis + m_intHeight - CInt(m_diameter / 2)) 
    Return gp 
End Function



 
The final part of the code is the overridden WndProc where the pressed HotKey is processed and the semi-transparent balloonTip is saved to your desktop.

 
Protected
Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
    If m.Msg = WM_HOTKEY AndAlso m.WParam.ToInt32 = 0 Then
        'hotkey pressed
        Me.BringToFront() 
        Select Case True
            Case RadioButton1.Checked 'topleft
                Dim x As Integer = Math.Min(pointerSizer.Right, leftSizer.Right + 12)
                Dim y As Integer = Math.Min(pointerSizer.Bottom, topSizer.Bottom + 12)
                clipRect = New Rectangle(x, y, rightSizer.Left - 12 - x, bottomSizer.Top - 12 - y)
           Case RadioButton2.Checked 'topright
                Dim x As Integer = Math.Max(pointerSizer.Left, rightSizer.Left - 12)
                Dim y As Integer = Math.Min(pointerSizer.Bottom, topSizer.Bottom + 12)
                clipRect = New Rectangle(leftSizer.Right + 12, y, x - leftSizer.Right + 12, bottomSizer.Top - 12
- y)
            Case RadioButton3.Checked 'bottomleft
                Dim x As Integer = Math.Min(pointerSizer.Left, leftSizer.Right + 12)
                Dim y As Integer = Math.Max(pointerSizer.Top, bottomSizer.Top - 12)
                clipRect = New Rectangle(x, topSizer.Bottom + 12, rightSizer.Left - 12 - x, y - topSizer.Bottom
+ 12)
            Case RadioButton4.Checked 'bottomright
                Dim x As Integer = Math.Max(pointerSizer.Left, rightSizer.Left - 12)
                Dim y As Integer = Math.Max(pointerSizer.Top, bottomSizer.Top - 12)
                clipRect = New Rectangle(leftSizer.Right + 12, topSizer.Bottom + 12, x - leftSizer.Right + 12, y
 - topSizer.Bottom + 12)
        End Select 
        pointerSizer.Hide()
        leftSizer.Hide()
        rightSizer.Hide()
        topSizer.Hide()
        bottomSizer.Hide() 
        Application.DoEvents() 
        Dim img As New Bitmap(clipRect.Width, clipRect.Height)
        Dim gr As Graphics = Graphics.FromImage(img)
        gr.Clear(SystemColors.Control)
 
        Dim p As Point = Me.PointToScreen(clipRect.Location) 
        gr.CopyFromScreen(p, Point.Empty, img.Size) 
        img.MakeTransparent(SystemColors.Control)
        img.Save(IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "001.png"),
Drawing.Imaging.ImageFormat.Png)
 
        pointerSizer.Show()
        leftSizer.Show()
        rightSizer.Show()
        topSizer.Show()
        bottomSizer.Show()
 
        Application.DoEvents()
    End If
    MyBase.WndProc(m)
End Sub



Download

Download project here...


See Also

​Image Arrow Pointers