Introducing custom symbols
The combination and manipulation of existing symbols results in a range of display options. CharacterMarkerSymbols, CartographicLineSymbols, and PictureFillSymbols in particular are flexible, and when you combine effects in a multilayer marker, line, or fill symbol, a wide range of effects can be achieved. If your drawing requirements are not met by these symbols, you can implement a custom symbol.
Knowledge of the available options in ArcMap will help you decide on the appropriate symbol, but you should also review the Display objects model; you may be able to manipulate the existing symbols programmatically in a way that you cannot achieve using the ArcMap user interface (UI).
A custom symbol is a relatively low-level solution; for example, it can exist without the presence of an MxDocument. A custom symbol should never rely on the attributes of a particular feature. If required, consider a custom renderer instead. Also, a symbol does not generally change the location of an item—projections or the transformation of your data may be more appropriate.
Since you have programmatic access to ScreenDisplay, it is possible to draw items directly to the display without using a symbol, feature, or element. This type of solution may be appropriate to temporarily highlight the result of an operation, for example, in the way that a feature is flashed on the display when you select that feature in the Identify dialog box.
If your drawings need to be persisted with the document or after refreshing the view, or if user interaction with the shape is required as with selection and editing, direct drawing may not be suitable.
Using custom symbols
Once you have decided on a custom symbol, you need to consider your implementation detail—that you can achieve the drawing effects you require.
When planning and testing your symbol, don't forget drawing efficiency and platform function support. Make sure you are familiar with the application programming interface (API) you're using, as well as surrounding issues. You may find it frustrating waiting for drawing to complete on complex maps. Also consider platform support for Windows Graphic Device Interface (GDI) and GDI+ functions—your symbol may be drawn to a screen, exported to a file, or output to any type of printer.
Windows GDI and GDI+ are mature platforms for developers, and you can find information in the references in the bibliography and MSDN for further reading on this extensive topic.
Similarly, if you choose alternative methods of drawing, efficiency and platform support should be considered in addition to any issues specific to the method you're using.
A custom MarkerSymbol, the simplest type of symbol, is used in the following example. Many issues of designing and implementing a custom symbol are common to implementing a marker, line, fill, text, or chart symbol and are discussed in this example.
Logo marker symbol example
- Example Code: Logo Marker symbol
- Description: This example provides a custom symbol that draws a company logo to symbolize a point. Simple custom functionality is provided to alter the colors of the different parts of the symbol, and a property page is provided to allow end users to edit the properties of the symbol.
- Design class: LogoMarkerSymbol is a subtype of the MarkerSymbol abstract class. LogoMarkerPropertyPage is an accompanying property page class.
Case for a custom MarkerSymbol
Imagine that the fictitious company logo shown here must be used to symbolize point features or graphic elements. You need to use it repeatedly, as part of a renderer or graphic, and at a wide variety of scales including large format output. You must also add the ability to alter the color of each section of the logo to indicate different divisions of the company.
To create a symbol such as this using the core ArcObjects symbol classes, you have a couple of options.
You could create a PictureMarkerSymbol, since it can be used effectively to portray any design. However, changing the colors of the logo sections would require a different bitmap for each possible color combination. Also, PictureMarkerSymbols may appear pixilated when zoomed in; using a high resolution bitmap can solve this problem but can increase memory requirements and slow draw speeds.
Alternatively, you could construct MultiLayerMarkerSymbol, with separate CharacterMarkerSymbols to represent the different parts of the logo. Because the symbol is drawn with vectors, there would be no resolution problems. However, you would need to create a specialist TrueType font with glyphs designed to represent the different sections of the logo. Since no core symbol coclass provides the functionality you require, you can create a custom marker symbol.
Creating a subtype of MarkerSymbol
If you decide to create a custom symbol, start by reviewing the Display objects diagram. All Symbol classes—markers, lines, fills, text, and charts—inherit from a common abstract class called Symbol.
Therefore, any type of custom symbol you create must implement the ISymbol interface, along with interfaces for cloning and persistence. Any class that implements ISymbol can be drawn to a device; however, classes specialize in the type of objects they can draw.
Looking again at the Display objects model diagram, you can see that each class that draws point features also inherits from the MarkerSymbol abstract class. Therefore, to create MarkerSymbol, you should also implement IMarkerSymbol, ISymbolRotation, IMapLevel, and IPropertySupport.
Many of the existing MarkerSymbol classes also implement IMarkerMask. This interface provides the ability to draw a standard mask around MarkerSymbol, which can be useful when placing multicolored symbols on a multicolored background, as it helps identify the boundaries of the symbol more clearly. This interface is, therefore, also an appropriate interface to implement in this case.
A marker mask can help to distinguish symbols from a similarly colored background.
MarkerSymbols also implement IDisplayName, which provides a string description of each type of symbol and which is used in the Symbol Properties Editor dialog box.
Creating LogoMarkerSymbol
In this example, you will create a subtype of MarkerSymbol, called LogoMarkerSymbol, registered to the Marker Symbols component category.
You will implement ISymbol, IMarkerSymbol, ISymbolRotation, IMapLevel, IMarkerMask, IDisplayName, and IPropertySupport, as well as the standard interfaces for cloning and persistence. To add the custom functionality, you will also create and implement a custom interface, ILogoMarkerSymbol.
Drawing techniques
There are a number of ways to draw a symbol, such as with the GeometryDraw class or the ISymbol.Draw or IDisplay.Draw methods. In this case, the shape of the logo would be stored as existing geometries (polygons, polylines, envelopes, and so forth). You are limited to drawing with existing geometries and symbols, but this approach does allow you to utilize the full functionality of ArcObjects to transform and adapt the shape and appearance of your symbol as required. This design may suit the production of a scale-dependent symbol, for example, that renders differently according to the current display scale.
You can perform drawing operations using third party drawing libraries or the low-level libraries available as part of the Windows platform. You may want to investigate the OpenGL standard or the Windows-specific DirectX libraries. Both were originally designed for use by C++ programmers and may not be a straightforward programming task in non-C++ environments.
In this example, you will use the Windows GDI functions to draw the symbol. Using GDI calls can produce efficient draw routines as well as flexibility in the type of drawing you can do. However, you need to be familiar with using GDI calls. Also, you may need to perform extensive mathematical calculations to transform your symbol's coordinates according to size, angle, and so on. Since Windows GDI functions require instructions in device coordinates, you will store the shape of the logo in device coordinates.
Implementing ISymbol
The ISymbol interface is responsible for drawing a geometry to the appropriate device context, using the correct appearance, shape, size, and location.
When a refresh event is called, ArcMap determines which shapes need to be drawn and in what order. ArcMap then uses the ISymbol interface to request that the shape draw itself.
Before ISymbol is drawn, its SetupDC method is called, which receives information about the drawing device. Then the Draw method is called, which receives the shape and location (the geometry) of the item to be drawn. Finally, the ResetDC method is called.
A general overview of the actions that should be performed by a custom symbol during each of these members is given below. This can be used as a guide for any symbol drawn using GDI functions.
If you use GDI calls to draw your symbol, you should use the SetupDC and ResetDC members of ISymbol to handle the addition and release of GDI objects, device contexts, and handles.
The actions performed in each of the draw methods are summarized here. You will use the CreatePen and CreateSolidBrush GDI functions to define the appearance of a LogoMarkerSymbol and the Chord and Polygon functions to draw the sections of the symbol to the device context. You will also use the SelectObject and DeleteObject GDI functions to maintain the device context objects correctly.
Add these declarations to your project (located in the Utility static class). Also, declare a user-defined type called POINTAPI, since GDI functions require coordinates to be defined as POINTAPI structures.
[C#]
public struct POINTAPI
{
public int x;
public int y;
}
[VB.NET]
Public Structure POINTAPI
Public x As Integer
Public y As Integer
End Structure
Now define an array of POINTAPI structures as a member variable of the LogoMarkerSymbol class. This array will hold the control points, which are the significant points you will use to define the shape and location of the logo in device coordinates.
[C#]
private Utility.POINTAPI[] m_coords = new Utility.POINTAPI[7];
[VB.NET]
Private m_coords As Utility.POINTAPI() = New Utility.POINTAPI(6) {}
The control points used by the drawing methods are stored in the m_coords array. They define the locations used for the Chord and Polygon GDI calls.
Now you can begin coding the ISymbol methods.
SetupDC method
In SetupDC, you need to prepare the class members to draw to the specific device, which is passed in as parameters to this method (hDC and displayTransformation).
- Store the passed-in information.
[C#]
m_trans = Transformation as IDisplayTransformation;
m_lhDC = hDC;
[VB.NET]
m_trans = TryCast(Transformation, IDisplayTransformation)
m_lhDC = hDC
- Set up the device ratio. See the Null transformations and resolution in the Draw and QueryBoundary section later for more information.
[C#]
SetupDeviceRatio(m_lhDC, m_trans);
[VB.NET]
SetupDeviceRatio(m_lhDC, m_trans)
- Calculate the size of the symbol in device coordinates. You will use these later in Draw.
[C#]
m_dDeviceRadius = (m_dSize / 2) * m_dDeviceRatio;
m_dDeviceXOffset = m_dXOffset * m_dDeviceRatio;
m_dDeviceYOffset = m_dYOffset * m_dDeviceRatio;
[VB.NET]
m_dDeviceRadius = (m_dSize / 2) * m_dDeviceRatio
m_dDeviceXOffset = m_dXOffset * m_dDeviceRatio
m_dDeviceYOffset = m_dYOffset * m_dDeviceRatio
- Store the rotation. You may need to rotate the symbol based on the ISymbolRotation interface.
[C#]
if (m_bRotWithTrans)
m_dMapRotation = m_trans.Rotation;
else
m_dMapRotation = 0;
[VB.NET]
If m_bRotWithTrans Then
m_dMapRotation = m_trans.Rotation
Else
m_dMapRotation = 0
End If
- Create the pens and brushes that you will use to fill and outline the sections of the symbol, and set up the ROP2 code used for the drawing. Save the existing values for all the GDI objects you will change so you can replace them in ResetDC.
[C#]
// Set up the pen that is used to outline the shapes.
// Multiplying by m_dDeviceRatio allows the pen size to scale.
m_lPen = Utility.CreatePen(0, Convert.ToInt32(1 * m_dDeviceRatio), System.Convert.ToInt32(m_colorBorder.RGB));
// Set the appropriate raster operation code for this draw according to the ISymbol interface.
m_lROP2Old = (esriRasterOpCode)Utility.SetROP2(hDC, System.Convert.ToInt32(m_lROP2));
// Set up three solid brushes to fill in the shapes with the different color fills.
m_lBrushTop = Utility.CreateSolidBrush(System.Convert.ToInt32(m_colorTop.RGB));
m_lBrushLeft = Utility.CreateSolidBrush(System.Convert.ToInt32(m_colorLeft.RGB));
m_lBrushRight = Utility.CreateSolidBrush(System.Convert.ToInt32(m_colorRight.RGB));
// Select the new pen, and store the old pen. This is essential during cleanup.
m_lOldPen = Utility.SelectObject(hDC, m_lPen);
[VB.NET]
' Set up the pen that is used to outline the shapes.
' Multiplying by m_dDeviceRatio allows the pen size to scale.
m_lPen = Utility.CreatePen(0, Convert.ToInt32(1 * m_dDeviceRatio), System.Convert.ToInt32(m_colorBorder.RGB))
' Set the appropriate raster operation code for this draw according to the ISymbol interface.
m_lROP2Old = CType(Utility.SetROP2(hDC, System.Convert.ToInt32(m_lROP2)), esriRasterOpCode)
' Set up three solid brushes to fill in the shapes with the different color fills.
m_lBrushTop = Utility.CreateSolidBrush(System.Convert.ToInt32(m_colorTop.RGB))
m_lBrushLeft = Utility.CreateSolidBrush(System.Convert.ToInt32(m_colorLeft.RGB))
m_lBrushRight = Utility.CreateSolidBrush(System.Convert.ToInt32(m_colorRight.RGB))
' Select the new pen, and store the old pen. This is essential during cleanup.
m_lOldPen = Utility.SelectObject(hDC, m_lPen)
Draw method
In the Draw method, determine the location of each control point for the symbol, and draw the symbol based on these locations.
- Check that the passed-in Geometry parameter contains a valid object, then cast it to a point.
[C#]
if (Geometry == null)
return;
if (!(Geometry is ESRI.ArcGIS.Geometry.IPoint))
return;
ESRI.ArcGIS.Geometry.IPoint point = (IPoint)Geometry;
[VB.NET]
If Geometry Is Nothing Then
Return
End If
If Not (TypeOf Geometry Is ESRI.ArcGIS.Geometry.IPoint) Then
Return
End If
Dim point As ESRI.ArcGIS.Geometry.IPoint = TryCast(Geometry, IPoint)
- Transform the point to device coordinates using the device context and DisplayTransformation you saved in SetupDC. Call the CalcCoords function. This function will calculate the location of each control point used by the GDI functions.
[C#]
int lCenterX = 0;
int lCenterY = 0;
Utility.FromMapPoint(m_trans, ref point, ref lCenterX, ref lCenterY);
double tempy1 = System.Convert.ToDouble(lCenterY);
CalcCoords(System.Convert.ToDouble(lCenterX), ref tempy1);
[VB.NET]
Dim lCenterX As Integer = 0
Dim lCenterY As Integer = 0
Utility.FromMapPoint(m_trans, point, lCenterX, lCenterY)
Dim tempy1 As Double = System.Convert.ToDouble(lCenterY)
CalcCoords(System.Convert.ToDouble(lCenterX), tempy1)
- Draw the separate sections of the symbol to the device.
[C#]
m_lOldBrush = Utility.SelectObject(m_lhDC, m_lBrushTop);
lResult = Utility.Chord(m_lhDC, m_coords[5].x, m_coords[5].y, m_coords[6].x, m_coords[6].y, m_coords[4].x, m_coords[4].y, m_coords[1].x, m_coords[1].y);
…
Utility.SelectObject(m_lhDC, m_lOldBrush);
[VB.NET]
m_lOldBrush = Utility.SelectObject(m_lhDC, m_lBrushTop)
lResult = Utility.Chord(m_lhDC, m_coords(5).x, m_coords(5).y, m_coords(6).x, m_coords(6).y, m_coords(4).x, m_coords(4).y, m_coords(1).x, m_coords(1).y)
…
Utility.SelectObject(m_lhDC, m_lOldBrush)
ResetDC method
Complete the drawing functions by selecting the original GDI pen and ROP code and releasing other GDI resources in the ResetDC method.
[C#]
m_lROP2 = (esriRasterOpCode)Utility.SetROP2(m_lhDC, System.Convert.ToInt32(m_lROP2Old));
Utility.SelectObject(m_lhDC, m_lOldPen);
Utility.DeleteObject(m_lPen);
…
m_trans = null;
m_lhDC = 0;
[VB.NET]
m_lROP2 = CType(Utility.SetROP2(m_lhDC, System.Convert.ToInt32(m_lROP2Old)), esriRasterOpCode)
Utility.SelectObject(m_lhDC, m_lOldPen)
Utility.DeleteObject(m_lPen)
…
m_trans = Nothing
m_lhDC = 0
If you use the Windows GDI to draw to the display, make sure you reselect the original GDI objects after drawing
QueryBoundary method
In the QueryBoundary method, you must populate the passed-in Boundary parameter, which is a polygon, with the shape of your symbol in map coordinates.
The nonsymmetrical nature of the logo means that it is simpler to calculate the exact shape of the symbol, rather than approximating a shape. You can create the shape of the logo by determining the radius of the circular section of the logo (dRad) and the length of the triangular sections of the symbol (dVal).
[C#]
ESRI.ArcGIS.Geometry.IPointCollection ptColl = null;
ESRI.ArcGIS.Geometry.ISegmentCollection segColl = null;
double dVal = 0; // dVal is the measurement of the short side of a triangle.
double dRad = 0;
ptColl = (IPointCollection)boundary;
segColl = (ISegmentCollection)boundary;
dRad = dMapSize / 2;
dVal = System.Math.Sqrt((dRad * dRad) / 2);
object missing = System.Reflection.Missing.Value;
ptColl.AddPoint(Utility.CreatePoint(point.X + dVal, point.Y - dVal), ref missing, ref missing);
ptColl.AddPoint(Utility.CreatePoint(point.X - dVal, point.Y - dVal), ref missing, ref missing);
ptColl.AddPoint(Utility.CreatePoint(point.X - dVal, point.Y + dVal), ref missing, ref missing);
IPoint p = ptColl.get_Point(0);
segColl.AddSegment((ISegment)Utility.CreateCircArc(point, ptColl.get_Point(2), ref p), ref missing, ref missing);
[VB.NET]
Dim ptColl As ESRI.ArcGIS.Geometry.IPointCollection = Nothing
Dim segColl As ESRI.ArcGIS.Geometry.ISegmentCollection = Nothing
Dim dVal As Double = 0 ' dVal is the measurement of the short side of a triangle.
Dim dRad As Double = 0
ptColl = CType(boundary, IPointCollection)
segColl = CType(boundary, ISegmentCollection)
dRad = dMapSize / 2
dVal = System.Math.Sqrt((dRad * dRad) / 2)
Dim missing As Object = System.Reflection.Missing.Value
ptColl.AddPoint(Utility.CreatePoint(point.X + dVal, point.Y - dVal), missing, missing)
ptColl.AddPoint(Utility.CreatePoint(point.X - dVal, point.Y - dVal), missing, missing)
ptColl.AddPoint(Utility.CreatePoint(point.X - dVal, point.Y + dVal), missing, missing)
Dim p As IPoint = ptColl.Point(0)
segColl.AddSegment(CType(Utility.CreateCircArc(point, ptColl.Point(2), p), ISegment), missing, missing)
QueryBoundary is a client-side storage function; therefore, you should add Point objects to the ISegmentCollection interface of the passed-in Boundary object.
ROP2 property
The ROP2 property indicates which type of pen (or raster operation) is used to draw a symbol. The ROP2 code of the device can easily be changed using the GDI functions SetROP2 and GetROP2, but remember to change the ROP2 code back to its original value in ResetDC because other symbols will be sharing the same device.
The esriRasterOpCodes enumeration defines the possible ROP2 codes. Changing the ROP2
code can dramatically alter the appearance of the symbol.
Null transformations and resolution in Draw and QueryBoundary
(converting from map to device units)
Because the scalar properties Size, XOffset, and YOffset hold values in points, you must convert from points to device units (pixels) before drawing the symbol (for example, during SetupDC) using device coordinates.
You can calculate a device resolution, m_dDeviceRatio, in pixels per point using DisplayTransformation passed to the SetupDC method.
SetupDeviceRatio calculates the number of pixels on the device that equal one printer's point—this is used to transform Size, XOffset, and YOffset from points to device units. The ReferenceScale of the Transformation, if present, is also accounted for here.
[C#]
private void SetupDeviceRatio(int hDC, ESRI.ArcGIS.Display.IDisplayTransformation displayTransform)
{
if (displayTransform != null)
{
if (displayTransform.Resolution != 0)
{
m_dDeviceRatio = displayTransform.Resolution / 72;
// Check the ReferenceScale of the display transformation. If not zero,
// adjust the size, XOffset and YOffset of the symbol you hold internally before drawing.
if (displayTransform.ReferenceScale != 0)
m_dDeviceRatio = m_dDeviceRatio * displayTransform.ReferenceScale / displayTransform.ScaleRatio;
}
}
[VB.NET]
Private Sub SetupDeviceRatio(ByVal hDC As Integer, ByVal displayTransform As IDisplayTransformation)
If Not displayTransform Is Nothing Then
If displayTransform.Resolution <> 0 Then
m_dDeviceRatio = displayTransform.Resolution / 72
' Check the ReferenceScale of the display transformation. If not zero,
' adjust the size, XOffset and YOffset of the symbol you hold internally before drawing.
If displayTransform.ReferenceScale <> 0 Then
m_dDeviceRatio = m_dDeviceRatio * displayTransform.ReferenceScale / displayTransform.ScaleRatio
End If
End If
In some situations, your symbol may be required to draw to a device context for which this parameter is null—for example, when drawing to the table of contents (TOC). In this case, you can get the resolution directly from the screen by using the GetDeviceCaps Windows API call.
[C#]
else
{
// If you dont have a display transformation, calculate the resolution
// from the actual device.
if (hDC != 0)
{
// Get the resolution from the device context hDC.
m_dDeviceRatio = System.Convert.ToDouble(Utility.GetDeviceCaps(hDC, Utility.LOGPIXELSX)) / 72;
}
else
{
// If invalid hDC, assume you're drawing to the screen.
m_dDeviceRatio = 1 / (Utility.TwipsPerPixelX() / 20); // 1 Point = 20 Twips.
}
}
}
[VB.NET]
Else
' If you dont have a display transformation, calculate the resolution
' from the actual device.
If hDC <> 0 Then
' Get the resolution from the device context hDC.
m_dDeviceRatio = System.Convert.ToDouble(Utility.GetDeviceCaps(hDC, Utility.LOGPIXELSX)) / 72
Else
' If invalid hDC, assume you're drawing to the screen.
m_dDeviceRatio = 1 / (Utility.TwipsPerPixelX() / 20) ' 1 Point = 20 Twips.
End If
End If
End Sub
Once the device ratio is calculated, Draw can use the FromMapPoint function (see accompanying sample code) to convert the geometry at which the symbol is drawn from map units to device units.
SetupDeviceRatio and FromMapPoint function together to transform map units to points.
Converting from points to map units
In the QueryBoundary method, you need to convert size, XOffset, and YOffset from points to map units to construct a geometry in map units representing the boundary of your symbol. Add a function called PointsToMap to complete this conversion; if no DisplayTransformation is present, use the value from SetupDeviceRatio.
[C#]
private double PointsToMap(ESRI.ArcGIS.Geometry.ITransformation displayTransform, double dPointSize)
{
double tempPointsToMap = 0;
IDisplayTransformation tempTransform = null;
if (displayTransform == null)
tempPointsToMap = dPointSize * m_dDeviceRatio;
else
{
tempTransform = (IDisplayTransformation)displayTransform;
tempPointsToMap = tempTransform.FromPoints(dPointSize);
}
return tempPointsToMap;
}
[VB.NET]
Private Function PointsToMap(ByVal displayTransform As ESRI.ArcGIS.Geometry.ITransformation, ByVal dPointSize As Double) As Double
Dim tempPointsToMap As Double = 0
Dim tempTransform As ESRI.ArcGIS.Display.IDisplayTransformation = Nothing
If displayTransform Is Nothing Then
tempPointsToMap = dPointSize * m_dDeviceRatio
Else
tempTransform = CType(displayTransform, IDisplayTransformation)
tempPointsToMap = tempTransform.FromPoints(dPointSize)
End If
Return tempPointsToMap
End Function
Drawing efficiently
Code the ISymbol methods efficiently, as they may be called frequently. There are a number of issues to consider to increase your symbol's drawing efficiency.
-
Calculating and storing the shape of the symbol
LogoMarkerSymbol calculates the shape and size of the symbol in two different coordinate spaces: device units for ISymbol.Draw and map coordinates for ISymbol.QueryBoundary and IMarkerMask.QueryMarkerMask. Consider the amount of processing each set of calculations will require and which will limit the speed of these functions. Storing and calculating the shape of the symbol in both map and device coordinates may enable you to create a more efficient symbol; however, using a single method can make your code simpler and more maintainable. Consider also the routines you use to manipulate the shape of your symbol; these may be called frequently. Therefore, providing a direct mathematical approach may be quicker than the query interfaces (QIs) and object creation you may need to use to convert using the geometrical transformations inside ArcObjects.
-
Caching the shape of the symbol
If more than one item is drawn with the same symbol, the drawing sequence starts with a call to SetupDC. Then Draw is called once for each item, and finally, ResetDC is called. The diagram below shows the sequence of calls for a SimpleRenderer and a ClassBreaksRenderer.
It may be most efficient to determine the size and shape of your symbol once in the SetupDC method, then use this repeatedly in the Draw method by changing its location, depending on how you draw your symbol.
- Efficient object creation
Consider how your code will scale when it is used for hundreds of features or elements. For example, QueryBoundary is called frequently by ArcMap when drawing FeatureLayer and when drawing elements. QueryBoundary is also called when displaying the TOC, saving the document, and displaying property pages that show the symbol. Ensure that your QueryBoundary routine is efficient enough not to impede these processes, which may interrupt your workflow. You may see a decrease in your draw times if you instantiate all the objects you need when the symbol is instantiated, then reset the values each time. For example, the QueryBoundsFromGeom function creates new Point objects to build the boundary of the symbol.
[C#]
ptColl.AddPoint(Utility.CreatePoint(point.X + dVal, point.Y - dVal), ref missing, ref missing);
ptColl.AddPoint(Utility.CreatePoint(point.X - dVal, point.Y - dVal), ref missing, ref missing);
ptColl.AddPoint(Utility.CreatePoint(point.X - dVal, point.Y + dVal), ref missing, ref missing);
IPoint p = ptColl.get_Point(0);
segColl.AddSegment((ISegment)Utility.CreateCircArc(point, ptColl.get_Point(2), ref p), ref missing, ref missing);
[VB.NET]
ptColl.AddPoint(Utility.CreatePoint(point.X + dVal, point.Y - dVal), missing, missing)
ptColl.AddPoint(Utility.CreatePoint(point.X - dVal, point.Y - dVal), missing, missing)
ptColl.AddPoint(Utility.CreatePoint(point.X - dVal, point.Y + dVal), missing, missing)
Dim p As IPoint = ptColl.Point(0)
segColl.AddSegment(CType(Utility.CreateCircArc(point, ptColl.Point(2), p), ISegment), missing, missing)
You could declare Point objects as member variables m_pt1, m_pt2, and m_pt3, instantiate them when the class is initialized, and reuse them in the QueryBoundsFromGeom function.
The following code can execute approximately 50 percent faster when you repeatedly call QueryGeometry.
[C#]
m_pt1.PutCoords(point.X + dVal, pPoint.Y - dVa);
m_pt2.PutCoords(point.X + dVal, pPoint.Y - dVal);
m_pt3.PutCoords(point.X + dVal, pPoint.Y - dVal);
ptColl.AddPoint(m_pt1, ref missing, ref missing);
ptColl.AddPoint(m_pt2, ref missing, ref missing);
ptColl.AddPoint(m_pt3, ref missing, ref missing);
IPoint p = ptColl.get_Point(0);
segColl.AddSegment((ISegment)Utility.CreateCircArc(point, ptColl.get_Point(2), ref p), ref missing, ref missing);
[VB.NET]
m_pt1.PutCoords(point.X + dVal, pPoint.Y - dVa)
m_pt2.PutCoords(point.X + dVal, pPoint.Y - dVal)
m_pt3.PutCoords(point.X + dVal, pPoint.Y - dVal)
ptColl.AddPoint(m_pt1, missing, missing)
ptColl.AddPoint(m_pt2, missing, missing)
ptColl.AddPoint(m_pt3, missing, missing)
Dim p As IPoint = ptColl.Point(0)
segColl.AddSegment(CType(Utility.CreateCircArc(point, ptColl.Point(2), p), ISegment), missing, missing)
Creating and implementing ILogoMarkerSymbol
You need to provide a way to change the colors of the separate sections of the logo design.
Create an interface called ILogoMarkerSymbol with four read-write properties: ColorLeft, ColorRight, ColorTop, and ColorBorder.
[C#]
public interface ILogoMarkerSymbol
{
ESRI.ArcGIS.Display.IColor ColorTop {get; set;}
ESRI.ArcGIS.Display.IColor ColorLeft {get; set;}
ESRI.ArcGIS.Display.IColor ColorRight {get; set;}
ESRI.ArcGIS.Display.IColor ColorBorder {get; set;}
}
[VB.NET]
Public Interface ILogoMarkerSymbol
Property ColorTop() As ESRI.ArcGIS.Display.IColor
Property ColorLeft() As ESRI.ArcGIS.Display.IColor
Property ColorRight() As ESRI.ArcGIS.Display.IColor
Property ColorBorder() As ESRI.ArcGIS.Display.IColor
End Interface
Implement ILogoMarkerSymbol in the LogoMarkerSymbol coclass. In each property, clone the incoming IColor parameters and set the appropriate member variable.
[C#]
ESRI.ArcGIS.Display.IColor ILogoMarkerSymbol.ColorBorder
{
get
{
// Return ColorBorder by Value.
IClone clone = m_colorBorder as IClone;
return clone.Clone() as IColor;
}
set
{
// Set ColorBorder by Value.
IClone clone = value as IClone;
m_colorBorder = clone.Clone() as IColor;
}
}
[VB.NET]
Private Property ColorBorder() As ESRI.ArcGIS.Display.IColor Implements ILogoMarkerSymbol.ColorBorder
Get
' Return ColorBorder by Value.
Dim clonee As IClone = TryCast(m_colorBorder, IClone)
Return TryCast(clonee.Clone(), IColor)
End Get
Set(ByVal value As ESRI.ArcGIS.Display.IColor)
' Set ColorBorder by Value.
Dim clonee As IClone = TryCast(value, IClone)
m_colorBorder = TryCast(clonee.Clone(), IColor)
End Set
End Property
Implementing IMarkerSymbol
Implementing IMarkerSymbol allows ArcGIS to recognize that your class can be used to symbolize points. This interface is commonly used by ArcGIS applications, for example, when setting color and size using the Element Properties dialog box.
By implementing IMarkerSymbol, you ensure that a symbol can interact with the ArcMap UI, for example, the Element Properties dialog box
Code the Color property to refer to the predominant color at the top of the logo by calling the ILogoMarkerSymbol.ColorTop property.
[C#]
ESRI.ArcGIS.Display.IColor ESRI.ArcGIS.Display.IMarkerSymbol.Color
{
get
{
// Return Color property by Value.
IClone clone = (IClone)m_colorTop;
return clone.Clone() as IColor;
}
set
{
// Set Color property by Value.
IClone clone = value as IClone;
m_colorTop = clone.Clone() as IColor;
}
}
[VB.NET]
Private Property Color() As ESRI.ArcGIS.Display.IColor Implements ESRI.ArcGIS.Display.IMarkerSymbol.Color
Get
' Return Color property by Value.
Dim clonee As IClone = CType(m_colorTop, IClone)
Return TryCast(clonee.Clone(), IColor)
End Get
Set(ByVal value As ESRI.ArcGIS.Display.IColor)
' Set Color property by Value.
Dim clonee As IClone = TryCast(value, IClone)
m_colorTop = TryCast(clonee.Clone(), IColor)
End Set
End Property
In the Angle property, you can add a check for angles greater than 360 degrees.
[C#]
double ESRI.ArcGIS.Display.IMarkerSymbol.Angle
{
get
{
//The angle is set as degrees and is also used internally as degrees in this class.
return m_dAngle;
}
set
{
// In this symbol, you can correct for an angle > 360 degrees.
if (value > 360)
value = value - (Convert.ToInt32(value / 360) * 360);
//The angle is set as degrees and is also used internally as degrees in this class.
m_dAngle = value;
}
}
[VB.NET]
Private Property Angle() As Double Implements ESRI.ArcGIS.Display.IMarkerSymbol.Angle
Get
'The angle is set as degrees and is also used internally as degrees in this class.
Return m_dAngle
End Get
Set(ByVal value As Double)
' In this symbol, you can correct for an angle > 360 degrees.
If value > 360 Then
value = value - (Convert.ToInt32(value / 360) * 360)
End If
'The angle is set as degrees, and is also used internally as degrees in this class.
m_dAngle = value
End Set
End Property
Implementing ISymbolRotation
If you want your symbol to adjust itself to a rotated map display, implement ISymbolRotation. Although it is not essential to implement this interface, it requires little extra coding, as you should have already added symbol rotation code to allow for the IMarkerSymbol.Angle property.
When you rotate the symbol for drawing, simply subtract the map rotation angle from IMarkerSymbol.Angle. You can get the map rotation value from DisplayTransformation passed in SetupDC:
[C#]
dAngle = 360.0 - (m_dAngle + m_dMapRotation);
[VB.NET]
dAngle = 360.0 - (m_dAngle + m_dMapRotation)
ISymbolRotation allows a symbol to work with the Data Frame tools in ArcMap.
Implementing IMapLevel
IMapLevel is commonly used by the ArcMap Advanced Drawing Options to draw joined and merged symbols, most commonly those used to draw cased roads. It is simple to implement, as you only need to store an integer value in the read-write MapLevelproperty.
[C#]
int ESRI.ArcGIS.Display.IMapLevel.MapLevel
{
get
{
return m_lMapLevel;
}
set
{
m_lMapLevel = value;
}
}
[VB.NET]
Private Property MapLevel() As Integer Implements ESRI.ArcGIS.Display.IMapLevel.MapLevel
Get
Return m_lMapLevel
End Get
Set(ByVal value As Integer)
m_lMapLevel = Value
End Set
End Property
This value will be used when your symbol is used in MultiLayerMarkerSymbol, when the Advanced Drawing Options indicate symbols must be drawn joined and merged.
Implementing IMarkerMask
IMarkerMask is used to draw a mask around a symbol. The QueryMarkerMask method should populate the Boundary parameter with the shape of the symbol if drawn at the specified geometry. The shape needs to be in map units, as it will be passed to the ISymbol.Draw method of IFillSymbol by ArcMap.
By implementing IMarkerMask, you allow the framework to draw a mask area around your symbol.
Ensure the boundary is empty, then use the same technique you used in ISymbol.QueryBoundary to populate Boundary.
[C#]
Boundary.SetEmpty();
IPoint point = Geometry as IPoint;
IDisplayTransformation displayTrans = (IDisplayTransformation)Transform;
QueryBoundsFromGeom(hDC, ref displayTrans, ref Boundary, ref point);
// Unlike ISymbol.QueryBoundary, QueryMarkerMask requires a simple geometry.
ITopologicalOperator topo = Boundary as ITopologicalOperator;
if (!topo.IsKnownSimple)
{
if (!topo.IsSimple)
topo.Simplify();
}
[VB.NET]
Boundary.SetEmpty()
Dim point As IPoint = TryCast(Geometry, IPoint)
Dim displayTrans As IDisplayTransformation = CType(Transform, IDisplayTransformation)
QueryBoundsFromGeom(hDC, displayTrans, Boundary, point)
' Unlike ISymbol.QueryBoundary, QueryMarkerMask requires a simple geometry.
Dim topo As ITopologicalOperator = TryCast(Boundary, ITopologicalOperator)
If (Not topo.IsKnownSimple) Then
If (Not topo.IsSimple) Then
topo.Simplify()
End If
End If
Implementing IPropertySupport
IPropertySupport is used to apply an object to one or more of the symbol's properties. It is a generic interface, which can be used by a client without the client knowing the exact nature of the underlying class.
[C#]
public bool Applies(object pUnk)
{
IColor color = pUnk as IColor;
ILogoMarkerSymbol logoMarkerSymbol = pUnk as ILogoMarkerSymbol;
if (null != color || null != logoMarkerSymbol)
return true;
return false;
}
[VB.NET]
Public Function Applies(ByVal pUnk As Object) As Boolean Implements ESRI.ArcGIS.esriSystem.IPropertySupport.Applies
Dim c As IColor = TryCast(pUnk, IColor)
Dim logoMarkerSym As ILogoMarkerSymbol = TryCast(pUnk, ILogoMarkerSymbol)
If Not Nothing Is c OrElse Not Nothing Is logoMarkerSym Then
Return True
End If
Return False
End Function
In the CanApply method, check if the object can be applied at the particular moment the method is called; a more complex class can involve checking the internal state of the class. In the case of LogoMarkerSymbol, the result does not depend on any state, so you can delegate the call to Applies.
In the Current property, check the incoming object reference—if it can be applied to any of the properties of the class, set the pUnk pointer to the current value of that property.
[C#]
object get_Current(object pUnk)
{
IColor color = pUnk as IColor;
if (null != color)
{
IColor currentColor = ((IMarkerSymbol)this).Color;
return (object)currentColor;
}
ILogoMarkerSymbol logoMarkerSymbol = pUnk as ILogoMarkerSymbol;
{
IClone clone = ((IClone)this).Clone();
return (object)clone;
}
}
[VB.NET]
Private ReadOnly Property Current(ByVal pUnk As Object) As Object Implements ESRI.ArcGIS.esriSystem.IPropertySupport.Current
Get
Dim c As IColor = TryCast(pUnk, IColor)
If Not Nothing Is c Then
Dim currentColor As IColor = (CType(Me, IMarkerSymbol)).Color
Return CObj(currentColor)
End If
Dim logoMarkerSym As ILogoMarkerSymbol = TryCast(pUnk, ILogoMarkerSymbol)
Dim clonee As IClone = (CType(Me, IClone)).Clone()
Return CObj(clonee)
End Get
End Property
In the Apply
method, set the incoming object as the appropriate member of your symbol class. In the code below, the incoming object can be an instance of the LogoMarkerSymbol class itself, in which case the values of the incoming object are assigned to the class member by using the IClone.Assign
method.
[C#]
object Apply(object newObject)
{
object oldObject = null;
IColor color = newObject as IColor;
if (null != color)
{
oldObject = ((IPropertySupport)this).get_Current(newObject);
((IMarkerSymbol)this).Color = color;
}
ILogoMarkerSymbol logoMarkerSymbol = newObject as ILogoMarkerSymbol;
{
oldObject = ((IPropertySupport)this).get_Current(newObject);
IClone clone = (IClone)newObject;
((IClone)this).Assign(clone);
}
return oldObject;
}
[VB.NET]
Private Function Apply(ByVal newObject As Object) As Object Implements ESRI.ArcGIS.esriSystem.IPropertySupport.Apply
Dim oldObject As Object = Nothing
Dim c As IColor = TryCast(newObject, IColor)
If Not Nothing Is c Then
oldObject = (CType(Me, IPropertySupport)).Current(newObject)
CType(Me, IMarkerSymbol).Color = c
End If
Dim logoMarkerSym As ILogoMarkerSymbol = TryCast(newObject, ILogoMarkerSymbol)
oldObject = (CType(Me, IPropertySupport)).Current(newObject)
Dim clonee As IClone = CType(newObject, IClone)
CType(Me, IClone).Assign(clonee)
Return oldObject
End Function
To be consistent with core symbols, you should at least apply an IColor object to the IMarkerSymbol.Color property, although you can extend this to allow the setting of any of your properties.
Initializing members
Add a private routine to the LogoMarkerSymbol class to initialize the member variables. Call this function from the class initialize.
[C#]
private void InitializeMembers()
{
// Set up default values as far as possible.
m_lhDC = 0;
…
IColor color = null;
IClone clone = null;
color = ESRI.ArcGIS.ADF.Converter.ToRGBColor(Color.Red);
m_colorTop = clone.Clone() as IColor;
…
// ISymbol property defaults.
m_lROP2 = esriRasterOpCode.esriROPCopyPen;
…
m_bRotWithTrans = true;
}
[VB.NET]
Private Sub InitializeMembers()
' Set up default values as far as possible.
m_lhDC = 0
…
Dim c As IColor = Nothing
Dim clonee As IClone = Nothing
c = ESRI.ArcGIS.ADF.Converter.ToRGBColor(Drawing.Color.Red)
clonee = CType(c, IClone)
m_colorTop = TryCast(clonee.Clone(), IColor)
…
m_lROP2 = esriRasterOpCode.esriROPCopyPen
…
m_bRotWithTrans = True
End Sub
Placing the initialization code in a separate function allows you to reset LogoMarkerSymbol to default values at any point, which is particularly useful when implementing persistence.
Implementing cloning and persistence
Cloning and persistence are essential functions for any symbol. Every time a reference to a symbol is passed to a property page, the symbol object is cloned. This allows any changes made to the symbol to be discarded and also allows the change to be added to the Undo/Redo stack in ArcMap. Every time a map document is saved, all the symbols applied to features and graphic elements are persisted. Add a standard implementation of persistence and cloning for the LogoMarkerSymbol example.
In the IPersistVariant.Save method, save the persistence version number first, then each required member of the class.
[C#]
void ESRI.ArcGIS.esriSystem.IPersistVariant.Save(ESRI.ArcGIS.esriSystem.IVariantStream Stream)
{
// Write the persistence version number first.
Stream.Write(m_lCurrPersistVers);
// Persist ISymbol properties.
Stream.Write(m_lROP2);
// Persist IMarkerSymbol properties.
Stream.Write(m_dSize);
Stream.Write(m_dXOffset);
…
}
[VB.NET]
Private Sub Save(ByVal Stream As ESRI.ArcGIS.esriSystem.IVariantStream) Implements ESRI.ArcGIS.esriSystem.IPersistVariant.Save
' Write the persistence version number first.
Stream.Write(m_lCurrPersistVers)
'Persist ISymbol properties.
Stream.Write(m_lROP2)
' Persist IMarkerSymbol properties.
Stream.Write(m_dSize)
Stream.Write(m_dXOffset)
…
End Sub
In IPersistVariant.Load, check the persistence version number first. Call the InitializeMembers function to set default values into the symbol, before reading values from the Stream, in the same order they were saved to set the member variables.
[C#]
void ESRI.ArcGIS.esriSystem.IPersistVariant.Load(ESRI.ArcGIS.esriSystem.IVariantStream Stream)
{
// Read the persisted version number first.
int lSavedVers = 0;
lSavedVers = Convert.ToInt32(Stream.Read());
if ((lSavedVers > m_lCurrPersistVers) | (lSavedVers <= 0))
{
throw new Exception("Failed to read from stream");
}
// Set members to default values.
InitializeMembers();
// Load the first persistance pattern.
if (lSavedVers == 1)
{
m_lROP2 = (esriRasterOpCode)Stream.Read();
m_dSize = Convert.ToDouble(Stream.Read());
m_dXOffset = Convert.ToDouble(Stream.Read());
…
}
}
[VB.NET]
Private Sub Load(ByVal Stream As ESRI.ArcGIS.esriSystem.IVariantStream) Implements ESRI.ArcGIS.esriSystem.IPersistVariant.Load
' Read the persisted version number first.
Dim lSavedVers As Integer = 0
lSavedVers = Convert.ToInt32(Stream.Read())
If (lSavedVers > m_lCurrPersistVers) Or (lSavedVers <= 0) Then
Throw New Exception("Failed to read from stream")
End If
' Set members to default values.
InitializeMembers()
' Load the first persistance pattern.
If lSavedVers = 1 Then
m_lROP2 = CType(Stream.Read(), esriRasterOpCode)
m_dSize = Convert.ToDouble(Stream.Read())
m_dXOffset = Convert.ToDouble(Stream.Read())
…
End If
End Sub
In the IClone.IsEqual
method, you may decide that the source and other symbols are equal if the RGB property values of IColor members are equivalent, instead of casting
to IClone the color class and checking its IsEqual member in turn.
[C#]
bool ESRI.ArcGIS.esriSystem.IClone.IsEqual(ESRI.ArcGIS.esriSystem.IClone other)
{
bool tempIClone_IsEqual = false;
LogoMarkerSym.ILogoMarkerSymbol srcLogoSym = null;
LogoMarkerSym.ILogoMarkerSymbol pRecLogoSym = null;
IMarkerSymbol srcMarkerSym = null;
IMarkerSymbol recMarkerSym = null;
…
if (other != null)
{
if (other is LogoMarkerSym.ILogoMarkerSymbol)
{
// Check for equality on default interface.
srcLogoSym = other as ILogoMarkerSymbol;
pRecLogoSym = this;
tempIClone_IsEqual = tempIClone_IsEqual & (System.Drawing.ColorTranslator.FromOle(System.Convert.ToInt32(pRecLogoSym.ColorBorder.RGB)).Equals(System.Drawing.ColorTranslator.FromOle(System.Convert.ToInt32(srcLogoSym.ColorBorder.RGB))));
tempIClone_IsEqual = tempIClone_IsEqual & (System.Drawing.ColorTranslator.FromOle(System.Convert.ToInt32(pRecLogoSym.ColorLeft.RGB)).Equals(System.Drawing.ColorTranslator.FromOle(System.Convert.ToInt32(srcLogoSym.ColorLeft.RGB))));
…
[VB.NET]
Private Function IsEqual(ByVal other As ESRI.ArcGIS.esriSystem.IClone) As Boolean Implements ESRI.ArcGIS.esriSystem.IClone.IsEqual
Dim tempIClone_IsEqual As Boolean = False
Dim srcLogoSym As ILogoMarkerSymbol = Nothing
Dim pRecLogoSym As ILogoMarkerSymbol = Nothing
Dim srcMarkerSym As IMarkerSymbol = Nothing
Dim recMarkerSym As IMarkerSymbol = Nothing
…
If Not other Is Nothing Then
If TypeOf other Is ILogoMarkerSymbol Then
' Check for equality on default interface.
srcLogoSym = TryCast(other, ILogoMarkerSymbol)
pRecLogoSym = Me
tempIClone_IsEqual = tempIClone_IsEqual And (System.Drawing.ColorTranslator.FromOle(System.Convert.ToInt32(pRecLogoSym.ColorBorder.RGB)).Equals(System.Drawing.ColorTranslator.FromOle(System.Convert.ToInt32(srcLogoSym.ColorBorder.RGB))))
tempIClone_IsEqual = tempIClone_IsEqual And (System.Drawing.ColorTranslator.FromOle(System.Convert.ToInt32(pRecLogoSym.ColorLeft.RGB)).Equals(System.Drawing.ColorTranslator.FromOle(System.Convert.ToInt32(srcLogoSym.ColorLeft.RGB))))
…