ESRI ArcGIS Pro and Desktop License Type and Program Selector User Interfaces

by Jeremy Saunders on June 20, 2023

Updated 17th July 2024

Here are two awesome User Interfaces (UI’s) I built that will compliment any ESRI ArcGIS Pro (AGP) and/or ArcGIS Desktop (AGD) deployment. More specifically from my point of view, they have been developed with a Citrix Published Application, VMware Horizon, Remote Desktop, VDI and AVD deployment in mind, giving the users the ability to easily switch licensing types and launching the different programs and tools, making life much easier for them.

ArcGIS Pro Challenges

  • Changing the licensing is not as simple as it seems, especially when launching it as a published application. This is because you have to change it from within the program once launched; and then the program needs to restart for the new licensing type to work. What that means is that the ArcGISPro.exe process will terminate and restart. Whilst that happens a published application session may log itself off, as there are no processes running to keep it open. The code in this UI will manipulate the user registry values before starting ArcGIS Pro, and therefore setting the licensing type as required. I call this Self-Service by allowing the users to switch License Levels/Types on demand!
  • Having a single interface allowed me to integrate other tools and processes as requested by users over time that could all be associated with ArcGIS Pro. For example, users wanted an easy way to start File Explorer in the same session. Let’s make it easy for the users to do their work! That’s my job!
  • Update on 17th July 2024 for Python code changes I completed in back February:
    • Due to some corporate licensing challenges with ArcGIS Pro, the ArcGIS Pro License Selector has been updated for the following reasons:
    • It defaults to a Basic license, regardless of what you last used. If you need to use a Standard or Advanced license type, please select it before launching ArcGIS Pro. This behaviour can be changed back by using the AlwaysDefaultToBasic script variable as documented below.
    • I have added a “Reset Extensions” checkbox, that is selected by default. This will remove any Extensions you may have selected in your previous session, as the licensing for some of these Extension is also limited. You have the option here of deselecting it if you wish to continue to use previously selected Extensions in your new session. This behaviour can be changed back by using the AlwaysResetExtensions script variable as documented below.
    • Unfortunately, neither of these settings are managed by ESRI at an enterprise level, so I’ve enhanced this tool to assist in reducing the usage of the more advanced licensing options.
    • I have also improved the flow of the Python code.

ArcGIS Desktop Challenges

  • In a large enterprise deployment, setting the licensing model can be a pain. This is done via a user environment variable. So you could use Group Policy Preferences to target them based on AD Security Groups. But that adds an administrative overhead, and users must then log support tickets via the Service Desk, which creates unnecessary overhead and lost time whilst they wait for it to be actioned. That’s not agile, and doesn’t allow users to change their license type to suite their needs. The code in this UI will manipulate the user environment variable before starting the ArcGIS Desktop program of choice, such as ArcMap or ArcCatalog, and therefore setting the licensing type as required. Again, I call this Self-Service by allowing the users to switch License Levels/Types on demand!
  • There are way too many Start Menu items to publish, which is not a neat and tidy way to present it, so why not just present it via a single UI?
  • Having a single interface allowed me to integrate all the common ArcGIS Desktop programs and other tools and processes as requested by users over time that could all be associated with ArcGIS Desktop. For example, users wanted an easy way to start File Explorer in the same session, and they wanted a way to install specific Python modules that are not part of the default Python install. Again, let’s make it easy for the users to do their work! That’s my job.
  • ArcGIS Administrator notes:
$acl= get-acl -path "HKLM:\SOFTWARE\Wow6432Node\ESRI"
$inherit = [system.security.accesscontrol.InheritanceFlags]"ContainerInherit, ObjectInherit"
$propagation = [system.security.accesscontrol.PropagationFlags]"None"
$rule=new-object system.security.accesscontrol.registryaccessrule "USERS","FullControl",$inherit,$propagation,"Allow"
$acl.addaccessrule($rule)
$acl|set-acl

$acl= get-acl -path "HKLM:\SOFTWARE\Wow6432Node\Classes\CLSID\{E6BDAA76-4D35-11D0-98BE-00805F7CED21}"
$inherit = [system.security.accesscontrol.InheritanceFlags]"ContainerInherit, ObjectInherit"
$propagation = [system.security.accesscontrol.PropagationFlags]"None"
$rule=new-object system.security.accesscontrol.registryaccessrule "USERS","FullControl",$inherit,$propagation,"Allow"
$acl.addaccessrule($rule)
$acl|set-acl

About the UIs

They are both written using the Python Tkinter library. This is the built-in de facto GUI library for Python. So nothing overly fancy, but easy to use. A very basic and poorly coded version of the ArcGIS Desktop Selector was supplied to me by a colleague back in 2018. I believe he got it from an ESRI consultant. I then took ownership of it and have re-written most of it since then, adding a majority of the features you see in this release. In early 2020 the need came up to create one for ArcGIS Pro. I decided to keep them both as Python code, as I felt that the support teams that typically support GIS applications would find them easier to support themselves.

These are individual scripts and UIs. However, the following screen shot shows them side-by-side for documentation purposes only. But they can certainly be deployed together on the same computer without issues.

ArcGIS License And Program Selectors

You’ll notice from the screen shots that both interfaces:

  • Tell the user which host they are running on. This is extremely helpful for support.
  • Will automatically close themselves after 720 minutes. This is configurable by a variable called MinutesBeforeClosing as documented below. This reduces lingering sessions running for days with pythonw.exe process running because the user hasn’t closed down the UI.

How we change the license types?

For ArcGIS Pro Licensing type we manipulate the following string values from under the “HKEY_CURRENT_USER\Software\ESRI\ArcGISPro\Licensing” registry key.

  • ARCPRO_AUTH_TYPE
  • SOFTWARE_CLASS
  • SOFTWARE_CLASS_FN
  • SingleUse_Installed

I worked with ESRI Support to ensure we covered off all registry values they have used since the early days of ArcGIS Pro through to the latest version. They are either set to:

  • Viewer for Basic licensing type
  • Editor for Standard licensing type
  • Professional for Advanced licensing type

For ArcGIS Desktop Licensing type, we manipulate the “ESRI_SOFTWARE_CLASS” user environment variable. It is either set to:

  • Viewer for Basic licensing type (formerly ArcView)
  • Editor for Standard licensing type (formerly ArcEditor)
  • Professional for Advanced licensing type (formerly ArcInfo)

Deploying the ArcGIS Pro License Selector UI

It is made up of two scripts.

  • ArcGIS-Pro-License-Selector.cmd
  • ArcGIS_Pro_License_Selector.pyw

Here is the full ArcGIS Pro License and Program Selector (210 downloads)  in the in the form of a zip file.

Use an installation script simply copy them to the “C:\Program Files\ArcGIS\Pro” folder as per the following screen shot:

ArcGIS Pro Folder Structure

The ArcGIS-Pro-License-Selector.cmd batch script is simply used to launch the correct Python environment used by ArcGIS Pro. This is extremely important because if you enable the InstallPythonModules feature as documented below, the Python modules must be installed into the correct Python environment. It was also easier to use a batch script for launching as a Citrix published application.

A Desktop shortcut and/or Start Menu shortcut could also be added to launch the batch script if preferred for a Full Desktop or VDI deployment.

@echo off

SetLocal

Set ScriptLocation=%~dp0
Set ScriptLocation=%ScriptLocation:~0,-1%

CD "%ScriptLocation%"
start "Launching..." "%ProgramFiles%\ArcGIS\Pro\bin\Python\envs\arcgispro-py3\pythonw.exe" "%ScriptLocation%\ArcGIS_Pro_License_Selector.pyw"

:Finish
EndLocal
echo Exiting...
Exit

The ArcGIS_Pro_License_Selector.pyw Python script has many variables you can change to suite your needs.

  • MinutesBeforeClosing : Default is 720. This is set the minutes that this window will stay open for before closing. Set to 0 to disable the timer.
  • AlwaysDefaultToBasic : Default is True. Set to True to default the license selection to Basic. This helps to reset the licensing after each use. Setting it to False will set it to whatever was last selected.
  • AlwaysResetExtensions : Default is True. Set to True to enable the Reset Extensions checkbox. This will clear any Extensions that were previously selected, which helps to reset the licensing after each use.
  • InstallPythonModules : Default is False. Set to True to give the users the ability to install extra Python Modules from the UI. These modules are contained in the PythonModulesToInstall variable array.
  • PythonModulesToInstall : Defaults to ‘pyodbc’,’PyYAML’,’Pillow’. This variable contains a list of extra Python modules for the user to install if the InstallPythonModules variable is set to True. You can add as many modules as needed. The UI will expand in height to allow for the list.
  • AddFileExplorer : Default is True.  This gives the users the ability to launch File Explorer from the UI.
  • EnableScreenResolutionDebugging : Default is False. Setting it to True will enable extra screen resolution (width and height) output in pixels to appear in the UI which has assisted with the geometry of the main window.
################################################################################
# This script is an license sector listbox for ArcGIS Pro
# 
#  Script name: ArcGIS_Pro_License_Selector.pyw
#  Release 1.8
#  Written by Jeremy Saunders (jeremy@jhouseconsulting.com) 17th February 2020
#  Modified by Jeremy Saunders (jeremy@jhouseconsulting.com) 22nd February 2024
# 
################################################################################

# Set these variables

# Set the minutes that this window will stay open for before closing
# Set to 0 to disable the timer
MinutesBeforeClosing = 720

# Set to True to default the license selection to Basic. This helps to reset the
# licensing after each use. Setting it to False will set it to whatever was last
# selected.
AlwaysDefaultToBasic = True

# Set to True to enable the Reset Extensions checkbox. This will clear any
# Extensions that were previously selected, which helps to reset the licensing
# after each use.
AlwaysResetExtensions = True

# Set to True to give the users the ability to install extra Python Modules
InstallPythonModules = False

# This variable contains a list of extra Python modules to install
PythonModulesToInstall = ['pyodbc','PyYAML','Pillow']

# Set to True to give the users the ability to launch File Explorer
AddFileExplorer = True

# Set to True to enable screen resolution (width and height) output in pixels to
# assist with the geometry of the main window.
EnableScreenResolutionDebugging = False

################################################################################

import os, sys, subprocess
if sys.version_info[0] == 3:
    import winreg
    from tkinter import *
    from tkinter import messagebox
else:
    import _winreg as winreg
    from Tkinter import *
    import tkMessageBox

# Derive the installation location and ensure ArcGISPro.exe exists    
try:
    rk = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,r"SOFTWARE\ESRI\ArcGISPro")                        
    tuple = winreg.QueryValueEx(rk,"InstallDir")
    Path = tuple[0]                     
    winreg.CloseKey(rk)
except:
    print ("Registry key not accessible!")

try:
    Path
except NameError:
    print("Could not get the ArcGIS Pro InstallDir from the registry")
    sys.exit(0)

ArcGISProPath = Path + "bin\\ArcGISPro.exe"
exists = os.path.isfile(ArcGISProPath)
if not exists:
    print("The folder path for ArcGIS Pro does not exist")
    sys.exit(0)

# Set registry value
def set_reg(path, name, value):
    try:
        winreg.CreateKey(winreg.HKEY_CURRENT_USER, path)
        registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, 
                                       winreg.KEY_WRITE)
        winreg.SetValueEx(registry_key, name, 0, winreg.REG_SZ, value)
        winreg.CloseKey(registry_key)
        return True
    except WindowsError:
        return False

# Get registry value
def get_reg(path, name):
    try:
        registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0,
                                       winreg.KEY_READ)
        value, regtype = winreg.QueryValueEx(registry_key, name)
        winreg.CloseKey(registry_key)
        return value
    except WindowsError:
        return None

# Delete all registry values under a key
def delete_all_reg_values(path):
    try:
        registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0,
                                       winreg.KEY_ALL_ACCESS)
        while True:
            try:
                value_name = winreg.EnumValue(registry_key, 0)[0]
                winreg.DeleteValue(registry_key, value_name)
            except OSError:
                # When EnumValue raises an exception, it means there are no more values
                break
        winreg.CloseKey(registry_key)
        return True
    except WindowsError:
        return False

path = r"Software\ESRI\ArcGISPro\Licensing"

# Use the %USERPROFILE% as the working folder
workingfolder = os.getenv('USERPROFILE')

# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

# To avoid the GUI from becoming unresponsive due to a "thread lock" issue, we can utilize the after()
# method of the tkinter window to schedule the window to close after a specified duration without
# blocking the main event loop. We define the close_after() function, which uses the after() method to
# schedule the close_window() function to be called after the specified delay. This approach allows the
# main event loop to continue running and keeps the GUI responsive. It is good practice to define these
# outside of the mainloop to ensure it's only called once and not with each iteration of the loop.

def close_window():
    root.destroy()

def close_after(delay):
    root.after(delay, close_window)

# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

root = Tk()
# Preventing the root window to change size
root.resizable(0,0)
# Allowing the root window to change size
#root.resizable(True, True)
root.title("ArcGIS Pro License Selector")
root.iconbitmap(ArcGISProPath)

computername = os.environ['COMPUTERNAME']
l1 = Label(root, text='You are running this on ' + computername ,relief = FLAT, fg = 'gray40' , anchor = "w", font = ('Arial', 8, 'bold'))
l1.grid()

l2 = Label(root, text='Choose the type of license:' ,relief = FLAT, fg = 'gray20' , anchor = "w", font = ('Arial', 10, 'bold'), width = 25)
l2.grid()
choices = ["Basic", "Standard", "Advanced"]
list = Listbox(root, height=3, width=20, selectmode = SINGLE)
list.grid()
for item in choices:
    list.insert(END, item)

foo = IntVar()

# Create a variable to hold the checkbox state and set it to True or False by default based on the AlwaysResetExtensions variable
checkbox_var = BooleanVar(value=AlwaysResetExtensions)

# Create a checkbox and link it to the variable
l3 = Checkbutton(root, text="Reset Extensions", variable=checkbox_var)
l3.grid()

l4 = Label(root, text='Choose the application:' ,relief = FLAT, fg = 'gray20' , anchor = "w", font = ('Arial', 10, 'bold'), width = 25)
l4.grid()

rb1 = Radiobutton(root, text = "ArcGIS Pro", variable=foo, value=0, anchor = "w", width = 25, padx=25)
rb1.grid()

if (InstallPythonModules and PythonModulesToInstall):
    rb2 = Radiobutton(root, text = "Install the following modules:", variable=foo, value=1, anchor = "w", width = 25, padx=25)
    rb2.grid()
    for item in PythonModulesToInstall:
        l4 = Label(root, text="- " + item , justify=LEFT, anchor = "w", padx=45)
        l4.grid(sticky=W)

if AddFileExplorer:
    rb3 = Radiobutton(root, text = "File Explorer", variable=foo, value=2, anchor = "w", width = 25, padx=25)
    rb3.grid()

def view_selected():
    flavour = str("")
    item = int(list.curselection()[0])
    number = foo.get()
    if (number == 0):
        if (item == 0):
            #print("Setting Basic (Viewer) licensing...")
            set_reg(path, 'ARCPRO_AUTH_TYPE', "Viewer")
            set_reg(path, 'SOFTWARE_CLASS', "Viewer")
            set_reg(path, 'SOFTWARE_CLASS_FN', "Viewer")
            set_reg(path, 'SingleUse_Installed', "Viewer")
        if (item == 1):        
            #print("Setting Standard (Editor) licensing...")
            set_reg(path, 'ARCPRO_AUTH_TYPE', "Editor")
            set_reg(path, 'SOFTWARE_CLASS', "Editor")
            set_reg(path, 'SOFTWARE_CLASS_FN', "Editor")
            set_reg(path, 'SingleUse_Installed', "Editor")
        if (item == 2):        
            #print("Setting Advanced (Professional) licensing...")
            set_reg(path, 'ARCPRO_AUTH_TYPE', "Professional")
            set_reg(path, 'SOFTWARE_CLASS', "Professional")
            set_reg(path, 'SOFTWARE_CLASS_FN', "Professional")
            set_reg(path, 'SingleUse_Installed', "Professional")
        if checkbox_var.get():
            delete_all_reg_values(path + r"\Extensions")
        flavour = str(ArcGISProPath)
    if (InstallPythonModules and PythonModulesToInstall):
        if (number == 1):
            for item in PythonModulesToInstall:
                output = ""
                command = [sys.executable, '-m', 'pip', 'install', item]
                if (sys.version_info[0] == 2 or (sys.version_info[0] == 3 and sys.version_info[1] < 5)):
                    try:
                        output = subprocess.check_output(command,universal_newlines=True)
                    except subprocess.CalledProcessError as e:
                        output = e.output
                    except:
                        output = "Unexpected error"
                    #print(output)
                if ((sys.version_info[0] == 3 and sys.version_info[1] >= 5) or sys.version_info[0] > 3):
                    process = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
                    if(len(process.stdout.strip()) > 0):
                        output = process.stdout
                    if(len(process.stderr.strip()) > 0):
                        output = process.stderr
                    if(len(process.stdout.strip()) == 0 and len(process.stderr.strip()) == 0):
                        output = "Unexpected error"
                    #print(output)
    if AddFileExplorer:
        if (number == 2):        
            flavour = str("C:\Windows\explorer.exe /n ,::{20D04FE0-3AEA-1069-A2D8-08002B30309D}")


    if flavour.strip():
        if os.path.exists(workingfolder):
            # Change the current working Directory to a user specific folder before launching
            os.chdir(workingfolder)
            #if sys.version_info[0] == 3:
                # messagebox.showinfo("Working Folder", workingfolder)
            #else:
                # tkMessageBox.showinfo("Working Folder", workingfolder)
        subprocess.Popen(flavour)

btn = Button(root, text = "Launch Selection", command = view_selected, width = 20, font = ('Arial', 10, 'bold') )
btn.grid()

if AlwaysDefaultToBasic:
    list.selection_set(0,)
else:
    current = (get_reg(path, 'SOFTWARE_CLASS_FN'))
    if current:
        if (current == "Viewer"):
            list.selection_set(0,)
        if (current == "Editor"):
            list.selection_set(1,)
        if (current == "Professional"):
            list.selection_set(2,)
        else:
            list.selection_set(0,)

if (MinutesBeforeClosing != 0):
    MinutesBeforeClosingText = StringVar()
    MinutesBeforeClosingText.set('after ' + str(MinutesBeforeClosing) + ' minutes.')
    l4 = Label(root, text= 'This window will automatically close' ,relief = FLAT, fg = 'gray60' , anchor = "w", font = ('Arial', 8, 'bold'))
    l4.grid()
    l5 = Label(root, textvariable = MinutesBeforeClosingText ,relief = FLAT, fg = 'gray60' , anchor = "w", font = ('Arial', 8, 'bold'))
    l5.grid()

# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# According to your system settings such as fonts and screen resolution, 
# you might need to adjust the root.geometry(width,height). It's typically
# the height that becomes a problem with higher resolution (4K) screens.
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

# Getting screen's height in pixels
height = root.winfo_screenheight()
# Getting screen's width in pixels
width = root.winfo_screenwidth()
if EnableScreenResolutionDebugging:
    ScreenResolution = StringVar()
    ScreenResolution.set('width x height = ' + str(width) + ' x ' + str(height) + ' (in pixels)')
    l6 = Label(root, text='Screen Resolution:' ,relief = FLAT, fg = 'gray60' , anchor = "w", font = ('Arial', 8, 'bold'))
    l6.grid()
    l7 = Label(root, textvariable = ScreenResolution ,relief = FLAT, fg = 'gray60' , anchor = "w", font = ('Arial', 8, 'bold'))
    l7.grid()

height = str("220")
if AddFileExplorer:
    height = str(int(height) + 25)
if (InstallPythonModules and PythonModulesToInstall):
    height = str(int(height) + ((len(PythonModulesToInstall) + 1) * 25))
if (MinutesBeforeClosing != 0):
    height = str(int(height) + 50)
if EnableScreenResolutionDebugging:
    height = str(int(height) + 50)
root.geometry("315x" + height)

# Automatically close the window after a specified time
mytimer = int(MinutesBeforeClosing * 60 * 1000)
if (MinutesBeforeClosing != 0):
    close_after(mytimer)

root.mainloop()

Deploying the ArcGIS Desktop License and Program Selector UI

It is made up of two scripts and an icon file:

  • ArcGIS-Desktop-Selector.cmd
  • ArcGIS_Desktop_Selector_en.pyw
  • ArcGIS_Desktop_Product_Icon.ico

Here is the full ArcGIS Desktop License and Program Selector (212 downloads)  in the in the form of a zip file.

Use an installation script simply copy them to the “C:\Program Files (x86)\ArcGIS” folder as per the following screen shot:

ArcGIS Desktop Folder Structure

The ArcGIS-Desktop-Selector.cmd batch script is simply used to launch the correct Python environment used by ArcGIS Desktop. This is extremely important because if you enable the InstallPythonModules feature as documented below, the Python modules must be installed into the correct Python environment. It was also easier to use a batch script for launching as a Citrix published application.

A Desktop shortcut and/or Start Menu shortcut could also be added to launch the batch script if preferred for a Full Desktop or VDI deployment.

A command line argument in the form of MajorVersion.MinorVersion is passed with the batch file to select the correct version of ArcGIS Desktop deployed.

  • For version 10.5.x, we pass the argument 10.5
  • For version 10.7.x, we pass the argument 10.7
  • For version 10.8.x, we pass the argument 10.8
@echo off

SetLocal

:: Set the version of ArcGIS Desktop
Set ArcGISVersion=%1

IF /I "%ArcGISVersion%"=="" GOTO Finish

:: Set the version of Python
Set PythonVersion=27

Set ScriptLocation=%~dp0
Set ScriptLocation=%ScriptLocation:~0,-1%

CD "%ScriptLocation%"
start "Launching..." "%SystemDrive%\Python%PythonVersion%\ArcGIS%ArcGISVersion%\pythonw.exe" "%ScriptLocation%\ArcGIS_Desktop_Selector_en.pyw" "%ArcGISVersion%"

:Finish
EndLocal
echo Exiting...
Exit

The ArcGIS_Desktop_Selector_en.pyw Python script has many variables you can change to suite your needs.

  • VersionAsArgument : Default is True. Pass the Version as an argument. If you set this to False, you must set a version number for the version variable.
  • version : Default is “10.7”. This is only used if the VersionAsArgument variable is set to False. If you want to use it, you must set it to the MajorVersion.MinorVersion of ArcGIS Desktop you have deployed.
  • MinutesBeforeClosing : Default is 720. This is set the minutes that this window will stay open for before closing. Set to 0 to disable the timer.
  • InstallPythonModules : Default is True. This give users the ability to install extra Python Modules from the UI. These modules are contained in the PythonModulesToInstall variable array. Setting it to False will disable this feature.
  • PythonModulesToInstall : Defaults to ‘pyodbc’,’PyYAML’,’Pillow’. This variable contains a list of extra Python modules for the user to install if the InstallPythonModules variable is set to True. You can add as many modules as needed. The UI will expand in height to allow for the list.
  • AddFileExplorer : Default is True.  This gives the users the ability to launch File Explorer from the UI.
  • DeleteNormalmxt : Default is True. This will give the users the ability to delete their Normal.mxt file. It’s important to understand that this is an advanced user feature, so make sure your users understand the ramifications of deleting it. Otherwise set it to False.
  • EnableArcMapStartupLog : Default is False. Set to True to enable the ArcMap startup log. This will write an ArcMap.log file to the current directory and help for debugging if needed.
  • EnableScreenResolutionDebugging : Default is False. Setting it to True will enable extra screen resolution (width and height) output in pixels to appear in the UI which has assisted with the geometry of the main window.
################################################################################
# This script is an license and program sector for ArcGIS Desktop
# 
#  Script name: ArcGIS_Desktop_Selector_en.pyw
#  Release 2.4
#  Written by unknown 12th January 2018
#  Modified by Jeremy Saunders (jeremy@jhouseconsulting.com) 24th February 2024
#
#  Enhanced to run with Python 2.7 and above and 3.6 and above.
#
#  Note that to run ArcGIS Administrator users must have either local Administrator permissions,
#  or full control of the following registry key structures:
#  - HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\ESRI
#  - HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Classes\CLSID\{E6BDAA76-4D35-11D0-98BE-00805F7CED21}
#
# Note that on a 64-bit machine there are two Python environments installed with ArcGIS Desktop.
# For example, for ArcGIS Desktop 10.5 the following two Python environments exist.
# - C:\Python27\ArcGIS10.5
# - C:\Python27\ArcGISx6410.5
# Interestingly the Windows Shell/Context Menu integration uses C:\Python27\ArcGISx6410.5.
# However, as ArcGIS Desktop programs are 32-bit, they use C:\Python27\ArcGIS10.5. It can
# be confusing. So if you are going to use this menu selector to install Python modules,
# it must be run with the "C:\Python27\ArcGISx6410.5\pythonw.exe" interpreter to ensure
# that the modules are correctly installed for use with the ArcGIS Desktop programs.
#
################################################################################

# Set these variables

# Pass the Version as a argument. If you set this to False, you must set a version number
# below
VersionAsArgument = True

# Set the Version of ArcGIS Desktop in the format of major.minor if not passing it as an
# argument
version = "10.8"

# Set the minutes that this window will stay open for before closing
# Set to 0 to disable the timer
MinutesBeforeClosing = 720

# Set to True to give the users the ability to install extra Python Modules
InstallPythonModules = True

# This variable contains a list of extra Python modules to install
PythonModulesToInstall = ['pyodbc','PyYAML','Pillow']

# Set to True to give the users the ability to launch File Explorer
AddFileExplorer = True

# Set to True to give the users the ability to delete the Normal.mxt file
DeleteNormalmxt = True

# Set to True to enable the ArcMap startup log. This will write an ArcMap.log file to the
# current directory
EnableArcMapStartupLog = False

# Set to True to give the users the ability to copy existing Geodatabase Connections
CopyGeodatabaseConnections = False
# C:\Users\<username>\AppData\Roaming\ESRI\Desktop<release#>\ArcCatalog
# os.path.join(os.getenv("APPDATA"), r"ESRI\Desktop"+version+"\ArcCatalog")

# Set to True to give the users the ability to copy existing Coordinate Systems and
# projection files
CopyCoordinateSystems = False
# C:\Users\<username>\AppData\Roaming\ESRI\Desktop<release#>\ArcMap\Coordinate Systems
# os.path.join(os.getenv("APPDATA"), r"ESRI\Desktop"+version+"\ArcMap\Coordinate Systems")
# Note that this includes custom projection files (.prj)

# Set to True to enable screen resolution (width and height) output in pixels to
# assist with the geometry of the main window.
EnableScreenResolutionDebugging = False

################################################################################

import os, sys, subprocess

if VersionAsArgument:
    if (len(sys.argv) == 2):
        version = sys.argv[1]
    else:
        print("Version argument missing")
        sys.exit(0)

if sys.version_info[0] == 3:
    import winreg
    from tkinter import *
    from tkinter import messagebox
else:
    import _winreg as winreg
    from Tkinter import *
    import tkMessageBox

# Derive the ArcGIS Installation directory from the registry
try:
    rk = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,"SOFTWARE\\Wow6432Node\\ESRI\\Desktop" + version)
    tuple = winreg.QueryValueEx(rk,"InstallDir")
    Path = tuple[0]                     
    winreg.CloseKey(rk)
except:
    print("Registry key not accessible!")

ArcGISDesktopPath = Path + "bin\\"
exists = os.path.isdir(ArcGISDesktopPath)
if not exists:
    print("ArcGIS Desktop is not installed")
    sys.exit(0)

try:
    rk = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,"SOFTWARE\\Wow6432Node\\ESRI\\ArcGIS")
    tuple = winreg.QueryValueEx(rk,"InstallDir")
    Path = tuple[0]                     
    winreg.CloseKey(rk)
except:
    print("Registry key not accessible!")

ArcGISDesktopCommonPath = Path + "bin\\"
exists = os.path.isdir(ArcGISDesktopCommonPath)
if not exists:
    print("ArcGIS Desktop is not installed")
    sys.exit(0)

# Set the name of the icon file to use. If not found it will use the default Tkinter icon.
current_directory = os.path.dirname(os.path.abspath(__file__))
iconfile = current_directory  + r"\ArcGIS_Desktop_Product_Icon.ico"
useicon = False
if os.path.exists(iconfile):
    useicon = True

# Derive the path to the Normal.mxt file
normalmxtfile = os.getenv('APPDATA') + r"\ESRI\Desktop" + version + r"\ArcMap\Templates\Normal.mxt"

# Use the %USERPROFILE% as the working folder
workingfolder = os.getenv('USERPROFILE')

# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

# To avoid the GUI from becoming unresponsive due to a "thread lock" issue, we can utilize the after()
# method of the tkinter window to schedule the window to close after a specified duration without
# blocking the main event loop. We define the close_after() function, which uses the after() method to
# schedule the close_window() function to be called after the specified delay. This approach allows the
# main event loop to continue running and keeps the GUI responsive. It is good practice to define these
# outside of the mainloop to ensure it's only called once and not with each iteration of the loop.

def close_window():
    root.destroy()

def close_after(delay):
    root.after(delay, close_window)

# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

root = Tk()
# Preventing the root window to change size
root.resizable(0,0)
# Allowing the root window to change size
#root.resizable(True, True)
root.title("ArcGIS Desktop " + version + " Selector")
if useicon:
    root.iconbitmap(iconfile)

computername = os.environ['COMPUTERNAME']
l1 = Label(root, text='You are running this on ' + computername ,relief = FLAT, fg = 'gray40' , anchor = "w", font = ('Arial', 8, 'bold'))
l1.grid()

l2 = Label(root, text='Choose the type of license:' ,relief = FLAT, fg = 'gray20' , anchor = "w", font = ('Arial', 10, 'bold'), width = 25)
l2.grid()
choices = ["Basic", "Standard", "Advanced"]
list = Listbox(root, height=3, width=20, selectmode = SINGLE)
list.grid()
for item in choices:
    list.insert(END, item)

foo = IntVar()

l3 = Label(root, text='Choose the application:' ,relief = FLAT, fg = 'gray20' , anchor = "w", font = ('Arial', 10, 'bold'), width = 25)
l3.grid()

rb1 = Radiobutton(root, text = "ArcMap", variable=foo, value=0, anchor = "w", width = 25, padx=25)
rb1.grid()

rb2 = Radiobutton(root, text = "ArcCatalog", variable=foo, value=1, anchor = "w", width = 25, padx=25)
rb2.grid()

rb3 = Radiobutton(root, text = "ArcGlobe", variable=foo, value=2, anchor = "w", width = 25, padx=25)
rb3.grid()

rb4 = Radiobutton(root, text = "ArcScene", variable=foo, value=3, anchor = "w", width = 25, padx=25)
rb4.grid()

rb5 = Radiobutton(root, text = "ArcGIS Administrator", variable=foo, value=4, anchor = "w", width = 25, padx=25)
rb5.grid()

if DeleteNormalmxt:
    rb6 = Radiobutton(root, text = "Delete the Normal.mxt file", variable=foo, value=5, anchor = "w", width = 25, padx=25)
    rb6.grid()
    if not os.path.exists(normalmxtfile):
        rb6.configure(state = DISABLED)

if (InstallPythonModules and PythonModulesToInstall):
    rb7 = Radiobutton(root, text = "Install the following modules:", variable=foo, value=6, anchor = "w", width = 25, padx=25)
    rb7.grid()
    for item in PythonModulesToInstall:
        l4 = Label(root, text="- " + item , justify=LEFT, anchor="w", padx=45)
        l4.grid(sticky=W)

if AddFileExplorer:
    rb8 = Radiobutton(root, text = "File Explorer", variable=foo, value=7, anchor = "w", width = 25, padx=25)
    rb8.grid()

def get_item_selected(slecteditem):
    if (slecteditem == 0):   
        os.environ['ESRI_SOFTWARE_CLASS']='Viewer'
    if (slecteditem == 1):        
        os.environ['ESRI_SOFTWARE_CLASS']='Editor'
    if (slecteditem == 2):        
        os.environ['ESRI_SOFTWARE_CLASS']='Professional'

def view_selected():
    flavour = str("")
    item = int(list.curselection()[0])
    number = foo.get()
    if (number == 0):
        get_item_selected(item)
        if not EnableArcMapStartupLog:
            flavour = str(ArcGISDesktopPath + "\ArcMap.exe")
        else:
            flavour = str(ArcGISDesktopPath + "\ArcMap.exe" + " /log")
    if (number == 1):
        get_item_selected(item)
        flavour = str(ArcGISDesktopPath + "\ArcCatalog.exe")
    if (number == 2):
        get_item_selected(item)
        flavour = str(ArcGISDesktopPath + "\ArcGlobe.exe")
    if (number == 3):
        get_item_selected(item)
        flavour = str(ArcGISDesktopPath + "\ArcScene.exe")
    if (number == 4):
        get_item_selected(item)
        flavour = str(ArcGISDesktopCommonPath + "\ArcGISAdmin.exe")
    if DeleteNormalmxt:
        if (number == 5):
            if os.path.exists(normalmxtfile):
                os.remove(normalmxtfile)
            if not os.path.exists(normalmxtfile):
                rb6.configure(state = DISABLED)
    if (InstallPythonModules and PythonModulesToInstall):
        if (number == 6):
            for item in PythonModulesToInstall:
                output = ""
                command = [sys.executable, '-m', 'pip', 'install', item]
                if (sys.version_info[0] == 2 or (sys.version_info[0] == 3 and sys.version_info[1] < 5)):
                    try:
                        output = subprocess.check_output(command,universal_newlines=True)
                    except subprocess.CalledProcessError as e:
                        output = e.output
                    except:
                        output = "Unexpected error"
                    #print(output)
                if ((sys.version_info[0] == 3 and sys.version_info[1] >= 5) or sys.version_info[0] > 3):
                    process = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
                    if(len(process.stdout.strip()) > 0):
                        output = process.stdout
                    if(len(process.stderr.strip()) > 0):
                        output = process.stderr
                    if(len(process.stdout.strip()) == 0 and len(process.stderr.strip()) == 0):
                        output = "Unexpected error"
                    #print(output)
    if AddFileExplorer:
        if (number == 7):        
            flavour = str("C:\Windows\explorer.exe /n ,::{20D04FE0-3AEA-1069-A2D8-08002B30309D}")

    if flavour.strip():
        if os.path.exists(workingfolder):
            # Change the current working Directory to a user specific folder before launching
            os.chdir(workingfolder)
            #if sys.version_info[0] == 3:
                # messagebox.showinfo("Working Folder", workingfolder)
            #else:
                # tkMessageBox.showinfo("Working Folder", workingfolder)
        subprocess.Popen(flavour)

btn = Button(root, text = "Launch Selection", command = view_selected, width = 20, font = ('Arial', 10, 'bold') )
btn.grid()

list.selection_set(0,)

l5 = Label(root, text='Basic ArcGIS Desktop formerly ArcView' ,relief = FLAT, fg = 'gray60' , anchor = "w", font = ('Arial', 8, 'bold'))
l5.grid()
l6 = Label(root, text='Standard ArcGIS Desktop formerly ArcEditor' ,relief = FLAT, fg = 'gray60' , anchor = "w", font = ('Arial', 8, 'bold'))
l6.grid()
l7 = Label(root, text='Advanced ArcGIS Desktop formerly ArcInfo' ,relief = FLAT, fg = 'gray60' , anchor = "w", font = ('Arial', 8, 'bold'))
l7.grid()

if (MinutesBeforeClosing != 0):
    MinutesBeforeClosingText = StringVar()
    MinutesBeforeClosingText.set('after ' + str(MinutesBeforeClosing) + ' minutes.')
    l8 = Label(root, text= 'This window will automatically close' ,relief = FLAT, fg = 'gray60' , anchor = "w", font = ('Arial', 8, 'bold'))
    l8.grid()
    l9 = Label(root, textvariable = MinutesBeforeClosingText ,relief = FLAT, fg = 'gray60' , anchor = "w", font = ('Arial', 8, 'bold'))
    l9.grid()

# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# According to your system settings such as fonts and screen resolution, 
# you might need to adjust the root.geometry(width,height). It's typically
# the height that becomes a problem with higher resolution (4K) screens.
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

# Getting screen's height in pixels
height = root.winfo_screenheight()
# Getting screen's width in pixels
width = root.winfo_screenwidth()
if EnableScreenResolutionDebugging:
    ScreenResolution = StringVar()
    ScreenResolution.set('width x height = ' + str(width) + ' x ' + str(height) + ' (in pixels)')
    l10 = Label(root, text='Screen Resolution:' ,relief = FLAT, fg = 'gray60' , anchor = "w", font = ('Arial', 8, 'bold'))
    l10.grid()
    l11 = Label(root, textvariable = ScreenResolution ,relief = FLAT, fg = 'gray60' , anchor = "w", font = ('Arial', 8, 'bold'))
    l11.grid()

height = str("375")
if AddFileExplorer:
    height = str(int(height) + 25)
if DeleteNormalmxt:
    height = str(int(height) + 25)
if (InstallPythonModules and PythonModulesToInstall):
    height = str(int(height) + ((len(PythonModulesToInstall) + 1) * 25))
if (MinutesBeforeClosing != 0):
    height = str(int(height) + 50)
if EnableScreenResolutionDebugging:
    height = str(int(height) + 50)
root.geometry("320x" + height)

# Automatically close the window after a specified time
mytimer = int(MinutesBeforeClosing * 60 * 1000)
if (MinutesBeforeClosing != 0):
    close_after(mytimer)

root.mainloop()

Enjoy!

Jeremy Saunders

Jeremy Saunders

Technical Architect | DevOps Evangelist | Software Developer | Microsoft, NVIDIA, Citrix and Desktop Virtualisation (VDI) Specialist/Expert | Rapper | Improvisor | Comedian | Property Investor | Kayaking enthusiast at J House Consulting
Jeremy Saunders is the Problem Terminator. He is a highly respected IT Professional with over 35 years’ experience in the industry. Using his exceptional design and problem solving skills with precise methodologies applied at both technical and business levels he is always focused on achieving the best business outcomes. He worked as an independent consultant until September 2017, when he took up a full time role at BHP, one of the largest and most innovative global mining companies. With a diverse skill set, high ethical standards, and attention to detail, coupled with a friendly nature and great sense of humour, Jeremy aligns to industry and vendor best practices, which puts him amongst the leaders of his field. He is intensely passionate about solving technology problems for his organisation, their customers and the tech community, to improve the user experience, reliability and operational support. Views and IP shared on this site belong to Jeremy.
Jeremy Saunders
Jeremy Saunders

Previous post:

Next post: