Friday, August 12, 2011

Connect to ArcMap layers and tables with VBA

Example description:
I am editing a coverage of stream segments (lines) and would like to log any updates to a non-spatial, standalone table in the geodatabase so I can keep track of the evolution of the dataset over time.  The application will first need to know which datasets to edit, so I start by adding  two combo boxes to the form that will be automatically populated with the two necessary types of data, named cboFlowingWatersLayer, and cboFlowingWatersUpdateTable, respectively.


The first combo box will allow me to select any of the the spatial datasets (geodatabase coverages/feature classes/shapefiles/projected datasets/etc.) that are available in the Table of Contents pane:

Map layers in an active ArcMap project

The second combo box will select the non-spatial, standalone table (highlighted below) that is otherwise available under the Source tab in the Table of Contents pane.  There are two additional dummy tables that I added to this project that are not visible in the screenshot below, but you'll see them later.

Non-spatial tables in an ArcMap project

Coding:
I add some code to populate the combo boxes with any available layers or standalone tables when the form activates.  There's a brief Setup section, then an If/Then statement will check to see if there is a layer already set in there.  If not, it'll clear it and populate the control with any spatial layers that are available.  If there is anything in there, it will run a separate function to check if it's the right layer.  I won't address that procedure here, so it's commented out below (though I recommend adding such a function). Let's get started with filling in the names of just the spatial datasets first:

Private Sub UserForm_Activeate()
'Setup
  Dim pMxDoc As IMxDocument
  Dim pMap As IMap

  Set pMxDoc = ThisDocument
 
Set pMap = pMxDoc.FocusMap

'Populate the first combo box with any available spatial
' layers that will show up in the Display tab in the
' ArcMap Table of Contents pane
  Dim i As Integer
  If cboFlowingWatersLayer.ListCount = 0 Then
      cboFlowingWatersLayer.Clear
      For i = 0 To pMap.LayerCount - 1
          Me.cboFlowingWatersLayer.Additem (pMap.Layer(i).Name)
      Next i
  Else
      'Call Me.FlowingWatersLayerCheck
  End If

End Sub

What should appear when the first combo box is selected are any layers that are available the Display tab of an ArcMap project.


(Note that apps can only work with shapefiles, geodatabase coverages, etc. and not groups of data.  Data groups appear in this example as individual layers, but since they are not really spatial datasets, selecting them will cause an error at run time if they are selected and processed.)

Next we'll add some more code to add any standalone tables that are in the project using the IStandaloneTableCollection interface:

Private Sub UserForm_Activeate()
'Setup
  Dim pMxDoc As IMxDocument
  Dim pMap As IMap
  Dim pSATCollection As IStandaloneTableCollection 

  Set pMxDoc = ThisDocument
 
Set pMap = pMxDoc.FocusMap
 
Set pSATCollection = pMxDoc.FocusMap

'Populate the first combo box with any available spatial
' layers that will show up in the Display tab in the
' ArcMap Table of Contents pane
  Dim i As Integer
  If cboFlowingWatersLayer.ListCount = 0 Then
      cboFlowingWatersLayer.Clear
      For i = 0 To pMap.LayerCount - 1
          Me.cboFlowingWatersLayer.Additem (pMap.Layer(i).Name)
      Next i
  Else
      'Call Me.FlowingWatersLayerCheck
  End If

'Populate the second combo box with any available non-spatial
' tables that will show up in the Source tab in the
' ArcMap Table of Contents pane
  Dim j As Integer
  If cboFlowingWatersUpdateTable.ListCount = 0 Then
      cboFlowingWatersUpdateTable.Clear
      For j = 0 To pSATCollection.StandaloneTableCount - 1
          Me.cboFlowingWatersUpdateTable.Additem _
              (pSATCollection.StandaloneTable(j).Name)
      Next j
  Else
      'Call Me.FlowingWatersTableCheck
  End If

End Sub

Finally, this is what the second combo box will look like (with the additional two dummy datasets that were hidden from view in the screenshot above - they're named "Delete - Test 1" and "Delete - Test 2"):


Referencing the selected layer/table:
There are a virtually unlimited number of uses for mapping layers like this.  The idea is that the method above provides a heads-up interface to access the position of a given layer or table in the Table of Contents.  Think of the position as the dataset's number in line, starting at 0 instead of 1.  Take the five spatial "layers" that are available for example:

Place in lineVB Position #Layer Name
1st / Top
2nd
3rd
4th
5th / Bottom
0
1
2
3
4
"FlowingWaters layer"
"New Group Layer"
"Florida NHD"
"SWFWMD_draft_primary_canals"
"Reporting Units"

Whenever you need to use a specific layer, the interface we built above will allow a quick way for Visual Basic to reference the position number of a selected layer.  Of course you will need some error checking and handling code to ensure that the layer position isn't changed (adding, removing, or moving the order of layers in the Table of Contents will affect each layer's position number).

The following are a few examples on how to reference a layer's position number using the interface that we built above.

Example 1 - Connect to a dataset:
A working example follows, so don't worry about the function of the code yet.  This simply illustrates the structure of references needed to connect to a dataset.

The previous sections explain that an ArcMap project is made of up layers and sometimes non-spatial tables.  Further, those layers and tables have position numbers associated with them that are usually just unimportant background information.  Well, now we will use those position numbers to tell a program where to target its procedures.

After we choose a layer from the program interface, that combo box/pull down menu will store our selection as a number, which it refers to as its ListIndex.  Now if we want to know which layer or table we chose, we'll call it by referencing cboFlowingWatersLayer.ListIndex.

In the example below, we are instantiating a new variable based off of the IFeatureLayer interface (again, don't worry about what's happening yet).  We will set that new variable, named pFeatureLayer, equal to a specific layer number (position number) in an ArcMap project so we can do some more work on it later.  Just pay attention to the way that the combo box position is referenced:

Dim pMxDoc As IMxDocument 'Whatever
Dim pFeatureLayer As IFeatureClass 'Slightly Important
'...more code

Set pMxDoc = ThisDocument 'Still not the point
'Oh, here we go!
Set pFeatureLayer = _
  pMxDoc.FocusMap.Layer(cboFlowingWatersLayer.ListIndex)
'...more code


Ok! This essentially told the function/sub routine that:
  1. We're working with this project (aka ThisDocument)
  2. We're looking for a specific feature layer (spatial dataset/shapefile/geodatabase coverage/etc.)
  3. That dataset of interest will be in a certain position when you focus the map; and that position number can be found by A) looking at the combo box (pull down menu named cboFlowingWatersLayer) and B) pulling its current ListIndex value.
Got it.  Let's move on to a working example.

Example 2 - Count the selected spatial features:
Add two command buttons to the form, named cboLayerCount & cboTableCount, and change their captions to match the screenshot below

Added two command buttons

When a user clicks on one of these buttons, a message box will report how many records are selected in a layer or a table.  I'll add a quick error check at the beginning to make sure that a layer has been selected.  If a layer has not been selected yet, the ListIndex value will be -1. Here's the code:


Private Sub cmdLayerCount_Click()
'Error Checking
  If cboFlowingWatersLayer.ListIndex = -1 Then
      MsgBox "Please choose a layer to count."
      Exit Sub
  End If

'Setup
  Dim pMxDoc As IMxDocument
  Dim pMap As IMap
  Dim pFS As IFeatureSelection
  Dim pSelectedFeatures As ISelectionSet

  Set pMxDoc = ThisDocument
  Set pMap = pMxDoc.FocusMap
  Set pFS = pMap.Layer(cboFlowingWatersLayer.ListIndex)
  Set pSelectedFeatures = pFS.SelectionSet 

'Display a message box to report info
  If pSelectedFeatures.Count = 1 Then
    MsgBox "There is 1 feature selected."
  Else
    MsgBox "There are " & pSelectedFeatures.Count & _
        " features selected."
  End If

End Sub

Save this, run the code, select a few features and you should get something like this:


Example 3 - Count the selected standalone table features
Using the same GUI (graphic user interface) that was built in the previous example, apply the following code to the "Table Count" command button to count the number of selected records in a standalone table.  A very similar method will be used for this procedure, however we will need to use the ITableSelection inteface in place of the IFeatureSelection interface since we're working with a standalone table instead of a spatial dataset.  Further, I won't need to sort through any of the selected features in the standalone table in my larger project, so I'll leave out the Selection Set and count directly from my Table Selection:

Private Sub cmdTableCount_Click()
'Error Checking
  If cboFlowingWatersUpdateTable.ListIndex = -1 Then
      MsgBox "Please choose a layer to count."
      Exit Sub
  End If

'Setup
  Dim pMxDoc As IMxDocument
  Dim pSATCollection As IStandAloneTableCollection
  Dim pTS As ITableSelection

  Set pMxDoc = ThisDocument
  Set pSATCollection = pMxDoc.FocusMap
'This next bit must be on the a single line;
'  the format of this blog makes coding difficult.
  Set pTS = pSATCollection.StandaloneTable(
       cboFlowingWatersUpdateTable.ListIndex)

'Display a message box to report info
  If pTS.SelectionSet.Count = 1 Then  
      MsgBox "There is 1 feature selected."
  Else 
      MsgBox "There are " & pTS.SelectionSet.Count & _
          " features selected."
  End If

End Sub

Now, after you map a standalone table in the Flowing Waters Update Table combo box and click the Table Count command button, you will be able to count any selected records in the table, just like you would count the records in a spatial layer.