Im ersten Teil dieser Beitragsreihe haben wir gezeigt, wie das Filterformular in eine eigene Datenbank eingebaut und bedient wird. In diesem zweiten Teil werfen wir einen Blick hinter die Kulissen: Wie ist das Filterformular aufgebaut, wie erkennt es den Feldtyp aus dem Recordset, wie baut es den SQL-WHERE-String zusammen, wie funktioniert die dynamische Höhenanpassung – und welche technischen Besonderheiten mussten gelöst werden. Einige dieser Lösungen sind nicht auf den ersten Blick offensichtlich, liefern aber wertvolle Denkanstöße für eigene Projekte.
Architektur: Zwei Bausteine, eine Schnittstelle
Das Filterformular besteht aus zwei Bausteinen: dem Modul mdlFilter mit allen Hilfsfunktionen und dem Formularmodul Form_frmFilter mit der gesamten Steuerlogik. Die Trennung ist bewusst gewählt: mdlFilter enthält ausschließlich Funktionen ohne Bindung an ein konkretes Formular – Feldtyperkennung, SQL-Aufbau, Datumsparsen, Wertevalidierung.
Das Formularmodul nutzt diese Funktionen und kümmert sich um alles was mit der Benutzeroberfläche zu tun hat: Steuerelemente ein- und ausblenden, Positionen setzen, Ereignisse verarbeiten.
Diese klare Aufgabentrennung hat einen praktischen Vorteil: mdlFilter kann unverändert wiederverwendet werden wenn das Formular künftig angepasst oder durch eine andere Darstellung ersetzt wird.
Die Kommunikation zwischen dem Filterformular und dem aufrufenden Formular läuft über zwei öffentliche Eigenschaften und eine öffentliche Prozedur. Das aufrufende Formular übergibt beim Öffnen über CallingForm eine Referenz auf sich selbst und über RecordsetForm das zu filternde Recordset.
Das Filterformular ruft später FilterAnwenden im aufrufenden Formular auf und übergibt den fertigen WHERE-String als Parameter. Diese drei Elemente bilden die vollständige Schnittstelle – mehr ist für den Einbau nicht notwendig. Das folgende Listing zeigt die beiden Property-Prozeduren:
Public Property Set CallingForm(frm As Form) Set m_frmAufrufend = frm End Property Public Property Set RecordsetForm(rst As Object) Set m_rst = rst m_bolDAO = (TypeName(rst) = "Recordset") _ Or (TypeName(rst) = "Recordset2") LadeFelder End Property
Der Parameter RecordsetForm ist als Object typisiert statt als konkreter Recordset-Typ.
Das hat zwei Vorteile: Erstens funktioniert die Übergabe sowohl mit einem DAO-Recordset als auch mit einem ADODB-Recordset, ohne dass der Aufrufer sich um den Typ kümmern muss.
Zweitens ist das Filterformular damit für künftige Erweiterungen vorbereitet. Das Flag m_bolDAO wird über TypeName gesetzt: Ein DAO-Recordset trägt den Typnamen Recordset oder Recordset2 – je nach Access-Version und Datenbanktyp – ein ADODB-Recordset dagegen nicht.
Dieses Flag ist entscheidend für die Feldtyperkennung, wie wir im nächsten Abschnitt sehen werden.
Sobald RecordsetForm gesetzt wird, ruft die Property-Prozedur automatisch LadeFelder auf. Diese Prozedur liest alle Felder des Recordsets aus, bestimmt für jedes den Feldtyp und befüllt die RowSource aller 25 cboFeld-Kombinationsfelder mit den Feldnamen.
Das bedeutet: Der Benutzer sieht im Feldauswahl-Dropdown sofort nach dem Öffnen alle verfügbaren Felder – ohne dass eine manuelle Konfiguration der Felder notwendig wäre. Das Filterformular passt sich damit automatisch an jedes Formular an, dem es zugewiesen wird.
Das Steuerelemente-Raster: 175 Elemente auf einem Haufen
Das Filterformular unterstützt bis zu fünf ODER-Gruppen mit je fünf Filterzeilen – also 25 Zeilen insgesamt.
Jede Zeile besteht aus sieben Steuerelementen die je nach Feldtyp und gewählter Bedingung wechselweise ein- und ausgeblendet werden.
Die Steuerelemente pro Zeile im Überblick: cboFeld_g_z für die Feldauswahl, cboBedingung_g_z für den Vergleichsoperator, cboDatumstyp_g_z für den Datumstyp bei Datumsfeldern, txtWert_g_z für den Vergleichswert bei Text- und Zahlenfeldern, txtDatum_g_z für konkrete Datumseingaben, txtDatumA_g_z für den Bis-Wert bei Datum-Zwischen-Bedingungen, txtWert2_g_z für den Bis-Wert bei Zahlen-Zwischen-Bedingungen und cmdLoeschen_g_z zum Entfernen der Zeile.
Das Namensschema ist einheitlich: g steht für die Gruppennummer (1 bis 5), z für die Zeilennummer (1 bis 5).
Alle Ereignisprozeduren im Formularmodul folgen demselben Schema und delegieren an generische Hilfsprozeduren: cboFeld_1_1_AfterUpdate ruft FeldGewaehlt 1, 1 auf, cmdLoeschen_2_3_Click ruft ZeileLeeren 2, 3 auf.
Das macht den Code überschaubar trotz der großen Zahl von Ereignisprozeduren. Für alle 25 Zeilen entstehen so je Ereignistyp 25 identisch aufgebaute Einzeiler. Die eigentliche Logik steckt in den generischen Hilfsprozeduren die g und z als Parameter erhalten und alle Steuerelementnamen daraus zusammensetzen – also etwa Me(„cboFeld_“ & g & „_“ & z).
Alle 175 Einzelsteuerelemente liegen im Formular an der Y-Position 0, also übereinander gestapelt. Nur die jeweils aktiven Zeilen werden sichtbar gemacht und an ihre richtige vertikale Position verschoben.
Unsichtbare Zeilen bleiben bei Y=0 und nehmen keinen Platz ein. Diese Technik ist der zentrale Trick der es überhaupt ermöglicht, die Höhe des Formulars dynamisch zu steuern: Access richtet die Höhe des Detailbereichs nicht nach den unsichtbaren Steuerelementen aus, sondern nach dem Wert der Eigenschaft Section(acDetail).Height – und den setzen wir per Code.
Würden die unsichtbaren Steuerelemente an ihren ursprünglichen Positionen verbleiben, würde Access den Detailbereich nicht kleiner darstellen als notwendig um alle Elemente – auch die unsichtbaren – zu umschließen. Das Formular wäre dann stets so hoch wie alle 25 Zeilen zusammen.
Feldtypen erkennen: Die DAO/ADODB-Kollision
Sobald das Recordset übergeben wurde, liest LadeFelder alle Felder aus und bestimmt für jedes den Feldtyp. Das Filterformular unterscheidet vier eigene Feldtypen, die als öffentliche Konstanten in mdlFilter definiert sind: cFeldtypText (1), cFeldtypZahl (2), cFeldtypDatum (3) und cFeldtypJaNein (4).
Diese vier Typen steuern, welche Vergleichsoperatoren angeboten werden, welches Eingabefeld erscheint und wie der Wert später im SQL-Ausdruck gequotet wird. Die Zuordnung vom nativen Typwert des Recordset-Feldes auf einen dieser vier Typen übernimmt die Funktion GetFeldtyp in mdlFilter. Sie erhält neben dem numerischen Typwert auch das Flag bolDAO (siehe Listing 1).
Public Function GetFeldtyp(lngTyp As Long, bolDAO As Boolean) As Long If bolDAO Then Select Case lngTyp Case 2, 3, 4, 5, 6, 7, 15, 16, 17, 18, 19, 20 GetFeldtyp = cFeldtypZahl Case 8 GetFeldtyp = cFeldtypDatum 'dbDate = 8 Case 1 GetFeldtyp = cFeldtypJaNein 'dbBoolean = 1 Case Else GetFeldtyp = cFeldtypText End Select Else Select Case lngTyp Case 2, 3, 4, 5, 6, 14, 15, 16, 17, 18, 19, 20, 21, 131 GetFeldtyp = cFeldtypZahl Case 7, 133, 134, 135 GetFeldtyp = cFeldtypDatum 'adDate = 7 Case 11 GetFeldtyp = cFeldtypJaNein 'adBoolean = 11 Case Else GetFeldtyp = cFeldtypText End Select End If End Function
Listing 1: GetFeldtyp unterscheidet DAO- und ADODB-Typnummern
Warum ist die strikte Trennung zwischen DAO und ADODB notwendig? Der Grund ist eine handfeste Kollision der Typnummern: DAO verwendet für den Typ dbDouble (Gleitkommazahl) den Wert 7, ADODB verwendet denselben Wert 7 für adDate (Datum).
Eine einheitliche Behandlung beider Bibliotheken mit derselben Fallunterscheidung ist daher nicht möglich. Würde man in einem DAO-Recordset den Wert 7 als Datum interpretieren, würden alle Double-Felder fälschlicherweise als Datumsfelder behandelt – mit entsprechend falschen Vergleichsoperatoren und Datumsformatierung im SQL-Ausdruck als Folge.
Die Lösung ist das Flag m_bolDAO das beim Setzen von RecordsetForm automatisch über TypeName bestimmt wird. Im DAO-Zweig von GetFeldtyp wird der Wert 7 korrekt als Zahl (dbDouble) behandelt, während DAO-Datumsfelder den eindeutigen Wert 8 (dbDate) tragen. Im ADODB-Zweig wird 7 dagegen korrekt als Datum (adDate) behandelt, während ADODB-Boolean-Felder den Wert 11 (adBoolean) haben.
Das bolDAO-Flag ist damit nicht nur eine technische Notwendigkeit für den aktuellen Stand des Filterformulars – es ist gleichzeitig die Vorbereitung für die geplante Erweiterung auf ADODB-gebundene Formulare.
Wird das Filterformular künftig mit einem ADODB-Recordset aufgerufen, wird m_bolDAO automatisch auf False gesetzt und der ADODB-Zweig kommt zum Einsatz, ohne dass an GetFeldtyp etwas geändert werden muss.
Der Aufruf in LadeFelder ist entsprechend einfach gehalten:
For i = 0 To m_rst.Fields.Count - 1
m_strFeldnamen(i) = m_rst.Fields(i).Name
m_lngFeldtypen(i) = GetFeldtyp( _
m_rst.Fields(i).Type, m_bolDAO)
Next i
Die ermittelten Feldnamen und Feldtypen werden in den Modulvariablen m_strFeldnamen und m_lngFeldtypen gespeichert.
Auf diese Arrays greift das Formularmodul später zu, wenn der Benutzer ein Feld auswählt. Der Feldtyp steckt dann bereits im Array und muss nicht erneut aus dem Recordset ausgelesen werden.
Das ist effizienter und vermeidet Probleme, wenn das Recordset zwischenzeitlich geschlossen oder verändert wurde.
Bedingungen und Wertfelder dynamisch einblenden
Sobald der Benutzer ein Feld auswählt, reagiert FeldGewaehlt auf das AfterUpdate-Ereignis des jeweiligen cboFeld-Kombinationsfelds. Die Prozedur ermittelt den Feldtyp des gewählten Felds aus dem gespeicherten Array und befüllt cboBedingung mit den passenden Operatoren über GetBedingungRowsource (siehe Listing 2).
Public Function GetBedingungRowsource(lngFeldtyp As Long) As String Select Case lngFeldtyp Case cFeldtypText GetBedingungRowsource = _ "Gleich;Nicht gleich;Enthaelt;Enthaelt nicht;Beginnt mit;Endet mit;Ist leer;Ist nicht leer" Case cFeldtypZahl GetBedingungRowsource = _ "Gleich;Nicht gleich;Groesser als;Kleiner als;Ist zwischen;Ist leer;Ist nicht leer" Case cFeldtypDatum GetBedingungRowsource = _ "Gleich;Ist vor;Ist nach;Ist zwischen;Ist leer;Ist nicht leer" Case cFeldtypJaNein GetBedingungRowsource = "Wahr;Falsch" End Select End Function
Listing 2: GetBedingungRowsource liefert die passenden Operatoren je Feldtyp
Gleichzeitig mit dem Aktivieren des Bedingungsfelds blendet FeldGewaehlt alle Wertfelder der Zeile aus und setzt den Wert von cboBedingung auf Null. Ein Feldwechsel macht die bisherige Bedingung ungültig – ein Textoperator wie Enthält ergibt für ein Zahlenfeld keinen Sinn. Außerdem setzt FeldGewaehlt die Left-Positionen aller Wertfelder auf ihre Ursprungswerte zurück.
Das ist notwendig weil bei Datumsfeldern mit der Bedingung Ist zwischen die Positionen der Eingabefelder per Code verändert werden – dazu gleich mehr.
Schließlich blendet FeldGewaehlt automatisch eine neue leere Zeile unterhalb ein, sofern die aktuelle Zeile die letzte aktive in der Gruppe ist und noch nicht fünf Zeilen belegt sind.
Sobald der Benutzer eine Bedingung auswählt, entscheidet BedingungGewaehlt welche Eingabefelder eingeblendet werden. Bei Bedingungen wie Ist leer, Ist nicht leer, Wahr oder Falsch erscheint kein Eingabefeld – der Operator definiert bereits vollständig den Filterausdruck. Für Textfelder erscheint txtWert.
Für Zahlenfelder erscheint ebenfalls txtWert, bei Ist zwischen zusätzlich txtWert2 für den Bis-Wert. Bei Datumsfeldern mit Ist zwischen erscheinen txtDatum (Von) und txtDatumA (Bis) nebeneinander.
Bei allen anderen Datumsbedingungen erscheint das Kombinationsfeld cboDatumstyp mit den vordefinierten Zeiträumen – und je nach gewähltem Datumstyp zusätzlich txtDatum für ein konkretes Datum oder txtWert für die Anzahl Tage bei Letzte X Tage.
Da alle Steuerelemente einer Zeile an derselben X-Position liegen, werden beim Wechsel der Bedingung zuerst alle ausgeblendet und dann nur das jeweils benötigte eingeblendet. Bei Datumsfeldern mit Ist zwischen müssen txtDatum und txtDatumA nebeneinander sichtbar sein.
Da beide im Formular übereinander bei derselben X-Position liegen, setzt das Formular beim Einblenden die Left-Eigenschaft von txtDatum auf die Position des Wertfelds und platziert txtDatumA direkt dahinter.
Beim Wechsel zu einer anderen Bedingung oder beim Wechsel des Felds wird Left auf den gespeicherten Ursprungswert zurückgesetzt. Dieser Ursprungswert ist als Konstante im Erstellungsmodul hinterlegt und gilt für alle 25 Zeilen gleichermaßen.
Eingaben validieren
Das Filterformular validiert Tastatureingaben in den Wertfeldern in Echtzeit. Jedes der 25 txtWert-Steuerelemente sowie die txtDatum-Felder implementieren ein KeyPress-Ereignis das an die zentrale Funktion ValidiereEingabe in mdlFilter delegiert. Diese erhält den KeyAscii-Wert des gedrückten Zeichens sowie den Feldtyp der aktuellen Zeile und gibt den bereinigten KeyAscii-Wert zurück (siehe Listing 3).
Public Function ValidiereEingabe(KeyAscii As Integer, lngFeldtyp As Long) As Integer Select Case lngFeldtyp Case cFeldtypZahl If Not (Chr(KeyAscii) Like "[0-9]" _ Or KeyAscii = 44 _ Or KeyAscii = 46 _ Or KeyAscii = 45 _ Or KeyAscii < 32) Then KeyAscii = 0 End If Case cFeldtypDatum If Not (Chr(KeyAscii) Like "[0-9]" _ Or KeyAscii = 46 _ Or KeyAscii = 45 _ Or KeyAscii = 47 _ Or KeyAscii < 32) Then KeyAscii = 0 End If End Select ValidiereEingabe = KeyAscii End Function
Listing 3: ValidiereEingabe filtert ungültige Zeichen je nach Feldtyp heraus
Bei Zahlenfeldern lässt die Funktion Ziffern, Komma (ASCII 44), Punkt (46), Minuszeichen (45) und Steuerzeichen (ASCII-Wert kleiner als 32, also Rück-, Tab-, Entf-Taste und so weiter) durch – alle anderen Zeichen werden durch Rückgabe von 0 stillschweigend abgewiesen.
Das Setzen von KeyAscii auf 0 ist die VBA-Standardmethode um eine Tastatureingabe zu unterdrücken: Access ignoriert das Zeichen dann vollständig. Bei Datumsfeldern sind zusätzlich Punkt und Schrägstrich (47) erlaubt.
Für Textfelder und Ja/Nein-Felder ist keine Validierung nötig – Textfelder akzeptieren beliebige Zeichen, Ja/Nein-Felder haben gar kein Eingabefeld.
Darüber hinaus prüft cmdAnwenden_Click vor dem Aufbau des WHERE-Strings ob alle ausgefüllten Bedingungen auch einen Vergleichswert haben, sofern die gewählte Bedingung einen erfordert. Fehlt ein Wert, erscheint eine Hinweismeldung mit dem Namen des betroffenen Feldes und die Verarbeitung wird abgebrochen.
Nur Bedingungen wie Ist leer, Ist nicht leer, Wahr und Falsch sind von dieser Prüfung ausgenommen. Bei Datumsfeldern mit Ist zwischen prüft die Funktion PruefeDatumVonBis zusätzlich ob das Von-Datum kleiner oder gleich dem Bis-Datum ist.
Nur für Abonnenten
Ab hier wird’s wirklich spannend – der Rest ist exklusiv für Abonnenten.
Mit dem Abo von Access im Unternehmen bekommst du den kompletten Artikel – inklusive vollständigem Code, Beispieldatenbank und Schritt-für-Schritt-Erklärung.
So sparst du dir stundenlanges Herumprobieren, vermeidest teure Fehler in deiner Access-Anwendung und kannst Lösungen direkt in deinem Unternehmen einsetzen, statt nur darüber zu lesen.
Teste Access im Unternehmen jetzt 4 Wochen lang kostenlos: Voller Zugriff auf alle Artikel, Downloads und Beispieldatenbanken. Kein Risiko – wenn es für dich nicht passt, kündigst du einfach innerhalb der ersten vier Wochen.
Bereits Abonnent? Hier einloggen
Kostenlos & unverbindlich
Oder hast Du eine konkrete Frage zu Deiner eigenen Access-Anwendung?
Vielleicht stellt Deine Anwendung Dich vor eine Herausforderung, zu der Du bisher keine Lösung findest. Schlechte Performance, kein ausreichender Zugriffsschutz, Du bist unsicher über Dein Datenmodell oder Dein Code liefert unerklärliche Fehler?
In unserem kostenlosen Access-Audit schaut sich André Minhorst persönlich gemeinsam mit Dir Deine Lösung per Zoom an – und zeigt Dir, wo Datenmodell, VBA-Code, Ergonomie und Sicherheit Optimierungspotenzial bieten.
Jetzt kostenloses Access-Audit anfordern →