|
Multiple Selections in a TreeView
This example provides a class which you can attach to the vbAccelerator TreeView control to provide multiple selection support in a TreeView. Although Windows TreeView controls don't actually support multi-select, you can emulate it by using Owner-Drawing to set the colours of the nodes. This technique is similar to the one used in the Eclipse Java IDE. Emulating Multiple SelectsIn an owner-draw TreeView, you can change the colours used to draw the node in any state. By setting the colours of selected nodes to the normal background and foreground colours, the "real" selection in the TreeView is completely hidden. With that in place, you can start setting individual node's background and foreground colours so that the nodes appear to be selected. Setting which nodes are then "selected" is simply a matter of setting the colours at the right time and responding to the selection modifier keys (shift and control) correctly. A Class To Allow Multi-SelectsThe TreeView control fires some of its notification events in an unexpected order. For example, SelectedNodeChanged fires before the MouseDown event. This makes the code to implement multi-select a little sensitive and hence a good candidate to wrap up in a class. The first thing to sort out is setting the colours of a node so that it appears selected or not. The NodeSelected property of the class is used for this:
Public Property Let NodeSelected( _
node As cTreeViewNode, _
ByVal bState As Boolean _
)
Dim nodAlready As cTreeViewNode
On Error Resume Next
Set nodAlready = m_colSelected(node.Key)
On Error GoTo 0
If (bState) Then
If (nodAlready Is Nothing) Then
m_colSelected.Add node, node.Key
node.BackColor = vbHighlight
node.ForeColor = vbHighlightText
node.MouseOverBackColor = vbHighlight
node.MouseOverForeColor = vbHighlightText
node.SelectedMouseOverBackColor = vbHighlight
node.SelectedMouseOverForeColor = vbHighlightText
node.SelectedBackColor = vbHighlight
node.SelectedForeColor = vbHighlightText
node.SelectedNoFocusBackColor = vbHighlight
node.SelectedNoFocusForeColor = vbHighlightText
node.Text = node.Text
End If
Else
If Not (nodAlready Is Nothing) Then
m_colSelected.Remove nodAlready.Key
node.BackColor = vbWindowBackground
node.ForeColor = vbWindowText
node.MouseOverBackColor = vbWindowBackground
node.MouseOverForeColor = vbWindowText
node.SelectedMouseOverBackColor = vbWindowBackground
node.SelectedMouseOverForeColor = vbWindowText
node.SelectedBackColor = vbWindowBackground
node.SelectedForeColor = vbWindowText
node.SelectedNoFocusBackColor = vbWindowBackground
node.SelectedNoFocusForeColor = vbWindowText
node.Text = node.Text
End If
End If
End Property
In order to maintain the virtual selection state in an easily accessible way, a VB collection m_colSelected is used to store the list of selected nodes. This is also used to determine whether there is any need to change the colours of a node. With that done, the logic to set the selections to the right state can be added. It turns out there are only two events you need to respond to: SelectedNodeChanged and MouseDown. The SelectedNodeChanged does not provide information about whether any of the shift modifier keys are pressed, so this needs to be discovered using the GetAsyncKeyState API. Also, the code needs to keep track of two nodes in order to perform correctly:
The code to achieve that looks like this:
Private m_cNodeSelectionRoot As cTreeViewNode
Private m_colSelected As New Collection
Private m_lastSelectedItem As cTreeViewNode
Private m_bSelectionChanged As Boolean
Private WithEvents m_tvw As vbalTreeView
Private Declare Function GetAsyncKeyState Lib "user32" (ByVal vKey As Long) As Integer
Private Sub m_tvw_MouseDown( _
Button As Integer, Shift As Integer, _
x As Single, y As Single _
)
' Note we only need to do this on MouseDown, since
' the TreeView uses the Control key to allow scrolling
' when the mouse is not clicked.
If (Shift And vbCtrlMask) = vbCtrlMask Then
If Not (m_bSelectionChanged) Then
If Not (m_lastSelectedItem Is Nothing) Then
NodeSelected(m_lastSelectedItem) = Not (NodeSelected(m_lastSelectedItem))
End If
End If
End If
m_bSelectionChanged = False
End Sub
Private Sub m_tvw_SelectedNodeChanged()
Dim nodSelected As cTreeViewNode
'
m_bSelectionChanged = True
Set m_lastSelectedItem = m_tvw.SelectedItem
If Not (GetAsyncKeyState(vbKeyControl) = 0) Then
' Invert selection state for this node:
If Not (m_lastSelectedItem Is Nothing) Then
NodeSelected(m_lastSelectedItem) = Not (NodeSelected(m_lastSelectedItem))
End If
ElseIf Not (GetAsyncKeyState(vbKeyShift) = 0) Then
' Ensure all items between m_cNodeSelectionRoot and m_lastSelectedItem
' are selected, and anything else is not selected.
If Not (m_cNodeSelectionRoot Is Nothing) Then
If Not (m_lastSelectedItem Is Nothing) Then
SelectBetween m_tvw, m_cNodeSelectionRoot, m_lastSelectedItem
End If
End If
Else
' Clear anything that's selected
For Each nodSelected In m_colSelected
NodeSelected(nodSelected) = False
Next
Set m_colSelected = New Collection
Set m_cNodeSelectionRoot = m_lastSelectedItem
' Select this item if necessary
If Not (m_cNodeSelectionRoot Is Nothing) Then
NodeSelected(m_cNodeSelectionRoot) = True
End If
End If
'
End Sub
With that in place, all that's needed to complete the implementation is a way of selecting all the items in-betweeen two nodes. As with most things in a TreeView, this can be achieved using iteration:
Private Sub SelectBetween( _
tvw As vbalTreeView, _
node1 As cTreeViewNode, _
node2 As cTreeViewNode _
)
Dim bInSelection As Boolean
Dim bNoneFoundYet As Boolean
Dim nodStart As cTreeViewNode
Dim sKey1 As String
Dim sKey2 As String
Set m_colSelected = New Collection
Set nodStart = tvw.nodes(1)
sKey1 = node1.Key
sKey2 = node2.Key
iterateSelectBetween nodStart, sKey1, sKey2, bInSelection, True
End Sub
Private Sub iterateSelectBetween( _
nodStart As cTreeViewNode, _
ByVal sKey1 As String, ByVal sKey2 As String, _
ByRef bInSelection As Boolean, _
ByRef bNoneFoundYet As Boolean _
)
Do While Not (nodStart Is Nothing)
If Not (bInSelection) Then
If (bNoneFoundYet) Then
If (nodStart.Key = sKey1) Then
bInSelection = True
bNoneFoundYet = False
ElseIf (nodStart.Key = sKey2) Then
Dim sKeySwap As String
sKeySwap = sKey2
sKey2 = sKey1
sKey1 = sKeySwap
bInSelection = True
bNoneFoundYet = False
End If
End If
End If
NodeSelected(nodStart) = bInSelection
If (bInSelection) Then
If (nodStart.Key = sKey2) Then
bInSelection = False
End If
End If
If (nodStart.Expanded And nodStart.Children.count > 0) Then
iterateSelectBetween nodStart.Children(1), _
sKey1, sKey2, bInSelection, bNoneFoundYet
End If
Set nodStart = nodStart.NextSibling
Loop
End Sub
This procedure is fairly simple, the only thing to note is that the start node may be the first or the last in the selection, and so the code always iterates from the top of the control down to the bottom, and once it detects either of the two nodes which delimit the selection it then swaps them if necessary and only continues iteration until the other node is found. Using the ClassSince the class incorporates a WithEvents instance of the TreeView control there's very little you need to do to use it. The only important thing is to make sure the NoCustomDraw property of the control is set to False before attempting to use the class (it defaults to True) otherwise the class will have no visible effect. The following code snippet provides an example which shows the selected nodes in the debug Window when you right click the TreeView:
Private m_cTreeViewMultiSelect As cTreeViewMultiSelect
Private Sub Form_Load()
Set m_cTreeViewMultiSelect = New cTreeViewMultiSelect
m_cTreeViewMultiSelect.Attach tvwMultiSelect
End Sub
Private Sub tvwMultiSelect_NodeRightClick(node As cTreeViewNode)
'
If (m_cTreeViewMultiSelect.SelectionCount > 0) Then
Dim i As Long
For i = 1 to m_cTreeViewMultiSelect.SelectionCount
Debug.Print m_cTreeViewMultiSelect.SelectedNode(i).Text & _
" is selected."
Next i
End If
'
End Sub
ConclusionThis article demonstrates how to build a TreeView which allows multiple selections. Multiple selections in TreeViews are very helpful when you want to provide as many options as possible in as small a screen space as possible; a great example of this use is in the Eclipse Java IDE where you can use it to format, refactor or check-out multiple files from source control at the same time.
|
|
|
|
||
|
|
||