Conditionally prevent selection changes in TreeView control

It may be desirable to prevent a user from changing the currently selected node in a TreeView control. Unfortunately, the NodeClick and Click events don't provide a cancel mechanism. The way to work around this is through subclassing the TreeView control.

Brad Martinez compiled an excellent sample project on how to subclass the TreeView control. You can download the TVEventCancel project here.

Unfortunately while the TVEventCancel project covers how to outright prevent a selection change, it does not cover conditionally canceling that change. An example where this may be helpful is if you wish to prompt the user if they wish to save changes before allowing a node change.

While this may not seem like a big deal, the TreeView control is actually a superclassed version of the treeview common control in Comctl32.dll which causes it to behave somewhat strangely at times. This is one of those times.

Conditionally canceling the selection becomes somewhat more complex because the TreeView actually sends two TVN_SELCHANGING messages when the mouse is used to select a new node. One action is type "Unknown" (0) while the other is type "Mouse" (1). When using the keyboard to change selections, only a single "Keyboard" (2) message is sent. In order to prevent the selection change, all of these messages must be canceled by having your custom WindowProc function return 1. However, a conditional statement with a user prompt (e.g. MsgBox function) will be raised twice by any mouse selection.

To work around this you can set a flag to prevent changes on the first Unknown action. An example replacement WindowProc follows:

'
' Based on TVEventCancel code by Brad Martinez
' http://btmtz.mvps.org/treeview/
'

' The NMHDR structure contains information about a notification 
' message.  The pointer to this structure is specified as the lParam 
' member of the WM_NOTIFY message.
Private Type NMHDR
    hwndFrom As Long    ' Window handle of control sending message
    idFrom As Long      ' Identifier of control sending message
    code  As Long       ' Specifies the notification code
End Type

Private Type NMTREEVIEW   ' was NM_TREEVIEW
    hdr As NMHDR
    ' The 'action' member specifies a notification-specific action 
    ' flag.  Is NMTREEVIEW_action for TVN_SELCHANGING, TVN_SELCHANGED,
    ' TVN_SETDISPINFO.  Is TVM_EXPAND_wParam for TVN_ITEMEXPANDING, 
    ' TVN_ITEMEXPANDED
    action As Long
    itemOld As TVITEM
    itemNew As TVITEM
    ptDrag As POINTAPI
End Type

Private blnAllowNodeChange as Boolean ' a flag that allows the current
                                      ' node selection to change, 
                                      ' necessary because of duplicate
                                      ' actions (0 and 1) when  
                                      ' changing nodes conditionally.
Private blnChanged as Boolean  ' a flag set elsewhere in the 
                               ' application to indicate data 
                               ' has changed and has not yet been 
                               ' saved for the purposes of this 
                               ' example
Private lngOldWinProc as Long ' address of original WinProc

Public Function WindowProc(ByVal hwnd As Long, ByVal uMsg As Long, _
  ByVal wParam As Long, ByVal lParam As Long) As Long
    
  Dim bytAction As Long
  Dim lngReturn As Long
  Dim nmHeader As NMHDR
  Dim nmTree As NMTREEVIEW
    
  Const USE_OLD_PROC As Byte = 0
  Const USE_TV_SUB_PROC As Byte = 1
  Const USE_TV_SUB_PROC_2 As Byte = 2
    
  Select Case uMsg
    Case WM_NOTIFY
      ' handle treeview notify messages
      If TreeView1.hwnd Then
        ' Fill the NMHDR struct from the lParam pointer.
        ' (for any WM_NOTIFY msg, lParam always points to a struct 
        ' which is either the NMHDR struct, or whose 1st member is 
        ' the NMHDR struct)
        MoveMemory nmHeader, ByVal lParam, Len(nmHeader)
                             
        ' The actual notification message is in the code member of 
        ' the NMHDR struct.
        Select Case nmHeader.code
          ' The current selection is about to change.  Return TRUE 
          ' to prevent the selection from changing.
          Case TVN_SELCHANGING
            If blnAllowNodeChange = False Then
              ' action was previously cancelled
              ' there's a bogus second action that has to be captured
              ' and handled
              bytAction = USE_TV_SUB_PROC_2
                        
            Else
              ' lParam points to the NMTREEVIEW struct, fill it.                                         
              MoveMemory nmTree, ByVal lParam, Len(nmTree)                          
              '
              ' THIS IS A SAMPLE CONDITIONAL STATEMENT
              '
              If blnChanged Then ' data has changed

                Select Case MsgBox("The data for the current " _
                  & "selection have changed." & vbCr _
                  & "Do you want to save before proceeding?", _
                  vbQuestion + vbYesNoCancel, "Save")

                    Case vbYes ' save settings
                      ' execute some save procedure here
                      ' clear changed flag
                      blnChanged = False
                      
                      ' proceed with normal treeview msg handling
                      bytAction = USE_OLD_PROC

                    Case vbCancel
                      If nmTree.action = _
                        NMTREEVIEW_action.TVC_BYKEYBOARD Then
                          ' only one msg sent by keyboard selection
                          bytAction = USE_TV_SUB_PROC_2

                      Else
                        ' use the custom procedure to prevent 
                        ' selection change
                        bytAction = USE_TV_SUB_PROC

                      End If
                                            
                    Case Else ' no button pressed
                      ' don't save, clear flag
                      blnChanged = False

                      ' proceed with standard treeview handling
                      bytAction = USE_OLD_PROC

                End Select

              Else
                ' data has not changed, proceed with standard 
                ' treeview message handling
                bytAction = USE_OLD_PROC
                                
              End If

            End If
                        
          Case Else
            ' a non-subclassed treeview message
            bytAction = USE_OLD_PROC
                
        End Select
                
      End If
            
    Case Else
      ' a non-subclassed message
      bytAction = USE_OLD_PROC
            
  End Select
       
  ' determine what we're supposed to be doing
  Select Case bytAction
    Case USE_TV_SUB_PROC
      ' this is the first event to fire when a node is clicked

      ' If nmTree.itemNew.hItem has a value, then the item is 
      ' being selected.
      If nmTree.itemNew.hItem Then
        ' set flag to prevent secondary action from changing nodes
        blnAllowNodeChange = False
        ' Return 1 to cancel the selection
        WindowProc = 1
      End If
        
    Case USE_TV_SUB_PROC_2
      ' this is a second action which fires when a node is clicked 
      ' with the mouse and the user is prompted with a conditional 
      ' prompt... no traceable source of this message...
           
      ' allow node changes again
      blnAllowNodeChange = True
      ' Return 1 to cancel the selection
      WindowProc = 1
            
    Case Else
      ' proceed normally
      WindowProc = CallWindowProc(lngOldWinProc, hwnd, uMsg, _
        wParam, lParam)

  End Select

End Function

Author: ASAK
Created: Nov 25 2005
Categories: Visual Basic
TechByte #92

Warning: By visiting this site and/or by using any information contained herein, you agree to the Techbytes.ca terms of use.



Add a comment about this TechByte

If you wish to add a comment regarding this TechByte, please use the form below. Please note that by submitting comments using this form you are allowing all of the information submitted to be visible on this website. Any comments submitted using this form will only be shown on the website if they are approved by the administrators of this site. IF APPROVED, COMMENTS MAY TAKE SEVERAL DAYS TO BE POSTED.

Posted By: (Optional)

Comments:


Other TechBytes: