Extending ArcObjects  

Logo Marker Symbol Example

This topic is relevant for the following:
Product(s): ArcGIS Desktop: All
Version(s): 9.0, 9.1, 9.2
Language(s): VB6, VC++
Experience level(s): Intermediate to advanced



In this section:

  1. Case for a custom Marker symbol
  2. Creating a subtype of MarkerSymbol
  3. Creating the LogoMarkerSymbol
  4. Symbol Property Pages

Logo marker symbol example

Object Model Diagram Click here

Example Code Click here

Description This example provides a custom symbol, which 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 also provided to allow end users to edit the properties of the symbol.

Design Coclass LogoMarkerSymbol is a subtype of the MarkerSymbol abstract class. LogoMarkerPropertyPage is an accompanying property page coclass.

License required ArcView or above

Libraries Framework, Display, DisplayUI, Geometry, and System

Languages Visual Basic (some restrictions), Visual C++

Categories Marker Symbols, ESRI Property Pages, and Symbol Property Pages

Interfaces IClone, ISymbol, IPersist, IMarkerMask, IMapLevel, IMarkerSymbol, ISymbolRotation, IDisplayName, IPropertySupport, IComPropertyPage, IPropertyPageContext, ISymbolPropertyPage

How to use

  1. If using VB, register LogoMarkerSymbolVB.dll and double-click the LogoMarkerSymbolVB.reg file to register to component categories.

    If using VC++, open and build the project LogoMarkerSymbolVC.dsp to register the DLL and register to component categories.

  2. Open ArcMap and add a layer with point features or a marker graphic element. Open the Symbol Selector for the item.

    For a layer, right-click the layer in the ArcMap table of contents, click Properties, then in the Layer Properties dialog box, click the Symbology tab. The Single Symbol renderer should be selected by default; click the Symbol button to show the Symbol Selector.

    For a marker element, right-click the element and click Properties. In the Properties dialog box, make sure the Symbol tab is selected, and click the Change Symbol button.

  3. In the Symbol Selector dialog box, click the Properties button to display the Symbol Editor.
  4. In the Symbol Editor dialog box, pull down the Type list box and click Logo Marker Symbol.

    You can now set the properties of a LogoMarkerSymbol. Click OK to select the symbol and return to the Symbol Selector.


Case for a custom Marker symbol

Imagine that the fictitious company logo shown here must be used to symbolize point features or graphic elements. You must be able 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 like this by using the core ArcObjects symbol classes, you have a couple of options available.

You could create a PictureMarkerSymbol, as this may 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 pixelated when zoomed in; using a high resolution bitmap may solve this problem, but can also increase memory requirements, and slow down draw speeds.

Alternatively, you could construct a MultiLayerMarkerSymbol, with separate CharacterMarkerSymbols to represent the different parts of the logo. As 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.

As no core symbol coclass provides the functionality you require, you can create a custom marker symbol.

This example provides a custom symbol that draws a company logo. Different colors can be used for the sections of the symbol.


Creating a subtype of MarkerSymbol

If you decide to create a custom symbol, start by reviewing the Display object diagram. You will see that 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 begin by implementing 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 object model diagram, you can see that each coclass for drawing point features also inherits from the MarkerSymbol abstract class.

Therefore, to create a MarkerSymbol, you should also implement IMarkerSymbol, ISymbolRotation, IMapLevel, and IPropertySupport.


Looking at the existing MarkerSymbol classes, you can see many of them also implement IMarkerMask. This interface provides the ability to draw a standard mask around a MarkerSymbol, which can be useful when placing multicolored symbols on a multicolored background, as it helps the eye to 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.

All 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.

IPropertySupport cannot be implemented in VB

Note that it is not possible to implement IPropertySupport in VB. This will not affect the main functionality, as ArcMap does not assume that this interface is implemented, but may check for its presence on any MarkerSymbol.

Most of the discussion for this example centers on the VB example project, as the approach taken is the same regardless of the development environment. The implementation of IPropertySupport is discussed for the benefit of those developing in VC++.


Creating the LogoMarkerSymbol

To solve the requirements of 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, and IDisplayName, 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.


Techniques for drawing

There are a number of ways you could perform the actual drawing of a symbol.

You can use 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 will be 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. It may also suit a VB programmer who does not want to apply the alternative techniques discussed below.

You may decide to 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. Note that 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 Graphics Device Interface (GDI) functions to draw the symbol.

Using GDI calls can produce efficient draw routines and also offers flexibility in the kind of drawing you can do. However, you need to be familiar with using GDI calls; some VB programmers may not have used these before. Also, you may need to perform extensive mathematical calculations to transform your symbol's coordinates according to Size, Angle, and so on. As 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 actually drawing a geometry to the appropriate device context, using the correct appearance, shape, size, and location.

When a refresh event is called, ArcMap will work out which shapes need to be drawn and in which order. It then uses the ISymbol interface to request that the shape draw itself.

Before any 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 make use of GDI calls to draw your symbol, you should use the SetupDC and ResetDC members of ISymbol to handle the adding 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 need to use the SelectObject and DeleteObject GDI functions to maintain the device context objects correctly.

Add these declarations to your project (in the VB project, they are located in the basUtility.bas module). Also, declare a user-defined type called POINTAPI, as GDI functions require coordinates to be defined as POINTAPI structures.

[Visual Basic 6]
Public Type POINTAPI
  x As Long
  y As Long
End Type

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.

[Visual Basic 6]
Private m_pCoords(6) As POINTAPI

The control points used by the drawing methods are stored in the m_pCoords 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).

  1. First, store the passed-in information.
    [Visual Basic 6]
    Set m_pTrans = Transformation
    m_lhDC = hdc
  2. Next, set up the device ratio. See the Null transformations and resolution in the Draw and QueryBoundary section later for more information.
    [Visual Basic 6]
    SetupDeviceRatio m_lhDC, m_pTrans
  3. Calculate the size of the symbol in device coordinates. You will use these later in Draw.
    [Visual Basic 6]
    m_dDeviceRadius = (m_dSize / 2) * m_dDeviceRatio
    m_dDeviceXOffset = m_dXOffset * m_dDeviceRatio
    m_dDeviceYOffset = m_dYOffset * m_dDeviceRatio
  4. Now you are ready to create the pens and brushes, which 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 these in ResetDC.
    [Visual Basic 6]
    m_lPen = CreatePen(0, 1 * m_dDeviceRatio, m_pColorBorder.RGB) 'Scale Pen
    m_lOldPen = SelectObject(hdc, m_lPen)
    m_lROP2Old = SetROP2(hdc, CLng(m_lROP2))
    m_lBrushTop = CreateSolidBrush(m_pColorTop.RGB)   ' Draws Chord
    m_lBrushLeft = CreateSolidBrush(m_pColorLeft.RGB) ' Draws left Poly
    m_lBrushRight = CreateSolidBrush(m_pColorRight.RGB)  ' Draws right Poly

Draw method

In the Draw method, work out the location of each control point for the symbol, and draw the symbol based on these locations.

  1. First, check that the passed in Geometry parameter contains a valid object, then cast it to a Point.
    [Visual Basic 6]
    If Not TypeOf Geometry Is IPoint Then Exit Sub
    Dim pPoint As esriGeometry.IPoint
    Set pPoint = Geometry
  2. 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 of the control points used by the GDI functions (see the diagram on previous page).
    [Visual Basic 6]
    Dim lCenterX As Long, lCenterY As Long
    Set pPoint = Geometry
    FromMapPoint m_pTrans, pPoint, lCenterX, lCenterY
    CalcCoords CDbl(lCenterX), CDbl(lCenterY)
  3. Then draw the separate sections of the symbol to the device.
    [Visual Basic 6]
    m_lOldBrush = SelectObject(m_lhDC, m_lBrushTop)
    Chord(m_lhDC, m_pCoords(5).x, m_pCoords(5).y, m_pCoords(6).x, _
     m_pCoords(6).y, m_pCoords(4).x, m_pCoords(4).y, m_pCoords(1).x,
     m_pCoords(1).y)
    ...
    SelectObject m_lhDC, m_lOldBrush

ResetDC method

Complete the drawing functions by selecting back the old GDI pen and ROP code and releasing other GDI resources in the ResetDC method.

[Visual Basic 6]
m_lROP2 = SetROP2(m_lhDC, CLng(m_lROP2Old))
SelectObject m_lhDC, m_lOldPen
DeleteObject m_lPen
...
Set m_pTrans = Nothing
m_lhDC = 0

If using the Windows GDI to draw to the display, make sure you reselect the old 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 working out the radius of the circular section of the logo (dRad) and the length of the triangular sections of the symbol (dVal).

[Visual Basic 6]
Dim pPtColl As IPointCollection, pSegColl As ISegmentCollection
Dim dVal As Double, dRad As Double
Set pPtColl = pBoundary
Set pSegColl = pBoundary

dRad = dMapSize / 2
dVal = Sqr((dRad * dRad) / 2)
pPtColl.AddPoint CreatePoint(pPoint.x + dVal, pPoint.y - dVal)
pPtColl.AddPoint CreatePoint(pPoint.x - dVal, pPoint.y - dVal)
pPtColl.AddPoint CreatePoint(pPoint.x - dVal, pPoint.y + dVal)
pSegColl.AddSegment CreateCircArc(pPoint, pPtColl.Point(2),pPtColl.Point(0))

QueryBoundary is a client-side storage function; therefore, you should add Point objects to the ISegmentCollection interface of the passed-in Boundary object. See the discussion of 'Coding Interface Members' for advice on coding client-side storage methods in VB.

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, as 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.

For more information on drawing with different raster operations, search Windows documentation. Windows raster operation constants correspond to esriRasterOpCodes.

Null transformations and resolution in Draw and QueryBoundary
(converting from map to device units)

As 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 the DisplayTransformation passed to the SetupDC method.

[Visual Basic 6]
Private Sub SetupDeviceRatio(ByVal hDC As Long, ByVal displayTransform _
 As IDisplayTransformation)
  
  If Not displayTransform Is Nothing Then
  If displayTransform.Resolution <> 0 Then
    m_dDeviceRatio = displayTransform.Resolution / 72
    
    If displayTransform.ReferenceScale <> 0 Then
      m_dDeviceRatio = m_dDeviceRatio * _
       displayTransform.ReferenceScale / displayTransform.ScaleRatio
    End If
  End If

SetupDeviceRatio calculates how many pixels on the device equal one printer's Point—this is used to transform Size, XOffset, and YOffset from Points to device units. Note that the ReferenceScale of the Transformation, if present, is also accounted for here.

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. In this case, you can get the resolution directly from the screen by using the GetDeviceCaps Windows API call.

[Visual Basic 6]
  Else
    If hdc <> 0 Then
      m_dDeviceRatio = CDbl(GetDeviceCaps(hdc, LOGPIXELSX)) / 72
    Else
      m_dDeviceRatio = 1 / (Screen.TwipsPerPixelX / 20)     ' 1 Pt = 20 Twips.
    End If
  End If

Once the device ratio is calculated, Draw can use the FromMapPoint function (see accompanying sample code) to convert the Geometry the symbol is drawn at from map units into device units.

The 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.

[Visual Basic 6]
Private Function PointsToMap(ByVal pDisplayTransform As ITransformation, _
 ByVal dPointSize As Double) As Double
  
  If pDisplayTransform Is Nothing Then
    PointsToMap = dPointSize * m_dDeviceRatio
  Else
    Dim pTempTransform As IDisplayTransformation
    Set pTempTransform = pDisplayTransform
    PointsToMap = pTempTransform.FromPoints(dPointSize)
  End If
End Function

The PointsToMap function transforms values from Points to map coordinates.

Drawing efficiently

Code the ISymbol methods efficiently, as they may be called frequently. There are a number of issues you could consider to increase your symbol's drawing efficiency.

A symbol's draw methods may be called frequently; consider the efficiency of your code.

Creating and implementing the ILogoMarkerSymbol interface

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. For more information on how you can create a new interface, see the 'Developing Objects' section.

The custom ILogoMarkerSymbol interface allows a client to change the colors of the different sections of the logo.

Implement ILogoMarkerSymbol in the LogoMarkerSymbol coclass. In each property, clone the incoming IColor parameters and set the appropriate member variable.

[Visual Basic 6]
Private Property Let ILogoMarkerSymbol_ColorBorder(ByVal RHS As esriDisplay.IColor)
  Dim pClone As IClone
  Set pClone = RHS
  Set m_pColorBorder = pClone.Clone
End Property

Implementing IMarkerSymbol

Implementing IMarkerSymbol allows ArcGIS to recognize that your class can be used to symbolize points. MarkerSymbol properties for the LogoMarkerSymbol. This interface is commonly used by the ArcGIS applications, for example, when setting Color and Size using the Element Properties dialog box.

Implementing IMarkerSymbol ensures that a Symbol is able to interact with the ArcMap user interface, 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.

[Visual Basic 6]
Private Property Get IMarkerSymbol_Color() As esriDisplay.IColor
  Dim pLogoMS As ILogoMarkerSymbol
  Set pLogoMS = Me
  Set IMarkerSymbol_Color = pLogoMS.ColorTop

In the Angle property you can add a check for angles greater than 360 degrees.

[Visual Basic 6]
Private Property Let IMarkerSymbol_Angle(ByVal RHS As Double)
  If RHS > 360 Then
    RHS = RHS - (Int(RHS / 360) * 360)
  End If
  m_dAngle = RHS
End Property

Implementing ISymbolRotation

If you want your symbol to be able 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 the IMarkerSymbol::Angle. You can get the Map rotation value from the DisplayTransformation passed in SetupDC:

[Visual Basic 6]
dAngle = 360 - (m_dAngle - m_dMapRotation)

RotateWithTransform is True by default for existing ArcGIS symbols.

ISymbolRotation allows a Symbol to work with the Data Frame tools in ArcMap, rotating with the Map.

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 a Long value in the read-write MapLevel property

[Visual Basic 6]
Private Property Let IMapLevel_MapLevel(ByVal RHS As Long)
  m_lMapLevel = RHS  ' Store passed in value in a global variable.
End Property

This value will be used when your symbol is used in a MultiLayerMarkerSymbol, when the Advanced Drawing Options indicate symbols must be drawn joined and merged.

IMapLevel allows a symbol to take part in the ArcMap Advanced Drawing Options.

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 an IFillSymbol by ArcMap.

By implementing IMarkerMask, you allow the framework to draw a mask area around your symbol.

First ensure the Boundary is empty, then use the same technique you used in ISymbol::QueryBoundary to populate Boundary.

[Visual Basic 6]
Boundary.SetEmpty
QueryBoundsFromGeom hDC, Transform, Boundary, Geometry

Unlike QueryBoundary, however, QueryMarkerMask requires a Simple geometry, so simplify the geometry before returning.

[Visual Basic 6]
Dim pTopo As ITopologicalOperator
Set pTopo = Boundary
If Not pTopo.IsKnownSimple Then
  If Not pTopo.IsSimple Then
    pTopo.Simplify
  End If
End If

Implementing IPropertySupport

IPropertySupport can be implemented in VC++ and 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 needing to know the exact nature of the underlying class.

IPropertySupport is an optional interface. It cannot be implemented in VB6.

In the Applies method, you should assess the incoming object reference pUnk, to see if it can be applied to a property of your class.

[Visual C++]
STDMETHODIMP CLogoMarkerSymbol::Applies(LPUNKNOWN pUnk,VARINT_BOOL *Applies)
{
  if (!Applies)
    return E_POINTER;
  
  *Applies = VARIANT_FALSE;
  
  IColorPtr ipColor(pUnk);
  ILogoMarkerSymbolPtr ipLogo(pUnk);
  if (ipColor != NULL && ipLogo != NULL)
    *Applies = VARIANT_TRUE;
  return S_OK;
}

In the CanApply method, check if the object can be applied at the particular moment the method is called; a more complex class may involve checking the internal state of the class). In the case of the 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.

[Visual C++]
STDMETHODIMP CLogoMarkerSymbol::get_Current(LPUNKNOWN pUnk, LPUNKNOWN *currentObject)
{
  IColorPtr ipColor(pUnk);
  if (ipColor)
  {
    IColorPtr ipCurrentColor;
    get_Color(&ipCurrentColor);
    ipCurrentColor.QueryInterface(IID_IUnknown, (void**)currentObject);
    return S_OK;
  }
  ...
}

In the Apply method, set the incoming object as the appropriate member of your symbol class. Note in the code below that the incoming object may 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.

[Visual C++]
STDMETHODIMP CLogoMarkerSymbol::Apply(LPUNKNOWN NewObject, LPUNKNOWN *oldObject)
{
  IColorPtr ipColor(NewObject);
  if (ipColor)
  {
    get_Current(NewObject, oldObject);
    put_Color(ipColor);
    return S_OK;
  }
  
  ILogoMarkerSymbolPtr ipSymbol(NewObject);
  if (ipSymbol)
  {
    get_Current(NewObject, oldObject);
    IClonePtr ipClone(NewObject);
    Assign(ipClone);
    return S_OK;
  }
  return E_FAIL;
}

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.

[Visual Basic 6]
Private Sub InitializeMembers()
  m_lhDC = 0
  
  Dim pColor As IColor, pClone As IClone
  Set pColor = New RgbColor
  Set pClone = pColor
  pColor.RGB = RGB(255, 0, 0)
  Set m_pColorTop = pClone.Clone
  ...
  m_lROP2 = esriROPCopyPen
  ...
  m_bRotWithTrans = True
End Sub

Placing the initialization code in a separate function enables you to reset the 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. See the section 'Developing Objects', for more information on cloning and persistence.

In the IPersistVariant::Save method, save the persistence version number first, then each required member of the class.

[Visual Basic 6]
Private Sub IPersistVariant_Save(ByVal Stream As esriSystem.IVariantStream)
  Stream.Write m_lCurrPersistVers
  Stream.Write m_lROP2
  Stream.Write m_dSize
  Stream.Write m_dXOffset
  ...

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.

[Visual Basic 6]
Private Sub IPersistVariant_Load(ByVal Stream As IVariantStream)
  Dim lSavedVers As Long
  lSavedVers = Stream.Read
  If (lSavedVers > m_lCurrPersistVers) Or (lSavedVers <= 0) Then
    Err.Raise E_FAIL
    Exit Sub
  End If
  
  InitializeMembers
  If lSavedVers = 1 Then
    m_lROP2 = Stream.Read
    m_dSize = Stream.Read
    m_dXOffset = Stream.Read
    ...

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 QIing to IClone on the color class and checking its IsEqual member in turn.

[Visual Basic 6]
Private Function IClone_IsEqual(ByVal other As IClone) As Boolean
  IClone_IsEqual = True
  If Not other Is Nothing Then
  If TypeOf other Is ILogoMarkerSymbol Then
    Dim pSrcLogoSym As ILogoMarkerSymbol
    Dim pRecLogoSym As ILogoMarkerSymbol
    Set pSrcLogoSym = other
    Set pRecLogoSym = Me
    IClone_IsEqual = IClone_IsEqual And _
     (pRecLogoSym.ColorBorder.RGB = pSrcLogoSym.ColorBorder.RGB)
    IClone_IsEqual = IClone_IsEqual And _
     (pRecLogoSym.ColorLeft.RGB = pSrcLogoSym.ColorLeft.RGB)
    ...

Symbol Property Pages

All core symbols have a property page that is displayed in the Symbol Editor dialog box. This allows the user to edit the properties of the symbol in the user interface. Some more complex symbols have multiple property pages, displayed as separate tabs in the dialog box.

It is not absolutely essential for a custom Symbol coclass to have an accompanying property page, although it is recommended that you do. If you only intend a Symbol to be applied and edited programmatically, you need not implement a property page, but users may be confused when they try to change the properties of the Symbol in the editor.

Create a LogoMarkerPropertyPage coclass as shown in the accompanying sample code, and register the class to the Symbol Property Pages component category. Follow the general rules for property pages given in the topic 'Creating Property Pages'. Additionally, the following section highlights particular details relevant to this implementation, in particular the implementation of ISymbolPropertyPage.

Add a separate Form class to provide the UI component of the property page. You will link the LogoMarkerPropertyPage coclass to the form by adding public properties to the form.


The LogoMarkerPropertyPage is displayed in the Symbol Editor dialog box.
Note that when you open the Symbol Editor for a 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 the LogoMarkerPropertyPage

The interfaces implemented on the LogoMarkerPropertyPage are dependent on the development environment, as described in the earlier section, 'Developing Objects'. In either case, the basic structure of the property page class is similar. In the Applies method, return True if you receive a LogoMarkerSymbol.

[Visual Basic 6]
For i = 0 To Objects.Count - 1
  Set pObj = Objects.Next
  If TypeOf pObj Is ILogoMarkerSymbol Then
    Set pAppliesClone = pObj
    IComPropertyPage_Applies = True
    Exit Function
  End If
Next i

Add a property named LogoMarkerSymbol to the Form class. In the SetObjects method, check that you receive a LogoMarkerSymbol object, then set the LogoMarkerSymbol property of the Form to this object.

[Visual Basic 6]
Dim pObj As Variant, i As Long
Objects.Reset
  
For i = 0 To Objects.Count - 1
  Set pObj = Objects.Next
  If Not pObj Is Nothing Then
    If TypeOf pObj Is ILogoMarkerSymbol Then
      Set m_pObjectLogoMarker = pObj      ' hold on to the symbol
    End If
  End If
Next i
  
If Not m_pObjectLogoMarker Is Nothing Then
  Set m_frmPage.LogoMarkerSymbol = m_pObjectLogoMarker
  m_frmPage.UpdateControls
  m_frmPage.IsPageDirty = False
End If

Add a property called IsPageDirty to the form, and link this to the IComPropertyPage::IsPageDirty property of the LogoMarkerPropertyPage class.

This allows events from controls on the Form to directly change properties of the Symbol. The changes can then be seen in the Preview box after calling IComPropertyPageSite::PageChanged to refresh the dialog box.

Implementing ISymbolPropertyPage

The Size, XOffset, and YOffset properties of a MarkerSymbol are returned and set onto the LogoMarkerSymbol as a printer's Point measurement. However, users may prefer to 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 coclasses themselves, this would not only mean each class contained 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 side of the Symbol Editor dialog box, you can see the Units combo box. When the user changes 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 the user has selected by setting the ISymbolPropertyPage::SymbolUnits property.

By implementing ISymbolPropertyPage, you can allow a property page to react correctly to changes in the Units combo box of the containing property sheet.

When creating a symbol property page, you must provide the ability to convert from the selected units of measurement to Points.

Add a read-write property to the form called Units, which stores the selected units value in a member variable of the Form. When this property is changed, call the UpdateControls method.

UpdateControls should account for a change in Units by converting the values shown in the Size, XOffset, and YOffset controls to the currently selected unit type—this is because these properties are always stored internally in Points.

[Visual Basic 6]
txtSize.Text = PointsToUIValue(m_pMarker.Size)
txtXOffset.Text = PointsToUIValue(m_pMarker.XOffset)
txtYOffset.Text = PointsToUIValue(m_pMarker.YOffset)

Then add the PointsToUI procedure to convert values from Points to the current display units—it returns a formatted string, in the currently selected units, which can be displayed in the Size, XOffset, and YOffset controls.

[Visual Basic 6]
Private Function PointsToUIValue(ByVal dValue As Double) As String
  Select Case m_lUnits
    Case esriPoints
      PointsToUIValue = FormatNumber(dValue, 2)
    Case esriInches
      PointsToUIValue = FormatNumber(dValue / 72#, 4)
    Case esriCentimeters
      PointsToUIValue = FormatNumber((dValue / 72#) * 2.54, 2)
    Case esriMillimeters
      PointsToUIValue = FormatNumber((dValue / 72#) * 24.5, 2)
  End Select
End Function

 


Back to top

Go to the VB6 example code or the VC++ example code.

See Also Customizing the Display, Creating custom symbols, and Vertex Line Symbol Example.