ProConcepts GraphicsLayers - Esri/arcgis-pro-sdk GitHub Wiki

Graphics layers are layers that act as containers for graphic elements - points, lines, polygons, text, or images - that can be added to a map. Graphics elements provide a way to include simple annotations on a map or layout that highlight particular areas or points of interest.

Language:      C#
Subject:       Map Authoring
Contributor:   ArcGIS Pro SDK Team <[email protected]>
Organization:  Esri, http://www.esri.com
Date:          10/06/2024
ArcGIS Pro:    3.4
Visual Studio: 2022

In this topic

Background

Graphics layers are a special kind of layer designed to work with graphic elements. Graphics elements can contain a geometry or a picture and provide a way to include a simple annotation or highlight on a given map. Graphics elements can be used to visualize a map without the need to create features. Graphics elements derive from ArcGIS.Desktop.Layout.GraphicElement and are identical in their implementation to graphics elements added to a layout for similar purposes.

Each element in a graphics layer will have its own unique name across all graphics layers in the map. A graphic element's name acts as the primary key for all the elements within a map (or layout). Their visibility is controlled by the graphics layer. If the coordinate system of the map container changes, graphics elements will remain in their assigned geographic position. However, because graphic elements are not features, they do not have associated attributes and cannot be queried.

As of 2.6 graphics layers are supported in 2D only. Multipatch geometry is not supported. When dealing with multipatches, developers should use the existing mapview add overlay methods instead.

Creating Graphics Layer

To create a GraphicsLayer use the LayerFactory.Instance.CreateLayer<T>() templated "flavor", topic26527.html, and set the templated type to be GraphicsLayer. For the layer creation params use GraphicsLayerCreationParams. Graphics layers, same as other layers, can be added directly to the TOC or as a child of a group layer. By default, Graphics layers are added to the top of the TOC unless otherwise specified. Their drawing order is controlled by their position in the TOC also the same as other layer (this is different than the behavior of graphics in the map overlay which always renders on top of all other map content). In the following example we are adding a graphics layer called "Graphics Layer" to the currently active map. We are checking the map type to ensure it is 2D.

 var map = MapView.Active.Map;
 if (map.MapType != MapType.Map)
    return;// not 2D

 var gl_param= new GraphicsLayerCreationParams { Name = "Graphics Layer" };
 QueuedTask.Run( () => {
   //By default will be added to the top of the TOC
   var grahicsLayer = LayerFactory.Instance.CreateLayer<GraphicsLayer>(gl_param, map);

   //or add to the bottom of the TOC
   gl_param.MapMemberPosition = MapMemberPosition.AddToBottom;
   LayerFactory.Instance.CreateLayer<GraphicsLayer>(gl_param, map);

   //or add a graphics layer to a group layer...
   var group_layer = map.GetLayersAsFlattenedList().OfType<GroupLayer>().First();
   LayerFactory.Instance.CreateLayer<GraphicsLayer>(gl_param, group_layer);

   //TODO...use the graphics layer

 });

Note: The created graphics layer will have the spatial reference of the map.

Accessing Graphics Layer

Graphics layers are retrieved from the Map's layers collection in the same way as other layers. Use map.GetLayersAsFlattenedList() and cast the returned layers to be .OfType<GraphicsLayer>() or search for a specific Graphics Layer by uri or name using map.FindLayer(...) and map.FindLayers(...) respectively.

 //get the first graphics layer in the map's collection of graphics layers
 var grahicsLayer = map.GetLayersAsFlattenedList().OfType<GraphicsLayer>().FirstOrDefault();
 if (grahicsLayer != null) {
    ...

TargetGraphicsLayer

The "TargetGraphicsLayer" is a special property on the map. It points to the graphics layer that will act as the target for the interactive drawing tools (used to create new map graphics). If the TargetGraphicsLayer has not been set when the Graphics tab is activated on the UI, it will default to the selected graphics layer in the TOC. If no graphics layer is selected in the TOC, it will default to the first Graphics Layer (in the TOC). To set it programatically, retrieve the desired graphics layer instance from the map and set it as the TargetGraphicsLayer: map.TargetGraphicsLayer = graphicsLayer;. Users can interactively change the TargetGraphicsLayer via the "Target layer:" combo on Graphics tab.

CIM Definition

Like all layers, Graphics Layers contain a CIM Definition, in this case CIMGraphicsLayer. The CIM Definition, however, does not contain the graphics elements. For optimization reasons, the graphics themselves are stored in what is called "Element Storage", an external binary file managed by native (cpp) code associated with the layer. The "normal" layer properties, such as visibility, selectablity, minimum and maximum viewing scales, names and so forth are accessible via the CIM (in addition to the managed properties and "set" methods on the model).

Add Elements

Graphics layer provides a number of AddElement overloads for adding elements as CIMGraphics. Point, multi-point, line, envelope, polygon, text, and picture graphics can currently be added to a graphics layer. Multipatch geometry is currently not supported.

To add an element either provide the "raw" geometry and a (optional) symbol or create the CIMGraphic first within add-in code. Internally, when a geometry is provided, the graphics layer itself creates a CIMGraphic using the symbol that was provided or a default if the provided symbol was null. When a graphics element is created it is given a unique name - unique across all element content in all layers for the given map - and its top-level parent is set as the graphics layer to which it was added. The returned GraphicElement will contain the original CIMGraphic that was used to create it.

The geometry of the graphics elements should always be in the same spatial reference as the map (containing the graphics layer).

  var graphicsLayer = MapView.Active.Map.GetLayersAsFlattenedList()
	.OfType<ArcGIS.Desktop.Mapping.GraphicsLayer>().FirstOrDefault();
			if (graphicsLayer == null)
				return;
  //place a point graphic in the middle of the map
  QueuedTask.Run(() =>  {
    var extent = MapView.Active.Extent;
    var location = extent.Center;
    //use the default symbology (a black circle)
    var ge = graphicsLayer.AddElement(location);
    var cimGraphic = ge.GetGraphic() as CIMPointGraphic;//get the contained CIMGraphic

    //specify a symbol
    var pt_symbol = SymbolFactory.Instance.ConstructPointSymbol(
                          ColorFactory.Instance.GreenRGB);
    graphicsLayer.AddElement(location, pt_symbol);

    //create a CIMGraphic 
    var graphic = new CIMPointGraphic() {
        Symbol = pt_symbol.MakeSymbolReference(),
	Location = location
    };
    graphicsLayer.AddElement(graphic);
    ...

Starting at 3.0, developers can also use the new GraphicFactory to create CIMGraphics:

var graphicsLayer = MapView.Active.Map.GetLayersAsFlattenedList()
	.OfType<ArcGIS.Desktop.Mapping.GraphicsLayer>().FirstOrDefault();
			if (graphicsLayer == null)
				return;

  //create graphics using GraphicFactory
  QueuedTask.Run(() =>  {
    var location = MapPointBuilderEx.CreateMapPoint(...);
    var line = PolylineBuilderEx.CreatePolyline(...);
    var pt_symbol = SymbolFactory.Instance.ConstructPointSymbol(...);
    var poly_symbol = SymbolFactory.Instance.ConstructPolygonSymbol(...);

    //Use GraphicsFactory
    var ptGraphic = GraphicFactory.Instance.CreateSimpleGraphic(location);
    var ptGraphic2 = GraphicFactory.Instance.CreateSimpleGraphic(location, pt_symbol);
    var lineGraphic = GraphicFactory.Instance.CreateSimpleGraphic(line);

    //use custom, predefined shapes
    var cloudGraphic = GraphicFactory.Instance.CreatePredefinedShapeGraphic(
                              PredefinedShape.Cloud, location, 10, 10, poly_symbol);
    //arrow graphic
    var arrowGraphic = GraphicFactory.Instance.CreateArrowGraphic(line, new ArrowInfo() {
                                ArrowOnBothEnds = true,
                                ArrowSizePoints = 10,
                                LineWidthPoints = 2
                        });
    //etc
    ...

Note: there are two limits for adding elements to a graphics layer (or layout):

  • A maximum graphics count of 4000 elements (to include group elements) per map
  • A total combined size limit of 10MB per map (meaning the combined size of all elements cannot exceed the size limit)

Whichever limit is hit first (if a limit is hit) will result in a InValidOperationException. Elements will have to be removed at that point before more can be added.

CIMGraphic

Each GraphicElement contains the original CIMGraphic used to create it. To access and update the contained CIMCGraphic use the GraphicElement.GetGraphic(), SetGraphic(cimGraphic) method pair. For example, use the contained graphic to retrieve all text elements within a graphics layer:

  
  var all_text = graphicsLayer.GetElementsAsFlattenedList()
                    .Where(e => e.GetGraphic() is CIMTextGraphic);

Changes can be made to symbology and/or geometry of the graphic element and applied back via SetGraphic.

 //within a queued Task
 //get the first line element in the layer
 var ge = graphicsLayer.FindElement("Line 10") as GraphicElement;
 var graphic = ge.GetGraphic();
 if (graphic is CIMLineGraphic lineGraphic) {
    //change its symbol
    lineGraphic.Symbol = 
     SymbolFactory.Instance.ConstructLineSymbol(
       SymbolFactory.Instance.ConstructStroke(
	 ColorFactory.Instance.BlueRGB, 2, SimpleLineStyle.DashDot)).MakeSymbolReference();
     //apply the change
     ge.SetGraphic(lineGraphic);
  }

Select and UnSelect Elements

When a graphics element or group element is selected, it means its bounding box is displayed on the view and grab handles are also shown. Selected elements can be moved and resized interactively with any of the COTS graphics tools.

Setting elements as selected can be done programmatically or graphically on the view. Programmatic selection takes the element or elements to be selected and highlights them on the screen. Use SelectElement(element) and SelectElements(list_of_elements) accordingly. Graphic selection uses the selection geometry to intersect the geometries of those elements (graphic or group) that will be selected and then highlights them. In this example, all the elements with a name beginning with "Text" are selected programmatically:

 //on the QueuedTask
 var elements = graphicsLayer.GetElementsAsFlattenedList()
                     .Where(e => e.Name.StartsWith("Text"));
 graphicsLayer.SelectElements(elements);

In this example a sketch geometry is being used to do a graphic element selection on the view:

 //on the QueuedTask...note: the selected elements may belong to more than one
 //layer...
 var elems = MapView.Active.SelectElements(selPoly, SelectionCombinationMethod.New);

When selecting elements programmatically, only a group element or its children can be selected but not both. This is reflected on the display when selected elements are highlighted. Note that the child elements nested within a given group element are never highlighted in conjunction with their parent - it's an either/or. If you specify a group element and one or more of its children in the list of elements to be selected then the child elements are filtered out of the list. When selecting graphically, the view element selection automatically applies the same filtering logic (the same is true for element selection on a layout) so that if a group element and one or more of its children are intersected by the selection geometry, the parent group element will be element that is selected.

Group can be selected...or Its children can be selected....but not both
Group element selected Child elements selected

In this example, a group element and its children are being specified for selection. The selection logic will filter out the children selecting only the (top most) parent - the group element in this case.

 //on the QueuedTask
 //get a group element
 var group = graphicsLayer.GetElements().OfType<GroupElement>().First();
 //try to select the group element and its children
 var elements = new List<Element>() { group };
 elements.AddRange(group.Elements);
 //children are filtered out...
 graphicsLayer.SelectElements(elements);//only the group element will be selected...
				

Unselecting elements is the same as programmatic selection only in reverse. The list of elements provided as the argument to UnSelectElements are unselected (or UnSelectElement if a single element is unselected). Their highlight on the view is cleared. Specifying an empty or null list of elements as the parameter results in all elements for the graphics layer being unselected. Specify a null element as the element to be unselected via UnSelectElement (singular) will throw a ArgumentNullException.

  //On the QueuedTask...
  //unselect the first element in the currently selected elements
  var elem = graphicsLayer.GetSelectedElements().FirstOrDefault();
  if (elem != null)
     graphicsLayer.UnSelectElement(elem);//will throw if 'elem' is null

  //unselect all elements
  graphicsLayer.UnSelectElements();//equivalent to graphicsLayer.ClearSelection()

Element Selection Changed

Whenever the selection on a graphics layer changes (elements are selected or unselected), the ArcGIS.Desktop.Layout.Events.ElementEvent is fired with an ElementEventArgs.Hint of type ElementEventHint.SelectionChanged. The ElementEventArgs.Container contains the container whose selection changed and the count of elements selected. The container can be either a Layout or a GraphicsLayer. If the selection change was an unselect or clear select, the count of selected elements will be zero. The event can fire more than once per selection. For example, assume a map contains two graphics layers with some elements already selected on one of the layers. Another selection is made (eg with the COTS select elements tool) in the layer with no elements selected. The ElementEvent fires twice. Once for the graphics layer whose selection changed because elements were selected (via the tool) and once for the graphics layer whose selection got cleared because no elements were selected (by the tool). Subscribing to the event is shown below:

 //subscribe to the ElementEvent event
 ArcGIS.Desktop.Layouts.Events.ElementEvent.Subscribe((args) =>
{
  if (args.Hint == ArcGIS.Desktop.Layouts.Events.ElementEventHint.SelectionChanged) {
    //elements have been selected - get the container on which
    //the selection has occured
    if (args.Container is Layout layout) {
      //get the selected elements
      //var sel_elems = layout.GetSelectedElements();

    }
    else if (args.Container is GraphicsLayer graphicsLayer) {
      //get the selected elements
      var sel_elems = graphicsLayer.GetSelectedElements();
      if (sel_elems.Count > 0) {
        //TODO - process selected elements
      }
      //else an unselect or clear select
    }
  }
});

Group and Ungroup Elements

To add or remove elements to a group use the GroupElements and UnGroupElement or UnGroupElements methods respectively.

When elements are grouped (to include other group elements) a new group element is created that contains the "grouped" elements. The elements' parent is set to the newly created Group Element. Elements can be grouped in any number of nested hierarchies (groups contained within groups contained within groups ....) as required. Ungrouping elements ungroups the elements within the specified group element hierarchy. Elements should be ungrouped from the top down not the bottom up. To ungroup multiple hierarchies, UnGroupElement(s) should be called multiple times starting with the top most group element in the hierarchy.

The bounding box of a group element will be set to the union of the bounding boxes of all its contained elements. Child elements are accessed via the group element Elements property. To get a flattened list of elements from a group element use group_elem.GetElementsAsFlattenedList().

Note: the top most parent of all elements is always its container - either a Layout or GraphicsLayer.

 //on the QueuedTask
 //group some elements
 var group_elem = graphicsLayer.GroupElements(graphicsLayer.GetSelectedElements());
 //flattened list
 var flat_list = group_elem.GetElementsAsFlattenedList()

 ...
 
 //check the parent
 var parent = group_elem.Elements.First().GetParent();//will be the group element
 //top-most parent
 var top_most = group_elem.Elements.First().GetParent(true);//will be the GraphicsLayer

 //Do your own recursion of group_elem.Elements...eg "Flatten"...
 var flat_list = new List<Element>();
 Flatten(group_elem.Elements, flat_list);
 ...

 private static void Flatten(IEnumerable<Element> elements, IList<Element> flattened) {
      foreach (var element in elements)
      {
        flattened.Add(element);

        // Tail recursion for Group Elements.
        if (element is GroupElement)
          Flatten((element as GroupElement).Elements, flattened);
      }
}

//Ungroup grouped elements
var selectedElements = graphicsLayer.GetSelectedElements().ToList(); ;
var elementsToUnGroup = new List<GroupElement>();
//All selected elements should be grouped.
if (selectedElements.Count() == selectedElements.OfType<GroupElement>().Count()) {
      //Convert to a GroupElement list.
      elementsToUnGroup = selectedElements.ConvertAll(x => (GroupElement)x);
}
//UnGroup
graphicsLayer.UnGroupElements(elementsToUnGroup); 

Remove Elements

Use RemoveElement or RemoveElements specifying the element or elements to be removed. Removing a Group Element will remove all of its children. Specifying null or an empty list as the list of elements to be removed results in a no-op.

 //on the QueuedTask
 //remove all the currently selected elements
 graphicsLayer.RemoveElements(graphicsLayer.GetSelectedElements());

Copy Elements

Elements from one graphics layer can be copied to any other graphics layer in the map. Copying from multiple layers requires multiple copy operations - one per layer. Elements can be copied into the graphics layer at the root level or into a group element within the graphics layer.

  //on the QueuedTask
  var elems = sourceLayer.FindElements(new List<string>() { "Point 1", "Line 3", "Text 1"});
  var copiedElements = targetLayer.CopyElements(elems);

Z-Order

Z-Order controls the order of display of individual graphics within their respective containers. Z-order runs from 0 to n-1 where 0 is the bottom of the z-order and this element draws first, and n-1 is the top of the z-order and this element draws last. Z-order of a given element can only be changed within that element's respective parent container. For example, if an element is a child of a group element, then it can be moved up or down in the z-order of children in that group element. The group element can be moved up or down in the z-order of children within its parent and so-on. This is not that much different than the way z-order works for layers in the map TOC or for elements in the Layout TOC. Layers at the bottom of the TOC (low z-order) draw first, layers at the top of the TOC (high z-order) draw last. Ditto for elements in the layout TOC.

To check the current z-order of an element, use elem.ZOrder. To change the z-order of an element or elements use BringForward and SendBackward. To move an element or elements to the top or bottom of their z-order use BringToFront and SendToBack respectively. If more than one element is specified then all elements must have the same parent (eg be children of the same group element or all be immediate children of the graphics layer). If elements do not have the same parent then an ArgumentException will be thrown. To check whether an element or elements' z-order can be changed call CanBringForward and CanSendBackward.

 //On the QueuedTask
 //get the current selection set
 var sel_elems = graphicsLayer.GetSelectedElements();
 //can they be brought forward? This will also check that all elements have the same parent
 if (graphicsLayer.CanBringForward(sel_elems)) {
   //bring forward
   graphicsLayer.BringForward(sel_elems);
   //bring to front (of parent)
   //graphicsLayer.BringToFront(sel_elems);
 }
 else if (graphicsLayer.CanSendBackward(sel_elems)) {
   //send back
   graphicsLayer.SendBackward(sel_elems);
   //send to the back (of parent)
   //graphicsLayer.SendToBack(sel_elems);
 }

 //list out the z order
 foreach(var elem in sel_elems)
   System.Diagnostics.Debug.WriteLine($"{elem.Name}: z-order {elem.ZOrder}");

Move Elements

Elements can be moved programmatically by applying a new anchor point via element.SetAnchorPoint(). The element will automatically move its location such that its anchor point is re-positioned at the new position specified. The relative position of the anchor point does not change (to change the anchor use element.SetAnchor(anchor) where anchor can be one of 9 relative positions)

Elements can also be moved interactively once they are selected by using the built-in element selection tool (which changes to "move" mode whenever the cursor is over or "hittests" against a selected element).

In this example, a user is using a custom map tool to move the selected elements to a point he or she clicks on the map. The tool calculates the distance between the point clicked and the closest element in the selected set then moves all elements "that" distance. This maintains the relative position of all the selected elements to each other (rather than them all simply being moved, one on top of the other, to the same point):

 //in the tool ctor
 public MoveGraphicsTool() {
   SketchType = SketchGeometryType.Point;
   ...

protected override Task<bool> OnSketchCompleteAsync(Geometry geometry) {
  var elems = new Dictionary<Element, Coordinate2D>();
  var pt = geometry as MapPoint;//the point clicked
  var sr = MapView.Active.Map.SpatialReference;

  var graphicsLayers = MapView.Active.Map.GetLayersAsFlattenedList()
                           .OfType<GraphicsLayer>();

  return QueuedTask.Run(() => {

      var coords = new List<Coordinate2D>();
      foreach(var graphicsLayer in graphicsLayers) {
         foreach(var elem in graphicsLayer.GetSelectedElements()) {
	      //check minimum distance to the clicked point
              var anchor = elem.GetAnchorPoint();
	      coords.Add(anchor);
	      elems.Add(elem, anchor);
          }
       }
       var mb = new MultipointBuilderEx(coords, sr);
       var nearest_res = GeometryEngine.Instance.NearestPoint(mb.ToGeometry(), pt);
					
       //move all elements the same minimum distance to maintain their position
       //relative to each other
       var dx = pt.X - nearest_res.Point.X;
       var dy = pt.Y - nearest_res.Point.Y;

       //do the move
       foreach(var kvp in elems)  {
          var anchor = new Coordinate2D(kvp.Value.X + dx, kvp.Value.Y + dy);
          kvp.Key.SetAnchorPoint(anchor);
       }
       return true;
  });
}

Styling Graphic Elements

Graphic elements can be styled using an existing StyleItem in a StyleProjectItem. A style item can be programmatically applied to graphic elements (point, line, polygon and text) in Maps (and layouts). To get a specific StyleItem from a StyleProjectItem, use the following workflow:

  • Get the StyleProjectItem that has the style you need (typically ArcGIS 2D).
  • Use the SearchSymbols method on the StyleProjectItem to retrieve your StyleItem. Pass in the StyleItemType enumeration and search string for the item you are looking for.

The code snippet below shows how you can get the "City Hall" point style item from the "ArcGIS 2D" StyleProjectItem.

   //Use within QueuedTask.Run
   //Reference a point symbol in a style
   var stylePrjItm = Project.Current.GetItems<StyleProjectItem>().FirstOrDefault(
                                                         item => item.Name == "ArcGIS 2D");
   //Get the specific SymbolStyleItem
   var ssi = stylePrjItm.SearchSymbols(StyleItemType.PointSymbol, "City Hall")[0];
   //set the symbol size to be 50pts
   ((CIMPointSymbol)ssi.Symbol).SetSize(50);

Next, use the element instance ApplyStyle method to change its existing style, passing in the the Symbol StyleItem to be applied. Additionally, call CanApplyStyle to determine if the input style can be applied. When applying a style to graphic elements - points, lines, polygons and texts - the default behavior is to apply the symbol size embedded in the style to the styled element (this also matches the behavior of the style gallery on the Pro UI). To retain the existing size of your graphic elements when they are being styled use the ApplyStyle method overload with a keepSymbolSize = true parameter value. For example:

 //Use within QueuedTask.Run
 //using the "City Hall" (point) symbol style item retrieved in the previous snippet
 var ssi = ... ;

 //get the desired point graphics element to be styled
 var gl = .... //get the desired graphics layer
 var ptElm = gl.FindElement("New Point") as GraphicElement;
 //Check if the input style can be applied to the element
 if (ptElm.CanApplyStyle(ssi))
   //Apply the style - use the symbol size of the style item (default)
   ptElm.ApplyStyle(ssi);

   //to keep the symbol size of the element use the overload of ApplyStyle
   //with a keepSymbolSize param of "true"
   ptElm.ApplyStyle(ssi, true);
⚠️ **GitHub.com Fallback** ⚠️