Translate

Montag, 11. Dezember 2017

ACCESS: Fokus auf das erste mögliche Steuerelement in einem Unterformular setzen

Eine häufige Aufgabe ist es, den Fokus auf ein Steuerelement zu setzen, welches sich in einem Unterformular befindet.

Nehmen wir mal an, unser Unterformular trägt den Name subFormBestellungen. Innerhalb von diesem Unterformular möchten wir den Fokus auf das Steuerelement mit den Namen cboArtikel setzen.

Die folgende Methode dazu ist zwar korrekt geschrieben, wird jedoch einen Fehler liefern:
Me.subFormBestellungen.Form.cboArtikel.SetFocus
Warum? Weil der Fokus explizit vorher auf das Unterformular gesetzt werden muss.

Daher benötigen wir immer diese 2 Anweisungen:
Me.subFormBestellungen.SetFocus
Me.subFormBestellungen.Form.cboArtikel.SetFocus

Die Aufgabe ist soweit gelöst, nur müsen wir uns in der Programmierung auf ein genaues Steuerelement festlegen, in unserem Fallbeispiel cboArtikel.
Nun könnte es ja sein, dass dieses Steuerelement momentan deaktiviert oder ausgeblendet oder aus anderen Gründen nicht verfügbar ist. Dann sollte das nächste Steuerelement genommen werden. Ich benötige also eine Funktion, die den Fokus auf das nächst mögliche Steuerelement setzt und dabei die Aktivierreihenfolge der Steuerelemente berücksichtigt.

Der Verweis auf das erste Steuerelement in einem Formular erfolgt mit Controls(0). Leider ist Controls(0) nicht das erste Steuerelement aus der hinterlegten Aktivierreihenfolge, sondern es entspricht das erste Steuerelement welches im Entwurfsmodus eingefügt wurde. Interessiert uns für unsere Aufgabe aber herzlich wenig!

Um die Steuerelemente in der Reihenfolge aus der Aktivierreihenfolge zu erhalten, müssen wir die Eigenschaft TabIndex (Reihenfolgeposition) auswerten:


Wir brauchen zu nächste eine Prozedur die uns eine Liste aller Steuerelemente aus einem Formular in der aufsteigenden Aktivierreihenfolge zusammenstellt. Dabei sollten wir die Steuerelemente die nicht die Eigenschaft TabIndex haben, außenvorlassen. Und ggf. auch die nicht berücksichtigen, bei denen die Eigenschaft TabStop auf Nein steht.

Fangen wir somit mit der Prozedur an, die uns die Information liefert, ob ein Objekt eine Eigenschaft besitzt:
Public Function ExistProperty(ByRef obj As Object, ByRef prpName As String, Optional ByRef prp As Property) As Boolean
   For Each prp In obj.Properties
      If prpName = prp.Name Then
         ExistProperty = True
         Exit For
      End If
   Next
End Function

Nun zur Prozedur die uns eine Liste aller Steuerelemente aus einem Formular in der aufsteigenden Aktivierreihenfolge zusammenstellt:
Public Function GetControlsInTabindexOrder(frm As Access.Form, ByRef ctlList() As Access.control _
                                            , Optional OnlyWithTabStop As Boolean) As Boolean
    'OnlyWithTabStop=True means just consider the controls where tabstop is true
    'Function return True if at least one control was found
    Dim ctl As Access.control, i As Long, ii As Long, z As Long
    
    Do
        For Each ctl In frm.Controls
            If ctl.Parent.Name = frm.Name Then
                If ExistProperty(ctl, "TabIndex") Then
                    If ctl.TabIndex = i Then
                        If ctl.TabStop Or Not OnlyWithTabStop Then
                            ReDim Preserve ctlList(ii)
                            Set ctlList(ii) = ctl
                            ii = ii + 1
                            GetControlsInTabindexOrder = True
                            Exit For
                        End If
                        i = i + 1
                    End If
                End If
            End If
        Next
        z = z + 1
    Loop Until z >= frm.Controls.count
End Function

Zu guter letzt die Prozedur die versucht aus der Steuerelement-Liste den Fokus solange auf einen Steuerelement nach dem anderen zu setzen, bis es erfolgreich ist:
Public Function SetFocusFirstControl(frm As Access.Form _
                                        , Optional OnlyWithTabStop As Boolean = True) As Boolean
    Dim ctlList() As Access.control, i As Long
    
    'Hole alle Controls in Reihenfolge laut Aktivierreihenfolge
    If GetControlsInTabindexOrder(frm, ctlList(), OnlyWithTabStop) Then
        On Error Resume Next
        'Schleife durch alle mögliche Controls die den Fokus bekommen könnten
        For i = 0 To UBound(ctlList)
            ctlList(i).SetFocus 'Versuch den Fokus zu setzen (Könnte misslingen aus div. Gründen (Not Enabled, not Visisble usw...)
            If Err.Number = 0 Then
                SetFocusFirstControl = True
                Exit For 'Fokus konnte erfolgreich gesetzt werden, also Schleife beenden, Job erfolgreich erledigt
            End If
            Err.Clear 'Fokus konnte nicht erfolgreich gesetzt werden, also Schleife fortführen
        Next
    End If
End Function

Anwendungsbeispiel: Bei Klick auf die Schaltfläche 'cmdEingabeBestelldaten' soll der Fokus im Unterformular 'SubForm' auf das erste mögliche Steuerelement gesetzt werden:
Private Sub cmdEingabeBestelldaten_Click()
    Me.SubForm.SetFocus
    SetFocusFirstControl Me.SubForm.Form
End Sub

Montag, 20. November 2017

ACCESS: Anpassen der Spaltenbreiten in ListBox bei Formular-Resize




Ich widmete mich mal wieder dem Listenfeld (ListBox) von Microsoft Access.

Die Spaltenbreiten bei Listenfelder können flexibel gehalten werden, in dem die Spaltenbreiten nicht explizit auf eine genaue Breite festgelegt werden. Wobei es durchaus ausreicht, dass nur einer der Spalten ohne festgelegte Breite definiert ist. Dies könnte z.B. so aussehen:

Private Sub Form_Load()
       Me.MeinListenfeld.ColumnWidths = "2cm;;1cm;;4cm"
End Sub
 
Was ist wichtig an dieser Stelle? Dass für die Spalte, deren Breite flexibel eingestellt werden soll, keinen numerischen Wert angeben, sondern den Platz zwischen den entsprechenden Semikola (;) einfach freilassen.

Seit der Version 2007 gibt es die Möglichkeit über die Eigenschaft Horizontaler Anker (HorizontalAnchor), die Einstellung so vorzunehmen, das die Breite der Steuerelemente sich automatisch entsprechend der Breite vom Formular anpassen. Das finde ich tatsächlich eine sehr hilfreiche Funktion, die ich für die vorherigen Access-Versionen aufwendig in VBA programmiert habe.

Leider hat Microsoft es verpasst für diesen Steuerelementtyp die Spaltenbreiten bei Änderung der Formulargröße auch mit anpassen zu lassen.
Das ist sehr schade, den dadurch ist die dynamik in der Breite bei Listenfelder so ziemlich nutzlos.

Abhilfe schafft da einen kleiner VBA Code beim Event Bei Größenänderung (Resize), der die Spaltenbreiten einfach jeweils erneut setzt:
Private Sub Form_Resize()
       Me.MeinListenfeld.ColumnWidths = Me.MeinListenfeld.ColumnWidths
End Sub
Das Ergebnis überzeugt: Von nun an ändert sich die Breite der Spalte mit der Größenänderung des Formulars.

Damit könnte mein Post auch schon zu Ende sein. Aufmerksame Leser meines Blogs wissen jedoch, dass ich gerne auch Multiselect-Listenfelder verwende.

Und schon haben wir das nächste Problem vorliegen.

In Kombination der Resize-Lösung, schmeißt das Multiselect-Listenfeld alle aktuellen Markierungen raus!

An dieser Stelle habe ich meine Lösung in Frage gestellt und nach einer anderen Lösung gesucht, die nicht dieses Problem hervorruft. Leider vergebens. Ich würde mich sehr freuen, wenn jemand hierfür bereits eine Lösung gefunden hat, die erst gar nicht das Problem auslöst. Bitte als Kommentar posten, danke!

Das ist jetzt wieder sehr ärgerlich, aber auch hierfür habe ich mir eine Lösung überlegt:

Bevor wir die ColumnWidths erneut festlegen, merken wir uns die aktuellen Markierungen über die Auflistung ItemsSelected in einer Array-Variable. Anschließend setzen wir die ColumnWidths, und die verlorenen Markierungen stellen wir über unser zuvor erstelltes Array wieder her:
Public Sub RefreshColumnWidths(cListbox As Access.ListBox)
    Dim itms() As Long, i As Variant, ii As Long
   
    For Each i In cListbox.ItemsSelected
        ReDim Preserve itms(ii)
        itms(ii) = i
        ii = ii + 1
    Next
    cListbox.ColumnWidths = cListbox.ColumnWidths
    If ii > 0 Then
        For i = 0 To UBound(itms)
            cListbox.Selected(itms(i)) = True
        Next
    End If
End Sub
Bei eine große Menge an Einträgen, kann die Funktion durchaus etwas länger dauern (im Sekunden Bereich), da sollten wir diese nicht nach jedem Resize-Event des Formulars ausführen lassen. Nutzen wir doch einfach das Timer-Event des Formulars, um erst nach einer gewissen Verzögerung die Spaltenbreiten neu setzen zu lassen.


Private Sub Form_Resize()
    Me.TimerInterval = 100
End Sub

Private Sub Form_Timer()
    Me.TimerInterval = 0
    RefreshColumnWidths MeinListenfeld
End Sub
Wir benötigen nun also 3 Prozeduren (2 Event- und eine Public-Prozedur), um die Versäumnisse von Microsoft nachzuholen. Also Code, der für das eigentliche Projekt nicht dient, sondern nur für Korrekturen.

An dieser Stelle kommt ein Klassenmodul ins Spiel, indem ich die 3 Prozeduren in dieses Klassenmodul auslagere.
In meinem Post Markierung geht in ListBox mit Multiselect (extended) Einstellung verloren hatte ich ja bereits die Listenfelder behandelt. Daher habe ich die drei benötigten Prozeduren einfach in das dort vorliegende Klassenmodul integriert und schon sind wir fertig.

Es bleibt bei einem Minum an Anweisungen pro Formular:
Dim cCorrectionControls As clsCorrectionControls
Private Sub Form_Load()
    Set cCorrectionControls = New clsCorrectionControls
    cCorrectionControls.SetForm = Me
End Sub

Die aktuelle Datei steht hier zum Herunterladen bereit.