ArcGIS SDK  

Extending ArcObjects

Clippable Index Grid Example

In this section:

  1. Clippable index grid example
  2. The case for a custom map grid
  3. Creating the clippable index grid
  4. Implementing other kinds of custom grids
  5. Plugging your custom grid into ArcMap
  6. Creating a ClippableIndexGridFactory
  7. Creating a property page for the ClippableIndexGrid
  8. A User Interface for creating new custom map grids

Clippable index grid example

Object Model Diagram Click here

Example Code Click here

Description The project provides an index grid for a Map that can be clipped to a certain shape. An accompanying factory coclass allows the map grid to be created by using the standard ArcMap user interface. The property pages allow the properties of the grid to be set via the ArcMap user interface.

Design ClippableMapGrid is a subtype of the MapGrid abstract class and IndexGrid coclass. ClippableIndexGridFactory is a subtype of the MapGridFactory abstract class. ClippableGridPage and NewClippableGridPage both implement standard property page interfaces. A helper coclass, EnumElement, implements IEnumElement.

License required ArcView or above.

Libraries ArcMapUI, Carto, CartoUI, Display, Framework, Geometry, Geodatabase, GeodatabaseUI, System, and SystemUI.

Languages Visual C++

Categories ESRI Map Grid Factories, ESRI Map Grid Property Pages, and ESRI Map Property Pages

Interfaces IMapGrid, IIndexGrid, IMapGridFactory, and IEnumElement.

How to use

    1. Open the CustomMapGrid.dsp workspace and build the project. This will register the CustomMapGrid.dll and register coclasses to the required component categories.
    2. Open ArcMap and add a few layers to the map in the default data view.
    3. Zoom the data view to the extent you want to display.
    4. Create a graphic element defining the shape of the grid you require; ensure that graphic element is selected before continuing.
      If you want to define the shape of the grid based on a feature in the map, first use the Select Features tool to select the feature. Then use the Pointer tool, right-click the graphic, and click Convert Features to Graphics.
    5. Choose the page layout view, right-click the map frame, and click Properties from the context menu. The Data Frame Properties dialog box should now be displayed.
    6. Click the New Clippable Index Grid tab and check the Create new clippable index grid check box. Set the name, columns, rows, and tab style as required.
    7. Click the Use Selected Data Graphic button to set the selected graphic element as the shape of the clippable index grid. Click OK to dismiss the Data Frame Properties dialog box.
      You should now be able to see your clippable index grid displayed around your dataframe.

      Notes The grid will draw inside the dataframe, instead of around the edge of the dataframe, as is more usual for other grids. You may want to restrict the extent of the data frame further, once the grid is displayed. You may also want to remove from the map any features that intersect or fall outside of the grid by applying a definition query to filter the visible features.

      If you used an element for the clip geometry, you may want to return to the data view and delete the graphic after the grid has been set up. Alternatively, hide the graphic by setting its color to 'No Color'.

The case for a custom map grid

Maps are often presented in a rectangular format—for example, the pages of a road atlas.

However, geographical features—cities, administrative regions, counties, countries, and rivers—are most often irregularly shaped and do not always fit well into a rectangular frame.

When producing maps in ArcGIS, if the area you are mapping does not conform to a rectangular frame, you can clip your dataframe to a shape of your choosing, as shown in the map on the left.

Imagine now that you need to produce an overview map, dividing this map into sections that indicate the boundaries of a series of more detailed maps, like the index map that is often given at the start of a road atlas. To satisfy this requirement, you can create an overview map with an overlaid grid using the Grids and Graticules wizard in ArcMap.


Generally, in this situation you would apply an index grid (called a reference grid in the Grids and Graticules wizard) to your map, which is specifically designed for this requirement. Displayed on a PageLayout, it divides a map frame into a chosen number of columns and rows, labelling each division along the axes, allowing each section to be identified clearly.

Your overview map has an irregular shape—but the index grid uses a rectangular shape. You can see (left) that your overview map will have some empty divisions, which you do not require and which may be misleading.

Alternatively, you could apply a measured grid or a graticule, dividing the map into sections based on a chosen map distance or by latitude and longitude. Both of these grids also use a rectangular grid, and neither is specifically designed for use as an index map.

By programming with ArcObjects, you have a fourth option—you could use a CustomOverlayGrid. This option is not available through the Grids and Graticules wizard. By using this coclass, you can create a grid based on the line features of your own data source.

The CustomOverlayGrid, however, labels the grid lines themselves, not the grid squares created. It is also a somewhat rigid solution because to change the number of columns or rows in the grid, you are required to edit the line features on which your grid is based.

As your requirements for an index map grid are not met by the standard map grids available in ArcGIS, you must create a custom map grid.



Creating the clippable index grid


To solve the requirements of this example, you will create a subtype of IndexGrid, called ClippableIndexGrid. You will implement IMapGrid and IIndexGrid as well as the standard interfaces for cloning and persistence. To add the custom functionality, you will also create and implement a custom interface, IClippableIndexGrid.


As the design is based closely on the standard index grid, you can delegate many of its members to the members of a contained IndexGrid. You will adapt the standard functionality of this index grid to create a grid that can follow the shape of the map data or map frame—the most flexible approach being to allow the grid to be clipped to any chosen shape.

This can be achieved by implementing your own Draw method, instead of delegating the call to the contained IndexGrid.


To allow users to add a ClippableIndexGrid to a dataframe, you will continue the example by creating a factory object, which can be used by ArcMap to create instances of your custom grid. You also need to allow the properties of a ClippableIndexGrid to be set and edited by a user. Both these issues are dealt with in later sections.

Now that the design of the class is decided, you need to look in more detail at how to implement the important members of each interface on the ClippableIndexGrid coclass.

Implementing IMapGrid

When your coclass implements IMapGrid, it becomes a map grid and can be treated as such in the ArcGIS system.

As the ClippableIndexGrid class design uses containment, most of the members of IMapGrid can be delegated directly to the contained IndexGrid coclass.

The members of IMapGrid for which you need to modify behavior—those which cannot be directly delegated to the IndexGridare discussed in turn below. For the benefit of those adapting this sample, typical actions that should be performed in each of the members are also summarized separately in the following table.

IMapGrid members and descriptions
BorderReturn or set an IMapGridBorder reference, storing the map grid border.
DrawYou will perform much of the work of IMapGrid in this method. Draw the map grid for a map frame to the given Display. Draw all the components of the map grid: the grid lines, ticks, subticks, tick marks, border, and labels.
ExteriorWidthReturn the width (in display units) of the portion of the grid that is outside the frame.
GenerateGraphicsGenerate graphic elements corresponding to the grid lines and store them in the specified graphics container. Your code will be similar to the Draw method, except that instead of drawing geometries to the display with their respective symbols, the symbols and the geometries are put into an element and the element is added to a group element.
LabelFormatReturn or set an IGridLabel reference storing the label format for the map grid labels.
LineSymbolReturn or set an ISymbol reference. Use this to draw the grid lines. If this property is null, you do not need to draw any grid lines.
NameReturn or set a string value indicating the name of the current map grid.
PrepareForOutputPerform any actions required to prepare the map grid for output to a device. Generally, you would get the Map associated with the MapFrame parameter. From the Map's IActiveView interface, you would get the ScreenDisplay; from the ScreenDisplay, get the DisplayTransformation. Apply the Map's FullExtent as the transformation's Bounds, and the Map's VisibleExtent as the transformation's VisibleBounds. You would also apply the passed in PixelBounds as the DisplayTransformation's DeviceFrame.
QueryLabelVisibilityReturn values indicating the visibility of the labels along all four sides of the map grid.
QuerySubTickVisibilityReturn values indicating the visibility of the subticks along all four sides of the map grid.
QueryTickVisibilityReturn values indicating the visibility of the ticks along all four sides of the map grid.
SetDefaultsReset all the member variables storing properties of the map grid to their default values.
SetLabelVisibilitySet values indicating the visibility of the labels along all four sides of the map grid.
SetSubTickVisibilitySet values indicating the visibility of the subticks along all four sides of the map grid.
SetTickVisibilitySet values indicating the visibility of the ticks along all four sides of the map grid.
SubTickCountReturn or set an integer indicating the number of subticks to draw between the major ticks.
SubTickLengthReturn or set a double indicating the length of the subticks in points.
SubTickLineSymbolReturn or set an ILineSymbol reference storing the LineSymbol used to draw the subtick lines.
TickLengthReturn or set a double indicating the length of the major ticks in points.
TickLineSymbolReturn or set an ILineSymbol reference storing the LineSymbol used to draw the major ticks.
TickMarkSymbolReturn or set an IMarkerSymbol reference storing the MarkerSymbol used to draw tick marks at the grid interval intersections. If null, do not draw any tick mark intersections.
VisibleReturn or set a Boolean value indicating if the map grid is visible.

The Draw and GenerateGraphics methods

The Draw method is called when a PageLayout containing a clippable index grid is refreshed. In this method, you must draw all of the elements of the clippable index grid to the specified Display object.

The GenerateGraphics method is called if the user clicks the Convert to Graphics button on the Grids property page of the Data Frame Properties dialog box. In this method, you need to convert your grid into individual graphic elements.

As the ClippableIndexGrid has a fundamentally different appearance than the standard IndexGrid and does not display all the items that an IndexGrid would, you must implement these two methods from scratch instead of delegating them.

Much of the internal logic required for these two methods is similar; therefore, you can modularize your code by using a single internal method to do most of the work for both the Draw and GenerateGraphics methods. In this example, the internal method DisplayGrid can either draw directly to a Display or add elements to a GroupElement, depending on the type of parameters it receives.

The creation of the actual appearance of a map grid is done by the Draw and GenerateGraphics methods.
The Draw method draws the grid to a Display, and the GenerateGraphics method creates a graphic element for each part of the grid.
The ClippableIndexGrid uses a general function, DisplayGrid, to perform either of these acts.

This design helps you keep all your grid calculation and drawing code in one place, making your code more modular and easier to update should you need to change how your grid draws.

The following steps describe the main actions of the DisplayGrid function, illustrated by brief extracts of code; the full code can be found in the accompanying VC++ example project.

For clarity, the code is described as for the Draw method. In the accompanying VC++ project you can see how this function deals with both drawing to a Display and adding graphic elements to a GroupElement.

The 'clip geometry'

Note that in this section you will use a 'clip geometry'—this is the geometry set by the user on which the shape of the clippable index grid is based. Its value will come from the ClipGeometry property of the IClippableIndexGrid interface, which you will implement later.

DisplayGrid Part 1preparing the shape of the clipped grid

The first step to displaying a grid is to calculate the shape of the grid and the intervals of the grid lines. To do this, you will need to transform from Map space to PageLayout space.
  1. Get the properties used for drawing from the contained IndexGrid.
  2. QI the MapFrame for its IElement interface and get its Geometry.
  3. Store the extent of the grid in the variable ipExtent. This extent must be in page units, as it will be used later for drawing the clipped grid to the PageLayout. The source of this extent depends on whether or not the clip geometry has been set.

    a) If the clip geometry is not specified, the extent is taken from the MapFrame's Geometry from step 2. This is already in page units.

    [VC++]
    ipFrameGeometry->get_Envelope(&ipExtent);

    b) If a clip geometry is specified, the extent is taken from this Geometry—this geometry is in map units (ipExtentMap) and must, therefore, be transformed to page units. This transformation has a number of steps and is explored in depth below.

    [VC++]
    IEnvelopePtr ipExtentMap;
    m_ipClipGeometry->get_Envelope(&ipExtentMap);
    • First, you will need to get the DisplayTransformation of both the Map and the PageLayout.
      [VC++]
      IMapPtr ipMap;
      pMapFrame->get_Map(&ipMap);
      IActiveViewPtr ipMapView(ipMap);
      IScreenDisplayPtr ipMapDisplay;
      ipMapView->get_ScreenDisplay(&ipMapDisplay);
      IDisplayTransformationPtr ipPageTrans, ipMapTrans;
      ipMapDisplay->get_DisplayTransformation(&ipMapTrans);
      pDisplay->get_DisplayTransformation(&ipPageTrans);
      m_ipClipGeometry->get_Envelope(&ipExtent);
    • Next, transform ipExtentMap to page units. The transformation is in two stages, from map to device units, then from device to page units.
      [VC++]
      tagRECT pageRect;
      ipMapTrans->TransformRect(ipExtent, &pageRect, esriTransformToDevice + esriTransformPosition);
      ipPageTrans->TransformRect(ipExtent, &pageRect, esriTransformToMap + esriTransformPosition);

      You can transform measurements from Map to PageLayout space by accessing the DisplayTransformation of the MapFrame and Display passed to Draw and GenerateGraphics.
      You can access the appropriate Display object by using the GetScreenDisplay property of the IActiveView interface of the IGraphicsContainer parameter.

    • Now that you have the extent Envelopes of the clip geometry in both map units and page units, you can create an AffineTransformation2D.
      [VC++]
      IAffineTransformation2DPtr ipAT2D(CLSID_AffineTransformation2D);
      ipAT2D->DefineFromEnvelopes(ipExtentMap, ipExtent);
    • Clone the clip geometry to preserve the shape in map units, then use the ipAT2D transformation you just created to transform it to page space.
      [VC++]
      IClonePtr ipClone(m_ipClipGeometry);
      IClonePtr ipNew;
      ipClone->Clone(&ipNew);
      ipClipGeometryPage = ipNew;
      ITransform2DPtr ipT2D(ipClipGeometryPage);
      ipT2D->Transform(esriTransformForward, (ITransformationPtr)ipAT2D);
    • The ipClipGeometryPage variable will hold this clip geometry in page units for later use in the DisplayGrid method.
  4. Next, calculate the details of the individual grid cells. From the extent in page units (ipExtent) calculated in step 3, calculate the grid origin and intervals—for example, the minimum, maximum, and intervals are calculated for the x-axis below.

    [VC++]
    double xmin,xmax;
    ipExtent->get_XMin(&xmin);
    ipExtent->get_XMax(&xmax);
    double xOrigin = xmin;
    double xInterval = (xmax - xmin) / (double)numColumns;

    Once you have calculated the shape and extent of the grid, you can work out the extent of each grid cell.

  5. Your clipped index grid must only draw the cells of the grid that overlap with the clip geometry.
    • Create a GeometryBag. You will use this to collect geometries representing the individual cells in the clipped index grid.

      [VC++]
      IGeometryCollectionPtr ipGeomCol(CLSID_GeometryBag);
    • Next, using the clip geometry and the grid cell information calculated in step 4, create a Polygon representing each cell in the map grid. Use the IRelationalOperator::Disjoint method to figure out which grid cells have a non-null intersection with the clip geometry.
      [VC++]
      for (int nRow = 0; nRow < numRows; nRow++) //Iterate Rows of cells
      {
        ...
        for (int nCol = 0; nCol < numColumns; ++nCol) //Iterate Columns
        {
          ...
          VARIANT_BOOL bDisjoint;
          ipRel->Disjoint((IGeometryPtr)ipPointCol, &bDisjoint);
          if ((bDisjoint == VARIANT_FALSE))
            ipGeomCol->AddGeometry((IGeometryPtr)ipPointCol);
    • Union these cells to get one Polygon, ipClippedCells, which determines the overall shape of the clipped grid.
      [VC++]
      IGeometryPtr ipClippedCells(CLSID_Polygon);
      ipTopoClippedCells = ipClippedCells;
      ipTopoClippedCells->ConstructUnion((IEnumGeometryPtr)ipGeomCol);
      ipTopoClippedCells->Simplify();
    • Remove any inner rings from the Polygon, as they are not relevant to the clippable index grid.
      [VC++]
      IGeometryCollectionPtr ipRingCol(ipClippedCells);
      for (long l = count - 1; l >= 0; --l)
      {
        ipRingCol->get_Geometry(l, &ipGeom);
        ipRing = ipGeom;
        ipRing->get_IsExterior(&bExterior);
        if (bExterior == VARIANT_FALSE)
        {
          ipRingCol->RemoveGeometries(l, 1);
        }
      }
    • Get the boundary Polyline (ipBoundary) of the Polygon representing the cells in the clipped grid.
      [VC++]
      IGeometryPtr ipBoundary;
      ipTopoClippedCells->get_Boundary(&ipBoundary);

DisplayGrid Part 2drawing the clipped grid

Now you can begin to actually draw the grid. The grid lines, border, and labels are drawn in turn; tick marks are not drawn as they are not appropriate on an IndexGrid.

Now that you have calculated the shape and size of the grid and its cells, you can begin to display the grid, starting with the grid lines.

  1. To draw the grid lines, first get the intersection of the lines and the clipped cells Polygon and pClipTopo, if present. Draw only the part of the grid lines that fall within the Polygon.
    [VC++]
    if (pClipTopo != NULL)
    {
      pClipTopo->Intersect((IGeometryPtr)m_ipPolyline, esriGeometry1Dimension, &ipClippedLine);
    }
    if (ipClippedLine == NULL)
      ipClippedLine = m_ipPolyline;
    ...
    pDisplay->DrawPolyline(ipClippedLine);
  2. Draw the grid border, ipBorder, by asking it to draw itself.
    [VC++]
    ipBorder->Draw(pDisplay, ipClippedCellsBoundary, 0);

    At this stage, you should also draw any tick marks if your grid requires them.

    After the grid lines, you should display the border and labels of the grid.

  3. Draw the labels.

    a) If the clip geometry is not specified, draw the labels in the conventional way.

    b) If the clip geometry is specified, then determine the location of the labels.

    • Using the boundary of the grid from step 5 (ipClippedCellsBoundary), iterate each of its Segments.
      [VC++]
      ISegmentCollectionPtr ipSegCol(ipClippedCellsBoundary);
      long lSegs;
      ipSegCol->get_SegmentCount(&lSegs);
      ...
      ISegmentPtr ipSeg;
      for (l = 0; l < lSegs; ++l)
      {
           ipSegCol->get_Segment(l, &ipSeg);
    • For each Segment, determine its orientation (horizontal or vertical), by examining the x and y coordinates of its end points.
      [VC++]
      IPointPtr fromPt, toPt;
      ipSeg->get_FromPoint(&fromPt);
      ipSeg->get_ToPoint(&toPt);
      double fx, fy, tx, ty;
      fromPt->QueryCoords(&fx, &fy);
      toPt->QueryCoords(&tx, &ty);
      if ( fabs(ty - fy) < 0.0001 )    //Y coordinates match = horizontal
    • Next, determine the label's position relative to the Segment. In this example, a test Point is created on top of a horizontal Segment.
      [VC++]
      testPt->PutCoords( ((tx + fx) / 2.0), ty + (yInterval / 2.0) );
      ipRelClippedCells->Contains((IGeometryPtr)testPt, &bContains);
    • If testPt is contained by the clip geometry, then the label is known to be at the bottom of the grid. If testPt is not contained by the clip geometry, you need to place the label above a horizontal Segment or to the right of a vertical Segment.

      Due to the irregular shape of the ClippableIndexGrid, you will need to work out the positioning of each grid label.

    • At this point you should also check the value of the label visibility properties (these are retrieved at the start of the DisplayGrid function). For example, as shown in the code below, only draw the top axis labels if the property indicates they should be visible.
      [VC++]
      if (bContains == VARIANT_FALSE && labelTopVis)
      {
        ...

      Check whether labels should be visible before drawing them by using the IMapGrid::QueryLabelVisibility method.

    • Calculate the correct label text based on the current row and column of the grid, then draw the label. Pass in to the Draw method of the GridLabel the leftmost point of the Segment (this will be the FromPoint of the Segment if it is above the grid, and the ToPoint if it is below the grid), and also the appropriate esriGridAxisEnum constant to indicate the label's relative position to the Point.
      [VC++]
      ipTabStyle->PrepareDraw(_bstr_t(label), xInterval, corner);
      ...
      ipLabelFormat->Draw(tx, toPt, esriGridAxisBottom, pDisplay);

      Calculate a map grid label's text from the current grid row and column numbers.

The example code shows one way of solving the problems of drawing a complex grid. There are, of course, a variety of different approaches to each of the logic issues encountered.

The Draw method

The Draw method is called by ArcMap when a page layout is refreshed.

For the ClippableIndexGrid, Draw simply calls the DisplayGrid function, passing in the references to IDisplay and IMapFrame it receives. In the discussion of the DisplayGrid function above, it is assumed that DisplayGrid is called from the Draw method.

The GenerateGraphics method

The GenerateGraphics method may be called to create a GroupElement representing the grid.

GenerateGraphics also calls DisplayGrid, but first creates a GroupElement to pass in; this signifies to DisplayGrid that it should create and add graphic elements to the GroupElement, rather than drawing the shapes directly to the Display.

In the GenerateGraphics method, you need to create a GroupElement containing other graphic elements representing the individual elements of a map grid.

  1. The GenerateGraphics method receives a GraphicsContainer parameter; use this to get the current Display.
    [VC++]
    IActiveViewPtr ipActiveView(pGraphicsContainer);
    IScreenDisplayPtr ipDisplay;
    if (ipActiveView)
      ipActiveView->get_ScreenDisplay(&ipDisplay);
  2. The Display will not be drawing at this point (as this method is called from the Convert To Graphics button, as discussed), therefore, you must call StartDrawing on this display to prepare the device for drawing.
    [VC++]
    OLE_HANDLE hDC;
    ipDisplay->get_hDC(&hDC);
    ipDisplay->StartDrawing(hDC, esriNoScreenCache);
  3. Create a GroupElement to contain all the graphic elements that will compose the grid, then call DisplayGrid, passing in this GroupElement.
    [VC++]
    IGroupElementPtr ipGroupElement(CLSID_GroupElement);
    HRESULT hr = DisplayGrid(ipDisplay, pMapFrame, ipGroupElement);
  4. Tidy up by calling FinishDrawing on the Display. Also, add the GroupElement (now full of the graphic elements that compose a ClippableIndexGrid) to the GraphicsContainer.
    [VC++]
    ipDisplay->FinishDrawing();
    pGraphicsContainer->AddElement(IElementPtr(ipGroupElement), 0);

    If your code calls StartDrawing, you must ensure you call FinishDrawing when you have finished drawing to a Display.

You can find the full details of how DisplayGrid creates graphic elements for the GenerateGraphics method in the accompanying example.

Implementing IIndexGrid

As ClippableIndexGrid is a type of IndexGrid, the IIndexGrid interface is implemented and most members are delegated directly to the contained IndexGrid coclass. IIndexGrid inherits from IMapGrid, which has been previously discussed.

If you are adapting this sample to create a different type of custom map grid, consider implementing IIndexGrid if your grid will divide the dataframe into equal sections and if part of your adaptation involves the specific members of IIndexGrid.

For example, you may want to perform spatial operations on the extent of standard grid cells, as demonstrated in this example using the QueryCellExtent method. You may also want to provide access for clients to set each column and row label themselves, via the XLabel and YLabel properties.

IIndexGrid provides access to properties, allowing users to set label text individually.

Below is a table describing the typical actions you should perform for each member of IndexGrid; this table contains only members that are not inherited from IMapGrid.

IIndexGrid members and descriptions
ColumnCountReturn or set the number of columns in the index grid.
QueryCellExtentReturn the cell extent in page space for the given row and column.
RowCountReturn or set the number of rows in the index grid.
XLabelAllow read-write access to an array of strings, which you should use as the labels for the columns of the index grid.
YLabelAllow read-write access to an array of strings, which you should use as the labels for the rows of the index grid.

Creating and Implementing IClippableIndexGrid

Your ClippableIndexGrid needs two more things: you must be able to uniquely identify this class from other grids when programming, and you must also provide a way to specify the clip geometry for the grid.

You can achieve both these goals by creating and implementing the IClippableIndexGrid interface.

The basic shape of the clippable index grid will be set via a new interface, IClippableIndexGrid.

The read-write IndexGrid property exposes the IndexGrid contained by your ClippableIndexGrid for convenience—its IClone interface can be used externally for operations such as cloning and checking for equality. In normal operation, the contained IndexGrid referenced by this member is set at the start of the ClippableIndexGrid's constructor.

The read-write ClipGeometry property simply holds the shape of the grid—the key to the ClippableIndexGrid's shape.

[VC++]
STDMETHODIMP CClippableIndexGrid::put_ClipGeometry(IGeometry *newVal)
{
  if (newVal == NULL)
  {
    m_ipClipGeometry = NULL;
    return S_OK;
  }
  IClonePtr ipNew(newVal);
  IClonePtr ipClone;
  ipNew->Clone(&ipClone);
  m_ipClipGeometry = ipNew;
  return S_OK;
}

This geometry, which is set in map units, is used as the base for the grid; only the cells of this base grid that overlap the geometry are included as part of the final grid.

The value of the ClipGeometry property will be set in two circumstances: when a ClippableIndexGrid is created by the NewClippableIndexGrid property page and when the ClipGeometry is reset by the ClippableIndexGrid property page. You will construct both these property pages in the 'Plugging your custom grid into ArcMap' section below.

The ClipGeometry property is the key to the functionality of the ClippableIndexGrid. This property will be set via the user interface using the property pages you will create later.

Implementing IGraphicsComposite

The presence of IGraphicsComposite indicates that a class is a composite of other graphic elements. It also allows clients to access those elements.

The IMapGrid::GenerateGraphics method also returns the grid in graphic element form. However, IGraphicsComposite is a generic interface, implemented by many ArcObjects coclasses, allowing clients to work at this generic level.

As the client should not be able to change the composite parts of the grid, IGraphicsComposite only needs to return a copy of the elements that compose the grid. This is achieved by the Graphics property, which returns an element enumerator—a class that implements IEnumElement. Neither of the standard classes that implement IEnumElement can be used in this case, as you cannot add elements to these classes. Therefore, you will need to create your own enumerator class—see the 'Creating an element enumerator' section below.

IGraphicsComposite is a generic interface, which returns an enumeration of graphic elements. None of the existing element enumerators are suitable for this job, so you will create a new enumerator coclass.

As the IGraphicsComposite interface is designed to be generic, the second parameter passed to the Graphics property is an IUnknown reference, pData; the expected coclass of this parameter will vary according to the implementing class. A map grid class would expect pData to be a reference to the Map with which the grid is associated.

[VC++]
IMapFramePtr ipMapFrame = pData;
if (ipMapFrame == NULL)
  return E_INVALIDARG;

The Graphics method also receives an IDisplay reference, pDisplay, indicating the Display for which the graphics should be created. Call the StartDrawing method of the Display to prepare it for drawing.

[VC++]
if (FAILED(hr = pDisplay->StartDrawing(0, esriNoScreenCache)))
 return hr;

You can make use of the DisplayGrid function again to create the actual graphic elements. Create a GroupElement, then call the DisplayGrid function, passing in a reference to this GroupElement. This signifies to DisplayGrid that it should create and add graphic elements to the GroupElement instead of drawing to the display.

[VC++]
IGroupElementPtr ipGroupElement(CLSID_GroupElement);
if (FAILED(hr = DisplayGrid(pDisplay, ipMapFrame, ipGroupElement)))
  return hr;

After the function returns, finish drawing on the display.

[VC++]
pDisplay->FinishDrawing();

Finish by creating an EnumElement and adding the GroupElement to the enumerator via the IEnumElementAdmin interface.

[VC++]
IEnumElementAdminPtr ipEnumElementAdmin;
if (FAILED(hr = ipEnumElementAdmin.CreateInstance(CLSID_EnumElement)))
  return hr;
IElementPtr ipElem = ipGroupElement;
if (FAILED(hr = ipEnumElementAdmin->Add(ipElem)))
  return hr;

Return the EnumElement from the Graphics property.

Use the DisplayGrid function to fill a GroupElement with graphic elements representing the map grid.
Then add each individual element from the GroupElement to an EnumElement to return the Graphics property element enumeration.

At ArcGIS 9, ArcMap does not call IGraphicsComposite::GetGraphics, but you should implement it to ensure correct operation of your map grid with future or alternative clients.

Creating an element enumerator

As you cannot add specific elements to the existing element enumerators, ElementSelection and SimpleElementSelection, you should create a new enumerator class, named EnumElements, to return an enumeration from the IGraphicsComposite::Graphics method.

Also, create an interface called IEnumElementAdmin with a single method called Add that takes an IElement parameter. Implementing this interface on EnumElement will allow you to add elements to your element enumerator.

Creating the EnumElement class and IEnumElementAdmin interface help you to implement IGraphicsComposite.

To store the elements in the enumerator, declare a member variable as an Array; add another member variable to store the current array position of the enumerator.

[VC++]
IArrayPtr m_pElements;
long     m_lPosition;

In the IEnumElementAdmin::Add method, add a reference to pElement to the last position of the array.

[VC++]
long lCount;
m_pElements->get_Count(&lCount);
 
IUnknownPtr ipUnk = pElement;
return m_pElements->Insert(lCount, ipUnk);

Finish the EnumElement class by implementing IEnumElement, as shown in the accompanying source code. More information on creating enumerators can be found in Chapter 2, 'Developing Objects'.

Implementing IClone, IPersist, and IPersistStream

Cloning and persistence are essential functions for plugging any map grid into the ArcGIS system. For example, each time a map grid's property sheet is displayed, the map grid will be cloned. Persistence is essential to allow your grid to be saved to and loaded from a map document.

The ClippableIndexGrid example provides a standard implementation of the IClone, IPersist, and IPersistStream interfaces.

A map grid must implement the standard cloning and persistence interfaces.

In the implementation of IPersist, the clip geometry, m_ipClipGeometry, and contained IndexGrid, m_ipIndexGrid, are persisted to the stream's ObjectStream. The vector arrays of label strings, m_xLabels and m_yLabels, are persisted as individual strings by first saving the number of string elements.

See Chapter 2, 'Developing Objects', for more information on cloning and persistence.


Implementing other kinds of custom grids

If you are designing a different kind of map grid, you may also want to implement the IProjectedGrid, IMeasuredGrid, or ICustomOverlayGrid interfaces depending on the design of the grid.

IMeasuredGrid

Consider implementing this interface if your grid is designed to follow a coordinate system. Measured grids have an origin, and grid lines are drawn at fixed distance intervals.

IMeasuredGrid members and descriptions
FixedOriginReturn or set a value indicating if the grid should take its origin from the XOrigin and YOrigin properties (true) or if it is computed dynamically from the data frame (false).
UnitsReturn or set a constant indicating the units for the intervals and origin.
XIntervalSizeReturn or set the interval between grid lines along the x axis.
XOriginReturn or set the origin of the grid on the x axis.
YIntervalSizeReturn or set the interval between grid lines along the y axis.
YOriginReturn or set the origin of the grid on the y axis.

IProjectedGrid

Consider implementing the IProjectedGrid interface if you will be exposing a spatial reference for your grid. This interface has a single member, SpatialReference, indicating the coordinate system of the grid. This member should be coded to allow an ISpatialReference object to be read or written by reference.

ICustomOverlayGrid

You may want to implement this interface if your grid will be based on the Features of an existing FeatureClass, and your grid label text is stored as attributes of those Features.

ICustomOverlayGrid members and descriptions
IDataSourceReturn or set an IFeatureClass reference, indicating the data source of the grid lines.
LabelFieldReturn or set a string indicating the name of the Field in the data source that should be used to label the map grid.

Plugging your custom grid into ArcMap

Now that you have created your custom map grid coclass, the next step is to enable a user to create a new ClippableIndexGrid within ArcMap and edit the grid's properties. In ArcMap, a user may create a new instance of an existing grid in one of three ways.

First, a user may create a grid by opening the Data Frame properties dialog box, clicking the Grids property page, and clicking New Grid. This has one of two actions.

If the ArcMap 'Use wizards if available' option is selected, the Grids and Graticules wizard is displayed. This allows the user to select the type and properties of the new grid. However, this action cannot be extended to work with your custom grid, as the wizard is hard coded.

If the 'Use wizards if available' option is not selected, the Reference System Selector dialog box is displayed instead, allowing you to select a predefined grid from those stored in the StyleGalleries and to edit the details of a grid by clicking Properties.

If you have previously stored a ClippableIndexGrid StyleItem in a referenced StyleGallery, then you will be able to select this grid and alter its properties. However, the dialog box does not allow you to create a new ClippableIndexGrid from scratch.

If you do want to provide a way to create a new ClippableIndexGrid from the Grid's property page, see the section Creating the NewClippableGridPage.

Alternatively, a user can create a grid, either based on an existing grid in a StyleGallery or from scratch, by using the Style Manager dialog box.

To open the Style Manager in ArcMap, click Tools, Styles, then Style Manager. To create a new grid, click the Reference Systems folder. Then, to create a grid based on an existing StyleItem, click an existing grid. Alternatively, to create a new grid from scratch based on a grid type, right-click the left-hand pane, and click New from the context menu.

This list of options for a new grid is taken from the MapGridFactory classes currently registered to the ESRI Map Grid Factories component category.

So, to allow user access to create a new ClippableIndexGrid, you will now create an accompanying grid factory object class.


Creating a ClippableIndexGridFactory

By reviewing the ArcMap object model diagram, you can see that the existing map grid factories inherit from the abstract MapGridFactory abstract class and implement only one interfaceIMapGridFactory.

To solve the requirements of this example, you will create a class that is a subtype of MapGridFactory called ClippableIndexGridFactory.

Once the ClippableIndexGridFactory is registered to the ESRI Map Grid Factories component category, a user will be able to create a new ClippableIndexGrid from the Style Manager dialog box.


Create a map grid factory to allow users to create new ClippableIndexGrids in the Style Manager.

Implementing IMapGridFactory

IMapGridFactory has one property and one method. The read-only Name property should return the name of the type of grid the factory creates. In this example it returns "Clippable Index Grid".

Once your custom map grid is built and registered, you will see this name on the context menu when you attempt to create a new grid in the Style Manager dialog box.

In the Create method, you should create a new instance of the ClippableIndexGrid coclass and call the IMapGrid::SetDefaults method to set the default properties of the MapGrid. Then return this new grid to the caller.

[VC++]
STDMETHODIMP CClippableIndexGridFactory::Create(IMapFrame *MapFrame, IMapGrid **MapGrid)
{
  if (!MapGrid)
    return E_POINTER;
  *MapGrid = NULL;
  
  IMapGridPtr ipGrid(CLSID_ClippableIndexGrid);
  ipGrid->SetDefaults(MapFrame);
  *MapGrid = ipGrid;
  (*MapGrid)->AddRef();
  
  return S_OK;
}

Create will be called when the user selects ClippableIndexGrid from the new grid context menu in the Style Manager dialog box.

The ClippableIndexGridFactory creates a new ClippableIndexGrid in its IMapGridFactory::Create method.


Creating a property page for the ClippableIndexGrid

When you attempt to edit the properties of any map grid, the Reference System dialog box appears. When this dialog box is displayed, it will interrogate all the property pages currently registered to the ESRI Map Grids Property Pages component category and will display all those pages that apply to the type of map grid being edited.

As your custom map grid class implements IMapGrid, the dialog box will contain the existing Axes, Labels, and Lines property pages (IPropertyPageContext::Applies for these pages will return True if passed any class that implements IMapGrid.)

The ClippableIndexGrid also implements IIndexGrid; therefore, the Index property page will also be displayed.

At this point, a user will be able to change all the properties of a ClippableIndexGrid, except IClippableIndexGrid::ClipGeometrythe one property that is not available via the existing property pages.

Creating the ClippableGridPage

Add to your project a simple property page to allow users to set the clip geometry of a ClippableIndexGrid. Add to the dialog box a button called Use Selected Data Graphic, allowing the user to set the value of the clip geometry equal to the geometry of the currently selected graphic element.


Once you have registered this property page to the ESRI Map Grids Property Pages component category, it will appear in the Reference System dialog box when the user has selected a ClippableIndexGrid.

Implementing IPropertyPage for the ClippableGridPage

ClippableGridPage is a standard implementation of a property page. See 'Creating Property Pages' in Chapter 2 for more information on implementing a property page.

In the Applies method, iterate through the objects referenced by the SafeArray parameter and return True if you find an object that implements IIndexGrid and IClippableIndexGrid.

[VC++]
*Applies = VARIANT_FALSE;
long lNumElements = saArray->rgsabound->cElements;
for (long i = 0; i < lNumElements; i++)
{
  IClippableIndexGridPtr ipInd(pUnk[i]);
  if (ipInd != 0)
  {
    *Applies = VARIANT_TRUE;
    m_ipGrid = ipInd;
    break;
  }
}

In the SetObjects method, check the array of objects passed in. You should receive a Map and ClippableIndexGrid, which should be stored as member variables.

[VC++]
STDMETHODIMP CClippableGridPage::SetObjects(ULONG nObjects, IUnknown *ppUnk)
{
  for (ULONG i=0; i < nObjects; i ++)
  {
    IMapPtr ipMap(ppUnk[i]);
    if (ipMap != 0)
      m_ipMap = ipMap;
    IClippableIndexGridPtr ipGrid(ppUnk[i]);
    if (ipGrid != NULL)
      m_ipGrid = ipGrid;
  }
  ...

IPropertyPage::SetObjects should receive a reference to a Map and a reference to a ClippableIndexGrid.

In response to the user clicking the Use Selected Data Graphic button on the property page, retrieve the graphic element that is currently selected on the Map you received in SetObjects.

[VC++]
IViewManagerPtr ipViewManager(m_ipMap);
ISelectionPtr ipSelection;
ipViewManager->get_ElementSelection(&ipSelection);
IEnumElementPtr ipEnumElement(ipSelection);
ipEnumElement->Reset();
IElementPtr ipElement;
ipEnumElement->Next(&ipElement);

The Use Selected Data Graphic button allows the user to set the shape of the ClippableIndexGrid.

As the clip geometry must be a Polygon, check the type of this graphic element. Then set the ClipGeometry property of the ClippableIndexGrid you received in SetObjects to the Geometry of the graphic element.

[VC++]
...
IGeometryPtr ipGeometry;
ipElement->get_Geometry(&ipGeometry);
esriGeometryType type;
ipGeometry->get_GeometryType(&type);
if (type != esriGeometryPolygon)
{
  :MessageBoxW(0, L"Clip geometry was not a polygon.", L"ClippableGrid", MB_OK);
    return 0;
}
m_ipClipGeometry = ipGeometry;

Set the IClippableIndexGrid::ClipGeometry property from the ElementSelection of the Map.


A User Interface for creating new custom map grids

Previously, you saw you cannot create a new custom map grid from the Data Frame Properties dialog box via the Grids and Graticules wizard.

The Grids and Graticules wizard is not extensible—you cannot add your custom grid to this wizard.

You can still allow the creation of a new custom map grid from the Data Frame Properties dialog box by adding a new property page that has this functionality to the dialog box.

Although this is a slightly nonstandard way to extend the framework, this technique does show you the flexibility of property pages used in conjunction with component categories.

Creating the NewClippableGridPage

You will create a simple property page, NewClippableGridPage, to allow users to add a new ClippableIndexGrid to a Map. This property page will appear in the Data Frame Properties dialog box, as you will register it to the ESRI Map Property Pages component category.


The NewClippableGridPage property page is shown here—it is a standard implementation of a property page (again, see the 'Creating Property Pages' section in Chapter 2).

The check box at the top is unchecked by default. When checked, it enables the remainder of the dialog box's controls.

You can set a name for the grid and change the number of columns and rows in the grid. These changes are stored as simple member variables while the page is displayed.

There is also a dropdown list box that allows you to choose from a number of options for the tab style of the labels for the grid; these are hard-coded in this example, but could be identified at run time from the Grid Labels component category. This selection is also stored as a member variable.

Last, there is also a button that allows you to set the clip geometry of the ClippableIndexGrid to equal the currently selected graphic. The code behind this button is similar to that shown for the ClippableGridPage previously—the geometry of the graphic is stored as a member variable.

[VC++]
IViewManagerPtr ipViewManager(m_ipMap);
ISelectionPtr ipSelection;
ipViewManager->get_ElementSelection(&ipSelection);
IEnumElementPtr ipEnumElement(ipSelection);
ipEnumElement->Reset();
IElementPtr ipElement;
ipEnumElement->Next(&ipElement);
IGeometryPtr ipGeometry;
ipElement->get_Geometry(&ipGeometry);
m_ipGeometry = ipGeometry;

Implementing IPropertyPage for the NewClippableIndexGrid

In the Applies method, instead of iterating through the array passed in and checking for a particular type of object, simply return True. You want the NewClippableGridPage to always appear in the Data Frame Properties dialog box, regardless of the properties.

In the SetObjects method, check the array of objects passed in—you should receive a reference to a Map. Store this reference as a member variable; you will add your grid to this Map later in the Apply method.

[VC++]
for (ULONG i=0; i < nObjects; i ++)
{
  IMapPtr ipMap(ppUnk[i]);
  if (ipMap != 0)
    m_ipMap = ipMap;
}

IPropertyPage::SetObjects should receive a reference to a Map.

The majority of the work done by the NewClippableGridPage is in the Apply method. First, instantiate a new ClippableIndexGrid.

[VC++]
IClippableIndexGridPtr ipClippedGrid(CLSID_ClippableIndexGrid);
IIndexGridPtr ipGrid(ipClippedGrid);

Next, set the values of its Name, Rows, and Columns properties from the member variables you stored previously.

[VC++]
TCHAR sText[100];
::GetWindowText(m_hEdtName, sText, 100);
_bstr_t bsName = sText;
ipGrid->put_Name(bsName);
...

Then, set the IIndexGrid::TabStyle property by instantiating the correct type of tab style class based on the style selected by the user.

[VC++]
::GetWindowText(m_hCboTabType, sText, 100);
_bstr_t bsTabStyle = sText;
IIndexGridTabStylePtr ipTabStyle;
if (bsTabStyle == _bstr_t(L"Button Tabs"))
{
  ipTabStyle.CreateInstance(CLSID_ButtonTabStyle);
}
else if (bsTabStyle == _bstr_t(L"Filled Background"))
{
  ...

Apply default values for the color and thickness of the tab, then set the IIndexGrid::TabStyle property of the ClippableIndexGrid.

[VC++]
IRgbColorPtr color(CLSID_RgbColor);
color->put_Red(255);
color->put_Blue(190);
color->put_Green(190);
ipTabStyle->put_ForegroundColor((IColorPtr)color);
color->put_Blue(110);
color->put_Green(110);
color->put_Red(110);
ipTabStyle->put_OutlineColor((IColorPtr)color);
[VC++]
ipTabStyle->put_Thickness(20.0);
ipGrid->put_LabelFormat((IGridLabelPtr)ipTabStyle);

In the IPropertyPage::Apply method, create the new ClippableIndexGrid and set its properties according to the selections made by the user on the NewClippableIndexGrid property page.

Don't forget to set the IClippableIndexGrid::ClipGeometry property.

[VC++]
ipClippedGrid->put_ClipGeometry(m_ipGeometry);

Now you need to add the ClippableIndexGrid to the Map. Start by getting the GraphicsContainer of the PageLayout, and from this find the FrameElement of the Map.

[VC++]
IApplicationPtr ipApp(CLSID_AppRef);
IDocumentPtr ipDoc;
ipApp->get_Document(&ipDoc);
IMxDocumentPtr ipMxDoc(ipDoc);
IPageLayoutPtr ipPageLayout;
ipMxDoc->get_PageLayout(&ipPageLayout);
IGraphicsContainerPtr ipGC(ipPageLayout);
IFrameElementPtr ipFrame;
ipGC->FindFrame(_variant_t((IUnknown*)m_ipMap), &ipFrame);

The Apply method should also add the new ClippableIndexGrid to the MapFrame.

Note that the code here assumes it is running inside the ArcMap process and uses the AppRef object. If there is a chance that your property page may be used outside ArcMap, using AppRef may cause errors. You may want to refer to Chapter 2, 'Developing Objects', for information on a technique to avoid the instantiation of AppRef outside the ArcGIS applications.

Using AppRef may cause errors if your code finds itself running in a process outside ArcMap.

Finally, add the ClippableIndexGrid and refresh the view to show your new grid.

[VC++]
IMapGridsPtr ipMapGrids(ipFrame);
ipMapGrids->AddMapGrid((IMapGridPtr)ipGrid);
IActiveViewPtr ipAV(ipPageLayout);
ipAV->PartialRefresh(esriViewBackground, NULL, NULL);

CreateCompatibleObject and QueryObject are not applicable methods in this context, as the grid property pages are mutually exclusive—so return E_NOTIMPL.

Once compiled and registered, your clippable index grid is ready for use.


Back to top

Go to example code

See Also About Map Grids and Creating Cartography.