Optimierter „Suchen und Ersetzen“-Dialog: Funktionen

Im Beitrag „Besserer „Suchen und Ersetzen“-Dialog: Grundgerüst“ haben wir das Grundgerüst für einen Ersatz der eingebauten Suchen- und Ersetzen-Funktion von Access erstellt. Im vorliegenden Artikel wollen wir nun die Such- und Ersetzungsfunktionen mit Leben füllen. Dabei werden Sie nicht nur die begonnene Lösung vervollständigen, sondern auch noch Einiges über das VBA-gesteuerte Suchen und Ersetzen in Datenblättern lernen.

Die Lösung aus dem oben genannten Beitrag bildet bisher die vollständige Benutzeroberfläche des eingebauten Suchen und Ersetzen-Dialogs ab.

Allerdings haben wir die eigentlichen Suchen- und Ersetzen-Funktionen noch nicht abgebildet. Das holen wir in diesem Beitrag nach.

Dabei unterscheiden wir verschiedene Fälle, die wir in den folgenden Abschnitten im Detail beschreiben.

Einfaches Weitersuchen in einem Feld

Der erste und einfachste Fall ist die Suche im aktuell markierten Feld (siehe Bild 1). Dazu enthält die Eigenschaft Suchen in den Wert Aktuelles Feld. Vorher müssen wir uns allerdings noch um einige weitere Informationen kümmern.

Erster Fall: Suchen in einem einzelnen Feld

Bild 1: Erster Fall: Suchen in einem einzelnen Feld

Referenzieren des aktuellen Datenblatts

Wichtig ist für den Suchen und Ersetzen-Dialog, dass wir einen Verweis auf das Datenblatt erhalten, das wir untersuchen wollen. Dazu führen wir in der Ereignisprozedur, die durch das Öffnen des Formulars frmSuchenUndErsetzen ausgelöst wird, einige Aktionen aus (siehe Listing 1).

Private Sub Form_Open(Cancel As Integer)
     Dim strObjektname As String
     Set objAktuellesDatenblatt = AktuelleDatenblattansichtObjekt
     If objAktuellesDatenblatt Is Nothing Then
         MsgBox "Suchen und Ersetzen steht nur für die Datenblattansicht zur Verfügung.", vbOKOnly + vbExclamation, _
             "Keine Datenblattansicht aktiviert"
         Cancel = True
         Exit Sub
     Else
         Set rstDatenblatt = objAktuellesDatenblatt.Recordset
         Me.TimerInterval = 100
     End If
     If Not IsNull(Me.OpenArgs) Then
         If Me.OpenArgs = "Ersetzen" Then
             bolErsetzen = True
         End If
     End If
End Sub

Listing 1: Aktionen beim Öffnen des Formulars

Die Prozedur ruft zuerst die Funktion AktuelleDatenblattansichtObjekt auf. Diese sehen wir in Listing 2. Sie versucht, bei deaktivierter Fehlerbehandlung über Screen.ActiveDatasheet auf das aktive Datasheet-Objekt zuzugreifen. Das gelingt in vielen Fällen, aber zum Beispiel nicht, wenn sich das Datenblatt in einem Unterformular befindet. Dann liefert der Zugriff auf Screen.ActiveDatasheet einen Fehler zurück.

Public Function AktuelleDatenblattansichtObjekt() As Object
     Dim objAktuellesObjekt As Object
     On Error Resume Next
     Set objAktuellesObjekt = Screen.ActiveDatasheet
     On Error GoTo 0
     If objAktuellesObjekt Is Nothing Then
         On Error Resume Next
         Set objAktuellesObjekt = Screen.ActiveForm.ActiveControl
         If objAktuellesObjekt.CurrentView = acCurViewDatasheet Then
             Set AktuelleDatenblattansichtObjekt = objAktuellesObjekt.Form
             Exit Function
         End If
         On Error GoTo 0
     Else
         Set AktuelleDatenblattansichtObjekt = objAktuellesObjekt
     End If
End Function

Listing 2: Aktuelle Datenblattansicht holen

Deshalb deaktivieren wir hier die Fehlerbehandlung und prüfen anschließend, ob objAktuellesObjekt einen Wert enthält. Falls nicht, versuchen wir es auf einem anderen Weg. Wir versuchen, ebenfalls bei deaktivierter Fehlerbehandlung, mit Screen.ActiveForm.ActiveControl einen Verweis auf das Unterformular zu erhalten und in objAktuellesObjekt einzutragen. Wie gesagt: Datenblätter in Hauptformularen, Tabellen oder Abfragen liefert uns bereits Screen.ActiveDatasheet das aktuelle Datenblatt.

Die Funktion prüft dann, ob das aktive Steuerelement ein Datenblatt ist. Falls ja, liefert sie einen Verweis auf die Form-Eigenschaft von objAktuellesObjekt zurück.

Die Prozedur Form_Open schreibt das Ergebnis in die Variable objAktuellesDatenblatt, das wie folgt im Kopf des Klassenmoduls des Formulars deklariert wird:

Private objAktuellesDatenblatt As Object

Dort deklarieren wir direkt auch noch:

Private rstDatenblatt As DAO.Recordset

Wenn objAktuellesDatenblatt nun leer ist, gibt Form_Open eine Meldung aus, dass die Suchen und Ersetzen-Funktion nur für die Datenblattansicht zur Verfügung steht.

Danach wird die Prozedur mit Cancel = True verlassen, sodass das Formular gar nicht erst angezeigt wird.

Anderenfalls schreibt sie einen Verweis auf das Recordset in objAktuellesDatenblatt in die Variable rstDatenblatt und aktiviert einen Timer, der alle 100 Millisekunden feuern soll (es ginge vermutlich auch ein größeres Intervall). Diesen Timer schauen wir uns gleich an.

Schließlich prüft die Prozedur noch das Öffnungsargument. Enthält dieses den Wert Ersetzen, wird bolErsetzen auf True eingestellt. Dies sorgt später dafür, dass neben dem Suchen– auch das Ersetzen-Tab im Formular angezeigt wird.

Die Prozedur, die durch das Ereignis Bei Zeitgeber ausgelöst wird, prüft bei jedem Aufruf, ob das Datenblatt aus rstDatenblatt noch vorhanden ist. Dazu greifen wir einfach auf die RecordCount-Eigenschaft zu. Löst dies einen Fehler aus, sind das Recordset und somit wohl auch die Datenblattansicht, für die der Suchen und Ersetzen-Dialog geöffnet wurde, wohl nicht mehr vorhanden:

Private Sub Form_Timer()
     Dim lngRecordcount As Long
     On Error Resume Next
     lngRecordcount = rstDatenblatt.RecordCount
     If Not Err.Number = 0 Then
         On Error GoTo 0
         Me.TimerInterval = 0
         DoCmd.Close acForm, Me.Name
     End If
End Sub

Einfachen Suchvorgang ausführen

Damit kommen wir zum eigentlichen Suchvorgang. Wir schauen uns zuerst einen einfachen Suchvorgang mit den Parametern aus Bild 2 an.

Einfache Suche

Bild 2: Einfache Suche

Ein Klick auf die Schaltfläche Weitersuchen löst die Prozedur aus Listing 3 aus. Diese holt sich den Suchbegriff aus dem Kombinationsfeld cboSuchenNach in die Variable txtSuchbegriff. Wenn dieser eine Länge von 0 hat, bricht sie die Prozedur ab.

Private Sub cmdWeitersuchen_Click()
     Dim strFeld As String
     Dim strSuchbegriff As String
     Dim strKriterium As String
     Dim bolFound As Boolean
     Dim strMeldung As String
     strSuchbegriff = Nz(Me.cboSuchenNach.Column(1), "")
     If Len(strSuchbegriff) = 0 Then Exit Sub
     Select Case Me.cboSuchenIn
         Case "Aktuelles Feld"
             If KriteriumAktuellesFeld(strSuchbegriff, strFeld) = True Then
                 bolFound = False
                 Select Case Me.cboSuchen
                     Case "Alle"
                         Call SuchenDatensatz(bolFound, strKriterium, strFeld, strSuchbegriff)
                     Case "Abwärts"
                         rstDatenblatt.FindNext strKriterium
                         If rstDatenblatt.NoMatch Then
                             bolFound = False
                         Else
                             bolFound = True
                         End If
                     Case "Aufwärts"
                         rstDatenblatt.FindPrevious strKriterium
                         If rstDatenblatt.NoMatch Then
                             bolFound = False
                         Else
                             bolFound = True
                         End If
                 End Select    
                 If Not bolFound Then
                     lngAbsolutePositionStart = -1
                     MsgBox "Kein weiterer Eintrag gefunden."
                 Else
                     Select Case Me.cboVergleichen
                         Case "Teil des Feldinhalts", "Anfang des Feldinhalts"
                             Call SuchbegriffMarkieren(objAktuellesDatenblatt.ActiveControl, Me.cboSuchenNach.Column(1))
                     End Select
                 End If
             Else
                 MsgBox strMeldung, vbExclamation + vbOKOnly, "Suche nicht erfolgreich"
             End If
         Case "Aktuelles Dokument"
             MsgBox "Nicht implementiert."
     End Select
End Sub

Listing 3: Code hinter der Schaltfläche Weitersuchen

Anderenfalls erfolgt eine Unterscheidung nach dem Umfang der Suche, der mit dem Kombinationsfeld cboSuchenIn festgelegt wird. Im Fall von Aktuelles Feld beginnt der Teil, den wir hier beschreiben wollen.

Es folgt ein Aufruf der Funktion KriteriumAktuellesFeld, der als Ergebnis das Suchkriterium für die Suche liefern soll. Das ist recht aufwendig, da wir prüfen müssen, welchen Datentyp das Feld enthält, ob dieser zum eingegebenen Suchbegriff passt und so weiter – mehr dazu weiter unten. Wir gehen an dieser Stelle davon aus, dass wir zum Beispiel den Suchbegriff adi eingegeben haben und als Kriterium mit dem Parameter strKriterium den folgenden Ausdruck zurückerhalten:

Vorname LIKE ''*adi*''

Dann wird die Variable bolFound zunächst auf False eingestellt. Nun prüfen wir den Inhalt von cboSuchen und steuern abhängig davon einen der Case-Zweige für Alle, Abwärts oder Aufwärts an.

Suchen nach allen Vorkommen im aktuellen Feld

Wenn der Benutzer Alle gewählt hat, haben wir die umfangreichste Ausgabe vor uns. Während wie bei Abwärts oder Aufwärts einfach vom aktuellen Datensatz aus nach oben oder unten suchen können, müssen wir in diesem Fall vom aktuellen Datensatz aus nach unten suchen, dann oben neu beginnen und vor dem aktuellen Datensatz stoppen. Da dies ein wenig aufwendiger ist, haben wir diesen Teil in eine eigene Prozedur namens SuchenDatensatz ausgelagert (siehe Listing 4).

Private Sub SuchenDatensatz(bolGefunden As Boolean, strKriterium As String)
     If lngAbsolutePositionStart = -1 Then
         lngAbsolutePositionStart = rstDatenblatt.AbsolutePosition
         bolVonVornGestartet = False
     Else
         If Not lngLetzteFundstelle = rstDatenblatt.AbsolutePosition Then
             lngAbsolutePositionStart = rstDatenblatt.AbsolutePosition
             bolVonVornGestartet = False
         End If
     End If
     rstDatenblatt.FindNext strKriterium
     If rstDatenblatt.NoMatch = False Then
         If bolVonVornGestartet = True Then
             If rstDatenblatt.AbsolutePosition > lngAbsolutePositionStart Then
                 rstDatenblatt.AbsolutePosition = lngLetzteFundstelle
                 bolGefunden = False
                 Exit Sub
             End If
         End If
         lngLetzteFundstelle = rstDatenblatt.AbsolutePosition
         bolGefunden = True
         Exit Sub
     Else
         If bolVonVornGestartet = False Then
             rstDatenblatt.MoveFirst
             bolVonVornGestartet = True
             rstDatenblatt.FindNext strKriterium
             If rstDatenblatt.NoMatch = False Then
                 If rstDatenblatt.AbsolutePosition <= lngAbsolutePositionStart Then
                     lngLetzteFundstelle = rstDatenblatt.AbsolutePosition
                     bolGefunden = True
                     Exit Sub
                 Else
                     rstDatenblatt.AbsolutePosition = lngLetzteFundstelle
                     bolGefunden = False
                     Exit Sub
                 End If
             End If
         Else
             bolGefunden = False
             Exit Sub
         End If
     End If
End Sub

Listing 4: Code zum Suchen nach einem Datensatz

Dieser übergeben wir verschiedene Parameter, die zum größten Teil auch als Rückgabeparameter dienen:

  • bolGefunden gibt an, ob ein Datensatz gefunden wurde.
  • strKriterium nimmt das Kriterium entgegen.

Die beiden folgenden Variablen müssen wir noch im Modulkopf deklarieren, da diese von mehreren Prozeduren genutzt werden:

Private lngAbsolutePositionStart As Long
Private varLetzteFundposition As Variant

Beim Aufrufen des Formulars frmSuchenUndErsetzen wird der Wert von lngAbsolutePositionStart auf -1 eingestellt. Deshalb ist die erste Bedingung beim ersten Suchlauf nach dem Öffnen immer wahr und wir stellen nun lngAbsolutePositionStart auf die aktuelle Position des Datensatzzeigers aus rstDatenblatt.AbsolutePosition ein.

Außerdem legen wir hier den Wert der Variablen bolVonVornGestartet auf False fest. Diese Variable gibt an, ob wir den Suchlauf bereits von vorn begonnen haben. Das ist wichtig, wenn wir mittendrin mit der Suche beginnen, damit auch die vor dem Startpunkt liegenden Datensätze noch durchsucht werden können.

Wenn der Benutzer nach dem ersten Suchlauf noch weitere Suchvorgänge startet, hat lngAbsolutePositionStart bereits einen anderen Wert als -1. Also wird der zweite Teil der If…Then-Bedingung angesteuert.

Hier prüfen wir, ob sich der Datensatzzeiger aktuell auf der letzten Fundstelle befindet. Das sollte immer der Fall sein, wenn der Benutzer mehrere Suchvorgänge hintereinander aufruft. Wenn sich der Datensatzzeiger woanders befindet, gehen wir davon aus, dass der Benutzer diesen manuell an eine andere Stelle verschoben hat. In diesem Fall stellen wir lngAbsolutePositionStart wieder auf die aktuelle Position ein und bolVonVornGestartet auf False, da selbst ein Suchvorgang, der bis zum Ende lief und von vorn beginnt, nun zurückgesetzt werden soll.

Damit können wir endlich suchen und nutzen dafür die FindNext-Methode mit dem Kriterium aus strKriterium. Warum FindNext und nicht FindFirst? Weil wir genau von der Stelle mit der Suche beginnen wollen, auf der sich der Datensatzzeiger aktuell befindet.

Die folgende If…Then-Bedingung prüft durch den Vergleich der Eigenschaft NoMatch mit dem Wert False, ob es einen Suchtreffer gab. In diesem Fall prüfen wir, ob bolVonVornGestartet den Wert True hat, also ob wir die Suche bereits wieder ganz oben fortgesetzt haben. Ist das der Fall, kann es sein, dass wir mit der Suche ein zweites Mal auf dem gleichen Datensatz landen. Um das herauszufinden, ermitteln wir, ob die aktuelle Position des Datensatzzeigers aus rstDatenblatt.AbsolutePosition größer ist als die Startposition aus lngAbsolutePositionStart. Ist diese Bedingung wahr, sind wir mehr als einmal durch die Datensätze navigiert. Dann verschieben wir den Datensatzzeiger wieder auf die Position der vorherigen Fundstelle, geben für bolGefunden den Wert False zurück und verlassen die Prozedur.

Sind die letzten beiden Bedingungen nicht erfüllt, haben wir einen Suchtreffer. Wir speichern die Position des Datensatzzeigers für diesen Datensatz in der Variablen lngLetzteFundstelle, geben den Wert True für bolGefunden zurück und verlassen die Prozedur.

Nun kommt der Fall, wo wir von der Startposition aus bereits alle Vorkommen des gesuchten Wertes gefunden haben. In diesem Fall prüfen wir, ob wir die Suche bereits von vorn begonnen haben. Falls nicht, ist es nun an der Zeit. Wir verschieben den Datensatzzeiger mit MoveFirst an die erste Position und stellen bolVonVornGestartet auf True ein. Danach rufen wir erneut FindNext mit unserem Kriterium auf.

Ist NoMatch danach False, haben wir einen erneuten Treffer. Dann prüfen wir, ob wir gegebenenfalls bereits mehr als eine Runde durch die Datensätze gedreht haben.

Dazu prüfen wir, ob der Wert von rstDatenblatt.AbsolutePosition kleiner ist als lngAbsolutePositionStart. Ist das der Fall, sind wir noch im ersten Durchlauf und haben eine Fundstelle.

Wenn nicht, stellen wir AbsolutePosition auf die Position des letzten Suchergebnisses ein, verschieben den Datensatzzeiger also zurück zum letzten regulären Treffer. Außerdem geben wir für bolGefunden den Wert False zurück.

Abwärts-Suche und Aufwärts-Suche

Die einfache Abwärts- und Aufwärts-Suche ist wesentlich einfacher zu programmieren, weil wir nicht prüfen müssen, ob wir am Ende angekommen sind und gegebenenfalls noch Suchergebnisse in dem bisher nicht berücksichtigten Bereich vorhanden sind. Wir gehen also weiter in der Prozedur cmdWeitersuchen_Click.

Hier folgen nun die beiden Case-Zweige Abwärts und Aufwärts. Im Abwärts-Zweig rufen wir die FindNext-Methode auf und übergeben strKriterium. Dies liefert entweder einen Treffer (NoMatch = False) oder nicht.

Abhängig davon stellen wir bolFound auf True oder False ein. Im Aufwärts-Zweig rufen wir FindPrevious auf, um nach oben zu suchen.

In beiden Fällen wird der Datensatzzeiger jeweils auf der Fundstelle platziert.

Danach untersuchen wir in der Prozedur für alle drei Suchfälle den Wert der Variablen bolFound. Hat es den Wert False, stellen wir lngAbsolutePositionStart auf -1 ein, damit wir uns bei der nächsten Suche über alle Daten wieder die korrekte Startposition merken können. Außerdem erscheint eine Meldung, dass kein weiterer Eintrag gefunden werden konnte.

Ende des frei verfügbaren Teil. Wenn Du mehr lesen möchtest, hole Dir ...

den kompletten Artikel im PDF-Format mit Beispieldatenbank

diesen und alle anderen Artikel mit dem Jahresabo

Schreibe einen Kommentar