Translate

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.

Montag, 2. Januar 2017

ACCESS: Markierung geht in ListBox mit Multiselect (extended) Einstellung verloren!

Das Access-Steuerelement ListBox in einem Formular ist dazu da Einträge in einer Listenform darzustellen. Diese Einträge lassen sich auswählen/markieren.
Die Markierten Einträge können in VBA über die Auflistung ItemsSelected ausgewertet werden.
Über die Eigenschaft "Mehrfachauswahl/MultiSelect" kann eingestellt werden ob ein Benutzer immer nur einen Eintrag oder mehrerer auswählen kann.
Die dritte Option "Erweitert" welches den Wert 2 entspricht ist leider Buggy!
Die Markierung gehen teilweise verloren, sobald das Listenfeld den Fokus verliert.

Folgendermaßen kann der Fehler reproduziert werden:
- Mit der Maus ziehen und dabei einige Einträge in der ListBox markieren
- Einen oder mehrere Einträge nachträglich abwählen mit Hilfe der gedrückten STRG-Taste
- Wenn die ListBox nun den Fokus verliert (z.B durch Klick auf ein anderes Feld) sind alle Einträge unterhalb des ersten abgewählten Eintrages nicht mehr markiert.

Das Problem ist lediglich in der Anzeige, die Auflistung ItemsSelected  enthält weiterhin die korrekt ausgewählten Einträge!

Aus Benutzersicht sind die Eintrage jedoch nicht mehr markiert, was sehr verwirrend ist. Die Anzeige ist somit schlichtweg falsch!

Das sollten wir schnellstmöglich lösen, und zwar in dem wir beim Fokusverlust der ListBox einfach die ausgewählten Einträge per VBA einfach nochmal explizit auswählen lassen. Das geht, weil ja nur die Anzeige falsch ist, die Auflistung ItemsSelected  ist weiterhin korrekt.

Somit löst die folgende Prozedur den kleinen aber sehr unschönen Bug:
Private Sub Liste_LostFocus()
    Dim itm As Variant
   
    For Each itm In Me.Liste.ItemsSelected
        Me.Liste.Selected(itm) = False
        Me.Liste.Selected(itm) = True
    Next
End Sub

Anmerkung: Sollten sehr viele Einträge ausgewählt sein, kann die Schleife zu Verzögerungen führen.

Ein kleines Beispiel zur Veranschaulichung kann hier heruntergeladen werden.
Dort habe ich auch die Lösung in einer Klasse gekapselt, so dass die Klasse selbstständig für alle ListBoxen im Formular die Korrektur vornimmt.

Somit reduzieren sich die Anweisungen pro Formular auf ein Minimum:
   
Dim cCorrectionControls As clsCorrectionControls
Private Sub Form_Load()
    Set cCorrectionControls = New clsCorrectionControls

    cCorrectionControls.SetForm = Me
End Sub


Das Jahr fängt ja gut an!

Mittwoch, 21. Dezember 2016

ACCESS: Anpassung der Unterformular-Steuerelement-Größe an die exakte Größe des abgebildeten Unterformulars

Wird in einer Formular-Entwurfsansicht ein Unterformular hinzugefügt, dann passt Access das Unterformular-Steuerelement an die Größe des Unterformulars an.
Diese Anpassung ist leider nicht sehr exakt, was wieder manuelle Nachbesserung erfordert.
Noch problematischer ist es, wenn nachträglich die Größe des Unterformulars geändert wird. Dann erfolgt leider keine Anpassung mehr.
Schon alleine das ändern der Bildlaufleisten oder das hinzufügen von einem Formularkopf haben bereits Auswirkung auf die Formulargröße. 

Auch genervt von ständigen Anpassen der Unterformular-Steuerelemente?

Dann lassen wir doch einfach zur Laufzeit die Unterformular-Steuerelement-Größe an die tatsächliche Unterformulargröße anpassen.
Um die Größe des Unterformular korrekt auszulesen, müssen alle Elemente die eine Auswirkung auf die Größe haben berücksichtigt werden, wie z.B. Navigationsschaltflächen/Datensatzmarkierer/Bildlaufleisten/Formularkopf-/fuß

Genau das erledigt die folgende Prozedur:
Public Sub ResizeControlFromSubform(SubformControl As Access.SubForm _
                                , Optional ByRef SubformWidth As Long _
                                , Optional ByRef SubformHeight As Long)
    Dim FormReportOBJ As Object, SectionOBJ As Section
    Const SCROLLBARWIDTH As Integer = 290
    Const BORDERFACTOR As Byte = 20, MINIBORDERWIDTH As Byte = 7
   
    If Not SubformControl Is Nothing Then
        Set FormReportOBJ = SubformControl.Form
        Select Case FormReportOBJ.ScrollBars
            Case 1
                FormReportOBJ.ScrollBars = 0
            Case 3
                FormReportOBJ.ScrollBars = 2
        End Select
       
        SubformHeight = MINIBORDERWIDTH
        SubformWidth = MINIBORDERWIDTH
        If FormReportOBJ.Section(acDetail).Visible = True Then
            SubformHeight = SubformHeight + FormReportOBJ.Section(acDetail).Height
        End If
        If SectionExists(FormReportOBJ, acHeader, SectionOBJ) Then
            If SectionOBJ.Visible = True Then
                SubformHeight = SubformHeight + SectionOBJ.Height
            End If
        End If
        If SectionExists(FormReportOBJ, acFooter, SectionOBJ) Then
            If SectionOBJ.Visible = True Then
                SubformHeight = SubformHeight + SectionOBJ.Height
            End If
        End If
        If FormReportOBJ.NavigationButtons = True Then
            SubformHeight = SubformHeight + SCROLLBARWIDTH
        End If
       
        SubformWidth = SubformWidth + FormReportOBJ.Width
        If FormReportOBJ.ScrollBars = 2 Or FormReportOBJ.ScrollBars = 3 Then
            SubformWidth = SubformWidth + SCROLLBARWIDTH
        End If
        If FormReportOBJ.RecordSelectors = True Then
            SubformWidth = SubformWidth + SCROLLBARWIDTH
        End If
       
        If Not SubformControl.BorderStyle = 0 Then
            SubformHeight = SubformHeight + (SubformControl.BorderWidth * BORDERFACTOR)
            SubformWidth = SubformWidth + (SubformControl.BorderWidth * BORDERFACTOR)
        End If
       
        SubformControl.Width = SubformWidth
        SubformControl.Height = SubformHeight
    End If
End Sub


Hinweis: Es wird die Funktionsprozedur SectionExists() benötigt, die hier mit ausführlicher Beschreibung hinterlegt ist.

Jetzt brauchen wir nur beim starten des Hauptformulars die Prozedur ausführen zu lassen, in dem wir das abgebildete Formular-Objekt übergeben:
Private Sub Form_Load()
    ResizeControlFromSubform (me.MeinUnterformularsteuerelement)
End Sub


Noch besser, wir belasten unser Hauptformular erst gar nicht damit, sondern wir hinterlegen den Code einfach im Unterformular:
Private Sub Form_Load()
    ResizeControlFromSubform ControlFromSubform (Me)
End Sub

Durch die Funktion ControlFromSubform() wird das unterformular-Steuerelement im übergeordneten Formular ermittelt. Diese Funktion ist hier ausführlich beschrieben.

Somit können wir jetzt unser Unterformular in beliebigen Formularen einbetten, es wird immer vollständig dargestellt, byby das Gefummel mit der Steuerelementgröße.
Und an den Hauptformularen muss kein Code hinterlegt werden, den dieser befindet sich ja dank der Funktion ControlFromSubform() im Unterformular :-)

Ein kleines Beispiel mit ausführlicher Kommentierung kann hier heruntergeladen werden.

Anmerkung:  Die Werte der Konstanten SCROLLBARWIDTH / BORDERFACTOR / MINIBORDERWIDTH müssen ggf. angepasst werden, wenn besondere Windows-Design-Einstellungen vorgenommen wurden. Diese könnte man dynamische über Windows-Funktionen auslesen. Vielleicht findet sich jemand, der eine Lösung dazu hier postet.

PS:
Unsere Prozedur ResizeControlFromSubform liefert sogar die entsprechenden ermittelten Werte über die Übergabeparameter-Variablen SubformWidth & SubformHeight zurück.

Wenn gewünscht kann man im Formularentwurf des Hauptformulars die Eigenschaften Breite+Höhe manuell mit den ermittelten Werten befüllen. Achtung, die manuelle Angabe muss in cm erfolgen, geliefert werden jedoch twips. Also einfach die Werte durch 567 teilen ;-)