Vision MultiMonitor issues

I’ve been running into a variety of issues with multi monitor support in Vision Clients. I’m not happy with the state of multimonitor support in general in Ignition, but I’ve been able to get a quad monitor desktop to at least display a distinct main window and docked navigation window on each of the four desktops.

Right now, the biggest issue I can’t seem to work through is how to consistently determine which monitors are used if I’m not using all the monitors on a workstation for Ignition. We have three identical desktops with an identical quad monitor setup (same computer model, same monitors, same arrangement of monitors in windows (2x2 matrix with top left as 1, going counter clockwise to 4), same cabling of monitors.

I’ve setup a script to launch the client on 2 of the four monitors with a screen and navigation tab on each. When I launch the Vision client on the three nodes I get three different responses. On one machine, the two vision windows are the top two monitors. One the next they are the left two monitors, on the final machine they are the right two monitors. I want to launch the clients in fullscreen, with no max/min controls, so I don’t have any way at runtime to move them to different monitors.

How can I either explicitly set the monitors in the windows setup (or somewhere in the depths of Java), or how can I script the startup to only use the two monitors I want on startup? At this point, I would be at least semi happy with the systems acting the same, but my goal would be to be able to explicitly specify which monitor and which windows.

Our operators use Ignition and several other applications, so I don’t want to designate the entire quad setup as Ignition clients currently.

Thanks,
Brian

It’s not a great solution (there are no great solutions in this space; I think I worked with the support rep who was helping you), but one option would be to avoid trying to manage this in Ignition at all. You could pretty easily kick off an AutoHotkey script (via system.util.execute in a startup script) that ‘forces’ your Vision windows in to the right location. As a bonus, AHK ties much more tightly into Windows than Java, so your monitor indices should align with Window’s.

@PGriffith Interesting concept. Any more tips to get started down that path? I’ve heard of AHK, but don’t have any personal experience with it.

We have a table with the node name, and a sub table for Monitor name, window name.
We query against the table in the startup script, if records exist then we launch instances to the monitor ID and open the listed window.

You can get the monitor ID with system.gui.getCurrentDesktop()
I just have a test window with a button and label on it.
The code in the button is
event.source.parent.getComponent('Label').text = str(system.gui.getCurrentDesktop())

1 Like

The AutoHotkey forums probably have prior art you can crib off (Googling "autohotkey move window to monitor" seems fruitful) but it looks like you can use SysGet to retrieve information:

And then WinMove to actually do something:

Thanks, I have been poking around their forums as well. I think I’ll start with something more manual like https://autohotkey.com/board/topic/63931-another-multi-monitor-window-mover/ with a goal towards automating it at startup down the line. Most of our stations are up 24x7, so if I can get the window moved to the right place I can at least get going with out too much operator angst.

Almost word for word on how we have our systems setup. We use a client dataset with the hostname and monitor number and then a fall through case for a default where the hostname isn’t found. Works like a champ.

2 Likes

I know this is a really old post. But I'm finally getting to testing on a quad setup and all our navigation stuff needs to be reworked to allow multi-monitors. Can you give me more details on your client dataset? Do you mean you just have a vision client tag with rows and columns? are you willing to share the start up script that works through the table? I'm using a start up script that I found elsewhere on the forum to load up the 4 desktops and so far they just kind of open on whatever screen they feel like...

Yep.

When I get into the office tomorrow I'll gather stuff up and send it over.

OK... I think I cleaned everything up enough for you. This is a small part of a larger scripting backend, so I had to strip out some overhead stuff, but I tested it and it is working.

Just to make sure you are understanding how we do things with the software we deploy.

Our screens that we have developed have three components. A header, a slide out navigation dock on the west side of the screen, and then a main window.

The header window has a custom property named MainDesc that is populated when a new main window is opened on that desktop.

This is the dataset simply called MultiMonitor in the client tags.

"#NAMES"
"Node","Header","Navigation","Monitor1","Monitor1Params","Monitor2","Monitor2Params","Monitor3","Monitor3Params","Monitor4","Monitor4Params","Setup"
"#TYPES"
"str","str","str","str","str","str","str","str","str","str","str","str"
"#ROWS","2"
"Default","Header/Top Header","Navigation/Popout Navigation Left","Main Windows/Analogs","","","","","","","",""
"ESPLAPBRS2021","Header/Top Header","Navigation/Popout Navigation Left","Main Windows/Motors","","Main Windows/Monitor 2","","","","","",""

Node is the client node as defined from system.net.getHostName()
Header is the string path to the the main header bar at the top.
Navigation is the string path to the docked slide out window.
Monitor# is the string path to the window to display on monitor #
Monitor#Params is a JSON string that is passed to the Monitor# as the parameters for that window (optional)
Setup is not used at this moment.

Now for the code. This is called either from the client startup, or we have a welcome popup that the operator acknowledges.

import sys
from java.lang import Throwable,System
import java.lang.Exception
from java.awt import GraphicsEnvironment

def loadHome():
	logger = system.util.getLogger('Client.Startup.loadHome')
	mmTagPath = '[Client]MultiMonitor'
	
	#Load up the java graphic environment so we can get details about the screens
	#Develop a list of tuples representing each of the screens on the client system. Tuple is index,monitor number,width,height,x,y
	#screen index is 0 based, monitor number is 1 based
	ge = GraphicsEnvironment.getLocalGraphicsEnvironment()
	screens = [[i,i+1,gd.getDefaultConfiguration().getBounds().width,gd.getDefaultConfiguration().getBounds().height,gd.getDefaultConfiguration().getBounds().x,gd.getDefaultConfiguration().getBounds().y] for i,gd in enumerate(ge.getScreenDevices())]
	logger.trace('screens reported from java: {}'.format(screens))
	
	currentNode = system.net.getHostName()
	
	#Grab the multiMonitor client tag
	tagPaths = [mmTagPath]
	tagVals = system.tag.readBlocking(tagPaths)
	multiMonitorDataSet = system.dataset.toPyDataSet(tagVals[0].value)

	#Now search for this node
	nodeColIdx = multiMonitorDataSet.getColumnIndex('Node')
	nodeColumn = multiMonitorDataSet.getColumnAsList(nodeColIdx)
	multiMonitorSetup = None
	
	try:
		nodeIdx = nodeColumn.index(currentNode)
		logger.debug('found node {} as index {}'.format(currentNode,nodeIdx))
		multiMonitorSetup = dict(zip(multiMonitorDataSet.getColumnNames(),multiMonitorDataSet[nodeIdx])) #Create a dict of the multi-monitor setup. Easier to manipulate.
	except:
		logger.debugf('Node {} not found, trying for default'.format(currentNode))
		try:
			nodeIdx = nodeColumn.index('Default')
			logger.debug('Found default node')
			multiMonitorSetup = dict(zip(multiMonitorDataSet.getColumnNames(),multiMonitorDataSet[nodeIdx]))
		except:
			logger.debug('Exception when looking for the default setup.')
			
	#If the current node is found in the multimonitor list then open the screens
	if multiMonitorSetup is not None:
		logger.trace('multiMonitorSetup: {}'.format(multiMonitorSetup))
		logger.debug('setup found, loading')
		
		#For each of the screens in the screens list, loadup the monitor setup from the multiMonitorSetup
		for screen in screens:
			__loadScreen(multiMonitorSetup,screen)

def __loadScreen(setup,screen):
	"""
	Called internally to load a monitor
	setup (dictionary): The monitor setup
	screen (tuple): tuple representing the screen index, monitor number, width and height
	
	"""
	logger = system.util.getLogger('Client.Startup.__loadScreen')
	
	#Unpack the tuple
	index,monitorNum,width,height,x,y = screen
	
	#check if there is a mainWindow
	if setup.get('Monitor{}'.format(monitorNum)) is not None and len(setup.get('Monitor{}'.format(monitorNum))) > 0:
		headerPath = setup.get('Header')
		navPath = setup.get('Navigation')
		mainPath = setup.get('Monitor{}'.format(monitorNum))
		mainParams = system.util.jsonDecode(setup.get('Monitor{}Params'.format(monitorNum)))
		handleName = 'Monitor {}'.format(monitorNum)
		
		#If the headerPath in globals isn't set then set it
		if system.util.getGlobals().get('headerPath') is None:
			system.util.getGlobals()['headerPath'] = headerPath
			
		desktop = system.nav.desktop()
		if index > 0:
			if handleName not in system.gui.getDesktopHandles():
				system.gui.openDesktop(screen=index, handle=handleName, width=width, height=height)
			desktop = system.nav.desktop(handleName)	
		
		if desktop is not None:
			#Open the header	
			if headerPath is not None and len(headerPath) > 0:
				header = desktop.swapTo(headerPath,{'DesktopHandle':handleName})
				
			if navPath is not  None and len(navPath) > 0:
				#Open the navigation. Note... this is assumed to be a east/west window
				navWin = desktop.swapTo(navPath,{'DesktopHandle':handleName})
				#If not a docked window then center
				if navWin.dockPosition <=0:
					navWin.setLocation(navWin.getX(),(height-navWin.height)/2)
			if mainPath and len(mainPath) > 0:
				params = {}
				#check the params
				if mainParams is not None and isinstance(mainParams,dict):
					#params exist and it is a dictionary. So merge them into the existing params
					params = {key: value for d in (params, mainParams) for key, value in d.items()}
				mainWin = desktop.swapTo(mainPath,params)
				if header:
					try:
						header.getRootContainer().MainDesc = mainWin.title
					except:
						#At least we tried
						pass
		else:
			logger.fatal('Error getting desktop')
1 Like

Wow, Thanks! That looks much more robust than my current 10 lines of code. The java stuff is over my head, but I'll get to work on it. We have the same setup with a docked header, west navigation tree that slides in and out and a main screen.

1 Like

If you have any questions feel free to ask away.

Thanks! I'm finally getting to this. I'm stuck on this line:

header = desktop.swapTo(headerPath,{'DesktopHandle':handleName})

It's complaining because my top header window doesn't have a parameter called DesktopHandle. Is that a custom property that you have on all your windows?

Also struggling to find how to see the logs. I just added a bunch of "print" statements for now.

It's almost working, it opens two main screens, but the second one doesn't get a header bar.

Yep custom property that was on there to track handles for debugging. That's safe to remove.