In this section:
Object Model Diagram Click here.
Example Code Click here.
Description This project enables a point feature layer to be drawn with the features moved so that none of their symbols overlap. An accompanying property page allows the properties of the renderer to be set via the ArcMap user interface.
Design PointDispersalRenderer is a subtype of the FeatureRenderer abstract class. PointDispersalPropertyPage implements the standard property page interfaces.
License required ArcView or above.
Libraries Carto, CartoUI, Display, DisplayUI, Framework, Geodatabase, Geometry, and System
Languages Visual Basic
Categories ESRI Renderer Property Pages
Interfaces IFeatureRenderer, IPersistVariant, ILegendInfo, IComPropertyPage, IComEmbeddedPropertyPage, and IRendererPropertyPage.
How to use
Your layer will now draw using the point dispersal renderer.
Imagine that you have a point feature class where many of the points lie close to or on top of each other. You would like to force the points to all display individually, even if it means that the point is drawn slightly away from its true location.
Some point datasets have features close to or an top of each other. A custom feature renderer can be developed to disperse the point symbols so all the features can be seen.
Before describing the custom feature renderer solution, it is worth noting that there are many different approaches to this problem. A custom layer is probably a more complete solution, since for this particular problem the custom renderer will result in the selection tools not working properly.
Another approach to dispersing the points would be to label the features with a single character, placed directly over the feature; the labelling functionality could then be used to avoid overlapping labels, although in this scenario it is hard to ensure all features are labelled.
By reviewing the Display object diagram, you can see that all renderer classes inherit from the FeatureRenderer abstract class. Therefore, any type of custom renderer you create should begin by implementing the IFeatureRenderer interface, along with interfaces for cloning and persistence.
You can see that there are a few other interfaces that are commonly implemented by a renderer, such as IRendererFields, IRotationRenderer, IBarrierProperties2, IDataExclusion, IDataNormalization, ILookupSymbol, and ITransparencyRenderer. You will not need to implement any of these interfaces in this example; however, you can find more information on implementing these interfaces at the end of this example.
In the DisplayUI object diagram, you can also see that each renderer has an associated property page class. In the ArcMap user interface, this not only allows a user to assign an instance of the custom renderer to a layer, but also to alter the properties of the renderer.
To answer the display requirements described above, you will create a custom feature renderer called PointDispersalRenderer, that disperses the points as necessary to avoid their symbols overlapping.
You will also provide an accompanying property page implementation for your class.
The IFeatureRenderer interface is the core of a renderer. The main method that will be called by the ArcGIS framework is Draw, at which point, it is the job of your renderer to draw the feature layer in any way you specify.
The Draw method receives a reference to the Display to which the renderer should draw and also a feature cursor indicating the features to be drawn. Start by identifying the Symbol you will be drawing the feature with; this is stored in the LegendGroup for this renderer (see the Implementing ILegendInfo section for more information).
Private SubIFeatureRenderer_Draw(ByValCursorAsIFeatureCursor, _ByValdrawPhaseAsesriDrawPhase,ByValDisplayAsIDisplay,ByValtrackCancelAsITrackCancel) ...DimpSymAsISymbolSetpSym = m_pLegendGroup.Class(0).Symbol Display.SetSymbol pSym ...DimpFeatureAsIFeatureSetpFeature = Cursor.NextFeature
Draw also receives a trackCancel parameter, which indicates if the user has pressed the Esc key to cancel the drawing. This is important, since the point dispersal could become slow in extreme situations with large datasets. This is a suitable point to check the cancel tracker. You should also check this cancel tracker at the end of the main drawing loop.
DimbContinueAs Boolean If NottrackCancelIs Nothing ThenbContinue = trackCancel.Continue
The Draw method provides the main functionality of a renderer; Draw receives a reference to a FeatureCursor, which contains all
the features the renderer should draw.
You should check the cancel tracker and stop drawing if it indicates the user
has pressed Esc.
Implement the main Draw method loop by iterating through the feature cursor, taking each feature in turn, and drawing to the specified Display by calling the PlaceFeature function to find the dispersed location chosen for the feature.
Do While(NotpFeatureIs Nothing)And(bContinue =True)DimpPointAsIPointSetpPoint = pFeature.Shape ... PlaceFeature pPoint, 0, pGeomColl, Display, pSym, pPlacedPoint, _ pSymPoly, dDispersalDist ... pGeomColl.AddGeometry pSymPoly.Envelope Display.DrawPoint pPlacedPoint ...SetpFeature = Cursor.NextFeatureIf NottrackCancelIs Nothing ThenbContinue = trackCancel.ContinueLoop
Each time a feature is drawn, add the Envelope of the feature to a GeometryBag variable, which holds the extent of all the dispersed points placed so far and is passed in to the PlaceFeature function each time to allow the function to identify where features have previously been placed.
Drawing the features
Note that the actual drawing is done with IDisplay::SetSymbol and then, for each feature, IDisplay::DrawPoint. This is not typical for a custom renderer; it is general practice for the renderer to pass the relevant symbol to the IFeatureDraw::Draw method on the feature. Calling IFeatureDraw::Draw allows custom features to use their own drawing methods. In the case of the point dispersal example, the rendering is incompatible with IFeatureDraw since the feature is to be drawn potentially away from its true location.
Generally, custom renderers should draw the features on the display by calling IFeatureDraw::Draw.
If drawing directly to the display, there is no need to call IDisplay::StartDrawing and IDisplay::FinishDrawing, since you are already inside a drawing phase started by the ArcGIS framework.
PlaceFeature function
The actual dispersal of the points is done in the PlaceFeature function. A geometry bag of symbol envelopes is cached, which records all the currently drawn points on the display.
Each feature is first placed at its true location. If the symbolized feature overlaps any of the already drawn features in the layer, then a new attempt to place it is made at a certain distance away.
All four points of the compass are tried, and then the dispersal distance is increased until the feature is eventually placed, and its envelope added to the geometry bag.
PlaceFeatures calculates a new location for a feature, so it does not overlap other features that have already been drawn.
Layer draw phases
There are three draw phases for a layergeography, annotation, and selection. Except for the selection phase, the Draw method of a renderer will be called for each phase that you specify in IFeatureRenderer::RenderPhase.
Private Property GetIFeatureRenderer_RenderPhase(ByValdrawPhaseAs_ esriDrawPhase)As Boolean IfdrawPhase = esriDPGeographyThenIFeatureRenderer_RenderPhase =True ElseIFeatureRenderer_RenderPhase =False End IfEnd Property
Draw is then also called for the selection phase if there are selected features on the display. The example chooses to ignore this phase, since the features are dispersed from their true locations and thus incompatible with the selection tool. Incidentally, raising E_FAIL from your Draw routine for a selection phase will result in the default selection rendering for the layer.
For an example of a renderer that uses the annotation phase, see the 'BivariateRenderers' sample in the ArcGIS Developer Help.
Drawing during debugging
There is one more thing to mention about drawing features to the display. If you are debugging your custom renderer with Visual Basic 6, the features will not display on the map, as the Display has a process-dependent device context. See Chapter 2, 'Developing Objects', for more information on debugging.
With the Visual Basic 6 debugger, the features drawn by a custom renderer will not appear on the display.
Preparing the query filter
Before IFeatureRenderer::Draw is called, you are given an opportunity to modify the query filter that produces the feature cursor in the PrepareFilter method. In this method, you must add into the filter any fields you need for your renderer. The point dispersal renderer does not rely on any particular attributes.
The PrepareFilter method gives you a chance to specify which fields your renderer needs to perform drawing.
However, the ExclusionSet property allows the framework to specify that a renderer should exclude a certain set of features from drawing.
Private Property SetIFeatureRenderer_ExclusionSet(ByValpFeatureIDSet _AsIFeatureIDSet)Setm_pExclusionSet = pFeatureIDSetEnd Property
Therefore, if there is a set of features that have been specified in ExclusionSet, you need to make sure that the object ID field name is added to the QueryFilter parameter.
Private SubIFeatureRenderer_PrepareFilter(ByValfcAsIFeatureClass, _ByValQueryFilterAsIQueryFilter)If Notm_pExclusionSetIs Nothing Then Ifm_pExclusionSet.Count > 0ThenQueryFilter.AddField fc.OIDFieldNameEnd If End If End Sub
If you implement IFeatureRenderer::ExclusionSet, you must ensure the object ID field is fetched with PrepareFilter.
Although the PointDispersalRenderer example implements IFeatureRenderer::ExclusionSet, it is unlikely that you would find that an exclusion set is used with this renderer in ArcMap, since the ExclusionSet is mainly related to the Convert Features to Graphics command, which is inappropriate with this renderer.
Other client programs however may make their own exclusion sets. Note that if clients call IFeatureRenderer::ExclusionSet directly it will be ignored, since the feature layer exclusion set overrides the renderer exclusion set. If you use IGeoFeatureLayer::ExclusionSet this will be passed down to the renderer.
The query filter is used by the ArcGIS framework to produce the feature cursor passed to the Draw method. It is actually a spatial filter (you could QI for ISpatialFilter to prove this to yourself), with the display extent being used to limit which features are returned.
You will find that there are normally more features in the cursor than are within the display extent, since the spatial filter criteria is set against the spatial index rather than the feature geometries. It is more efficient for the renderer to draw these offscreen features than have a slower query. In the case of data that does not have a spatial index (for example, some shapefiles), you will find all the features in the dataset are present in the feature cursor.
You may find features in the cursor that are not within the current display extent. It is generally more efficient to draw these features than to check their extent yourself. ArcGIS has produced the query for speed of execution.
For layers with feature class extensions or custom features, the query filter may already have some subfields set, as it is the feature layer rather than the renderer that is responsible for checking IFeatureClassDraw::RequiredFieldsForDraw.
CanRender property
If you want to restrict which layers your custom renderer can be applied to, such as being applicable only to line layers, then in your implementation of IFeatureRenderer::CanRender, you can test properties of the feature layer and return True if your renderer supports it and False if it does not.
The CanRender property should indicate if a renderer can draw a certain FeatureClass.
In your code, ensure the PointDispersalRenderer can only be applied to point layers.
Private FunctionIFeatureRenderer_CanRender(ByValfeatClass _AsIFeatureClass,ByValDisplayAsIDisplay)As Boolean IffeatClass.ShapeType = esriGeometryPointThenIFeatureRenderer_CanRender =True ElseIFeatureRenderer_CanRender =False End If End Function
Other types of renderers may check for other things about the FeatureClass or Display references passed infor example, a renderer specially designed for networks may check if the FeatureClass contains a particular type of network feature by checking the IFeatureClass::FeatureType property.
SymbolByFeature methodThe SymbolByFeature method should return the symbol appropriate to a given feature. For the PointDispersalRenderer, this is simple, since the point dispersal renderer only uses one symbol for all featuresnote that you can only return the original locations of the features.
SymbolByFeature is called repeatedly by the ArcMap Convert Features to Graphics tool, and hence this command, when called on a layer symbolized with a PointDispersalRenderer, will generate graphics in the original feature locations.
SymbolByFeature should return the symbol the renderer would use to draw a specific, individual feature.
Using SymbolByFeature also enables the possibility of containing other renderers within your custom renderer. Imagine that in the example, you would like to disperse the points, but instead of a single symbol, you use one of other symbology options such as proportional symbols or unique values. This could be achieved by keeping a reference to a contained renderer class, your custom renderer, then for each feature in the Draw loop, calling SymbolByFeature on the contained renderer to determine the symbol to use. In the ArcGIS Developer Help, you can see that the 'BivariateRenderers' sample custom renderer operates in this way.
ILegendInfo is often quite straightforward to implement. This interface ensures the table of contents and legends are able to show a list of the symbols, labels, and headings your renderer is using. In the IFeatureRenderer::Draw method, you have already seen how you can reuse the existing LegendGroup and LegendClass objects and use these to hold the symbols with which your custom renderer will draw.
ILegendInfo helps link a renderer with the table of contents.
Declare a member variable to hold a reference to a LegendGroup.
Privatem_pLegendGroupAsILegendGroup
Use m_LegendGroup to return the values of the LegendGroupCount, LegendGroup, and LegendItem properties. The LegendGroup is set up in the class initialization code and by the IDispersalRenderer interfacesee the example code project for full details.
Return False from SymbolsAreGraduated, and do not allow this property to be changed, as you will not implement any symbol graduation functionality to the PointDispersalRenderer.
You need to provide a way for clients to change the Symbol used by the PointDispersalRenderer and also the dispersal distance.
Create an interface called IDispersalRenderer, with two read-write properties, DispersalRatio and Symbol. Implement IDispersalRenderer in the PointDispersalRenderer coclass. For more information on how you can create a new interface, see Chapter 2, 'Developing Objects'.
Private Property SetIDispersalRenderer_Symbol(ByValpSymbolAsISymbol)Setm_pLegendGroup.Class(0).Symbol = pSymbolEnd Property
The custom IDispersalRenderer interface provides access to the symbol and dispersal distance used by the renderer.
The Symbol property is set into the first Class of the LegendGroup, which means that the LegendGroup contains the correct symbol and will display correctly in a legend or table of contents. Note that the Symbol property is passed by reference.
Private Property LetIDispersalRenderer_DispersalRatio(ByValRHSAs Double) m_dDispersalRatio = RHSEnd Property
The dispersal ratio value is used by the PlaceFeature function, as described previously. You will create a renderer property page, which will be the main consumer of IDispersalRenderer.
You must implement the standard persistence interface or interfaces, to preserve the state of the renderer in a map document (.mxd) or layer file (.lyr). Implement IPersistStream and IPersistStream if using VC++ or IPersistVariant if using VB.
A renderer must be persistable.
In your PointDispersalRenderer, you need to save the legend group, which is the dispersal distance ratio. Any objects you persist must implement IPersistStream (as does the legend group) or IPersistVariant.
Private SubIPersistVariant_Save(ByValStreamAsesriSystem.IVariantStream)'Persistence version numberStream.Write m_lCurrPersistVers Stream.Write m_pLegendGroup Stream.Write m_dDispersalRatioEnd Sub
See Chapter 2, 'Developing Objects', for more information about the version checking used in the PointDispersalRenderer persistence code.
Implementing a custom renderer property page will allow users to interact with the settings of your custom renderer. By registering the property page in the ESRI Renderer Property Pages component category, the page will appear on the Symbology tab of the Layer Properties dialog box along with all the standard symbology options. The Symbology tab is itself a property page; therefore, your property page needs to be an embedded property page.
Define your custom renderer property page as a class called PointDispersalPropertyPage, that implements the standard interfaces for an embedded property page and the IRendererPropertyPage interface.
Design your UI on a form as shown belowyou can place all the controls and descriptive text for the main part of the page onto another control, which has a window handle (the example project uses a Picture box control). Reference this form through a private data member in the PointDispersalPropertyPage class.
The interfaces implemented on a property page class are dependent upon your development environment; refer to Chapter 2, 'Developing Objects', for general information on implementing property page interfaces. This discussion will follow the use of the interfaces implemented in the VB example project.
The Applies method may not actually be called for an embedded page; however, it is best practice to implement this method fully anyway. Return True if you find a PointDispersalRenderer.
Objects.ResetSetpObj = Objects.NextDo While Not TypeOfpObjIsIDispersalRendererSetpObj = Objects.NextIfpObjIs Nothing ThenIComPropertyPage_Applies =False Exit Function End If Loop
In the SetObjects method, you are passed a set of objectsyou should find the renderer in this list, check it is a PointDispersalRenderer, then initialize the controls on the accompanying form using the properties of the supplied renderer.
DimpObjAs Variant SetpObj = Objects.NextDo UntilpObjIs Nothing If TypeOfpObjIsIDispersalRendererThen Setm_pRenderer = pObj m_frmPage.InitControls m_pRendererEnd If SetpObj = Objects.NextLoop
The Applies and SetObjects methods should both check that they are passed a reference to a PointDispersalRenderer.
The PointDispersalRenderer property page only requires a reference to the renderer itself. However, the object set passed to SetObjects will also include the map, feature layer, and feature class. If you are adapting this example to create a different kind of renderer, you may need these references to allow users to set the properties of the renderer correctly.
The Apply method is triggered when the user clicks Apply or OK on the layer properties property sheet. After calling this method the framework will set the renderer supplied in SetObjects as the live renderer. As shown in the example project code, you can use the implementation of IComEmbeddedPropertyPage::QueryObject to apply the changes to the renderer object.
Private SubIComPropertyPage_Apply() IComEmbeddedPropertyPage_QueryObject m_pRendererEnd Sub
Use IComPropertyPage::Priority to control where your renderer appears in the listbox of available renderers. Use a lower number to have your renderer and category appear toward the top of the list (the priority of the first page in a category controls where that category fits in the list). Generally, you should use a high number for custom renderers to ensure they display after the standard renderersfor the PointDispersalPropertyPage, return a value of 600.
The table below lists standard renderer property pages and their priorities.
| Type | Name | Priority |
|---|---|---|
| Features | Single symbol | 100 |
| Categories | Unique values | 200 |
| Unique values, many fields | 210 | |
| Match to symbols in a style | 300 | |
| Graduated symbols | 310 | |
| Proportional symbols | 320 | |
| Dot density | 330 | |
| Charts | Pies | 400 |
| Bars | 410 | |
| Stacked | 420 | |
| Attributes | Quantity by category | 500 |
Implementing IComEmbeddedPropertyPage
Custom renderer property pages fall into the class of embedded property pages. In VC++ you should ensure you implement the CreateCompatibleObject and QueryObject members of IPropertyPageContext; in VB you will need to implement IComEmbeddedPropertyPage.
In ArcMap, users choose from different symbology options from the tree view on the Layer Properties Symbology tab. Because the internal representation of each option is a different renderer object, as the user chooses a new option, a new renderer is being edited. In some cases, properties are preserved during this transition. For example, when a user switches between the Bar chart and Pie chart options, the renderer fields and symbols are preserved from the old to the new renderer.
As you will create an embedded property page, the properties of your renderer can be preserved when users switch between types of renderer.
In addition to managing the retention of properties from an old renderer, you should also use CreateCompatibleObject to avoid excessive cloning of renderers. In this method check to see if the in parameter is an object of the type your page should edit. If so, return that same object. If not, create and return a new renderer object of the proper type.
Private FunctionIComEmbeddedPropertyPage_CreateCompatibleObject(ByValkindAs Variant)As Variant DimpDispersalRendAsIDispersalRendererIf TypeOfkindIsIDispersalRendererThen SetpDispersalRend = kindElse SetpDispersalRend =NewPointDispersalVB.Renderer ...End If SetIComEmbeddedPropertyPage_CreateCompatibleObject = pDispersalRendEnd Sub
If you want, you can also copy any compatible properties you can find.
...SetpDispersalRend =NewPointDispersalVB.Renderer If TypeOf kindIsILegendInfoThen DimpLegendInfoAsILegendInfoSetpLegendInfo = kindIfpLegendInfo.LegendGroupCount > 0Then If TypeOfpLegendInfo.LegendGroup.Class(0).SymbolIsIMarkerSymbolThen SetpDispersalRend.Symbol = pLegendInfo.LegendGroup.Class(0).SymbolEnd If End If End If ...
CreateCompatibleObject should return a PointDispersalRenderer. You can attempt to copy any compatible properties from the renderer reference passed in to the PointDispersalRenderer.
In the QueryObject method, apply the changes made on the property page to the supplied object. This renderer will become the live renderer for the layer.
Private SubIComEmbeddedPropertyPage_QueryObject(ByValtheObjectAs Variant)DimpRendererAsIDispersalRendererIf NottheObjectIs Nothing Then If(TypeOftheObjectIsIDispersalRenderer)Then SetpRenderer = theObject m_frmPage.ApplyToRenderer pRendererEnd If End If End Sub
Typically, a property page creates a temporary object and allows changes to this object. Then, if the Apply or OK buttons are clicked, the temporary renderer replaces the 'live' renderer object on the feature layer. If the Cancel button is clicked, then the temporary renderer is discarded.
The ArcGIS framework automatically creates the temporary renderer by cloning the renderer on the layer before passing it to your page, so it is not necessary for your code to make a copy. Note that this cloning will make use of the persistence code for your renderer, as renderers do not support IClone.
All renderer property pages implement an additional interface IRendererPropertyPage. Some of its properties will appear on the ArcMap Symbology property page, which will be the container of your property page when displayed in the Layer Properties dialog box. These properties help guide users when accessing your custom page.
The Description string will appear at the top of the parent page, and the PreviewImage will appear in the bottom left of the dialog box. A preview image size of 116 by 88 pixels will display at a 1:1 ratioif the image is larger or smaller, it will be scaled to fit the preview box. IRendererPropertyPage::Name appears in the tree view on the left side of the symbology property page.
For IRendererPropertyPage::Type, use 'Custom Renderers', so that your renderer displays in the same category as the other renderer developer samples. If you use an already existing Type (for example, 'Features'), your renderer will appear under that category, listed by Priority order.
IRendererPropertyPage is used to edit the items on a property page that are common to all renderers. The Preview property gives users an idea of how a renderer will display.
The Type property dictates where your renderer will appear in the listbox of available renderers. The Name property is used to display an entry for the custom renderer in the list.In IRendererPropertyPage::CanEdit you should check the in parameter to make sure your custom page can edit the specified renderer. Typically, your custom property page will only edit your custom renderer. For the PointDispersalPropertyPage, you can check for the presence of the IDispersalRenderer interface to identify your renderer.
Private FunctionIRendererPropertyPage_CanEdit(ByValobjAs_ IFeatureRenderer)As Boolean If TypeOfobjIsIDispersalRendererThenIRendererPropertyPage_CanEdit =True ElseIRendererPropertyPage_CanEdit =False End If End Function
If you create a different kind of renderer, it is well worthwhile defining an interface, which will uniquely identify your renderer to help you implement CanEdit.
Note that standard renderers also implement CanEdit in this way, which has implications for the interfaces you might like to implement on a custom renderer.
For example, it might make sense for the point dispersal renderer to implement ISimpleRenderer, since all of its methods and properties are appropriate. However, this would cause problems with the property page. The Features/Single Symbol property page will return True from CanEdit for any renderer that implements ISimpleRenderer. As this page has a higher priority than the custom property page, the wrong page would be shown for a point dispersal renderer. In practice, it is straightforward to avoid implementing the interfaces that identify the standard renderers as they are all named similarly to their coclasses.
Each renderer has an interface that identifies it to its property page.
Now you are ready to use your renderer. See the Managing custom feature renderers topic for further advice.
Go to example code
See Also Customizing the Display, About custom feature renderers, and Managing custom feature renderers.