Search code examples
ms-accesscheckboxtreeviewcommon-controls

Is it possible to simulate tri-state checkboxes in Microsoft TreeView Control 6.0 (MSComctlLib.TreeCtrl.2)?


I'm using the Microsoft TreeView Control 6.0 in Microsoft Access. It seems to work very well, except that it doesn't seem to have a greyed out state, indicating that some, but not all child nodes are checked.

I've looked into using my own images to simulate the checkboxes, but if I do this, I then have to remove the real checkboxes or it looks like I have two checkboxes for each item... but then I don't have any checkboxes and I can't work out how to handle a click on the images.

I can find loads of people having the same sort of questions for this control in other languages/uses, but I can't find a solution for Microsoft Access.

I would happy moving over to a different control, if there's something else available that gives me a hierarchical structure with tri-state checkboxes.


Solution

  • After a bit of research and a couple of hours coding, I was able to write a solution myself.

    I had to add an ImageList, associate that to the TreeView and add an image of a checkbox for each of the three states. Google Image search saved me some time here :).

    'Enumeration for simulated tri-state checkboxes, matching up to the TreeView's associated Image List's Index
    Private Enum CheckboxEnum
      Unchecked = 1
      Checked = 2
      Partial = 3
    End Enum
    
    '---------------------------------------------------------------------------------------
    ' Procedure : objTreeView_MouseDown
    ' Author    : Matty Brown
    ' Date      : 19/05/2014
    ' Purpose   : Because TreeView doesn't support tri-state checkboxes, these have to be simulated using images.
    '---------------------------------------------------------------------------------------
    '
    Private Sub objTreeView_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal x As stdole.OLE_XPOS_PIXELS, ByVal y As stdole.OLE_YPOS_PIXELS)
      Const CHECKBOX_WIDTH As Integer = 195 '195=13px
    
      Dim objNode As Node
      Set objNode = objTreeView.HitTest(x, y)
    
      If objNode Is Nothing Then
        'Miss
      Else
        'Find left side of node by moving left one pixel at a time until you fall off the node, then move one pixel to the right
        Dim intX As stdole.OLE_XPOS_PIXELS
        For intX = x To 0 Step -15
          If Not objNode Is objTreeView.HitTest(intX, y) Then
            If x <= intX + CHECKBOX_WIDTH Then
              'User clicked on the checkbox
              Select Case objNode.Image
                Case CheckboxEnum.Unchecked:
                  objNode.Image = CheckboxEnum.Checked
                Case Else:
                  objNode.Image = CheckboxEnum.Unchecked
              End Select
    
              'Recursively check child nodes
              Call CheckTreeNodes(objTreeView, objNode, objNode.Image)
    
              'Update parent node(s)
              Call UpdateParentNodes(objTreeView, objNode)
            Else
              'User clicked outside of the checkbox
              '
            End If
    
            Exit For
          End If
        Next
      End If
    End Sub
    
    '---------------------------------------------------------------------------------------
    ' Procedure : CheckTreeNodes
    ' Author    : Matty Brown
    ' Date      : 16/05/2014
    ' Purpose   : Checks or unchecks all of the child nodes for the specified node
    '---------------------------------------------------------------------------------------
    '
    Private Sub CheckTreeNodes(ByRef tv As TreeView, ByRef nodNode As Node, ByVal Value As CheckboxEnum)
      Dim lngIndex As Long
    
      'Cascade change to children
      If nodNode.Children > 0 Then
        lngIndex = nodNode.Child.Index
        Call CheckTreeNodes(tv, tv.Nodes(lngIndex), Value)
    
        Do While lngIndex <> nodNode.Child.LastSibling.Index
          lngIndex = tv.Nodes(lngIndex).Next.Index
          Call CheckTreeNodes(tv, tv.Nodes(lngIndex), Value)
        Loop
      End If
    
      nodNode.Image = Value
    End Sub
    
    '---------------------------------------------------------------------------------------
    ' Procedure : CountChildNodes
    ' Author    : Matty Brown
    ' Date      : 19/05/2014
    ' Purpose   : Counts how many child nodes are checked or unchecked, so that a parent node can be set correctly
    '---------------------------------------------------------------------------------------
    '
    Private Sub CountChildNodes(ByRef tv As TreeView, ByRef nodNode As Node, ByRef lngChecked As Long, ByRef lngUnchecked As Long)
      Dim lngIndex As Long
    
      'Check this node's children
      If nodNode.Children > 0 Then
        lngIndex = nodNode.Child.Index
        Call CountChildNodes(tv, tv.Nodes(lngIndex), lngChecked, lngUnchecked)
    
        Do While lngIndex <> nodNode.Child.LastSibling.Index
          lngIndex = tv.Nodes(lngIndex).Next.Index
          Call CountChildNodes(tv, tv.Nodes(lngIndex), lngChecked, lngUnchecked)
        Loop
      Else
      'Update totals
        Select Case nodNode.Image
          Case CheckboxEnum.Checked:
            lngChecked = lngChecked + 1
          Case CheckboxEnum.Unchecked:
            lngUnchecked = lngUnchecked + 1
        End Select
      End If
    End Sub
    
    
    '---------------------------------------------------------------------------------------
    ' Procedure : UpdateParentNodes
    ' Author    : Matty Brown
    ' Date      : 19/05/2014
    ' Purpose   : Steps through parent nodes, updating them according to how many checked/unchecked child nodes they have
    '---------------------------------------------------------------------------------------
    '
    Private Sub UpdateParentNodes(ByRef tv As TreeView, ByRef nodNode As Node)
      Dim lngIndex As Long
      Dim nodParent As Node
      Dim lngChecked As Long, lngUnchecked As Long
    
      'If this node has no parents, there's nothing to update
      If nodNode.Parent Is Nothing Then Exit Sub
      Set nodParent = nodNode
    
      Do While Not nodParent.Parent Is Nothing
        Set nodParent = nodParent.Parent
    
        'Reset counters
        lngUnchecked = 0
        lngChecked = 0
    
        'Count children
        Call CountChildNodes(tv, nodParent, lngChecked, lngUnchecked)
    
        'Update parent nodes
        If lngUnchecked = 0 And lngChecked > 0 Then
          nodParent.Image = CheckboxEnum.Checked
        ElseIf lngUnchecked > 0 And lngChecked > 0 Then
          nodParent.Image = CheckboxEnum.Partial
        Else
          nodParent.Image = CheckboxEnum.Unchecked
        End If
      Loop
    End Sub