[System Image List]

The namespace, the system image list, and subclassing

Sunday, October 14, 2001

This is sample project that demonstrates how to subclass both the TreeView and the ListView common controls in order to use the icons in the system image list directly. It involves some subclassing, and, as you might guess, the whole exercise has to do with walking through the Windows Shell NameSpace.

You can download the sample, but note that it depends on a few things, such as the VB6Core component, my subclassing DLL, and the WinShell DLL.

Where do icons come from?

You can call APIs such as ExtractAssociatedIcon, if you want to write your own file manager. You can also let the common controls take care of that, and let the system provide the icons in the system image list.

Hooking up the system image list to common controls, such as TreeView or ListView, is easy:

Private Sub AssignSysImageList()
	m_SysImgList = VB6Core.GFileUtil.GetSystemImageList
	If m_SysImgList <> 0 Then
		Call SendMessage(m_TreeView.hWnd, TVM_SETIMAGELIST, TVSIL.Normal, m_SysImgList)
	End If
End Sub

The problem is, once VB finds out the no image list "control" (from MSComCtl.ocx) is assigned to the TreeView, it will remove the system image list from the underlying TreeView control, undoing our SendMessage call. This is where subclassing comes into play:

Private Sub ISubClass_WndProc(ByVal message As SubClass.CMessage)
	Select Case message.wMsg
		Case TVM_SETIMAGELIST
			' use sys il only
			If message.lParam <> m_SysImgList Then
				message.Handled = True
			End If
       		' ... 
	End Select
End Sub

Mapping icons to nodes

While we're at it, we'll also use some APIs to add nodes to the TreeView instead of the control's methods. The problem is, if we call the Add method of the Nodes collection, VB will check if any icons indices are in range. Of course, they cannot possibly be, because the wrapper control doesn't know about our image list assignment.

There are several approaches to hack arround that. I've found it most reliable to add a fake node with the standard methods, and intercept the appropriate message. So let's fill a few structures, and save one of them in a private variable that will be used in the WndProc:

Private Sub AddNode(ByVal hParent As Long, ByVal so As ShellObject)
	' fill item from ShellObject (WinShell.dll gets the data)
	Dim item As TVITEM
	item.pszText = StrPtr(String$(MAX_PATH, 0))
	Call lstrcpyA(ByVal item.pszText, ByVal so.DisplayName)
	item.cchTextMax = MAX_PATH
	item.iImage = so.IconIndex(0, 0)
	item.iSelectedImage = so.IconIndex(1, 0)
	item.lParam = ObjPtr(so)
	If m_TreeView.Nodes.Count = 0 Then
			item.cChildren = 1
		Else:	item.cChildren = Abs(so.HasSubFolders)
	End If
	item.mask = TVIF.Text Or TVIF.Image Or TVIF.SelectedImage Or TVIF.Param Or TVIF.Children
	' save insertstruct in private variable
	m_CurrentItem.item = item
	m_CurrentItem.hParent = hParent
	' call (we'll intercept the msg)
	Dim nde As Node
	Set nde = m_TreeView.Nodes.Add
	Set nde.Tag = so
End Sub

In the WndProc, we'll intercept the TVM_INSERTITEM message, and change the insertstruct that the lParam points to:

Private Sub ISubClass_WndProc(ByVal message As SubClass.CMessage)
	Select Case message.wMsg
       	' ... 
		Case TVM_INSERTITEM
			' trick treeview
			Call CopyMemory(ByVal message.lParam, m_CurrentItem, Len(m_CurrentItem))
	End Select
End Sub

TreeView blues

Finding out when a node is about to expand isn't so easy. The Expand event won't fire, because the nodes do not have subnodes yet (but the "+" icons is there, because we have specified that the nodes has children in the TVITEM structure). We have to receive a notification message, which isn't send to the TreeView, but rather to its parent window. The WndProc of the parenting form looks like this:

Private Sub ISubClass_WndProc(ByVal message As SubClass.CMessage)
	Select Case message.wMsg
	Case WM_NOTIFY
		Dim nmh As NMHDR, nmtv As NMTREEVIEW
		Dim nde As Node, pNode As Long, so As ShellObject
		CopyMemory nmh, ByVal message.lParam, Len(nmh)
		If nmh.code = TVN_ITEMEXPANDING Then
			CopyMemory nmtv, ByVal message.lParam, Len(nmtv)
			' steal node reference
			CopyMemory pNode, ByVal nmtv.itemNew.lParam + 8, 4
			If pNode Then
				CopyMemory nde, pNode, 4
				' get shell object
				Set so = nde.Tag
				' add children
				m_SubTree.RemoveChildren nde
				m_SubTree.AddChildren nmtv.itemNew.hItem, so
				' prevent ((IUnknown*)nde)->Release();
				CopyMemory nde, 0&, 4
			End If
		End If
	End Select
End Sub

You might wonder how we get the Node reference, but I can't really tell you how. The trick isn't really my baby. So far, it has always worked out. Normally, you'd store extra info, such as a reference to a ShellObject instance, in the lParam of the TVITEM structure. But with the VB version of the TreeView, this doesn't work. Don't ask me why; after a couple of hours of crash-testing, I decided to use the Tag property, and use the CopyMemory trick. Note that we're copying a 32 bit zero to the reference in order to keep VB from calling "IUnknown::Release()" behind the scenes.

Missing pieces

You can figure out the rest in the source code you can download. There is a little more to the TreeView logic, such as selecting a node the represents a given folder. For the ListView, things are similiar, but simpler. Of course, there are a lot of declarations that I haven't documented here. The sample also shows you how to use VB6Core.CSplitter, and demonstrates how to use the WinShell component. Let me know if you have any problems with running this sample.