Now we are going to develop the main code for our tool that imports the gui_main.py and gui_newshapefile.py files, sets up the application, and connects the different GUI elements with event handler functions and functions that realize the actual functionality of the tool. We will organize the code into several modules. In particular, we will keep the functions that realize the main functionality, such as querying the different web portals, creating a new shapefile, etc. in a separate Python script file called core_functions.py . These functions are completely independent of the GUI of our tool and the few global variables we will need, so we might want to use them in other projects. Separating the project cleanly into GUI dependent and GUI independent code fosters reusability of the GUI independent code. Overall, the project will consist of the following Python files:
In the following, we will focus on the code from main.py but we will start with a brief look at core_functions.py, so please download the file core_functions.py [1] and open it so that you can read the code for the different functions. Most of the functions defined in the script should be rather easy to understand from the comments in the code and from your experience with using arcpy to work with shapefiles. Here is an overview of the functions with a few additional explanations:
Now that you know the functions we have available for realizing the different operations that we will need, let’s develop the code for main.py together. Open a new file main.py in your IDE, then follow the steps listed on the next few pages.
We start by importing the different packages of the Python standard library and PyQt5 that we will need in this project. In addition, we import the gui_main.py and gui_newshapefile.py files so that the Ui_MainWindow and Ui_Dialog classes defined in them are available for creating the GUIs of the main window and dialog for creating a new shapefile, and of course the core_functions module. We are not importing arcpy here because we want the tool to be runnable even when arcpy is not available and that is why we defined the auxiliary function for testing its availability in core_functions.py. In addition, we are including some comments to define sections within the script for different purposes. We will fill in the code for these sections step-by-step in the following steps. At the very end, we already have the by-now-familiar code for showing the main window and starting the event processing loop of our application (even though we are not creating the application and main window objects yet).
import sys, csv from PyQt5.QtWidgets import QApplication, QMainWindow, QStyle, QFileDialog, QDialog, QMessageBox, QSizePolicy from PyQt5.QtGui import QStandardItemModel, QStandardItem, QDoubleValidator, QIntValidator from PyQt5.QtCore import QVariant from PyQt5.Qt import Qt try: from PyQt5.QtWebEngineWidgets import QWebEngineView as WebMapWidget except: from PyQt5.QtWebKitWidgets import QWebView as WebMapWidget import gui_main import gui_newshapefile import core_functions # ======================================= # GUI event handler and related functions # ======================================= #========================================== # create app and main window + dialog GUI # ========================================= #========================================== # connect signals #========================================== #================================== # initialize global variables #================================== #============================================ # test availability and if run as script tool #============================================ #======================================= # run app #======================================= mainWindow.show() sys.exit(app.exec_())
You may be wondering what is happening in lines 8 to 10. The reason for the try-except construct there is the web view widget we are using in the "Results" part of the GUI to display a Leaflet-based web map of the results. There have been some changes with regard to the web view widget over the last versions of QT5 with the old class QWebView becoming deprecated and a new class QWebEngineView being added to replace it. The purpose of the code is to use QWebEngineView if it is available (meaning the code is run with a newer version of PyQt5) and otherwise fall back to using QWebView. The alias WebMapWidget is used to make sure that in both cases the imported class is available under the same name.
In the next step, we add the code for creating the QApplication and the QMainWindow and QDialog objects for the main window and the dialog for creating a new shapefile with their respective GUIs. For this, please paste the following code into your script directly under the comment “# create app and main window + dialog GUI”:
app = QApplication(sys.argv) # set up main window mainWindow = QMainWindow() ui = gui_main.Ui_MainWindow() ui.setupUi(mainWindow) ui.actionExit.setIcon(app.style().standardIcon(QStyle.SP_DialogCancelButton)) ui.layerRefreshTB.setIcon(app.style().standardIcon(QStyle.SP_BrowserReload)) ui.directInputLatLE.setValidator(QDoubleValidator()) ui.directInputLonLE.setValidator(QDoubleValidator()) ui.nominatimLimitLE.setValidator(QIntValidator()) ui.geonamesLimitLE.setValidator(QIntValidator()) mapWV = WebMapWidget() mapWV.page().profile().setHttpAcceptLanguage("en-US") mapWV.setHtml(core_functions.webMapFromDictionaryList([])) ui.resultsListAndMapHBL.addWidget(mapWV) mapWV.setFixedSize(300,200) mapWV.setSizePolicy(QSizePolicy(QSizePolicy.Fixed,QSizePolicy.Fixed)) # set up new shapefile dialog createShapefileDialog = QDialog(mainWindow) createShapefileDialog_ui = gui_newshapefile.Ui_Dialog() createShapefileDialog_ui.setupUi(createShapefileDialog)
In lines 4 to 6, we are creating the mainWindow object and then its GUI by calling the ui.SetupUi(…) method of an object we created from the Ui_MainWindow class defined in gui_main.py. The same happens in lines 23 to 25 for the dialog box for creating a new shapefile. The rest of the code is for creating some additional elements or setting some properties of GUI elements that we couldn’t take care of in QT Designer:
We now add some code to initialize some global variables that we will need. Please add the following code directly under the comment “# initialize global variables”:
# dictionary mapping tabs from services tab widget to event handler functions queryHandler = { ui.nominatimTab: runNominatimQuery, ui.geonamesTab: runGeonamesQuery, ui.directInputTab: runDirectInput } # dictionary mapping tabs from add feature tab widget to event handler functions addFeaturesHandler = { ui.layerTab: addFeaturesToLayer, ui.shapefileTab: addFeaturesToShapefile, ui.csvTab: addFeaturesToCSV } result = [] # global variable for storing query results as list of dictionaries arcValidLayers= {} # dictionary mapping layer names to layer objects arcpyAvailable = False # indicates whether is available for import runningAsScriptTool = False # indicates whether script is run as script tool inside ArcGIS
The first two variables defined, queryHandler and addFeaturesHandler, are dictionaries that contain the information of which event handler functions should be called when the “Run query” button and “Add features” button are clicked, respectively, depending on which of the tabs of the two different tab widgets are currently selected. Line 2, for instance, says that if currently the tab ui.nominatimTab is open in the Services section, then the function runNominatimQuery(…) should be called. So far we have not defined that function yet, hence, you will not be able to run the program at the moment but it shows you that functions in Python are treated like other kinds of objects, meaning they can, for instance, be stored in a dictionary. You will hear more about this in Lesson 3.
The other global variables we define in this piece of code are for keeping track of the results currently displayed in the Results list view widget of our GUI, of the currently open Point layers in ArcGIS when being run as a script tool, of whether the arcpy module is available, and of whether the program is being run as a script tool or not. We will add code to initialize the last two of these variables correctly in a moment
To now initialize the variables arcpyAvailable and runningAsScriptTool correctly and potentially disable some GUI elements, please add the following code directly under the comment “# test availability and if run as script tool”:
arcpyAvailable = core_functions.importArcpyIfAvailable() if not arcpyAvailable: ui.addFeaturesTW.setCurrentWidget(ui.csvTab) ui.addFeaturesTW.setTabEnabled(ui.addFeaturesTW.indexOf(ui.shapefileTab),False) ui.addFeaturesTW.setTabEnabled(ui.addFeaturesTW.indexOf(ui.layerTab),False) ui.statusbar.showMessage('arcpy not available. Adding to shapefiles and layers has been disabled.') else: import arcpy if core_functions.runningAsScriptTool(): runningAsScriptTool = True updateLayers() else: ui.addFeaturesTW.setTabEnabled(ui.addFeaturesTW.indexOf(ui.layerTab),False) ui.statusbar.showMessage(ui.statusbar.currentMessage() + 'Not running as a script tool. Adding to layer has been disabled.')
What happens here is that we first use importArcpyIfAvailable() from the core_functions module to check whether we can import arcpy. If this is not the case, we make the “CSV” tab the current and only selectable tab from the Add Features tab widget by disabling the “Shapefile” and “Layer” tabs. In the else-part (so if arcpy is available), we further use the runningAsScriptTool() function to check if the program is being run as a script tool inside ArcGIS. If not, we just disable the “Layer” tab and make the “Shapefile” tab the currently selected one. In addition, some warning message in the statusbar is produced if either the “Layer” or both the “Layer” and “Shapefile” tabs had to be disabled.
Time to get back to the GUI and implement the required event handler functions for the different GUI elements, in particular the different buttons. This will be quite a bit of code, so we will go through the event handler functions individually. Please add each function in the order they are listed below to the section labeled “# GUI event handler and related functions”:
# query and direct input functions def runQuery(): """run one of the different query services based on which tab is currently open""" queryString = ui.queryTermLE.text() activeTab = ui.queryServicesTW.currentWidget() queryHandler[activeTab](queryString) # call a function from the dictionary in queryHandler
This is the event handler function for when the “Run query” button is clicked. We already mentioned the global variable queryHandler that maps tab widgets to functions. So we here first get the text the user entered into the queryTermLE widget, then get the currently selected tab from the queryServicesTW tab widget, and finally in the last line we call the corresponding function for querying Nominatim, GeoNames, or providing direct input. These functions still need to be defined.
def setListViewFromResult(r): """populate list view with checkable entries created from result list in r""" m = QStandardItemModel() for item in r: item = QStandardItem(item['name'] + ' ('+item['lat'] + ',' + item['lon'] + ')') item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) item.setData(QVariant(Qt.Checked), Qt.CheckStateRole) m.appendRow(item) ui.resultsLV.setModel(m)
setListViewFromResult(…) is an auxiliary function for populating the resultsLV list view widget with the result from a query or a direct input. It will be called from the functions for querying the different web services or providing direct input that will be defined next. The given parameter needs to contain a list of dictionaries with name, lat, and lon properties that each represent one item from the result. The for-loop goes through these items and creates list items for the QStandardItemModel from them. Finally, the resulting model is used as the list model for the resultsLV widget.
def runNominatimQuery(query): """query nominatim and update list view and web map with results""" ui.statusbar.showMessage('Querying Nominatim... please wait!') country = ui.nominatimCountryCodeLE.text() if ui.nominatimCountryCodeCB.isChecked() else '' limit = ui.nominatimLimitLE.text() try: items = core_functions.queryNominatim(query, limit, country) # run query # create result list from JSON response and store in global variable result global result result = [(lambda x: {'name': x['display_name'],'lat': x['lat'], 'lon': x['lon']})(i) for i in items] # update list view and map with results setListViewFromResult(result) mapWV.setHtml(core_functions.webMapFromDictionaryList(result)) ui.statusbar.showMessage('Querying done, ' + str(len(result)) + ' results returned!') except Exception as e: QMessageBox.information(mainWindow, 'Operation failed', 'Querying Nominatim failed with '+ str(e.__class__) + ': ' + str(e), QMessageBox.Ok ) ui.statusbar.clearMessage()
This is the function that will be called from function runQuery() defined above when the currently selected “Service” tab is the Nominatim tab. It gathers the required information from the line edit widgets on the Nominatim query tab, taking into account the status of the corresponding checkboxes for the optional elements (using the "... if ... else ..." ternary operator). Then it calls the queryNominatim(…) function from the core_functions module to perform the actual querying (line 9) and translates the returned JSON list into a result list of dictionaries with name, lat, and lon properties that will be stored in the global variable result. Note that we are using list comprehension here to realize this translation of one list into another. The resultLV list view and mapWV web map widget will then be updated accordingly. This happens inside a try-except block to catch exceptions when something goes wrong with querying the web service or interpreting the results. Statusbar messages are used to keep the user informed about the progress and a message box is shown if an exceptions occurs to inform the user.
def runGeonamesQuery(query): """query geonames and update list view and web map with results""" ui.statusbar.showMessage('Querying GeoNames... please wait!') username = ui.geonamesUsernameLE.text() country = ui.geonamesCountryCodeLE.text() if ui.geonamesCountryCodeCB.isChecked() else '' fclass = ui.geonamesFeatureClassLE.text() if ui.geonamesFeatureClassCB.isChecked() else '' limit = ui.geonamesLimitLE.text() try: items = core_functions.queryGeonames(query, limit, username, country, fclass ) # run query # create result list from JSON response and store in global variable result global result result = [(lambda x: {'name': x['toponymName'],'lat': x['lat'], 'lon': x['lng']})(i) for i in items] # update list view and map with results setListViewFromResult(result) mapWV.setHtml(core_functions.webMapFromDictionaryList(result)) ui.statusbar.showMessage('Querying done, ' + str(len(result)) + ' results returned!') except Exception as e: QMessageBox.information(mainWindow, 'Operation failed', 'Querying GeoNames failed with '+ str(e.__class__) + ': ' + str(e), QMessageBox.Ok) ui.statusbar.clearMessage()
This function that will be called from runQuery() if the currently selected “Service” tab is the GeoNames tab works exactly like the previous function for Nominatim, just the query parameters extracted in lines 5 to 8 are different and the translation into a result list looks a little bit different because GeoNames uses other property names ("toponymName" instead of "display_name" and "lng" instead of "lon").
def runDirectInput(query): """create single feature and update list view and web map with results""" name = ui.directInputNameLE.text() lon = ui.directInputLonLE.text() lat = ui.directInputLatLE.text() # create result list with single feature and store in global variable result global result result = [{ 'name': name, 'lat': lat, 'lon': lon }] # update list view and map with results setListViewFromResult(result) mapWV.setHtml(core_functions.webMapFromDictionaryList(result)) ui.statusbar.showMessage('Direct input has been added to results list!')
This function will be called from runQuery() if the currently selected “Service” tab is the Direct Input tab. Again, we are collecting the relevant information from the input widgets (line 3 to 5) but here we directly produce the result consisting of just a single item (line 9). The rest works in the same way as in the previous two functions.
These were the functions required for the query section of our tool. So we can now move on to the Results section where we just need the three event handler functions for the three buttons located below the list view widget.
# list view selection functions def selectAll(): """select all items of the list view widget""" for i in range(ui.resultsLV.model().rowCount()): ui.resultsLV.model().item(i).setCheckState(Qt.Checked) def clearSelection(): """deselect all items of the list view widget""" for i in range(ui.resultsLV.model().rowCount()): ui.resultsLV.model().item(i).setCheckState(Qt.Unchecked) def invertSelection(): """invert current selection of the list view widget""" for i in range(ui.resultsLV.model().rowCount()): currentValue = ui.resultsLV.model().item(i).checkState() ui.resultsLV.model().item(i).setCheckState(Qt.Checked if currentValue == Qt.Unchecked else Qt.Unchecked)
These three functions all work very similarly: We go through all items in the list model underlying the resultsLV list view widget. In selectAll(), the check state of each item is set to “Checked”, while in clearSelection() it is set to “Unchecked” for each item. In invertSelection(), we take the item’s current state and either change it from “Checked” to “Unchecked” or vice versa (using the ternary "... if ... else ..." operator once more).
# adding features functions def addFeatures(): """run one of the different functions for adding features based on which tab is currently open""" activeTab = ui.addFeaturesTW.currentWidget() addFeaturesHandler[activeTab]() # call a function from the dictionary in addFeatureHandler
We have now arrived at the last row of our main window graphical interface for adding the selected result features to a layer, shapefile, or csv file. The addFeatures() function corresponds to the runQuery() function from the beginning in that it invokes the right function depending on which tab of the addFeaturesTW tab widget is currently selected. This is based on the global variable addFeaturesHandler that map tabs to functions.
def updateShapefileFieldCB(): """update shapefileFieldCB combo box with field names based on shapefile name""" ui.shapefileFieldCB.clear() fileName = ui.shapefileAddLE.text() ui.shapefileFieldCB.addItems(core_functions.getValidFieldsForShapefile(fileName))
updateShapefileFieldCB() is an auxiliary function for updating the content of the shapefileFieldCB combo box whenever the name of the shapefile in the shapefileAddLE line edit widget changes so that the combo always displays the editable string fields of that shapefile.
def selectShapefile(): """open file dialog to select exising shapefile and if accepted, update GUI accordingly""" fileName, _ = QFileDialog.getOpenFileName(mainWindow,"Select shapefile", "","Shapefile (*.shp)") if fileName: ui.shapefileAddLE.setText(fileName) updateShapefileFieldCB()
When the shapefileOpenFileTB tool button is clicked, we want to display a file dialog for picking the shapefile. Opening the dialog and processing the result happens in the function selectShapefile(). When a file name is returned (meaning the dialog wasn’t cancelled by the user), the name is put into the shapefileAddLE line edit field and updateShapefieldCB() is called to update the combo box with the field names of that file.
def addFeaturesToShapefile(): """add selected features from list view to shapefile""" fieldName = ui.shapefileFieldCB.currentText() fileName = ui.shapefileAddLE.text() ui.statusbar.showMessage('Adding entities has started... please wait!') try: with arcpy.da.InsertCursor(fileName, ("SHAPE@",fieldName)) as cursor: for i in range(ui.resultsLV.model().rowCount()): # go through all items in list view if ui.resultsLV.model().item(i).checkState() == Qt.Checked: point = arcpy.Point( result[i]['lon'], result[i]['lat']) cursor.insertRow( (point, result[i]['name'][:30]) ) # name shortened to 30 chars ui.statusbar.showMessage('Adding entities has finished.') except Exception as e: QMessageBox.information(mainWindow, 'Operation failed', 'Writing to shapefile failed with '+ str(e.__class__) + ': ' + str(e), QMessageBox.Ok ) ui.statusbar.clearMessage()
This function contains the code for writing the selected features from the results list in global variable result to the shapefile with the help of an arcpy insert cursor. We first read the relevant information from the shapefileAddLE and shapefileFieldCB widgets and then in the for-loop go through the items in the resultsLV list view to see whether they are checked or not. If an item is checked, an arcpy.Point object is created from the corresponding dictionary in variable result and then written to the shapefile together with the name of the location. Statusbar messages are used to inform on the progress or a message box will be shown if an exception occurs while trying to write to the shapefile.
def updateLayerFieldCB(): """update layerFieldCB combo box with field names based on selected layer""" ui.layerFieldCB.clear() layer = ui.layerPickLayerCB.currentText() try: ui.layerFieldCB.addItems(core_functions.getStringFieldsForDescribeObject(arcpy.Describe(arcValidLayers[layer]))) except Exception as e: QMessageBox.information(mainWindow, 'Operation failed', 'Obtaining field list failed with '+ str(e.__class__) + ': ' + str(e), QMessageBox.Ok ) ui.statusbar.clearMessage()
This is the corresponding function to updateShapefileFieldCB() but for the layerFieldCB combo box widget part of the Layer tab.
def updateLayers(): """refresh layers in global variable arcValidLayers and layerPickLayerCB combo box""" layers = [] global arcValidLayers arcValidLayers = {} ui.layerPickLayerCB.clear() ui.layerFieldCB.clear() try: layers = core_functions.getPointLayersFromArcGIS() # get all point layers for l in layers: # add layers to arcValidLayers and GUI arcValidLayers[l.name] = l ui.layerPickLayerCB.addItem(l.name) updateLayerFieldCB() except Exception as e: QMessageBox.information(mainWindow, 'Operation failed', 'Obtaining layer list from ArcGIS failed with '+ str(e.__class__) + ': ' + str(e), QMessageBox.Ok ) ui.statusbar.clearMessage() ui.shapefileFieldCB.clear()
This function is for populating the layerPickLayerCB and arcValidLayer global variable with the Point vector layers currently open in ArcGIS. It uses the getPointLayersFrom ArcGIS() function from core_functions.py to get the list of layers and then in the for-loop stores the layer objects under their layer name in the arcValidLayers dictionary and just the names as items in the combo box. If something goes wrong with getting the layers from ArcGIS, the corresponding exception will be caught and a message box will warn the user about the failure of the operation.
def addFeaturesToLayer(): """add selected features from list view to layer""" layer = ui.layerPickLayerCB.currentText(); fieldName = ui.layerFieldCB.currentText() ui.statusbar.showMessage('Adding entities has started... please wait!') try: with arcpy.da.InsertCursor(arcValidLayers[layer], ("SHAPE@",fieldName)) as cursor: for i in range(ui.resultsLV.model().rowCount()): # go through all items in list view if ui.resultsLV.model().item(i).checkState() == Qt.Checked: point = arcpy.Point( float(result[i]['lon']), float(result[i]['lat'])) cursor.insertRow( (point, result[i]['name'][:30]) ) # name shortened to 30 chars ui.statusbar.showMessage('Adding entities has finished.') except Exception as e: QMessageBox.information(mainWindow, 'Operation failed', 'Writing to layer failed with '+ str(e.__class__) + ': ' + str(e), QMessageBox.Ok ) ui.statusbar.clearMessage()
This is the analogous function to the previously defined function addFeaturesToShapefile() but for a currently open layer and based on the information in the widgets of the Layer tab.
def selectCSV(): """open file dialog to select exising csv/text file and if accepted, update GUI accordingly""" fileName, _ = QFileDialog.getOpenFileName(mainWindow,"Select CSV file", "","(*.*)") if fileName: ui.csvAddToFileLE.setText(fileName)
Similarly to selectShapefile(), this function opens a file dialog to select a csv file to append the features to.
def addFeaturesToCSV(): """add selected features from list view to csv/text file""" fileName = ui.csvAddToFileLE.text() ui.statusbar.showMessage('Adding entities has started... please wait!') try: with open(fileName, 'a', newline='') as csvfile: csvWriter = csv.writer(csvfile) for i in range(ui.resultsLV.model().rowCount()): # go through all items in list view if ui.resultsLV.model().item(i).checkState() == Qt.Checked: csvWriter.writerow( [ result[i]['name'], result[i]['lon'], result[i]['lat'] ]) ui.statusbar.showMessage('Adding entities has finished.') except Exception as e: QMessageBox.information(mainWindow, 'Operation failed', 'Writing to csv file failed with '+ str(e.__class__) + ': ' + str(e), QMessageBox.Ok ) ui.statusbar.clearMessage()
Working similarly to addFeaturesToShapefile() and addFeaturesToLayer(), this function writes the selected features as rows to a text file using the csv.writer class from the csv module of the Python standard library.
def selectNewShapefile(): """open file dialog to creaete new shapefile and if accepted, update GUI accordingly""" fileName, _ = QFileDialog.getSaveFileName(mainWindow,"Save new shapefile as", "","Shapefile (*.shp)") if fileName: createShapefileDialog_ui.newShapefileLE.setText(fileName)
The final two functions are for creating a new shapefile. selectNewShapefile() is called when the newShapefileBrowseTB button that is part of the dialog box for creating a new shapefile is clicked and displays a file dialog for saving a file under a new name. The chosen name is used to set the text of the newShapefileBrowseTB line edit widget.
def createNewShapefile(): """create new shapefile and adds field based on info in dialog GUI""" if createShapefileDialog.exec_() == QDialog.Accepted: file = createShapefileDialog_ui.newShapefileLE.text() field = createShapefileDialog_ui.fieldForNameLE.text() try: core_functions.createPointWGS1984Shapefile(file,field) ui.shapefileAddLE.setText(file) updateShapefileFieldCB() ui.shapefileFieldCB.setCurrentIndex(ui.shapefileFieldCB.findText(field)) ui.statusbar.showMessage('New shapefile has been created.') except Exception as e: QMessageBox.information(mainWindow, 'Operation failed', 'Creating new shapefile failed with '+ str(e.__class__) + ': ' + str(e), QMessageBox.Ok ) ui.statusbar.clearMessage() ui.shapefileFieldCB.clear()
This function is called when the “Create new shapefile” button on the Shapefile tab is clicked. It first displays the createShapefileDialog dialog box modally by calling its exec_() method. If the dialog is accepted (= closed by clicking Ok), the function creates the new shapefile with the help of the createPointWGS1984Shapefile() function from core_functions.py and based on the input fields in the dialog box for creating a new shapefile (newShapefileLE and fieldForNameLE). If no exception is raised, the file name and field name from the dialog box will be used to change the text of the shapefileAddLE line edit widget and the shapefileFieldCB combo box.
At this point, we are almost done. The last thing that has to happen is connecting the widgets’ relevant signals to the corresponding slots or event handler functions. For this, please add the following code under the comment “# connect signals”:
ui.runQueryPB.clicked.connect(runQuery) ui.resultsClearSelectionPB.clicked.connect(clearSelection) ui.resultsSelectAllPB.clicked.connect(selectAll) ui.resultsInvertSelectionPB.clicked.connect(invertSelection) ui.shapefileOpenFileTB.clicked.connect(selectShapefile) ui.addFeatureAddPB.clicked.connect(addFeatures) ui.shapefileCreateNewPB.clicked.connect(createNewShapefile) ui.csvOpenFileTB.clicked.connect(selectCSV) ui.layerRefreshTB.clicked.connect(updateLayers) ui.shapefileAddLE.editingFinished.connect(updateShapefileFieldCB) ui.layerPickLayerCB.activated.connect(updateLayerFieldCB) createShapefileDialog_ui.newShapefileBrowseTB.clicked.connect(selectNewShapefile)
Lines 1 to 9 and line 13 all connect “clicked” signals of different buttons in our GUI to the different event handler functions defined previously and should be easy to understand. In line 10, the “editingFinished” signal of the text field for entering the name of a shapefile is connected with the updateShapefileFieldCB() function so that, whenever the name of the shapefile is changed, the list of the fields in the combo box is updated accordingly. In line 11, we connect the “activated” signal of the combo box for selecting a layer with the upateLayerFields() function. As a result, the second combo box with the field names will be updated whenever the layer selected in the first combo box on the Layer tab is changed.
That’s it. The program is finished and can be tested and used either as a standalone application (writing features either to a shapefile or to a .csv file) or as an ArcGIS script tool. Give it a try yourself and think about which parts of the code are being executed when performing different operations. In case you want to run it as a script tool inside ArcGIS Pro, setting up the script tool for it should be straightforward. You just have to create a new script tool without specifying any parameters and provide the path to the main.py script for the source. If you have any problems running the code with your own script, the entire code can be downloaded via this link to the Locations from Web Services Complete zip file [3]. If something in the code above is unclear to you, please ask about it on the course forums.
Obviously, the tool is still somewhat basic and could be extended in many ways including:
Moreover, while we included some basic error handling with try-except, the program is not completely bullet proof and in some cases it would be desirable to provide more direct and specific feedback to the user, for instance if the user enters something into the feature class field of the GeoNames query tab that is not a valid feature class code. We are also quietly assuming that the shapefile or layer we are adding to is using a WGS1984 geographical coordinate system. Adding reprojection of the input features to the CRS of the destination would certainly be a good thing to do.
Still, the tool can be useful for creating point feature classes of locations of interest very quickly and in a rather convenient way. More importantly, this walkthrough should have provided you with a better understanding of how to create Python programs with a GUI by roughly separating the code into a part for setting up the GUI elements, a part for realizing the actual functionality (in this case the part defining the different event handler functions (GUI dependent) with the help of the core functions from core_functions.py (GUI independent)), and a part that makes the connection between the other two parts based on GUI events. You will practice creating GUIs and PyQt5 based Python programs yourself in this lesson’s homework assignment and also again in lesson 4. But for now, we will continue with looking at another aspect of creating and publishing Python applications, namely that of package management and packaging Python programs so that they can easily be shared with others.
Links
[1] https://www.e-education.psu.edu/geog489/sites/www.e-education.psu.edu.geog489/files/downloads/core_functions_Sept2023.zip
[2] https://www.e-education.psu.edu/geog489/2233
[3] https://www.e-education.psu.edu/geog489/sites/www.e-education.psu.edu.geog489/files/downloads/LocationsFromWebServicesComplete_Sept2023.zip