In this section:
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
Maps are often presented in a rectangular formatfor example, the pages of a road atlas.
However, geographical featurescities, administrative regions, counties, countries, and riversare 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 shapebut 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 optionyou 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.
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 framethe 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.
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 behaviorthose 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 | |
|---|---|
| Border | Return or set an IMapGridBorder reference, storing the map grid border. |
| Draw | You 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. |
| ExteriorWidth | Return the width (in display units) of the portion of the grid that is outside the frame. |
| GenerateGraphics | Generate 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. |
| LabelFormat | Return or set an IGridLabel reference storing the label format for the map grid labels. |
| LineSymbol | Return 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. |
| Name | Return or set a string value indicating the name of the current map grid. |
| PrepareForOutput | Perform 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. |
| QueryLabelVisibility | Return values indicating the visibility of the labels along all four sides of the map grid. |
| QuerySubTickVisibility | Return values indicating the visibility of the subticks along all four sides of the map grid. |
| QueryTickVisibility | Return values indicating the visibility of the ticks along all four sides of the map grid. |
| SetDefaults | Reset all the member variables storing properties of the map grid to their default values. |
| SetLabelVisibility | Set values indicating the visibility of the labels along all four sides of the map grid. |
| SetSubTickVisibility | Set values indicating the visibility of the subticks along all four sides of the map grid. |
| SetTickVisibility | Set values indicating the visibility of the ticks along all four sides of the map grid. |
| SubTickCount | Return or set an integer indicating the number of subticks to draw between the major ticks. |
| SubTickLength | Return or set a double indicating the length of the subticks in points. |
| SubTickLineSymbol | Return or set an ILineSymbol reference storing the LineSymbol used to draw the subtick lines. |
| TickLength | Return or set a double indicating the length of the major ticks in points. |
| TickLineSymbol | Return or set an ILineSymbol reference storing the LineSymbol used to draw the major ticks. |
| TickMarkSymbol | Return 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. |
| Visible | Return 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.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.
ipFrameGeometry->get_Envelope(&ipExtent);
b) If a clip geometry is specified, the extent is taken from this Geometrythis 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.
IEnvelopePtr ipExtentMap; m_ipClipGeometry->get_Envelope(&ipExtentMap);
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);
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.
IAffineTransformation2DPtr ipAT2D(CLSID_AffineTransformation2D); ipAT2D->DefineFromEnvelopes(ipExtentMap, ipExtent);
IClonePtr ipClone(m_ipClipGeometry); IClonePtr ipNew; ipClone->Clone(&ipNew); ipClipGeometryPage = ipNew; ITransform2DPtr ipT2D(ipClipGeometryPage); ipT2D->Transform(esriTransformForward, (ITransformationPtr)ipAT2D);
doublexmin,xmax; ipExtent->get_XMin(&xmin); ipExtent->get_XMax(&xmax);doublexOrigin = xmin;doublexInterval = (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.
Create a GeometryBag. You will use this to collect geometries representing the individual cells in the clipped index grid.
IGeometryCollectionPtr ipGeomCol(CLSID_GeometryBag);
for(intnRow = 0; nRow < numRows; nRow++)//Iterate Rows of cells{ ...for(intnCol = 0; nCol < numColumns; ++nCol)//Iterate Columns{ ... VARIANT_BOOL bDisjoint; ipRel->Disjoint((IGeometryPtr)ipPointCol, &bDisjoint);if((bDisjoint == VARIANT_FALSE)) ipGeomCol->AddGeometry((IGeometryPtr)ipPointCol);
IGeometryPtr ipClippedCells(CLSID_Polygon); ipTopoClippedCells = ipClippedCells; ipTopoClippedCells->ConstructUnion((IEnumGeometryPtr)ipGeomCol); ipTopoClippedCells->Simplify();
IGeometryCollectionPtr ipRingCol(ipClippedCells);for(longl = count - 1; l >= 0; --l) { ipRingCol->get_Geometry(l, &ipGeom); ipRing = ipGeom; ipRing->get_IsExterior(&bExterior);if(bExterior == VARIANT_FALSE) { ipRingCol->RemoveGeometries(l, 1); } }
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.
if(pClipTopo != NULL) { pClipTopo->Intersect((IGeometryPtr)m_ipPolyline, esriGeometry1Dimension, &ipClippedLine); }if(ipClippedLine == NULL) ipClippedLine = m_ipPolyline; ... pDisplay->DrawPolyline(ipClippedLine);
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.
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.
ISegmentCollectionPtr ipSegCol(ipClippedCellsBoundary);longlSegs; ipSegCol->get_SegmentCount(&lSegs); ... ISegmentPtr ipSeg;for(l = 0; l < lSegs; ++l) { ipSegCol->get_Segment(l, &ipSeg);
IPointPtr fromPt, toPt; ipSeg->get_FromPoint(&fromPt); ipSeg->get_ToPoint(&toPt);doublefx, fy, tx, ty; fromPt->QueryCoords(&fx, &fy); toPt->QueryCoords(&tx, &ty);if( fabs(ty - fy) < 0.0001 )//Y coordinates match = horizontal
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.
if (bContains == VARIANT_FALSE && labelTopVis)
{
...Check whether labels should be visible before drawing them by using the IMapGrid::QueryLabelVisibility method.
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.
IActiveViewPtr ipActiveView(pGraphicsContainer);
IScreenDisplayPtr ipDisplay;
if (ipActiveView)
ipActiveView->get_ScreenDisplay(&ipDisplay);OLE_HANDLE hDC; ipDisplay->get_hDC(&hDC); ipDisplay->StartDrawing(hDC, esriNoScreenCache);
IGroupElementPtr ipGroupElement(CLSID_GroupElement); HRESULT hr = DisplayGrid(ipDisplay, pMapFrame, ipGroupElement);
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.
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 | |
|---|---|
| ColumnCount | Return or set the number of columns in the index grid. |
| QueryCellExtent | Return the cell extent in page space for the given row and column. |
| RowCount | Return or set the number of rows in the index grid. |
| XLabel | Allow read-write access to an array of strings, which you should use as the labels for the columns of the index grid. |
| YLabel | Allow read-write access to an array of strings, which you should use as the labels for the rows of the index grid. |
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 convenienceits 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 gridthe key to the ClippableIndexGrid's shape.
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.
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 enumeratora 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 classsee 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.
IMapFramePtr ipMapFrame = pData;if(ipMapFrame == NULL)returnE_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.
if(FAILED(hr = pDisplay->StartDrawing(0, esriNoScreenCache)))returnhr;
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.
IGroupElementPtr ipGroupElement(CLSID_GroupElement);if(FAILED(hr = DisplayGrid(pDisplay, ipMapFrame, ipGroupElement)))returnhr;
After the function returns, finish drawing on the display.
pDisplay->FinishDrawing();
Finish by creating an EnumElement and adding the GroupElement to the enumerator via the IEnumElementAdmin interface.
IEnumElementAdminPtr ipEnumElementAdmin;if(FAILED(hr = ipEnumElementAdmin.CreateInstance(CLSID_EnumElement)))returnhr; IElementPtr ipElem = ipGroupElement;if(FAILED(hr = ipEnumElementAdmin->Add(ipElem)))returnhr;
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.
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.
IArrayPtr m_pElements;
long m_lPosition;In the IEnumElementAdmin::Add method, add a reference to pElement to the last position of the array.
longlCount; m_pElements->get_Count(&lCount); IUnknownPtr ipUnk = pElement;returnm_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'.
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.
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.
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 | |
|---|---|
| FixedOrigin | Return 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). |
| Units | Return or set a constant indicating the units for the intervals and origin. |
| XIntervalSize | Return or set the interval between grid lines along the x axis. |
| XOrigin | Return or set the origin of the grid on the x axis. |
| YIntervalSize | Return or set the interval between grid lines along the y axis. |
| YOrigin | Return or set the origin of the grid on the y axis. |
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.
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 | |
|---|---|
| IDataSource | Return or set an IFeatureClass reference, indicating the data source of the grid lines. |
| LabelField | Return or set a string indicating the name of the Field in the data source that should be used to label the map grid. |
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.
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.
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.
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.
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.
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.
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.
*Applies = VARIANT_FALSE;longlNumElements = saArray->rgsabound->cElements;for(longi = 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.
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.
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.
... 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);return0; } m_ipClipGeometry = ipGeometry;
Set the IClippableIndexGrid::ClipGeometry property from the ElementSelection of the Map.
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 extensibleyou 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.
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 hereit 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 previouslythe geometry of the graphic is stored as a member variable.
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;
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 inyou 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.
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.
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.
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.
::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.
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);
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.
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.
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.
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 exclusiveso return E_NOTIMPL.
Once compiled and registered, your clippable index grid is ready for use.
Go to example code
See Also About Map Grids and Creating Cartography.