Passing GUI events to another GUI / letting GUI events fall through

Get help with using AutoHotkey (v2 or newer) and its commands and hotkeys
wpb
Posts: 150
Joined: 14 Dec 2015, 01:53

Passing GUI events to another GUI / letting GUI events fall through

19 Mar 2024, 03:36

Hi there,
I have a main GUI which is a framework for various sub-GUIs.
The main GUI listens for ContextMenu events:

Code: Select all

this.MainGui.OnEvent("ContextMenu", (*) => this.ContextMenu.Show())
I want the sub-GUIs to show the same context menu, but I'd rather avoid recoding the same line on the sub-GUIs. Is there a way to either let the ContextMenu event "drop through" to the GUI underneath, or to trigger the ContextMenu event in the main GUI?
lexikos
Posts: 9593
Joined: 30 Sep 2013, 04:07
Contact:

Re: Passing GUI events to another GUI / letting GUI events fall through

21 Mar 2024, 03:41

You can post a WM_CONTEXTMENU message to the main GUI.
wpb
Posts: 150
Joined: 14 Dec 2015, 01:53

Re: Passing GUI events to another GUI / letting GUI events fall through

21 Mar 2024, 07:43

Thanks, @lexikos - that's a good idea.

I tried this, but it does nothing:

Code: Select all

SubGui.OnEvent("ContextMenu", this.test.Bind(this))

...

test(*)
{
	currentMode := A_CoordModeMouse
	A_CoordModeMouse := "Screen"
	MouseGetPos(&x, &y)
	A_CoordModeMouse := currentMode
	PostMessage(0x7B, MainGui.Hwnd, (x & 0xFFFFFFFF)|y<<32, MainGui)	; 0x007B->WM_CONTEXTMENU
}
Can you tell me what I'm doing wrong?
lexikos
Posts: 9593
Joined: 30 Sep 2013, 04:07
Contact:

Re: Passing GUI events to another GUI / letting GUI events fall through

22 Mar 2024, 00:08

ContextMenu
Launched whenever the user right-clicks anywhere in the window except the title bar and menu bar.
You are passing the coordinates incorrectly (words are 16-bit, not 32-bit). The message handler receives a Y coordinate of zero (assuming (x >> 16) & 0xFFFF = 0), which is very likely to be above the client area of the GUI and therefore assumed to be over the title bar or menu bar.

When you pass the coordinates correctly ((x & 0xFFFF)|y<<16), you will have the same issue if y < the top of the main GUI's client area. Passing -1 for lParam would avoid the issue, but the IsRightClick parameter will be false and the X,Y parameters won't be useful.

Unless you are using -1 or otherwise overriding the coordinates, you should utilize the parameters of the original event callback, not retrieve the current mouse position.

But I don't see why you want to use this roundabout way of triggering the event, when you can just call a method of MainGui.
wpb
Posts: 150
Joined: 14 Dec 2015, 01:53

Re: Passing GUI events to another GUI / letting GUI events fall through

22 Mar 2024, 02:46

Thanks, the -1 worked perfectly. Nice and simple. I hadn't spotted that in the docs before.
I just wanted to do this so that the code for opening the submenu is all in one place. Now if I make changes to the way the main GUI opens the context menu, I don't also have to make the same changes to the sub GUI. It feels like the right approach to me, rather than duplicating the code (albeit a tiny amount of code). Your guidance was much appreciated.
lexikos
Posts: 9593
Joined: 30 Sep 2013, 04:07
Contact:

Re: Passing GUI events to another GUI / letting GUI events fall through

22 Mar 2024, 03:35

When the event is handled by an anonymous function, of course you can't call that function. I wasn't suggesting that you duplicate the event handler in its entirety, but "just call a method of MainGui" from both event handlers. Then if you want to change how the context menu is shown, you modify that method, which is called by all GUIs. Simulating the event to trigger the event handler is like using Send to trigger a hotkey; it is not the ideal way to avoid code duplication.

If this has a reference to the GUI, both (*) => this.ContextMenu.Show() and this.test.bind(this) will create a circular reference: this → GUI → event handler list → anonymous closure/BoundFunc → this. In some cases you can avoid this by utilizing the parameters of the event callback instead of binding or capturing a variable.

I tend to define event handlers as methods of a Gui subclass (see __New (Gui)). This avoids circular references (unlike both of the previous cases) and naturally gives you a method that can be called directly instead of simulating the event.
wpb
Posts: 150
Joined: 14 Dec 2015, 01:53

Re: Passing GUI events to another GUI / letting GUI events fall through

22 Mar 2024, 03:45

Sure, I did consider a method in the main GUI class, which could be called from both the main GUI and the sub GUI, but the anonymous function was so neat and compact, I was lured in!

However, I am plagued by circular references in my code. I'm starting to recognize when I introduce them, but often they slip in without me noticing. Do you have a simple example showing how to use a Gui subclass for event handlers in this way?
lexikos
Posts: 9593
Joined: 30 Sep 2013, 04:07
Contact:

Re: Passing GUI events to another GUI / letting GUI events fall through

22 Mar 2024, 17:03

The AutoHotkey Dash and related GUIs all use this technique.
https://github.com/AutoHotkey/AutoHotkeyUX
  • Create a class which extends Gui (direct or indirect).
  • "override __New and call super.__New(Options, Title, this)"
  • Pass a method name to OnEvent. (If you pass an object, it will work the same as usual.)
  • Keep in mind that "events for the main window (such as Close) do not pass an explicit Gui parameter, as this already contains a reference to the Gui."

Code: Select all

#Requires AutoHotkey v2.0

class GuiWithButton extends Gui {
	__new() {
		super.__new(,, this)
		this.SetFont('s20')
		this.Add('Button', 'vTheBigButton', "Click me").OnEvent('Click', 'Clicked')
		this.OnEvent('ContextMenu', 'RightClicked')
	}
	Clicked(ctrl, *) {
		MsgBox ctrl.name " was clicked."
	}
	RightClicked(ctrl, *) {
		MsgBox (ctrl ? ctrl.name : type(this)) " was right-clicked (or AppsKey/Shift+F10 was pressed)."
	}
	__delete() {
		MsgBox type(this) " will be deleted."
	}
}

GuiWithButton().Show()
Another option is to avoid storing a reference to any GUI. Supposing that this is the non-Gui class which contains the main logic, each GUI can hold a reference to it, but this refers to the GUI by HWND. The Gui object can be retrieved with GuiFromHwnd or from the event handler's first parameter. That might only work if the GUI remains visible, because of the built-in mechanism that keeps the object alive while the GUI is visible.

I think these approaches are easier to grasp than messing with the reference count (which is hard to even explain how to utilize without a specific problem to solve).

If you pass an object to the EventObj parameter of the Gui constructor, the Gui retains a reference to it. You can then define the handlers like 'test' instead of this.test.bind(this), though it doesn't help you avoid circular references.
wpb
Posts: 150
Joined: 14 Dec 2015, 01:53

Re: Passing GUI events to another GUI / letting GUI events fall through

23 Mar 2024, 12:09

Thanks, @lexikos , I really appreciate you taking the time to post an example.

When I run it, everything works as I'd expect, other than that I thought I'd see the "... will be deleted" message when I x-ed (closed) the GUI, but I don't. Why isn't the destructor called?

Also, is it subclassing the GUI that's avoiding the circular refs, or is it just using an event sink and method names with OnEvent, rather than calling annonymous functions or function/boundfunc objects? I can't see why the subclassing helps per se. If I were to add something like this to your example:

Code: Select all

this.LineLimit := this.Add("Edit", , "9999")
this.Add("Checkbox", "", "Line limit").OnEvent("Click", (*) => this.LineLimit.Enabled := not this.LineLimit.Enabled)
...then I'd still be introducing a circular reference with the anonymous function in the same way as you described before, wouldn't I?
lexikos
Posts: 9593
Joined: 30 Sep 2013, 04:07
Contact:

Re: Passing GUI events to another GUI / letting GUI events fall through

23 Mar 2024, 22:07

The script ceases to be "persistent" when you close the GUI, so it exits (apparently before releasing the object). If you use Persistent, you will see the message.
lexikos wrote:
22 Mar 2024, 03:35
(see __New (Gui)). This avoids circular references
There is a special case in the implementation of __New: when EventObj == this, it avoids calling AddRef or Release, since they aren't needed and would only have a detrimental effect.

If you pass some other object as the Gui's event sink, it must AddRef before storing the reference (and Release when the Gui is destroyed), otherwise the object could be deleted prematurely, leaving the Gui with a dangling pointer.

Each control also has a pointer to its Gui, but not a counted reference. Holding a reference to a control will not prevent the Gui from being deleted. After the GUI is destroyed (which implicitly occurs when it is deleted, if not before), the control is no longer valid and will not allow you to retrieve ctrl.Gui.

Code: Select all

g := Gui()
c := g.AddText()
g := ""
MsgBox type(c.gui) ; Error: The control is destroyed.
The lesson is that if you know when the object will be deleted or can control what happens when it is deleted, you can keep a pointer instead of a counted reference.
wpb wrote:If I were to add something like this to your example:
..then I'd still be introducing a circular reference with the anonymous function in the same way as you described before, wouldn't I?
Yes. There's no point specifying EventObj if you're not going to use method names for your event handlers. You can also create circular references completely independent of the event handlers, such as by assigning properties. EventObj will obviously not help in those cases.

I have added the following to the documentation.
The Gui retains a reference to EventObj for the purpose of calling event handlers, and releases it when the window is destroyed. If EventObj itself contains a reference to the Gui, this would typically create a circular reference which prevents the Gui from being automatically destroyed. However, an exception is made for when EventObj is the Gui itself, to avoid a circular reference in that case.
The point of the exception is to provide an alternative to the other methods of specifying event handlers which are prone to circular references. You need to actually use that alternative as such, not in combination with the more problematic methods.

Come to think of it, the event handlers are also deleted when the window is destroyed. So although your last example does create a circular reference due to capturing this, the circle is broken if you destroy the window. However, it may still be more efficient to avoid creating a closure; i.e. (this, *) => this.LineLimit.Enabled := not this.LineLimit.Enabled. (Naming the parameter this prevents you from accidentally capturing the outer one.)
wpb
Posts: 150
Joined: 14 Dec 2015, 01:53

Re: Passing GUI events to another GUI / letting GUI events fall through

25 Mar 2024, 12:40

Let me thank you again, @lexikos , for your detailed response. The extra information about the special case in Gui's __New() implementation explains it all! Thank you.

Basically, avoiding function references or boundfunc objects in event handlers is essential to avoid circular references, it seems.

In other GUIs I have used OnMessage handlers to catch clicks over the entire GUI, for example. In that case, I guess there's no way to avoid the circular reference since a function object is required, right?

Same goes for SetTimer used to call a method in an object. That's presumably also going to create a circular reference, isn't it?

The more I think about it, the more of a thorny subject it seems to be! :(
lexikos
Posts: 9593
Joined: 30 Sep 2013, 04:07
Contact:

Re: Passing GUI events to another GUI / letting GUI events fall through

25 Mar 2024, 17:05

No, avoiding references to the GUI itself being passed indirectly back into the GUI (via an event handler or assigned property) is essential to avoid circular references. You can use bound functions and closures that refer to objects which don't directly or indirectly have any counted reference to the GUI.

SetTimer creates a timer, and a timer internally keeps a reference to the function to be called. You can't have a reference to a timer, so it is not a "circular" reference, but it is like storing the function (presumably bound function or closure) in a global variable.
wpb
Posts: 150
Joined: 14 Dec 2015, 01:53

Re: Passing GUI events to another GUI / letting GUI events fall through

26 Mar 2024, 10:04

lexikos wrote:
25 Mar 2024, 17:05
No, avoiding references to the GUI itself being passed indirectly back into the GUI (via an event handler or assigned property) is essential to avoid circular references. You can use bound functions and closures that refer to objects which don't directly or indirectly have any counted reference to the GUI.
I should have been explicit in saying "avoiding function references or boundfunc objects that point to methods in the Gui subclass in event handlers is essential to avoid circular references". Presumably that's an accurate statement, given all we've discussed above?

For the timer situation, I meant something like this:

Code: Select all

a := SomeClass()
Sleep(10000)
a := ""

class SomeClass
{
	__New()
	{
		SetTimer(this.timedFunc.Bind(this), 1000)
	}
	
	__Delete()
	{
		SetTimer(this.timedFunc.Bind(this), 0)
	}
	
	timedFunc()
	{
		; Blah, blah...
	}
}
In the above example, my current understanding is that __Delete() would not be called when a is set to null, because the timer still holds a reference to the object via the bound "this" parameter. In such situations, do you need to explicitly call code to remove the timer, to ensure that __Delete() will subsequently be called, or is there some other way? I rather like the neat symmetry of setting the timer up in __New() and removing it in __Delete(), but I don't think it works.
lexikos
Posts: 9593
Joined: 30 Sep 2013, 04:07
Contact:

Re: Passing GUI events to another GUI / letting GUI events fall through

30 Mar 2024, 19:44

wpb wrote: I should have been explicit in saying "avoiding function references or boundfunc objects that point to methods in the Gui subclass in event handlers is essential to avoid circular references". Presumably that's an accurate statement, given all we've discussed above?
No, it's actually missing the point. There's no problem with retaining references to methods of the Gui subclass; after all, the object already has indirect references to those via its base object. You could even bind some other Gui instance to a method of your subclass and pass that to OnEvent without creating a reference cycle. The problem is with the Gui object referring indirectly to itself. If you were to bind the Gui object to a global or nested function and pass that to OnEvent, you would create a cycle even though you haven't used a method of the Gui subclass.

Consider just the one Gui reference held by the event handler. When will this reference be released? When the event handler is deleted. The event handler will be deleted when you deregister it with OnEvent, or when the Gui is destroyed. The Gui is destroyed when you call Destroy or the object is deleted as a result of its last reference being released. Under normal circumstances, the object's last reference being released means that there aren't any other references to release; i.e. the event handler's reference was already released, so the former won't cause the latter. That leaves only Destroy or OnEvent as possible ways that the reference will be released. If those aren't done, the Gui will not be deleted.

I have updated the documentation:
wpb
Posts: 150
Joined: 14 Dec 2015, 01:53

Re: Passing GUI events to another GUI / letting GUI events fall through

02 Apr 2024, 01:01

@lexikos, this documentation you've added is absolutely fantastic. It's really got to the core of the issue(s) in a clear and consise way, and I for one appreciate it very much. A big thank you from me.

Return to “Ask for Help (v2)”

Who is online

Users browsing this forum: akirofe and 27 guests