|
Steuerelemente wie das ListView- oder das TreeView-Steuerelement aus den Microsoft Common Controls machen es vor, wie man ein Partner-Steuerelement zu einem Steuerelement auswählen kann. Dort können Sie nämlich im Eigenschaften-Dialog das zur Darstellung der Symbole gewünschte (und benötigte) ImageList-Steuerelement aus einer Liste (Combo) der auf dem Eltern-Form (bzw. -Container) vorhandenen ImageList-Steuerelemente auswählen.
Das Prinzip ist eigentlich recht einfach. Denn die Eigenschaft ParentControls des UserControls liefert alle Objekte des Containers (Forms), auf dem es platziert ist, in einer Collection. Sie brauchen nur noch die Objekte des gesuchten Steuerelement-Typs herauszufischen und darunter das gewünschte auswählen zu lassen.
Dazu versehen Sie Ihr UserControl mit einer Friend-Methode (Function), über die von einer Eigenschaftenseite (Propertypage) aus die ParentControls-Eigenschaft zur Verfügung gestellt wird.
Friend Function FormControls() As VBRUN.ParentControls
Set FormControls = UserControl.ParentControls
End Function
Im Ereignis SelectionChanged in der Eigenschaftenseite können Sie dann diese Collection auslesen, aussieben und die Namen der passenden Steuerelemente in eine ListBox oder ein eine ComboBox einfügen.
Private Sub PropertyPage_SelectionChanged()
Dim nObj As Object
With cboImageLists
.Clear
.AddItem "[keine]"
For Each nObj In SelectedControls(0).FormControls
If TypeOf nObj Is ImageList Then
.AddItem nObj.Name
End If
Next
End With
Changed = False
End Sub
Zumindest sollte das eigentlich theoretisch so gehen. Der erste Haken dabei ist, dass die Friend-Methode FormControls offensichtlich nicht verfügbar zu sein scheint. Das liegt daran, dass in einer Eigenschaftenseite ein UserControl von "außen" gesehen wird, wie auch zur Laufzeit von einem Container-Projekt aus. Und Friend-Methoden oder -Eigenschaften sind von dort aus nicht sichtbar.
Statt dessen müssen Sie zunächst eine Variable des Typs Ihres UserControls anlegen und dieser das in SelectedControls(0) zur Verfügung stehende Objekt zuweisen. Damit werden auch die Friend-Elemente greifbar, gewissermaßen über die "Friend-Schnittstelle".
Private Sub PropertyPage_SelectionChanged()
Dim nUC As ucWithImageList
Dim nObj As Object
Set nUC = SelectedControls(0)
With cboImageLists
.Clear
.AddItem "[keine]"
For Each nObj In nUC.FormControls
If TypeOf nObj Is ImageList Then
.AddItem nObj.Name
End If
Next
End With
Changed = False
End Sub
Ein weiterer Haken zeigt sich, falls sich unter den gesuchten Steuerelementen einige aus einem Steuerelementfeld (Control-Array) befinden sollten. Sie können zwar auch die Namen dieser Steuerelemente einlesen. Doch können Sie sie in der Liste dann nicht mehr voneinander unterscheiden - die typische Kennzeichnung mit dem in Klammern dahintergesetzten Index fehlt noch. Holen wir also auch dies noch nach:
Private Sub PropertyPage_SelectionChanged()
Dim nUC As ucWithImageList
Dim nObj As Object
Dim nName As String
Set nUC = SelectedControls(0)
With cboImageLists
.Clear
.AddItem "[keine]"
For Each nObj In nUC.FormControls
If TypeOf nObj Is ImageList Then
If nObj.Index > -1 Then
nName = nObj.Name & "(" & nObj.Index & ")"
Else
nName = nObj.Name
End If
.AddItem nName
End If
Next
End With
Changed = False
End Sub
Das klappt nun schon ganz prächtig. Nun stellt sich die Frage, wie Sie die dann getroffene Auswahl Ihrem UserControl bekannt machen. Wie sie oben gesehen haben, begnügen wir uns damit, lediglich die Namen der ausgesiebten Steuerelemente in die ComboBox einzulesen. Die betreffenden Steuerelemente selbst interessieren uns gar nicht weiter - mit dem Ende des SelectionChanged-Ereignisses sind sie ja wieder vergessen.
Der erste Gedanke wäre, Ihr UserControl mit einer Friend-Eigenschaft zu versehen, der Sie den Namen des ausgewählten Steuerelements übergeben könnten. Dort wäre lediglich wieder das zu dem Namen gehörende Steuerelement aus der ParentControls-Collection herauszufischen und einer Variablen innerhalb des UserControls zuzuweisen.
Damit wären wir schon beim nächsten Problem. Denn wie würden Sie das gewählte Steuerelement im PropertyBag (für das Gespann WriteProperties/ReadProperties) ablegen wollen, damit sich das UserControl daran erinnert?
Die direkte Übergabe eines Steuerelements an das PropertyBag ist leider nicht auf die Weise möglich, wie Sie es etwa von einem Picture- oder einem Font-Objekt her gewohnt sind. Auch dort können Sie lediglich den Namen des Steuerelements ablegen und wieder zurückbekommen. Schon aus diesem Grunde liegt es nahe, den Gedanken einer reinen Ablage als String, sowohl in der UserControl-internen Variablen, als auch im PropertyBag, weiter zu verfolgen.
Wie Sie wissen, kennt ein UserControl zwei verschiedene Laufzeit-Zustände. Zum einen die Laufzeit zur Entwicklungszeit des Containers, zum anderen die "richtige" Laufzeit in der kompilierten (bzw. Debug-)Laufzeit-Version des Containers. Die Auswahl über eine Eigenschaftenseite als auch das Ablegen im PropertyBag (WriteProperties) sind nur zur Entwicklungs-Laufzeit relevant. In diesem Zustand des UserControls können wir uns also auf die Verwaltung eines Namens in String-Form beschränken. Treffen Sie also die klare Unterscheidung - dies ist anhand der UserMode-Eigenschaft des Ambient-Objekts möglich -, und Sie benötigen das gewählte Steuerelement dann auch nur noch zur Container-Laufzeit als tatsächliches Steuerelement-Objekt.
Wenn Sie nun aber schon einmal dabei sind, das betreffende Steuerelement einmal in String-Form und einmal in Objekt-Form zu behandeln und die notwendigen Mechanismen vorzusehen und zu entwickeln, können Sie Ihr UserControl mit einem Feature ausstatten, das so kein direktes Vorbild hat. Nämlich zum einen der Möglichkeit, die öffentliche Eigenschaft, über die auf das ausgewählte Steuerelement zugegriffen werden soll, auch im Eigenschaften-Fenster anzuzeigen und dort auch von Hand den Namen des gewünschten Steuerelements eingeben zu können. Aufgrund der dafür notwendigen "Dualität" der Eigenschaft (wie das genau aussieht, werde ich Ihnen gleich zeigen) sollte es dann auch möglich sein, zur kompilierten Laufzeit der Container-Anwendung ein Steuerelement nicht nur als Objekt zuzuweisen, sondern nur dessen Namen als String zu übergeben.
Die Lösung hierfür bietet die tatsächliche "Dualität" einer Eigenschaft, wenn es sich um eine Objekt-Eigenschaft handelt. Wie Sie sicher festgestellt haben werden, müssen sich die Get-Prozedur und die Let-Prozedur immer auf den gleichen Datentyp beziehen. Dies gilt jedoch nicht für die Set-Eigenschaft. Ihr Datentyp kann ein anderer sein, allerdings mit der naheliegenden Beschränkung, dass es sich um einen Objekt-Datentyp handeln muss. Dafür kann sich jedoch die Get-Prozedur als auch die Let-Prozedur auf einen beliebigen Datentyp beziehen. Soll aber über die Get-Prozedur ein über die Set-Prozedur zugewiesenes Objekt auch wieder ausgelesen werden können, muss hier natürlich auch ein für dieses Objekt gültiger Objekt-Datentyp verwendet werden. Doch nein, das stimmt nicht ganz: Sie können hier auch den Variant-Datentyp verwenden! Das Dreigespann Get/Let/Set kann also über das Datentyp-Muster Variant/Variant/Steuerelement-Typ verfügen. Und damit hätten wir, was wir benötigen.
Im UserControl brauchen wir nun also zwei Variablen zur Verwaltung des Steuerelements: eine String-Variable und eine Objekt-Variable des Steuerelement-Typs:
Private mImageListName As String
und
Private pImageList As ImageList
Die Get-Prozedur sieht noch einigermaßen einfach aus (erschrecke ich Sie etwa?):
Public Property Get ImageList() As Variant
If Ambient.UserMode Then
Set ImageList = pImageList
Else
ImageList = mImageListName
End If
End Property
Je nach Laufzeit-Zustand wird somit entweder ein Steuerelement als Objekt oder sein Name zurückgegeben.
Die Set-Prozedur weist schon einige Zeilen mehr auf. In erster Linie rührt das von einigen Sicherheitsmaßnahmen her - schließlich soll ja kein beliebiges Objekt, sondern ein Steuerelement eines ganz bestimmten Typs übernommen werden. Und natürlich muss auch das Löschen per Übergabe von Nothing möglich sein.
Public Property Set ImageList(New_ImageList As Object)
If TypeOf New_ImageList Is ImageList Then
If Ambient.UserMode Then
Set pImageList = New_ImageList
Else
mImageListName = New_ImageList.Name
End If
ElseIf New_ImageList Is Nothing Then
If Ambient.UserMode Then
Set pImageList = Nothing
Else
mImageListName = "[keine]"
End If
Else
Err.Raise 380
End If
PropertyChanged "ImageList"
End Property
Sie fragen sich, warum die Übergabe eines falschen Steuerelement-Typs nicht einfach durch die entsprechende Deklaration des Übergabeparameters gewährleistet wird? Grundsätzlich wäre das schon möglich, allerdings ausschließlich dann, wenn das UserControl privat in einem Projekt verwendet wird. Bei einem öffentlichen UserControl in einem OCX-Projekt dürfen zumindest VB-interne Steuerelement-Typen nicht übergeben werden. Auch wenn es sich hier um ein ImageList-Steuerelement handelt - VB wird trotzdem unter Ausgabe einer Fehlermeldung streiken (ach so, ja, Sie werden es doch sicher längst gemerkt haben, dass es sich die ganze Zeit um die Auswahl eines ImageList-Steuerelements dreht, gemäß den Vorbildern des ListView- und TreeView-Steuerelements, oder?). Daher die Deklaration der Übergabe "As Object" und die interne Prüfung.
Nun wird es tatsächlich etwas komplizierter - wir kommen zur Let-Prozedur:
Public Property Let ImageList(New_ImageList As Variant)
Dim nObj As Object
Dim nNewImageListName As String
Dim nIsCtlArray As Boolean
Dim nIndex As Integer
Dim nPos As Integer
If Not Ambient.UserMode Then
If VarType(New_ImageList) <> vbString Then
Err.Raise 380
End If
End If
If IsObject(New_ImageList) Then
If TypeOf New_ImageList Is ImageList Then
Set pImageList = New_ImageList
PropertyChanged "ImageList"
Exit Property
End If
ElseIf VarType(New_ImageList) = vbString Then
nNewImageListName = Trim$(New_ImageList)
nPos = InStr(nNewImageListName, "(")
If nPos Then
nIsCtlArray = True
nIndex = Val(Mid$(nNewImageListName, nPos + 1))
nNewImageListName = Left$(nNewImageListName, nPos - 1)
End If
Select Case LCase$(nNewImageListName)
Case "", "[keine]"
If Ambient.UserMode Then
Set pImageList = Nothing
Else
mImageListName = kNoImageList
PropertyChanged "ImageList"
End If
Exit Property
Case Else
For Each nObj In UserControl.ParentControls
If TypeOf nObj Is ImageList Then
If StrComp(nObj.Name, nNewImageListName, vbTextCompare) _
= 0 Then
If nIsCtlArray Then
If nObj.Index <> nIndex Then
Set nObj = Nothing
End If
End If
If Not (nObj Is Nothing) Then
If Ambient.UserMode Then
Set pImageList = nObj
Exit Property
Else
If nIsCtlArray Then
mImageListName = nObj.Name & "(" & nIndex & ")"
Else
mImageListName = nObj.Name
End If
PropertyChanged "ImageList"
Exit Property
End If
End If
End If
End If
Next
End Select
End If
Err.Raise 380
End Property
Auch hier sind wieder verschiedene Prüfungen notwendig. Denn da wir es hier, passend zur Get-Prozedur, mit der Übergabe eines Variant-Wertes zu tun haben, könnte ja alles mögliche aufkreuzen. Wir wollen es aber zur Entwicklungslaufzeit nur mit Strings, und zur Container-Laufzeit nur mit Strings oder mit Objekt-Referenzen des betreffenden Steuerelement-Typs zu tun haben. Der Aufwand zur Container-Laufzeit ist ähnlich gering wie in der Get-Prozedur, wenn ein Steuerelement-Objekt übergeben wird. Wird dagegen ein String übergeben, müssen wir uns auch darum kümmern, dass ein Steuerelement-Name am Ende eine Index-Kennung tragen kann. Oder dass ein leerer String oder ein Default-Name wie beispielsweise "[keine]" als Löschung übergeben wird. Grundsätzlich müssen wir natürlich auch prüfen, in beiden Laufzeitzuständen gleichermaßen, ob es das mit dem übergebenen Namen bezeichnete Steuerelement (und dann gegebenenfalls auch noch mit dem richtigen Index) tatsächlich gibt. Und zur Container-Laufzeit müssen wir schließlich das betreffende Steuerelement aus der ParentControls-Collection der im UserControl vorgesehenen Objekt-Variablen für das Steuerelement zuweisen.
Das Schwierigste ist nun eigentlich geschafft. Bleibt nur noch die "Kleinigkeit" der Ablage im PropertyBag. Der kleinere Teil dieser "Kleinigkeit" wiederum betrifft das WriteProperties-Ereignis. Hier brauchen wir nur den Namen ins PropertyBag hineinzuschieben...:
Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
PropBag.WriteProperty "ImageList", mImageListName, "[keine]"
End Sub
...und auch wieder auszulesen:
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
mImageListName = PropBag.ReadProperty("ImageList", "[keine]")
End Sub
Das ist doch einfach, oder nicht? Klar. Nur, zur Container-Laufzeit können Sie bzw. Ihr UserControl mit dem Namen allein ja nicht viel anfangen. Also einfach den Namen sogleich an unsere raffinierte Let-Prozedur übergeben?
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
mImageListName = PropBag.ReadProperty("ImageList", "[keine]")
If Ambient.UserMode Then
Me.ImageList = mImageListName
End If
End Sub
Sie werden pures Glück haben, wenn das funktioniert - rein zufällig funktioniert. Denn zu dem Zeitpunkt, zu dem das ReadProperties-Ereignis eintrifft, ist keineswegs sichergestellt, dass der übergeordnete Container das gewünschte Steuerelement dieses Namens bereits geladen hat und kennt. Es hängt einfach von der Ladereihenfolge ab, auf die Sie keinerlei Einfluss haben und nehmen können. Sie brauchen also eine Verzögerung, bis der Ladevorgang aller Steuerelemente im Container abgeschlossen ist, bevor das gesuchte Steuerelement sicher gefunden werden kann. Diese Verzögerung beschafft uns ein Timer mit einem extrem kurzen Intervall (= 1), der standardmäßig deaktiviert ist und nur zur Container-Laufzeit aktiviert wird:
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
mImageListName = PropBag.ReadProperty("ImageList", "[keine]")
tmr.Enabled = Ambient.UserMode
End Sub
Private Sub tmr_Timer()
tmr.Enabled = False
Me.ImageList = mImageListName
End Sub
Im Timer-Ereignis wird der Timer sofort wieder deaktiviert - wir benötigen seinen Dienst ja nur ein einziges Mal. Erst hier und jetzt wird der aus dem PropertyBag ausgelesene Steuerelement-Name der Let-Prozedur zugewiesen. Dieses Timer-Ereignis wird erst ausgelöst, wenn die ganzen Initialisierungsvorgänge des Containers als auch der darauf befindlichen Steuerelemente erledigt sind - das heißt, dass das beispielsweise das Form_Load-Ereignis vollständig abgearbeitet ist. Bis dies jedoch der Fall ist, kann uns noch etwas anderes in die Quere kommen. Nämlich ein Aufruf der Get-Prozedur, also ein Auslesen der Eigenschaft im Form_Load-Ereignis - er würde Nothing zurück geben, da die Objekt-Variable für das Steuerelement noch nicht belegt worden ist. Natürlich könnten Sie eine solche Aktion per Hinweis in der Dokumentation verbieten. Aber erstens ist das nicht die feine Art, und zweitens bedarf es nur geringfügiger Modifikationen, auch diesen Stolperstein aus dem Weg zu räumen.
Auch wenn das Timer-Ereignis erst nach Abschluss des Form_Load-Ereignisses eintrifft, steht das gesuchte Steuerelement ja bereits während der ganzen Form_Load-Ereignisprozedur zur Verfügung. Wir haben nur deswegen den Timer als Hilfsmittel ausgesucht, weil es keine Möglichkeit gibt, mit vertretbarem Aufwand den Zeitpunkt zwischen vollständiger Initialisierung der Steuerelemente auf dem Form (bzw. im Container) und dem Beginn des Form_Load-Ereignisses zu bestimmen Zwar könnten Sie sich auf COM-Ebene in die Geschehnisse einklinken. Doch das ist mit Visual Basic-Bordmitteln erstens gar nicht möglich, und zweitens stünde der Aufwand in keinem Verhältnis mehr zum Effekt. Denn die Timer-Hilfe funktioniert zuverlässig, wenn das UserControl bzw. das kompilierte OCX in einem VB(A)-Projekt verwendet wird.
Die wesentlichste Modifikation betrifft die Get-Prozedur. Hier müssen wir sicherstellen, dass ein vorzeitiger Zugriff (ein Zugriff vor dem Auslösen des Timer-Ereignisses) trotzdem zum Aufruf der Let-Prozedur mit dem bereits aus dem PropertyBag ausgelesenen Steuerelement-Namen führt. Dazu benötigen wir eine weitere Hilfsvariable, mFirst, die im ReadProperties-Ereignis auf True gesetzt wird.
Private mFirst As Boolean
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
mImageListName = PropBag.ReadProperty("ImageList", "[keine]")
mFirst = Ambient.UserMode
tmr.Enabled = mFirst
End Sub
In der Get-Prozedur erfolgt zur Container-Laufzeit ausdrücklich dann erst einmal die Selbstzuweisung des Steuerelement-Namens an die Eigenschaft ImageList, so dass danach der korrekte Inhalt der Objekt-Variablen pImageList gewährleistet ist. Da ja auch tatsächlich kein Steuerelement gewählt worden sein kann, kann sie natürlich auch nach wie vor Nothing enthalten und zurückgeben.
Public Property Get ImageList() As Variant
If Ambient.UserMode Then
If mFirst And pImageList Is Nothing Then
mFirst = False
Me.ImageList = mImageListName
End If
Set ImageList = pImageList
Else
ImageList = mImageListName
End If
End Property
Für den Fall, dass doch das Timer-Ereignis früher eintreten sollte, wird dort die Hilfsvariable mFirst auch zurückgesetzt, so dass sich die Get-Prozedur danach "völlig normal" verhält.
Private Sub tmr_Timer()
tmr.Enabled = False
If mFirst Then
mFirst = False
Me.ImageList = mImageListName
End If
End Sub
Die Innereien des UserControls hätten wir damit nun komplett erledigt. Kommen wir nun noch einmal zur Eigenschaftenseite zurück. Dort hatten wir ja zuvor lediglich das Einlesen der Steuerelement-Namen in die Auswahl-ComboBox erledigt. Es fehlt noch die konkrete Zuweisung der Auswahl an die Eigenschaft des UserControls.
Zuvor schieben wir aber noch das nette Feature dazwischen, dass die ComboBox beim Öffnen der Eigenschaftenseite gleich den Namen des eventuell früher schon ausgewählten Steuerelements anzeigt.
Private mSelectedName As String
Private Sub PropertyPage_SelectionChanged()
Dim nUC As ucWithImageList
Dim nObj As Object
Dim nName As String
mSelectedName = ""
Set nUC = SelectedControls(0)
With cboImageLists
.Clear
.AddItem "[keine]"
For Each nObj In nUC.FormControls
If TypeOf nObj Is ImageList Then
If nObj.Index > -1 Then
nName = nObj.Name & "(" & nObj.Index & ")"
Else
nName = nObj.Name
End If
.AddItem nName
If nName = nUC.ImageList Then
mSelectedName = nName
End If
End If
Next
End With
If mSelectedName = "" Then
zComboFindText cboImageLists, "[keine]", , True, True
Else
zComboFindText cboImageLists, mSelectedName, , True, True
End If
Changed = False
tmr.Enabled = True
End Sub
Wir fügen hier einfach den Vergleich des in der Schleife jeweils gerade eingelesenen Namens mit dem noch aktuellen Wert der Eigenschaft im UserControl ein - ist dieser gerade an der Reihe, wird er in der Variablen mSelectedName festgehalten. Im Anschluss an die Schleife wird der Name, so denn einer gefunden wurde, in der ComboBox gesucht und der ListIndex entsprechend gesetzt. Wurde kein (gültiger) Name gefunden, wird die Default-Anzeige "[keine]" gewählt. Auf die Suche des Namens in der ComboBox gehe ich hier nicht weiter ein - eine Beschreibung der Hilfsfunktion zComboFindText finden Sie unter: "Wer suchet, der findet". (Wundern Sie sich bitte nicht über die Zeile "tmr.Enabled = True" - was es mit diesem Timer auf sich hat, werde ich später noch erklären.)
Da wir die Variable mSelectedName zur Aufnahme den gefundenen Namens nicht prozedurlokal deklariert haben, sondern modulweit für die Eigenschaftenseite, können wir ihn dazu verwenden, im Click-Ereignis der ComboBox nur dann das Changed-Flag der Eigenschaftenseite zu setzen, wenn ein anderer Name gewählt worden ist.
Private Sub cboImageLists_Click()
Changed = Not (mSelectedName = cboImageLists.Text)
End Sub
Nun folgt noch eine allerletzte Kleinigkeit. Wenn wir schon mit dem Get/Let/Set-Dreigespann im UserControl dafür gesorgt haben, dass die Eigenschaft im Eigenschaftenfenster aufgelistet wird, könnten wir ja auch noch dafür sorgen, dass die Eigenschaftenseite auch separat zu der Eigenschaft über eine "..."-Schaltfläche neben der Eigenschaft geöffnet werden kann. Dazu ordnen Sie einfach die Eigenschaftenseite in den Prozedureigenschaften der Eigenschaft des UserControls zu.
Allerdings sorgt irgendeine Unstimmigkeit (wenn es nicht gar ein Bug ist) in der Verwaltung oder Implementierung der Eigenschaftenseiten in VB dafür, dass nach dem Betätigen der "Übernehmen"-Schaltfläche die Eigenschaftenseite die Verbindung zum UserControl verliert. Ein ausdrücklicher, interner Aufruf der Ereignisprozedur SelectionChanged nach der Zuweisung an das UserControl im ApplyChanges-Ereignis behebt offensichtlich und interessanterweise den Fehler.
Private Sub PropertyPage_ApplyChanges()
SelectedControls(0).ImageList = cboImageLists.Text
PropertyPage_SelectionChanged
End Sub
Damit jedoch die ComboBox sowohl nach diesem Wiederanzeigen als auch nach dem erstmaligen Anzeigen beim Öffnen der Eigenschaftenseite zuverlässig den Fokus bekommen kann, benötigen wir hier ebenfalls einen (bereits weiter oben erwähnten) Timer zur Verzögerung. Er wird zum Abschluss der SelectionChanged-Prozedur aktiviert und setzt den Fokus auf die ComboBox, sobald diese sichtbar zur Verfügung steht.
Private Sub tmr_Timer()
tmr.Enabled = False
On Error Resume Next
cboImageLists.SetFocus
End Sub
|