A simple User Guidance for AHK apps

Post your working scripts, libraries and tools.
Spitzi
Posts: 309
Joined: 24 Feb 2022, 03:45

A simple User Guidance for AHK apps

Post by Spitzi » 24 May 2023, 08:42

I use this function to guide users in using my application. It shows a GUI with some text on it, that follows the mouse. On the gui, info for the user on what to do next (click a button etc.) is shown. The gui is destroyed when the user executes the defined action (can be a LClick, MClick, RClick, LDrag, MDrag, RDrag).

Maybe this is helpful for somebody. I got tons of helf from this forum, so here's some return on investment (if only little). There's probalby also alot of room for improvement, so any feedback is appreciated as well.

Greets.

Code: Select all

; Example
success := ShowInfoGuiUntilNextEvent("Now click on button xy", event:="LClick", &clickX:=0, &clickY:=0, &dragToX:=0, &dragToY:=0, fontColor:="Red", fontSize:=20, bgColor:="AAAAAA", offsetX:=-250, offsetY:=50, guiWidth:=500, timeOut:=10000)
MsgBox("Action was timed out or escaped: " . !success)
return


; shows a transparent info gui that follows the mouse, to tell the user what to do for next mouse click
; - msg: the message to show
; - event: the event that is waited for to make the gui disappear again, can be LClick, LClickOrDrag, LDrag, MClick, MClickOrDrag, MDrag, RClick, RClickOrDrag, RDrag, X = any event
; - fontColor: the color of the text
; - fontSize: the size of the text
; - bgColor: the color of the background. use "AAAAAA" for transparent
; - offsetX/Y: the offset from the mouse pointer
; - guiwidth: the width of the gui in pixels
; - timeOut: time in Milliseconds, before the GUI is dismissed. 0 is never
; returns true if the user clicked, false if escape was pressed
; returns by reference the location where click occurred (clickX and clickY)
ShowInfoGuiUntilNextEvent(msg, event:="LClick", &clickX:=0, &clickY:=0, &dragToX:=0, &dragToY:=0, fontColor:="Red", fontSize:=20, bgColor:="AAAAAA", offsetX:=20, offsetY:=20, guiWidth:=1000, timeOut:=0) {
	global showInfoGui := true																; global variables to communicate between functions
	global infoGuiQuitEvent := ""
	global infoGuiOffsetX := offsetX
	global infoGuiOffsetY := offsetY
	global infoGuiEvent := event
	global infoGuiClickX := 0
	global infoGuiClickY := 0
	global infoGuiDragToX := 0
	global infoGuiDragToY := 0
	global infoGuiTimeOutText
	startTime := A_TickCount

	if WinExist("InfoGuiWinTitle")															; if function is called when a window is being shown - immediatly return
		return

	; 1) create GUI
	InfoGui := Gui("+LastFound +AlwaysOnTop -DPIScale -Caption +ToolWindow +Border", "InfoGuiWinTitle")
	InfoGui.BackColor := bgColor, InfoGui.MarginX := "5", InfoGui.MarginY := "5"

	InfoGui.SetFont("s12 underline  c" . fontColor, "Verdana")
	InfoGui.Add("Text", "section", "MyApp User Guidance")
	InfoGui.SetFont("s12 norm  c" . fontColor, "Verdana")
	InfoGui.Add("Text", "x+10", "(hit 'Esc' to chancel)")
	if (timeOut > 0)
		timeoutTextCtrl := InfoGui.Add("Text", "x+5 vinfoGuiTimeOutText", "- TimeOut in XXX")
	InfoGui.SetFont("s" . fontSize . " norm bold  c" . fontColor, "Verdana")
	InfoGui.Add("Text", "xs y+5 w" . guiWidth, msg)
	InfoGui.Show("NoActivate AutoSize")

	if (bgColor == "AAAAAA")
		WinSetTransColor("AAAAAA", "InfoGuiWinTitle")

	; 2) wait for next click or esc to delete gui again - showing timeout if necessary
	while (showInfoGui && (((A_TickCount - startTime) < timeOut) || (timeOut == 0))  ) {
		if (timeOut > 0) {
			timeLeft := Round((timeOut - (A_TickCount - startTime)) / 1000)
			timeoutTextCtrl.Value := "- TimeOut in " timeLeft "s"
		}
		MoveInfoGui()
		Sleep(50)
	}

	; 3) clean up
	InfoGui.Destroy()
	clickX := infoGuiClickX
	clickY := infoGuiClickY
	dragToX := infoGuiDragToX
	dragToY := infoGuiDragToY

	return (infoGuiQuitEvent == event)
}

#HotIf WinExist("InfoGuiWinTitle")
	~LButton:: DetectMouseButtonEvent("LButton")						; ~ send LButton also to underlying app
	~MButton:: DetectMouseButtonEvent("MButton")							; keep MButton to script - don't want to start annoying autoscroll-mode in Word
	~RButton:: DetectMouseButtonEvent("RButton")						; ~ send RButton also to unterlying app
	Esc:: QuitInfoGui("Esc")
#HotIf ;WinExist("InfoGuiWinTitle")

; moves the info gui
MoveInfoGui() {
	global infoGuiOffsetX, infoGuiOffsetY
	CoordMode("Mouse", "Screen")
	MouseGetPos(&mx, &my)
	wx := mx + infoGuiOffsetX
	wy := my + infoGuiOffsetY
	WinMove(wx, wy, , , "InfoGuiWinTitle")
	; WinMove, InfoGuiBackgroundWinTitle, , %wx%, %wy%
}

; sets the information to quit the gui and what key was the reason (the quit key)
QuitInfoGui(quitEvent) {
	global showInfoGui, infoGuiQuitEvent
	showInfoGui := false
	infoGuiQuitEvent := quitEvent
}

; determines if a leftclick, middleclick or rightclick was a drag
; - buttonName: is either LButton, MButton, RButton
; returns true if it was a drag
; returns false if the click was less than 200ms, or if drags counts as click
DetectMouseButtonEvent(buttonName := "LButton") {
	global showInfoGui
	global infoGuiDragIsClick
	global infoGuiEvent 												; the event that is waited for to make the gui disappear again, can be LClick, LClickOrDrag, LDrag, MClick, MClickOrDrag, MDrag, RClick, RClickOrDrag, RDrag
	global infoGuiQuitEvent												; the event that triggered quitting the gui
	global infoGuiClickX, infoGuiClickY, infoGuiDragToX, infoGuiDragToY	; the coordinates of the event
	startTime := A_TickCount

	if (infoGuiEvent == "X") {
		eventHappened := true
	} else if (SubStr(buttonName, 1, 1) == SubStr(infoGuiEvent, 1, 1)) {			; if the event is for the correct button

		CoordMode("Mouse", "Screen")
		MouseGetPos(&infoGuiClickX, &infoGuiClickY)

		while GetKeyState(buttonName, "P") {							; while Button is pressed
			MoveInfoGui() 												; move info gui as well during drag
			Sleep(30)													; ... wait until it is released
		}
		MouseGetPos(&infoGuiDragToX, &infoGuiDragToY)
		wasDrag := duration := (A_TickCount - startTime) > 200			; a drag is more than 200ms long

		switch SubStr(infoGuiEvent, 2) {								; what event was it?
			case "Click": eventHappened := !wasDrag
			case "Drag": eventHappened := wasDrag
			case "ClickOrDrag": eventHappened := true
			Default: eventHappened := false
		}

	} else {
		eventHappened := false											; the defined event is for another button -> return false
	}

	if eventHappened {
		showInfoGui := false
		infoGuiQuitEvent := infoGuiEvent
	}
}


Return to “Scripts and Functions (v2)”