I have been fascinated by classes and look for every opportunity to put another one together. I was intrigued by FG's use of a hidden Gui for cross communication between scripts. Well, here is the functionality in the form of a class. It may or may not be useful, but the more I worked with it, the more I liked it, so I thought I'd share. The level of complexity is higher at the code level but should make using this functionality more natural (er, I think
) and it enforces some discipline in defining shared variables.
Put the class and the two demo scripts in the same directory and run script#1.ahk. It will automatically start script#2.ahk and then run through a demonstration of the capabilities. Read the class documentation header first to get a primer.
Let me know what you think. -Relayer
Code: Select all
/*
Class GuiVar
class by: Relayer
inspired by: Fanatic Guru's LIBRARY that uses hidden Gui windows to pass information between scripts.
Features:
- all variables are instantiated by each script before use
- syntax for variable use is much like that of objects: [variable name].value
- a given variable must use the same name in all scripts accessing that variable
- all variables are bi-directional - can be written and read by all participating scripts
- each variable has but one owner called the server... the first instantiation
- a variable must be known to exist before instantiation as a client... use poll() to check
- each participating script can have both server (owned) variables and client variables
- can set up notifications to flag instantiation of a server variable owned by another scipt
- can set up notification when a variable has a specific content or simply changes
- notification is by a call to either a label or to a user function
- notification timer only runs when needed for pending notifications
Remarks:
The beauty of this class is that multiple scripts can share variables by using the same class.
Each script will have its own instance of the class which will manage which variables are
owned and manage which variables are simply used by the others. The instances of each
script's class is normalized by the common variable name and the HWND of the hidden Gui
that holds the variable's contents.
Methods:
__New(varName, varValue = "null")
Used to instantiate a variable. A server script is the owner of a variable and can instantiate
variables at any time. Client scripts use variables and instantiate them once they are known
to exist. In other words, this method makes the first instantiation of a variable the server
copy and all others are set to client.
__Set(a, b)
__Get(a)
These two methods are used internally. They enforce the management of the key designated
to hold the contents or value of a variable. The default for the key is ".value". This can
be changed by modifying the class variable GuiVar.keyForVariableContent
_set(varValue)
_get()
These two methods are for internal use... do not use.
list()
Returns an object of all instantiated variables as keys and their contents.
poll(varName)
Returns true if the variable has been instantiated by a server script and is now ready to
be instantiated by a client script for use.
destroy()
Destroys an instantiated variable. Destroying a server variable (by its owner) will invoke
an error in client scirpts using that variable name. Destroying a client variable has no
effect on other scripts and simply unregisters that variable for use by that client script.
_hwndList()
Returns an object containing all Gui names as keys and their HWND. Used internally.
notifyOnGuiVarAvailable(varName, notifyService)
Used by scripts to set automatic notification of the instantiation of a variable by another
script designed to be the owner of that variable. This method will instantiate the client
version of the variable and then call the given notifyService which can be a label or a
function name. NOTE: notification needs a "Return" by the host to complete the transaction.
setOnGuiVar(service, triggerOnContent = "", frequency = 1)
Used by either server or client scripts to notify when a variable changes where:
- service = the label or function name to call
- triggerOnContent = "" for any change in contents or set to a specific contents
- frequency is decremented on each notification and disables notification when = zero.
- NOTE: notification needs a "Return" by the host to complete the transaction.
getOnGuiVar()
Retrieves the class object for this variable from GuiVar.onGuiVar for inspection.
------------------------------------------
class member keys of interest:
[varName].value is the default contents of a variable; change by writing to GuiVar.keyForVariableContent
[varName].relationship returns either "server" or "client" from the script's perspective
Limitations:
1) A script serving a variable does not have a simple method for determining if there is a
client using that variable. A dialog needs to take place between the scripts.
2) Since the class will and its timer routine are instantiating Gui's there may be a need
for a using script to do a "Gui, [guiName]:Default" to return to its captive Gui
3) Every effort has been made to avoid race conditions since multiple scripts will be
running asynchronously.
*/
/*
Class initialization
*/
GuiVar_Class()
{
SetTimer, GuiVarTimer, 100
SetTimer, GuiVarTimer, Off
Return
}
/*
Class
*/
Class GuiVar
{
static onGuiVar := Object()
static keyForVariableContent := "value"
static GuiVarTimerState := "off"
__New(varName, varValue = "null")
{
Critical, On
this.varName := varName
this.guiID := "GuiVar_" . varName
this.relationship := "server"
for key, GuiHWND in this._hwndList()
if (key = varName)
{
this.relationship := "client"
this.hwnd := GuiHWND
GuiVar.onGuiVar[this.guiID] := Object()
GuiVar.onGuiVar[this.guiID].hwnd := this.hwnd
;a client instantiation can write a value to the variable
;text 'null' allows user to initialize to "" to clear the variable
;otherwise it will remain with the value set by the server of that variable
if (varValue != "null")
this[GuiVar.keyForVariableContent] := varValue
Break
}
;the first instantiation of a variable will be a server variable
;client variables must be instantiated after the server version
if (this.relationship = "server")
{
guiID := this.guiID
Gui, %guiID%:New, +hwndGuiHWND
Gui, %guiID%:Add, Text,, % ((varValue = "null") ? ("") : (varValue))
Gui, %guiID%:Show, Hide, %guiID%
this.hwnd := GuiHWND
GuiVar.onGuiVar[this.guiID] := Object()
GuiVar.onGuiVar[this.guiID].hwnd := this.hwnd
}
Critical, Off
}
__Set(a, b)
{
if (a = GuiVar.keyForVariableContent)
{
;variable name must be registered with the class object
if (GuiVar.onGuiVar.Haskey(this.guiID))
{
Critical, On
this._set(b)
;prevent triggering on change initiated by same script waiting for trigger
;this lets the trigger condition be set up before the contents of a
;shared variable is written to by the same script
;i.e. trigger must come from an external source
GuiVar.onGuiVar[this.guiID].watchValue := b
Critical, Off
Return
}
else
{
Msgbox % "error: " . this.varName . " not instantiated"
Return
}
}
}
__Get(a)
{
if (a = GuiVar.keyForVariableContent)
{
;variable name must be registered with the class object
if (GuiVar.onGuiVar.Haskey(this.guiID))
Return this._get()
else
{
Msgbox % "error: " . this.varName . " not instantiated"
Return
}
}
}
_set(varValue)
{
Setting_A_DetectHiddenWindows := A_DetectHiddenWindows
DetectHiddenWindows, On
guiID := this.guiID
Hwnd := this.hwnd
ControlSetText,, %varValue%, ahk_id %Hwnd%
DetectHiddenWindows, %Setting_A_DetectHiddenWindows%
if (ErrorLevel)
{
Msgbox % "error: " . this.varName . " not found"
Return False
}
Return True
}
_get()
{
Setting_A_DetectHiddenWindows := A_DetectHiddenWindows
DetectHiddenWindows, On
Hwnd := this.hwnd
ControlGetText, value,, ahk_id %Hwnd%
DetectHiddenWindows, %Setting_A_DetectHiddenWindows%
if (ErrorLevel)
{
Msgbox % "error: " . this.varName . " not found"
Return False
}
Return value
}
list()
{
Setting_A_DetectHiddenWindows := A_DetectHiddenWindows
DetectHiddenWindows, On
result := Object()
for key, Hwnd in this._hwndList()
{
ControlGetText, value,, ahk_id %Hwnd%
if (ErrorLevel)
{
result := "error: " . this.varName . " not found"
Break
}
else
result[key] := value
}
DetectHiddenWindows, %Setting_A_DetectHiddenWindows%
Return result
}
poll(varName)
{
result := False
for key, Hwnd in this._hwndList()
{
if (key = varName)
{
result := True
Break
}
}
Return result
}
destroy()
{
Setting_A_DetectHiddenWindows := A_DetectHiddenWindows
DetectHiddenWindows, On
guiID := this.guiID
if (this.relationship = "server")
Gui, %guiID%:Destroy
GuiVar.onGuiVar.Remove(guiID)
DetectHiddenWindows, %Setting_A_DetectHiddenWindows%
}
_hwndList()
{
Setting_A_DetectHiddenWindows := A_DetectHiddenWindows
DetectHiddenWindows, On
Array := Object()
WinGet, WinList, List, GuiVar_
Loop, %WinList%
{
X := A_Index
Hwnd := WinList%X%
WinGetTitle, WinTitle, ahk_id %Hwnd%
ControlGetText, Text,, ahk_id %Hwnd%
WinTitle := SubStr(WinTitle, 8)
Array[WinTitle] := Hwnd ;Text
}
DetectHiddenWindows, %Setting_A_DetectHiddenWindows%
Return Array
}
/*
Notify when variable becomes available
*/
notifyOnGuiVarAvailable(varName, notifyService)
{
if (isLabel(notifyService)) or (isFunc(notifyService))
{
SetTimer, GuiVarTimer, Off
GuiVar.onGuiVar[varName] := Object()
GuiVar.onGuiVar[varName].notifyService := notifyService
if (GuiVar.GuiVarTimerState = "off")
SetTimer, GuiVarTimer, On
Return True
}
else
Return False
}
/*
Notify on variable content or content change
*/
;use trigger content that is unlikely to match user data... allows trigger on blank ("")
setOnGuiVar(service, triggerOnContent = "wackenfus", frequency = 1)
{
if ( (isLabel(service)) or (isFunc(service)) ) and (GuiVar.onGuiVar.Haskey(this.guiID))
{
SetTimer, GuiVarTimer, Off
GuiVar.onGuiVar[this.guiID].triggerOnContent := triggerOnContent
GuiVar.onGuiVar[this.guiID].watchValue := this._get()
GuiVar.onGuiVar[this.guiID].state := 0
GuiVar.onGuiVar[this.guiID].service := service
GuiVar.onGuiVar[this.guiID].frequency := frequency
if (GuiVar.GuiVarTimerState = "off")
SetTimer, GuiVarTimer, On
Return True
}
else
Return False
}
getOnGuiVar()
{
Return GuiVar.onGuiVar[this.guiID]
}
}
GuiVarTimer:
GuiVar.GuiVarTimerState := "on"
SetTimer, GuiVarTimer, Off
for GuiVarObjName, GuiVarObj in GuiVar.onGuiVar
if (GuiVarObj.HasKey("service")) and (GuiVarObj.frequency > 0)
{
GuiVarHwnd := GuiVarObj.hwnd
GuiVarService := GuiVarObj.service
Setting_A_DetectHiddenWindows := A_DetectHiddenWindows
DetectHiddenWindows, On
ControlGetText, GuiVarText,, ahk_id %GuiVarHwnd%
DetectHiddenWindows, %Setting_A_DetectHiddenWindows%
if ((GuiVarObj.triggerOnContent = "wackenfus")
and (GuiVarObj.watchValue != GuiVarText)) ;on change (most useful)
or ((GuiVarObj.triggerOnContent = GuiVarText)
and (GuiVarObj.state = 0)) ;immediate trigger if var already matches
or ((GuiVarObj.triggerOnContent = GuiVarText)
and (GuiVarObj.watchValue != GuiVarText)
and (GuiVarObj.state != 0)) ;trigger once per becoming the trigger value
{
if (GuiVarObj.triggerOnContent = "")
GuiVarObj.watchValue := text
GuiVarObj.frequency -= 1
GuiVarObj.state := 1
if (isLabel(GuiVarService))
Gosub % GuiVarService
else
%GuiVarService%(%GuiVarObjName%)
}
}
else if (GuiVarObj.HasKey("notifyService")) and (GuiVar.poll(GuiVarObjName))
{
%GuiVarObjName% := New GuiVar(GuiVarObjName) ;instantiate this variable for the client
GuiVarNotifyService := GuiVarObj.notifyService
GuiVar.onGuiVar.Remove(GuiVarObjName)
if (isLabel(GuiVarNotifyService))
Gosub % GuiVarNotifyService
else
%GuiVarNotifyService%(%GuiVarObjName%)
}
;check full structure of class object to see if timer needs to be turned on again
for GuiVarObjName, GuiVarObj in GuiVar.onGuiVar
if ((GuiVarObj.HasKey("service")) and (GuiVarObj.frequency > 0)) or (GuiVarObj.HasKey("notifyService"))
{
SetTimer, GuiVarTimer, On
Break
}
else
GuiVar.GuiVarTimerState := "off"
Return
Demo script#1.ahk
Code: Select all
;Sript#1.ahk
#SingleInstance Force
#NoEnv
SetTitleMatchMode, 2 ;contains
CoordMode, Mouse, Relative
GuiVar_Class() ;initialize
blurb := "This is script #1: Foo"
. "`n`nI have set GuiVar to notify me when fruit, color"
. ", and cost are made available by Script #2 called Bar"
Gui, Add, Edit, w275 h400 vEdit1, % blurb
Gui, Show, x200 y200, Foo
SendInput, {End}
;set up to get notifications when external script instantiates these variables
GuiVar.notifyOnGuiVarAvailable("cost", "costFound")
GuiVar.notifyOnGuiVarAvailable("fruit", "fruitFound")
GuiVar.notifyOnGuiVarAvailable("color", "colorFound")
Sleep, 2500
blurb .= "`n`nFoo will now start script #2: Bar"
GuiControl,, Edit1, % blurb
Sleep, 5000
Run, %A_ScriptDir%\script#2.ahk
Goto, jump
;these three are the service routines for the notifications
fruitFound:
;Msgbox % "Script #1 found Fruit is: " . fruit.value
blurb .= "`nScript #1 found Fruit is: " . fruit.value
GuiControl,, Edit1, % blurb
Sleep, 250
Return
colorFound(color)
{
Global blurb, Edit1
;Msgbox % "`nScript #1 found Color is: " . color.value
blurb := blurb . "`nScript #1 found Color is: " . color.value
GuiControl,, Edit1, % blurb
Sleep, 250
Return
}
costFound:
;Msgbox % "`nScript #1 found Cost is: " . cost.value
blurb .= "`nScript #1 found Cost is: " . cost.value
GuiControl,, Edit1, % blurb
Sleep, 250
Return
jump:
While % (!InStr(blurb, "found fruit")) or (!InStr(blurb, "found color")) or (!InStr(blurb, "found cost"))
{
}
blurb .= "`n`n ...Now I will echo what Bar says to me `n`nEchoing Bar:"
GuiControl,, Edit1, % blurb
comChannel := New GuiVar("comChannel") ;instantiate the server variable owned by this script
comChannel.setOnGuiVar("next") ;set up notification triggered when comChannel changes
;say hi to script #2 -- note that this setting the variable doesn't trigger the notification
;triggers come from external scripts.
comChannel.value := "Hello Bar, how are you?"
Return
next:
Sleep, 2500
blurb .= "`nBar replied: " comChannel.value ;display what Bar had to say
GuiControl,, Edit1, % blurb
comChannel.setOnGuiVar("next1")
comChannel.value := "Fine, thanks for asking!"
Return
next1:
Sleep, 2500
blurb .= "`n`nBar replied: " comChannel.value
GuiControl,, Edit1, % blurb
comChannel.setOnGuiVar("next2")
comChannel.value := "OK, give me a the variable name to store it in?"
Return
next2:
Sleep, 2500
blurb .= "`n`nBar replied: " comChannel.value
GuiControl,, Edit1, % blurb
tmp := comChannel.value ;this contains the variable name Bar wants the data in
;now look for Bar to instantiate his variable as server then instantiate it here as a client
GuiVar.notifyOnGuiVarAvailable(comChannel.value, "varReady")
Return
varReady:
%tmp%.value := "0,0.50,0.80,1.00,1.50,2.00,2.50,3.00,5.00,7.00,10.00"
comChannel.setOnGuiVar("next3")
comChannel.value := "Here ya go! Your data is now in """ . tmp . """."
Return
next3:
Sleep, 2500
blurb .= "`n`n`n`n`nBar replied: " comChannel.value
GuiControl,, Edit1, % blurb
comChannel.setOnGuiVar("next4")
if (comChannel.value = "Thank you")
comChannel.value := "You are welcome... see ya!"
Return
next4:
blurb .= "`n`nBar replied: " comChannel.value
GuiControl,, Edit1, % blurb
Sleep, 3000
Msgbox, Foo is finished
ExitApp
Escape::ExitApp
#Include %A_ScriptDir%\GuiVar_class.ahk
Demo script#2
Code: Select all
;Sript#2.ahk
#SingleInstance Force
#NoEnv
SetTitleMatchMode, 2 ;contains
CoordMode, Mouse, Relative
GuiVar_Class() ;initialize
blurb := "This is script #2: Bar `n`nI will instantiate 3 variables:"
. "`n`tfruit = apple"
. "`n`tcolor = red"
. "`n`tcost = Round(.99*1.095,2)"
Gui, Add, Edit, w375 h400 vEdit1, % blurb
Gui, Show, x500 y200, Bar
SendInput, {End}
Sleep, 2500
blurb .= "`n`n ...Now I will echo what Foo says to me `n`n`n`n`nEchoing Foo:"
GuiControl,, Edit1, % blurb
Sleep, 250
fruit := New GuiVar("fruit", "apple")
color := New GuiVar("color", "red")
cost := New GuiVar("cost", Round(.99*1.095,2))
Gui, 1: Default
;this is one way to monitor when the server variable is instantiated
;later, Foo (script #1) will use GuiVar.notifyGuiVarAvaliable() to automatically wait for it
while !GuiVar.poll("comChannel")
{
}
Sleep, 2500
comChannel := New GuiVar("comChannel") ;instantiate the client version of the variable
while (comChannel.value = "") ;can monitor contents like this or use [varName].setOnGuiVar()
{
}
blurb .= "`nFoo asked: " . comChannel.value ;display what Foo had to say
GuiControl,, Edit1, % blurb
Sleep, 2500
comChannel.setOnGuiVar("next")
comChannel.value := "I am doing fine! How about you?"
Return
next:
Sleep, 2500
blurb .= "`n`nFoo replied: " comChannel.value
GuiControl,, Edit1, % blurb
comChannel.setOnGuiVar("next1")
comChannel.value := "I need some data"
Return
next1:
Sleep, 2500
blurb .= "`n`nFoo replied: " comChannel.value
GuiControl,, Edit1, % blurb
comChannel.value := "someData" ;tell Foo the variable name to put data in
comChannel.setOnGuiVar("next2")
someData := New GuiVar("someData") ;instantiate the server variable
Return
next2:
Sleep, 2500
blurb .= "`n`nFoo replied: " comChannel.value
GuiControl,, Edit1, % blurb
blurb .= "`n`nBar reveals the data sent:`n Data is: " . someData.value
GuiControl,, Edit1, % blurb
comChannel.setOnGuiVar("next3")
comChannel.value := "Thank you"
Return
next3:
Sleep, 2500
blurb .= "`n`nFoo replied: " comChannel.value
GuiControl,, Edit1, % blurb
if (InStr(comChannel.value, "see ya"))
comChannel.value := "Bye, bye"
Sleep, 3000
Msgbox, Bar is finished
ExitApp
Escape::ExitApp
#Include %A_ScriptDir%\GuiVar_class.ahk