Die Funktion des gegenseitigen Auslösens (wie bei Tasten zur Senderwahl an einem Radio) bieten eigentlich nur der OptionButton oder selbst geschriebene Steuerelemente auf der Basis eines UserControls. Falls Ihnen der Aufwand, ein eigenes Steuerelement zu schreiben, zu hoch ist, und Sie vorhandene Steuerelemente als Optionsgruppe behandeln wollen, bleibt Ihnen zunächst nur der mühsame Weg "zu Fuß". Wenn Sie etwa ein Steuerelementfeld (Control-Array) von Labels dazu bringen wollen, immer das zuletzt angeklickte Element fett darzustellen, könnten Sie das so erreichen (bei einem Label-Control-Array von 1 - 5):
Private mLastIndex As Integer
Private Sub Label1_Click(Index As Integer)
If mLastIndex Then
Label1(mLastIndex).FontBold = False
End If
Label1(Index).FontBold = True
mLastIndex = Index
End Sub
Schon das gleichzeitige Setzen einer zweiten Eigenschaft, etwa der Schriftfarbe (ForeColor), wird aufwändiger:
Private mLastIndex As Integer
Private Sub Label1_Click(Index As Integer)
If mLastIndex Then
With Label1(mLastIndex)
.FontBold = False
.ForeColor = vbWindowText
End With
End If
With Label1(Index)
.FontBold = True
.ForeColor = vbBlue
End With
mLastIndex = Index
End Sub
Je öfter Sie solche Quasi-Optionsgruppen verwenden, um so häufiger schreiben Sie den im Prinzip immer wieder gleichen Code. Und standardisieren lässt er sich anscheinend auch nicht, da es ja viele verschiedene Steuerelemente gibt - und ein jedes hat wieder andere Eigenschaften und Datentypen. Trotzdem bietet es sich an, für derartige Optionsgruppen eine wiederverwendbare Klasse anzulegen.
Sie werden sich fragen, wie das in Anbetracht der Verschiedenartigkeit der Steuerelemente möglich sein soll? Nun, seit Visual Basic 6 können Sie Methoden und Eigenschaften von Steuerelementen (und überhaupt von beliebigen Objekten) über Ihren Namen aufrufen - über die Funktion CallByName. Dieser übergeben Sie das betreffende Objekt, den Namen der Methode bzw. Eigenschaft, die Art des Aufrufs (Methode oder Let/Set/Get einer Eigenschaft) und gegebenenfalls Parameter bzw. den zu setzenden Wert. Zum Beispiel:
CallByName Label1, "ForeColor", vbLet, vbBlue
Eine allgemeine Optionsgruppen-Klasse (nennen wir sie "clsRadio") müsste also nur wissen, welcher Eigenschaft welche Werte jeweils für den gewählten und den ungewählten Zustand zuzuweisen wären. Und sie müsste natürlich auch die Objekte der Optionsgruppe enthalten - etwa in einer Collection.
Letzteres ist eine Standard-Angelegenheit. Sie haben doch bestimmt schon einmal eine eigene Collection-Klasse angelegt, die eine VB-Collection kapselt. Die Verwaltung der Objekte einer Optionsgruppe erfolgt in der Klasse clsRadio auf gleich Weise:
Private mItems As Object
Public Property Get Count() As Long
Count = mItems.Count
End Property
Public Property Get Item(KeyIndex As Variant) As Object
On Error Resume Next
Set Item = mItems(KeyIndex)
End Property
Public Sub Add(Item As Object, Optional Key As Variant, _
Optional Before As Variant, Optional After As Variant)
mItems.Add Item, Key, Before, After
End Sub
Public Sub Remove(KeyIndex As Variant)
On Error Resume Next
mItems.Remove KeyIndex
End Sub
Public Function NewEnum() As IUnknown
Set NewEnum = mItems.[_NewEnum]
End Function
Private Sub Class_Initialize()
Set mItems = New Collection
End Sub
Private Sub Class_Terminate()
Set mItems = Nothing
End Sub
Sie fragen sich vielleicht, warum die interne Variable mItems zur Aufnahme der Collection nur als "As Object" und nicht direkt als "As Collection" deklariert ist? Nun, so können Sie auch bereits vorhandene Collections anderer Herkunft (etwa die ListItems-Collection eines ListView-Steuerelements) direkt aufnehmen:
Public Sub AddCollection(Items As Object)
Set mItems = Items
End Sub
Und damit Sie die einzelnen Elemente eines Control-Arrays nicht auch einzeln einfügen müssen, erledigt die folgende Methode die Übernahme eines Control-Arrays:
Public Sub AddControlArray(CtlArray As Object)
Dim nControl As Control
Set mItems = New Collection
With mItems
For Each nControl In CtlArray
.Add nControl
Next
End With
End Sub
Die Auswahl eines Elements der Optionsgruppe erfolgt über die Methode SelectItem. Praktischerweise können Sie ihr sowohl den Index, den Schlüssel und sogar ein Objekt der Optionsgruppe übergeben.
Public Function SelectItem(KeyIndexItem As Variant) As Object
Dim nSelectItem As Object
Dim l As Long
Dim nUnselectItem As Object
Dim nOldSelectedKeyIndex As Variant
nOldSelectedKeyIndex = pSelectedKeyIndex
If IsObject(KeyIndexItem) Then
For l = 1 To mItems.Count
If mItems(l) Is KeyIndexItem Then
pSelectedKeyIndex = l
Set nSelectItem = mItems(l)
Exit For
End If
Next
Else
pSelectedKeyIndex = KeyIndexItem
On Error Resume Next
Set nSelectItem = mItems(pSelectedKeyIndex)
End If
If Not (nSelectItem Is Nothing) Then
On Error Resume Next
Set nUnselectItem = mItems(nOldSelectedKeyIndex)
zSelect nUnselectItem, False
zSelect nSelectItem, True
Set SelectItem = nSelectItem
zChanged
End If
End Function
Wird tatsächlich ein Objekt übergeben, wird versucht, dessen Index innerhalb der Collection ausfindig zu machen. Wurde ein Index oder ein Schlüssel übergeben, wird versucht, der lokalen Variablen nSelectItem das entsprechende Objekt aus der Collection zuzuweisen. Schlägt der eine oder der andere Versuch fehl, geschieht nichts weiter (der Einfachheit halber ohne Auslösen eines Laufzeitfehlers).
Wurde ein Objekt gefunden, wird zunächst noch versucht, das zuvor gewählte Objekt zu ermitteln - anhand des Eingangs der Methode in nOldSelectedKeyIndex vorab gesicherten alten Indexes bzw. Schlüssels. Unabhängig davon, ob dieses ermittelt werden konnte, wird der Inhalt der Variablen nUnselectItem mit dem Vermerk der Abwahl ("False") an die private Prozedur zSelect übergeben (zum Code dieser Prozedur kommen wir später noch). Direkt danach wird zSelect mit dem neu zu wählenden Objekt und True als Vermerk aufgerufen. Als kleiner Service gibt die Methode auch noch das neu gewählte Objekt zurück. Und schließlich wird die private Prozedur zChanged aufgerufen (auch hierzu später mehr).
Sie werden sicher noch die Deklaration der Variablen pSelectedKeyIndex vermissen. Sie ist die private klassenweit gültige Variable zur Aufnahme des Indexes bzw. Schlüssels des aktuell gewählten Objekts:
Private pSelectedKeyIndex As Variant
Public Property Get SelectedKeyIndex() As Variant
SelectedKeyIndex = pSelectedKeyIndex
End Property
Sie sehen, dass nur dieser indirekte Verweis auf das gewählte Objekt gespeichert wird. Das reicht vollkommen aus, um auch das gewählte Objekt jederzeit liefern zu können:
Public Property Get SelectedItem() As Object
On Error Resume Next
Set SelectedItem = mItems(pSelectedKeyIndex)
End Property
Da wir gerade noch bei den Funktionen zur Auswahl sind, schieben wir noch schnell die Methode InitItems ein, über die Sie alle eingefügten Elemente auf einen einheitlichen, nicht gewählten Stand bringen können:
Public Sub InitItems()
Dim nItem As Object
For Each nItem In mItems
zSelect nItem, False
Next
pSelectedKeyIndex = Empty
End Sub
Sinnvollerweise bietet es sich an, die oben bereits gezeigten Methoden zur Übergabe einer vorhandenen Collection und eines Controls-Arrays mit einem optionalen Parameter zu versehen, der angibt, ob InitItems auch gleich nach der Übernahme der Objekte ausgeführt werden soll:
Public Sub AddCollection(Items As Object, _
Optional ByVal InitItems As Boolean)
Set mItems = Items
If InitItems Then
Me.InitItems
End If
End Sub
Public Sub AddControlArray(CtlArray As Object, _
Optional ByVal InitItems As Boolean)
Dim nControl As Control
Set mItems = New Collection
With mItems
For Each nControl In CtlArray
.Add nControl
Next
End With
If InitItems Then
Me.InitItems
End If
End Sub
Nähern wir uns nun etwas mehr der eigentlichen Aufgabe der Verwaltung der Eigenschaft(en), die zur Darstellung der Auswahl gesetzt werden soll. Allzuviel verrate ich Ihnen noch nicht - nur erst einmal soviel, dass die Informationen in einem Array oder einer Collection verwaltet werden und einer Eigenschaft der Klasse übergeben werden:
Private pProperties As Variant
Public Property Get Properties() As Variant
Properties = pProperties
End Property
Public Property Let Properties(New_Properties As Variant)
If IsObject(New_Properties) Then
Set pProperties = New_Properties
Else
pProperties = New_Properties
End If
End Property
Public Property Set Properties(New_Properties As Variant)
Set pProperties = New_Properties
End Property
Ich habe mit verschiedenen Möglichkeiten experimentiert, ehe ich mich zu der Form der Organisation der benötigten Informationen entschlossen habe, wie Sie sie nun vorfinden werden. Mit erschien es schließlich am einfachsten, für jede zu setzende Eigenschaft des gewählten bzw. abgewählten Objekts ein Array zu definieren. Dieses Array enthält als erstes Element den Namen der Eigenschaft, als zweites Element den Wert für den nicht gewählten Zustand, und als drittes Element den Wert für den gewählten Zustand. Diese Arrays je Eigenschaft werden in einer Collection (oder einem weiteren Array) zusammengefasst, die (bzw. das) an die oben stehende Properties-Eigenschaft übergeben wird. Das Zusammenstellen der Informationen ist auf diese Weise einfach handzuhaben. Hier sehen Sie nun schon einmal ein Beispiel, wie eine Instanz der Klasse clsRadio für eine Optionsgruppe aus drei Labels initialisiert werden könnte:
Private mOptionGroup As clsRadio
' ...
Set mOptionGroup = New clsRadio
With mOptionGroup
Set nProperties = New Collection
With nProperties
.Add Array("FontBold", False, True)
.Add Array("ForeColor", vbWindowText, vbBlue)
End With
Set .Properties = nProperties
.Add Label1
.Add Label2
.Add Label3
End With
Nun bin ich endlich soweit, dass ich Ihnen die Prozedur zSelect zeigen kann, in der die Darstellung der Auswahl bzw. der Nicht-Auswahl erfolgt.
Private Sub zSelect(Item As Object, ByVal Selected As Boolean)
Dim nProperty As Variant
Dim i As Integer
Dim nPropertyName As Variant
Dim nValue As Variant
Const kPropertyName = 0
Const kUnselectedValue = 1
Const kSelectedValue = 2
If Item Is Nothing Then
Exit Sub
End If
For Each nProperty In pProperties
nPropertyName = Trim$(nProperty(kPropertyName))
Select Case Selected
Case False
If IsObject(nProperty(kUnselectedValue)) Then
Set nValue = nProperty(kUnselectedValue)
Else
nValue = nProperty(kUnselectedValue)
End If
Case True
If IsObject(nProperty(kSelectedValue)) Then
Set nValue = nProperty(kSelectedValue)
Else
nValue = nProperty(kSelectedValue)
End If
End Select
On Error Resume Next
If Len(nPropertyName) Then
If IsObject(nValue) Then
CallByName Item, nPropertyName, VbSet, nValue
Else
CallByName Item, nPropertyName, VbLet, nValue
End If
Else
If IsObject(nValue) Then
Set Item = nValue
Else
Item = nValue
End If
End If
Next 'i
End Sub
Eigentlich passiert hier nichts Besonderes. Die Liste der für jede zu setzende Eigenschaft vorhandenen Arrays wird durchlaufen (per For...Each können sowohl Collections als auch Arrays bearbeitet werden), der Name der Eigenschaft wird aus dem gerade bearbeiteten Array ausgelesen, und je nach Wert des Selected-Parameters der Wert für den gewählten oder den nicht gewählten Zustand. Dann wird CallByName mit dem im Item-Parameter übergebenen Objekt, dem Namen der Eigenschaft und mit dem zu setzenden Wert als Parameter aufgerufen. Die Art des Aufrufs (VbLet oder VbSet) ergibt sich aus dem Datentyp des zu setzenden Wertes - Sie können hier einer Eigenschaft auch Objekte zuweisen - etwa Picture- oder Font-Objekte.
In der ebenfalls erwähnten privaten Prozedur zChange passiert auch nicht allzuviel - hier wird lediglich ein Ereignis ausgelöst, das Sie beispielsweise ein einem Form über die Änderung der Auswahl informiert:
Public Event Changed()
Private Sub zChanged()
Dim nSyncObject As Object
RaiseEvent Changed
If TypeOf pSyncObject Is clsRadio Then
pSyncObject.SelectItem pSelectedKeyIndex
Else
For Each nSyncObject In pSyncObject
If TypeOf nSyncObject Is clsRadio Then
nSyncObject.SelectItem pSelectedKeyIndex
End If
Next
End If
End Sub
Ach so, ja, dieses SyncObject kennen Sie ja noch gar nicht. Nun, da wir ansonsten die eigentliche Funktionalität der Klasse hinter uns gebracht haben, rüsten wir sie noch mit ein paar komfortablen Features auf - einfach aus Spaß an der Freude...
Die Eigenschaft SyncObject der Klasse clsRadio kann entweder einen Verweis auf eine einzelne weitere Instanz von clsRadio enthalten, oder eine Collection (oder auch hier ein Array) mehrerer Instanzen. Damit können Sie auf einfache Weise beliebig viele Instanzen synchronisieren - etwa Kombinationen aus Labels und Image-Steuerelementen: Eine clsRadio-Instanz nimmt die Labels auf, eine andere die Image-Steuerelemente.
Private pSyncObject As Variant
Public Property Get SyncObject() As Variant
Set SyncObject = pSyncObject
End Property
Public Property Let SyncObject(New_SyncObject As Variant)
If IsObject(New_SyncObject) Then
Set pSyncObject = New_SyncObject
Else
pSyncObject = New_SyncObject
End If
End Property
Public Property Set SyncObject(New_SyncObject As Variant)
Set pSyncObject = New_SyncObject
End Property
Falls Sie per Code oder vielleicht von einem Timer gesteuert die Auswahl in der Optionsgruppe von einem Objekt zum nächsten verschieben möchten, werden Sie die Methoden FirstItem, NextItem, PrevItem und LastItem sicher recht nützlich finden. Die Eigenschaft Cycle gibt dazu an, ob bei Erreichen des letzten bzw. ersten Objekts die Iteration umlaufen soll.
Private pCycle As Boolean
Public Property Get Cycle() As Boolean
Cycle = pCycle
End Property
Public Property Let Cycle(New_Cycle As Boolean)
pCycle = New_Cycle
End Property
Public Sub FirstItem()
Dim nOldSelectedItem As Object
Dim nSelectItem As Object
If mItems.Count Then
On Error Resume Next
Set nOldSelectedItem = mItems(pSelectedKeyIndex)
pSelectedKeyIndex = 1
Set nSelectItem = mItems(pSelectedKeyIndex)
If Not (nOldSelectedItem Is nSelectItem) Then
zSelect nOldSelectedItem, False
zSelect nSelectItem, True
zChanged
End If
End If
End Sub
Public Sub LastItem()
Dim nOldSelectedItem As Object
Dim nSelectItem As Object
With mItems
If .Count Then
On Error Resume Next
Set nOldSelectedItem = mItems(pSelectedKeyIndex)
pSelectedKeyIndex = .Count
Set nSelectItem = mItems(pSelectedKeyIndex)
If Not (nOldSelectedItem Is nSelectItem) Then
zSelect nOldSelectedItem, False
zSelect nSelectItem, True
zChanged
End If
End If
End With
End Sub
Public Sub NextItem()
Dim nOldSelectedItem As Object
Dim nSelectItem As Object
On Error Resume Next
Set nOldSelectedItem = mItems(pSelectedKeyIndex)
If IsNumeric(pSelectedKeyIndex) Then
pSelectedKeyIndex = pSelectedKeyIndex + 1
With mItems
If .Count Then
Select Case pCycle
Case False
If pSelectedKeyIndex > .Count Then
pSelectedKeyIndex = .Count
End If
Case True
If pSelectedKeyIndex > .Count Then
pSelectedKeyIndex = 1
End If
End Select
Else
Exit Sub
End If
End With
Else
pSelectedKeyIndex = 1
End If
Set nSelectItem = mItems(pSelectedKeyIndex)
If Not (nOldSelectedItem Is nSelectItem) Then
zSelect nOldSelectedItem, False
zSelect nSelectItem, True
zChanged
End If
End Sub
Public Sub PrevItem()
Dim nOldSelectedItem As Object
Dim nSelectItem As Object
Set nOldSelectedItem = mItems(pSelectedKeyIndex)
If IsNumeric(pSelectedKeyIndex) Then
pSelectedKeyIndex = pSelectedKeyIndex - 1
With mItems
If .Count Then
Select Case pCycle
Case False
If pSelectedKeyIndex < 1 Then
pSelectedKeyIndex = 1
End If
Case True
If pSelectedKeyIndex < 1 Then
pSelectedKeyIndex = .Count
End If
End Select
Else
Exit Sub
End If
End With
Else
pSelectedKeyIndex = 1
End If
Set nSelectItem = mItems(pSelectedKeyIndex)
If Not (nOldSelectedItem Is nSelectItem) Then
zSelect nOldSelectedItem, False
zSelect nSelectItem, True
zChanged
End If
End Sub
Diese Iterations-Möglichkeiten sind übrigens auch ein Grund dafür, dass lediglich der Index bzw. der Schlüssel des gewählten Objekts gespeichert wird, und nicht ein Verweis auf das Objekt.
Ach ja, ehe ich es vergesse: Sie können auch darauf verzichten, die Properties-Eigenschaft mit Informationen zu füllen und damit die Darstellung der Objekte einer Optionsgruppe zu ändern. Die Funktionalität, dass immer nur ein Element der Gruppe gewählt ist, haben Sie trotzdem zur Verfügung. Und Sie können die Optionsgruppe auch mit beliebigen anderen Objekten als Steuerelementen füttern - und über den Properties-Mechanismus diese Objekte auf die Auswahl reagieren lassen.
|