TreeView with tri-state checkboxes - anyone know how?

Get help with using AutoHotkey (v1.1 and older) and its commands and hotkeys
ahketype
Posts: 191
Joined: 27 Oct 2016, 15:06
Location: Yorkshire, UK

TreeView with tri-state checkboxes - anyone know how?

19 Jan 2024, 14:34

I'd like to have tri-state checkboxes in a Treeview control, and it looks like it's feasible, but I don't know how to do it. User rbrtryn seemed to have a solution in 2013 using TVM_SETITEM message. Unfortunately, the link to his Dropbox where he had an example is now 404, and Windows API calls are a dark art to me. If anyone has a solution, it would be much appreciated!
colt
Posts: 291
Joined: 04 Aug 2014, 23:12
Location: Portland Oregon

Re: TreeView with tri-state checkboxes - anyone know how?

20 Jan 2024, 01:11

We are having some horrible weather right now, so i let myself get carried away with this one.

If you don't use icons on your treeview, then the following code can mimic what you ask. Right now there are 5 available check states, but you can add or replace icons in the iconLookup variable as needed.
It hinges on a class to keep track of the state of each node.

There is a slight bug, however. If you double click on a standard checkbox it will just toggle the value, but if you double click on an icon it will collapse the row. Maybe not a big deal... i wonder if there is a way to intercept and block that action.

demo.jpg
demo.jpg (29.71 KiB) Viewed 166 times

Code: Select all

;"C:\Windows\ShellNew\Template.ahk"
#NoEnv
#SingleInstance Force
SetBatchLines -1
coordmode, tooltip , Window

;struct used for determining if clicking "CHECKBOX" or "ROW"
;https://www.autohotkey.com/boards/viewtopic.php?t=61294
global TVHITTESTINFO
VarSetCapacity(TVHITTESTINFO, A_PtrSize=8?24:16)
NumPut(0, &TVHITTESTINFO, 0, "Int") ;pt ;x
NumPut(0, &TVHITTESTINFO, 4, "Int") ;pt ;y
NumPut(0, &TVHITTESTINFO, 8, "UInt") ;flags
NumPut(0, &TVHITTESTINFO, (A_PtrSize = 8) ? 16 : 12, "Ptr") ;hItem

;icons to replace checkbox
ImageListID := IL_Create() 

;variable that stores available checkstates 
global iconLookup := ["empty.ico","cross.ico","filled.ico","green.ico","red.ico"]
for index,fn in iconLookup
{
	IL_Add(ImageListID, iconLookup[index], index, True)
}

margin := 5
w := 300
h := 400
gw := margin*3 + w*2
gh := margin*2 + h
Gui, Add, TreeView,x%margin% y%margin% h%h% w%w% hwndtv1HWND gtvClickEvent vgTv1 altsubmit ImageList%ImageListID% 
Gui, Add, TreeView,yp hp wp x+%margin% hwndtv2HWND gtv2ClickEvent vgTv2 altsubmit ImageList%ImageListID% 
Gui, Show ,w%gw% h%gh%,Treeview Test

;build first treeview
tv := new tvObj(tv1HWND) 
a := tv.addNode("A",icon := 1)
a.addNode("AA",icon := 2)
b := tv.addNode("B",icon := 3)
bb := b.addNode("BB",icon := 4)
bb.addNode("BBB",icon := 5)
cc := b.addNode("CC",icon := 5)
cc.addNode("CCC",icon := 4)
tv.expandChildren()

;build second treeview
tv2 := new tvObj(tv2HWND) 
a := tv2.addNode("A2",icon := 1)
a.addNode("AA2",icon := 2)
b := tv2.addNode("B2",icon := 3)
bb := b.addNode("BB2",icon := 4)
bb.addNode("BBB2",icon := 5)
cc := b.addNode("CC2",icon := 5)
cc.addNode("CCC2",icon := 4)
tv2.expandChildren()
return

tvClickEvent:		
	if(A_GuiEvent == "Normal")
	{		
		tooltip % A_GuiControl . ":" . A_GuiEvent . ":" . A_EventInfo, 0, -25
		performCheckOperation(tv,A_EventInfo)
	}
	else
	{
		;tooltip % "UNHANDLED EVENT : " . A_GuiEvent ,0,-50
	}	
	setTimer clearTT, -2000
return

tv2ClickEvent:	
	if(A_GuiEvent == "Normal")
	{		
		tooltip % A_GuiControl . ":" . A_GuiEvent . ":" . A_EventInfo , 0, -25
		performCheckOperation(tv2,A_EventInfo)		
	}
	else
	{
		;tooltip % "UNHANDLED EVENT : " . A_GuiEvent ,0,-50
	}
	setTimer clearTT, -2000
return

clearTT:
	tooltip
return

performCheckOperation(activeTV,tvID) ;increment the checkbox to next icon
{
	hoveredFlag := whatInRowIsHovered(activeTV.rootHWND) ;make sure they are hovering over the icon when clicked (icon: flag = 2)
	if(hoveredFlag == 2)
	{
		selNode := activeTV.getNode(tvID) ;search treeview obj for node that matches id
		incrementIcon(selNode) ;update the icon and node properties
		return true
	}
	return false
}

whatInRowIsHovered(ctrlHWND)
{
	;https://www.autohotkey.com/boards/viewtopic.php?t=61294	
	static TVM_HITTEST := 0x1111
	coordmode mouse,Window 
	MouseGetPos ,mx,my	
	ControlGetPos ,cx,cy,,,, ahk_id %ctrlHWND% 	
	x := mx - cx
	y := my - cy	
	;ToolTip % cx . "," . cy . "`n" . mx . "," . my . "`n" . x . "," . y 
	NumPut(x, &TVHITTESTINFO, 0, "Int") ;pt ;x
	NumPut(y, &TVHITTESTINFO, 4, "Int") ;pt ;y
	SendMessage % TVM_HITTEST, 0, % &TVHITTESTINFO,, % "ahk_id " . ctrlHWND
	;msgbox % "hItem > " . (hItem:=ErrorLevel)
	;msgbox % "flag > " . NumGet(TVHITTESTINFO, 8, "UInt")
	flag := NumGet(TVHITTESTINFO, 8, "UInt")
	;tooltip % "flag > " . flag
	return flag
}

incrementIcon(selecteNode)
{
	curIcon := selecteNode.icon
	curIcon++
	if(curIcon > iconLookup.count())
	{
		curIcon := 1
	}
	selecteNode.changeIcon(curIcon)
	selecteNode.changeText(strSplit(selecteNode.text," ")[1] . " - MODIFIED TO ICON : " . iconLookup[curIcon])
}

GuiClose: 
	if(getKeyState("CTRL"))
	{
		reload
	}
	ExitApp	
return

class tvObj
{
	__new(rootHWND,parent := 0,text := "",icon := 1)
	{
		this.rootHWND := rootHWND
		this.children := {}
		this.parent := parent
		if(text) ;assuming root node will be only node without text
		{			
			this.icon := icon
			this.text := text			
			this.tvID := TV_Add(this.text, this.parent,"Icon" . this.icon)	
		}
	}
	activate()
	{
		Gui,TreeView,% this.rootHWND
	}
	addNode(text,icon := 1)
	{	
		this.activate()
		tvNode := new tvObj(this.rootHWND,this.tvID,text,icon)
		this.children[tvNode.tvID] := tvNode		
		return tvNode 
	}
	expandChildren()
	{
		this.activate()
		for tvID,node in this.children
		{
			TV_Modify(tvID,"Expand")
			node.expandChildren()			
		}
	}
	getNode(tvIdSearch)
	{	
		if(res := this.children.haskey(tvIdSearch))	;it is direct child	of this node
		{			
			return this.children[tvIdSearch] 
		}
		else
		{
			for tvID,node in this.children
			{
				if(res := node.getNode(tvIdSearch))
				{
					return res ;was somwhere downstream
				}				
			}
		}
		return false ;not in this branch
	}	
	changeIcon(icon)
	{
		this.activate()
		this.icon := icon
		TV_Modify(this.tvID,"Icon" . this.icon)
	}
	changeText(text)
	{
		this.activate()
		this.text := text
		TV_Modify(this.tvID,,this.text)
	}
}
Attachments
icons.zip
icons needed for iconlookup, or you can create your own 12x12 ico file.
(1.11 KiB) Downloaded 30 times
ahketype
Posts: 191
Joined: 27 Oct 2016, 15:06
Location: Yorkshire, UK

Re: TreeView with tri-state checkboxes - anyone know how?

20 Jan 2024, 19:29

Thanks, @colt, three icons is the route I started going down when I didn't find a solution - although not as well-ordered and neat as yours. It works fine, and I don't think the double-clicking issue is a bug, just a slightly unconventional behaviour (and why double click a checkbox - they cycle with a single click).

In my case, unfortunately, I did decide I needed the icons for file types - it's for a backup routine where I can select files and folders from a drive, with the intermediate state for folders where some data is selected but not all. It's just lacking too much without the icon for file type. There's code on the forum for getting that using a DllCall to Shell32\ExtractAssociatedIconA.

So I've been experimenting with prefixing the name of the items with a simple text indicator, e.g. # Program Files or ? AppData when adding them to the TV, which looks good enough (with a monospaced font so they're aligned). The state could change (cycle) by clicking, I guess - I haven't got that far yet - or with a GuiContextMenu or hotkeys.

I hope you enjoyed the exercise and the weather improves!

Return to “Ask for Help (v1)”

Who is online

Users browsing this forum: Google [Bot] and 200 guests