[V2] Gui in class

Get help with using AutoHotkey (v2 or newer) and its commands and hotkeys
AHK_user
Posts: 515
Joined: 04 Dec 2015, 14:52
Location: Belgium

[V2] Gui in class

07 Apr 2023, 12:34

I created a nice Gui that looks like the Command Prompt, but was expecting that the Gui gets destroyed if I delete the class instance.

Should not the Delete method be called when I set the instance to empty? (oGui := "")

Code: Select all

#Requires AutoHotkey v2
#SingleInstance Force

; Optional code to change the menu in the gui
; DllCall("dwmapi\DwmSetWindowAttribute", "ptr", A_ScriptHwnd, "int", 20, "int*", true, "int", 4)
; if VerCompare(A_OSVersion, "10.0.17763") >= 0 {
;     attr := 19
;     if VerCompare(A_OSVersion, "10.0.18985") >= 0 {
;         attr := 20
;     }
;     DllCall("dwmapi\DwmSetWindowAttribute", "ptr", A_ScriptHwnd, "int", attr, "int*", true, "int", 4)
; }
; uxtheme := DllCall("GetModuleHandle", "str", "uxtheme", "ptr")
; SetPreferredAppMode := DllCall("GetProcAddress", "ptr", uxtheme, "ptr", 135, "ptr")
; FlushMenuThemes := DllCall("GetProcAddress", "ptr", uxtheme, "ptr", 136, "ptr")
; DllCall(SetPreferredAppMode, "int", 1) ; Dark
; DllCall(FlushMenuThemes)

class Gui_Log { ; object oriented GUI structure
    ;===============================================================================

	__New(WinTitle:="Log",W:=800, H:=200, x := "Center", y := "Center"){ ; constructor
		static
        ; Building the Gui
		this.myGui := Gui(,WinTitle)
        this.myGui.BackColor := 0x0C0C0C
        this.MyGui.SetFont("s11 cCCCCCC", "Consolas")
        this.myGui.x := x
        this.myGui.y := y

		this.myGui.Opt("+LastFound +AlwaysOnTop +Border +Resize")
        this.myGui.OnEvent("Size",(p*)=>(this.Gui_Size(p*)))
		This.EControl := This.myGui.AddEdit("x0 y0 h" H " w" W " -E0x200 +ReadOnly")
        This.EControl.Opt("Background0x0C0C0C cCCCCCC")

        DllCall("dwmapi\DwmSetWindowAttribute", "ptr", This.myGui.hwnd, "int", 20, "int*", true, "int", 4)
        DllCall("uxtheme\SetWindowTheme", "ptr", This.EControl.hwnd, "str", "DarkMode_Explorer", "ptr", 0)
        this.myGui.Show("x" this.myGui.x " y" this.myGui.y)
        this.Gui_Size()
        Return
	}

	SetProgress(Value:=0, ProgressText?){
        This.Show()
        this.myGui.GetClientPos(&X, &Y, &Width, &Height)
        This.Progress := Value
        if (!This.HasProp("CProgress")){
            This.CProgress := This.myGui.AddText("x0 y0 w" Width*This.Progress/100 " h16  Background0x4D4D4D")
            This.EControl.Move(,16,Width, Height-16)
            This.ProgressText := This.myGui.AddText("x0 y0 w" Width " h16 +0x200 +Center +BackgroundTrans", ProgressText ?? "")
        } else {
            (IsSet(ProgressText)) ? (This.ProgressText.Text := ProgressText) : ""
            This.CProgress.Move(,,Width*This.Progress/100)
            This.ProgressText.Move(0,0,Width)
        }
        This.ProgressText.Redraw()
    }

	Show(){
		this.myGui.Show()
	}

	Gui_Size(*){
        this.myGui.GetClientPos(&X, &Y, &Width, &Height)
        This.EControl.GetPos(&x, &y, &w, &h)
        if (This.HasProp("CProgress")){
            This.CProgress.Move(,,Width*This.Progress/100)
            This.ProgressText.Move(0,0,Width)
        }
        This.EControl.Move(,,Width, Height-y)
	}

	AddLine(Line, newline:=true, ReplaceLastLine:=false){

		delimiter:="`r`n"
        if (This.EControl.Text="" or !newline){
            delimiter := ""
        }
        if (ReplaceLastLine){
            TextWithoutLastLine := RegExReplace(This.EControl.Text, "(.*)\r\n.*$", "$1",&OutputVarCount) ; Remove the last line
            TextWithoutLastLine := (OutputVarCount=0) ? "" : TextWithoutLastLine delimiter
            This.EControl.Text := TextWithoutLastLine Line
            ControlSend("^{end}", , This.EControl)
			return
		}

        ControlSend("^{end}", , This.EControl)
		EditPaste(delimiter Line,This.EControl)

	}

	__Delete(){
		this.myGui.Destroy()
	}

	Delete(){
		this.myGui.Destroy()
	}

	Destroy(){
		this.myGui.Destroy()
	}
}


oGui := Gui_Log()

NumberLoops := 5
loop NumberLoops
{
    oGui.SetProgress(A_Index*100/NumberLoops,Round(A_Index*100/NumberLoops) "/100")
    oGui.AddLine("This Program will close in " 6-A_Index " Seconds...")
    sleep(500)
}
oGui := ""
swagfag
Posts: 6222
Joined: 11 Jan 2017, 17:59

Re: [V2] Gui in class

07 Apr 2023, 19:52

the Delete method should not be called, because its just a method u wrote, whose name has no special significance to ahk
the __Delete method should not be called either, even if uve already cleared the variable that holds a reference to ur object, since in the process of writing all this code, uve introduced circular references:

Code: Select all

...
this.myGui.OnEvent("Size",(p*)=>(this.Gui_Size(p*)))
...
Image
  • this holds a reference to a Gui object (because uve decided to store it there, in one of this's properties)
  • the Gui object's .OnEvent('Size', ...) then holds a reference to a lambda (because uve decided to pass it a lambda)
  • the lambda then holds a reference to back to this, since uve decided to call a method on this inside the lambda's body and now this(which in its purest form is just hidden parameter of whatever method ure invoking that ahk has generated for u under the hood, ie a local variable) has been captured by the lambda
  • bada bing bada boom, circular reference

to fix this u can:
  • pass the object's address to the GUI_Size handler and reconstruct the object from that address

    Code: Select all

    	...
    		this.myGui.OnEvent("Size", this.Gui_Size.Bind(WeakRef.FromObject(this)))
    	...
    	Gui_Size(*){
    		if (this is not Gui_Log ; if it was Gui_Log, then this means ure calling it as a method, eg someInstanceOfGuiLog.Gui_Size()
    		&&  this is WeakRef) ; if it was a WeakRef, then '.OnEvent("Size",' is calling it as a callback, so we expect the WeakRef to be a still-valid address
    			this := WeakRef.ToObjectAddRef() ; reconstruct the 'someInstanceOfGuiLog' from that address
    		else ; and if 'this' is neither of those 2, then ure misusing the method somehow
    			throw TypeError('unexpected this type', -1, 'expected Gui_Log or WeakRef, but got ' Type(this))
    
            this.myGui.GetClientPos(&X, &Y, &Width, &Height)
    		...
    
    class WeakRef
    {
    	static FromPtr(Ptr) => WeakRef(Ptr)
    	static FromObject(Obj) => WeakRef(ObjPtr(Obj))
    
    	__New(Ptr) => this.Ptr := Ptr
    	ToObjectAddRef() => ObjFromPtrAddRef(this.Ptr)
    	ToObject() => ObjFromPtr(this.Ptr)
    }
  • knowingly decrement the refcount(as many times as it is needed, depending on how many circular references ure creating) and knowingly restore it(as many times as it is needed, depending on how many circular references had created) in the destructor (and u have to watch out that the refcount is actually restored to whatever its proper value is supposed to be, otherwise the script would crash)

    Code: Select all

    	...
    		this.myGui.OnEvent("Size",(p*)=>(this.Gui_Size(p*))) ; +1 refcount because of the lambda
    		ObjRelease(ObjPtr(this)) ; -1 refcount, on purpose, cancelling out the lambda
    	...
    	__Delete(){
    		ObjPtrAddRef(this) ; +1 refcount, to restore our previous manual -1 adjustment
    		...
  • rewrite ur Gui code to inherit from Gui and use eventsinks. then ahk will manage the references for u. however, in this case, clearing the variable oGui := "" will not close the Gui if the Gui is visible on-screen, since the Gui's refcount is stays incremented for as long as the Gui stays visible on-screen(this behavior that is unique to Gui is explained somewhere in the docs)
AHK_user
Posts: 515
Joined: 04 Dec 2015, 14:52
Location: Belgium

Re: [V2] Gui in class

08 Apr 2023, 00:31

@swagfag: Thanks a lot for the explanation, it has indeed some logic, althrough I would almost expect an error.
I noticed that If you first destroy the gui, then the __Delete() method will be called properly.

I solved it in a simpler way by with removing all the instance references form the Gui_size function.

So it is best practice to:
- Not use a "this" references in the onsize function.
- Or first destroy the gui before deleting the Class.

Is there a possibility to destroy a class instance from within the class? I noticed this:= unset inside a class has no effect.
J1Y3M4XIEb.png
J1Y3M4XIEb.png (7.52 KiB) Viewed 852 times
Here is my solution:

EDITED:
- Removed the double Gui_size code.

Code: Select all

#Requires AutoHotkey v2
#SingleInstance Force

; Optional code to change the menu in the gui
; DllCall("dwmapi\DwmSetWindowAttribute", "ptr", A_ScriptHwnd, "int", 20, "int*", true, "int", 4)
; if VerCompare(A_OSVersion, "10.0.17763") >= 0 {
;     attr := 19
;     if VerCompare(A_OSVersion, "10.0.18985") >= 0 {
;         attr := 20
;     }
;     DllCall("dwmapi\DwmSetWindowAttribute", "ptr", A_ScriptHwnd, "int", attr, "int*", true, "int", 4)
; }
; uxtheme := DllCall("GetModuleHandle", "str", "uxtheme", "ptr")
; SetPreferredAppMode := DllCall("GetProcAddress", "ptr", uxtheme, "ptr", 135, "ptr")
; FlushMenuThemes := DllCall("GetProcAddress", "ptr", uxtheme, "ptr", 136, "ptr")
; DllCall(SetPreferredAppMode, "int", 1) ; Dark
; DllCall(FlushMenuThemes)

class Gui_Log { ; object oriented GUI structure
    ;===============================================================================

	__New(WinTitle:="Log",W:=800, H:=200, x := "Center", y := "Center"){ ; constructor
		static
        ; Building the Gui
        this.oGui := Gui(,WinTitle)

        this.oGui.BackColor := 0x0C0C0C
        this.oGui.SetFont("s11 cCCCCCC", "Consolas")
        this.oGui.x := x
        this.oGui.y := y

		this.oGui.Opt("+LastFound +AlwaysOnTop +Border +Resize")
        this.oGui.OnEvent("Size",(GuiObj,*)=>(Gui_Size(GuiObj)))

		this.oGui.EControl := this.oGui.AddEdit("x0 y0 h" H " w" W " -E0x200 +ReadOnly")
        this.oGui.EControl.Opt("Background0x0C0C0C cCCCCCC")

        DllCall("dwmapi\DwmSetWindowAttribute", "ptr", this.oGui.hwnd, "int", 20, "int*", true, "int", 4)
        DllCall("uxtheme\SetWindowTheme", "ptr", this.oGui.EControl.hwnd, "str", "DarkMode_Explorer", "ptr", 0)
        this.oGui.Show("x" this.oGui.x " y" this.oGui.y)
        Gui_Size(this.oGui)
        return

        Gui_Size(GuiObj,*){
            GuiObj.GetClientPos(&X, &Y, &Width, &Height)
            GuiObj.EControl.GetPos(&x, &y, &w, &h)
            if (GuiObj.HasProp("CProgress")){
                GuiObj.CProgress.Move(,,Width*GuiObj.Progress/100)
                GuiObj.ProgressText.Move(0,0,Width)
                GuiObj.ProgressText.Redraw()
            }
            GuiObj.EControl.Move(,,Width, Height-y)
        }
	}

	SetProgress(Value:=0, ProgressText?){
        this.oGui.Show()
        this.oGui.GetClientPos(&X, &Y, &Width, &Height)
        this.oGui.Progress := Value
        if (!this.oGui.HasProp("CProgress")){
            this.oGui.CProgress := this.oGui.AddText("x0 y0 w" Width*this.oGui.Progress/100 " h16  Background0x4D4D4D")
            this.oGui.EControl.Move(,16,Width, Height-16)
            this.oGui.ProgressText := this.oGui.AddText("x0 y0 w" Width " h16 +0x200 +Center +BackgroundTrans", ProgressText ?? "")
        } else {
            (IsSet(ProgressText)) ? (this.oGui.ProgressText.Text := ProgressText) : ""
            this.oGui.CProgress.Move(,,Width*this.oGui.Progress/100)
            this.oGui.ProgressText.Move(0,0,Width)
        }
        this.oGui.ProgressText.Redraw()
    }

	AddLine(Line, newline:=true, ReplaceLastLine:=false){

		delimiter:= "`r`n"
        if (this.oGui.EControl.Text="" or !newline){
            delimiter := ""
        }
        if (ReplaceLastLine){
            TextWithoutLastLine := RegExReplace(this.oGui.EControl.Text, "(.*)\r\n.*$", "$1",&OutputVarCount) ; Remove the last line
            TextWithoutLastLine := (OutputVarCount=0) ? "" : TextWithoutLastLine delimiter
            this.oGui.EControl.Text := TextWithoutLastLine Line
			return
		} else {
            EditPaste(delimiter Line,this.oGui.EControl)
        }
        ControlSend("^{end}", , this.oGui.EControl)
	}

	__Delete(){
		this.oGui.Destroy()
	}

}


oGui := Gui_Log()

NumberLoops := 5
loop NumberLoops
{
    oGui.SetProgress(A_Index*100/NumberLoops,Round(A_Index*100/NumberLoops) "/100")
    oGui.AddLine("This Program will close in " 6-A_Index " Seconds...",,1)
    sleep(500)
}
oGui.Destroy()
oGui := ""
Last edited by AHK_user on 12 Apr 2023, 23:43, edited 2 times in total.
swagfag
Posts: 6222
Joined: 11 Jan 2017, 17:59

Re: [V2] Gui in class

12 Apr 2023, 17:01

althrough I would almost expect an error.
u cant expect an error, because the behavior is not erroneous. everything uve written so far is valid in the context of the reference semantics/object lifetime ruleset that ahk(and most other garbage collecting systems based on reference counting, for that matter) imposes. it may not be the behavior that u desired, but it is valid behavior nonetheless
If you first destroy the gui, then the __Delete() method will be called properly.
if u have to call an extra method to clear references for u, so that ahk would be then able to invoke the destructor for u, it kinda defeats the purpose of having a destructor in the first place.
u want the object to clean up after itself after it goes out of scope. u dont want to have to perform extra steps(that ull inevitably forget to do at some point in the future) in order to ensure the object can be cleaned up before it goes out of scope. thats the point of having a destructor
I solved it in a simpler way by with removing all the instance references form the Gui_size function.
i dont see what uve solved. i only see the sameish code copypasted twice in two different places(bad) and a baked-in requirement to ensure ur .Destroy() method is called manually every time the object is about to go out of scope(bad, see reasons explained earlier)
So it is best practice to:
- Not use a "this" references in the onsize function.
- Or first destroy the gui before deleting the Class.
id say best practice is to understand first very well how this entire reference counting garbage collection mechanism works, learn to identify when references are created and when theyre released and when theyre NOT being released, and then u can make a better decision for urself and ur program how u want to approach this problem
Is there a possibility to destroy a class instance from within the class?
no. if ure "within the class", then this means theres at least one thing holding a reference to ur object. u can mess with the refcount and get ahk to invoke the destructor, but whenever the actual thing that was originally holding the reference finally releases the reference, the destructor would be invoked a second time(which is typically not the desired/intended effect)
I noticed this:= unset inside a class has no effect.
someVariable := unset is invalid syntax
Last edited by swagfag on 12 Apr 2023, 17:46, edited 1 time in total.
swagfag
Posts: 6222
Joined: 11 Jan 2017, 17:59

Re: [V2] Gui in class

12 Apr 2023, 17:46

huh, didnt see its documented now https://www.autohotkey.com/docs/v2/Concepts.htm#uninitialized-variables
although not in the place id expect to look when searching for it, thats for sure...
nevermind
AHK_user
Posts: 515
Joined: 04 Dec 2015, 14:52
Location: Belgium

Re: [V2] Gui in class

12 Apr 2023, 23:46

i dont see what uve solved. i only see the sameish code copypasted twice in two different places(bad) and a baked-in requirement to ensure ur .Destroy() method is called manually every time the object is about to go out of scope(bad, see reasons explained earlier)
Sorry, it seems I copied the wrong script, I updated my second post to make it work.
There is indeed no need for the gui_size method.
This version seems to work well, when the class is deleted, the gui is destroyed.
swagfag
Posts: 6222
Joined: 11 Jan 2017, 17:59

Re: [V2] Gui in class

13 Apr 2023, 02:47

in that case, yes, that solves it (albeit at the expense of ur object no longer exposing a method the client could call anytime they wanted to. if that wasnt an important feature to have, then whatever, it doesnt matter. but in other cases, u wont be able to avoid storing a circular reference regardless, eg because the function/method would need access to the object itself, eg because uve stored some properties in the object and want to now read/writeto them or u want to call other methods on the object, etc)

and if that is the case, then u dont need the intermediate lambda in this.oGui.OnEvent("Size",(GuiObj,*)=>(Gui_Size(GuiObj))), which serves no apparent purpose other than to introduce additional overhead of re-passing arguments along. u can write just this.oGui.OnEvent("Size", Gui_Size)

and if that is the case, and for ur callback function uve deciedd ure gonna be using a nested function, and that nested function is gonna be re-using the names of variables that exist in the enclosing scope(ie local variables existing in __New's scope), and that nested function does NOT need to interact with the actual variables from the enclosing scope(ie read/writeto them), then u may as well mark the nested function static Gui_Size(GuiObj,*){ ... } to avoid the pointless additional overhead(and potential source of logic errors, in other cases) of a Closure being created

Return to “Ask for Help (v2)”

Who is online

Users browsing this forum: djnjbjb, Fyre_Dealer, Google [Bot], mcl and 30 guests