Page 1 of 1

Advice: Abstracting a Program GUI as a Class?

Posted: 22 Jun 2020, 09:39
by sharonhuston
I'm an instructional designer on a team of 14 designers. Most of the team uses Adobe Captivate all day, every day. We've made some real productivity gains using AHK with Captivate, but I'm always looking for improvements to the process. Sadly, Captivate isn't easy to automate. It doesn't use COM or Microsoft UI. And the control names change every time the program starts. So we're using a ton of ImageSearch and MouseClick statements, and lately it seems like we're solving the same problems over and over. I feel like I'm spending too much time looking for a snippet of code I wrote two years ago, or updating an old process in one script because I now have a more reliable way to code the problem. Then of course I have to consider every other script that might benefit from that tweak . . .

So I'm thinking about abstracting the Captivate GUI as a class. I'm curious if anyone has any experience in this, and any thoughts to share. Is this a complete waste of time? Is there a reason it's a bad idea? Are there any major roadblocks you can think of? I've posted my starting code below. Right now I'm thinking every panel and every dialog will be a subclass, but I'm open to suggestions.

A few cravats:
  • Everyone on the team uses an identical Captivate workspace, so all buttons and menu options are in the same location. We also have a script to reset the workspace, and almost all code starts with a reset to give us a blank slate.
  • Our team has me, an intermediate programmer, as the primary developer. Several beginning programmers help out occasionally, so it's more important for code to be readable than it is for code to be compact. :D

Thank you!

Code: Select all

Class CpClass 
{
	class Properties
	{
		static x:=1665
		static y:=160
		select() 
		{
			MouseClick, left, this.x, this.y
			Sleep, 1200
		}
		; The Properties panel has two states that determine which controls show.  
		; If an object on the stage is selected the panel shows one set of buttons.
		; We'll call this the primary state since it's used the most.  
		; If no object is selected, the panel shows a different set of buttons.
		; This is the secondary state.
		; A few objects (mainly the hamburger menu in the upper corner, and the Style/Options/Actions buttons) are available in both states.
		; Methods for these objects are omni-state methods.
		; Utility methods are used by both states for things like error checking and state detection.
		
		; -------------------------- PRIMARY STATE METHODS --------------------------
		txtfieldObjectName(newName) 
		{
			; won't be available unless an object is selected
			this.objectSelected()
			MouseClick, left, 1695, 195
			spamDelete()  ; this is a function in a library. It removes the existing value from the control.
			Send %newName%
			Sleep, 300
			Send {tab}
		}
		
		; -------------------------- SECONDARY STATE METHODS --------------------------		
		menuMasterSlide(masterName) 
		{
			objectNotSelected()	
			; OK, this one is going to be a pain. The menu is not mouse-navigable.
			; Will have to get all the text on the control, and click whatever 
			; matches the slide type the user wants to select.
		}
		btnResetMasterSlide() 
		{
			objectNotSelected()
			MouseClick, left, 1780, 390
		}
		menuTheme(option) 
		{
			objectNotSelected()
			MouseClick, left, 1735, 235	
			; INCOMPLETE
		}
		btnActions() 
		{
			objectNotSelected()
			MouseClick, left, 1775, 425
		}
		menuOnEnter(option) 
		{
			; This will be a pain because unavailable objects are greyed out, and are skipped over when pressing the down key.
			; We can't reliably navigate via kbd.
			; Instead we need to get the text on the controls.
			objectNotSelected()
			MouseClick, left, 1655, 475	
			; INCOMPLETE			
		}
		menuOnExit(option) 
		{
			objectNotSelected()
			MouseClick, left, 1655, 530	
			; INCOMPLETE
		}
		; -------------------------- OMNI STATE METHODS --------------------------
		menuAccessibility(option:=1) 
		{
			; This menu has 2 options if the user DOES NOT have an object selected.  
			; It has ONE option is an object is selected.
			; In the no-obj state, it's also counter-intuitive:
			;	one down arrow opens the 2nd menu option.
			; 	two down arrows open the 1st option.
			MouseClick, left, 1905, 195
			objectSelected := superSearch("properties_objectSelected.png", 15, false, false)  ; superSearch is a custom ImageSearch function. 
			if (objectSelected[1] = 0) 
			{
				Send {down}{enter}
			} else 
			{
				if (option = 1) 
				{
					Send {down 2}{enter}
				} else 
				{
					if (option = 2) 
					{
						Send {down}{enter}
					}
					else 
					{
						msg := simpleGui("Error", "This hamburger menu only has two options.  Reloading.", 250, 100)   ; simpleGui is a custom message-box like function that centers the message on the screen where the mouse is located.
					}
				}
			}			
		}
		btnStyle() 
		{
			objectSelected := superSearch("properties_objectSelected.png", 15, false, false)
			if (objectSelected[1] = 0) 
			{
				MouseClick, left, 1730, 465
			} else 
			{
				MouseClick, left, 1680, 425
			}
		}
		btnOptions() 
		{
			objectSelected := superSearch("properties_objectSelected.png", 15, false, false)
			if (objectSelected[1] = 0) 
			{
				MouseClick, left, 1730, 465
			} else 
			{
				MouseClick, left, 1680, 425
			}
		}
		; -------------------------- UTILITY METHODS --------------------------
		objectSelected() 
		{
			; Make sure the user has an object selected.
			; Selected/not selected state radically changes the panel
			objectSelected := superSearch("properties_objectSelected.png", 15, false, false)
			if (objectSelected[1] != 0) 
			{
				msg := simpleGui("Error", "You don't have an object selected.  Reloading.", 250, 100)
				WinWaitClose, ahk_id %msg%
				Reload
			} 
		}
		objectNotSelected() 
		{
			objectSelected := superSearch("properties_objectSelected.png", 15, false, false)
			if (objectSelected[1] = 0) {
				msg := simpleGui("Error", "You have an object selected.  This control isn't available when an object is selected.  Reloading.", 250, 100)
				WinWaitClose, ahk_id %msg%
				Reload
			} 		
		}
	}
	class ItemAccessibility
	{
		btnCancel() 
		{
			MouseClick, left, 340, 300
		}
		btnOK() 
		{
			MouseClick, left, 235, 300
		}
		autoLabel(state) 
		{
			WinWaitActive, Item Accessibility
			Sleep, 1500
			if (state = "on") 
			{
				; iterate through all possible states.  If one if found, click it.
				objectSelected := superSearch("accessability_autoLabelOff1.png", 15, offset:=[7,7], false)
				if (objectSelected[1] != 0) 
				{
					objectSelected := superSearch("accessability_autoLabelOff2.png", 15, offset:=[7,7], false)
					if (objectSelected[1] != 0) 
					{
						objectSelected := superSearch("accessability_autoLabelOff3.png", 15, offset:=[7,7], false)
						if (objectSelected[1] != 0) 
						{
							objectSelected := superSearch("accessability_autoLabelOff4.png", 15, offset:=[7,7], true)
						}
					}
				}
			} else 
			{
				; iterate through all possible states.  If one if found, click it.
				objectSelected := superSearch("accessability_autoLabelOn1.png", 15, offset:=[7,7], false)
				if (objectSelected[1] != 0) 
				{
					objectSelected := superSearch("accessability_autoLabelOn2.png", 15, offset:=[7,7], false)
					if (objectSelected[1] != 0) 
					{
						objectSelected := superSearch("accessability_autoLabelOn3.png", 15, offset:=[7,7], false)
						if (objectSelected[1] != 0) 
						{
							objectSelected := superSearch("accessability_autoLabelOn4.png", 15, offset:=[7,7], true)
						}
					}
				}
			}
		}
	}
}

Re: Advice: Abstracting a Program GUI as a Class?

Posted: 01 Jul 2020, 13:04
by AHK_user
In this post you find a few examples, including mine that makes it simple to add tooltip and statusbar responses to the controls.
https://www.autohotkey.com/boards/viewtopic.php?f=76&t=68912&start=20

Re: Advice: Abstracting a Program GUI as a Class?  Topic is solved

Posted: 01 Jul 2020, 23:25
by toralf
@AHK_user IIUC she/he doesn't want to create a GUI (in a class). He wants to put the code that "automates"/interacts with Adobe Captivate into a class.

@sharonhuston To share code between different scripts you should use #Include. But in this included file you can either use functions/labels or a class. The more you reuse this file, the more robust it should be. In robust I mean: no duplicate names for variables or functions. And in this respect a class will most likely better suited, due to the encapsulation of it's properties and methods.
In general I believe to write reusable code when you can use it is a good practice, it is more work up front, but it is better then just copy and paste code between scripts. Specially if the code has to be maintained. Imagine Adobe Captivate gets an update and dialogs change, with an robust include file you only need to tweak this file.
Regarding if it is a good idea to subclass all dialogs: I have no strong feeling about it. The benefit I see is that you could "subcontract"/delegate the writing of the code to your occasional programmers. Specially if these subclasses are included files by themselfes. But that is depending of the required interaction between the subclasses.