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.