ArcMap Search Add-In with Python

Most people who work with ArcMap have at least a basic familiarity with python, writing scripts with arcpy, and adding those scripts as a custom tool.  I’ve semi-recently realized that you’re also able to create add-ins with python using the Python Add-In Wizard.  I’ve talked to a couple people about it who were also surprised to learn that it’s possible to make add-ins with python.

Making python addins for ArcMap is easy if you are already familiar with python and arcpy. If you frequently find yourself repeating the same task in your GIS workflow, why not make a button in the toolbar to help you out? It’s unfortunate that ESRI seems to have taken away the ability to make python addins in ArcGIS Pro (though you can still do it with .net), but luckily VDOT still uses ArcMap. I made a handy addin that helps me enter a very frequently used query and will select a specific record from the table that I choose from a dropdown menu. I thought I would share it here in case anyone else might be interested in creating something similar for their projects.

The Problem

Most of the tables I work with use a combination of 3 fields as a unique id for each record: juris_no, route_no, and seq_no. Most of the tables have a single field that is a concatenation of these three, but the name (and length!) of that field is not always consistent between tables. I find it easier to enter my queries all day like this:

JURIS_NO = ‘123’ AND ROUTE_NO = ‘12345’ AND SEQ_NO = ‘123’

Which is pretty easy, but then if I have to do the same query in multiple tables or clear it out to search for something else, I would have to type it in again. Sometimes I have to do this all day. It would be nice to be able to have text box where I could just type in the record’s id (eg ‘12312345123’), press Enter, and have the correct record selected in the correct table.

The Addin

So I made a python addin that does that for me.

The dropdown box on the left shows a list of all of the layers and tables in the current map that have the three necessary fields to search. The list automatically updates as layers are added or removed. The search box accepts multiple formats as input to make it easy to copy and paste record ids from an email. Some people might write the same record as 12312345123 or 123 2345 123 or 123/12345/123. I just want to copy anything into the box and make it work.
I decided to use Class Variables to allow each control to communicate with each other. The search button was very straight-forward. All it does is calls the search function when it is clicked.

import arcpy
import pythonaddins as pa

class btnSearch(object):
“””Implementation for btnSearch.button (Button)”””
def __init__(self):
self.enabled = True
self.checked = False

def onClick(self):
search()

The search box was equally straight-forward.

class txtSearch(object):
"""Implementation for txtSearch.combobox (ComboBox)"""

# Stores a copy of the text that is currently entered in the search box
curSearch = ”

def __init__(self):
self.items = []
self.editable = True
self.enabled = True
self.dropdownWidth = ‘W’ * 11
self.width = ‘W’ * 11

def onSelChange(self, selection):
pass

def onEditChange(self, text):
txtSearch.curSearch = text

def onFocus(self, focused):
pass

def onEnter(self):
search()

def refresh(self):
pass

The layers dropdown was a bit more complicated to create. In order to keep the list up to date, I have each layer and table in the mxd checked in the onFocus method. It causes a little bit of a delay, but it’s still pretty fast.

class cbLayers(object):
"""Implementation for cbLayers.combobox (ComboBox)"""

# Stores the current object to be searched
curObject = None

# Contains all of the valid searchable objects in the mxd
curObjectDict = {}

def __init__(self):
self.items = []
self.editable = True
self.enabled = True
self.dropdownWidth = ‘W’ * 25
self.width = ‘W’ * 25
self.value = ‘— Select a Layer —‘
self.refresh()

def onSelChange(self, selection):
“””When the user makes a selection, the visible text changes to the
new selection and the curObject variable is filled with the object
selected from the curObjectDict dictionary”””

self.value = selection
if selection != (”):
cbLayers.curObject = cbLayers.curObjectDict[selection]

def onEditChange(self, text):
pass

def onFocus(self, focused):
“””The list of items are refreshed every time that the user clicks on
the combo box. This ensures that new items are added and items
that are no longer in the mxd are removed.”””
if (focused):
self.items = []
layers, cbLayers.curObjectDict = GetListOfLayers()
for layer in layers:
self.items.append(layer.name)

self.refresh()

def onEnter(self):
pass

def refresh(self):
pass

The GetListOfLayers function that is called in onFocus() only returns layers and tables that have the 3 fields that I need to do the search. I’m sure there’s a more efficient way of doing this, but here’s what I came up with:

def GetListOfLayers():
"""Returns a list containing layer and table objects that are currently
in the Table of Contents (layersOutput) and a dictionary of those objects
(curObjectDict). Only layers with the required fields are included in
the list."""

# Get a list of all layers and table views and store them in a single list
# called ‘layers’
mxd = arcpy.mapping.MapDocument(‘CURRENT’)
layers = arcpy.mapping.ListLayers(mxd)
tables = arcpy.mapping.ListTableViews(mxd)
for table in tables:
layers.append(table)

# Set up object dictionary. This will be used to store the selected object
# in the curObject variable
curObjectDict = {}
for layer in layers:
curObjectDict[layer.name] = layer

layersOutput = []
for layer in layers:
try:
# Filter out basemaps because they take a long time to analyze
basemapList = [‘Reference’, ‘World Light Gray Reference’,
‘Light Gray Canvas Reference’,
‘World Dark Gray Reference’,
‘Dark Gray Canvas Reference’,
‘World Boundaries and Places’,
‘Basemap’, ‘World Street Map’,
‘World Imagery’, ‘Low Resolution 15m Imagery’,
‘High Resolution 60cm Imagery’,
‘High Resolution 30cm Imagery’, ‘Citations’,
‘World Topographic Map’,
‘World Dark Gray Canvas Base’,
‘Dark Gray Canvas Base’,
‘World Light Gray Canvas Base’,
‘Light Gray Canvas Base’,
‘National Geographic World Map’,
‘NatGeo_World_Map’, ‘OpenStreetMap’]

if (layer.name in basemapList):
continue

fields = arcpy.ListFields(layer)
neededFields = [“JURIS_NO”, “ROUTE_NO”, “SEQ_NO”]
hasFields = []
for field in fields:
if field.name in neededFields:
hasFields.append(field.name)
if len(hasFields) == 3:
layersOutput.append(layer)
except:
print(“Error with {}”.format(layer.name))
continue

return layersOutput, curObjectDict

Finally, the search function tries to search for the id entered in the search text box.

def search():
"""Validates the search text in txtSearch.curSearch, then searches
for it in the selected object in cbLayers.curObject"""

# Catch no layer selected to search
if (not cbLayers.curObject):
pa.MessageBox(“No layer selected to search”, “Error”)
return

# Catch blank search text
if (txtSearch.curSearch == ”):
pa.MessageBox(“Nothing entered to search”, “Error”)
return

# Catch incorrect length
if (len(txtSearch.curSearch) < 10 or len(txtSearch.curSearch) > 13):
pa.MessageBox(“Invalid Search\n\nThe following formats are acceptable:\n JJJRRRRRSSS\n JJJRRRRSSS\n JJJ RRRRR SSS”, “Error”)
return

# Catch using non-numbers
validChars = [‘1′,’2′,’3′,’4′,’5′,’6′,’7′,’8′,’9′,’0′,’ ‘,’-‘,’/’,’.’,’\\’]
for char in txtSearch.curSearch:
if char not in validChars:
pa.MessageBox(“Search must only include number”, “Error”)
return

# Parse search text and select
curSearch = txtSearch.curSearch
juris_no = ”
route_no = ”
seq_no = ”

if(len(curSearch) == 11):
juris_no = curSearch[:3]
route_no = curSearch[3:8]
seq_no = curSearch[8:]

if (len(curSearch) == 10):
juris_no = curSearch[:3]
route_no = ‘0’ + curSearch[3:7]
seq_no = curSearch[7:]

if (len(curSearch) == 13):
juris_no = curSearch[:3]
route_no = curSearch[4:9]
seq_no = curSearch[10:]

sql = “JURIS_NO = ‘{}’ AND ROUTE_NO = ‘{}’ AND SEQ_NO = ‘{}'”.format(juris_no, route_no, seq_no)

try:
arcpy.SelectLayerByAttribute_management(cbLayers.curObject,
“NEW_SELECTION”,
sql)
except Exception, e:
pa.MessageBox(“There was an error with the search. Check to make sure that the layer is in the mxd.\n\n{}”.format(repr(e)), “Error”)
return

if len(cbLayers.curObject.getSelectionSet()) == 0:
pa.MessageBox(“No results for \n\n{}\n\n in {}.”.format(sql, cbLayers.curObject.name), “No results in {}”.format(cbLayers.curObject.name))

Hopefully someone out there will find this example useful!

Leave a Reply

Your email address will not be published. Required fields are marked *