Die allermeisten Prozeduren und Funktionen des VBA-Projekts Ihrer Datenbank werden Sie in ganz normalen Modulen unterbringen. Dabei wird leicht übersehen, dass es sich etwa bei Formularen um eine andere Art Module handelt, nämlich um Klassenmodule. Diese können Sie aber auch selbst anlegen. Erfahren Sie hier, wie Sie dabei vorgehen. Wir leiten das Thema zunächst jedoch mit den benutzerdefinierten Typen ein, welche Klassenmodule verständlicher machen.
Beispieldatenbank
Die Beispiele dieses Artikels finden Sie in der Datenbank 1601_Klassenmodule.accdb
Benutzerdefinierte Typen
Ein benutzerdefinierter Typ ist nichts weiter, als eine zusammengefasste Struktur von Variablen.
Interessanterweise findet man Beispiele dafür im VBA-Objektkatalog weder in der Access-Bibliothek, noch unter den VBA- oder Office-Bibliotheken. Also stellen wir gleich eine eigene Kreation vor:
Public Type TAdresse ID As Long Nachname As String Vorname As String Strasse As String PLZ As String Ort As String End Type
Der reservierte Begriff Type definiert, dass unter dem folgenden Namen TAdresse eine Variablenstruktur folgt. Solche Typen können im Gültigkeitsbereich auch mit Private gekennzeichnet sein, was sie nur für das Modul zugreifbar macht, in dem sich die Deklaration befindet. Public bedeutet hier, dass nun das gesamte VBA-Projekt den Typen kennt und auch aus anderen Modulen heraus verwendet werden kann.
Die einzelnen Variablen des Typs werden in den folgenden Zeilen durch Namen und Variablentypen definiert. Jeglicher Datentyp ist hier möglich – etwa auch As Object.
Wozu ist das gut Kann man die Elemente der Struktur nicht auch als einzelne Variablen deklarieren Es gibt sicher viele Fälle, für die die Antwort hier Ja lautet. Doch schauen wir uns an, welche Vorteile der benutzerdefinierte Typ bietet. Denn er verhält sich genau so, wie jeder andere Datentyp. Also kann er einer Variablen zugeordnet werden:
Dim A As TAdresse
Der Zugriff auf die Elemente des Typs geschieht über Punkt und Namen:
A.Nachname = "Minhorst"
A.Vorname = "André"
A.Ort = "Duisburg"
Debug.Print A.Nachname
Der Code lässt sich noch über die With-Anweisung vereinfachen:
With A .Nachname = "Minhorst" .Vorname = "André" .Ort = "Duisburg" Debug.Print .Nachname End With
Das wäre schon einmal Vorteil Nummer eins. Nach Eingabe von A und Punkt stellt das IntelliSense von VBA gleich alle Elemente des Typs in der Auswahlliste vor. Und das übrigens auch innerhalb des With-Blocks, wo allein die Punkt-Eingabe für das Auslösen von IntelliSense ausreicht.
Besonders nützlich sind benutzerdefinierte Typen jedoch dann, wenn Sie in Arrays Verwendung finden:
Dim Adressen() As TAdresse ReDim Adressen(99) Adressen(0).Nachname = "Minhorst" Adressen(0).Vorname = "André" Adressen(1).Nachname = "Trowitzsch" Adressen(1).Vorname = "Sascha" ...
Das Array Adressen ist vom Typ TAdresse. Es wird im ersten Schritt auf Hundert Elemente dimensioniert. Danach folgt die Zuweisung der Werte einerseits über die Indizes des Arrays, andererseits über den Namen der Elemente. Wollte man das über separate Variablen erreichen, so benötigte man für jedes Element des Typs ein eigenes gesondertes Array. Das machte die Angelegenheit deutlich unüberschaubarer, als es das Handling mit einem Array des benutzerdefinierten Typs erlaubt.
Die Sache lässt sich noch weiterspinnen. Durch den Fakt, dass ein Typelement Variablen beliebigen Datentyps aufweisen darf, kommt für dieses selbst ein benutzerdefinierter Typ infrage:
Public Type TAdressen Items() As TAdresse End Type
Zwar enthält der Typ nur ein Element, welches jedoch als Array des benutzerdefinierten TAdresse ausgelegt ist. Damit ist nur eine Variable vom Typ TAdressen imstande mehrere Adressen zu speichern:
Dim A As TAdressen ReDim A.Items(99) With A .Items(0).Nachname = "Minhorst" .Items(0).Vorname = "André" .Items(1).Nachname = "Trowitzsch" .Items(1).Vorname = "Sascha" ... End With Debug.Print A.Items(1).Vorname
Auch hier führt der benutzerdefinierte Typ zu strukturierterer Programmierung. Treiben wir es noch einen Schritt weiter. Es lägen etwa Adressen für verschiedene Anwendungsbereiche vor, seien es Kundenadressen, Ansprechpartner, Behörden, Firmen.
Dann könnten alle Adressen zusammen wiederum in einen neuen Typ überführt werden:
Public Enum eAddressType eAddressUnspecific = 0 eAddressCustomer = 1 eAddressPartner = 2 eAddressCompany = 3 End Enum Public Type TAdressenGesamt AddressType As sAddressType AddressBlock As TAdressen End Type
Die Zuordnung der Werte wird nun zwar etwas länglich, ist aber dennoch gut zu überblicken:
Dim A() As TAdressenGesamt ReDim A(3) A(1).AddressType = eAddressCustomer Redim A(1).AdressBlock.Items(9) With A(1).AdressBlock .Items(0).Nachname = "Minhorst" .Items(0).Vorname = "Minhorst" .Items(1).Nachname = "Trowitzsch" .Items(1).Vorname = "Sascha" ... End With A(2).AddressType = eAddressUnspecific Redim A(2).AdressBlock.Items(9) With A(2).AdressBlock .Items(0).Nachname = "Häberle" .Items(0).Vorname = "Adam" .Items(1).Nachname = "Gutschmidt" .Items(1).Vorname = "Andrea" ... End With ...
Immerhin haben wir nun mehrere Adressblöcke unterschiedlichen Umfangs und Typs in einer Variablen vereint! Das einzig hinderlich ist die Notwendigkeit, die Items-Arrays jeweils neu zu dimensionieren. Das ließe sich mit einer festen Anzahl von Elementen umgehen:
Public Type TAdressen Items(99) As TAdresse End Type
Damit läge die Maximalzahl von Adressen für einen Block bei Hundert. Probleme gibt stellen sich also ein, wenn doch mehr benötigt würden. Und bei kleinerer Anzahl würde außerdem Speicherplatz vergeudet. Dieses Beispiel sollte eigentlich nur eines demonstrieren, nämlich wie sich benutzerdefinierte Typen im Interesse von Strukturierung verschachteln lassen. Das Ganze geht in Richtung Objektorientierter Programmierung, mit der wir es später bei den Klassenmodulen noch mehr zu tun bekommen.
Tabellendaten in benutzerdefinierten Typen
Damit es nicht bei trockener Theorie bleibt, folgt ein Beispiel, das in der einen oder anderen Datenbank durchaus vertreten sein kann. Es geht hier darum, die Datensätze einer Tabelle oder Abfrage in VBA-Strukturen zu überführen.
Listing 1 zeigt das Grundgerüst mit der Prozedur TestUDT. Nebenbei erwähnt ist UDT das Kürzel für einen benutzerdefinierten Typ (User Defined Type). Eine Datensatzgruppe rs wird auf alle Datensätze der Tabelle tblAdressen geöffnet. In einer Do-Loop-Schleife werden sie alle durchlaufen und die einzelnen Feldinhalte den Elementen der Variablen T des Typs TAdresse verabreicht. Da mache Felder der Datensätze leer sind und Leer nicht in einem String gespeichert werden kann – Strasse ist etwa vom Typ String – müssen die Werte noch einer Vorbehandlung mit der Funktion Nz unterzogen werden, die aus Nullwerten ordnungsgemäße Strings der Länge 0 macht.
Sub TestUDT() Dim rs As DAO.Recordset Dim T As TAdresse Set rs = CurrentDb.OpenRecordset("SELECT * FROM tblAdressen", dbOpenDynaset) Do While Not rs.EOF With T .ID = rs!ID.Value .Nachname = Nz(rs!Nachname.Value) .Vorname = Nz(rs!Vorname.Value) .Strasse = Nz(rs!Strasse.Value) .PLZ = Nz(rs!PLZ.Value) .Ort = Nz(rs!Ort.Value) End With rs.MoveNext Loop rs.Close End Sub
Listing 1: Hier werden die Daten der Tabelle tblAdressen hintereinander in einer Variablen T des benutzerdefinierten Typs TAdressen abgespeichert
Viel Sinn macht das Beispiel nicht. Schließlich landen alle Daten in der einen Variablen T und überschreiben sich damit bei jedem Schleifendurchlauf. In Listing 2 ist die Routine aufgebohrt und um den Typ TAdressen erweitert, welcher, wie bereits weiter oben dargestellt, mehrere Adressen aufnehmen kann. Die Items des Typs T, also die einzelnen Adressen, müssen zunächst auf die Zahl der Datensätze des Recordsets dimensioniert werden. Sie erhalten diese über die Methode RecordCount, nachdem Sie den Daten-Cursor über MoveLast einmal auf das Ende der Datensätze einstellen. Da das Array Items nullbasiert ist, muss die Obergrenze auf RecordCount – 1 festgelegt werden. In der folgenden Do-Loop-Schleife wird auf eine Adresse nun über den Array-Index i Bezug genommen, wobei T.Items(i) als Spezifikation des With-Blocks dient. Die Zählervariable i wird nach erfolgtem MoveNext innerhalb der Schleife erhöht. Im Endergebnis haben Sie damit alle Datensätze der Tabelle in der einen Variablen T.
Sub TestUDTArray() Dim rs As DAO.Recordset Dim T As TAdressen Dim i As Long Set rs = CurrentDb.OpenRecordset("SELECT * FROM tblAdressen", dbOpenDynaset) rs.MoveLast: rs.MoveFirst ReDim T.Items(rs.RecordCount - 1) Do While Not rs.EOF With T.Items(i) .ID = i .Nachname = Nz(rs!Nachname.Value) .Vorname = Nz(rs!Vorname.Value) .Strasse = Nz(rs!Strasse.Value) .PLZ = Nz(rs!PLZ.Value) .Ort = Nz(rs!Ort.Value) End With rs.MoveNext: i = i + 1 Loop rs.Close End Sub
Listing 2: Hier kommt der benutzerdefinierte Typ TAdressen zum Tragen
Was damit dann anstellen, ist ein anderes Thema. Beispiel wäre etwa das Abspeichern in eine Textdatei. Tatsächlich lassen sich Variablen eines benutzerdefinierten Typs über nur eine einzige Zeile exportieren. Sie könnten die Routine von Listing 2 am Ende mit diesen Zeilen erweitern:
Dim F As Integer F = FreeFile Open "c:\daten\tabelle.txt" For Binary As F Put F,, T Close F
Mit der dergestalt exportierten Textdatei kann allerdings kein anderes Programm etwas anfangen. Sie lässt sich aber analog in einem VBA-Programm einlesen, das den Typ TAdressen kennt; sinngemäß:
Dim T As TAdressen Get F,, T
Klassenmodule
Recht grundlos fristen Klassenmodule unter VBA ein Nischendasein. Entweder wird der Umgang mit ihnen immer wieder als kompliziert dargestellt, oder aber der Nutzen in Frage gestellt. Schließlich lässt sich mit den normalen Modulen dasselbe bewerkstelligen Dem ist aber nicht so. Grund genug, sich einmal deren Grundlagen zu widmen.
Rein mechanisch fügen Sie Ihrer Datenbank ein Klassenmodul hinzu, indem Sie den gleichnamigen Button im Ribbon unter der Registerseite Erstellen anklicken, oder, alternativ, im Projekt-Explorer der VBA-Umgebung und das Kontextmenü Einfügen | Klassenmodul betätigen. Bei Speichern vergeben Sie dem Modul einen Namen. Es hat sich eingebürgert, hier ein abweichendes Präfix zu verwenden. Versieht man normale Module meist mit dem Präfix mod oder mdl, so kommt hier eher ein cls oder schlicht C zum Einsatz. Ist das Thema des Klassenmoduls die Verwaltung von Adressen, so verwenden Sie etwa clsAdressen.
Vom programmtechnischen Standpunkt aus unterscheidet sich ein Klassenmodul nicht von normalen Modulen. Es existiert lediglich eine kleine Erweiterung, das Event, auf welches wir noch zu sprechen kommen. Der Unterschied besteht vielmehr darin, dass ein Klassenmodul eben eine Klasse zur Verfügung stellt, während normale Module eine Sammlung von allgemein verwendbaren Funktionen verzeichnet. Die Methoden der Klasse können aber nicht direkt aufgerufen werden. Eine Klasse ist nämlich lediglich der Bauplan für ein Objekt! Ein Objekt stellt eine Instanz einer Klasse dar, welche erst per New-Anweisung erzeugt werden muss, um an die Funktionen der Klasse zu kommen:
Dim C As clsAdressen Set C = New clsAdressen
oder einfach
Dim C As New clsAdressen
über C gelangen Sie dann an die Methoden der Klasse. ähnlich, wie beim benutzerdefinierten Typ, zeigt IntelliSense die Methoden der Klasse, wenn Sie einen Punkt hinter die Variable setzen. Enthielte die Adressenklasse eine Prozedur FindeNachnamen, so riefen Sie jene auf diese Weise auf:
C.FindeNachname
Der Umstand, dass Klassenmodule erst auf dem Umweg über deren Instanziierung ansprechbar werden, macht sie wahrscheinlich unbeliebt, ist doch der Aufwand höher, als bei normalen Modulfunktionen.
Es gibt an anderer Stelle erschöpfende Darlegungen über Klassen, Objekte, Instanziierung und objektorientierte Programmierung. Da viele der Charakteristiken von Objektstrukturen unter VBA jedoch nicht implementiert sind, lassen wir es für den Anfang bei einigen grundlegenden Erläuterungen.
Ein Schlagwort bei Klassen ist Kapselung. Dadurch, dass eine Klasse erst dezidiert über eine Variable instanziiert werden muss, kapselt sich deren Funktionalität und schottet den Zugriff im Vergleich zu normalen Modulen ab. Die Methoden sind nur Teil des Objekts und damit nicht generell verfügbar.
Deshalb eignet sich eine Klasse und deren Objekt auch für die Aufnahme von Daten. Nicht zufällig hatten wir es eingangs mit benutzerdefinierten Objekten zu tun. UDT-Variablen sind häufig öffentlich ansprechbar und damit unsicherer, als ein Objekt.
Container-Klassen
Möchten Sie weiterlesen? Dann lösen Sie Ihr Ticket!
Hier geht es zur Bestellung des Jahresabonnements des Magazins Access [basics]:
Zur Bestellung ...
Danach greifen Sie sofort auf alle rund 400 Artikel unseres Angebots zu - auch auf diesen hier!
Oder haben Sie bereits Zugangsdaten? Dann loggen Sie sich gleich hier ein: