Add custom items to existing right-click menu's

Get help with using AutoHotkey (v1.1 and older) and its commands and hotkeys
Shortcut18

Add custom items to existing right-click menu's

19 Oct 2017, 09:12

This has been my most frustrating AHK experience as of yet and I need your help perfecting my code.

I've been trying to:
1) add custom items to an existing right-click context menu, and
2) when that item is clicked, execute something. Lastly,
3) add some of those items in a submenu.

I managed 1 and 2 but 3 is giving me headaches.

Script so far:

Code: Select all

~RButton::
n := MenuHack([3],[1,"item1"],[1,"item2"])
if (n = item1)
{
	do stuff
}


; I used arrays because I wanted to be able to distinguish between 'regular' items, submenu's and seperators. I can get submenus to work but the items added in a submenu cannot be activated (you can click them but nothing happens).

; Part 1 (adding items)

MenuHack(arr*)
{
	; Identify right window
	hWin := DllCall("GetParent", UInt,WinExist("A")), hWin := !hWin ? WinExist("A") : hWin
	
	; wait for context menu to open
	WinWait, ahk_class #32768
	
	; identify menu handle
	SendMessage, 0x1E1, 0, 0		; MN_GETHMENU
	hMenu := ErrorLevel
	
	; add new items
	For each, item in arr
	{
		if (item[1] = 1)
		{
			DllCall("AppendMenu", "Int", hMenu, "Int", MF_STRING, "Int",, "Str", Item[2])
			p%A_Index% := GetMenu(hMenu, Item[2], hWin)
		}
		if (item[1] = 3)
		{
			; add separator
			DllCall("AppendMenu", "Int", hMenu, "Int", MF_SEPARATOR, "Int", 0, "Int", "") 
		}
	}


; Part 2 (do something when clicked)	

	; wait for click but not forever
	Loop
	{
		IfWinNotExist, ahk_class #32768
		{
			return false
		}
		KeyWait, Lbutton, D
		KeyWait, Lbutton, U
		
		CoordMode, Mouse, Screen
		MouseGetPos, x, y
		
		For each, item in arr ; check whole array to see if mouse position matches item rectangle
		{
			if (item[1] = 1)
			{
				If ((x >= p%A_Index%.x1) AND (x <= p%A_Index%.x2)) AND ((y >= p%A_Index%.y1) AND (y <= p%A_Index%.y2))
				{
					return item[2] ; return item title
				}
			}
		}
	}
	return false
}	


; I modified some code by Sean (found here https://autohotkey.com/board/topic/39011-how-to-get-popup-menu-items/) to get the Item Rectangle

GetMenu(hMenu, Item, hWin)
{
	Loop, % DllCall("GetMenuItemCount", "Uint", hMenu)
	{
		idx := A_Index - 1
		idn := DllCall("GetMenuItemID", "Uint", hMenu, "int", idx)
		nSize++ := DllCall("GetMenuString", "Uint", hMenu, "int", idx, "Uint", 0, "int", 0, "Uint", 0x400)
		VarSetCapacity(sString, 100, 0) ;nSize+1)
		DllCall("GetMenuString", "Uint", hMenu, "int", idx, "str", sString, "int", nSize, "Uint", 0x400)	;MF_BYPOSITION
		If (sString = Item)
		{
			varSetCapacity( RECT, 16, 0 )
			DLLCall("GetMenuItemRect", "Uint", hWin, "Uint", hMenu, "Uint", idx, "Uint", &RECT )
			Return {x1:DecodeInteger("int4", &rect, 0, 0 ),y1:DecodeInteger( "int4", &rect, 4, 0 ),x2:DecodeInteger( "int4", &rect, 8, 0 ),y2:DecodeInteger( "int4", &rect, 12, 0 ) }
			
		}
	}
	Return false
}

So, I have code to add a submenu:

Code: Select all

if (item[1] = 2)
{
subMenu%A_index% :=  DllCall("CreateMenu")
DllCall("AppendMenu", "Int", hMenu, "Int", 16, "UPtr", subMenu%A_index%, "Str", Item[2]) ; add submenu
s := subMenu%A_index%
a := A_index
Loop % item.MaxIndex()
{
	if (A_Index > 2)
	{
		DllCall("AppendMenu", "Int", s, "Int", MF_STRING, "Int",, "Str", Item[A_Index]) ; submenu items
		p%a%%A_Index% := GetMenu(hMenu, Item[A_Index], hWin) ; rect of submenu
	}
}



;
; To use this you need to specify an array to the function as follows:
;  MenuHack([2,"submenuTitle","submenuItem1","submenuItem2"])
;
Problem is that the Item rectangle of the items inside the submenu is not retrievable when it is not yet drawn (when it's not open).

So i'm basically looking for a different way to detect when what item is clicked.
I tried WM_COMMAND but those are not registered by AHK when coming from a context menu
I tried DLLCalling "MenuItemFromPoint" which looked promising and actually works, but only sometimes (?)

Help?
User avatar
Godvicien
Posts: 5
Joined: 25 Jun 2017, 13:26

AHK Add Items to Context Menu with Submenus

03 Sep 2019, 04:22

AHK Add Items to Context Menu with Submenus

Hello!

Success: topic solved!

Fonctionnalities:
– Clarified Code for opensource projects.
– Easy to understand, with all comments.
– Uses: Items, Separators, and Submenus.
– Position of Items where you want (inserted), like in 2nd position: just after 'Open' Item.
– Can be used for: files, folders, multiples, empty selection...
– Submenus with Items and Separators (no Submenus of Submenu).
– It works with special screens as: 2K, 4K, 8K ...

I wrote this function from scratch as part of a large opensource project (Recycle.bin for USB), that answers to this topic, based on it.

I think this topic is definitely solved. You can copy/paste all the code and try it to see if it works.

Nico.Godvician: french / sorry for my poor english


How to use:

Code: Select all

; « Right Click Mouse Button » –> Context menu opening, then modify it:
;***********************************************************************
~RButton::   ; Hook Right Click

; 2 Classes for explorer:
Global Win_Explorer_Class1 := "ExploreWClass" ; New Class
Global Win_Explorer_Class2 := "CabinetWClass" ; Old Class

; Witch active program is it ?
WinGetClass, ClassCurrentWindow, A ; Get class of current Window

; Here we need: explorer.exe
If ( ClassCurrentWindow = Win_Explorer_Class1 ) or ( ClassCurrentWindow = Win_Explorer_Class2 ) ; 2 classes for explorer
	Goto Label_MenuContext_Explorer 

Return ; End of Right Click
;**************************



Label_MenuContext_Explorer: 
;***************************

; Global vars:
Global MenuContext_RightClickAgain := "-> MenuContext: Right Click Again instead of regular click <-" 
Global MenuContext_NoClicked_AddedItem := "-> MenuContext: Added items have not been clicked <-"
Global MenuContext_Separator := "-> MenuContext: Separator <-" 
Global MenuContext_Item := "-> MenuContext: Standard Item <-"
Global MenuContext_Submenu := "-> MenuContext: Popup Submenu <-"

;Value of items to add:
AddItem_Hello1 := "Hi!"
AddItem_Hello2 := "Bonjour"
AddItem_Hello3 := "Buenos dias"
AddItem_Hello4 := "Hallo"
AddItem_Folder := "Parent's Folder"

; Position of items in the menu:
InsertItemsAtPosition := 2 ; Here just after the 1st standard item: 'Open'

ArrayToAddItems := { } ; Object as array of items to add into the context menu

; Add items to array:

ArrayToAddItems.Push( Fct_MenuContext_Separator() )  ; Separator

ArrayToAddItems.Push( Fct_MenuContext_Item( "Welcome" ) ) ; Standard item

ArrayToAddItems.Push( Fct_MenuContext_Item( AddItem_Folder ) ) ; Standard item

ArrayToAddItems.Push( Fct_MenuContext_Separator() ) 

;Submenu, with an array of Subitems:
ArrayToAddItems.Push( Fct_MenuContext_Submenu( "Say Hello" ; Name of submenu ; And Array of Items:
	,[ AddItem_Hello1 		; 1st Item
	, MenuContext_Separator 	; Separator...
	, AddItem_Hello2
	, AddItem_Hello3
	, AddItem_Hello4
	, "Test" ] ) )

ArrayToAddItems.Push( Fct_MenuContext_Separator() ) 

TargetProgram_Classes := [ Win_Explorer_Class1, Win_Explorer_Class2 ]



; Function that add the items to the context menu, and return the clicked item
ClickedItem := Fct_Get_ClickedItem_MenuContext_AddedItems( TargetProgram_Classes, InsertItemsAtPosition, ArrayToAddItems* )



If ( ClickedItem = MenuContext_RightClickAgain )
	Goto Label_MenuContext_Explorer ; When User do right click again, We must rebuild the items 

Else If ( ClickedItem = AddItem_Folder ) {
	SplitPath, A_ScriptDir, , ParentFolder
	Run, %ParentFolder%
}

Else If ( ClickedItem = MenuContext_NoClicked_AddedItem ) 
	Return

Else If ( SubStr( ClickedItem, 1, 2 ) =  "->" ) ; Error
	Return

Else If ( ClickedItem = False )
	Return

Else 
	MsgBox % ClickedItem

Return ; End Label_Menu_Context_Explorer
;******


Don't forget these tools functions:

Code: Select all

Fct_MenuContext_Separator() {
	Return { Type: MenuContext_Separator }
}

Fct_MenuContext_Item( Name ) {
	Return { Type: MenuContext_Item, Name: Name }
}

Fct_MenuContext_Submenu( Name, ArraySubitems ) {
	Return { Type: MenuContext_SubMenu, Name: Name, Items: ArraySubitems }
}


The function that add the items to the context menu, and return the clicked item:

Code: Select all

; Writed by  Nico.Godvician  /  Code Opensource - CC: BY-NC-SA 2.0  –> Copy thsee lines
; Pubished:  2019/09/21 –> https://www.autohotkey.com/boards/viewtopic.php?f=76&t=38579&p=291014#p290862
; Function that add the items to the context menu, and return the clicked item
;*********************************************************************************************************
Fct_Get_ClickedItem_MenuContext_AddedItems( TargetProgram_Classes, InsertItemsAt_Position, ArrayOf_Items* ) 
;**********************************************************************************************************
{
	; Get handle of the target program:
	For each, ProgramClass in TargetProgram_Classes 
	{
		; Handle of active window:
		If Program_Handle := WinActive( "ahk_class " ProgramClass ) ; Handle return false if class it's not active
			Break ; Program is active, then OK
	} ; Or not if the loop ( for each ) is finished
	
	; Check we are really in the right active program:
	If not Program_Handle ; False or Handle
		Return "-> Not into the right program <-"	
	
	; Class #32768 is for all standard windows context menu:
	Global MenuContext_AhkClass := "ahk_class #32768" 
	
	; Wait context menu appears:
	WinWait, %MenuContext_AhkClass% ;
	
	; Context menu opened –> Get handle:
	MN_GETHMENU := 0x1E1 ; Shell Constant: "Menu_GetHandleMenu"
	SendMessage, MN_GETHMENU, False, False 
	MenuContext_Handle := ErrorLevel ; Return Handle in ErrorLevel
	
	
	;***************************
	; 	Section: Add Items:
	;***************************
	
	; Constants for menus in User32.dll:
	Static MF_SEPARATOR := 0x800
	Static MF_STRING := 0x0
	Static MF_POPUP := 0x10
	Static MF_BYPOSITION := 0x400 
	
	; Add each new item into the context menu:
	For each, ItemToAdd in ArrayToAdd_Items :=  ArrayOf_Items
	{
		; Save absolut position of this Item in the menu: 
		ItemToAdd.Position := InsertItemsAt_Position-1 + A_Index-1 ; Zero based
		
		; Add Separator:
		If ( ItemToAdd.Type == MenuContext_Separator ) 
		{
			; Insert Separator: –> https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-InsertMenuA
			DllCall( "User32\InsertMenu"
					,"UPtr", MenuContext_Handle
					,"UInt", ItemToAdd.Position ; At the specified position
					,"UInt", MF_SEPARATOR + MF_BYPOSITION
					,"UPtr", False
					,"UInt", False )
		}
		
		; Add Classic Item:
		Else If ( ItemToAdd.Type == MenuContext_Item )
		{
			; Insert text of item: –> https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-InsertMenuA
			DllCall( "User32\InsertMenu" 
					,"UPtr", MenuContext_Handle
					,"UInt", ItemToAdd.Position ; At the specified position
					,"UInt", MF_STRING + MF_BYPOSITION
					,"UPtr", False 
					,"Str", ItemToAdd.Name ) ; Insert Value ( text )
		}
		
		; Add Submenu and its Subitems:
		Else If ( ItemToAdd.Type == MenuContext_Submenu )
		{
			AddSubmenu := ItemToAdd ; Renames to clarify
			; Create Submenu, and return handle:
			AddSubmenu.Handle := DllCall( "User32\CreatePopupMenu" ) 
			
			; Insert Submenu into the context menu: –> https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-InsertMenuA
			DllCall( "User32\InsertMenu"
					,"UPtr", MenuContext_Handle
					,"UInt", AddSubmenu.Position  ; At the specified position
					,"UInt", MF_STRING + MF_BYPOSITION + MF_POPUP
					,"UPtr", AddSubmenu.Handle
					,"Str", AddSubmenu.Name )
			
			; Now add each Item and Separator into this Submenu:
			For each, ItemOfSubmenu in AddSubmenu.Items 
			{
				; AppendMenu –> https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-AppendMenuA
				
				; In case of Separator:
				If ( ItemOfSubmenu == MenuContext_Separator ) 
					DllCall( "User32\AppendMenu"
							,"UPtr", AddSubmenu.Handle
							,"UInt", MF_SEPARATOR
							,"UPtr", False, "UInt", False )
				
				Else  ; In case of Subintem: insert Value as text:
					DllCall( "User32\AppendMenu"
							,"UPtr", AddSubmenu.Handle
							,"UInt", MF_STRING
							,"UPtr", False
							,"Str", ItemOfSubmenu )
			}
		}
		Continue ; Continue to add next item...
	}
	; End for each add items
	
	
	;***************************
	; 	Section: Wait Click:
	;***************************
	
	Label_Wait_New_Click: ; When User clicks on Separator: wait new click
	
	; Wait User do a regular or right click:
	While not GetKeyState( "LButton" ) and not GetKeyState( "RButton" ) 
	{
		; Fix error if 2 right clicks are very too close: menu disappears sometimes for some apps:
		If not WinExist( MenuContext_AhkClass ) {
			Send {RButton} 	; Reopen context menu when disappears
			Return MenuContext_RightClickAgain ; Refill the menu 
		}
	}
	
	; Is it a right click on another file ? This means that the menu is closed and reopening...
	If GetKeyState( "RButton" )
		Return MenuContext_RightClickAgain ; Then refill the menu
	;TODO: Fix error when the User right-clicks on context menu, it's filled up again

	; Else Yes: Click into an item, but witch item ?
	
	
	;***********************************
	; 	Section: Get Clicked Item:
	;***********************************
	
	; Get position of mouse into screen:
	CoordMode, Mouse, Screen
	MouseGetPos, MouseScreenX, MouseScreenY  ; Int vars: 4 octets
	
	; POINT –> https://docs.microsoft.com/fr-fr/previous-versions/dd162805(v=vs.85)
	; Create a generic C++ POINT{x,y} with a 'ULongLong' –> 'Int64' 
	; X start at the 1st ULong (right), and Y start at 2nd ULong (32th bit on left):
	MousePointScreen := x := MouseScreenX  | y := ( MouseScreenY << 32 ) 
	
	;Calculate DPI of special screen:  1K, 2K, 4K, 8K, etc...
	WinDPIMultiplicator := A_ScreenDPI / 96  ; 96 is the standard DPI screen: 1K (1600x900)
	
	; Check if clicked item is into the new added items:
	For each, AddedItemInMenu in ArrayOfAdded_Items := ArrayOf_Items 
	{
		; Click on Separator –> Disable:
		If ( AddedItemInMenu.Type == MenuContext_Separator )
		{
			; Get Rectangle of Separator:
			VarSetCapacity( ItemRectangle, 16, 0 ) ; Create Rectangle of 16 octets: 4 corners of Int (4 octets)
			
			; Fill Rectangle: –> https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-GetMenuItemRect
			isFilledRectangle := DLLCall( "User32\GetMenuItemRect"
									,"UPtr", Program_Handle
									,"UPtr", MenuContext_Handle
									,"UInt", AddedItemInMenu.Position ; Absolut position in the context menu
									,"UPtr", &ItemRectangle )
			; Is clicked on separator ?
			If isFilledRectangle 
			and isPointIntoRectangle := DllCall( "User32\PtInRect", "UPtr", &ItemRectangle, "Int64", MousePointScreen )
				Goto Label_Wait_New_Click 
		}
		
		; Click on added Item –> Return Value
		Else If ( AddedItemInMenu.Type == MenuContext_Item ) 					
		{
			VarSetCapacity( ItemRectangle, 16, 0 )
			; https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-GetMenuItemRect
			isFilledRectangle := DLLCall( "User32\GetMenuItemRect"  
									,"UPtr", Program_Handle
									,"UPtr", MenuContext_Handle
									,"UInt", AddedItemInMenu.Position 
									,"UPtr", &ItemRectangle )
			If isFilledRectangle 
			and isPointIntoRectangle := DllCall( "User32\PtInRect", "UPtr", &ItemRectangle, "Int64", MousePointScreen ) 
				Return AddedItemInMenu.Name
		}
		
		; Click on Item of Submenu –> Check each Subitem:
		Else If ( AddedItemInMenu.Type == MenuContext_Submenu )  				
		{
			Loop 3 ; 3 times, because sometimes this function does not work 1 or 2 times:
				For each, ItemInSubmenu in ( ItemIsSubmenu := AddedItemInMenu ).Items 
				{
					ItemPositionInMenu := A_Index
					
					; Get Rectangle for Subitem
					VarSetCapacity( ItemRectangle, 16, 0 ) 
					; https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-GetMenuItemRect
					isFilledRectangle := DLLCall( "User32\GetMenuItemRect"  
											,"UPtr", False ; Indicate Submenu, instead of window
											,"UPtr", ItemIsSubmenu.Handle ; Handle of the Submenu
											,"UInt", ItemPositionInMenu -1 ; Zero based
											,"UPtr", &ItemRectangle )
					
					; Get each corner of Rectangle:
					ItemRectangleX1 := NumGet( &ItemRectangle, 0, "Int" ) ; Int –> 4 octets
					ItemRectangleY1 := NumGet( &ItemRectangle, 4, "Int" )
					ItemRectangleX2 := NumGet( &ItemRectangle, 8, "Int" )
					ItemRectangleY2 := NumGet( &ItemRectangle, 12, "Int" )
					
					; Use DPI multiplicator for special screen (2K, 4K, ...):
					isMouseInto4Corners :=  ( MouseScreenX >= ItemRectangleX1 *WinDPIMultiplicator ) 
									and ( MouseScreenX <= ItemRectangleX2 *WinDPIMultiplicator ) 
									and ( MouseScreenY >= ItemRectangleY1 *WinDPIMultiplicator ) 
									and ( MouseScreenY <= ItemRectangleY2 *WinDPIMultiplicator ) 
					
					;ToolTip % "isFilledRectangle: " isFilledRectangle "`n" "isMouseInto4Corners: " isMouseInto4Corners "`nX: " ItemRectangleX1*WinDPIMultiplicator "<" MouseScreenX ">" ItemRectangleX2*WinDPIMultiplicator "`nY: " ItemRectangleY1*WinDPIMultiplicator "<" MouseScreenY ">" ItemRectangleY2*WinDPIMultiplicator, , , 5
					
					; Check if mouse is into this Rectangle: 
					If isFilledRectangle and isMouseInto4Corners
					{
						; Click on Separator ?
						If ( ItemInSubmenu == MenuContext_Separator ) ; Disable Separator:
							Goto Label_Wait_New_Click ; Wait new click
						
						Else ; Standard text Subitem: 
							Return ItemInSubmenu ; Return value of item
					}
				}
		}
		; End of one item: this is not this item
		Continue ; Then check next item...
	}
	; End: For all the new items, all checked without return
	
	; Then clicked Item is probably on another item of context menu ( like 'Open' ):
	Return MenuContext_NoClicked_AddedItem
}
;End Fct_Get_ClickedItem_MenuContext_AddedItems


Last edited by Godvicien on 23 Sep 2019, 11:16, edited 50 times in total.
User avatar
Godvicien
Posts: 5
Joined: 25 Jun 2017, 13:26

AHK Add Items to Context Menu with Submenus

04 Sep 2019, 07:27

Hope it helps!
Last edited by Godvicien on 14 Oct 2019, 10:47, edited 26 times in total.
User avatar
Godvicien
Posts: 5
Joined: 25 Jun 2017, 13:26

AHK Add Items to Context Menu with Submenus

17 Sep 2019, 02:49

You can replace:

Code: Select all

; Here we need: explorer.exe
If ( ClassCurrentWindow = Win_Explorer_Class1 ) or ( ClassCurrentWindow = Win_Explorer_Class2 ) ; 2 classes for explorer
	Goto Label_MenuContext_Explorer

By:

Code: Select all

; Here we need: explorer.exe
If ( ClassCurrentWindow = Win_Explorer_Class1 ) or ( ClassCurrentWindow = Win_Explorer_Class2 ) ; 2 classes for explorer
	SetTimer, Label_MenuContext_Explorer, -1

Last edited by Godvicien on 29 Oct 2019, 06:55, edited 8 times in total.
Didier3L
Posts: 14
Joined: 27 Dec 2016, 09:33

Re: Add custom items to existing right-click menu's

25 Oct 2019, 12:23

Hello
How does one make it work in another program other than Explorer :?:
Outlook, firefox :?:
User avatar
Godvicien
Posts: 5
Joined: 25 Jun 2017, 13:26

Re: Add custom items to existing right-click menu's

27 Oct 2019, 01:34

Hello

You have to use the Window_Spy of autohotkey to search for the "ahk_class" of your program.
But the code itself only works with Windows classic menus, essentially: "explorer.exe"

Another example with the "Notepad++" class that You can put in the function:

Code: Select all

ClickedItem := Fct_Get_ClickedItem_MenuContext_AddedItems(   ["Notepad++"]  ,  InsertItemsAtPosition, ArrayToAddItems* )
For G-Chrome or Outlook it's not possible because they use their own menus ...
payaAHK
Posts: 106
Joined: 30 Oct 2016, 15:28

Re: Add custom items to existing right-click menu's

13 Jun 2020, 07:31

hello
fine
but numerous lines are not showed on rclick
need to pass mouse downd to show tem
any clues
regards ap

Return to “Ask for Help (v1)”

Who is online

Users browsing this forum: LAPIII, peter_ahk and 323 guests