This tutorial shows how to create user-defined annotation objects, thumb (control points) styles, and line ending styles in a WinForms C# .NET 6 application using the LEADTOOLS SDK.
Overview | |
---|---|
Summary | This tutorial covers how to create user-defined annotation objects in a WinForms C# application. |
Completion Time | 45 minutes |
Visual Studio Project | Download tutorial project (8 KB) |
Platform | Windows WinForms C# Application |
IDE | Visual Studio 2022 |
Development License | Download LEADTOOLS |
Get familiar with the basic steps of creating a project by reviewing the Add References and Set a License and Draw and Edit Annotations on Documents tutorials before working on the Implement User-Defined Objects With LEADTOOLS Annotations - WinForms C# .NET 6 tutorial.
Start with a copy of the project created in the Draw and Edit Annotations on Documents tutorial. If the project is not available, follow the steps in that tutorial to create it.
The references needed depend upon the purpose of the project. References can be added by one or the other of the following two methods (but not both). For this project, the following references are needed:
if using NuGet references, this tutorial requires the following NuGet packages and their dependencies:
Leadtools.Annotations.Winforms
Leadtools.Document.Sdk
Leadtools.Document.Viewer.Winforms
If local DLL references are used, the following DLLs are needed. The DLLs are located at <INSTALL_DIR>\LEADTOOLS23\Bin\net:
Leadtools.dll
Leadtools.Annotations.Automation.dll
Leadtools.Annotations.Designers.dll
Leadtools.Annotations.Engine.dll
Leadtools.Annotations.Rendering.WinForms.dll
Leadtools.Annotations.WinForms.dll
Leadtools.Caching.dll
Leadtools.Controls.WinForms.dll
Leadtools.Core.dll
Leadtools.Document.dll
Leadtools.Document.Viewer.WinForms.dll
For a complete list of which DLL files are required for your application, refer to Files to be Included With Your Application.
The License unlocks the features needed for the project. It must be set before any toolkit function is called. For details, including tutorials for different platforms, refer to the Setting a Runtime License tutorial.
There are two types of runtime licenses:
Note: Adding LEADTOOLS NuGet and local references and setting a license are covered in more detail in the Add References and Set a License tutorial.
With the project created, the references added, the license set, and annotations added, coding can begin.
The object being created is a simple triangle. This object will have three points for the end points of the triangle and it will use a stroke for the triangle edges and a fill for the interior.
Add the following statements to the using
block at the top:
using Leadtools;
using Leadtools.Document;
using Leadtools.Caching;
using Leadtools.Document.Viewer;
using Leadtools.Controls;
using Leadtools.Annotations.Automation;
using Leadtools.Annotations.WinForms;
using Leadtools.Annotations.Designers;
using Leadtools.Annotations.Engine;
using Leadtools.Annotations.Rendering;
Add a new C# class called AnnTriangleObject.cs
to the project:
Add the following code to the new AnnTriangleObject.cs
file:
Add the AnnTriangleDrawDesigner
class below the AnnTriangleObject
class:
// The draw designer
public class AnnTriangleDrawDesigner : AnnDrawDesigner
{
public AnnTriangleDrawDesigner(IAnnAutomationControl automationControl, AnnContainer container, AnnObject annObject) :
base(automationControl, container, annObject)
{
}
// Override the Pointer Down event and add three points for the triangle
public override bool OnPointerDown(AnnContainer sender, AnnPointerEventArgs e)
{
// See if the base class wants to handle the event
bool handled = base.OnPointerDown(sender, e);
if (handled)
return true;
// Will work on left button only
if (e.Button != AnnMouseButton.Left)
return false;
// Get the current number of points in the object
AnnObject annObject = this.TargetObject;
int pointCount = annObject.Points.Count;
if (pointCount < 3)
{
// Add the current point
annObject.Points.Add(e.Location);
// If there are zero points, then add another one.
// When the pointer is moved, the last point will also move (second in this case)
if (pointCount == 0)
annObject.Points.Add(e.Location);
if (pointCount == 0)
{
if (!this.StartWorking())
return true;
}
}
else
{
// Now that there are three points, the process is finished
this.EndWorking();
}
return true;
}
// Override the Pointer Move event
public override bool OnPointerMove(AnnContainer sender, AnnPointerEventArgs e)
{
bool handled = base.OnPointerMove(sender, e);
// See if we have points
// If so, move the last point in the object to this new location
AnnObject annObject = this.TargetObject;
if (annObject.Points.Count > 0)
{
annObject.Points[annObject.Points.Count - 1] = e.Location;
Working();
return true;
}
return false;
}
public override bool OnPointerUp(AnnContainer sender, AnnPointerEventArgs e)
{
base.OnPointerUp(sender, e);
return true;
}
}
Add the InitializeTriangleObject
method to the main form:
private static void InitializeTriangleObject(AutomationManagerHelper annotations)
{
// Create a new automation object
AnnAutomationObject automationObj = new AnnAutomationObject();
// Set the object ID
automationObj.Id = AnnTriangleObject.MyId;
automationObj.Name = "Triangle";
// Set its designers
automationObj.DrawDesignerType = typeof(AnnTriangleDrawDesigner);
automationObj.EditDesignerType = typeof(AnnPolylineEditDesigner);
automationObj.RunDesignerType = typeof(AnnRunDesigner);
// Set the template
AnnTriangleObject annObj = new AnnTriangleObject();
// Set the default stroke
annObj.Stroke = AnnStroke.Create(AnnSolidColorBrush.Create("red"), LeadLengthD.Create(2));
annObj.Fill = AnnSolidColorBrush.Create("yellow");
automationObj.ObjectTemplate = annObj;
// Set the renderer, same as the AnnPolylineObject
IAnnObjectRenderer polylineRenderer = annotations.AutomationManager.RenderingEngine.Renderers[AnnObject.PolylineObjectId];
IAnnObjectRenderer renderer = new AnnPolylineObjectRenderer();
renderer.LabelRenderer = polylineRenderer.LabelRenderer;
renderer.LocationsThumbStyle = polylineRenderer.LocationsThumbStyle;
renderer.RotateGripperThumbStyle = polylineRenderer.RotateGripperThumbStyle;
renderer.RotateCenterThumbStyle = polylineRenderer.RotateCenterThumbStyle;
annotations.AutomationManager.RenderingEngine.Renderers[AnnTriangleObject.MyId] = renderer;
// Set its context menu and toolbar image
AnnAutomationObject extData = new();
extData.ContextMenu = new ObjectContextMenu();
extData.DrawCursor = Cursors.Cross;
Bitmap bitmap = new Bitmap(24, 24, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
using (Graphics g = Graphics.FromImage(bitmap))
{
g.Clear(Color.Transparent);
g.DrawLine(Pens.Black, 12, 2, 22, 22);
g.DrawLine(Pens.Black, 22, 22, 2, 22);
g.DrawLine(Pens.Black, 2, 22, 12, 2);
}
extData.ToolBarToolTipText = "Triangle";
extData.ContextMenu = new ObjectContextMenu();
automationObj.ToolBarImage = bitmap;
automationObj.UserData = extData;
annotations.AutomationManager.Objects.Add(automationObj);
}
Next, call InitializeTriangleObject
below the _documentViewer.Annotations.Initialize()
call in the InitAnnotations
method:
_documentViewer.Annotations.Initialize();
// Create the triangle automation object
InitializeTriangleObject(automationManagerHelper);
Run the project by pressing F5, or by selecting Debug -> Start Debugging.
If the steps were followed correctly, the application will run, and there will be a new triangle icon in the annotation toolbar. Load a document and test the new triangle annotation. The result should be similar to the image below:
With LEADTOOLS Annotations, it is possible to create custom thumbs (control points). Create custom styles for the location, rotation center and rotation gripper thumbs.
To implement a user-defined thumb style, create a class that implements the IAnnThumbStyle interface. Then, assign the custom thumb style class to an IAnnObjectRenderer interface, which will then use the custom thumb style when rendering annotation objects.
First, create a new class in the AnnTrangleObject.cs
file, derived from the base class AnnThumbStyle, and override the AddPath method:
public class AnnTriangleThumbStyle : AnnThumbStyle
{
protected override AnnThumbStyle Create()
{
return new AnnTriangleThumbStyle();
}
protected override void AddPath(System.Drawing.Drawing2D.GraphicsPath path, LeadRectD rect)
{
if (path != null)
{
// Add the triangle
float left = (float)rect.Left;
float right = (float)rect.Right;
float width = (right - left) / 2;
float top = (float)rect.Top;
float bottom = (float)rect.Bottom;
path.AddLine(left, bottom, left + width, top);
path.AddLine(left + width, top, right, bottom);
path.AddLine(right, bottom, left, bottom);
path.CloseFigure();
}
}
}
In the main form, replace the code after the "Set the renderer, same as the AnnPolylineObject" comment in the InitializeTriangleObject
method:
// Get the current polyline renderer (Need to use some of the properties that are not changing)
IAnnObjectRenderer polylineRenderer = annotations.AutomationManager.RenderingEngine.Renderers[AnnObject.PolylineObjectId];
// Create the new renderer
IAnnObjectRenderer renderer = new AnnPolylineObjectRenderer();
// Use the existing label renderer. It is not being changed
renderer.LabelRenderer = polylineRenderer.LabelRenderer;
// Now, use the new triangle thumbs:
// Change the location thumb style
AnnTriangleThumbStyle locationThumb = new AnnTriangleThumbStyle();
locationThumb.Size = LeadSizeD.Create(72 * 2, 72 * 2);
locationThumb.Stroke = AnnStroke.Create(AnnSolidColorBrush.Create("black"), LeadLengthD.Create(1));
locationThumb.Fill = AnnSolidColorBrush.Create("#7F0000FF");
renderer.LocationsThumbStyle = locationThumb;
// Change the rotation center thumb style
AnnTriangleThumbStyle rotateCenterThumb = new AnnTriangleThumbStyle();
rotateCenterThumb.Size = LeadSizeD.Create(72, 72);
rotateCenterThumb.Stroke = AnnStroke.Create(AnnSolidColorBrush.Create("black"), LeadLengthD.Create(1));
rotateCenterThumb.Fill = AnnSolidColorBrush.Create("#EFFF0000");
renderer.RotateCenterThumbStyle = rotateCenterThumb;
// Change the Rotation gripper thumb style
AnnTriangleThumbStyle rotateGripperThumb = new AnnTriangleThumbStyle();
rotateGripperThumb.Size = LeadSizeD.Create(72 * 2, 72 * 2);
rotateGripperThumb.Stroke = AnnStroke.Create(AnnSolidColorBrush.Create("black"), LeadLengthD.Create(1));
rotateGripperThumb.Fill = AnnSolidColorBrush.Create("#3F00FF00");
renderer.RotateGripperThumbStyle = rotateGripperThumb;
annotations.AutomationManager.RenderingEngine.Renderers[AnnTriangleObject.MyId] = renderer;
Run the project by pressing F5, or by selecting Debug -> Start Debugging.
If the steps were followed correctly, the triangle annotation object has an updated thumb point style. Load a document and test the updated triangle annotation. The result should be similar to the image below:
Add a new C# class to the project and name it AnnCrossLineEnding.cs
.
Set this new class to inherit from AnnLineEnding. Then, override some members and implement new functionality for the custom ending style by adding the following code to the new class:
public class AnnCrossLineEnding : AnnLineEnding
{
protected override AnnLineEnding Create()
{
return new AnnCrossLineEnding();
}
public override AnnLineEnding Clone()
{
AnnCrossLineEnding arrowLineEnding = base.Clone() as AnnCrossLineEnding;
arrowLineEnding.Closed = _closed;
return arrowLineEnding;
}
public override int Id
{
// Note when creating custom ending style you add unique Id that is not used before
// for existing ending style the ids all are negative so set id = 1 for the custom ending style
get { return 1; }
}
// This is the property that controls if the ending style cross shape is closed
private bool _closed = false;
public bool Closed
{
get { return _closed; }
set { _closed = value; }
}
// Returns the array of points that composes style shape , this will be used when rendering the style and also when hit testing it
public override LeadPointD[] GetStylePoints(LeadPointD lineStart, LeadPointD lineEnd)
{
double length = this.Length.Value / 2;
// First cross on line start
double lineStartX = lineStart.X;
double lineStartY = lineStart.Y;
LeadPointD pt0 = LeadPointD.Create(lineStartX - length, lineStartY - length);
LeadPointD pt1 = LeadPointD.Create(lineStartX + length, lineStartY + length);
LeadPointD pt2 = LeadPointD.Create(lineStartX - length, lineStartY + length);
LeadPointD pt3 = LeadPointD.Create(lineStartX + length, lineStartY - length);
return new LeadPointD[] { pt0, pt1, pt2, pt3 };
}
// Here you can specify if you want the style to be hit testable and you can move the object by dragging it
public override bool HitTest(LeadPointD point, double hitTestBuffer, LeadPointD lineStart, LeadPointD lineEnd)
{
return false;
}
// Saving the custom style to the XML document
public override XmlNode Serialize(AnnSerializeOptions options, XmlNode parentNode, XmlDocument document, string elementName)
{
XmlNode styleNode = base.Serialize(options, parentNode, document, elementName);
XmlNode element = document.CreateElement("StyleClosed");
element.InnerText = _closed.ToString();
styleNode.AppendChild(element);
return styleNode;
}
// Loading the custom style from XML document
public override void Deserialize(AnnDeserializeOptions options, XmlNode element, XmlDocument document)
{
base.Deserialize(options, element, document);
XmlNode childNode = element.SelectSingleNode("StyleClosed");
if (childNode != null)
{
_closed = bool.Parse(childNode.FirstChild.Value);
}
}
public override string FriendlyName
{
get
{
return "Line Ending";
}
}
}
Add the following declaration to the using
block in the main form:
using System.Xml;
Add the following code to the InitUI
method, before the loadButton
deceleration:
var lineEndingButton = new Button();
lineEndingButton.Name = "lineEndingButton";
lineEndingButton.Text = "&Add Custom Line Ending";
lineEndingButton.Width = 175;
lineEndingButton.Dock = DockStyle.Left;
lineEndingButton.Click += (sender, e) => AddCustomLineEnding();
topPanel.Controls.Add(lineEndingButton);
var saveAnnotations = new Button();
saveAnnotations.Name = "saveAnnotations";
saveAnnotations.Text = "&Save Annotations";
saveAnnotations.Width = 175;
saveAnnotations.Dock = DockStyle.Left;
saveAnnotations.Click += (sender, e) => SaveAnnotations();
topPanel.Controls.Add(saveAnnotations);
Add the following class to the main form:
// The custom polyline renderer that will take care of rendering custom ending style
public class AnnCustomPolylineObjectRenderer : AnnPolylineObjectRenderer
{
public override void Render(AnnContainerMapper mapper, AnnObject annObject)
{
base.Render(mapper, annObject);
}
public override void RenderEndingStyles(AnnContainerMapper mapper, AnnPolylineObject annPolyLineObject)
{
// Call the base to draw the original ending styles
base.RenderEndingStyles(mapper, annPolyLineObject);
// Now draw the ending style
AnnCrossLineEnding startStyle = annPolyLineObject.StartStyle as AnnCrossLineEnding;
AnnCrossLineEnding endStyle = annPolyLineObject.EndStyle as AnnCrossLineEnding;
AnnWinFormsRenderingEngine engine = this.RenderingEngine as AnnWinFormsRenderingEngine;
LeadPointCollection objectPoints = annPolyLineObject.Points;
int count = objectPoints.Count;
if (count < 2)
return;
if (annPolyLineObject.SupportsLineEndings)
{
LeadPointD firstPoint = objectPoints[0];
if (startStyle != null)
RenderCrossLineEnding(startStyle, engine, mapper, annPolyLineObject.FixedStateOperations, firstPoint, objectPoints[1]);
if (endStyle != null)
RenderCrossLineEnding(endStyle, engine, mapper, annPolyLineObject.FixedStateOperations, objectPoints[count - 1], objectPoints[count - 2]);
}
}
private void RenderCrossLineEnding(AnnCrossLineEnding crossLineEnding, AnnWinFormsRenderingEngine engine, AnnContainerMapper mapper, AnnFixedStateOperations operations, LeadPointD lineStart, LeadPointD lineEnd)
{
AnnStroke stroke = mapper.StrokeFromContainerCoordinates(crossLineEnding.Stroke, operations);
if (stroke != null)
{
LeadPointD[] endingStylePoints = mapper.PointsFromContainerCoordinates(crossLineEnding.GetStylePoints(lineStart, lineEnd), operations);
if (endingStylePoints != null && endingStylePoints.Length == 4)
{
using (Pen pen = AnnWinFormsRenderingEngine.ToPen(stroke))
{
engine.Context.DrawLine(pen, AnnWinFormsRenderingEngine.ToPoint(endingStylePoints[0]), AnnWinFormsRenderingEngine.ToPoint(endingStylePoints[1]));
engine.Context.DrawLine(pen, AnnWinFormsRenderingEngine.ToPoint(endingStylePoints[2]), AnnWinFormsRenderingEngine.ToPoint(endingStylePoints[3]));
if (crossLineEnding.Closed)
{
engine.Context.DrawLine(pen, AnnWinFormsRenderingEngine.ToPoint(endingStylePoints[0]), AnnWinFormsRenderingEngine.ToPoint(endingStylePoints[3]));
engine.Context.DrawLine(pen, AnnWinFormsRenderingEngine.ToPoint(endingStylePoints[2]), AnnWinFormsRenderingEngine.ToPoint(endingStylePoints[1]));
}
}
}
}
}
}
Integrate the new custom polyline object renderer to the annotations framework. add the code in the InitAnnotations()
method after line var automationManagerHelper = new AutomationManagerHelper(automationManager);
:
// Hook the custom polyline renderer to the existing renderer
AnnPolylineObjectRenderer polyLineRenderer = automationManager.RenderingEngine.Renderers[AnnObject.LineObjectId] as AnnPolylineObjectRenderer;
AnnCustomPolylineObjectRenderer cutomerRenderer = new AnnCustomPolylineObjectRenderer();
cutomerRenderer.LocationsThumbStyle = polyLineRenderer.LocationsThumbStyle;
cutomerRenderer.RotateCenterThumbStyle = polyLineRenderer.RotateCenterThumbStyle;
cutomerRenderer.RotateGripperThumbStyle = polyLineRenderer.RotateGripperThumbStyle;
automationManager.RenderingEngine.Renderers[AnnObject.LineObjectId] = cutomerRenderer;
Add the following logic for the lineEndingButton
and saveButton
in the main form:
// Adds the custom line endings to the currently selected annotation line object
private void AddCustomLineEnding()
{
AnnAutomation automation = _documentViewer.Annotations.AutomationManager.Automations[0];
AnnCrossLineEnding startStyle = new AnnCrossLineEnding();
startStyle.Length = automation.Container.Mapper.LengthToContainerCoordinates(20);
AnnCrossLineEnding endStyle = new AnnCrossLineEnding();
endStyle.Length = automation.Container.Mapper.LengthToContainerCoordinates(20);
endStyle.Closed = true;
int annChildrenCount = automation.Container.Children.Count;
if(annChildrenCount == 0 )
{
MessageBox.Show("No lines found");
return;
}
AnnPolylineObject? annPolylineObject = automation.Container.Children[annChildrenCount-1] as AnnPolylineObject;
if (annPolylineObject != null)
{
annPolylineObject.StartStyle = startStyle;
annPolylineObject.EndStyle = endStyle;
automation.Invalidate(LeadRectD.Empty);
}
}
// Saves added annotations to an XML file
private void SaveAnnotations()
{
if(_documentViewer.Document == null)
{
MessageBox.Show("No file found in the Document Viewer");
return;
}
AnnAutomation automation = _documentViewer.Annotations.AutomationManager.Automations[0];
AnnContainer container = automation.Container;
AnnCodecs annCodecs = new AnnCodecs();
AnnDeserializeOptions options = new AnnDeserializeOptions();
annCodecs.DeserializeOptions = options;
// Hook to the DeserializeObject event to create the custom ending style instance when loading from xml
options.DeserializeObject += delegate (object sender2, AnnSerializeObjectEventArgs args)
{
AnnPolylineObject? polyLine = args.AnnObject as AnnPolylineObject;
if (polyLine != null && polyLine.Id == AnnObject.LineObjectId)
{
if (polyLine.StartStyle == null)
{
if (args.TypeName == "1") //The custom ending style id is 1
{
polyLine.StartStyle = new AnnCrossLineEnding();
}
}
else if (polyLine.EndStyle == null)
{
if (args.TypeName == "1") // The custom ending style id is 1
{
polyLine.EndStyle = new AnnCrossLineEnding();
}
}
}
}
!;
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.Filter = "XML File | *.xml";
if (saveFileDialog.ShowDialog() == DialogResult.OK)
{
annCodecs.Save(saveFileDialog.FileName, container, AnnFormat.Annotations, 1);
}
}
Run the project by pressing F5, or by selecting Debug -> Start Debugging.
If the steps were followed correctly, the application will run, and the Add Custom Line Ending and Save Custom Style buttons are now on the toolbar.
Load a document and use the line tool to draw a line on the document. Click the "Add Custom Line Ending" button to add the new line endings to the line.
Save the added annotations to an XML file by using the Save Annotations button.
This tutorial showed how to use the AnnPolylineObject
, AnnThumbStyle
, and AnnLineEnding
classes to create custom user-defined annotation objects, thumb control styles, and line ending styles.