Thought I would give this part of GeoNet a try, share my experience and hopefully have some python guru come in and shoot me down in flames and tell me how it should be done!
Scenario
I want a python script tool that has a value table with drop down choices. In this example I want to add multiple layers and select different fields as different datasets have different names for their ID fields. The interface I have created looks as below.
When you add a new activity layer the ID field becomes a drop down for that row, listing fields from the dataset.
How I did it
To create this interface I created a Python Toolbox. This was not a script that I had created and wired up into a Script Tool. The code is below and I've tried to document it mainly for my benefit but to help others understand. After the code are some limitations discussed.
import arcpy
class Toolbox(object):
def __init__(self):
"""Define the toolbox (the name of the toolbox is the name of the .pyt file)."""
self.label = "My first python toolbox"
self.alias = "Examples"
# List of tool classes associated with this toolbox
self.tools = [Tool1]
class Tool1(object):
def __init__(self):
"""Define the tool (tool name is the name of the class)."""
self.label = "Test tool"
self.description = "Example showing how to create a value table tool with drop downs"
self.canRunInBackground = False
def getParameterInfo(self):
"""Define parameter definitions"""
#
# Define Parameter 0
#
# Param0 will be a FeatureLayer that accepts only polygon layers
param0 = arcpy.Parameter(displayName = "Site Layer",name="Site_layer",datatype="GPFeatureLayer",parameterType="Required",direction="Input")
# This filters for polygon layers only
param0.filter.list = ["Polygon"]
#
# Define Parameter 1
#
# Param1 will be a field which is dependant on the layer in Param0
param1 = arcpy.Parameter(displayName = "Site ID Field",name="SiteID_field",datatype="Field",parameterType="Required",direction="Input")
# This is filtering for text fields only
param1.filter.list=["Text"]
# This says that param1 is dependant upon param0
param1.parameterDependencies=[param0.name]
#
# Define Parameter 2
#
# Param2 is a valuetable with 2 columns, a layer and an ID field
param2 = arcpy.Parameter(displayName = "Activity Layers",name="Activity_layers",datatype="GPValueTable",parameterType="Optional",direction="Input")
# This creates 2 columns a featurelayer and a string
param2.columns=[["Feature Layer","Activity Layer"],["String","ID field"]]
# This sets the filter on the first column, only point data is allowed
param2.filters[0].list = ["Point"]
# This says the filter on the second column will be a value List
param2.filters[1].type="ValueList"
# This adds a dummy value to the list, this will get overwritten by the updateParameters function
param2.filters[1].list=["x"]
# Create a list of parameters and return
params = [param0,param1,param2]
return params
def isLicensed(self):
"""Set whether tool is licensed to execute."""
return True
def updateParameters(self, parameters):
"""Modify the values and properties of parameters before internal
validation is performed. This method is called whenever a parameter
has been changed."""
# Update drop down for second column. Logic is that the last entry in
# the value table is the one you want to select a field ID for.
if parameters[2].altered:
# Return a list of lists
lol = parameters[2].values
# The number of rows in the table
numrows = len(lol)
i = 0
for al in lol:
i+=1
if i == numrows:
# We are at the end of the table, now update filter to
# list only the fields for the FeatureLayer in the last row.
lay = al[0]
fi = al[1]
desc = arcpy.Describe(lay)
fields = desc.fields
l=[]
for f in fields:
if f.type in ["Integer","OID"]:
l.append(f.name)
parameters[2].filters[1].list = l
return
def updateMessages(self, parameters):
"""Modify the messages created by internal validation for each tool
parameter. This method is called after internal validation."""
return
def execute(self, parameters, messages):
"""The source code of the tool."""
try:
# Some code to show that I had managed to get what I wanted intot the ValueTable
fo = open(r"C:\test.txt","w")
fo.write(str(parameters[2].values))
fo.close()
except arcpy.ExecuteError:
messages.addErrorMessage(arcpy.GetMessages())
Limitations
The main limitation is that the logic of the script searches for the last row, reads the layer name and then sets the filter to be the fields of that dataset (just what you want). If you have added more than 2 layers and then go back to say the first row to choose another ID field you discover that the filter applies to the whole column and not the individual row so you will get fields offered up from the last layer in the valuetable.
I have been unable to work out how you can set drop downs specific to a row. There is no way that I have found that captures the onclick event of a cell in a valuetable. If you are an esri python guru (may be part of the development team?) reading this I would love to know if this can be done? Just look at the Create TIN tool to observe the technique I am looking for.
Final note; there appears to be no way to run a python toolbox in debug mode, yes there is an esri blog page but that is a real fudge and you can't use it to test how the interface is responding.
Hope you find this helpful?
I think your code would only be available to 10.3 and up. the parameter.filters property wasn't available in 10.2. Just as an FYI. Otherwise, it looks interesting!
I've been trying to figure out how to do almost exactly this. Thanks! One thing I'm curious about is the updateParameters function. Is there any reason to loop through each layer rather than just using the index of the last row directly?
something like this:
def updateParameters(self, parameters):
"""Modify the values and properties of parameters before internal
validation is performed. This method is called whenever a parameter
has been changed."""
# Update drop down for second column. Logic is that the last entry in
# the value table is the one you want to select a field ID for.
if parameters[2].altered:
# Return a list of lists
lol = parameters[2].values
# The number of rows in the table
# Use numrows to refer directly to last row rather than looping through without doing anything
numrows = len(lol)
# now update filter to list only the fields for the FeatureLayer in the last row.
lay = lol[numrows][0]
fi = lol[numrows][1]
fields = arcpy.ListFields(lay)
l=[]
for f in fields:
if f.type in ["Integer","OID"]:
l.append(f.name)
parameters[2].filters[1].list = l
return
Hi David,
No there is no reason, just how my brain resolved it! Your approach is certainly more efficient. Thanks!
Duncan
def updateParameters(self, parameters): """Modify the values and properties of parameters before internal validation is performed. This method is called whenever a parameter has been changed.""" # Update drop down for second column. Logic is that the last entry in # the value table is the one you want to select a field ID for. if parameters[2].altered: # Return a list of lists lol = parameters[2].values # now update filter to list only the fields for the FeatureLayer in the last row. lay = lol[-1][0] # capture layer portion of the parameter in last row fi = lol[-1][1] fields = arcpy.ListFields(lay)
# using list comprehension
parameters[2].filters[1].list = [field.name for field in fields if field.type in
["Integer","OID"]
]
return
Python technicalities but looks cleaner
Hi Duncan,
Great document here, thanks for sharing.
For debugging purposes, you could use the arcpy.AddMessage function to get direct feedback in the arcpy console once you hit the OK button (?) , for example reading values from parameters
def execute(self, parameters, messages): """Execute the source code of the tool.""" fields =
parameters[2].values
arcpy.AddMessage(fields)
return