Für den einen oder anderen Anwendungsfall benötigen Sie einen Datei-öffnen-Dialog, um eine Datenbank auszuwählen. Dieser Dialog hat den Nachteil, dass er selten direkt die gewünschte Datenbank geschweige denn das Verzeichnis anzeigt. Wie wäre es also mit einem speziellen Dialog, der nur die Verzeichnisse einliest, die überhaupt Datenbankdateien enthalten, und diese in einem TreeView-Steuerelement zur Auswahl bereithält
Ziel
Das Ziel dieses Beitrags ist es, einen Dialog zu erstellen, der alle Datenbankdateien oder auch nur die in einem bestimmten Unterordner befindlichen anzeigt und zur Auswahl anbietet. Damit sollen die üblichen Dialoge ersetzt werden, die erstens immer alle Verzeichnisse und Dateien anzeigen und zweitens nie das Verzeichnis liefern, das man gerade benötigt. Für die Erstellung eines solchen Dialogs ist eine Menge Vorarbeit nötig – zum Beispiel müssen Sie eine Prozedur programmieren, die alle Verzeichnisse ab dem gewünschten Unterordner erfasst und nach Datenbankdateien durchsucht. Diese werden über die Dateiendung erkannt (.mdb, .accdb …). Der Dialog soll nur diejenigen Verzeichnisse anzeigen, die direkt oder in einem Unterverzeichnis mindestens eine Datenbankdatei enthalten. Bild 1 zeigt, wie der Dialog ungefähr aussehen soll. Sie erkennen hier ein Textfeld, in das Sie das beim Einlesen zu verwendende Startverzeichnis eintragen. Dieses lässt sich mit einem Mausklick auf die Schaltfläche rechts daneben auch per Verzeichnis auswählen-Dialog einlesen. Ein Klick auf die Schaltfläche Einlesen startet den Einlesevorgang, der je nach Umfang der einzulesenden Daten eine Weile dauern kann (auf meiner Festplatte, die zugegebenermaßen sehr viele Daten enthält, etwa acht Minuten – planen Sie also eine Kaffeepause für diesen Vorgang ein). Wenn Sie einmal ermittelt haben, wo sich die Datenbankdateien befinden, können Sie für den nächsten Einlesevorgang gegebenenfalls einen entsprechenden Unterordner als Startverzeichnis angeben, um Zeit zu sparen. Zu Beginn sollten Sie jedoch einmal die komplette Festplatte einlesen – ich fand es jedenfalls sehr interessant, an welchen Stellen überall Datenbankdateien gespeichert sind.
Bild 1: Dialog zur Anzeige von Datenbankdateien
Die erfassten Daten werden in einer Tabelle gespeichert, die wir weiter unten vorstellen. Die Daten sollen dann in einem TreeView-Steuerelement angezeigt werden. Dazu liest die Anwendung die Daten der Tabelle aus und trägt sie in das TreeView-Steuerelement ein. Verzeichnisse und Datenbankdateien werden mit entsprechenden Symbolen gekennzeichnet. Vorerst soll das Formular lediglich beim Schließen den Pfad der aktuell ausgewählten Datenbankdatei zurückliefern.
Überlegungen
Schauen wir uns die Ausgangssituation an. Das Dateisystem enthält beliebig tief verschachtelte Verzeichnisstrukturen, von denen nicht alle Access-Dateien speichern. Das zu füllende TreeView-Steuerelement soll lediglich die Verzeichnisse anzeigen, in denen sich auch Access-Dateien befinden. Dazu müssen wir alle Verzeichnisse durchlaufen und diejenigen mit Access-Dateien ermitteln. Die Pfade der Verzeichnisse mit Access-Dateien sollen dann vom Laufwerksbuchstaben angefangen über die einzelnen Verzeichnisse im TreeView-Steuerelement abgebildet werden.
Wie erledigen wir dies am einfachsten, wie werden die Daten über die Verzeichnisse und Access-Datenbanken gespeichert, wie füllen wir das TreeView-Steuerelement und wie oft soll diese Prozedur wiederholt werden
Verzeichnisse und Dateien per FileSystemObject durchlaufen
Die Werkzeuge zum Einlesen der Verzeichnisse und Dateien liefert das FileSystemObject. Es bietet Möglichkeiten, die Verzeichnisse rekursiv zu durchlaufen und die Dateien auf ihre Dateiendung hin zu untersuchen.
Informationen speichern
Die Tabelle zum Aufnehmen der Informationen muss folgende Felder enthalten und sieht, mit einigen Daten gefüllt, wie in Bild 2 aus:
Bild 2: Tabelle zum Speichern der Dateien und Verzeichnisse
- DateiVerzeichnisID: Primärschlüsselfeld der Tabelle, allerdings nicht als Autowert ausgelegt – mehr dazu später
- DateiVerzeichnisname: Name der Datei oder des Verzeichnisses
- IstVerzeichnis: Ja/Nein-Feld, das angibt, ob es sich bei dem Eintrag um ein Verzeichnis handelt
- VerzeichnisID: Wert des Feldes DateiVerzeichnisID für das übergeordnete Verzeichnis
- Pfad: Speichert den kompletten Pfad des aktuellen Datensatzes.
- Expanded: Für die Darstellung im TreeView fügen Sie der Tabelle außerdem noch ein Feld zum Speichern des Expanded-Zustands hinzu.
- Enthaltene Dateien: Speichert die Anzahl der im aktuellen Verzeichnis samt Unterverzeichnis enthaltenen Datenbankdateien.
Dateien einlesen und speichern
Warum verwenden wir in der Tabelle keinen Autowert für das Primärschlüsselfeld Normalerweise würden wir so vorgehen: Wir lesen das Root-Element ein, also beispielsweise das Verzeichnis c:\ und speichern es in der Tabelle. Dann arbeiten wir uns durch alle untergeordneten Verzeichnisse. Wenn c:\ nun mit dem Wert 1 im Primärschlüsselfeld gespeichert wurde, würden wir für die untergeordneten Verzeichnisse beziehungsweise Einträge den Wert 1 im Feld VerzeichnisID eintragen.
Dazu müssen wir aber jedes Verzeichnis in der Tabelle anlegen – auch wenn wir feststellen, dass sich weder in diesem Verzeichnis noch in einem der Unterverzeichnisse eine Access-Datei befindet. Und genau dies wollen wir ja verhindern, um nur die notwendigsten Verzeichnisse und Dateien in der Tabelle zu speichern.
Zur Veranschaulichung liefert Bild 3 einige Verzeichnisse und Dateien. Die Beschriftungen rechts neben den Elementen enthalten die Werte für das Primärschlüsselfeld DateiVerzeichnisID (in der Abbildung id) und für das Fremdschlüsselfeld VerzeichnisID (in der Abbildung fid).
Bild 3: Indizierung der Verzeichnisse und Dateien
Das Speichern des ersten Elements c:\ ist kein Problem – dieses soll auf jeden Fall gespeichert werden. Es erhält als Primärschlüsselwert etwa den Wert 1. Das Fremdschlüsselfeld bleibt leer, weil es ja keine übergeordneten Verzeichnisse gibt.
Das Unterverzeichnis Verzeichnis 1 würde mit dem Primärschlüsselwert 2 gespeichert. Das Feld VerzeichnisID erhält den Primärschlüsselwert des übergeordneten Verzeichnisses, also 1. Das Unterverzeichnis Verzeichnis 1-1 erhält den Primärschlüsselwert 3 und wird über den Wert 2 für das Feld VerzeichnisID dem Verzeichnis mit dem Primärschlüsselwert 2 untergeordnet.
Nun folgt die erste Datei, die nach dem gängigen Schema den Wert 4 im Feld DateiVerzeichnisID und den Wert 3 im Fremdschlüsselfeld VerzeichnisID erhält.
Wenn alle Verzeichnisse am Ende mindestens eine Datenbankdatei im letzten Verzeichnis enthielten, könnte man die Dateien auf diese Weise anlegen und durchlaufen. Es müssen jeweils die übergeordneten Verzeichnisse zuerst angelegt werden, damit den darunter befindlichen Elementen die Primärschlüsselwerte als Fremdschlüsselwert zugeordnet werden können.
Nun gibt es aber Verzeichnisse wie Verzeichnis1-2, das leer ist oder nur Dateien enthält, die nicht in die Tabelle aufgenommen werden sollen. Wir würden es mit den Werten 5 im Feld DateiVerzeichnisID und 2 im Feld VerzeichnisID speichern. Erst danach würden wir die in diesem Verzeichnis enthaltenen Elemente durchlaufen und schließlich feststellen, dass dort keine Datenbankdatei zu finden ist.
In diesem Fall gibt es kein weiteres untergeordnetes Verzeichnis, sodass die Untersuchung schnell abgeschlossen ist. Was aber nun mit dem Datensatz für das Verzeichnis mit dem Namen Verzeichnis 1-2 Der Datensatz kann wieder gelöscht werden, da er ja nicht mehr benötigt wird. Dummerweise gibt es gelegentlich sehr tiefe Verzeichnisstrukturen, in denen sich keine Datenbankdateien finden lassen. Dort würde man erst einige Datensätze für die Verzeichnisstruktur anlegen und müsste diese anschließend wieder löschen.
Dies ist der Grund, warum wir keinen Autowert als Primärschlüsselwert verwenden: Damit die Datensätze für Verzeichnisse ohne untergeordnete Datenbanken gar nicht erst angelegt werden, soll die entsprechende Funktion sich erst rekursiv bis zum Ende der Verzeichnisstruktur bewegen und prüfen, ob dort Datenbankdateien vorliegen. Ist dies der Fall, legt die Funktion zunächst das Element für die Datenbankdatei an und dann die darüber liegenden Verzeichnisse. Sprich: Im Falle von Datei 1 würde erst ein Datensatz für die Datei, dann jeweils einer für Verzeichnis 1-1 und für Verzeichnis 1 angelegt werden.
Dummerweise wissen wir beim Anlegen der untergeordneten Elemente noch nicht, welchen Primärschlüsselwert dessen Verzeichnis in der Tabelle aufweist. Wir müssen beim Navigieren in Richtung untergeordneter Verzeichnisse einen Primärschlüsselwert vorhalten und diesen dann von unten nach oben als Fremdschlüsselwert des jeweils untergeordneten Elements einfügen.
Wenn dabei wie im Fall von Verzeichnis 1-2 zunächst ein Primärschlüsselwert wie etwa 5 festgelegt wird, sich aber herausstellt, dass dieses Verzeichnis keine Access-Datenbanken enthält und somit nicht gespeichert werden soll, kann der Primärschlüsselwert 5 für das nachfolgend untersuchte Verzeichnis reserviert werden.
Rekursive Einlesefunktion
Wie lässt sich dies per VBA umsetzen Ganz einfach – mit einer rekursiv definierten Funktion. Diese besteht im vorliegenden Fall aus einer initialisierenden Funktion, die einen oder mehrere Datensätze für das angegebene Root-Verzeichnis schreibt und dann die eigentliche rekursive Funktion aufruft.
Schauen wir uns zunächst die Parameter der Startprozedur DateienEinlesen an (s. Listing 1):
Listing 1: Prozedur zum Initiieren des rekursiven Dateieinlese-Vorgangs
Public Sub DateienEinlesen(Optional strPfad As String, Optional intTiefeMax As Integer) Dim objFSO As Scripting.FileSystemObject Dim db As DAO.Database Dim strVerzeichnisse() As String Dim i As Long Set objFSO = New Scripting.FileSystemObject Set db = CurrentDb If Len(strPfad) = 0 Then strPfad = "c:\" End If strPfad = objFSO.GetFolder(strPfad) If Right(strPfad, 1) = "\" Then strPfad = Left(strPfad, Len(strPfad) - 1) End If db.Execute "SELECT * INTO tblDateienVerzeichnisse_" & Format(Now, "yyyymmdd_hhnnss") _ & " FROM tblDateienVerzeichnisse", dbFailOnError db.Execute "DELETE FROM tblDateienVerzeichnisse", dbFailOnError If objFSO.FolderExists(strPfad) Then strVerzeichnisse = Split(strPfad, "\") For i = LBound(strVerzeichnisse) To UBound(strVerzeichnisse) If i = 0 Then db.Execute "INSERT INTO tblDateienVerzeichnisse(DateiVerzeichnisID, DateiVerzeichnisName, IstVerzeichnis) VALUES(" & i + 1 & ", ''" _ & strVerzeichnisse(i) & "'', -1)", dbFailOnError Else db.Execute "INSERT INTO tblDateienVerzeichnisse(DateiVerzeichnisID, DateiVerzeichnisName, IstVerzeichnis, VerzeichnisID) VALUES(" & i + 1 & ", ''" _ & strVerzeichnisse(i) & "'', -1, " & i & ")", dbFailOnError End If Next i End If lngAktuellID = i + 1 DateienEinlesenRek db, objFSO, objFSO.GetFolder(strPfad & "\"), i, 0, intTiefeMax, 0 End Sub
- strPfad: Enthält die Angabe des Startverzeichnisses für den Einlesevorgang. Wenn Sie den Parameter weglassen, liest die Prozedur das komplette Verzeichnis c:\ ein.
- intTiefeMax: Dies ist eher ein Parameter, der für Entwicklungstests verwendet wurde. Damit können Sie angeben, wie tief die Funktion nach Access-Dateien suchen soll. Wenn Sie den Parameter weglassen, durchsucht die Funktion alle Verzeichnisebenen.
Die Prozedur deklariert zunächst ein Objekt auf Basis von Scripting.FileSystemObject. Dieses liefert die Methoden und Eigenschaften, um auf das Dateisystem zuzugreifen. Außerdem benötigen wir eine Database-Variable, um per Execute-Methode SQL-Aktionsabfragen ausführen zu können.
Die Prozedur prüft zunächst, ob der Parameter strPfad übergeben wurde und ersetzt diesen gegebenenfalls durch das Verzeichnis c:\. Der folgende Aufruf der GetFolder-Funktion des FileSystemObjects entledigt den Inhalt von strPfad eventuell anhängender Backslash-Zeichen, aus c:\Daten\ wird also c:\Daten.
Die folgende Anweisung kopiert den kompletten Inhalt der Tabelle tblDateienVerzeichnisse in eine neue Tabelle, die den gleichen Namen erhält, jedoch um Datum und Uhrzeit ergänzt. Dieses Feature war während der Entwicklung hilfreich, um umfangreiche erfolgreiche Einlesevorgänge zu sichern, bevor der Code weiter verfeinert wurde.
Danach leert die Tabelle tblDateienVerzeichnisse, da diese ja nun neu gefüllt werden soll. Ist das in strPfad angegebene Verzeichnis vorhanden, beginnt die eigentliche Bearbeitung.
Wenn der Benutzer einen Pfad angibt, der aus mehr als nur dem Laufwerksbuchstaben besteht, soll die Prozedur nämlich für jedes enthaltene Verzeichnis gleich einen Eintrag zur Tabelle tblDateienVerzeichnisse hinzufügen – und zwar verschachtelt. Dazu überträgt die Prozedur zunächst mit der Split-Funktion alle Verzeichnisse des Pfades in ein Array namens strVerzeichnisse().
Die folgende For…Next-Schleife durchläuft dazu alle Elemente dieses Arrays und legt jeweils einen neuen Eintrag in der Tabelle tblDateienVerzeichnisse an. Dabei gibt es wiederum zwei Möglichkeiten: Hat die Laufvariable i den Wert 0, wird das erste Element mit einer SQL-Anweisung wie folgt in die Tabelle tblDateienVerzeichnisse eingetragen:
INSERT INTO tblDateienVerzeichnisse(DateiVerzeichnisID, DateiVerzeichnisName, IstVerzeichnis) VALUES(1, ''C:'', -1)
Das heißt, dass das Feld VerzeichnisID zum Eintragen der Referenz auf das übergeordnete Verzeichnis leer bleibt. Für die übrigen Einträge gibt die Prozedur jeweils das übergeordnete Verzeichnis als weiteren Parameter der INSERT INTO-Anweisung an:
INSERT INTO tblDateienVerzeichnisse(DateiVerzeichnisID, DateiVerzeichnisName, IstVerzeichnis, VerzeichnisID) VALUES(2, ''Daten'', -1, 1)
In diesem Fall referenziert das hier angelegte zweite Element mit dem Primärschlüsselwert 2 das erste Element mit dem Primärschlüsselwert 1 über das Feld VerzeichnisID mit dem Wert 1. Wenn Sie mit dem Parameter strPfad also beispielsweise den Pfad c:\Daten\Fachartikel\AccessImUnternehmen\2013\03\ übergeben, sieht der Inhalt der Tabelle tblDateienVerzeichnisse nach dem Durchlaufen der For…Next-Schleife wie in Bild 4 aus.
Bild 4: Datensätze für den als Parameter übergebenen Pfad
Damit ist der erste Teil erledigt – alle Verzeichnisse des übergebenen Pfades wurden in der Tabelle angelegt.
Nun geht es mit der eigentlichen rekursiven Funktion weiter, die alle unterhalb dieses Pfades befindlichen Verzeichnisse und Dateien untersucht. Vorher stellt die Prozedur jedoch noch die wie folgt global deklarierte Variable lngAktuellID auf den Wert der Laufvariablen i ein:
Dim lngAktuellID As Long
lngAktuellID speichert den jeweils zu verwendenden Primärschlüsselwert beim Anlegen eines neuen Datensatzes.
Rekursive Funktion
Die rekursiv definierte Funktion DateienEinlesenRek erwartet die folgenden Parameter:
- db: Verweis auf das Database-Objekt für die aktuelle Datenbank
- objFSO: Verweis auf das FileSystemObject
- objVerzeichnis: Verzeichnisse, deren enthaltene Unterverzeichnisse und Dateien in diesem Aufruf untersucht werden sollen
- lngVerzeichnisID: Wert des Feldes DateiVerzeichnisID des übergeordneten Verzeichnisses in der Tabelle tblDateienVerzeichnisse
- intTiefe: Aktuelle Verzeichnistiefe
- intTiefeMax: Maximal zu untersuchende Verzeichnistiefe
- lngAnzahl: Rückgabeparameter mit der Anzahl der im zu untersuchenden Unterverzeichnis enthaltenen Datenbankdateien
Die Funktion DateienEinlesenRek stellt zunächst die als Parameter übergebene Variable lngAnzahl auf den Wert 0 ein (s. Listing 2). Diese Variable soll, sofern das aktuelle Verzeichnis Datenbankdateien enthält, entsprechend angepasst werden. Außerdem werden noch die in den unterhalb des aktuellen Verzeichnisses befindlichen Datenbankdateien hinzugezählt.
Listing 2: Rekursiver Teil der Routinen zum Einlesen der Verzeichnisstruktur und der enthaltenen Dateien
Public Function DateienEinlesenRek(db As DAO.Database, objFSO As Scripting.FileSystemObject, _ objVerzeichnis As Scripting.Folder, lngVerzeichnisID As Long, intTiefe As Integer, _ intTiefeMax As Integer, lngAnzahl As Long) As Boolean Dim objUnterverzeichnis As Scripting.Folder Dim objDatei As Scripting.File Dim lngDateiVerzeichnisID As Long Dim bolDiesesVerzeichnisSpeichern As Boolean Dim bolVaterverzeichnisSpeichern As Boolean Dim lngAnzahlRek As Long lngAnzahl = 0 If (intTiefeMax > 0) And (intTiefe > intTiefeMax) Then Exit Function End If On Error Resume Next For Each objDatei In objVerzeichnis.Files Select Case objFSO.GetExtensionName(objDatei.Name) Case "mdb", "accdb", "mda", "accda", "mde", "accde" lngAnzahl = lngAnzahl + 1 db.Execute "INSERT INTO tblDateienVerzeichnisse(DateiVerzeichnisID, " _ & "DateiVerzeichnisName, IstVerzeichnis, VerzeichnisID, Pfad) VALUES(" _ & lngAktuellID & ", ''" & objDatei.Name & "'', 0, " & lngVerzeichnisID & ", ''" _ & objDatei.Path & "'')", dbFailOnError lngAktuellID = lngAktuellID + 1 DateienEinlesenRek = True End Select Next objDatei bolVaterverzeichnisSpeichern = False For Each objUnterverzeichnis In objVerzeichnis.SubFolders lngDateiVerzeichnisID = lngAktuellID lngAktuellID = lngAktuellID + 1 bolDiesesVerzeichnisSpeichern = DateienEinlesenRek(db, objFSO, objUnterverzeichnis, _ lngDateiVerzeichnisID, intTiefe + 1, intTiefeMax, lngAnzahlRek) lngAnzahl = lngAnzahl + lngAnzahlRek bolVaterverzeichnisSpeichern = bolVaterverzeichnisSpeichern Or bolDiesesVerzeichnisSpeichern If bolDiesesVerzeichnisSpeichern = True Then db.Execute "INSERT INTO tblDateienVerzeichnisse(DateiVerzeichnisID, " _ & "DateiVerzeichnisName, IstVerzeichnis, VerzeichnisID, EnthalteneDateien) VALUES(" _ & lngDateiVerzeichnisID & ", ''" & objUnterverzeichnis.Name & "'', -1, " _ & lngVerzeichnisID & ", " & lngAnzahlRek & ")", dbFailOnError End If Next objUnterverzeichnis DateienEinlesenRek = DateienEinlesenRek Or bolVaterverzeichnisSpeichern End Function