ProConcepts Layouts - kataya/arcgis-pro-sdk GitHub Wiki
The layout functionality in ArcGIS Pro is delivered through the ArcGIS.Desktop.Layouts assembly. This assembly provides classes and members that support managing layouts, layout elements and working with layout views. This includes creating new layouts and layout elements, modifying existing elements, managing selections, and layout view control and navigation.
-
ArcGIS.Desktop.Core.dll
-
ArcGIS.Desktop.Layouts.dll
Language: C#
Subject: Layouts
Contributor: ArcGIS Pro SDK Team <[email protected]>
Organization: Esri, http://www.esri.com
Date: 11/24/2020
ArcGIS Pro: 2.7
Visual Studio: 2017, 2019
- Layout class
- Layout elements
- Layout metadata
- CIM elements
- LayoutView class
- A complete, working example
- Map Series
The Layout class represents a page layout in a project and provides access to basic layout properties, including page information, access to elements, and export methods. When working with layouts, one of the primary steps is to either reference an existing layout or create a new layout. A project can contain zero to many layouts. Within the application, existing layouts appear in the Catalog pane as individual project items, or as open views. Layout views are associated with a single layout. You can have multiple views for a single layout. For a project, only one view can be active at a time. The active view may, or may not be a layout view.
There are a couple of ways to reference an existing layout in a project. The easiest way is simply to reference a layout view that is already open and active in the application. The LayoutView class has an Active property that returns the current layout view if one exists. If there is indeed a active layout view, then the Layout property can be used to return the associated Layout.
//Reference a layout associated with an active layout view
LayoutView activeLytView = LayoutView.Active;
if (activeLytView != null)
{
Layout lyt = activeLytView.Layout;
}
A layout view may not always be active so another way to reference an existing layout is to return a layout that is associated with a LayoutProjectItem located in the Catalog pane. If the item exists, then the GetLayout method can be used to return the associated Layout.
//Reference a layout associated with a layout project item by name
LayoutProjectItem lytItem = Project.Current.GetItems<LayoutProjectItem>().FirstOrDefault(item => item.Name.Equals("Some Layout Name"));
if (lytItem != null)
{
//Get the layout associated with the layoutitem
Layout lyt = await QueuedTask.Run(() => layoutItem.GetLayout());
}
When you create a new layout using the SDK with the LayoutFactory.Instance.CreateLayout method, it creates a layout project item that automatically appears in the Contents pane but it does not automatically open the layout in a new layout view. This is a separate operation and will be covered in the LayoutView class section.
//Create a new layout using minimum properties and sets its name
QueuedTask.Run<Layout>(() =>
{
newLyt = LayoutFactory.Instance.CreateLayout(8.5, 11, LinearUnit.Inches);
newLyt.SetName("New Layout Name");
});
After having a reference to a layout, the next logical step is to either modify existing page layout elements or create new elements. The SDK managed API provides access to the common members for each element type but understand that many more properties are available via the Cartographic Information Model (CIM) associated with each element. Several examples will be provided in the CIM Elements section. This section will focus on the managed API components only.
The easiest way to reference a layout element is to use the FindElement method on the Layout class. All layout elements within a single layout are forced to have a unique name so that they can be uniquely searched. It is highly recommended to give each element a meaningful name to simplify referencing the element. The FindElement method will search for all elements in the layout Contents pane even if the elements are organized in group elements.
//Find an element anywhere in the layout
QueuedTask.Run(() =>
{
Layout lyt = layoutItem.GetLayout();
if (lyt != null)
{
Element rect = lyt.FindElement("Rectangle") as Element;
}
});
You can also search for an element in a collection based on how elements are organized in the layout Contents pane. For example, the Elements property called from the Layout class will only return root level elements. If the Elements property is called from a referenced GroupElement, then only the root level elements in that group are returned.
//Find an element within a specific Elements collection
QueuedTask.Run(() =>
{
Layout lyt = layoutItem.GetLayout();
if (lyt != null)
{
Element rect = lyt.Elements.FirstOrDefault(item => item.Name.Equals("Rectangle"));
}
});
The LayoutElementFactory class has an Instance property that allows you to create nearly all the different layout element types. When constructing a new element, the elementContainer parameter controls if the element gets created at the root level of the Contents pane or if it gets created within a group. If you pass in a reference to a Layout it gets created at the root level of the Contents pane and if you pass in a reference to a GroupElement the new element gets created in the referenced group. The second parameter represents the geometry of the element in page units and this will differ based on the element type. For example, some factories may require a rectangle or envelope to define the area on the layout for it to be displayed while other constructors may only take a simple point geometry. In most cases, the last, optional parameter defines the CIM symbol to be associated with the element. If not defined, then a default symbol will be assigned. The CIM elements section below provides an example of setting advanced symbology for layout elements.
//Create a simple 2D rectangle graphic with default symbology
QueuedTask.Run(() =>
{
//Build 2D envelope geometry
Coordinate2D rec_ll = new Coordinate2D(1.0, 4.75);
Coordinate2D rec_ur = new Coordinate2D(3.0, 5.75);
Envelope rec_env = EnvelopeBuilder.CreateEnvelope(rec_ll, rec_ur);
//Create and add element to layout
GraphicElement recElm = LayoutElementFactory.Instance.CreateRectangleGraphicElement(layout, env);
recElm.SetName("New Rectangle");
});
There is a large collection of examples that create other layout elements types in the Layout ProSnippets wiki page.
As discussed in the Create a new element section, elements can be created directly within an existing group element. You also have the ability to create new group elements, either at the root level of the layout Contents pane or create a group element within another group element. Again, if you pass in a reference to a Layout it gets created at the root level of the Contents pane and if you pass in a reference to a GroupElement the new element gets created in the referenced group.
//Create an empty group element at the root level of the contents pane
QueuedTask.Run(() =>
{
GroupElement grp1 = LayoutElementFactory.Instance.CreateGroupElement(layout);
grp1.SetName("Group");
});
//Create a group element inside another group element
QueuedTask.Run(() =>
{
GroupElement grp2 = LayoutElementFactory.Instance.CreateGroupElement(grp1);
grp2.SetName("Group in Group");
});
There are two basic methods on the Element class that allow you to control the exact placement and order of elements in the layout Contents pane. A single element or an entire group element can be repositioned. The SetTOCPositionAbsolute method sets the position of an element either at the top or bottom of the targetContainer. So for example, if the targetContainer is a Layout and the isTop parameter is set to true, the element will be placed at the top of the Contents pane. Likewise, if the targetContainer is a GroupElement and the isTop parameter is set to false, the element is placed at the bottom of the group element.
//Place an element at the top of the layout contents pane
element.SetTOCPositionAbsolute(layout, true);
The SetTOCPositionRelative method sets the position of an element relative to another referenceElement either above or below it. This also works if the referenceElement is an element at the root level of the Contents pane or if it is an element in a group. If the isAbove parameter is set to false, the element will be placed below the reference layer.
//Place an element below the referenced element
element.SetTOCPositionRelative(ref_element, false);
Layout implements the ArcGIS.Desktop.Core.IMetadataInfo
to provide read and write access to its metadata*. Layout metadata is saved with the layout in the .aprx. Metadata retrieved from the layout will be in xml format and will be styled according to whatever is the current metadata style in use. It is the add-in's responsibility to ensure that what the rules associated with the current metadata style are met when editing metadata.
//Supports access and update of metadata
public interface IMetadataInfo {
// Returns true if the metadata can be edited otherwise false
bool GetCanEditMetadata();
// Returns the xml formatted metadata
string GetMetadata();
//Set the metadata xml. The requirements of the current style
//should be met
void SetMetadata(string metadataXml);
}
In this example, the layout metadata is retrieved and "set" without any changes if the layout metadata is editable. As a minimum, for layout metadata to be editable, the project access permissions must be read/write.
var layout = LayoutView.Active.Layout; //Assume null check...
QueuedTask.Run(() => {
//read the layout metadata and set it back if the metadata
//is editable
var layout_xml = layout.GetMetadata();
if (layout.GetCanEditMetadata())
layout.SetMetadata(layout_xml);
});
* Metadata access is also available off the ArcGIS.Desktop.Layouts.LayoutProjectItem
The layout SDK managed API provides access to many of each layout element's common members but understand that many more properties are available via the Cartographic Information Model (CIM) associated with each element. You have the ability to either create new CIM elements or you can return the CIM definition for an existing element. A standard method that returns the CIM definition for an element is GetDefinition. Here is a list of all the CIM classes. The remainder of this section will simply demonstrate how CIM classes can be used to expand upon the managed API for layouts.
- The following example creates a new layout by passing in a new CIMPage with expanded capabilities verses creating a layout with only a few basic parameters exposed to the CreateLayout method.
//Set up a page
CIMPage newPage = new CIMPage();
//required properties
newPage.Width = 17;
newPage.Height = 11;
newPage.Units = LinearUnit.Inches;
//optional rulers
newPage.ShowRulers = true;
newPage.SmallestRulerDivision = 0.5;
layout = LayoutFactory.Instance.CreateLayout(newPage);
layout.SetName("New CIM Layout");
- This next example creates a new rectangle element like in the Create a new element section but this time uses the CIMStroke and CIMPolygonSymbol to improve the element's appearance.
QueuedTask.Run(() =>
{
//Build 2D envelope geometry
Coordinate2D rec_ll = new Coordinate2D(1.0, 4.75);
Coordinate2D rec_ur = new Coordinate2D(3.0, 5.75);
Envelope rec_env = EnvelopeBuilder.CreateEnvelope(rec_ll, rec_ur);
//Set symbolology, create and add element to layout
CIMStroke outline = SymbolFactory.Instance.ConstructStroke(ColorFactory.Instance.BlackRGB, 5.0, SimpleLineStyle.Solid);
CIMPolygonSymbol polySym = SymbolFactory.Instance.ConstructPolygonSymbol(ColorFactory.Instance.GreenRGB, SimpleFillStyle.DiagonalCross, outline);
GraphicElement recElm = LayoutElementFactory.Instance.CreateRectangleGraphicElement(layout, rec_env, polySym);
recElm.SetName("New Rectangle");
});
- This example first returns the CIM definition for the layout and then uses the members exposed to the CIMLegend class to make modifications that can't be done using the mangaged API.
await QueuedTask.Run(() =>
{
//Get LayoutCIM and iterate through its elements
var layoutDef = layout.GetDefinition();
foreach (var elem in layoutDef.Elements)
{
if (elem is CIMLegend)
{
var legend = elem as CIMLegend;
foreach (var legendItem in legend.Items)
{
//Change the text for item label to GREEN
var itemLabel = legendItem.LabelSymbol.Symbol as CIMTextSymbol;
foreach(var symlayer in ((CIMPolygonSymbol)itemLabel.Symbol).SymbolLayers)
{
if (symlayer is CIMSolidFill)
{
var itemLabelSym = (CIMSolidFill)symlayer;
itemLabelSym.Color.Values[0] = 0;
itemLabelSym.Color.Values[1] = 255;
itemLabelSym.Color.Values[2] = 0;
}
}
}
break;
}
}
//Apply the changes back to the layout
layout.SetDefinition(layoutDef);
});
- This example shows how you can set up a spatial map series using the CIMSpatialMapSeries class and then apply that to the referenced layout. Map series support will be enhanced in the layout API across the upcoming releases.
//Get the Layout CIM
CIMLayout layCIM = layout.GetDefinition();
//Create a new MapSeries CIM
layCIM.MapSeries = new CIMSpatialMapSeries();
CIMSpatialMapSeries ms = layCIM.MapSeries as CIMSpatialMapSeries;
//Set map series properties
ms.Enabled = true;
ms.MapFrameName = "New Map Frame";
ms.StartingPageNumber = 1;
ms.CurrentPageID = 1;
ms.IndexLayerURI = "CIMPATH=map/greatlakes.xml";
ms.NameField = "NAME";
ms.SortField = "NAME";
ms.SortAscending = true;
ms.ScaleRounding = 1000;
ms.ExtentOptions = ExtentFitType.BestFit;
ms.MarginType = ArcGIS.Core.CIM.UnitType.Percent;
ms.Margin = 0;
//Apply the map series CIM back to the layout
layout.SetDefinition(layCIM);
A LayoutView is simply a view of a layout. Layout views are the primary interface used to display, navigate, select, and edit layout elements in the layout. The layout being visualized in the view can be accessed via the Layout property (e.g. LayoutView.Active.Layout
). There are several scenarios that need to be evaluated when trying to reference a layout view:
- A layout view can only exist if there is layout project item.
- If a layout project item does exist, it doesn't mean that a layout view is open in the application.
- Multiple layout views that reference the same layout can exist.
- A layout view may be open but it may not be active.
- The active view is not necessarily a layout view.
In the Reference an existing layout section there is a sample that shows how to reference a layout associated with the active layout view. But another scenario involves making an already open layout view active. This requires that you iterate through the pane collection available to the FrameWorkApplication class, isolate the pane of interest, and then active it.
//Check to see if a layout view exists. If it does, activate it.
//Confirm if layout exists as a project item
LayoutProjectItem layoutItem = Project.Current.GetItems<LayoutProjectItem>().FirstOrDefault(item => item.Name.Equals("some layout"));
if (layoutItem != null)
{
Layout lyt = await QueuedTask.Run(() => layoutItem.GetLayout());
//Next check to see if a layout view is already open that referencs the Game Board layout
foreach (var pane in ProApp.Panes)
{
var lytPane = pane as ILayoutPane;
//if not a layout view, continue to the next pane
if (lytPane == null) //if not a layout view, continue to the next pane
continue;
//if there is a match, activate the view
if (lytPane.LayoutView.Layout == lyt)
{
(lytPane as Pane).Activate();
return;
}
}
As mentioned in the Create a new element section, when a layout is created using the SDK, a layout view is not automatically generated. You must open the layout in a new view using the CreateLayoutPaneAsync method. Be sure to call this on the main GUI thread and use the await statement.
//As a continuation of the code in the previous section
//If panes don't exist, then open a new pane
await ProApp.Panes.CreateLayoutPaneAsync(lyt);
return;
The LayoutView class has several methods for managing layout element selection: ClearElementSelection, GetSelectedElements, SelectAllElements, and SelectElements. When either getting or setting the selection, a list collection is used. The LayoutView class also has multiple methods for navigating the layout.
// Find two graphic rectangle elements and set them to be selected and then zoom the page to the selection.
if (LayoutView.Active != null)
{
LayoutView lytView = LayoutView.Active;
Layout lyt = await QueuedTask.Run(() => lytView.Layout);
Element rec = lyt.FindElement("Rectangle");
Element rec2 = lyt.FindElement("Rectangle 2");
List<Element> elmList = new List<Element>();
elmList.Add(rec);
elmList.Add(rec2);
//Set selection
lytView.SelectElements(elmList);
//Zoom to selection
await QueuedTask.Run(() => lytView.ZoomToSelectedElements());
}
This section provides an example of code that takes many of the elements mentioned in the previous sections and combines them into a single, working example that can be copied into a Visual Studio solution. It first confirms that the layout does not exist in a pane or as a layout project item, then it creates a new layout, then adds elements like map frames and text elements, sets element symbology, creates a new view, and finally, clears the current selection.
async protected override void OnClick()
{
//Check to see if the the layout already exists
LayoutProjectItem layoutItem = Project.Current.GetItems<LayoutProjectItem>().FirstOrDefault(item => item.Name.Equals("some layout"));
if (layoutItem != null)
{
Layout lyt = await QueuedTask.Run(() => layoutItem.GetLayout());
//Next check to see if a layout view is already open that references the layout
foreach (var pane in ProApp.Panes)
{
var lytPane = pane as ILayoutPane;
if (lytPane == null) //if not a layout view, continue to the next pane
continue;
if (lytPane.LayoutView.Layout == lyt) //if there is a match, activate the view
{
(lytPane as Pane).Activate();
System.Windows.MessageBox.Show("Activating existing pane");
return;
}
}
//If panes don't exist, then open a new pane
await ProApp.Panes.CreateLayoutPaneAsync(lyt);
System.Windows.MessageBox.Show("Opening already existing layout");
return;
}
//The layout does not exist so create a new one
Layout layout = await ArcGIS.Desktop.Framework.Threading.Tasks.QueuedTask.Run<Layout>(() =>
{
//*** CREATE A NEW LAYOUT ***
//Set up a page
CIMPage newPage = new CIMPage();
//required properties
newPage.Width = 17;
newPage.Height = 11;
newPage.Units = LinearUnit.Inches;
//optional rulers
newPage.ShowRulers = true;
newPage.SmallestRulerDivision = 0.5;
layout = LayoutFactory.Instance.CreateLayout(newPage);
layout.SetName("Game Board");
//*** INSERT MAP FRAME ***
// create a new map with an ArcGIS Online basemap
Map map = MapFactory.Instance.CreateMap("World Map", MapType.Map, MapViewingMode.Map, Basemap.NationalGeographic);
//Build map frame geometry
Coordinate2D ll = new Coordinate2D(4, 0.5);
Coordinate2D ur = new Coordinate2D(13, 6.5);
Envelope env = EnvelopeBuilder.CreateEnvelope(ll, ur);
//Create map frame and add to layout
MapFrame mfElm = LayoutElementFactory.Instance.CreateMapFrame(layout, env, map);
mfElm.SetName("Main MF");
//Set the camera
Camera camera = mfElm.Camera;
camera.X = 3365;
camera.Y = 5314468;
camera.Scale = 175000000;
mfElm.SetCamera(camera);
//*** INSERT TEXT ELEMENTS ***
//Title text
Coordinate2D titleTxt_ll = new Coordinate2D(6.5, 10);
CIMTextSymbol arial36bold = SymbolFactory.Instance.ConstructTextSymbol(ColorFactory.Instance.BlueRGB, 36, "Arial", "Bold");
GraphicElement titleTxtElm = LayoutElementFactory.Instance.CreatePointTextGraphicElement(layout, titleTxt_ll, "Feeling Puzzled?", arial36bold);
titleTxtElm.SetName("Title");
//Service layer credits
Coordinate2D slcTxt_ll = new Coordinate2D(0.5, 0.2);
Coordinate2D slcTxt_ur = new Coordinate2D(16.5, 0.4);
Envelope slcEnv = EnvelopeBuilder.CreateEnvelope(slcTxt_ll, slcTxt_ur);
CIMTextSymbol arial8reg = SymbolFactory.Instance.ConstructTextSymbol(ColorFactory.Instance.BlackRGB, 8, "Arial", "Regular");
String slcText = "<dyn type='layout' name='Game Board' property='serviceLayerCredits'/>";
GraphicElement slcTxtElm = LayoutElementFactory.Instance.CreateRectangleParagraphGraphicElement(layout, slcEnv, slcText, arial8reg);
slcTxtElm.SetName("SLC");
return layout;
});
//*** OPEN LAYOUT VIEW (must be in the GUI thread) ***
var layoutPane = await ProApp.Panes.CreateLayoutPaneAsync(layout);
var sel = layoutPane.LayoutView.GetSelectedElements();
if (sel.Count > 0)
{
layoutPane.LayoutView.ClearElementSelection();
}
}
}
}
At 2.3, layout introduces a ArcGIS.Desktop.Layout.MapSeries
class and map series export options to support multipage export.
The MapSeries class provides a static CreateSpatialMapSeries(...)
factory method that generates a spatial map series for a given layout using the specified index layer (passed as a parameter). (Note: A map series is a collection of map pages (also known as map sheets) built from a single layout that represents a geographic area. A spatial map series uses an index layer where each feature in the layer represents the geographic area of an individual map sheet, one per feature.) Use the returned SpatialMapSeries class instance to refine the map series format options (extent options, margin settings, etc.) before "setting" it on the layout using the layout.SetMapSeries()
method. Setting a map series onto a layout always overwrites any existing map series. The map series associated with a layout (if there is one) can be accessed off its layout.MapSeries
property.
//Construct map series on worker thread
await QueuedTask.Run(() =>
{
//SpatialMapSeries constructor - required parameters
var SMS = MapSeries.CreateSpatialMapSeries(layout, mapFrame, countiesLayer, "US Counties");
//Set optional, non-default values
SMS.CategoryField = "State";
SMS.SortField = "Population";
SMS.ExtentOptions = ExtentFitType.BestFit;
SMS.MarginType = ArcGIS.Core.CIM.UnitType.PageUnits;
SMS.MarginUnits = ArcGIS.Core.Geometry.LinearUnit.Centimeters;
SMS.Margin = 1;
layout.SetMapSeries(SMS); //Will overwrite an existing map series.
});
To export a map series, the MapSeriesExportOptions
class can be used, in conjunction with an ExportFormat
to create multi-page exports. For PDF and TIFF, the map series can be either be exported as individual files, one per map sheet, or as a single multi-page (or multi-image) file containing all of the (specified) map sheets. For all other export formats, individual pages are exported as individual files.
For example, the following code exports a custom range of pages from a map series to a multi-page PDF:
//Export a map series with multiple pages to a single PDF from the active layout.
var layout = LayoutView.Active;
await QueuedTask.Run(() => {
if (layout == null) return;
// create the name of the pdf file
var pdf = System.IO.Path.Combine(
System.IO.Path.GetTempPath(),
"US States.pdf");
if (System.IO.File.Exists(pdf))
System.IO.File.Delete(pdf);
//Specify the exportFormat - PDF,
var exportFormat = new PDFFormat() {
OutputFileName = pdf,
Resolution = 300,
DoCompressVectorGraphics = true,
DoEmbedFonts = true,
HasGeoRefInfo = true,
ImageCompression = ImageCompression.Adaptive,
ImageQuality = ImageQuality.Better,
LayersAndAttributes = LayersAndAttributes.LayersAndAttributes
};
//Set up map series export options,
//this is new at 2.3
var mapSeriesExportOptions = new MapSeriesExportOptions() {
ExportPages = ExportPages.Custom, //All, Current, SelectedIndexFeatures
CustomPages = "1-3",
ExportFileOptions = ExportFileOptions.ExportAsSinglePDF,
};
//Note:
//use ExportFileOptions.ExportMultipleNames or ExportFileOptions.ExportMultipleNumbers
//for multi file exports. Your pdf file name is modified with a suffix per map sheet
//Check to see if the path is valid and export
if (exportFormat.ValidateOutputFilePath()) {
layout.Export(exportFormat, mapSeriesExportOptions);
//pop the pdf
System.Diagnostics.Process.Start(pdf);
}
});