[Example] Encapsulating Gui Controls within a class

Post your working scripts, libraries and tools for AHK v1.1 and older
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

[Example] Encapsulating Gui Controls within a class

30 Dec 2014, 14:26

*This is not new*.
I have merely figured out how parts of Fins' AFC work and thought I would break it down a bit and share this lovely technique with the wider public.

There may be goofs in there, don't take it as gospel (eg I did not test GuiControlGet if you extended this class for other control types, or with any of the commands)

If you like working with classes, and like writing GUI scripts, this technique is a handy way to tie a GUI control to a class instance.

It is essentially a way of doing this:

Code: Select all

Class MyClass {
	__New(){
		; The this.OnChange() bit is the interesting bit!
		Gui, Add, Edit, vMyEdit gthis.OnChange()
	}

	OnChange(){
		; this is run when the GuiControl changes
	}
}
Basically, within an instance of a class, Address := Object(this) will return a numerical number which is the memory address of the class instance.

If you then do MyObj := Object(Address), you end back up with the original object you started with.

So the basis of the trick is to use the numerical memory address as part of a GuiControl's "vLabel".

You can then specify one label as the gLabel for all controls, so when each control changes and fires the gLabel, you can inspect A_GuiControl and get the memory address back from that.

You can then use the memory address to retrieve the object and call it's OnChange() event.

Code: Select all

; Example of how to (mostly) encapsulate a GUI control within a class.
; Works by using the memory address of a class as the name for a GUI control.
; Trick learned by studying Fincs' AFC https://github.com/fincs/AFC

#SingleInstance, force

; Configure stuff that pollutes the global namespace - set prefixes etc
CONTROL_PREFIX := "__CTest_Controls_"
CONTROL_PREFIX_LENGTH := StrLen(CONTROL_PREFIX) + 1
LABEL_PREFIX := "__CTest_Labels_"

; Create a custom Edit box
Class CTest {
	__New(name){
		static 	; Declare static, else Gui, Add fails ("Must be global or static")
		global CONTROL_PREFIX, LABEL_PREFIX
		local CtrlHwnd, addr

		; Store friendly name
		this.__Name := name

		; Find address of this class instance.
		addr := Object(this)

		; Prepend address to CONTROL_PREFIX to obtain unique name that links to this class instance.
		this.__VName := CONTROL_PREFIX addr

		; Create the GUI control
		Gui, Add, Edit, % "hwndCtrlHwnd v" this.__VName " g" LABEL_PREFIX "OptionChanged"

		; Store HWND of control for future reference
		this.__Handle := CtrlHwnd
	}

	; Mimic GuiControlGet behavior.
	GuiControlGet(cmd := "", value := ""){
		GuiControlGet, ov, %cmd%, % this.__Handle, % value
		return ov
	}

	; OnChange method is called by the __CTest_Labels_OptionChanged gLabel
	OnChange(){
		Tooltip % this.__Name " contents: " this.GuiControlGet()
	}
}

test1 := new CTest("One")
test2 := new CTest("Two")
Gui, Show
Return

; Change events for all controls route to this label
__CTest_Labels_OptionChanged:
	; Pull the address of the class instance from the control name and use it to obtain an object.
	; Then use the object to call the OnChange() method of the class instance that created the GUI control.
	Object(SubStr(A_GuiControl,CONTROL_PREFIX_LENGTH)).OnChange()
	return

GuiClose:
	ExitApp

temp_user1
Posts: 12
Joined: 13 Mar 2015, 18:26

Re: [Example] Encapsulating Gui Controls within a class

25 Apr 2015, 13:45

Thanks :) This helped me understanding how classes can be build :) Many thanks. :)
zeus19
Posts: 3
Joined: 24 Jun 2015, 23:29

Re: [Example] Encapsulating Gui Controls within a class

09 Nov 2016, 08:16

I'm really new to coding, so can you explain why "CONTROL_PREFIX_LENGTH := StrLen(CONTROL_PREFIX) + 1", not +2,3, or just "StrLen(CONTROL_PREFIX)"
tmplinshi
Posts: 1604
Joined: 01 Oct 2013, 14:57

Re: [Example] Encapsulating Gui Controls within a class

09 Nov 2016, 09:36

zeus19 wrote:I'm really new to coding, so can you explain why "CONTROL_PREFIX_LENGTH := StrLen(CONTROL_PREFIX) + 1", not +2,3, or just "StrLen(CONTROL_PREFIX)"
Yes, is kind of confusing. CONTROL_PREFIX_LENGTH is used as StartingPos in the SubStr function:
Object(SubStr(A_GuiControl,CONTROL_PREFIX_LENGTH)).OnChange()
So, CONTROL_PREFIX_LENGTH is not means "the length of CONTROL_PREFIX".
lexikos
Posts: 9641
Joined: 30 Sep 2013, 04:07
Contact:

Re: [Example] Encapsulating Gui Controls within a class

09 Nov 2016, 17:02

The original method shown here is obsolete.

v1.1.20 and later support this:

Code: Select all

; Example of how to (mostly) encapsulate a GUI control within a class.

#SingleInstance, force

; Create a custom Edit box
Class CTest {
	__New(name){
		local CtrlHwnd

		; Store friendly name
		this.__Name := name

		; Create the GUI control
		Gui, Add, Edit, hwndCtrlHwnd
		fn := this.OnChange.Bind(this)
		GuiControl +g, %CtrlHwnd%, % fn

		; Store HWND of control for future reference
		this.__Handle := CtrlHwnd
	}

	; Mimic GuiControlGet behavior.
	GuiControlGet(cmd := "", value := ""){
		GuiControlGet, ov, %cmd%, % this.__Handle, % value
		return ov
	}

	OnChange(){
		Tooltip % this.__Name " contents: " this.GuiControlGet()
	}
}

test1 := new CTest("One")
test2 := new CTest("Two")
Gui, Show
Return

GuiClose:
	ExitApp
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: [Example] Encapsulating Gui Controls within a class

16 Nov 2016, 04:26

I think to make a Radios OOP like you have to create each Radio as an individual object and then later group them in a group radio object.
That group Object could check each radio label on whether it is checked or not. It could also invoke the code that creates the controls. (something like a draw method):

Code: Select all

class Radio
{
	__New( ... )
	{
		;Do whatever needs to be done
	}
	setParent( parent, isNewGroup ) ;Your GUI group
	{
		GUI,% parent.GUI . ":Add",Button,% "w" . ... . ( isNewGroup ? "Group" : "" )
	}
}
The downside of such a method is that the Parent ( the control Group ) needs to draw each Radio button directly after the last and that Radio buttons cannot be added at a later time.
For such a feature you will probably have to use the gLabel of the Control and make the group uncheck every other checked Radio button in the group.
Recommends AHK Studio
lexikos
Posts: 9641
Joined: 30 Sep 2013, 04:07
Contact:

Re: [Example] Encapsulating Gui Controls within a class

17 Nov 2016, 02:39

@evilC: Yes. Just do it.

The associated variable is only necessary for retrieving the index of the checked button, not for linking them together (making them mutually-exclusive). If you don't have a variable, you just have to figure out the index on your own. That's assuming you even use an index; maybe you would use a reference to the radio option or some other user-specified value.
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: [Example] Encapsulating Gui Controls within a class

17 Nov 2016, 07:19

Maybe I should have been clearer.
Surely this is only the case if the radios are added without any other controls in-between (Which is often a bit of a pain when laying out a GUI)
I was hoping that there may be a way to link radios that are not all declared one after the other.

For anyone interested, here is some code which is able to wrap radios, whilst providing one object which can tell you which radio is selected, rather than having to inspect the value of each radio to determine which one is checked:

Code: Select all

#SingleInstance force

Gui, Add, Text, xm, Radio1
Gui, Add, Text, xm, Radio2
r1 := ControlWrapper.AddControl("Radio", "x50 ym")
r2 := ControlWrapper.AddControl("Radio", "x50 y+5")
rg := ControlWrapper.AddControl("RadioGroup", [r1, r2])
Gui, Add, Button, xm y+5 gTest, Test
Gui, Show, x0 y0
return

Test:
	msgbox % rg.Get()
	return

class ControlWrapper {
	AddControl(aParams*){
		if (aParams[1] = "radiogroup"){
			return new this.RadioGroup(aParams*)
		} else {
			return new this.Control(aParams*)
		}
	}
	
	class Control {
		__New(aParams*){
			Gui, Add, % aParams[1], % "hwndhwnd " aParams[2]
			this.hwnd := hwnd
		}
		
		Get(){
			GuiControlGet, value, , % this.hwnd
			return value
		}
	}
	
	class RadioGroup {
		GroupedControls := []
		__New(aParams*){
			this.GroupedControls := aParams[2]
		}
		
		Get(){
			for i, ctrl in this.GroupedControls {
				if (ctrl.Get()){
					return i
					break
				}
			}
			return 0
		}
	}
}

GuiClose:
	ExitApp
lexikos
Posts: 9641
Joined: 30 Sep 2013, 04:07
Contact:

Re: [Example] Encapsulating Gui Controls within a class

17 Nov 2016, 17:14

I was hoping that there may be a way to link radios that are not all declared one after the other.
That really has nothing to do with objects.

Groups are started by appling the WS_GROUP style. AutoHotkey applies it when you add the first Radio after a non-Radio control or when you add the first non-Radio control after a Radio control. To prevent that, just override the style.

Code: Select all

Gui Add, Radio,, Radio 1
Gui Add, Radio,, Radio 2
Gui Add, Edit, -0x20000, Edit
Gui Add, Radio, -0x20000, Radio 3
Gui Show
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: [Example] Encapsulating Gui Controls within a class

17 Nov 2016, 17:32

lexikos wrote:That really has nothing to do with objects.
Duh, yeah, sorry.

Whilst regarding manipulating WS_GROUP, wouldn't that technique limit you to only one radio group?
ie, being able to have multiple radio groups (Works as intended):

Code: Select all

Gui Add, Radio,, Radio 1
Gui Add, Radio,, Radio 2
Gui Add, Radio,, Radio 3
Gui Add, Edit,, Edit to break up groups
Gui Add, Radio,, Radio A
Gui Add, Radio,, Radio B
Gui Add, Radio,, Radio C
But also free the user from having to declare in-order (Does not work as intended):

Code: Select all

Gui Add, Radio,, Radio 1
Gui Add, Edit,, Edit1

Gui Add, Radio,, Radio 2
Gui Add, Edit,, Edit2

Gui Add, Radio,, Radio 3
Gui Add, Edit,, Edit3

Gui Add, Radio,, Radio A
Gui Add, Edit,, EditA

Gui Add, Radio,, Radio B
Gui Add, Edit,, EditB

Gui Add, Radio,, Radio C
Gui Add, Edit,, EditC
lexikos
Posts: 9641
Joined: 30 Sep 2013, 04:07
Contact:

Re: [Example] Encapsulating Gui Controls within a class

17 Nov 2016, 22:49

evilC wrote:Whilst regarding manipulating WS_GROUP, wouldn't that technique limit you to only one radio group?
Why would it? The whole point of removing the style is to continue the group. If you want a new group, don't remove the style.

If you want a new group without any non-radio controls preceding it, use the Group option (which merely applies the WS_GROUP style).
evilC wrote:But also free the user from having to declare in-order
A group is a sequence of controls where only the first has the WS_GROUP style. They must be in sequence, otherwise you must emulate the grouping yourself.

It isn't necessarily creation order that matters. Maybe you can reorder the controls (z-order) with SetWindowPos or similar. I've never tried.
User avatar
evilC
Posts: 4823
Joined: 27 Feb 2014, 12:30

Re: [Example] Encapsulating Gui Controls within a class

19 Nov 2016, 19:16

I had a play around encapsulating edits, radios, DDLs, checkboxes and listboxes and here is a little script which I thought some people might find useful.

It wraps most guicontrols, loads/saves settings to file, and provides callbacks on change.
It also allows out-of-order radio groups, and provides callbacks both for individual radios (fires 0 on old radio, 1 on new radio) and for the radio group (fires new index for radio group callback).

Code: Select all

; =========================================== SAMPLE SCRIPT =====================================================
#SingleInstance force
OutputDebug DBGVIEWCLEAR
History := []
ShowTooltips := 1
TooltipTimerOn := 0
TooltipTimerFn := Func("TooltipTimer")

gw := new GuiWrapper()
gw.Show("x0 y0 w220 h350")
; Demo out-of-order radio groups
gw.StartRadioGroup("Numbers", 1, Func("Changed").Bind("RG-Numbers"))
gw.AddControl("Radio1", "Radio", , "Radio1" , Func("Changed").Bind("Radio1"))
gw.AddControl("Edit1", "Edit", "w200", 100, Func("Changed").Bind("Edit1"))
gw.AddControl("Radio2", "Radio", , "Radio2", Func("Changed").Bind("Radio2"))
gw.AddControl("Edit2", "Edit", "w200", 200, Func("Changed").Bind("Edit2"))
gw.EndRadioGroup()

; Second out-of-order radio group. Defaults to option 2
gw.StartRadioGroup("Letters", 2, Func("Changed").Bind("RG-Letters"))
gw.AddControl("RadioA", "Radio", , "RadioA", Func("Changed").Bind("RadioA"))
gw.AddControl("EditA", "Edit", "w200", "AAA", Func("Changed").Bind("EditA"))
gw.AddControl("RadioB", "Radio", , "RadioB", Func("Changed").Bind("RadioB"))
gw.AddControl("EditB", "Edit", "w200", "BBB", Func("Changed").Bind("EditB"))
gw.EndRadioGroup()

; Checkbox - defaults on.
gw.AddControl("Check1", "Checkbox", "checked", "Check1", Func("Changed").Bind("Check1"))

; Name-based DDL
gw.AddControl("DDL1", "DDL", ,"A||B|C", Func("Changed").Bind("DDL1"))
; Index-based DDL
gw.AddControl("DDL2", "DDL", "AltSubmit","One||Two|Three", Func("Changed").Bind("DDL2"))
; Listbox
gw.AddControl("LB1", "listbox", ,"Apple|Banana|Cherry||", Func("Changed").Bind("LB1"))

;gw.Show("x0 y0")
return

GuiClose:
	ExitApp

; Called whenever a GuiControl changes.
; This includes once at load-time with the initial value
Changed(ctrl, value){
	global ShowTooltips, History, TooltipTimerFn
	OutputDebug % "AHK| Callback fired for control " ctrl " with value " value
	if (ShowTooltips){
		History.push({ctrl: ctrl, value: value})
		if (!TooltipTimerOn){
			SetTimer, % TooltipTimerFn, 500
		}
	}
}

; Handles showing when callbacks are fired
ToolTipTimer(){
	global History, TooltipTimerOn, TooltipTimerFn
	max := History.length()
	str := ""
	for i, obj in History {
		str .= "Callback fired for control " obj.ctrl " with value " obj.value "`n"
	}
	ToolTip % str
	if (max == 0){
		SetTimer, % TooltipTimerFn, Off
		TooltipTimerOn := 0
	} else {
		History.RemoveAt(1)
	}
}

; =========================================== WRAPPER CLASS =====================================================
class GuiWrapper {
	static _CheckTypes := {checkbox: 1, radio: 1}
	static _ListTypes := {ddl: 1, dropdownlist: 1, combobox: 1, listbox: 1}
	
	GuiControls := {}
	RadioGroups := {}
	_CurrentRadioGroup := 0
	_RadioGroupCount := 0
	
	__New(hwnd := 0){
		if (hwnd == 0){
			Gui, +Hwndhwnd
		}
		this.hwnd := hwnd
		
		this.IniName := RegExReplace(A_ScriptName, (A_IsCompiled ? "\.exe" : "\.ahk"), ".ini")
	}

	AddControl(name, aParams*){
		; Load the value from the INI file, or get default
		is_radio := (aParams[1] = "radio")
		is_radio_in_group := (is_radio && this._RadioGroupCount)
		is_checktype := ObjHasKey(this._CheckTypes, aParams[1])
		is_listtype := ObjHasKey(this._ListTypes, aParams[1])
		if (is_checktype){
			; Is of a type the uses "Checked" in the options to signify default value
			; strip checked from options, use it as default value
			aParams[2] := RegExReplace(aParams[2], "\bchecked\b", "", checked)
			; If not a radio that is in a group, get it's value from the INI file
			if (!is_radio_in_group){
				checked := this._ReadSetting(name, (checked) )
				aParams[2] .= (checked ? " checked" : "")
			}
		} else if (!is_listtype){
			; Load value from settings file into aParams[3]. Use current value of aParams[3] as default
			aParams[3] := this._ReadSetting(name, aParams[3])
		}
		
		; If we are mid-radio group, and this is not the first radio, then remove WS_GROUP
		if (this._RadioGroupCount > 1){
			aParams[2] .= " -0x20000"
		}
		
		; Create the GuiControl
		ctrl := new this.GuiControl(this, name, aParams*)
		this.GuiControls[name] := ctrl

		; List Types current values are set after creation of the GuiControl.
		if (is_listtype){
			value := this._ReadSetting(name, "")
			if (value != ""){
				ctrl.Set(value, 1, 0, 0)
			}
		}
		
		; If this is a radio and we are mid radio group, then add it to the list and progress _RadioGroupCount
		if (is_radio_in_group){
			this.RadioGroups[this._CurrentRadioGroup]._AddRadio(ctrl)
			ctrl._RadioGroupName := this._CurrentRadioGroup
			this._RadioGroupCount++
		}
		
		; If this is not a radio group radio, and has a callback, fire it now.
		if (!(is_radio && this._CurrentRadioGroup) && IsObject(aParams[4])){
			ctrl._ChangeValueCallback.Call(ctrl.Get())
		}
		return ctrl
	}

	StartRadioGroup(name, default := 1, callback := 0){
		this.RadioGroups[name] := new this.RadioGroup(this, name, default, callback)
		this._CurrentRadioGroup := name
		this._RadioGroupCount := 1
		return this.RadioGroups[name]
	}
	
	EndRadioGroup(){
		cg := this.RadioGroups[this._CurrentRadioGroup]
		; Load settings from the ini file, fire initial callback for group
		cg.Set(this._ReadSetting(this._CurrentRadioGroup, cg.Default), 0)
		this._CurrentRadioGroup := 0
		this._RadioGroupCount := 0
	}
	
	Show(aParams*){
		Gui, % this.hwnd ":Show", % aParams[1], % aParams[2]
	}
	
	_ControlChanged(ctrl){
		this._WriteSetting(ctrl.name, ctrl.Get())
	}
	
	_RadioGroupChanged(name, index){
		this.RadioGroups[name].Set(index)
		this._WriteSetting(name, index)
	}

	_ReadSetting(name, default := ""){
		IniRead, value, % this.IniName, GuiControls, % name, % default
		if (value == "ERROR")
			value := default
		OutputDebug % "AHK| _ReadSetting got value '" value "' for setting " name
		return value
	}

	_WriteSetting(name, value){
		OutputDebug % "AHK| _WriteSetting writing value '" value "' to setting " name
		IniWrite, % value, % this.IniName, GuiControls, % name
	}
	
	class GuiControl {
		_ChangeValueCallback := 0
		_RadioGroupName := 0
		_RadioGroupIndex := 0
		
		;  aParams      1             2       3
		; Gui, Add, ControlType [, Options, Text]
		__New(ParentGui, Name, aParams*){
			this.ParentGui := ParentGui
			
			Gui, % ParentGui.hwnd ":Add", % aParams[1], % "hwndhwnd " aParams[2], % aParams[3]
			this.hwnd := hwnd
			this.Name := name
			this.Type := aParams[1]
			this.IsListType := ObjHasKey(this.ParentGui._ListTypes, this.Type)
			this.IsAltSumbit := RegExMatch(aParams[2], "\bAltSubmit\b")
			
			if (IsObject(aParams[4])){
				this._ChangeValueCallback := aParams[4]
			}
			fn := this._ChangedValue.Bind(this)
			GuiControl, +g, % this.hwnd, % fn
		}
		
		; The user interacted with the guicontrol
		_ChangedValue(aParams*){
			if (this._RadioGroupName){
				this.ParentGui._RadioGroupChanged(this._RadioGroupName, this._RadioGroupIndex)
			} else {
				this.Set(this.Get(), 0)	; Don't update the GuiControl
			}
		}
		
		Get(){
			GuiControlGet, value, , % this.hwnd
			return value
		}
		
		Set(value, update_guicontrol := 1, update_ini := 1, fire_callback := 1){
			if (update_guicontrol){
				if (this.IsListType){
					opt := "choose"
				}
				GuiControl, % opt , % this.hwnd, % value
			}
			
			if (update_ini){
				this.ParentGui._ControlChanged(this)
			}
			
			if (fire_callback){
				if (this._ChangeValueCallback != 0){
					this._ChangeValueCallback.Call(value)
				}
			}
		}
	}
	
	class RadioGroup {
		Name := ""
		ParentGui := 0
		Radios := []
		ChangeValueCallback := 0
		
		__New(ParentGui, Name, default := 1, callback := 0){
			this.ChangeValueCallback := callback
			this.ParentGui := ParentGui
			this.Name := name
			this.Default := default
		}
		
		_AddRadio(ctrl){
			this.Radios.push(ctrl)
			ctrl._RadioGroupIndex := this.Radios.length()
		}
		
		Set(index){
			for i, radio in this.Radios {
				if (i == index)
					continue
				radio.Set(0, 1, 0)	; Don't update the ini
			}
			this.Radios[index].Set(1, 1, 0)
			if (this.ChangeValueCallback != 0){
				this.ChangeValueCallback.Call(index)
			}
		}
	}
}

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: No registered users and 140 guests