Want to give users the ability to annotate images with lines, shapes, highlights, and text? This project provides flexible drawing tools and customization options for a variety of use-cases.
Screenshot
The above example shows a base image that has been marked up with a text box, polygon (that also happens to be "selected"), line with arrowhead endpoint, and a highlighted date/time.
Features
Open BMP, PNG, and JPEG images.
Draw Lines with optional endpoints (Arrowhead, Ball, Bar).
Create rectangles and polygons with customizable fill and stroke.
Requires Olaf Schmidt's RC6 library available at https://www.vbrichclient.com/ - so make sure to have it installed and registered on your dev machine.
This is a first-pass and the project has only been lightly tested so far. There are likely to be bugs, so please report any issues you discover and I will try to fix them ASAP.
Thanks for trying it out yokesee - I haven't been able to get it to stick myself, so any additional information you can provide it that regard would be helpful/appreciated.
Some notes on the drawing though, as the way it has been designed to behave might be different than what you are used to from a drawing program.
While click and hold/drag is supported, the primary input method is to click and release to drop object points at desired positions as this enables more accurate placement.
The highlighter tool behaves a bit differently than the other tools in that the highlighter mode remains active after you draw your first rectangle. This is so you can continue drawing more and more highlight rectangles, and when they are filled, the translucency will be uniform even on overlapping rectangles. You can stop drawing highlights by pressing the Return key.
The polygon tool is similar to the highlighter tool in that regard as you can keep adding points as needed, then you will finish the polygon by pressing the Return key.
Latest version in first post updated with the following:
Improved hit-testing when objects have stroked borders (border width now included in test)
Fixed a bug that could cause "wild" movement of selected objects on double-click.
Fixed a bug that could cause Highlighter rects (after the first) to start at the top-left corner of the canvas instead of at the mouse position on double-click.
Selected object stroke now alternates between yellow/blue dashes so it is visible over light and dark backgrounds.
Fixed an unhandled "subscript out of range" error that could be raised when a Highlighter Rect's point values were all the same.
Latest version in first post updated with the following:
Added SendForward and SendBackward methods, and changed BringToFront to SendToFront so that all ZOrder modifier methods are more easily discoverable via Intellisense (Send*)
Added SendForward/Backward buttons to the demo UI
Improvements to Dotted stroke drawing (using Round line cap)
Right-click will now finalize drawing all objects (especially useful for Polygons and Highlighter rectangles). Previously only pressing the Return key would finalize objects, but now you can work "mouse only".
It works much better.
I've noticed that when you draw the rectangle and move it, it gets stuck, especially if you move it quickly.
But it doesn't in edit mode, it works perfectly.
Regards
It works much better.
I've noticed that when you draw the rectangle and move it, it gets stuck, especially if you move it quickly.
But it doesn't in edit mode, it works perfectly.
Regards
Thanks for the clarifications, and glad to hear things are working better. Unfortunately I have been unable to reproduce the issue with the rectangle getting stuck on move so far, but I will run some more tests and see if I can reproduce/fix that behaviour.
Latest version in first post updated with the following:
Added EnableKeyboardShortcuts property that you can set to False to disable all built-in keyboard handling.
Added Arrow key movement of selected objects (hold Shift for larger movement) when EnableKeyboardShortcuts = True (default).
Added DeleteSelectedObjects method (in case you would like to add a UI to delete selected objects).
Added MoveSelectedObjects method in case you want to move selected objects via UI.
Added SelectAllObjects method (in case you would like to add a UI to select all objects).
Added RequestText event when the Text tool mode is selected (instead of using ugly built-in InputBox, you can use your own input form to request text from the user).
Added ToolModeChanged event (in case you want to update UI based on tool mode changes).
Added Ctrl+A shortcut to select all objects when EnableKeyboardShortcuts = True (default).
Latest version in first post updated with the following:
Ability to set text horizontal and vertical alignment.
Other minor changes/improvements.
NOTE: The RC6 DrawText method only supports Top and Middle vertical alignment AFAICT. Hopefully Olaf will consider adding Bottom alignment support, but if not then I will add bottom alignment support in this project.
Latest version in first post updated with the following:
Double-clicking a Text box will raise the TextRequested event so you can give users an opportunity to edit text for existing objects (in case of typos for example).
Nice work, and thanks for the contribution!
I modified the CPolygon code to draw smooth polygons, theoretically the ratio for adding curves by hand should be 1/6 but in most cases it gives a very strong result, so the ratio 0.14 is used in this example.
Code:
Private Type IntPt
X As Long
Y As Long
End Type
...
Private Sub InterP(Result As IntPt, Pt As CPoint, Pt1 As CPoint, Pt2 As CPoint, ByVal t As Single)
Result.X = Pt.X + (Pt1.X - Pt2.X) * t
Result.Y = Pt.Y + (Pt1.Y - Pt2.Y) * t
End Sub
Private Sub IDrawable_Draw(po_CairoContext As RC6.cCairoContext, Optional ByVal po_LivePoint As CPoint = Nothing, Optional ByVal p_PathOnly As Boolean = False, Optional ByVal p_IsSelected As Boolean = False, Optional ByVal p_DrawHandles As Boolean = False, Optional ByVal p_Zoom As Double = 1#)
Dim lo_Points As RC6.cArrayList
Dim lo_Pt As CPoint
Dim ii As Long
Dim Ctrls() As IntPt
Dim Prv As CPoint, nxt As CPoint
Dim last As Long, nx As Long
With po_CairoContext
Set lo_Points = Me.Points(True)
If Me.BorderThickness > 0 Then OffsetPolygonPoints lo_Points, Me.BorderThickness * PointsPerPixel / 2
If Not po_LivePoint Is Nothing Then
lo_Points.Add po_LivePoint
End If
ReDim Ctrls(0 To lo_Points.Count * 2 - 1)
For ii = 0 To lo_Points.Count - 1
Set lo_Pt = lo_Points(ii)
nx = ii + 1: If nx = lo_Points.Count Then nx = 0
last = ii - 1: If last < 0 Then last = lo_Points.Count - 1
Set nxt = lo_Points(nx)
Set Prv = lo_Points(last)
Call InterP(Ctrls(last * 2 + 1), lo_Pt, Prv, nxt, 0.14)
Call InterP(Ctrls(ii * 2), lo_Pt, nxt, Prv, 0.14)
Next
Set lo_Pt = lo_Points(0)
.MoveTo lo_Pt.X, lo_Pt.Y
Dim p1 As IntPt, p2 As IntPt
last = 0
For ii = 1 To lo_Points.Count - 1
Set lo_Pt = lo_Points(ii)
p1 = Ctrls(last)
p2 = Ctrls(last + 1)
.CurveTo p1.X, p1.Y, p2.X, p2.Y, lo_Pt.X, lo_Pt.Y
last = last + 2
Next
Set lo_Pt = lo_Points(0)
p1 = Ctrls(last)
p2 = Ctrls(last + 1)
.CurveTo p1.X, p1.Y, p2.X, p2.Y, lo_Pt.X, lo_Pt.Y
'end of smooth code
.ClosePath
If p_PathOnly Then
.ClearPath
Else
.Save
If Me.BorderThickness > 0 Then
If Me.BorderAlpha > 0 Then
SetLineStyle po_CairoContext, Me.BorderStyle, Me.BorderThickness * PointsPerPixel
.SetLineWidth Me.BorderThickness * PointsPerPixel
.SetSourceColor Me.BorderColor, Me.BorderAlpha
.Stroke True
End If
End If
If Me.BackgroundAlpha > 0 Then
.ClearPath
With Me.Points(True)
Set lo_Pt = .Item(0)
po_CairoContext.MoveTo lo_Pt.X, lo_Pt.Y
For ii = 1 To .Count - 1
Set lo_Pt = .Item(ii)
po_CairoContext.LineTo lo_Pt.X, lo_Pt.Y
Next
End With
If Not po_LivePoint Is Nothing Then
' Extend polygon to current mouse position when it is the new object
.LineTo po_LivePoint.X, po_LivePoint.Y
End If
po_CairoContext.SetSourceColor Me.BackgroundColor, Me.BackgroundAlpha
po_CairoContext.Fill True
End If
.ClearPath
If p_IsSelected Then
' Offset polygon to draw selection highlight polygon
OffsetPolygonPoints lo_Points, Me.BorderThickness * PointsPerPixel / 2 + SelectionStrokeThickness
Set lo_Pt = lo_Points(0)
.MoveTo lo_Pt.X, lo_Pt.Y
For ii = 1 To lo_Points.Count - 1
Set lo_Pt = lo_Points(ii)
.LineTo lo_Pt.X, lo_Pt.Y
Next
If Not po_LivePoint Is Nothing Then
' Extend polygon to current mouse position when it is the new object
.LineTo po_LivePoint.X, po_LivePoint.Y
End If
.ClosePath
StrokeSelectionPath po_CairoContext
If p_DrawHandles Then
' Draw resize handles
DrawHandles po_CairoContext, Me.Points(False)
End If
End If
.Restore
End If
End With
End Sub
Nice work, and thanks for the contribution!
I modified the CPolygon code to draw smooth polygons, theoretically the ratio for adding curves by hand should be 1/6 but in most cases it gives a very strong result, so the ratio 0.14 is used in this example.
That looks really cool, and thanks for the contribution right back at ya!
I'll try to get this rolled in to the main code base today.
Hi jpbro,
I'm wondering if your code can do the following:
(1) Add shadows to the perimeter of regular shapes such as rectangles, circles, and triangles
(2) Add shadows to the perimeter of irregular shapes.
Thanks.
Last edited by SearchingDataOnly; Mar 21st, 2025 at 09:29 AM.
Hi jpbro,
I'm wondering if your code can do the following:
(1) Add shadows to the perimeter of regular shapes such as rectangles, gardens, and triangles
(2) Add shadows to the perimeter of irregular shapes.
Thanks.
One way would be to do a Gaussian Blur on the entire image and copy use the alpha channel only. If you put this code in the CompositeImage function right before the code that draws the objects it should do the trick:
Code:
' Draw Drop Shadow
Dim lO_SrfShadow As RC6.cCairoSurface
Dim lo_CcShadow As RC6.cCairoContext
Set lO_SrfShadow = Cairo.CreateSurface(lo_Cc.Surface.Width, lo_Cc.Surface.Height)
Set lo_CcShadow = lO_SrfShadow.CreateContext
For Each lo_DrawObject In mo_DrawObjects
DrawObject lo_CcShadow, lo_DrawObject, False
Next
Set lO_SrfShadow = lo_CcShadow.Surface.GaussianBlur(8, , True)
lo_Cc.RenderSurfaceContent lO_SrfShadow, -16, -16
Not sure I want to include this is the main code though as the GaussianBlur method is fairly slow and it cause things like moving objects around to become a bit sluggish - might need to come up with a way skip drawing shadows when performing some actions like move/resize. Maybe detecting if the mouse button is down would be enough, I'll see.
Latest version in first post updated with the following:
Optional Global Drop Shadow rendering via the EnableGlobalDropShadow property (example image below).
Other minor changes/refactorings.
Drop Shadow Example:
NOTE ABOUT DROP SHADOW RENDERING: The GaussianBlur method is a bit slow, so drop shadow rendering is disabled when the mouse button is down while moving/drawing/resizing objects. Shadows will be re-rendered after releasing the mouse button.
Latest version in first post updated with the following:
Fixed SaveImage method so that it saves the image in its original size to avoid stretching/upscaling artifacts (was incorrectly saving it at the max viewport size previously)
Private Sub IDrawable_Draw(po_CairoContext As RC6.cCairoContext, Optional ByVal po_LivePoint As CPoint = Nothing, Optional ByVal p_PathOnly As Boolean = False, Optional ByVal p_IsSelected As Boolean = False, Optional ByVal p_DrawHandles As Boolean = False, Optional ByVal p_Zoom As Double = 1#)
Dim lo_IDrawable As IDrawable
Dim lo_Rect As CRect
Set lo_IDrawable = Me.Rect
lo_IDrawable.Draw po_CairoContext, po_LivePoint, p_PathOnly, p_IsSelected, p_DrawHandles
po_CairoContext.SelectFont Me.Font.Name, _
Me.Font.Size * PointsPerPixel, _
Me.TextColor, _
Me.Font.Bold, _
Me.Font.Italic, _
Me.Font.Underline, _
Me.Font.Strikethrough
Set lo_Rect = Me.Rect.NormalizedRect ' Normalize the rectangle so that the text appears if the rect was drawn any direction other than top-left to bottom-right
If Me.VerticalAlignment = verticalalign_Bottom Then
' TODO:
' RC6 CairoContext.DrawText Method only supports Top/Middle alignment, so we have to do our own bottom alignment text rendering
DrawTextBottomAlign po_CairoContext, _
lo_Rect.X1, _
lo_Rect.Y1, _
lo_Rect.Width, _
lo_Rect.Height, _
Me.Text, _
False, _
Choose(Me.HorizontalAlignment + 1, vbLeftJustify, vbCenter, vbRightJustify), _
10
Else
po_CairoContext.DrawText lo_Rect.X1, _
lo_Rect.Y1, _
lo_Rect.Width, _
lo_Rect.Height, _
Me.Text, _
False, _
Choose(Me.HorizontalAlignment + 1, vbLeftJustify, vbCenter, vbRightJustify), _
10, _
Me.VerticalAlignment
End If
End Sub
Private Sub IDrawable_Draw(po_CairoContext As RC6.cCairoContext, Optional ByVal po_LivePoint As CPoint = Nothing, Optional ByVal p_PathOnly As Boolean = False, Optional ByVal p_IsSelected As Boolean = False, Optional ByVal p_DrawHandles As Boolean = False, Optional ByVal p_Zoom As Double = 1#)
Dim lo_IDrawable As IDrawable
Dim lo_Rect As CRect
Set lo_IDrawable = Me.Rect
lo_IDrawable.Draw po_CairoContext, po_LivePoint, p_PathOnly, p_IsSelected, p_DrawHandles
po_CairoContext.SelectFont Me.Font.Name, _
Me.Font.Size * PointsPerPixel, _
Me.TextColor, _
Me.Font.Bold, _
Me.Font.Italic, _
Me.Font.Underline, _
Me.Font.Strikethrough
Set lo_Rect = Me.Rect.NormalizedRect ' Normalize the rectangle so that the text appears if the rect was drawn any direction other than top-left to bottom-right
If Me.VerticalAlignment = verticalalign_Bottom Then
' TODO:
' RC6 CairoContext.DrawText Method only supports Top/Middle alignment, so we have to do our own bottom alignment text rendering
DrawTextBottomAlign po_CairoContext, _
lo_Rect.X1, _
lo_Rect.Y1, _
lo_Rect.Width, _
lo_Rect.Height, _
Me.Text, _
False, _
Choose(Me.HorizontalAlignment + 1, vbLeftJustify, vbCenter, vbRightJustify), _
10
Else
po_CairoContext.DrawText lo_Rect.X1, _
lo_Rect.Y1, _
lo_Rect.Width, _
lo_Rect.Height, _
Me.Text, _
False, _
Choose(Me.HorizontalAlignment + 1, vbLeftJustify, vbCenter, vbRightJustify), _
10, _
Me.VerticalAlignment
End If
End Sub
everything else works very well
Thanks yokesee - you can comment that line out for now, I forgot to (it's a placeholder for the bottom aligned text drawing that isn't implemented yet). I'll update the main source with this bit commented out ASAP.
Hi jpbro, very good, thank you.
I don't know why the software takes so long to load for me!
I have a suggestion: add a ruler. And maybe guide lines
I wrote a simple example.
Of course, I'm not a professional like you.
I don't know why the software takes so long to load for me!
You probably have a lot of fonts - the demo app/form loads all fonts into the combobox and it is quite slow. The more fonts, the worse it gets. There are better ways to populate combos with fonts, but they're outside the scope of this demo (here's one example by LaVolpe)
Originally Posted by Mojtaba
I have a suggestion: add a ruler. And maybe guide lines
It's a good idea, but I'm not sure I want to add them as features to the base class. Instead I have decided on the concept of an Underlay and Overlay layer. The underlay appears above the base background image and below the annotation layer. The overlay appears above all other layers.
There are 2 new events: BeforeRenderUnderlay and BeforeRenderOverlay, each of which provide a cCairoContext that you can draw anything you like against. The underlay would be good for rulers, and the overlay would be good for something like tooltips/coordinate reporting that follows the mouse.
NOTE: In the above image, the yellow diagonal line is painted on the underlay layer (drawing under the polygon in the annotation layer). The green diagonal line appears in the overlay layer (drawing over the polygon in the annotation layer).
I'll probably need to add some additional features to make this more useful (like exposing the mouse coords, and zoom factor so that you can adjust ruler values appropriately), but it's a good starting point if you want to play around with it and report back with any problems or places where it lacks necessary info you need to draw what you would like.
Latest version in first post updated with the following:
Added BeforeRenderUnderlay and BeforeRenderOverlay events that provide a cCairoContext object for you to draw anything you like (thanks Mojtaba for the idea to allow for the drawing of ruler/gridlines)
Fixed bug when exporting images to file with global drop shadow enabled (shadow wasn't scaling properly).
Fixed render bug with cHighlighter (broke the single fill of multiple rectangles, even if they were overlapping in an earlier release)
Glad you like it
You probably have a lot of fonts - the demo app/form loads all fonts into the combobox and it is quite slow. The more fonts, the worse it gets. There are better ways to populate combos with fonts, but they're outside the scope of this demo (here's one example by LaVolpe)
I've talked about this before. software runs late after compilation
This problem exists if you use the GDI+ library in fonts.
Of course, if the number of fonts exceeds about 600 or 700
the program's slow loading becomes clearly visible, but below these numbers it is not so noticeable.
There are several other suggestions: the ability to zoom in and take screenshots, and . . .
I noticed a small problem: when you load a new photo or minimize the program window, the image disappears and does not refresh.
And you must click on the board to see the image again.
I noticed a small problem: when you load a new photo or minimize the program window, the image disappears and does not refresh.
And you must click on the board to see the image again.
Thanks for reporting, this is fixed in the latest source in the first post.
Originally Posted by Mojtaba
I've talked about this before. software runs late after compilation
This problem exists if you use the GDI+ library in fonts.
Of course, if the number of fonts exceeds about 600 or 700
the program's slow loading becomes clearly visible, but below these numbers it is not so noticeable.
This project doesn't use GDI+ at all, so that shouldn't be an issue.
If you comment out the following code in Form_Load:
Code:
For ii = 0 To Screen.FontCount - 1
Me.cmbFont.AddItem Screen.Fonts(ii)
Next
And replace it with:
Code:
Me.cmbFont.AddItem "Segoe UI"
Does it load faster?
Originally Posted by Mojtaba
There are several other suggestions: the ability to zoom in and take screenshots, and . . .
Zoom is a possibility, but I don't think I'll get into screenshots as it is outside the scope of what this project is meant for. Just FYI the goal isn't for a full-featured paint application, nor a replacement for something like the Windows snipping tool or SnagIt. But feel free to expand on the source as required for your needs.
Thanks for reporting, this is fixed in the latest source in the first post.
Does it load faster?
Yes
I use FontCombo for projects where I need to select fonts. CommonControls (Replacement of the MS common controls)
Loads all fonts without slowing down
But I couldn't find a solution to the problem with GDI+.
I agree with you on the screenshot as well.
Latest version in first post updated with the following:
Added Zoom/Scaling Support via CanvasZoomPercent property.
Fixed bug related to deleting CHighlighter objects after Select All operation.
CHighlighter drawing is now excluded from global dropshadow rendering (it was darkening the highlight which seem counter to the intention of the tool).
Various minor fixes/improvements.
I think the project is complete feature-wise with this update - it does everything I need it to and more. I'll continue to fix bugs as needed, but otherwise I'm signing off.
I hope some of you find this project useful, enjoy!