How to switch to an external application

Hello. Maybe someone can point me to the right direction. I have a Goto button on one of my screens and I am simply trying to switch to an external running application on button press. I come from the vb world and in vb we simply ran the appActivate command to switch to another program. For example ... AppActivate "Microsoft Word" and the screen would simply switch over to that application. I am aware of the system.util.execute command where I can run the application, but how do you switch to it if it is already open? Thanks.

1 Like

Since Ignition is cross-platform and uses Python/Java you're not going to have the tight Windows integration you had with VB scripts.

You might be able to get something working by writing, for example, an AutoHotkey script, that was capable of doing this kind of app/window find/hide/show, and then executing that script with system.util.execute. (just a theoretical example, I don't know for sure if AHK and system.util.execute is the answer)

I had the night off, so I messed around with this problem a bit. I didn't find a way to do what you are asking without adding a custom module or using an external file, but I did get this script working that identifies whether or not an app is running and returns the relevant process ID. Perhaps it will help you to achieve your goal:

def isAppRunning(app):
	from java.lang import Runtime
	from java.io import InputStreamReader
	from java.io import BufferedReader
	processList= BufferedReader(InputStreamReader(Runtime.getRuntime().exec("tasklist.exe").getInputStream()))
	try:
		while processList.readLine() != None:
			if app in processList.readLine():
				print 
				print 'Name			PID Session'
				print processList.readLine()
				processList.close()
				return True
	except:
		pass
	processList.close()
	print
	return False
isAppRunning("chrome")
isAppRunning("notepad")
isAppRunning("supercalifragilisticexpialidocious")
isAppRunning("edge")

The preceding code produces the following output:
image

3 Likes

Kevin Thank You. You steered me in the right direction - Keyword "AutoHotkey script". This led me to discover the 'pywin32' extension which allows access to MS Win32 API which I am experimenting right now.

1 Like

pywin32 will most likely not work inside Ignition, since Ignition uses Jython and doesn't have c bindings.

That said, if you write a Python 3 script to do this outside of Ignition, you can invoke it from inside Ignition. Check out this guide, and for something as simple as this I'd recommend the " Can I run Python 3 code from Ignition?" - Approach 2 section.

1 Like

Justin very much appreciate you taking the time to support. I will take a closer look at your script. I just literary solved this a minute ago, but in a slightly goofy way. I will try your solution, as it seems a bit cleaner than mine. My solution requires me to install Python v3 software and the "pywin32" Win32 API extension. I then created a python script sitting on the outside of Ignition labeled "AppActivate.py" which includes scripting that does exactly what the AppActivate command does. Then in Ignition, my GoTo button uses the system.util.execute command to run the "AppActivate.py" script. The button script looks like: system.util.execute(['py','D:\myFolder\AppActivate.py']) The reason I dont like what I did, is because now I have to support Python v3, pywin32 extension and the .py script I developed. Your solution appears to be using already installed Java functionality making it a cleaner solution. I will try it out and advice. Thank You.

Kevin McClusky! What a surprise. If you don't remember me its been at least 10 years since you 1st supported me. Believe it or not some of your support code is still lingering in my projects. I just responded to Justin, and my solution is exactly what you described. I had to find out the hard way that pywin32 would not work inside ignition. That's what prompted me to create an outside .py script which I call from my Ignition Button. I will review your "Guide" shortly. Thank you very much for your support.

2 Likes

His script only locates a PID that might be the app you're looking for. It doesn't do the part that presumably requires Windows API - activate that app/window.

Oh, you are the Yiotis! Good to hear from you, yes I remember you, quite a walk down memory lane. Glad to see you on here. If you make it out to ICC this year, look me up, and let's catch up!

You are correct. The script works, but returns a True/False which doesn't solve my issue. Here's the 'AppActivate.py' script that does work. As Kevin McClusky pointed out earlier, this won't work inside Ignition. I will not take credit for the below, as I found examples online. The script below is called from an Ignition button and will switch screen to the 3rd party program and also SendKeys.

AppActivate.py (792 Bytes)

Thanks.

Sounds Good. This might be the excuse to get me out to ICC. Thanks Again.

Justin, I like what you did here. The code works great. Though as you also mentioned its missing the piece where it bounces over to the app. You're close. I solved my problem using outside ignition scripting which I don't like. If you have an epiphany and solve this that would be great. Thanks again.

1 Like

I found a way to accomplish this without having to modify ignition or use outside scripting. The code below checks to see if a designated app is open, and if so, it dynamically creates a temporary script in the user's cache file that does the work of swapping windows. Once the file is written to cache, the code below doesn't rewrite it again, but rather, just executes the file directly each time the script is called.

Here is the code with "chrome" designated as the target app:

import os
import getpass
from java.lang import Runtime
from java.io import InputStreamReader
from java.io import BufferedReader
def isAppRunning(taskName):
	processList= BufferedReader(InputStreamReader(Runtime.getRuntime().exec("tasklist.exe").getInputStream()))
	testLines = ""
	while testLines != None:
		if taskName in testLines:
			processList.close()
			return True
		testLines = processList.readLine()
	processList.close()
	return False
taskName = "chrome"
if isAppRunning(taskName):
	username = getpass.getuser()
	path = "C:/Users/" + username + "/.ignition/cache/tempFile.bat"
	if not os.path.exists(path):
		scriptLines = [
			"@echo WScript.CreateObject(\"WScript.Shell\").AppActivate(WScript.Arguments.Item(0))>%tmp%\switch.vbs",
			"%tmp%\switch.vbs \""+taskName+"\"",
			"exit"]
		with open(path, "w") as tf:
			for line in scriptLines:
				tf.write(line + "\n")
	system.util.execute([path])

Here is the result:

Initially, I did this as proof of concept to see if this approach was even work pursuing, and I had no intentions of actually using a batch file in the end product because I didn't like the idea of an annoying black popup appearing and disappearing. However, to my surprise, and as you can see in the video, the popup never appears [at least not on my system].

Obviously, there is nothing in this code that will specify which instance of a target app to give focus to, so if a user has multiple instances of a target app open, your guess is as good as mine as to which one will gain focus.

Edit: Corrected a flaw in the while loop design that was causing half the tasklist lines to be consumed without being checked and that was creating an occasional None type object error that required exception handling. The no longer needed exception handling has been removed.

3 Likes

Justin. So you've had an epiphany! I looked through your code and I see what you're doing. I think your approach using a temporary script via cache is genius. I'll be frank, I tried using the AppActivate via PowerShell but was unsuccessful thus moved forward with doing it via Python scripting. Essentially you are using PS which is built-into windows eliminating additional installs. Now, to the problem. I played a bit with your function but have been unsuccessful returning the task list. As you can see in the image below it returns a blank. The calculator is running in the background. Maybe OS specific. I am on Win Server 2019; ill try and test this a bit more tomorrow and also try it on Win10. Thank you again for giving some time to my issue.

One last thing, I am using Ignition 8.1.22 I might have filed this issue under 7.9.xx I doubt this is an Ignition versioning issue.

Line 18 and 19 in your script, you’re using a variable called “app” which is undeclared when you should be using “appName” so you’re throwing an error and printing “return false”

2 Likes

It looks like your code could be breaking on the line that says print app
I don't see where app is defined anywhere.

Dustin and I saw the same thing at the same time. The confusion was probably caused by the fact that the variable defined in my function was not the same as the variable I was passing into it. I've corrected this in my original example, and I've made a couple of other minor improvements as well. I also updated the video with a test on notepad and chrome. The script worked fine on both of those apps on my system as well. I don't know what your target app is, so I can't test it directly.

Edit: I realized that there was a flaw in the while loop design that could have also been a contributing factor to the problem you noted. Half of the task list lines were being consumed by the while statement without actually being checked. This flaw has now been corrected in the above example code.

1 Like

Hi Justin. I just now got around to testing a bit further. No doubt the script works, but there is an issue I am experiencing and am not sure how to fix. I can get the script to bounce over to the 'Calculator' or 'Chrome' no problem. It took me about an hour of testing to realize that the task list produced by your script is not the same as the task list in windows. Your script has many items from the windows task list but is missing quite a few programs. The program that I am interested is one of the many that doesn't show up in the task list produced by your script. Its almost like some programs are filtered out from the "While Loop". See attached task list from the test computer. Thanks.

It should be exactly the same. This was a problem that I had to fix in my original example, but I didn't initially notice it because I had multiple instances of everything open. Every time that processList.readLine() is scanned in the thread, a line in the task list is read, so if there is a while processList.readLine() != None:, then one line of the task list is read and not ever looked at again during that looping session. Furthermore, if there is a print processList.readLine() , then another line of the task list is read and not looked at again. The readLine() method has to be invoked only ONCE per loop to avoid potentially missing data. The only way to accomplish this is to assign the returned value to a variable that is used everywhere else to get the script work done:
Example:

	testLines = ""
	while testLines != None:
		if taskName in testLines:
			processList.close()
			return True
		testLines = processList.readLine()
	processList.close()
	return False

Notice how the variable testLines now controls the loop instead of the readLine() method. Moreover, notice how the variable is assigned a new value after the if statement. In this way, if the method returns None, there is no risk of the if statement causing a None type object not iterible exception.

Added:

Illustration Code:
from java.lang import Runtime
from java.io import InputStreamReader
from java.io import BufferedReader
#===============================================================
#Case 1: While loop and if statement both include processList.readline()
print "processList.readLine() used TWICE per loop"
methodInvokedCount = 0
ifStatementEvaluatedCount = 0
processList= BufferedReader(InputStreamReader(Runtime.getRuntime().exec("tasklist.exe").getInputStream()))
while processList.readLine() != None:
	methodInvokedCount += 1
	try:
		if "TargetApp" in processList.readLine():
			processList.close()
	except:
		print "A none type exception occured"
	methodInvokedCount += 1
	ifStatementEvaluatedCount += 1
processList.close()
print "Task List Lines Read = " + str(methodInvokedCount)
print "Lines Evaluated by the if statement = " + str(ifStatementEvaluatedCount)
print

#===============================================================
#Case 2: processList.readLine() is invoked once when assigned to a variable
#The variable is used in the while loop and if statement
print "processList.readLine() used only ONCE per loop"
methodInvokedCount = 0
ifStatementEvaluatedCount = 0
processList= BufferedReader(InputStreamReader(Runtime.getRuntime().exec("tasklist.exe").getInputStream()))
testLines = ""
while testLines != None:
	if "TargetApp" in testLines:
		processList.close()
	ifStatementEvaluatedCount += 1
	testLines = processList.readLine()
	methodInvokedCount += 1
processList.close()
print "Task List Lines Read = " + str(methodInvokedCount)
print "Lines Evaluated by the if statement = " + str(ifStatementEvaluatedCount)

Illustration Output:

>>> 
processList.readLine() used TWICE per loop
A none type exception occured
Task List Lines Read = 280
Lines Evaluated by the if statement = 140

processList.readLine() used only ONCE per loop
Task List Lines Read = 280
Lines Evaluated by the if statement = 280
>>> 
1 Like

That being said, is this check even necessary? I don't believe the batch file will cause an exception if the intended task doesn't have an instance. If I'm not mistaken, the command script will simply do nothing if the target app happens to be closed.

I suppose that if you intend to open the program if it is not already opened, then the check would probably be critical, but otherwise, it could be deleted entirely, and the program would still generate the swapping script and swap the windows if an instance of the target app existed.

1 Like

Hi Justin. The script works. It didn't work for me initially. What I discovered is the substring if taskName in testLines result is not perfect. In my case it found 2 items and returned the 1st item (something you mentioned early on). What solved this, was simply being more specific with my parameter name. Instead of using taskName = "DisplayClient", I now use taskName = "DisplayClient.exe". That said, this resolves my issues with the IsAppRunning function. Part #2 where you are creating a temp file also works, but the temp file never gets deleted or appended and retains the taskName when the file was 1st created. If the taskName is changed, say from taskName = "chrome" to taskName = "DisplayClient.exe" and re-run IsAppRunning, it will always default to chrome since that is what was written during file creation. In my case, I believe the batch file should always be deleted at the os.path.exists(path) position of the script, forcing a new version to be created every time. Obviously when the cache is deleted, the batch file will also be deleted. For now I am manually deleting the batch file. With that said, I am still unable to switch over to my app. I am wondering if the PS script is switching over to the 1st occurrence which happens to be a memory resident app, rather than the 2nd occurrence which is the app I am after. In my case the 2 tasks that contain a similar description are DisplayClient.exe and DisplayClientManager.exe. Anyways, coffee break for now... I'll experiment with the 2nd portion of the script shortly. Thanks.

1 Like