Creating custom symbols


Summary
A symbol is a class that can draw things, such as points, lines, and areas, to a display. The Display object model contains a range of MarkerSymbols, LineSymbols, and FillSymbols that can be used in conjunction with graphic elements or renderers to draw features, graphics, map surrounds, and so on. These can be combined into multilayer symbols to achieve a more complex display.
If none of the standard symbols can draw your features or graphics the way you require, you can implement your own custom symbol. Custom symbols can be applied to any graphic element or feature, take part in other multilayer symbols and renderers, can be edited, and can be saved and retrieved as StyleGalleryItems.
This topic explains how to create a custom symbol.


About 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 on 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're 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 graphics device interface (GDI) functions—your symbol may be drawn to a screen, exported to a file, or output to any type of printer.
Windows GDI is a mature platform for developers, and you can find information at the Microsoft Developer Network Web site (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 example in this topic. Many issues of designing and implementing a custom symbol are common to implementing a marker, line, fill, text, or chart symbol.

Case for a custom MarkerSymbol

Imagine that the following fictitious company logo 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. See the following illustration:
To create a symbol such as this using the core ArcObjects symbol classes, you have the following options:
  • You could create a PictureMarkerSymbol, since it can be used effectively to portray any design. However, changing the colors of the logo sections requires a different bitmap for each possible color combination. Also, PictureMarkerSymbols may appear pixelated when zoomed in; using a high-resolution bitmap can solve this problem but increase memory requirements and slow draw speeds. See the following illustration:
  • Alternatively, you could construct a 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, Symbol. See the following illustration:
 
 
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. See the following illustration:

 
Looking again at the previous object 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 also appropriate to implement in this case. See the following illustration:
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 is used on the Symbol Properties Editor dialog box.

Creating LogoMarkerSymbol

Create a subtype of MarkerSymbol—LogoMarkerSymbol—registered to the Marker Symbols component category. See the following illustration:
Implement ISymbol, IMarkerSymbol, ISymbolRotation, IMapLevel, IMarkerMask, IDisplayName, and IPropertySupport, as well as the standard interfaces for cloning and persistence. To add the custom functionality, also create and implement the 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 is stored as existing geometries (polygons, polylines, envelopes, and so forth). You are limited to drawing with existing geometries and symbols, but this approach allows 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.
In this example, the Windows GDI functions are used 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, 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 follows. This can be used as a guide for any symbol drawn using GDI functions.
If you use GDI calls to draw your symbol, 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. 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. 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, POINTAPI, since GDI functions require coordinates to be defined as POINTAPI structures. See the following code example:
[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 holds the control points, which are the significant points you used to define the shape and location of the logo in device coordinates. See the following code example:
[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. See the following illustration:
Now you can begin coding the ISymbol methods.

SetupDC method

In SetupDC, prepare the class members to draw to the specific device, which is passed in as parameters to this method (hDC and displayTransformation) as shown in the following steps:
  1. Store the passed-in information. See the following code example:
[C#]
m_trans = Transformation as IDisplayTransformation;
m_lhDC = hDC;
[VB.NET]
m_trans = TryCast(Transformation, IDisplayTransformation)
m_lhDC = hDC
  1. Set up the device ratio. For more information, see the Null transformations and resolution in the Draw method and QueryBoundary sections. See the following code example:
[C#]
SetupDeviceRatio(m_lhDC, m_trans);
[VB.NET]
SetupDeviceRatio(m_lhDC, m_trans)
  1. Calculate the size of the symbol in device coordinates to use later in Draw. See the following code example:
[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
  1. Store the rotation. You may need to rotate the symbol based on the ISymbolRotation interface. See the following code example:
[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
  1. Create the pens and brushes used to fill and outline the sections of the symbol, and set up the ROP2 (raster operation) code used for the drawing. Save the existing values for all the GDI objects to be changed so you can replace them in ResetDC. See the following code example:
[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 as shown in the following code:
  1. Check that the passed-in Geometry parameter contains a valid object, then cast it to a point. See the following code example:
[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)
  1. Transform the point to device coordinates using the device context and DisplayTransformation you saved in SetupDC. Call the CalcCoords function. This function calculates the location of each control point used by the GDI functions. See the following code example:
[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)
  1. Draw the separate sections of the symbol to the device. See the following code example:
[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. See the following code example:
[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's easier to calculate the exact shape of the symbol, than to approximate it. 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). See the following code example:
[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 share the same device.
The esriRasterOpCodes enumeration defines the possible ROP2 codes. Changing the ROP2code can dramatically alter the appearance of the symbol. See the following illustration:

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.
See the following code example:
[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. See the following code example:
[C#]
else
{
    // If you don't 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 don't 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 to convert the geometry at which the symbol is drawn from map units to device units.
SetupDeviceRatio and FromMapPoint work 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 PointsToMap function to complete this conversion; if no DisplayTransformation is present, use the value from SetupDeviceRatio. See the following code example:
[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. The following are the 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 requires and which limits 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 can be called frequently. Consequently, providing a direct mathematical approach can 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. Draw is then called once for each item, and finally, ResetDC is called. The following illustration shows the sequence of calls for a SimpleRenderer and a ClassBreaksRenderer:



    It can be most efficient to determine the size and shape of your symbol once in the SetupDC method and 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's used for hundreds of features or elements. For example, QueryBoundary is called frequently by ArcMap when drawing FeatureLayer and 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 and reset the values each time. For example, the QueryBoundsFromGeom function creates Point objects to build the boundary of the symbol. See the following code example:
[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 example 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 the ILogoMarkerSymbol interface with the following read and write properties:
  • ColorLeft
  • ColorRight
  • ColorTop
  • ColorBorder
See the following code example:
[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. See the following code example:
[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 clone As IClone = TryCast(m_colorBorder, IClone)
    Return TryCast(clone.Clone(), IColor)
    End Get
    Set(ByVal Value As ESRI.ArcGIS.Display.IColor)
    ' Set ColorBorder by value.
    Dim clone As IClone = TryCast(Value, IClone)
    m_colorBorder = TryCast(clone.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, on 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. See the following code example:
[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 clone As IClone = CType(m_colorTop, IClone)
    Return TryCast(clone.Clone(), IColor)
    End Get
    Set(ByVal Value As ESRI.ArcGIS.Display.IColor)
    ' Set Color property by value.
    Dim clone As IClone = TryCast(Value, IClone)
    m_colorTop = TryCast(clone.Clone(), IColor)
    End Set
End Property
In the Angle property, add a check for angles greater than 360 degrees. See the following code example:
[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

To have your symbol adjust itself to a rotated map display, implement ISymbolRotation. Although it is not essential to implement this interface, it requires little extra coding, since you've 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. See the following code example:
[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. See the following screen shot:

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 and write MapLevel property. See the following code example:
[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 is used when your symbol is used in MultiLayerMarkerSymbol, when the Advanced Drawing Options indicate that symbols must be drawn joined and merged. See the following illustration:

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 is 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 that the boundary is empty, then use the same technique you used in ISymbol.QueryBoundary to populate Boundary. See the following code example:
[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. See the following code example:
[C#]
public bool Applies(object Unk)
{
    IColor color = Unk as IColor;
    ILogoMarkerSymbol logoMarkerSymbol = Unk as ILogoMarkerSymbol;
    if (null != color || null != logoMarkerSymbol)
        return true;
    return false;
}
[VB.NET]
Public Function Applies(ByVal Unk As Object) As Boolean Implements ESRI.ArcGIS.esriSystem.IPropertySupport.Applies
    Dim color As IColor = TryCast(Unk, IColor)
    Dim logoMarkerSym As ILogoMarkerSymbol = TryCast(Unk, ILogoMarkerSymbol)
    If Not Nothing Is color 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 Unk pointer to the current value of that property. See the following code example:
[C#]
object get_Current(object Unk)
{
    IColor color = Unk as IColor;
    if (null != color)
    {
        IColor currentColor = ((IMarkerSymbol)this).Color;
        return (object)currentColor;
    }

    ILogoMarkerSymbol logoMarkerSymbol = Unk as ILogoMarkerSymbol;
    {
        IClone clone = ((IClone)this).Clone();
        return (object)clone;
    }
}
[VB.NET]
Private ReadOnly Property Current(ByVal Unk As Object) As Object Implements ESRI.ArcGIS.esriSystem.IPropertySupport.Current
Get
Dim color As IColor = TryCast(Unk, IColor)
If Not Nothing Is color Then
    Dim currentColor As IColor = (CType(Me, IMarkerSymbol)).Color
    Return CObj(currentColor)
End If
Dim logoMarkerSym As ILogoMarkerSymbol = TryCast(Unk, ILogoMarkerSymbol)
Dim clone As IClone = (CType(Me, IClone)).Clone()
Return CObj(clone)
End Get
End Property
In the Apply method, set the incoming object as the appropriate member of your symbol class. In the following code example, 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 color As IColor = TryCast(newObject, IColor)
    If Not Nothing Is color Then
        oldObject = (CType(Me, IPropertySupport)).Current(newObject)
        CType(Me, IMarkerSymbol).Color = color
    End If
    Dim logoMarkerSym As ILogoMarkerSymbol = TryCast(newObject, ILogoMarkerSymbol)
    oldObject = (CType(Me, IPropertySupport)).Current(newObject)
    Dim clone As IClone = CType(newObject, IClone)
    CType(Me, IClone).Assign(clone)
    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. See the following code example:
[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 color As IColor = Nothing
    Dim clone As IClone = Nothing
    
    color = ESRI.ArcGIS.ADF.Converter.ToRGBColor(Drawing.Color.Red)
    clone = CType(color, IClone)
    m_colorTop = TryCast(clone.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. See the following code example:
[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. See the following code example:
[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 persistence 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 persistence 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 red, green, and blue (RGB) property values of IColor members are equivalent instead of casting to IClone the color class and checking its IsEqual member in turn. See the following code example:
[C#]
bool ESRI.ArcGIS.esriSystem.IClone.IsEqual(ESRI.ArcGIS.esriSystem.IClone other)
{
    bool tempIClone_IsEqual = false;
    LogoMarkerSym.ILogoMarkerSymbol srcLogoSym = null;
    LogoMarkerSym.ILogoMarkerSymbol recLogoSym = 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;
            recLogoSym = this;
            tempIClone_IsEqual = tempIClone_IsEqual & 
                (System.Drawing.ColorTranslator.FromOle(System.Convert.ToInt32
                (recLogoSym.ColorBorder.RGB)).Equals
                (System.Drawing.ColorTranslator.FromOle(System.Convert.ToInt32
                (srcLogoSym.ColorBorder.RGB))));
            tempIClone_IsEqual = tempIClone_IsEqual & 
                (System.Drawing.ColorTranslator.FromOle(System.Convert.ToInt32
                (recLogoSym.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 recLogoSym 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)
            recLogoSym = Me
            tempIClone_IsEqual = tempIClone_IsEqual And (System.Drawing.ColorTranslator.FromOle(System.Convert.ToInt32(recLogoSym.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(recLogoSym.ColorLeft.RGB)).Equals(System.Drawing.ColorTranslator.FromOle(System.Convert.ToInt32(srcLogoSym.ColorLeft.RGB))))
            ...

Symbol property pages

All core symbols have a property page that's displayed on the Symbol Property Editor dialog box. This allows you to edit the symbol properties on the UI. Complex symbols have multiple property pages displayed as separate tabs in the dialog box. See the following screen shot:
It is not essential for a custom Symbol class to have an accompanying property page, but it is recommended. If you're only applying and editing a symbol programmatically, you don't need to implement a property page. However, users may be confused when they try to change the symbol properties in the editor. See the following illustration:
Use the ArcGIS Visual Studio .NET Integration Framework to create a LogoMarkerPropertyPage class, and register the class to the Symbol Property Pages component category.
For more information on the ArcGIS Visual Studio .NET Integration Framework, see ArcGIS Visual Studio IDE Integration Framework for extending ArcObjects.
The Logo Marker Symbol property page is displayed on the Symbol Property Editor dialog box. When you open the Symbol Editor for LogoMarkerSymbol, the dialog box also has a Mask property page—this is displayed automatically if the current symbol implements IMarkerMask.

Implementing property page interfaces for LogoMarkerPropertyPage

The interfaces implemented on LogoMarkerPropertyPage are dependent on the development environment. In either case, the basic structure of the Property Page class is similar. In the Applies method, return true if you receive a LogoMarkerSymbol. See the following code example:
[C#]
bool Applies(object unkArray)
{
    object[] arr = (object[])unkArray;
    if (null == arr || 0 == arr.Length)
        return false;

    for (int i = 0; i < arr.Length; i++)
    {
        if (arr[i] is ILogoMarkerSymbol)
            return true;
    }
    return false;
}
[VB.NET]
Private Function Applies(ByVal unkArray As Object) As Boolean Implements IPropertyPageContext.Applies
    Dim arr As Object() = CType(unkArray, Object())
    If Nothing Is arr OrElse 0 = arr.Length Then
        Return False
    End If
    
    Dim i As Integer = 0
    Do While i < arr.Length
        If TypeOf arr(i) Is ILogoMarkerSymbol Then
            Return True
        End If
        i + = 1
    Loop
    Return False
End Function
In the SetObjects method, check that you receive a LogoMarkerSymbol object, then cache the object inside the class ObjectBag dictionary. See the following code example:
[C#]
void IComPropertyPage.SetObjects(ESRI.ArcGIS.esriSystem.ISet objects)
{
    if (objects == null || objects.Count == 0)
        return ;

    //Prepare to hold on to editable objects.
    if (m_objectBag == null)
        m_objectBag = new Dictionary < string, object > ();
    else
        m_objectBag.Clear();

    objects.Reset();
    object testObject = null;
    while ((testObject = objects.Next()) != null)
    {
        if (testObject != null)
        {
            //If the input object is LogoMarkerSymbol, add it to the collection to cache it.
            if (testObject is ILogoMarkerSymbol)
                m_objectBag.Add("logoMarker", (ILogoMarkerSymbol)testObject);
        }
    }
}
[VB.NET]
Private Sub SetObjects(ByVal objects As ESRI.ArcGIS.esriSystem.ISet) Implements IComPropertyPage.SetObjects
    If objects Is Nothing OrElse objects.Count = 0 Then
        Return
    End If
    
    'Prepare to hold on to editable objects.
    If m_objectBag Is Nothing Then
        m_objectBag = New Dictionary(Of String, Object)()
    Else
        m_objectBag.Clear()
    End If
    
    objects.Reset()
    Dim testObject As Object = Nothing
    testObject = objects.Next()
    Do While Not testObject Is Nothing
        If Not testObject Is Nothing Then
            'If the input object is LogoMarkerSymbol, add it to the collection to cache it.
            If TypeOf testObject Is ILogoMarkerSymbol Then
                m_objectBag.Add("logoMarker", CType(testObject, ILogoMarkerSymbol))
            End If
        End If
        testObject = objects.Next()
    Loop
End Sub

Implementing ISymbolPropertyPage

The Size, XOffset, and YOffset properties of MarkerSymbol are returned and set onto LogoMarkerSymbol as a printer's point measurement. However, you can also define the properties of a symbol using a different system of measurement, for example, centimeters or millimeters.
If the ability to use different systems of measurement were encapsulated in the Symbol classes themselves, not only would each class contain similar conversion code, but it would complicate the use of the classes. Therefore, this functionality is added at the property page level by implementing the specialist ISymbolPropertyPage interface.
At the top right-hand corner of the Symbol Property Editor dialog box is the Units combo box. When you change the Units selection, the property sheet to which the page belongs (in this case the Symbol Editor) calls the active property page to tell it what type of units you have selected by setting the ISymbolPropertyPage.Units property. See the following screen shot:
Add the SetPageDirty method, and call it whenever the user applies changes to the UI. Use this method to apply these changes to the object and to the edited symbol on the Symbol Property Editor dialog box, as well as to activate the Apply and OK buttons. See the following code example:
[C#]
private void SetPageDirty(bool dirty)
{
    if (m_dirtyFlag != dirty)
    {
        m_dirtyFlag = dirty;
        if (m_pageSite != null)
            m_pageSite.PageChanged();

        ((IComPropertyPage)this).Apply();
    }
}
[VB.NET]
Private Sub SetPageDirty(ByVal dirty As Boolean)
    If m_dirtyFlag <> dirty Then
        m_dirtyFlag = dirty
        If Not m_pageSite Is Nothing Then
            m_pageSite.PageChanged()
        End If
        
        CType(Me, IComPropertyPage).Apply()
    End If
End Sub
In the UI event handler methods, call method SetPageDirty to apply the changes. See the following code example:
[C#]
private void btnLeft_Click(object sender, EventArgs e)
{
    Color color = GetColorFromUser();
    if (Color.Empty != color)
    {
        pnlLeft.BackColor = color;
        m_colLeft = color;
        SetPageDirty(true);
    }
}
[VB.NET]
Private Sub btnLeft_Click(ByVal sender As Object, ByVal e As EventArgs) Handles btnLeft.Click
    Dim color As Color = GetColorFromUser()
    If Not color.Empty.Equals(color) Then
        pnlLeft.BackColor = color
        m_colLeft = color
        SetPageDirty(True)
    End If
End Sub


See Also:

Working with symbols and colors




Development licensing Deployment licensing
ArcGIS for Desktop Basic ArcGIS for Desktop Basic
ArcGIS for Desktop Standard ArcGIS for Desktop Standard
ArcGIS for Desktop Standard ArcGIS for Desktop Advanced