|
Eine CheckBox kann nicht nur die zwei Zustände "gesetzt" (vbChecked) und "nicht gesetzt" (vbUnchecked) annehmen, sondern sie kennt auch noch einen Zwischenzustand, bei dem die CheckBox zwar als gesetzt gilt, aber der Hintergrund (in der "klassischen" Windows-Farbgebung) grau eingefärbt erscheint. Dieser Zwischenzustand soll andeuten, dass die Einstellung von anderen Einstellungen abhängig ist - etwa dass nach- bzw. untergeordnete Einstellungen nicht vollständig gesetzt sind.
Die CheckBoxen eines TreeView-Steuerelements (wenn Sie dessen Eigenschaft "Checkboxes" auf True setzen), kennen einen solchen Zwischenzustand leider nicht, obwohl eine Baum-Struktur doch geradezu ein typischer Fall der Repräsentation von untergeordneten, abhängigen Optionen darstellt. Wenn Sie solche Zwischenzustände darstellen wollen, müssen Sie jedoch auf die eingebauten CheckBoxen des TreeViews verzichten und statt dessen die Symbole der Knoten entsprechend setzen. Die beim Klick auf einen Knoten anfallende Analyse, welcher Zustand für ihn angezeigt werden soll, können Sie mittels einiger weniger Hilfsfunktionen automatisieren.
Die zugrunde liegenden Regeln lauten:
1.) Wenn unterhalb eines Knotens alle Knoten gesetzt sind, ist dieser Knoten gesetzt.
2.) Wenn unterhalb eines Knotens keine Knoten gesetzt ist, ist dieser Knoten nicht gesetzt.
3.) Wenn unterhalb eines Knotens nur einige Knoten gesetzt sind, ist dieser Knoten im Zwischenzustand.
Dazu benötigen wir noch weitere Regeln, die die Aktion festlegen, die beim Klick auf einen Knoten erfolgen soll:
4.) Ein gesetzter Knoten erscheint nach einem Klick als nicht gesetzt; ebenso erscheinen alle in der Hierarchie unter ihm liegenden Knoten als nicht gesetzt.
5.) Ein nicht gesetzter Knoten erscheint nach einem Klick als gesetzt; ebenso erscheinen alle in der Hierarchie unter ihm liegenden Knoten als gesetzt.
Diese letzten beiden Regeln mögen zwar offensichtlich und banal erscheinen. Doch die noch unbeantwortete Frage lautet nun: Was soll passieren, wenn ein Knoten angeklickt wird, der sich im Zwischenzustand befindet? Eine normale, einfache CheckBox im Zwischenzustand erscheint nach einem Klick automatisch als nicht gesetzt. Die noch fehlende Regel könnte also lauten:
6.) Ein im Zwischenzustand befindlicher Knoten erscheint nach einem Klick als nicht gesetzt; ebenso erscheinen alle in der Hierarchie unter ihm liegenden Knoten als nicht gesetzt.
Das Verhalten ausgehend vom Zwischenzustand würde also dem bei einem gesetzten Knoten entsprechen (Regel 4). Aber unbedingt zwingend ist diese Verhaltensweise eigentlich nicht. Denn die Regel könnte auch genau umgekehrt formuliert werden, entsprechend dem Verhalten bei einem zuvor nicht gesetzten Knoten (Regel 5):
6a.) Ein im Zwischenzustand befindlicher Knoten erscheint nach einem Klick als gesetzt; ebenso erscheinen alle in der Hierarchie unter ihm liegenden Knoten als gesetzt.
Ich will Ihnen allerdings nicht vorschreiben, welche der beiden Regelvarianten Sie für Ihre Anwendung vorsehen wollen (auch wenn ich die erste Variante, entsprechend dem Verhalten der Standard-CheckBox, vorziehen würde). Daher ist die folgende Hilfsfunktion, die Sie im Normalfall aus dem NodeClick-Ereignis eines Knotens aufrufen, für beide Fälle ausgelegt.
Sie übergeben der Funktion den angeklickten Knoten im ersten Parameter. Wenn Sie die Funktion im NodeClick-Ereignis aufrufen, soll die Zustandsänderung des Knotens automatisch erfolgen - den zweiten, optionalen Parameter State ignorieren Sie dort. Wenn Sie einen Knoten von einer anderen Stelle in Ihrem Code aus in einen bestimmten Zustand versetzen wollen, können Sie einen der beiden Werte der SemiCheckConstants-Enumeration übergeben (die Werte entsprechen denen der normalen CheckBox - es fehlt allerdings der Wert für den Zwischenzustand, da es keinen Sinn hätte, den Zwischenzustand explizit zu setzen).
Im letzten, ebenfalls optionalen Parameter legen Sie das Verhalten beim Antreffen des Zwischenzustandes fest, indem Sie diesen Parameter weglassen (voreingestellt ist das Standardverhalten), oder einen der beiden Werte der Enumeration SemiCheckModeConstants übergeben.
Public Enum SemiCheckConstants
scUnchecked
scChecked
End Enum
Public Enum SemiCheckModeConstants
scModeClearOnSemi
scModeCheckOnSemi
End Enum
Public Sub CheckNode(Node As Node, _
Optional ByVal State As SemiCheckConstants = -1, _
Optional ByVal Mode As SemiCheckModeConstants = scModeClearOnSemi)
Select Case State
Case scUnchecked, scChecked
Wenn Sie keinen zu setzenden Zustand vorgeben, wird hier der aktuelle Zustand des Knotens ausgewertet und der neue Zustand in der hier weiter verwendeten Parameter-Variablen State gesetzt. Der Zustand lässt sich am einfachsten anhand des Namens des aktuell gezeigten Symbols (Image-Eigenschaft des Knotens) ermitteln. Die Namen aller drei Zustandstandssymbole beginnen mit der Zeichenfolge "check", an die der Zustandswert (0, 1 oder 2), den das Symbol repräsentieren soll, als Ziffer angehängt ist.
Case -1
Select Case Mode
Beim Standardverhalten entspricht der Zwischenzustand dem gesetzten Zustand:
Case scModeClearOnSemi
Select Case Node.Image
Case "check0"
State = scChecked
Case "check1", "check2"
State = scUnchecked
End Select
Beim alternativen Verhalten entspricht der Zustand dem nicht gesetzten Zustand:
Case scModeCheckOnSemi
Select Case Node.Image
Case "check0", "check2"
State = scChecked
Case "check1"
State = scUnchecked
End Select
Case Else
Err.Raise 5
End Select
Case Else
Err.Raise 5
End Select
Zuerst werden alle untergeordneten Knoten auf den neuen Zustand gesetzt (private Hilfsfunktion zCheckNodesDown, siehe unten):
zCheckNodesDown Node, State
Und anschließend wird der für den aktuellen Knoten neu gesetzte Zustand "nach oben" gereicht:
zCheckNodesUp Node, State
End Sub
Die Hilfsfunktion, die den neuen Zustand in den untergeordneten Knoten setzt, ist trivial. Sie ruft sich selbst rekursiv auf, so lange ein Knoten noch weitere Kind-Knoten hat, und setzt den neuen Zustand im jeweiligen Knoten.
Private Sub zCheckNodesDown(Node As Node, ByVal State As SemiCheckConstants)
Dim nChildNode As Node
With Node
Set nChildNode = .Child
.Image = "check" & CStr(State)
Do While Not (nChildNode Is Nothing)
zCheckNodesDown nChildNode, State
Set nChildNode = nChildNode.Next
Loop
End With
End Sub
Um einiges komplizierter ist das Anpassen der übergeordneten Knoten in der privaten Hilfsfunktion zCheckNodesUp, deren Zustände ja nicht nur vom Zustand des aktuell gesetzten Knotens abhängen, sondern auch von der "Aufsummierung" der Zustände aller Knoten auf jeweils einer Ebene. Auch diese Hilfsfunktion ruft sich selbst rekursiv so lange immer wieder auf, bis die oberste Ebene erreicht worden ist (d.h. bis kein Eltern-Knoten mehr vorhanden ist, die Parent-Eigenschaft eines Knotens also Nothing zurückgibt). Da wir es in dieser Aufrufkette mit allen drei Zuständen zu tun haben können, wird hier der zweite Parameter der Funktion, State, als Typ der VB-eigenen Enumeration CheckBoxConstants deklariert.
Private Sub zCheckNodesUp(Node As Node, ByVal State As CheckBoxConstants)
Dim nSibling As Node
Dim nParentNode As Node
Dim nCheckCount As Long
Dim nSiblingsCount As Long
Dim nSemi As Boolean
With Node
Zunächst wird der gerade übergebene Knoten auf den übergebenen Zustand gesetzt. Beim ersten Aufruf ist der Zustand zwar bereits gesetzt, aber es schadet nichts, ihn erneut zu setzen.
.Image = "check" & CStr(State)
Set nParentNode = .Parent
If Not (nParentNode Is Nothing) Then
Wenn noch ein Elternknoten vorhanden ist, prüfen wir, welcher Zustand für diesen Aufruf der Funktion übergeben worden ist. Vor allem beim ersten Aufruf kann es sich nur um einen der beiden "eindeutigen" Zustände handeln:
Select Case State
Case vbChecked, vbUnchecked
Wir holen den ersten Schwester-Knoten des gerade übergebenen Knotens:
Set nSibling = .FirstSibling
Do While Not (nSibling Is Nothing)
Falls vorhanden, zählen wir die Schwesterknoten durch:
nSiblingsCount = nSiblingsCount + 1
Dann betrachten wir den Zustand des jeweiligen Schwesterknotens
Select Case nSibling.Image
Case "check0"
Ist der Zustand gesetzt, wird der Zustand der aktuellen Geschwister-Ebene mindestens der Zwischenzustand werden:
Case "check1"
nSemi = True
Die gesetzten Knoten dieser Ebene zählen wir separate ebenfalls durch:
nCheckCount = nCheckCount + 1
Ist der Zustand bereits ein Zwischenzustand, wird der Zustand der Geschwister-Ebene ebenfalls mindestens mindestens ein Zwischenzustand werden:
Case "check2"
nSemi = True
End Select
Set nSibling = nSibling.Next
Loop
Ist die Anzahl aller Geschwister-Knoten gleich der Anzahl der gesetzten Knoten, gilt der Zustand der ganzen Ebene als "gesetzt":
If nSiblingsCount = nCheckCount Then
State = vbChecked
Ist die Anzahl ungleich, und war mindestens in Knoten der Ebene gesetzt oder im Zwischenzustand, ist die ganze Ebene im Zwischenzustand:
ElseIf nSemi Then
State = vbGrayed
Im verbleibenden Fall war kein Knoten der Ebene gesetzt oder im Zwischenzustand, so dass die ganze Ebene den Zustand "nicht gesetzt" erhält:
Else
State = vbUnchecked
End If
Wurde bereits der Zwischenzustand übergeben, ändert sich nichts - er wird einfach weitergegeben:
Case vbGrayed
End Select
Hier wird nun der ermittelte Zustand der Ebene an den Eltern-Knoten weitergegeben, der zu Beginn des nun folgenden Aufrufs bei diesem gesetzt wird:
zCheckNodesUp nParentNode, State
End If
End With
End Sub
|