This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Die Wahl für professionelle Programmierer und Softwareentwickler. Anerkannte Experten wie z.B. Bjarne Stroustrup, der Erfinder von C++, liefern umfassendes Fachwissen zu allen wichtigen Programmiersprachen und den neuesten Technologien, aber auch Tipps aus der Praxis. Die Reihe von Profis für Profis!
Hier eine Auswahl: Professionelle Websites Stefan Münz 1136 Seiten € 59,95 (D), € 61,70 (A) ISBN-13: 978-3-8273-2370-5 ISBN-10: 3-8273-2370-3
Wenn heute von Webdesign die Rede ist, dann immer häufiger von striktem HTML, von sauberer Trennung zwischen Layout und Inhalt, und von Beachtung der Regeln für barrierefreie Websites. Beschrieben wird hier, was der Zukunft gehört und auf immer breiterer Front Anwendung findet: strukturell sinnvolles, am Strict-Standard des W3-Konsortiums orientiertes HTML, layout-formendes, intelligent eingesetztes CSS und benutzerfreundliches, DOM-orientiertes JavaScript. Auch die Serverseite darf nicht fehlen. Immer mehr Site-Betreiber steigen auf eigene Root-Server um. Vorinstalliert ist dort meistens das beliebte LAMP-Paket, bestehend aus einem Linux-Derivat, dem Apache Webserver, dem MySQL Datenbank-System und der Scriptsprache PHP. Genau diese Technologien werden im Buch gründlich und zusammenhängend behandelt.
Visual C# 2005 Frank Eller 1104 Seiten € 49,95 (D), € 51,40 (A) ISBN-13: 978-3-8273-2288-2 ISBN-10: 3-8273-2288-X
Fortgeschrittene und Profis erhalten hier umfassendes Know-how zur Windows-Programmierung mit Visual C# in der Version 2. Nach einer Einführung ins .NET-Framework und die Entwicklungsumgebung geht der Autor ausführlich auf die Grundlagen der C#-Programmierung ein. Anhand zahlreicher Beispiele zeigt er die verschiedenen Programmiertechniken wie z.B. Anwendungsdesign, Grafikprogrammierung oder das Erstellen eigener Komponenten. Besondere Schwerpunkte liegen auf der umfangreichen .NET-Klassenbibliothek und Windows Forms sowie auf dem Datenbankzugriff mit ADO.NET.
Michael Jendryschik André Minhorst
Access 2007in XHTML, Einführung Das Grundlagenbuch CSS und Webdesign für Entwickler Standardkonforme, moderne und barrierefreie Websites erstellen
An imprint of Pearson Education München • Boston • San Francisco • Harlow, England Don Mills, Ontario • Sydney • Mexico City Madrid • Amsterdam
Bibliografische Information Der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über abrufbar. Die Informationen in diesem Produkt werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht. Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Abbildungen und Texten wurde mit größter Sorgfalt vorgegangen. Trotzdem können Fehler nicht vollständig ausgeschlossen werden. Verlag, Herausgeber und Autoren können für fehlerhafte Angaben und deren Folgen weder eine juristische Verantwortung noch irgendeine Haftung übernehmen. Für Verbesserungsvorschläge und Hinweise auf Fehler sind Verlag und Herausgeber dankbar. Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und der Speicherung in elektronischen Medien. Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig. Fast alle Hardware- und Softwarebezeichnungen und weitere Stichworte und sonstige Angaben, die in diesem Buch verwendet werden, sind als eingetragene Marken geschützt. Da es nicht möglich ist, in allen Fällen zeitnah zu ermitteln, ob ein Markenschutz besteht, wird das ®-Symbol in diesem Buch nicht verwendet. Umwelthinweis: Dieses Produkt wurde auf chlorfrei gebleichtem Papier gedruckt.
Die Benutzeroberfläche 1.1.1 Navigationsbereich statt Datenbankfenster 1.1.2 Ribbon statt Menü- und Symbolleisten 1.1.3 Neuer Optionen-Dialog 1.1.4 Neuer Startbereich 1.1.5 Neue Onlinehilfe Neues Datenbankformat Runtime gratis Goodbye Sicherheitssystem Neue Sicherheitsfunktionen Bye-bye Replikation Adieu Datenzugriffsseiten ACE — die neue Jet-Engine MDI vs. SDI oder »Jedem Objekt sein Register« Neuer Datentyp: Anlagefeld Mehrwertige Felder Weitere Neuigkeiten zu Felddatentypen und Steuerelementen 1.12.1 Rich-Text 1.12.2 Historie in Memofeldern 1.12.3 Datumssteuerelement 1.12.4 Schaltflächen mit Bild und Text 1.12.5 Kombinationsfelder und Listenfelder 1.12.6 Bildsteuerelement mit Steuerelementinhalt 1.12.7 Zu große Zahlen ersetzen Neues in Tabellen 1.13.1 Alternative Hintergrundfarbe 1.13.2 Entwurf in der Datenblattansicht von Tabellen Neues bei den Formularen 1.14.1 Layoutansicht 1.14.2 Geteilte Ansicht 1.14.3 Filtern und Sortieren 1.14.4 Berechnungen auf Spaltenbasis 1.14.5 Vereinfachtes Layouten 1.14.6 Verankern von Steuerelementen 1.14.7 AutoHeight für Formularbereiche Neues bei Berichten 1.15.1 Berichtsansicht
Autowerte als Long oder GUID? Datenmodell-Muster 2.7.1 Adressen-/Kundenverwaltung 2.7.2 Rezepteverwaltung 2.7.3 Artikelverwaltung 2.7.4 CD-Verwaltung 2.7.5 Projektverwaltung 2.7.6 Mitarbeiterverwaltung 2.7.7 Literaturverwaltung 2.7.8 Mitgliederverwaltung 2.7.9 Urlaubsverwaltung 2.7.10 Aufgabenverwaltung 2.7.11 Projektzeitverwaltung 2.7.12 Kunden und Weihnachtsgeschenke 2.7.13 Fahrtenbuch
3 Abfragen 3.1 3.2 3.3
3.4 3.5
3.6 3.7 3.8
3.9
Anlegen von Abfragen mit Access 2007 Abfragen mit Anlage-Feldern und mehrwertigen Feldern Verwendung von Abfragen als Datensatzquelle oder Datensatzherkunft 3.3.1 Tabelle als Datensatzquelle 3.3.2 SQL-Ausdruck als Datensatzquelle 3.3.3 Gespeicherte Abfrage als Datensatzquelle 3.3.4 Datensatzquelle per VBA zuweisen 3.3.5 Parameter statt Zusammensetzen von SQL-Ausdrücken 3.3.6 Abfragen mit Parameter oder zusammengesetzte SQL-Ausdrücke? 3.3.7 Probleme mit Kriterienausdrücken bei SQL-Ausdrücken in VBA 3.3.8 Zeichenkette oder Zahlenwert? 3.3.9 Probleme mit Datumsangaben 3.3.10 Verweis auf Steuerelemente Aktualisierbarkeit von Abfragen 3.4.1 Wie erkennen Sie, ob das Abfrageergebnis aktualisierbar ist? 3.4.2 Nicht aktualisierbare Abfragen UNION-Abfragen 3.5.1 UNION-Abfragen zur Optimierung von Kombinationsfeldern 3.5.2 Eindeutige Schlüssel mit UNION-Abfragen 3.5.3 INSERT INTO mit UNION-Abfragen Suchen in m:n-Beziehungen Handhabung von 1:1-Beziehungen Extremwerte per Abfrage ermitteln 3.8.1 Extremwert einer Gruppierung ermitteln 3.8.2 Extremwert per TOP und ORDER BY 3.8.3 Extremwerte per Unterabfrage 3.8.4 Extremwerte von Gruppierungen Datensätze mehrfach anzeigen
Inhalt 3.10 Nummerierung von Datensätzen 3.10.1 Alternative: Nummerieren per Unterabfrage 3.10.2 Nummerierung von Abfrageergebnissen mit alternativen Sortierungen 3.10.3 Nummerierung von Abfrageergebnissen mit eingeschränkten Ergebnismengen 3.11 Reflexive 1:n-Beziehungen 3.12 Reflexive m:n-Beziehungen
4 Formulare 4.1
4.2 4.3
4.4 4.5
4.6 4.7 4.8
10
Formulare in Access 2007 4.1.1 Anlegen eines Formulars 4.1.2 Formularansichten 4.1.3 Geteilte Formulare 4.1.4 Hilfreiche Funktionen für den Formularentwurf 4.1.5 Sonstige Neuerungen 4.1.6 Formularvorlage Formulare öffnen Ereignisse in Formularen und Steuerelementen 4.3.1 Ereignisse in Formularen 4.3.2 Abfolge und Bedeutung der Ereignisse beim Öffnen und Schließen eines Formulars 4.3.3 Abfolge und Bedeutung der Ereignisse beim Bearbeiten von Datensätzen Ereignisse von Steuerelementen Abbildung verschiedener Beziehungsarten 4.5.1 Einfache Daten in der Detailansicht 4.5.2 Einfache Daten in der Übersicht mit Endlosformularen 4.5.3 Einfache Daten in der Übersicht als Datenblatt 4.5.4 Daten in der Übersicht als Listenfeld 4.5.5 1:1-Beziehungen 4.5.6 n:1-Beziehungen 4.5.7 1:n-Beziehungen 4.5.8 1:n-Beziehung per Unterformular und Datenblattansicht 4.5.9 1:n-Beziehung per Listenfeld 4.5.10 m:n-Beziehungen in Haupt- und Unterformular 4.5.11 m:n-Beziehungen per Listenfeld 4.5.12 Reflexive Beziehungen Von Formular zu Formular Besonderheiten von Unterformularen 4.7.1 Eingabe von Daten ohne Detaildatensatz 4.7.2 Undo in Haupt- und Unterformular Eingabevalidierung 4.8.1 Validieren direkt bei der Eingabe 4.8.2 Validieren vor dem Speichern 4.8.3 Sonderfälle beim Validieren
Suchen in Formularen 4.9.1 Schnelles Suchen in Formularen 4.9.2 Schnelles Filtern in der Datenblattansicht 4.9.3 Schnellauswahl per Kombinationsfeld 4.9.4 Schnelles Filtern von Listenfeldern
5 Steuerelemente Textfelder 5.1.1 Rich-Text in Textfeldern 5.1.2 Datum auswählen 5.1.3 Texte als Hyperlink anzeigen 5.1.4 Abgeschnittene Zahlenfelder 5.2 Schaltflächen 5.3 Kombinationsfelder 5.3.1 Wertliste erben 5.3.2 Formular zum Bearbeiten anzeigen 5.3.3 Wachsen und Schrumpfen 5.3.4 Hyperlinks 5.3.5 Mehrwertige Felder 5.4 Kombinationsfeld-Techniken 5.4.1 Kombinationsfeld aufklappen 5.4.2 Auswählen-Eintrag hinzufügen 5.4.3 Abhängige Kombinationsfelder 5.4.4 Bestimmten Eintrag auswählen 5.4.5 Aktuell markierten Eintrag auslesen 5.4.6 Wert zu einem gebundenen Kombinationsfeld hinzufügen 5.4.7 Weitere Techniken 5.5 Listenfelder 5.5.1 Mehrfachauswahl auslesen 5.5.2 Ja/Nein-Felder im Listenfeld anzeigen 5.5.3 Weitere Techniken 5.6 Unterformulare 5.7 Das Anlagen-Steuerelement 5.8 Optionsgruppe, Umschaltfläche, Kontrollkästchen, Bildsteuerelement und Co. 5.9 Weitere Steuerelementeigenschaften 5.9.1 Steuerelemente verankern 5.9.2 Layout oder nicht? 5.9.3 Gitternetzlinien 5.9.4 Textabstand 5.10 Das TreeView-Steuerelement 5.10.1 TreeView anlegen 5.10.2 Eigenschaften des TreeView-Steuerelements 5.10.3 Erzeugen eines Baumes 5.10.4 Stil einstellen 5.10.5 Element-Eigenschaften per VBA zuweisen
Inhalt 5.10.6 Symbole im TreeView 5.10.7 Daten aus Tabellen im TreeView-Steuerelement darstellen 5.10.8 Daten aus verknüpften Tabellen anzeigen 5.10.9 Reflexive Daten im TreeView-Steuerelement 5.10.10 TreeView füllen bei großen Datenbeständen 5.10.11 Elemente erst bei Bedarf anlegen 5.10.12 Neuzeichnen des Baumes verhindern 5.10.13 Drag and Drop im TreeView-Steuerelement 5.10.14 VBA-Ereignisprozeduren für Drag and Drop einrichten 5.11 ListView 5.11.1 Möglichkeiten des ListView-Steuerelements 5.11.2 Füllen des ListView-Steuerelements 5.11.3 Eigenschaften des ListView-Steuerelements 5.11.4 Sortieren von ListView-Einträgen 5.11.5 Einträge des ListView-Steuerelements auswählen 5.11.6 ListView-Steuerelement mit Daten füllen 5.11.7 Icons im ListView-Steuerelement 5.11.8 Drag and Drop mit dem ListView-Steuerelement 5.11.9 Reihenfolge per Drag and Drop einstellen 5.12 Das ImageList-Steuerelement 5.13 Tipps und Tricks zu Steuerelementen 5.13.1 Standardeinstellungen speichern 5.13.2 Autoformate
6 Berichte 6.1
6.2 6.3
6.4
12
Berichte erstellen 6.1.1 Anlegen eines Berichts 6.1.2 Vereinfachtes Layouten 6.1.3 Einheitliches Design mit Autoformat 6.1.4 Wechselnde Hintergrundfarbe 6.1.5 Bedingte Formatierung 6.1.6 Sonstige Layout-Vereinfachungen 6.1.7 Berichtsbereiche 6.1.8 Berichtsansichten 6.1.9 Gruppieren und sortieren Berichte anzeigen Filtern und sortieren 6.3.1 Filtern und Sortieren in der Berichtsvorschau 6.3.2 Filtern, Sortieren und Gruppieren in der Layoutansicht 6.3.3 Filtern in der Layoutansicht 6.3.4 Sortieren in der Layoutansicht 6.3.5 Gruppieren in der Layoutansicht 6.3.6 Summen in der Layoutansicht Berichtsbereiche und Ereignisse 6.4.1 Berichtsbereiche
Inhalt 6.4.2 Ereignisse in Berichten 6.4.3 Zusammenfassung der Berichtsereignisse 6.4.4 Zusammenfassung der Bereichsereignisse 6.4.5 Zugriff auf die Berichtsbereiche 6.5 Beispiele für den Einsatz der Berichts- und Bereichsereignisse in der Seitenansicht 6.5.1 Beim Öffnen: Auswertung von Öffnungsargumenten 6.5.2 Bei Aktivierung und Bei Deaktivierung: Berichtsabhängige Funktionen ein- und ausschalten 6.5.3 Bei Ohne Daten: Öffnen leerer Berichte vermeiden 6.5.4 Bei Fehler: Fehler abfangen 6.5.5 Bei Seite: Seiten verschönern 6.5.6 Beim Formatieren: Layout anpassen 6.5.7 Beim Drucken 6.6 Wichtige Eigenschaften von Berichten und Berichtsbereichen 6.6.1 Kopfzeilenbereich und Fußzeilenbereich 6.6.2 Gruppieren nach und Intervall 6.6.3 Zusammenhalten von Daten 6.6.4 Neue Seite, Zeile oder Spalte 6.6.5 Vergrößerbar und Verkleinerbar 6.6.6 Bereich wiederholen 6.7 Darstellung von Daten 6.7.1 Einzelne Tabellen 6.7.2 1:n-Beziehungen 6.7.3 m:n-Beziehungen 6.8 Berichte mit Unterberichten 6.8.1 Unterberichte 6.8.2 Einbinden der Unterberichte in den Hauptbericht 6.8.3 Unterberichte über mehrere Seiten 6.9 Rechnungserstellung mit Berichten 6.9.1 Konzept für die Erstellung des Berichts 6.9.2 Erstellen des Gruppenkopfs 6.9.3 Anlegen des Detailbereichs 6.9.4 Berechnungen in Berichten oder Berechnungen in Formularen 6.9.5 Summenbildung im Fußbereich der Gruppierung 6.9.6 Feinheiten: Zwischensumme und Übertrag 6.9.7 Überschriften für Folgeseiten und Rechnungsübertrag 6.9.8 Rechnungsentwurf im Zusammenhang und Restarbeiten 6.10 Die Berichtsansicht 6.11 Anwendungsbeispiel für die Berichtsansicht
7 VBA 7.1 7.2 7.3
VBA-Neuigkeiten in Access 2007 Namenskonventionen in VBA Layout von Code 7.3.1 Funktionalität vor Schönheit?
7.3.2 Code einrücken zur Verdeutlichung der logischen Struktur 7.3.3 Leerzeilen für bessere Lesbarkeit 7.3.4 Zeilenumbrüche 7.3.5 Anweisungen zusammenfassen Kommentare Konstanten Variablen 7.6.1 Variablennamen 7.6.2 Spezielle Variablennamen 7.6.3 Arrays 7.6.4 Benutzerdefinierte Typen 7.6.5 Alle Variablen verwenden 7.6.6 Globale Variablen Kontrollstrukturen 7.7.1 If...Then-Anweisung 7.7.2 Select Case 7.7.3 For...Next-Schleifen 7.7.4 For Each-Schleifen 7.7.5 Do…Loop-Schleifen und Varianten 7.7.6 Exit 7.7.7 Die GoTo-Anweisung und Sprungmarken Routinen 7.8.1 Routinenarten 7.8.2 Routinennamen 7.8.3 Starker Zusammenhalt von Routinen 7.8.4 Lose Kopplung zwischen Routinen 7.8.5 Parameter und Rückgabewerte einer Routine 7.8.6 Gleichzeitige Rückgabe von Statuswert und Ergebnis 7.8.7 Alle Routinen verwenden
8 Access-SQL 8.1 8.2 8.3
14
SQL-Versionen SQL und Access 8.2.1 Wozu trotz Abfrage-Entwurfsansicht SQL lernen? 8.2.2 Wo lässt sich SQL überall einsetzen? Daten auswählen 8.3.1 Festlegen der anzuzeigenden Felder 8.3.2 Festlegen der enthaltenen Tabellen 8.3.3 Festlegen von Bedingungen 8.3.4 Vergleichsausdrücke 8.3.5 Sortieren von Daten 8.3.6 Aggregatfunktionen 8.3.7 Gruppieren von Daten 8.3.8 WHERE, GROUP BY, HAVING und ORDER BY im Überblick 8.3.9 Verknüpfen von Tabellen in Abfragen 8.3.10 Zugriff auf externe Datenquellen
Zugriff auf Felder des Datentyps Anhang und mehrwertige Felder Daten manipulieren 8.4.1 Daten aktualisieren 8.4.2 Daten löschen 8.4.3 Daten an bestehende Tabelle anfügen 8.4.4 Neue Tabelle mit Daten erstellen Datenmodell erstellen und manipulieren 8.5.1 Tabellen erstellen 8.5.2 Primärschlüssel, Indizes und Einschränkungen mit CONSTRAINT 8.5.3 Tabelle ändern 8.5.4 Tabelle löschen 8.5.5 Index löschen
9 DAO 9.1 9.2
9.3 9.4
9.5 9.6
9.7
DAO und ADO im Einsatz Das DAO-Objektmodell 9.2.1 Zugriff auf die Elemente des Objektmodells 9.2.2 Deklarieren und Instanzieren 9.2.3 Auf Auflistungen zugreifen 9.2.4 Punkte und Ausrufezeichen DBEngine Workspace — Arbeitsbereich oder Sitzung? 9.4.1 Auflistungen des Workspace-Objekts 9.4.2 Aufgaben des Workspace-Objekts 9.4.3 Datenbanken erzeugen und öffnen Aktuelle Datenbank referenzieren 9.5.1 Users und Groups Das Database-Objekt 9.6.1 Manipulation des Datenmodells 9.6.2 Erstellen einer Tabelle 9.6.3 Autowert anlegen 9.6.4 Attachment-Feld anlegen 9.6.5 Mehrwertige Felder anlegen 9.6.6 Löschen einer Tabelle 9.6.7 Erstellen eines Index 9.6.8 Löschen eines Index 9.6.9 Erstellen einer Beziehung 9.6.10 Löschen einer Beziehung 9.6.11 Erstellen von Eigenschaften 9.6.12 Zugriff auf Auflistungen und Elemente 9.6.13 Datensatzgruppen erstellen mit OpenRecordset 9.6.14 Ausführen von Aktionsabfragen Daten bearbeiten mit dem Recordset- und dem Recordset2-Objekt
Inhalt Methoden und Eigenschaften des Recordset2-Objekts Datensätze durchlaufen Alle Datensätze durchlaufen Zu bestimmten Datensätzen springen Aktuelle Position des Datensatzzeigers ermitteln Anzahl der Datensätze ermitteln Daten aus Datensätzen ausgeben Datensätze suchen Die Seek-Methode zum Suchen in Table-Recordsets Die Find-Methoden zum Suchen in Dynaset- und Snapshot-Recordsets 9.7.11 Alle Datensätze mit einem bestimmten Kriterium finden 9.7.12 Lesezeichen 9.8 Sortieren und Filtern von Datensätzen 9.8.1 Sortieren mit der Sort-Eigenschaft 9.8.2 Sortieren mit der Index-Eigenschaft 9.8.3 Filtern mit der Filter-Eigenschaft 9.9 Daten bearbeiten 9.9.1 Anlegen eines Datensatzes 9.9.2 Bearbeiten eines Datensatzes 9.9.3 Löschen eines Datensatzes 9.9.4 Umgang mit Attachments 9.9.5 Attachment-Felder auslesen 9.9.6 Dateien aus einem Attachment-Feld auf der Festplatte speichern 9.9.7 Datei in Attachment-Feldern speichern 9.9.8 Löschen von Dateien in Attachment-Feldern 9.9.9 Ersetzen eines Attachments 9.9.10 Umgang mit mehrwertigen Feldern 9.9.11 Lesen des Inhalts von mehrwertigen Feldern, Variante I 9.9.12 Lesen des Inhalts mehrwertiger Felder, Variante II 9.10 QueryDefs — Auswahl oder Aktion nach Wahl 9.11 Transaktionen 9.7.1 9.7.2 9.7.3 9.7.4 9.7.5 9.7.6 9.7.7 9.7.8 9.7.9 9.7.10
10 ADO 10.1 Zugriff auf eine Datenquelle herstellen 10.2 Manipulation des Datenmodells 10.2.1 Anlegen einer Tabelle 10.2.2 Autowert anlegen 10.2.3 Löschen einer Tabelle 10.2.4 Erstellen eines Index 10.2.5 Löschen eines Index 10.2.6 Erstellen einer Beziehung 10.2.7 Löschen einer Beziehung 10.3 Zugriff auf Tabellen, Abfragen und die darin enthaltenen Daten 10.3.1 Ausgeben aller Tabellen 10.3.2 Prüfen, ob eine Tabelle vorhanden ist
10.3.3 Datensatzgruppe auf Basis einer Tabelle öffnen 10.3.4 Cursor-Typen 10.3.5 Sperrung von Daten 10.3.6 Datensätze eines Recordsets durchlaufen 10.3.7 Daten eines Recordsets mit mehrwertigen Feldern ausgeben 10.3.8 Daten eines Recordsets mit Attachment-Feldern ausgeben 10.3.9 Anzahl der Datensätze in einer Datensatzgruppe ermitteln 10.3.10 Prüfen, ob eine Datensatzgruppe leer ist 10.3.11 Ausgabe des Inhalts eines Recordsets 10.3.12 Speichern der Daten in einem Array 10.3.13 Abfragen mit Parametern verwenden Datensätze suchen 10.4.1 Gesuchte Datensätze per Source-Eigenschaft des Recordsets ermitteln 10.4.2 Seek 10.4.3 Find 10.4.4 Filtern 10.4.5 Sortieren 10.4.6 Lesezeichen Datensätze bearbeiten 10.5.1 Datensatz anlegen 10.5.2 Datensatz bearbeiten 10.5.3 Datensatz löschen 10.5.4 Aktionsabfragen ausführen Transaktionen Besonderheiten von ADO gegenüber DAO 10.7.1 Datensatzgruppe speichern 10.7.2 Datensatzgruppe laden 10.7.3 Ungebundene Recordsets verwenden 10.7.4 Disconnected Recordsets 10.7.5 Ereignisse von Datensatzgruppen
Bilder und Dateien als Anlage speichern Bilder aus Anlage-Feldern in Formularen anzeigen Bilder aus Anlage-Feldern in Berichten anzeigen Bilder und Dateien aus Anlage-Feldern auf der Festplatte speichern Dateien per VBA in Anlage-Felder importieren und exportieren 11.5.1 Importieren von Dateien in Anlage-Felder 11.5.2 Exportieren von Dateien aus dem Anlage-Feld Bilder und Dateien im OLE-Feld einbetten oder verknüpfen Bilder und Dateien als Binärstrom im OLE-Feld speichern Bilder und Dateien im binären Format aus einem OLE-Feld wiederherstellen Bilder von der Festplatte in Formularen und Berichten anzeigen
Inhalt 11.9.1 Anzeigen externer Bilddateien im Formular 11.9.2 Anzeige externer Bilddateien in Berichten 11.9.3 Alternative zum Bildsteuerelement von Access 11.10 Die Office Graphics Library 11.10.1 Bilder aus dem OLE-Feld in einem Formular anzeigen 11.10.2 Bild aus einem OLE-Feld wiederherstellen 11.10.3 Speichern in verschiedenen Formaten 11.10.4 Bilder bearbeiten 11.10.5 Ersatz für Anlagen?
12 Ribbon 12.1 Definition des Ribbons 12.2 Symbolleiste für den Schnellzugriff 12.3 Eigene Ribbon-Tabs erstellen 12.3.1 Ein einfaches Ribbon 12.3.2 Schaltfläche mit Funktion versehen 12.4 Fehler in Ribbon-XML-Dokumenten erkennen 12.5 Callback-Funktionen 12.5.1 Die get...-Attribute 12.5.2 Ereigniseigenschaften 12.5.3 Umgang mit Callback-Funktionen 12.6 Weitere Ribbon-Steuerelemente 12.6.1 Schaltflächen 12.6.2 Kontrollkästchen (checkBox) 12.6.3 Textfelder 12.6.4 Kombinationsfelder I: Das comboBox-Element 12.6.5 Kombinationsfelder II: Das dropDown-Element 12.6.6 Umschaltflächen 12.6.7 Galerien 12.6.8 Menüs (menu) 12.6.9 Splitbuttons (splitButton) 12.6.10 Gruppendialog anzeigen 12.6.11 Trennstrich (separator) 12.7 Weitere Anpassungen des Ribbons 12.7.1 Tastenkombinationen 12.7.2 Alle Ribbons ausblenden 12.7.3 Ribbon-Leiste minimieren 12.7.4 Ein tab-Element ein- und ausblenden 12.7.5 Eine Gruppe ein- und ausblenden 12.7.6 Ein Steuerelement ein- und ausblenden 12.7.7 Eingebaute Steuerelemente aktivieren und deaktivieren 12.7.8 Eingebaute Steuerelemente mit neuen Funktionen belegen 12.7.9 Sonderzeichen in Ribbon-Texten 12.7.10 Einen Eintrag zum Office-Menü hinzufügen 12.7.11 Einträge des Office-Menüs ausblenden 12.7.12 Einen Eintrag zur Schnellzugriffsleiste hinzufügen
Inhalt 12.8 Ribbons für Formulare und Berichte 12.9 XML-Dokument mit Application.LoadCustomUI laden 12.9.1 Dynamisches Aktualisieren des Ribbons 12.9.2 Beispiel: Abhängige Kontrollkästchen 12.10 Menü- und Symbolleisten aus bestehenden Access 2003-Anwendungen 12.11 Übersicht über Ribbon-XML-Elemente und Attribute 12.11.1 Auflistung der Ribbon-Elemente 12.11.2 Attribute der Ribbon-Elemente 12.11.3 Ereigniseigenschaften der Ribbon-Elemente 12.11.4 Steuerelemente und ihre Eigenschaften
13 Debugging, Fehlerbehandlung und Fehlerdokumentation 13.1 Fehlerarten 13.1.1 Syntaxfehler 13.1.2 Laufzeitfehler 13.1.3 Logische Fehler 13.2 Debugging in der VBA-Entwicklungsumgebung 13.2.1 Die Debuggen-Symbolleiste 13.2.2 Das Direktfenster 13.2.3 Haltepunkte 13.2.4 Die Aufrufliste 13.2.5 Ausdrücke überwachen 13.2.6 Das Lokal-Fenster 13.3 Fehlerbehandlung in VBA 13.3.1 Elemente der Fehlerbehandlung 13.3.2 Fehlerbehandlung einleiten 13.3.3 Klassischer Aufbau einer Fehlerbehandlung 13.3.4 Fehler auswerten 13.3.5 Das Err-Objekt 13.3.6 Nach der Fehlerbehandlung 13.3.7 Fehlernummern und -beschreibungen 13.3.8 Benutzerdefinierte Fehlerbehandlung temporär ausschalten 13.3.9 Funktionale Fehlerbehandlung 13.3.10 Benutzerdefinierte Fehler 13.3.11 Fehler bei API-Aufrufen 13.4 Fehlerdokumentation und -übermittlung 13.4.1 Wichtige Fehlerinformationen 13.4.2 Zeilen nummerieren 13.4.3 Einsatz der accessVBATools 13.5 Fehlerbehandlung in Formularen 13.5.1 Behandlung von Formularfehlern 13.5.2 Formularfehler dokumentieren
14.1.1 Normalisieren des Datenmodells 14.1.2 Indizes 14.1.3 Datentypen Abfragen 14.2.1 Abfragen und die ACE-Engine 14.2.2 Datenbank mit kompilierten Abfragen ausliefern 14.2.3 Gespeicherte Abfragen versus Ad-hoc-Abfragen 14.2.4 Abfragen auf Performance trimmen Formulare 14.3.1 Formulare offen halten oder schließen? 14.3.2 Daten des Formulars 14.3.3 Steuerelemente 14.3.4 VBA in Formularen Berichte 14.4.1 Datensatzquelle unsortiert übergeben 14.4.2 Keine Funktionen und Ausdrücke in Sortierungen und Gruppierungen 14.4.3 Bericht nur öffnen, wenn er Daten enthält VBA 14.5.1 Performance von VBA-Code optimieren 14.5.2 Punkt oder Ausrufezeichen 14.5.3 Datenzugriff optimieren Sonstige Performance-Tipps 14.6.1 Verwendung als .accde-Datei 14.6.2 Exklusiver Zugriff bei Einzelplatzanwendungen 14.6.3 Komprimieren der Datenbank 14.6.4 Objektnamen-Autokorrektur abschalten 14.6.5 Unterdatenblätter abschalten 14.6.6 Rechtschreibprüfung ausschalten Performance-Unterschiede messen 14.7.1 Werkzeug für Performance-Tests selbst gebaut
15 Objektorientierte Programmierung 15.1 Abstrakte Datentypen, Klassen und Objekte 15.2 Objekte 15.2.1 Eingebaute Objekte 15.2.2 Erzeugen eines Objekts 15.2.3 Zugriff auf die Methoden, Eigenschaften und Ereignisse eines Objekts 15.2.4 Lebensdauer eines Objekts 15.3 Klassenmodule 15.3.1 Anlegen eines Klassenmoduls 15.3.2 Benennen des Klassenmoduls 15.4 Eigenschaften einer Klasse 15.4.1 Öffentliche und nicht öffentliche Eigenschaften 15.4.2 Zugriff auf die Eigenschaften einer Klasse kontrollieren
15.4.3 Property Let: Setzen von skalaren Variablen 15.4.4 Property Set: Setzen von Objektvariablen 15.4.5 Property Get: Lesen von skalaren Variablen und Objektvariablen 15.4.6 Vertrauen ist gut, Kontrolle ist besser Methoden einer Klasse Standardereignisse in Klassen Benutzerdefinierte Ereignisse 15.7.1 Ereignisse abfangen 15.7.2 Eigene Ereignisse anlegen Benutzerdefinierte Auflistungen mit dem Collection-Objekt 15.8.1 Auflistungen selbst gemacht 15.8.2 Benutzerdefinierte Auflistungsklassen 15.8.3 Nachbildung relationaler Beziehungen per Auflistungsklasse 15.8.4 »Echtes« Objekt mit Auflistung Schnittstellen und Vererbung 15.9.1 Beispiel für den Einsatz der Schnittstellenvererbung 15.9.2 Vereinheitlichen per Schnittstellenvererbung 15.9.3 Realisierung der Schnittstellenvererbung 15.9.4 Was vom Beispiel übrig bleibt …
16 Objektorientierung im Praxiseinsatz 16.1 Standardfunktionen von Formularen auslagern 16.1.1 Formulare zur Datenbearbeitung 16.1.2 Codeauslagerung am Beispiel der OK-Schaltfläche 16.1.3 Abbrechen der Bearbeitung auslagern 16.1.4 Löschen von Datensätzen auslagern 16.1.5 Hinzufügen von Datensätzen auslagern 16.1.6 Einstellen des Kombinationsfeldes für die Schnellauswahl 16.1.7 Aktualisieren des Kombinationsfeldes 16.1.8 Anzeige des im Kombinationsfeld ausgewählten Datensatzes 16.1.9 Weitere Möglichkeiten 16.2 Mehrere Formularinstanzen anzeigen 16.2.1 Beispielformulare 16.2.2 Erzeugen einer neuen Instanz 16.2.3 Öffnen mehrerer Instanzen eines Formulars 16.2.4 Formularinstanz-Sammlung 16.2.5 Neue Formularinstanz erzeugen und zur Collection hinzufügen 16.2.6 Schließen aller Instanzen des Formulars 16.2.7 Schließen einer bestimmten Instanz 16.2.8 Schließen-Vorgang des Formulars anpassen 16.3 Mehrschichtige Anwendungen 16.3.1 Beispiel 16.3.2 Die GUI-Schicht 16.3.3 Die Business-Schicht 16.3.4 Die Datenzugriffsschicht 16.3.5 Die Datenschicht
Zusammenhänge der Objekte und Schichten Initialisieren des Formulars Initialisieren des Controller-Objekts Aufruf der Methode GetPersons der Business-Schicht Zugriff des Datenzugriffsobjekts auf die Datenschicht Die Klasse clsPerson Auswählen und Anzeigen eines Datensatzes Einlesen von Personen, die nicht in der Collection enthalten sind Neuer Datensatz Speichern eines Datensatzes Datensatz neu anlegen oder aktualisieren? Neuen Datensatz anlegen Aktualisieren eines Datensatzes Löschen eines Datensatzes Businesslogik und mehr Objektklassen und Datenzugriffsobjekte automatisch erstellen
17 Anpassen der Entwicklungsumgebung 17.1 Gründe für die Erweiterung der Entwicklungsumgebung 17.1.1 Automatische Codegenerierung 17.1.2 Fehlerbehandlung per Knopfdruck 17.1.3 Nummerieren von Codezeilen 17.2 Programmieren der Entwicklungsumgebung 17.3 Das Objektmodell der VBA-Entwicklungsumgebung 17.3.1 Verweis für den Zugriff auf das VBE-Objektmodell einrichten 17.3.2 Aufbau des Objektmodells 17.4 Mit Modulen arbeiten 17.4.1 Auflisten aller enthaltenen Module 17.4.2 Anlegen eines neuen Moduls 17.4.3 Entfernen eines Moduls 17.5 Mit Prozeduren arbeiten 17.5.1 Lesender Zugriff auf den Quellcode 17.5.2 Zählen der Codezeilen des Moduls 17.5.3 Zählen der Zeilen des Deklarationsbereichs eines Moduls 17.5.4 Erste Zeile und Deklarationszeile einer Prozedur 17.5.5 Zeilenanzahl einer Prozedur 17.5.6 Anzahl der Codezeilen einer Prozedur 17.5.7 Zu welcher Prozedur gehört eine Zeile? 17.5.8 Ausgabe des kompletten Codes eines Moduls 17.5.9 Ermitteln der Position der aktuellen Markierung 17.5.10 Ermitteln des Inhalts der aktuellen Markierung 17.5.11 In Modulen suchen 17.6 Beispielanwendung: Codeviewer 17.6.1 Anzeige der Module 17.6.2 Anzeige der Prozedurliste
Inhalt 17.6.3 Anzeige des Codes einer Prozedur 17.7 Manipulieren des Quellcodes 17.7.1 Code hinzufügen 17.7.2 Ereignisprozeduren hinzufügen 17.7.3 Löschen von Zeilen 17.7.4 Beispielanwendung: Nummerieren von Codezeilen in einem Modul 17.8 Toolwindows 17.8.1 Benutzerdefiniertes Toolwindow = COM-Add-In 17.8.2 Anlegen eines leeren Toolwindows 17.8.3 Anlegen eines neuen Projekts 17.8.4 Der COM-Add-In-Designer 17.8.5 Das Userdocument als Toolwindow 17.8.6 Ereignisprozeduren des COM-Add-Ins mit Leben füllen 17.8.7 Anpassen der Eigenschaften des COM-Add-Ins 17.8.8 Anzeige des Toolwindows beim Starten der VBAEntwicklungsumgebung 17.8.9 Testen des neuen Toolwindows 17.8.10 Das Toolwindow füllen 17.9 COM-Add-Ins per Menübefehl aufrufen 17.9.1 Vorbereitungen 17.9.2 Objekte hinzufügen 17.9.3 Eigenschaften der AddIn Class anpassen 17.9.4 Anpassen des Standardmoduls 17.9.5 Weitere Einstellungen 17.9.6 Projekt speichern 17.9.7 Hinzufügen der Funktionen und Menüs
18 Sicherheit von Access-Datenbanken Code schützen per .accde-Datenbank Code schützen per Kennwort Einfacher Kennwortschutz mit Verschlüsselung Vertrauensstellungscenter Digitale Signaturen Schutz vor bösartigen SQL-Statements Kein Sicherheitssystem — was nun? 18.7.1 Benutzer- und gruppenabhängige Benutzeroberfläche 18.7.2 Daten schützen: Alternativen 18.8 MySQL 18.8.1 MySQL installieren 18.8.2 Einfache Konfiguration 18.8.3 MySQL-Anweisungen 18.8.4 Sicherheit unter MySQL 18.8.5 Administrationstool für MySQL 18.8.6 Installation von MyODBC 18.9 Access und MySQL
Upsizing von Access-Datenbanken auf MySQL Export von Tabellen nach MySQL Verwenden von MySQL-Datenbanken mit Access Aktualisieren von Tabellen Internetverbindung mit MySQL Erstellen eines Profils mit PuTTY Testen des Tunnels
19 Installation, Betrieb und Wartung 19.1 Verschiedene Access-Versionen auf demselben Rechner 19.2 Weitergabe von Access-Datenbanken 19.2.1 Benutzerdefinierte Menüs 19.2.2 Fehlerbehandlung 19.2.3 Runtime-Simulation 19.2.4 Weitergabe ohne Runtime 19.3 Aktionen beim Starten oder Beenden der Datenbank durchführen 19.3.1 Code beim Starten einer Datenbank ausführen 19.3.2 Formular beim Starten einer Datenbank anzeigen 19.3.3 Aktion beim Schließen einer Datenbank ausführen 19.4 Datenbanken komprimieren und reparieren 19.5 Mehrbenutzerbetrieb mit Access-Datenbanken 19.5.1 Aufteilen einer Access-Datenbank 19.5.2 Tabellen in neue Datenbank importieren 19.5.3 Tabellen aus der Ausgangsdatenbank löschen 19.5.4 Tabellen als Verknüpfung einbinden 19.5.5 Erneutes Einbinden der Tabellen nach Umbenennen oder Verschieben des Backends 19.5.6 Zeitpunkt zum Wiedereinbinden von Tabellen 19.6 Sichern von Access-Datenbanken 19.6.1 Voraussetzungen und Vorbereitungen 19.6.2 Einfaches Kopieren mit FileCopy 19.6.3 Kopieren per API-Funktion 19.6.4 Kopieren und komprimieren 19.6.5 Kopieren und zippen 19.6.6 Sicherungsstrategie 19.7 Datenbank reparieren 19.7.1 Symptome 19.7.2 Sicherung geht vor 19.7.3 Allgemeine Reparaturversuche 19.7.4 Weitere Informationen 19.8 Verweise und Probleme mit Verweisen 19.8.1 Meldung bei fehlenden Verweisen 19.8.2 Ohne Verweise arbeiten? 19.8.3 Late Binding und Early Binding 19.8.4 Verweise und die Weitergabe von Anwendungen 19.8.5 Auf Nummer Sicher
Gleichnamige Objekte, Eigenschaften und Methoden in Bibliotheken
986
987
25
Vorwort Sie arbeiten mit Access? Und Sie glauben, Access sei was Besseres? Nun, Sie haben Recht. Ich kann mir keine Software vorstellen, mit der man so viel anstellen kann wie mit diesem Programm. Zuallererst können Sie damit natürlich Daten verwalten, und zwar bis zu zwei Gigabyte! Und wenn das nicht reicht, dann hängen Sie einfach ein anderes Backend an wie den MS SQL Server 2005 Express Edition oder MySQL. Die Menge der Daten ist also nicht ausschlaggebend, sondern vielmehr das »Wie«. Und dann die Entwicklungsumgebung: Es gibt kaum eine Konkurrenz zu Access, was das schnelle Erstellen von Benutzeroberflächen für den Zugriff auf die Daten in relationalen Datenbanken angeht. Sie erhalten mit einem Mausklick ein an eine Tabelle oder Abfrage gebundenes Formular – und einen Bericht erstellen Sie genauso schnell. Und mit Access 2007 wird das alles noch einfacher: Gerade für Einsteiger und Quick-and-Dirty-Anwendungen liefert diese Version von Access sehr hilfreiche Tools. Aber Vorsicht: Für eine schnell erstellte Anwendung mit geringer Halbwertszeit reicht dies aus, aber wenn AccessNeulinge, möglicherweise von Microsoft durch die vielen Neuerungen sanft von Excel in Richtung Access bugsiert, die Vorlagen und Assistenten ausgereizt haben und ihre Datenbank in eine professionelle Anwendung umwandeln wollen, stehen diese schnell auf dem Schlauch. Da fehlen die grundlegenden Kenntnisse der Datenmodellierung, um die zusammengetragenen Daten in ein relationales Datenmodell zu überführen, die Automation von For mularen funktioniert nicht wie gewünscht, weil den von
Vorwort
Microsoft in dieser Auflage gepuschten Makros natürliche Grenzen gesetzt sind und irgendwann soll man dann die Daten auch noch auf einen SQL-Server portieren – nein, da hört der Spaß auf. Wer auch weiterhin Freude mit seiner Datenbankanwendung haben oder gleich ein wenig professioneller an die Sache herangehen will, findet im vorliegenden Buch die richtigen Informationen dafür. Eine Vorstellung der einzelnen Kapitel möchte ich Ihnen ersparen. Bei dem zur Verfügung stehenden Platz würde eine solche Zusammenfassung allzu oberflächlich ausfallen. Schauen Sie doch stattdessen einfach einmal ins Inhaltsverzeichnis und suchen Sie sich die Bereiche heraus, die Sie besonders interessieren. Ein ganz wichtiges Kapitel ist jedoch das erste, denn es stellt alle wichtigen Neuerungen von Access 2007 vor und gibt Hinweise darauf, welche sich als nützlich erweisen können. Zu Letzteren finden Sie natürlich in den jeweiligen Kapiteln ausführliche Informationen. Dieses Buch können Sie in einem Rutsch lesen (lachen Sie nicht: viele Leser des Vorgängerbuchs, des Access 2003 Entwicklerbuchs, haben das getan) oder Sie legen es neben Ihren Rechner, damit Sie darin lesen können, wenn Sie Informationen zu einem der oben genannten Themen benötigen. Damit sich das Buch auch als Nachschlagewerk eignet, ist besonders viel Arbeit in die Erstellung des Index geflossen.
Ist dieses Buch das Richtige für Sie? Ein Buch wie dieses kostet auf den ersten Blick eine Menge Geld, aber wenn Sie es gebrauchen können, zahlt es sich schnell aus. Damit Sie keine unnötige Investition tätigen, kurz einige Voraussetzungen, wann dieses Buch für Sie geeignet ist: Sie kennen sich mit der Benutzeroberfläche von Access aus, wissen, wie Sie die einzelnen Objekte anlegen, und wollen nun richtig einsteigen. Sie haben schon ein Access-Grundlagenbuch gelesen und brauchen genauere Informationen, um professionelle Anwendungen zu entwickeln. Sie haben mit einem anderen Datenbanksystem gearbeitet und möchten sich nun auf hohem Niveau in Access einarbeiten. Wenn Sie mit diesem Buch aber erst in die Entwicklung von Access-Datenbanken einsteigen wollen, seien Sie gewarnt: Sie finden hier keine Grundlagen zu Access, sondern für Access-Entwickler, und das ist etwas völlig anderes. In diesem Fall sollten Sie sich zunächst anhand eines geeigneten Einsteigerbuchs in die Benutzeroberfläche von Access und die Grundlagen der einzelnen Objekte einarbeiten.
28
Vorwort
Die Access-Trilogie Natürlich ist dieses Buch nicht das Ende der Fahnenstange: Sie finden hier etwa keine Informationen über die Nutzung von Excel und Word als Reporting-Tools, Sie lernen nicht, wie Sie ein komplettes Software-Projekt aufziehen, und Sie finden auch keine komplette Beispielanwendung wie in anderen Büchern, die sich wie ein roter Faden durch das ganze Buch zieht. Dafür gibt es mehrere Gründe: Erstens sind die in diesem Buch enthaltenen Themen so wichtig und umfangreich, dass die fehlende Thematik nur oberflächlich hätte behandelt werden können. Eine durchgängige Anwendung in einem Buch wie diesem, das in vielen Bereichen sehr in die Tiefe geht, hätte wohl recht konstruiert gewirkt. Und der letzte und ausschlaggebende Punkt ist: Es muss ja nicht alles in einem einzigen Buch enthalten sein. Deshalb gibt es abgestimmt auf dieses Buch noch zwei weitere Werke (Erscheinungstermin und Informationen siehe http://www.access-entwicklerbuch.de): »Access 2007 – Das Praxisbuch für Entwickler« zeigt am Beispiel einer kompletten Anwendung (Dokumentenverwaltung), wie Sie ein Projekt von Anfang bis Ende bestreiten: Es zeigt, was beim Aufnehmen der Anforderungen beim Kunden wichtig ist und wie Sie den Aufwand abschätzen, erklärt, warum Sie kein Pflichtenheft erstellen, sondern mit Prototyping arbeiten sollten, wie Sie aus den Anforderungen ein Konzept und einen Prototypen erstellen und wie daraus schließlich die fertige Anwendung wird. Und da es auf dem vorliegenden Buch aufsetzt, finden Sie dort auch einiges aus der Trickkiste der Access-Entwickler. Und nun zum dritten Teil der Trilogie: »Access – Lösungen für Selbstständige« bietet zehn Access-Lösungen mit Erläuterung des Datenmodells, der Benutzeroberfläche und der verwendeten Techniken. Darauf aufbauend können Sie die Lösungen Ihren eigenen Anforderungen anpassen und dabei natürlich auch die im vorliegenden Buch beschriebenen Techniken einsetzen.
Dankeschön Sascha Trowitzsch hat sich erneut als sorgfältiger und kreativer Fachlektor bewährt und wird beim »Access 2007 – Das Praxisbuch für Entwickler« als Autor mit im Boot sein. Rita Klingenstein bügelte gewohnt zuverlässig die letzten sprachlichen Ungereimtheiten und Fehler aus. Beiden vielen Dank für die gute Zusammenarbeit, trotz meiner eigenwilligen Terminauslegung. Bei Addison-Wesley standen mir die Lektorin dieses Buchs, Sylvia Hasselbach, und Martha Kürzl-Harrison, verantwortlich für die Herstellung, jederzeit mit Rat und Tat zur Seite und erwiesen sich als sehr flexible und für Anregungen offene Partner. Einige Kapitel dieses Buchs konnten vor der Fertigstellung heruntergeladen und begutachtet werden; für die Anregungen bedanke ich mich bei allen Beteiligten (ich hoffe, ich
29
Vorwort
habe keinen vergessen ...). Danke auch an Günther Kramer, der in seinem Forum unter http://www.ms-office-forum.de Platz für die Diskussion der vorab veröffentlichten Kapitel eingeräumt hat. Das größte Dankeschön geht an meine drei Süßen, die mich zwischendurch immer wieder ins »normale Leben« zurückgeholt haben und mir nach Fertigstellung des Buches hoffentlich bei der Resozialisierung zur Seite stehen.
Weitere Informationen Wenn Sie in diesem Buch sachliche Fehler finden (was ich nicht hoffe), teilen Sie mir diese einfach unter [email protected] mit. Ich veröffentliche entsprechende Korrekturen unter http://www.access-im-unternehmen.de. Dort finden Sie auch die auf der CD enthaltenen Beispiele zum Download.
André Minhorst
30
1 Warum Access 2007? Alle paar Jahre wirft Microsoft eine neue Office-Version auf den Markt, und dann schreien alle auf – die einen vor Freude, die anderen vor Enttäuschung. Nun, das ist vielleicht etwas übertrieben, aber es gibt jedes Mal eine ganze Reihe Wortmeldungen – überwiegend von Menschen, die die neuen Features besonders rühmen oder völlig ablehnen. Ein Buchautor sollte da vorsichtig sein: Werden im ersten Kapitel, das klassischerweise die aktuelle Access-Version vorstellt, alle Neuerungen über den grünen Klee gelobt, macht er sich möglicherweise unglaubwürdig. Alle Neuheiten niedermachen? Das geht natürlich auch nicht, denn dann kauft keiner diese Version – und somit braucht auch keiner ein Buch dazu. Glücklicherweise liegt der Autor bei dieser neuen Version Access 2007 richtig, wenn er sie weder übertrieben lobt noch tadelt. Das neue Access bietet nämlich eine recht durchwachsene Auswahl neuer Funktionen. Manche wirken auf den ersten Blick genial und entpuppen sich auf den zweiten als nutzlos, bei anderen ist es umgekehrt. Und schließlich kommt es auch noch auf die Benutzergruppe an. Ein Quick-and-Dirty-Datenbankbastler oder ein Quer einsteiger (klassischerweise aus der Excel-Welt) findet eine ganze Reihe scheinbarer Erleichterungen, und gerade diese hat Microsoft auch am meisten hervorgehoben. Die Version scheint also mehr für die Ausweitung dieser Zielgruppe gedacht zu sein. Auch die Entwicklergemeinde
Kapitel 1
hat die neue Version natürlich sehr genau inspiziert, zeigt sich aber überwiegend enttäuscht. Das liegt zum größten Teil daran, dass wieder einmal wenig Rücksicht auf die von den Entwicklern geäußerten Wünsche genommen wurde. Und wenn das, was man sich von einer neuen Version erhofft, nicht ansatzweise erfüllt wird, fällt es natürlich schwer, sich mit Verbesserungen an anderer Stelle zufriedenzugeben. Dieses Buch wird jedoch zeigen, wie auch Entwickler von den Neuerungen in Access 2007 profitieren können. Das erste Kapitel soll die neuen Features nicht nur vorstellen, sondern auch ihren Nutzen für die unterschiedlichen Benutzergruppen beurteilen und Ihnen damit ermöglichen, die Vor- und Nachteile der neuen Version von Access einzuschätzen.
1.1 Die Benutzeroberfläche Eigentlich sollte dieses Kapitel übrigens »Neues in Access 2007 und Benutzeroberfläche heißen (was ja teilweise identisch ist): Wenn man sich als Access-Benutzer das erste Mal mit Access 2007 beschäftigt, findet man nämlich zunächst gar nichts mehr wieder. Es scheint, als habe Microsoft alle Elemente, Menüs, Optionen, den Dialog und das Datenbankfenster in einen großen Topf geworfen und dann wieder alles hervorgeholt und neu sortiert. Nur das Datenbankfenster, das liegt immer noch darin. Vielleicht angelt es ja noch jemand heraus ... Doch dazu gleich mehr. Zuerst sollen Sie erfahren, warum die Überschrift dieses ersten Kapitels nun »Warum Access 2007?« lautet. Zunächst sollte dieses Kapitel neben einer kurzen Beschreibung der Neuerungen vor allem die Benutzeroberfläche vorstellen, damit Sie sich nach der Lektüre des ersten Kapitels schon gut in der neuen Version von Access zurechtfinden. Es waren Tabellen geplant, die Ihnen übersichtlich zeigen sollten, welcher Menübefehl nun in welche Ribbon-Leiste gewandert ist, wo die Optionen im neuen Optionen-Dialog zu finden sind, wie Sie den Navigationsbereich am besten steuern, und dazu eine umfangreiche Einführung in die Konzepte von Ribbon, Navigationsleiste und Office-Menü. Nach ein paar Stunden Einarbeitung erschien dem Autor die neue Benutzeroberfläche dann in einem anderen, positiven Licht: Das Ribbon (so heißt nun der Ersatz für Menü- und Symbolleisten, aber das wissen Sie ja bereits) ist – bis auf einige Ausreißer – recht ordentlich sortiert und bietet teilweise bessere Möglichkeiten als die alten Menüleisten. Der Dialog mit den Access-Optionen enthält nun doch alle Optionen, die im alten Optionen-Dialog beheimatet waren. Und teilweise sogar besser sortiert – so gibt es keinen eigenen Dialog für die Start-Optionen mehr, sondern nur noch einen Bereich in den Access-Optionen namens Aktuelle Datenbank.
32
Warum Access 2007?
Zum Navigationsbereich – nun, wenn Sie gewohnt sind, viele Objekte in einer Datenbank zu halten und diese in einem beliebig vergrößerbaren Datenbankfenster in mehreren Spalten anzuzeigen, müssen Sie sich hier schon sehr umstellen. Es gibt nur eine einzige Spalte. Aber der Navigationsbereich bietet auch Vorteile, die Sie schnell schätzen lernen: beispielsweise das Suchen-Fenster zum schnellen Auffinden von Datenbankobjekten, und weitere. Arbeiten Sie einfach ein paar Stunden mit Access 2007 und Sie werden sich von selbst zurechtfinden. Das ist so, als sei Ihr Lieblingssupermarkt völlig umgebaut worden: Bei den ersten zwei, drei Besuchen dauert die Suche einige Zeit und es lässt sich vielleicht nicht einmal alles finden. Beim nächsten Mal geht’s schon erheblich besser. Und von diesem Gewöhnungseffekt einmal abgesehen, dieses Buch ist für AccessEntwickler (und nicht für Assistenten-Junkies) gedacht. Eine Beschreibung der Benutzeroberfläche wäre da recht unpassend. Doch nun soll es endlich losgehen: Schritt für Schritt lernen Sie die Neuerungen von Access 2007 kennen und auch die Einschätzung des Autors. Nach einer kurzen Vorstellung der jeweiligen Neuheit wird auf das entsprechende Kapitel verwiesen. Was sich nicht vernünftig in die geplante Struktur des Buchs einordnen ließ, beschreibt dieses erste Kapitel bereits etwas ausführlicher, wie zum Beispiel die Neuerungen im Bereich Makros, weil sie der Autor nicht unbedingt als Werkzeug für professionelle Entwickler ansieht und somit diesem Thema kein eigenes Kapitel widmete. Und seien Sie nachsichtig, da nicht jede Neuerung mit einem Screenshot versehen ist – diese können Sie am Bildschirm viel besser »life« betrachten.
1.1.1 Navigationsbereich statt Datenbankfenster Statt des Datenbankfensters gibt es jetzt den so genannten Navigationsbereich. Dieser klebt am linken Rand, kann sich ganz schmal machen und zeigt in einer einspaltigen Liste alle Elemente der Datenbank an – je nach Einstellung mal mehr, mal weniger. Dabei gibt es verschiedene Einteilungsmöglichkeiten: nach Objekttyp, nach Tabellen und nach den damit in Zusammenhang stehenden Objekten (also etwa tblArtikel, qryArtikel, frmArtikel), nach Erstellungs- und Änderungsdatum oder benutzerdefiniert – das entspricht ungefähr den »Gruppen« aus dem alten Datenbankfenster. Es gibt außerdem verschiedene Filter, mit denen Sie etwa nach den einzelnen Objekttypen filtern können.
Vorteil: Mehrfachauswahl Eine nützliche Neuerung ist die Mehrfachauswahl, mit der Sie beispielsweise mehrere Objekte gleichzeitig markieren und löschen, kopieren oder ausblenden können.
33
Kapitel 1
Vorteil: Suche Mit der Tastenkombination Strg + F zeigen Sie am oberen Rand des aktivierten Navigations bereichs ein Textfeld zur Eingabe eines Suchbegriffs an. Der hier eingegebene Such begriff wirkt wie ein Filter und aktualisiert bei jeder Änderung die angezeigten Objekte. Dabei durchsucht der Filter die kompletten Objektnamen nach einem Vorkommen des Suchbegriffs. Kleiner Tipp: Wenn Sie Ihre Objekte nach einer bestimmten Konvention (etwa nach Reddick) benennen und beispielsweise alle Tabellen mit tbl beginnen, können Sie den Filter prima zum schnellen Anzeigen bestimmter Objekttypen nutzen – vielleicht sogar schneller als die Auswahl der passenden Kategorien aus dem Menü der Navigations leiste.
Vorteil: Optionen Die Optionen zum Navigationsbereich können Sie über das Kontextmenü der Titelleiste des Navigationsbereichs über den Eintrag Navigationsoptionen... auswählen. Dort finden Sie etwa die Möglichkeit, Systemobjekte wie die MSys...-Tabellen oder die für benutzerdefinierte Ribbons wichtige Tabelle USysRibbons sichtbar zu machen. Sie können hier auch festlegen, ob Sie Objekte mit einem einfachen oder doppelten Mausklick öffnen möchten.
Nachteil: Klicks zum Objekt Im Datenbankfenster brauchten Sie in der Regel maximal zwei Klicks, um das gewünschte Objekt zu öffnen: die Auswahl der Registerseite und der Klick auf das Objekt. Wenn Sie viele Objekte eines Typs hatten, mussten Sie vielleicht noch mal ein wenig scrollen. Je nachdem, welche Kategorie und welche Kategorie, Sortierung und Filter Sie nun im Navigationsfenster aktiviert haben, brauchen Sie da nun schon ein wenig länger. Zumal sich längst nicht so viele Objekte eines Typs gleichzeitig anzeigen lassen wie im Datenbankfenster.
Nachteil: Sortierung Die Objektliste im Navigationsbereich lässt sich über passende Kontextmenüeinträge auch nach anderen Kriterien als nur nach dem Namen sortieren. Das nutzt allerdings nur wenig, wenn man neben dem Namen nicht gleichzeitig etwa das Änderungsdatum anzeigen kann. Dazu müssen Sie erst die Ansichtsart unter Anzeigen nach|Details aktivieren. In dieser Ansicht passen dann jedoch gerade noch 14 Einträge auf einem 19-Zoll-Monitor in die Liste – das ist sicher ein Grund, diese Ansicht nur selten zu verwenden.
34
Warum Access 2007?
1.1.2 Ribbon statt Menü- und Symbolleisten Statt mit Menü- und Symbolleisten arbeiten Sie in Access 2007 mit dem Ribbon. Die deutsche Bezeichnung lautet Multifunktionsleiste; dieses Buch verwendet allerdings durchgängig die Bezeichnung »Ribbon«. Das Ribbon besteht nicht nur aus den per Registerreiter erreichbaren Leisten, sondern auch aus dem Office-Menü der Schnellzugriffsleiste und mehr – warum man das alles unter dem Begriff Ribbon zusammenfassen kann, erfahren Sie in Kapitel 12, »Ribbon«. Warum Ribbon im Singular steht, lernen Sie im gleichen Zuge. Doch zunächst zu den »Leisten«, den so genannten »Tabs«: Es gibt vier feste, immer sichtbare Tabs und einige kontextabhängige weitere Tabs. Diese blenden sich in Zusammenhang mit verschiedenen Objekten und Ansichten ein und bieten passende Funktionen an. Das Office-Menü – zu öffnen mit der runden Schaltfläche links oben – enthält Befehle zum Öffnen, Anlegen und Verwalten von Datenbanken und bietet außerdem eine Schaltfläche zum Anzeigen der Access-Optionen.
Vorteil Steuerelementvielfalt Das Ribbon bietet gegenüber Menüleisten eine Vielzahl an Steuerelementen. Um den Rahmen dieses Abschnitts nicht zu sprengen, seien Sie auf die selbstständige Erkundung des Ribbons oder auf oben genanntes Kapitel verwiesen.
Vorteil Optik Es ist zwar Geschmackssache, aber die neuen gestaltungstechnischen Möglichkeiten sind denen der alten Menü- und Symbolleisten überlegen. Und wer die passenden Icons zur Verfügung hat, kann der Benutzeroberfläche einer Anwendung eine richtig attraktive Ribbon-Leiste verpassen.
Vorteil Tastensteuerung Beim Betätigen der Alt-Taste blendet Access sofort einige Buchstaben im Ribbon zur Auswahl der aktuell angezeigten Ribbon-Tabs an. Nach deren Betätigung erscheinen dann die Buchstaben, die Sie zum Ausführen der einzelnen Befehle verwenden müssen. Standardkombinationen wie etwa zum Anlegen neuer Tabellen (Alt + L, T, W) hat man schnell drauf und kann so den Einsatz der Maus reduzieren. Die von älteren Versionen bekannten Tastenkombinationen zum Anzeigen des Datenbankfensters (jetzt Navigationsbereich, F11), der Onlinehilfe (F1) oder zum Anzeigen des Eigenschafts fensters (F4) sind weiterhin vorhanden.
35
Kapitel 1
Vorteil: Menü- und Symbolleisten alter Versionen weiter verwendbar Nicht auf den ersten Blick ersichtlich, aber doch möglich: Sie können Menü- und Symbolleisten bestehender Anwendungen unter Access 2007 weiter verwenden, aber nur für Datenbanken im .mdb-Format. Mehr dazu in Kapitel 12, »Ribbon«.
Nachteil: Platzbedarf In älteren Access-Versionen konnten Sie nach Belieben weitere Symbolleisten zusätzlich zu den aktuellen Symbolleisten und zur Menüleiste anzeigen. Das Ribbon jedoch zeigt immer nur einen Tab an, dies allerdings recht flexibel: Wenn Sie etwa das AccessFenster verkleinern, blendet Access die Symbole der Elemente aus und zeigt nur noch die Bezeichnungen an. Dennoch: In vielen Fällen reicht eine Leiste einfach nicht aus und man muss im Ribbon hin- und herblättern, um alle gewünschten Funktionen zu erreichen.
Nachteil: Unlogische Aufteilung der Funktionen Manche Befehle befinden sich allerdings an unerwarteten Stellen. Die Funktionen zum Verankern von Steuerelementen in der Entwurfsansicht eines Formulars etwa im Ribbon unter Anordnen|Schriftgrad unterzubringen, ist eine Zumutung (genauso wie Befehle zum Anpassen der Steuerelementgröße).
Nachteil: Fehlender Editor Um das Ribbon benutzerdefiniert anzupassen, müssen Sie ein passendes XML-Dokument bereitstellen. Es gibt keinen Editor wie den Menü-Editor älterer Access-Versionen mehr. Möglicherweise liefert Microsoft ja ein passendes Tool nach, einige Dritthersteller bieten bereits Beta-Versionen selbst programmierter Ribbon-Tools an.
Nachteil: Anlegen neuer Objekte nur im Ribbon Das alte Datenbankfenster brachte direkt die zum Anlegen der einzelnen Objekte (mit und ohne Assistenten) notwendigen Steuerelemente mit. In Access 2007 finden Sie diese Funktionen nun im Erstellen-Tab des Ribbons.
1.1.3 Neuer Optionen-Dialog Der neue Dialog namens Access-Optionen wirkt aufgeräumter und besser strukturiert. Hier gilt: Früher oder später finden Sie die gewohnten Optionen auch hier wieder, au-
36
Warum Access 2007?
ßer denen zum Anzeigen verborgener Tabellen oder von Systemtabellen (siehe weiter oben). Übrigens: Unter »Häufig verwendet« finden Sie einen Bereich namens »Die beliebtesten Optionen bei der Arbeit mit Access«. Schauen Sie dort einmal herein. Wenn Sie eine Vermutung haben, wer diese ermittelt hat, schicken Sie eine E-Mail an info@ access-entwicklerbuch.de.
1.1.4 Neuer Startbereich Fast vergessen: Access 2007 kommt mit einem neuen Startbereich à la Visual Studio 2005. Dort können Sie eine der (zum Zeitpunkt der Drucklegung dieses Buches) nicht sehr zahlreichen Vorlagen auswählen, die Sie als professioneller Entwickler sowieso nicht verwenden. Die übrigen Funktionen zum Anlegen einer neuen und zum Öffnen einer bestehenden Datenbank sind ebenfalls vorhanden.
1.1.5 Neue Onlinehilfe Die neue Onlinehilfe ist – zumindest was den Stand zum Zeitpunkt der Drucklegung dieses Buches angeht und in Bezug auf Dokumentation und Beispiele zu den VBANeuerungen – eine kleine Katastrophe. Da sie ihre Inhalte teilweise online bezieht, bleibt zu hoffen, dass mit der Zeit nicht nur Einsteiger Gefallen daran finden ... Die neue Onlinehilfe hat im Übrigen die unangenehme Eigenart, sich standardmäßig sehr in den Vordergrund zu drängen. Dem beugen Sie am besten vor, indem Sie einmal auf die in der Symbolleiste befindliche Pin-Nadel klicken. Die Onlinehilfe verhält sich dann wie jedes andere normale Fenster. Eine nette Neuerung liefert die Onlinehilfe aber doch: Immerhin merkt sie sich die letzten 15 Suchbegriffe und bietet diese per Kombinationsfeld an.
1.2 Neues Datenbankformat Access 2007 führt ein neues Datenbankformat ein. Die neuen Dateiendungen lauten .accdb statt .mdb, .accde statt .mde, .accda statt .mda und .accdb statt .ldb. Außerdem gibt es mindestens drei neue Dateiendungen: .accdt kennzeichnet AccessVorlagen (Templates), auf deren Basis Sie komplett neue Datenbanken erstellen können. Einige Beispiele liefert Microsoft direkt mit, dabei handelt es sich um die auf der Startseite von Access angezeigten Datenbanken wie Posten, Kontakte oder Probleme. Sie finden die passenden .accdt-Dateien im Verzeichnis c:\Programme\Microsoft Office\ Templates\1031\Access. Dateien mit der Endung .accdt sind übrigens komprimiert und enthalten Dateien im Office Open XML-Format, das zum Beispiel mit Winzip entpackt oder mit speziellen Anwendungen eingesehen werden kann. Dieses Format ist
37
Kapitel 1
in der Microsoft‘schen Fassung so komplex (circa 5.000 Seiten Spezifikation), dass ein Analysieren und Reproduzieren in Form eigener Templates ohne weitere Hilfe wohl sehr zeitaufwändig und kaum zu realisieren wäre. Die Dateiendung .accfl weist auf eine Liste von Feldbeschreibungen hin, die als Grund lage für eine Liste von Feldern dient, die Access Ihnen beim Entwerfen von Tabellen in der Datenblattansicht über den Ribbon-Eintrag Datenblatt|Neues Feld zum Hinzufügen anbietet. Eine Dateiendung hat es übrigens gar nicht in die neue Version geschafft: Die .mdwDatei, Garant für nicht hundertprozentige Sicherheit, erlebt keine Renaissance in Form einer .accdw-Datei – mehr dazu weiter unten. Schließlich können Sie eine Datenbankdatei in eine Datei mit der Endung .accdr umwandeln. Wenn Sie diese per Doppelklick öffnen, startet Access im Runtime-Modus, das heißt, in dem Modus, den auch der Benutzer einer Access-Runtime-Version zu Gesicht bekommt. Damit können Sie gut simulieren, wie die Anwendung bei Benutzern läuft, die kein Access 2007 installiert haben und denen Sie daher die Runtime-Version von Access mitliefern.
1.3 Runtime gratis Und weil es hier gerade passt: Kurz vor Fertigstellung dieses Buchs hat Microsoft veröffentlicht, dass es die Access 2007-Runtime mit einem Weitergabe-Tool kostenlos bereitstellen wird. Wenn man bedenkt, dass diese Tools für Access 2003 zusammen mit einigen anderen Anwendungen viele hundert Euro gekostet haben, ist dies ein guter Grund zum Umsteigen.
1.4 Goodbye Sicherheitssystem Das alte Sicherheitssystem von Access, das ohnehin keinen wirklich sicheren Schutz für die Objekte und den Inhalt Ihrer Datenbanken lieferte, fällt ersatzlos weg. Damit nimmt Microsoft nicht nur den Entwicklern, die dieses System zumindest als hohe Hürde für Otto Normalbenutzer eingesetzt haben, sondern auch denen, die damit die Benutzer einer Anwendung verwaltet haben, um das Frontend mit benutzerabhängigen Funktionen auszustatten, ein wichtiges Werkzeug weg. Letztere dürfen nun selbst geeignete Funktionen nachprogrammieren, was auch kein Beinbruch sein dürfte – ein paar Tabellen für die Verwaltung der Benutzer, Benutzergruppen und ihrer Zuordnung und ein eigener Anmeldedialog fallen an; den
38
Warum Access 2007?
Rest, wie etwa die Ermittlung des aktuellen Benutzers und die Bereitstellung der dafür vorgesehenen Elemente der Benutzeroberfläche, musste man ja ohnehin programmieren. Und was das Thema Sicherheit angeht, verweist Microsoft primär auf den Microsoft SQL Server 2005 und dessen kostenloses Pendant namens Microsoft SQL Server 2005 Express Edition. Die Access-Projekte wurden leicht für den Einsatz mit dem SQL Server 2005 angepasst, aber Microsoft empfiehlt den Einsatz von ODBC für den Zugriff auf den Microsoft SQL Server. Nähere Informationen zum Thema Sicherheit erhalten Sie in Kapitel 18, »Sicherheit von Access-Datenbanken«. Dort finden Sie unter anderem Hinweise auf den Einsatz von MySQL als SQL-Server.
1.5 Neue Sicherheitsfunktionen Es gibt auch einige wenige neue Funktionen zur Verbesserung der Sicherheit – allerdings nur teilweise datenzentriert.
Verbesserter Kennwortschutz Den reinen .accdb-Anwendungen hat Microsoft einen verbesserten Kennwortschutz und eine damit einhergehende Verschlüsselung der enthaltenen Daten spendiert. Das Datenbank-Kennwort dient dabei als Key für die Verschlüsselung.
Vertrauen ist gut, ... Nach wie vor können Sie das Ausführen von VBA in nicht vertrauenswürdigen Daten banken in den Access-Optionen deaktivieren beziehungsweise die Sicherheitseinstellun gen anpassen. Zusätzlich zu der Möglichkeit, von vertrauenswürdigen Quellen signierte Datenbanken immer mit vollem Funktionsumfang zu öffnen, können Sie nun auch eines oder mehrere Verzeichnisse angeben, deren Datenbanken immer mit vollem Funktionsumfang geöffnet werden können.
Makros in a (Sand-)Box Neben einigen anderen Verbesserungen am Makro-Objekt und dessen Funktionsumfang können Sie diese nun in jedem Fall einsetzen, um die aktuellen Sicherheitseinstellungen in Zusammenhang mit der geöffneten Datenbankanwendung abzufragen und den Benutzer mittels Meldung darauf hinzuweisen, dass er gegebenenfalls auf einige
39
Kapitel 1
Funktionen verzichten muss, weil seine Sicherheitseinstellungen diese unterbinden – mehr dazu ebenfalls in Kapitel 18, »Sicherheit von Access-Datenbanken«.
1.6 Bye-bye Replikation Auch die Replikation hat es – laut Microsoft mangels Verbreitung – nicht in die neue Version (.accdb) geschafft. Access-basierten Ersatz gibt es nicht. Die Replikation von .mdbDatenbanken ist mit Access 2007 allerdings weiterhin möglich.
1.7 Adieu Datenzugriffsseiten Die DAPs (Data Access Pages oder zu deutsch Datenzugriffsseiten) sind ebenfalls rausgeflogen. Wenn man den Wortmeldungen im Usenet und in den Foren glaubt, haben wohl auch nicht viele Entwickler damit gearbeitet. Alternative: Selbst programmierte Webfrontends mit einer Programmiersprache nach Wahl. Dies ist aber kein Access-Thema mehr, denn das Ablegen einer .mdb- oder .accdbDatei als Datenbank für eine Internatanwendung macht nur in den wenigsten Fällen Sinn. Dann doch lieber ein SQL-Server ...
1.8 ACE — die neue Jet-Engine Die alte Jet-Engine kommt in Access 2007 nicht mehr zum Einsatz. Das AccessEntwicklerteam hat einen eigenen Ableger davon erhalten und entwickelt diesen nun unter dem Namen ACE weiter. Änderungen gibt es kaum – bis auf wenige für die neuen Datentypen, mehr dazu weiter unten. Die ACE ist voll abwärtskompatibel zur Jet-Engine.
1.9 MDI vs. SDI oder »Jedem Objekt sein Register« Standardmäßig zeigt Access jedes geöffnete Objekt in der neuen SDI-Ansicht (Single Document Interface) auf jeweils einer eigenen Seite in einem Register an. Das ist sicher eines der gewöhnungsbedürftigsten neuen Features, trägt aber auf gewisse Weise auch zur besseren Übersicht bei. Wer das nicht mag, kann in den Access-Optionen allerdings auf die alte MDI-Ansicht umschalten (Access-Optionen|Aktuelle Datenbank|Dokumentfensteroptionen|Überlappende Fenster).
40
Warum Access 2007?
1.10 Neuer Datentyp: Anlagefeld Im Anlagefeld können Sie Dateien und damit auch Bilder speichern. Mit dem passenden Steuerelement zeigen Sie im Formular Bilder an und bieten die Möglichkeit, neue Bilder hinzuzufügen, bestehende Bilder zu löschen, diese auf der Festplatte zu speichern oder – und das gilt für alle gespeicherten Dateien – Sie öffnen diese per Doppelklick in der passenden Anwendung (diese Funktionen stehen auch in der Datenblattansicht von Tabellen und Abfragen zur Verfügung, aber dort bearbeitet man ja keine Daten ...). Bilddateien werden standardmäßig im Format der ursprünglichen Datei gespeichert, Sie können aber in den Access-Optionen auch die in älteren Versionen verwendete Konvertierung in Bitmaps erzwingen. In Berichten können Sie mit dem AnlageSteuerelement ebenfalls Bilder anzeigen. Der Hasenfuß bei der Sache ist, dass Sie mehrere Dateien pro Anlage-Feld speichern können, aber keinen Einblick in die dahinterstehende Struktur haben. Anscheinend exis tiert jedoch mindestens eine interne, verborgene Tabelle, die die Dateien speichert. Viel mehr zu dem neuen Datentyp erfahren Sie in Kapitel 2, »Tabellen und Datenmodel lierung«; das passende Steuerelement beleuchtet Kapitel 11, »Bilder und binäre Dateien in Access«. Dort erfahren Sie auch, warum das gute alte OLE-Feld trotz des neuen Anlage-Felddatentyps keinesfalls ein Auslaufmodell ist. Natürlich können Sie auch per SQL (siehe Kapitel 8, »SQL«) oder VBA (siehe Kapitel 9, »DAO«) auf die in einem Anlage-Feld gespeicherten Daten zugreifen.
Komprimierung nach Wunsch Anlage-Felder und auch OLE-Felder speichern Bilder standardmäßig komprimiert, soweit es sich nicht schon um ein komprimiertes Bildformat wie .jpg handelt. Diese Einstellung können Sie in den Access-Optionen unter Aktuelle Datenbank|Bildeigenschaf ten-Speicherformat vornehmen.
1.11 Mehrwertige Felder Kein neuer Datentyp, aber eine wesentliche Erweiterung bestehender Datentypen sind die mehrwertigen Felder. Damit können Sie für ein Feld eine Liste oder eine Tabelle oder Abfrage mit mehreren Werten bereitstellen, aus denen Sie innerhalb des eigentlichen Feldes einen oder mehrere Werte auswählen können. Prinzipiell sind mehrwertige Felder eine Art erweitertes Nachschlagefeld – Sie können damit halt nicht nur einen, sondern mehrere der zur Verfügung stehenden Einträge auswählen. Damit lassen sich etwa die Ausstattungsmerkmale von Fahrzeugen festlegen, ohne dass Sie neben der
41
Kapitel 1
Tabelle mit den Fahrzeugen eine weitere Tabelle benötigen. Mehr zu diesem Thema lesen Sie in Kapitel 2, »Tabellen und Datenmodellierung«. Auf den Inhalt mehrwertiger Felder können Sie lesend per per SQL (siehe Kapitel 8, »SQL«) und per VBA/DAO (siehe Kapitel 9, «DAO«) zugreifen; der schreibende Zugriff ist entweder nicht möglich oder hat sich dem Zugriff des forschenden Autors entzogen. Selbst der mit einem Hex-Editor bewaffnete Fachlektor konnte keine weiteren Erkenntnisse liefern.
Vorteil: Verbergen von Komplexität ... Für unerfahrene Access-Anwender ist dieses Feature ein Riesenvorteil gegenüber älteren Versionen: Sie können damit ohne Kenntnisse der Datenmodellierung quasi m:n-Be ziehungen aufbauen, freilich ohne zu wissen, was Access da im Hintergrund anstellt.
Nachteil: ... auf Kosten der Kontrolle Erfahrene Entwickler werden wohl nur bedingt Gebrauch von mehrwertigen Feldern machen – es weiß eben niemand, wie Access die Daten intern verwaltet und, was noch schlimmer ist, man kann auch nicht vernünftig, also per VBA (DAO) auf den Inhalt mehrwertiger Felder zugreifen.
1.12 Weitere Neuheiten zu Felddatentypen und Steuerelementen Es gibt noch einige weitere Änderungen bei den Datentypen und den zur Anzeige ihrer Inhalte notwendigen Steuerelementen.
1.12.1 Rich-Text Textfelder und Memofelder können Sie nun mit dem Wert Rich-Text der Eigenschaft Textformat für das Anlegen von Rich-Text-Formatierungen vorbereiten. Access verwendet dabei nicht die Auszeichnungen etwa von .rtf-Dateien, wie sie Word produzieren kann, sondern HTML – und hiervon auch nur eine Teilmenge. Wenn Sie in einem Feld oder Steuerelement, das für die Eingabe von Rich-Text vorbereitet ist, Text markieren, erscheint automatisch eine transparente Menüleiste, die beim Überfahren komplett sichtbar wird und Möglichkeiten zum Festlegen der gewünschten Formatierungen bietet. Passende Steuerelemente dazu finden Sie allerdings auch im Ribbon. Mehr zum Thema Rich-Text in Access erfahren Sie in Kapitel 5, »Steuerelemente«, Abschnitt 5.1.1, »Rich-Text in Textfeldern«.
42
Warum Access 2007?
1.12.2 Historie in Memofeldern Sie können für Memofelder die Eigenschaft Nur anfügen auf Ja einstellen, um eine Historie der Änderungen am Inhalt des Feldes zu speichern.
1.12.3 Datumssteuerelement Für Textfelder, die auf einem Datumsfeld basieren, können Sie mit der Eigenschaft Datumsauswahl anzeigen festlegen, ob beim Eintreten in ein solches Feld eine Schaltfläche zum Öffnen eines Dialogs zur Datumsauswahl angezeigt werden soll. Dieser Dialog ist eine praktische Sache, hat aber den Nachteil, dass man nur den Kalendertag direkt auswählen kann, nicht aber den Monat und das Jahr. Das funktioniert nur über das Blättern durch die Monate. Zur Eingabe längst vergangener Datumsangaben ist dies denkbar ungünstig. Weitere Infos: Kapitel 5, Abschnitt 5.1.2, »Datum auswählen«.
1.12.4 Schaltflächen mit Bild und Text Schaltflächen weisen einige neue Eigenschaften auf, mit denen Sie einer Schaltfläche etwa gleichzeitig ein Symbol und einen Text anzeigen können. Dabei legen Sie außerdem fest, ob die Beschriftung rechts, links, über oder unter dem Symbol Platz finden soll. Das Layout von Schaltflächen lässt sich aber noch weiter anpassen: So können Sie als Hintergrundart nun Normal und Transparent einstellen. Letzteres lässt die eigentliche Schaltfläche verschwinden und zeigt nur noch Symbol und/oder Text an. Mit einem dahinterliegenden Rechteck können Sie somit farbige Schaltflächen erzeugen. Den Mauszeiger können Sie bei Bedarf so einstellen, dass er als Mauszeiger-Hand erscheint. Das wird bei der zu erwartenden Schwemme von Schaltflächen, die nicht mehr wie Schaltflächen aussehen, allerdings auch nötig sein ... Weitere Informationen finden Sie in Kapitel 5, Abschnitt 5.2, »Schaltflächen«.
1.12.5 Kombinationsfelder und Listenfelder Da Nachschlagefelder in Tabellen nun als mehrwertige Felder ausgeführt werden, kann man natürlich nicht vor Kombinationsfeldern halt machen: Auch diese zeigen – eine entsprechende Datensatzherkunft vorausgesetzt – die für mehrwertige Felder typischen Kontrollkästchen zum Auswählen der gewünschten Daten an. Außerdem kann man Kombinations- und Listenfelder mit Wertlisten so einstellen, dass der Benutzer die Wertliste bearbeiten kann. Beim Aufklappen eines so vorbereiteten Kombinationsfeldes erscheint am unteren Rand eine kleine Schaltfläche, mit der Sie einen ebenfalls neuen Dialog öffnen können, der die Einträge untereinander angeordnet
43
Kapitel 1
anzeigt und außerdem das Festlegen eines Standardwerts ermöglicht, der anschließend in die passende Steuerelementeigenschaft eingetragen wird. Wenn das Kombinations- oder Listenfeld auf einer Tabelle oder Abfrage basiert, können Sie für die Eigenschaft Bearbeitungsformular für Listenelemente ein Formular angeben, mit dem die Daten der Datensatzherkunft bearbeitet werden können. Auch hier erscheint beim Ausklappen des Kombinationsfelds eine Schaltfläche, die das angegebene Formular öffnet. Leider zeigt dieses nicht direkt den aktuell im Kombinationsfeld ausgewählten Datensatz an – dafür müssen Sie doch selbst zum VBA-Editor greifen. Weitere Informationen zu Kombinationsfeldern finden Sie in Kapitel 5, Abschnitt 5.3, »Kombinationsfelder«, und Abschnitt 5.4, »Kombinationsfeld-Techniken«. Mit Listen feldern beschäftigt sich Kapitel 5, Abschnitt 5.5, »Listenfelder«.
1.12.6 Bildsteuerelement mit Steuerelementinhalt Wenn Sie Bilder von der Festplatte in einem Bildsteuerelement anzeigen möchten, müssen Sie dieses nur an ein Feld der Datensatzquelle des Formulars oder Berichts binden, das den Pfad und den Dateinamen des Bilds enthält. Das ist eine wesentliche Erleichterung im Vergleich zu früher, als man noch per VBA die Picture-Eigenschaft des Steuerelements mit den entsprechenden Dateiinformationen füllen musste. Weitere Informationen: Kapitel 11, Abschnitt 11.9, »Bilder von der Festplatte in Formularen und Berichten anzeigen«.
1.12.7 Zu große Zahlen ersetzen Ganz nach Excel-Manier arbeitet eine Funktion, die Zahlen, die nicht komplett in dem passenden Feld angezeigt werden können, durch einen Platzhalter in Form von RauteZeichen (#) ersetzt. Sie können diese Funktion in den Access-Optionen unter Aktuelle Datenbank|Auf abgeschnittene Zahlenfelder prüfen aktivieren oder deaktivieren (siehe auch Kapitel 5, Abschnitt 5.1.4, »Abgeschnittene Zahlenfelder«).
1.13 Neues in Tabellen Neben den Felddatentypen gibt es auch bei den Tabellen selbst Neuheiten.
1.13.1 Alternative Hintergrundfarbe Tabellen und allgemeine Objekte in der Datenblattansicht können Sie nun mit wechselnden Hintergrundfarben ausstatten. In den Access-Optionen legen Sie im Bereich Daten blatt|Standardfarben die standardmäßig eingesetzten Farben fest, beim Anzeigen von
44
Warum Access 2007?
Objekten in der Datenblattansicht können Sie deren Farben mit den Ribbon-Einträgen unter Start|Schriftart festlegen (siehe Kapitel 4, Abschnitt 4.1.5, »Sonstige Neuerungen«, und Kapitel 6, Abschnitt 6.1.4, »Wechselnde Hintergrundfarbe«).
1.13.2 Entwurf in der Datenblattansicht von Tabellen Gemäß dem Credo »Alles für den Einsteiger« kann man nun Felder direkt in der Datenblattansicht anlegen – und Access findet den passenden Datentyp dazu. Dieses Feature dürfte auch das erste sein, das Sie nach dem ersten Öffnen einer neuen Datenbank mit Access 2007 gesehen haben: Access bietet eine ansonsten leere Tabelle mit einer Spalte mit der Beschriftung Neues Feld hinzufügen an (siehe Kapitel 2, Abschnitt 2.1, »Techniken zur Datenmodellierung«). Weitere neue Befehle zum Entwerfen von Tabellen in der Datenblattansicht finden Sie im Ribbon-Bereich Datenblatt: Neues Feld: Öffnet einen Dialog mit einigen Feldvorlagen zum direkten Übernehmen in die aktuelle Tabelle. Vorhandene Felder hinzufügen: Zeigt alle bereits in anderen Tabellen enthaltenen Felder an; auch diese können Sie direkt in das Datenblatt ziehen. Access aktiviert dann den Nachschlage-Assistenten, mit dem Sie genauere Einstellungen vornehmen können. Nachschlagespalte: Auch Nachschlagefelder lassen sich direkt in der Datenblattansicht anlegen. Der gewohnte Nachschlage-Assistent hilft beim Auswählen des gewünschten Feldes. Schade, dass das eine oder andere Feature nicht in der Entwurfsansicht verfügbar ist; das Hinzufügen von Feldvorlagen kann prinzipiell eine Menge Arbeit sparen. Leider bietet die Datenblattansicht nicht den Komfort der Entwurfsansicht, die wesentlich mehr Informationen in der Übersicht anzeigt.
1.14 Neues bei den Formularen Formulare bieten nicht nur neue Ansichten, sondern auch eine ganze Reihe weiterer neuer Funktionen.
1.14.1 Layoutansicht Bisher war es immer umständlich, die Größe von Steuerelementen eines Formulars so anzupassen, dass der Inhalt immer komplett sichtbar war. Die neue Layoutansicht ist prädestiniert für solche Anpassungen: Sie zeigt die echten Daten in den Steuer elementen an, während Sie fleißig weiter Änderungen am Entwurf vornehmen können.
45
Kapitel 1
Die Entwurfsansicht wird aber dadurch keineswegs überflüssig: Das Einfügen von ungebundenen Steuerelementen etwa können Sie nur dort durchführen. Details finden Sie in Kapitel 4, Abschnitt 4.1.2, »Formularansichten«.
1.14.2 Geteilte Ansicht Viele Szenarios verlangen nach der gleichzeitigen Anzeige einer Übersicht mehrerer Datensätze und der Details zu einem ausgewählten Datensatz. Bisher hat man die Übersicht mit einem Listenfeld oder einem Unterformular in der Endlos- oder Datenblattansicht realisiert. Jetzt gibt es dazu eine spezielle Ansicht, die sich »Geteilte Ansicht« oder »Split View« nennt. Ein solches geteiltes Formular können Sie entweder direkt über den Ribbon-Eintrag Erstellen|Formulare|Geteiltes Formular erstellen oder aber von einem normalen Formular durch Einstellung der passenden Eigenschaften ableiten. Informationen zu diesem Thema finden Sie in Kapitel 4, Abschnitt 4.1.3, »Geteilte Formulare«.
1.14.3 Filtern und Sortieren Eigentlich ein gemeinsames Feature der Datenblattansicht von Tabellen, Abfragen und Formularen, aber vor allem in Formularen als Element der Benutzerumgebung interessant sind die neuen Möglichkeiten zum schnellen Filtern und Sortieren von Daten in der Datenblattansicht. Sie können damit absteigend und aufsteigend sortieren und komfortabel filtern: Wenn Sie die kleine Pfeil-Schaltfläche im rechten Bereich einer jeden Spaltenüberschrift an klicken, erscheint ein Sortier- und Filtermenü, das beispielsweise alle enthaltenen Werte als Filterkriterium anbietet. Alternativ können Sie einen der zur Verfügung stehenden und datentypabhängigen Filter verwenden, um etwa alle Datumsangaben des folgenden Quartals auszugeben. Wenn Sie lieber gleich nach dem Inhalt eines bestimmten Feldes filtern wollen, klicken Sie einfach mit der rechten Maustaste auf das betroffene Feld: Ein Kontextmenü bietet dann die bereits oben genannten Sortiermöglichkeiten und feldabhängige Filterkriterien an, aber Sie können damit auch Filter auf Basis des aktuellen Feldinhalts einsetzen. Neugierig geworden? Details finden Sie in Kapitel 4, »Formulare«.
Filtern und Sortieren beim Laden Für Formulare und Berichte können Sie nun festlegen, ob ein eventuelles Filter- oder Sortierkriterium beim Laden des Formulars oder Berichts aktiv sein soll (weitere Infor mationen in Kapitel 4, »Formulare«, und Kapitel 6, »Berichte«).
46
Warum Access 2007?
1.14.4 Berechnungen auf Spaltenbasis In Datenblättern von Tabellen, Abfragen, Formularen und Berichten können Sie nun eine Berechnungsspalte einfügen, in der Sie das Ergebnis verschiedener Aggregatfunktionen zu den aktuell angezeigten Datensätzen anzeigen können – beispielsweise Summe, Mittelwert, Anzahl, Datensatzanzahl, Maximum, Minimum, Standardabweichung oder Varianz. Der Clou ist, dass Sie eine solche Berechnung mit zwei Mausklicks auf die Beine stellen – Sie brauchen nur die betreffende Spalte zu markieren und im Ribbon oder aus dem Kontextmenü die passende Funktion auszuwählen (weitere Informationen in Kapitel 4, Abschnitt 4.1.4, »Hilfreiche Funktionen für den Formularentwurf«). Das funktioniert sogar in Berichten – allerdings nur in der neuen Layoutansicht, die Sie weiter unten kennen lernen (siehe auch Kapitel 6, Abschnitt 6.3.6, »Summen in der Layoutansicht«).
1.14.5 Vereinfachtes Layouten Steuerelemente können Sie in der Layoutansicht zusammenfassen und in bestimmten Anordnungen positionieren lassen (Stichwörter: »Tabellarisch« und »Gestapelt«). Der Vorteil ist nicht nur, dass Access die Steuerelemente automatisch anordnet und Sie mit einem Klick zwischen den beiden Anordnungen wechseln können, sondern dass Sie die Größe eines Steuerelements etwa im tabellarischen Layout verändern können und Access die rechts vom veränderten Steuerelement liegenden Elemente automatisch näher heran oder weiter weg schiebt – je nachdem, ob Sie das Steuerelement vergrößert oder verkleinert haben. Ein weiterer Vorteil dieser Layouts ist, dass Sie einfach Gitternetzlinien hinzufügen und diese einzelnen anpassen können. Sie können auch nach dem Anlegen eines Layouts Steuerelemente hinzufügen oder entfernen; das Layout passt sich immer so an, dass keine leeren Stellen zurückbleiben. Mit diesem Feature kommen auch einige neue Eigenschaften, mit denen Sie nicht nur die passenden Gitternetzlinien, sondern auch den Abstand zwischen den Steuerelementen einstellen können (siehe auch Kapitel 4, Abschnitt 4.1.4, »Hilfreiche Funktionen für den Formularentwurf«). Die genannten Möglichkeiten zum vereinfachten Layouten können Sie auch in der Layoutansicht von Berichten einsetzen (siehe Kapitel 6, Abschnitt 6.1.2, »Vereinfachtes Layouten«).
1.14.6 Verankern von Steuerelementen Eines der coolsten neuen Features ersetzt quasi die Ereigniseigenschaft Bei Größen änderung von Formularen: Mit den zwei Eigenschaften Horizontaler Anker und Vertikaler Anker können Sie ein Steuerelement oben, unten, links und/oder rechts verankern
47
Kapitel 1
und dafür sorgen, dass es seine Größe zuverlässig zusammen mit dem Formular verändert. Alles über die nötigen Einstellungen finden Sie in Kapitel 5, Abschnitt 5.9.1, »Steuerelemente verankern«.
1.14.7 AutoHeight für Formularbereiche Mit der neuen AutoHeight-Eigenschaft der einzelnen Formularbereiche können Sie dafür sorgen, dass diese vergrößert werden, wenn darin enthaltene Steuerelemente vergrößert werden.
1.15 Neues bei Berichten Berichte bringen einige der Neuheiten mit, die auch Formulare aufweisen und die vorne schon erwähnt wurden. Dennoch eine kurze Aufzählung der bereits erwähnten Features und die Angabe des Kapitels mit weiteren Informationen: Layoutansicht: Ermöglicht wie bei Formularen komfortables Anordnen durch Layouts, Anzeigen von Gitternetzlinien, Einstellen von Abständen und die Anzeige der enthaltenen Daten während des Entwurfs. Filtern und Sortieren beim Laden Berechnungen auf Spaltenbasis: Leider nur in der Layoutansicht Informationen hierzu finden Sie in Kapitel 6, »Berichte«. Zusätzlich gibt es noch weiterer Neuheiten, wie die folgenden Abschnitte zeigen.
1.15.1 Berichtsansicht Die Berichtsansicht ist eine Mischung aus der Vorschauansicht von Berichten und der Formularansicht von Formularen – nur, dass man damit keine Daten ändern kann. Zumindest nicht direkt. Sie können aber damit immerhin zur Laufzeit filtern und auch Steuerelemente wie Schaltflächen anklicken. Und das ist natürlich sehr interessant: So können Sie die Vorteile der Gruppierung und Sortierung von Berichten zur Datenanzeige nutzen und gleichzeitig Steuerelemente wie Schaltflächen hinzufügen, um etwa ein Detailformular mit Informationen zum aktuellen Datensatz anzuzeigen oder gar zu bearbeiten. Gleichzeitig lösen Steuerelemente in der Berichts- wie auch in der Layoutansicht nun Ereignisse ähnlich wie in Formularen aus. In Kapitel 6, »Berichte«, finden Sie in Abschnitt 6.10, »Die Berichtsansicht«, ein Beispiel für die neuen Möglichkeiten. Leider kann man in dieser Ansicht nicht die üblichen Berichtsereignisse wie Beim Formatieren oder Beim Drucken der einzelnen Bereiche einsetzen, um die Anzeige zu optimieren.
48
Warum Access 2007?
1.15.2 Neues bei Makros Im Rahmen der vielfältigen Arbeiten zum Vereinfachen des Zugangs zu Access für NichtAccess-Entwickler hat Microsoft auch die Makros überarbeitet (für Quereinsteiger: unter Access sind Makros ein eigener Objekttyp, der eine Schnittstelle zum Automatisieren einfacher Aufgaben liefert und nichts mit VBA zu tun hat). Da Makros nur eine Teilmenge des Funktionsumfangs von VBA abbilden, kommen sie in diesem Buch nicht weiter zum Zuge – mit Ausnahme der Vorstellung der folgenden Neuerungen und der zwei beim Öffnen einer Datenbank automatisch ausgeführten Makros AutoExec und AutoKeys. Daher suchen Sie bei Interesse am Thema am besten den Artikel »Grundlegende Informationen zu Makros in Access 2007« in der Onlinehilfe auf. Da wären zum Beispiel die folgenden Neuerungen: Makros können nun fest mit den Ereigniseigenschaften von Formularen, Berichten und den enthaltenen Steuerelementen verknüpft werden. Das hat selbst gegenüber VBA-Ereignisprozeduren den klaren Vorteil, dass Sie das Steuerelement samt Funktion duplizieren oder von einem Formular ins andere kopieren können. Makros werden nun in einer Sandbox abgearbeitet und können dafür eingesetzt werden, beim Öffnen einer Datenbankanwendung die Sicherheitseinstellungen der Access-Instanz zu prüfen und den Benutzer gegebenenfalls darauf hinzuweisen, dass seine Einstellungen zu streng sind und einige Funktionen (wie etwa alle, die mit VBA zusammenhängen) nicht ausgeführt werden können (siehe Kapitel 18, »Sicherheit von Access-Datenbanken«). Makros haben nun eine rudimentäre Fehlerbehandlung. Weitere Informationen finden Sie in der Onlinehilfe. Makros können auf eine bestimmte Auflistung von Variablen zugreifen. Weitere Informationen finden Sie in der Onlinehilfe unter dem Stichwort »TempVars« oder weiter unten in diesem Kapitel. Die Argumente eines Makro-Befehls werden nicht nur im unteren Bereich des MakroFensters angezeigt, wo Sie diese auch eingeben, sondern auch noch in einer zusätzlichen Spalte in der Liste der Befehle. Auf diese Weise haben Makro-Programmierer eine wesentlich bessere Übersicht über das Geschehen.
1.16 VBA VBA wurde weitgehend von Neuerungen ausgeschlossen, soweit es nicht irgendein anderes Objekt berührt – beispielsweise Makros, das Anlage-Steuerelement oder die neuen Möglichkeiten von Formularen und Berichten.
49
Kapitel 1
Der VBA-Editor darf sich über eine bahnbrechende Neuerung freuen: Das Codefenster reagiert nun auch auf das Scrollen mit dem Mausrad. Die meisten werden dies nicht bemerken, weil sie irgendeines der dafür vorgesehenen Tools von Fremdherstellern verwenden, um diese Funktion nachzurüsten.
1.16.1 TempVars TempVars sind eine Neuerung, die VBA mit Makros verbindet. TempVars ist eine Auflistung, die eines oder mehrere TempVar-Elemente enthält, die – wie der Name schon sagt – Variablen repräsentieren. An folgenden Codebeispielen lässt sich am einfachsten zeigen, was man damit machen kann – die erste Funktion legt eine Variable unter einem bestimmten Namen an, die zweite fragt den Wert einer Variablen ab: Function SetVar(VarName As String, AValue As Variant) TempVars.Add VarName, AValue End Function Listing 1.1: Hinzufügen einer temporären Variablen mit dem Namen VarName und dem Wert AValue
Function Getvar(VarName As String) As Variant Getvar = TempVarsVarName) End Function Listing 1.2: Auslesen der temporären Variablen mit dem Namen VarName
Die TempVars-Auflistung lässt sich mit For Each durchlaufen, um alle enthaltenen Variablen samt Inhalt auszugeben. Interessant ist die TempVars-Auflistung für den Einsatz von Makros. Hier stehen die Makro-Aktionen EntfernenAlleTempvar, EntfernenTempVar und FestlegenTempVar zur Verfügung, um Elemente zur TempVarsAuflistung hinzuzufügen oder zu entfernen. Mit [TempVars]![Variablenname] greifen Sie in VBA und in Makros auf die in der TempVars-Auflistung gespeicherten Werte zu. Wirklich interessant ist aber, dass TempVars ihren Inhalt auch beim Auftreten von Laufzeitfehlern nicht verlieren und sich leicht serialisieren lassen. Dazu noch drei kleine Beispielcodes: Function SerializeTempVars(Optional sFile As String, _ Optional bAdd As Boolean) As Boolean Dim vTmp As TempVar Dim F As Integer On Error GoTo ErrHandler If TempVars.Count > 0 Then
50
Warum Access 2007? If Len(sFile) = 0 Then sFile = CurrentProject.path & „\Tempvars.dat“ SetAttr sFile, vbNormal F = FreeFile DoEvents If bAdd Then Open sFile For Append As F Else Open sFile For Output As F End If For Each vTmp In TempVars Write #F, vTmp.name Write #F, vTmp.value Next vTmp Close F DoEvents SetAttr sFile, vbHidden Or vbReadOnly Or vbSystem End If SerializeTempVars = True Exit Function ErrHandler: Msgbox Err.Description, vbCritical Reset End Function Function DeSerializeTempVars (Optional sFile As String, Optional bClear As Boolean) As Boolean Dim vTmp As Variant Dim sName As String Dim F As Integer On Error GoTo ErrHandler If Len(sFile) = 0 Then sFile = CurrentProject.path & „\TempVars.dat“ F = FreeFile Open sFile For Input As F If bClear Then TempVars.RemoveAll Do While Not EOF(F) Input #F, sName Input #F, vTmp TempVars.Add sName, vTmp Loop Close F DeSerializeTempVars = True Exit Function ErrHandler: Msgbox Err.Description, vbCritical End Function
51
Kapitel 1 Function ListVars() As String() Dim vVar As TempVar Dim arrVars() As String Dim i As Long ReDim arrVars(TempVars.Count-1) For Each vVar In TempVars Debug.Print vVar.name arrVars(i) = vVar.name i = i + 1 Next vVar ListVars = arrVars Set vVar = Nothing Erase arrVars End Function Listing 1.3 Beispiele für das Speichern, Einlesen und Ausgeben von TempVars
1.16.2 VBA in Formularen, Steuerelementen, Berichten und DAO Die übrigen Neuerungen in VBA kommen – wie bereits erwähnt – sämtlich aus anderen Bibliotheken. Informationen finden Sie in den entsprechenden Kapiteln, etwa Kapitel 4, »Formulare«, Kapitel 5, »Steuerelemente«, Kapitel 6, »Berichte«, und Kapitel 9, »DAO«.
1.17 Sonstige Neuheiten Nachfolgend finden Sie die übrigen Neuheiten von Access 2007.
1.17.1 Neuer Farbauswahldialog Access 2007 bietet einen neuen Farbauswahldialog. Dieser umfasst drei Bereiche: Der oberste zeigt die Farben des aktuellen Farbschemas von Access an (das Sie übrigens in den Access-Optionen unter Häufig verwendet|Die beliebtesten Optionen bei der Arbeit mit Access|Farbschema auswählen können). Damit können Sie Ihre Formulare diesem Schema anpassen. Der zweite Bereich enthält einige Standardfarben und der dritte die zuletzt verwendeten Farben. Außerdem können Sie in den Eigenschaften, die Objekten eine Farbe zuweisen, noch eine Reihe Systemfarben auswählen.
1.17.2 ClearType-Fonts in Access Sie können für Access getrennt festlegen, ob die Schriften mit ClearType-Effekt geglättet werden sollen. Die passende Option finden Sie in den Access-Optionen unter Häufig verwendet|Die beliebtesten Optionen bei der Arbeit mit Access|Immer ClearType verwenden.
52
Warum Access 2007?
1.17.3 Neue Schriftarten Office 2007 bringt drei neue Standardschriftarten mit – Calibri, Cambria und Segoe –, die die bisherigen Tahoma, Times und Courier ersetzen sollen. Die neuen Schriften lassen sich vor allem bei kleinen Schriftgraden besser lesen. Calibri kommt überall in der Oberfläche als Standardeinstellung daher.
1.17.4 Import und Export Nach dem Importieren oder Exportieren von Daten (mit neuen Dialogen, zu erreichen über die Befehle des Ribbon-Tabs Externe Daten) können Sie die Informationen über die durchgeführte Aktion speichern und diese später wiederholen, indem Sie den RibbonEintrag Externe Daten|Importieren|Gespeicherte Importe oder das Pendant für Exporte auswählen. Sie können sogar eine Outlook-Aufgabe auf Basis dieser Aktion festlegen.
Templates selbst gemacht Durch die Möglichkeit, auch Import-Vorgänge zu speichern, können Sie sich praktisch selbst Vorlagen basteln: Legen Sie oft verwendete Objekte beziehungsweise Vorlagen dazu in einer Datenbank an, importieren Sie diese je einmal in eine leere Datenbank und speichern Sie die Importvorgänge unter passenden Namen. In der nächsten Datenbank können Sie die Importe dann einfach wiederholen und haben damit quasi eigene Templates für verschiedene Objekte.
1.17.5 PDF-Export Der Export von Berichten in das PDF-Format ist seit jeher ein heißes Thema. Microsoft wollte einen PDF-Export zum Office-Paket hinzufügen, hat dann aber davon abgesehen und bietet nun ein entsprechendes Tool zum Download an, das hier nicht vorgestellt wird. Stattdessen empfiehlt der Autor dieses Buchs ein alternatives kostenloses Tool von Stephen Lebans. Es ist programmierbar und eignet sich daher auch für das Erstellen vieler PDF-Dokumente auf einen Rutsch. Weitere Informationen finden Sie auf der Internetseite zu diesem Buch unter http://www.access-entwicklerbuch.de.
1.17.6 Daten sammeln per E-Mail Sie können nun HTML-Formulare per E-Mail versenden, die Informationen abfragen, und diese nach dem Zurücksenden direkt in die Felder einer Tabelle eintragen lassen. Das Gleiche funktioniert nicht nur mit HTML-, sondern auch mit InfoPath-Formularen; hiermit können Sie dem Empfänger auch einen vorhandenen Datensatz schicken, den
53
Kapitel 1
dieser bearbeiten soll. Eine genauere Beschreibung dieses Features hat es nicht in dieses Buch geschafft, weil es dafür wesentlich bessere Möglichkeiten gibt – etwa ein einfaches Web-Formular, dessen Inhalt nach einer Validierung in eine Datenbank auf einem Webserver gespeichert wird.
1.17.7 SharePoint Die neue Access-Version enthält auch Funktionen für die Kooperation mit SharePoint. Dieses Buch geht jedoch nicht darauf ein, weil es sich auf die Datenbankentwicklung konzentriert und Dokumentenmanagement oder gar das Verwenden von Datenbank frontends mit Dokumentenmanagementbackends thematisch abseits liegen (anders ist das im »Access 2007 – Das Praxisbuch für Entwickler«: Hier steht das Dokumenten management im Mittelpunkt – allerdings nicht mit SharePoint, sondern als Beispiel für die Entwicklung einer professionellen Access-Anwendung (siehe http://www.access-entwicklerbuch.de). Detaillierte Informationen zu SharePoint 2007 finden Sie im Buch »Microsoft SharePoint 2007 im Einsatz« (ISBN: 978 3827324566), das ebenfalls bei Addison-Wesley erscheint.
54
2 Tabellen und Datenmodellierung Dieses Kapitel zeigt zunächst die grundlegenden Techni ken für das Anlegen von Tabellen. Erwarten Sie aber nicht, dass Sie alle x Varianten vorgesetzt bekommen, mit denen Sie Tabellen, Felder und Indizes in Form bringen – es gibt wesentlich bedeutendere Informationen, die Sie dringend kennen müssen, wenn Sie eine Datenbank mit den enthalte nen Tabellen aufsetzen und auch lange Spaß daran haben wollen. Das Konzipieren des richtigen Datenmodells einer Daten bank wird Sie weit länger aufhalten als es das Erlernen der grundlegenden Techniken erfordert. Das Buch geht daher in den ersten Abschnitten so kurz wie möglich darauf ein, wie Sie Tabellen, Felder, Indizes und Verknüpfungen anlegen, und vermittelt anschließend das notwendige Hintergrundwissen zu diesen Techniken, wobei der Fokus auf den neuen Features von Access 2007 liegt. Grundlegendere Informationen erhalten Sie etwa in der Onlinehilfe unter Suchbegriffen wie »Erstellen von Tabellen in einer Datenbank« oder »Einfügen, Hinzufügen oder Erstellen eines neuen Felds in der Datenbank«. Wenn Sie sich schon mit der Benutzeroberfläche vertraut gemacht haben oder sich intuitiv darin zurechtfinden, kön nen Sie die folgenden Abschnitte überspringen und sich di rekt mit Abschnitt 2.2, »Namenskonventionen für Tabellen und Felder« beschäftigen. Wenn Sie die neuen Features von Access 2007 jedoch noch nicht kennen, sollten Sie die ersten
Kapitel 2
Abschnitte dieses Kapitels auf keinen Fall auslassen: Es gibt nämlich einige Neuheiten in Access 2007, die Sie kennen sollten. Im mittleren Teil des Kapitels erfahren Sie, wie Sie die Anforderungen an die geplante Anwendung in ein adäquates Datenmodell umsetzen. Dabei ist es wichtig, dass die Da ten den Objekten der realen Welt entsprechend abgebildet und dazu in Relation gesetzt werden. Dabei helfen die Normalisierungsregeln, die manch einer vielleicht schon intuitiv einsetzt, und eine konsistente Benennung von Tabellen und Tabellenfeldern. Damit Sie nicht nur mit trockener Theorie abgespeist werden, finden Sie im hinteren Teil des Kapitels eine Menge Beispiele für Datenmodelle. Diese können Sie als Basis für eigene Anwendungen oder auch nur als Anregung verwenden. Hintergrund dieses Kapitels ist die Tatsache, dass viele (angehende) Entwickler ins kalte Access-Wasser geworfen werden und keine oder wenig Erfahrung in der Datenmodel lierung haben. Zudem findet sich in den meisten Grundlagen-Büchern zu Access meist nur eine Beispielanwendung mit Datenmodell – und das hält dann im ganzen restlichen Teil des Buches als Grundlage für Abfrage-, Formular-, Berichts- und VBA-Beispiele her. Beispiele auf CD: Sie finden alle Code-Beispiele dieses Kapitels auf der Buch-CD unter \Kap_02\TabellenUndDatenmodellierung.accdb. Die Datenbankdatei enthält auch alle für die Beispiele verwendeten Tabellen.
Zielsetzung Dieses Kapitel hat verschiedene Ziele: Für Einsteiger und Access 2007-Neulinge: Kurze Einführung in die Techniken zur Datenmodellierung (Tabellen, Felder, Indizes, Verknüpfungen) Einführung einer Konvention für die Namen von Tabellen und Feldern Normalformen: Wozu dienen sie und wie normalisiert man ein Datenmodell? Begriffsklärung (Beziehungsarten, relationale Integrität, Primärschlüssel, Fremd schlüssel) Erläuterung der Beziehungsarten anhand praktischer Beispiele Vermittlung eines Gefühls für die jeweils richtige Beziehungsart anhand einiger Datenmodelle verschiedener Anwendungen
56
Tabellen und Datenmodellierung
2.1 Techniken zur Datenmodellierung Eines vorneweg: Sie sollten in diesem Kapitel eigentlich nicht erfahren, wie Sie Tabellen in der Tabellenansicht von Access erstellen – die ist, so dachte der Autor dieser Zeilen zu nächst, nur für Anwender, die nicht wissen, welchen Datentyp die von ihnen eingegebe nen Daten haben, und Access die Festlegung des Datentyps überlassen möchten. Genau genommen ist diese Ansicht die erweiterte Datenblattansicht, die Sie beim Anlegen einer neuen Tabelle über den Ribbon-Eintrag Erstellen|Tabellen|Tabelle erwartet (siehe Abbildung 2.1). Wenn Sie möchten, können Sie diese Funktion in den Access-Optionen unter Aktuelle Datenbank|Anwendungsoptionen|Entwurfsänderungen für Tabellen in der Datenblattansicht aktivieren (für diese Datenbank) deaktivieren. Das wirkt sich übrigens erst nach dem nächsten Öffnen der Datenbank aus.
Abbildung 2.1: Access bietet die Möglichkeit, neue Felder ohne Angabe eines Datentyps anzulegen – das ist natürlich nichts für professionelle Entwickler
Die Funktion bietet aber einige interessante Ansätze, die es unter anderem ermöglichen, vorab oft benutzte Felder mit allen Eigenschaften zu definieren und diese dann per Mausklick in eine Tabelle einzufügen. Weitere Informationen dazu finden Sie in Ab schnitt 2.1.2, »Felder hinzufügen« unter »Felder in der Datenblattansicht hinzufügen«.
Keine Assistenten In diesem Buch werden Sie es mit keinem einzigen Assistenten zu tun bekommen – den Umgang mit Assistenten können Sie sich, wenn Sie möchten, selbst aneignen, indem Sie diese einfach ausprobieren. Wenn Sie die in diesem Buch vorgestellten manuellen Vor gehensweisen kennen, wissen Sie auch, was die Assistenten beherrschen, und können diese gefahrlos einsetzen.
2.1.1 Tabellen anlegen Zum Anlegen einer neuen Tabelle betätigen Sie die Schaltfläche Tabellenentwurf der Gruppe Tabellen im Erstellen-Tab des Ribbons oder die Tastenkombination Alt, L, T, W (was bedeutet, dass Sie nacheinander die Tasten Alt, L, T und W betätigen müssen – nach
57
Kapitel 2
dem Drücken von Alt erscheinen im Ribbon die Buchstabenkürzel zu jedem Befehl, aber das werden Sie dann selbst sehen ...). Es erscheint eine neue, leere Tabelle in der Ent wurfsansicht, in deren Zeilen Sie die Definition je eines Feldes eintragen. Anschließend führen Sie die folgenden Schritte durch: Fügen Sie Namen und Datentypen der Felder sowie gegebenenfalls ihre Beschrei bung ein. Legen Sie die Schlüssel und Indizes für die Tabelle fest. Weisen Sie der Tabelle weitere Eigenschaften zu. Speichern Sie die Tabelle unter einem sinnvollen Namen. Die folgenden Abschnitte beschreiben diese Aufgaben im Detail.
2.1.2 Felder hinzufügen Tabellen bestehen nicht wie bei Excel aus Zeilen und Spalten, sondern aus Feldern und Datensätzen. Dabei unterscheiden sich nicht nur die Bezeichnungen, sondern auch die Funktionen: In Access enthält ein Datensatz ein Objekt eines bestimmten Typs und die Felder enthalten Werte für dessen Eigenschaften. Für jedes Feld müssen Sie einen Feldnamen mit maximal 64 Zeichen sowie einen Feld datentyp festlegen. Informationen über die richtige Wahl von Feldnamen finden Sie in Abschnitt 2.2.2, »Feldnamen«, die Felddatentypen stellt Tabelle 2.1 vor. Sie sollten auch für jedes Feld eine Beschreibung eingeben. Diese wird später, wenn Sie die Daten der Tabelle in einem Formular anzeigen, als SteuerelementTip-Text oder in der Statusleiste eingeblendet. Außerdem können Sie damit anderen Entwicklern die Arbeit erleichtern, wenn diese Ihre Anwendung verstehen oder anpassen wollen. Bei der Auswahl des Felddatentyps sollten Sie folgende Faktoren berücksichtigen: Schätzen Sie ab, welche Länge Texte haben. Haben Sie kurze (bis 255 Zeichen) oder lange Texte? Verwenden Sie je nach Antwort entweder ein Feld mit dem Datentyp Text oder Memo. Welchen Wertebereich nehmen Zahlenfelder ein? Rechnen Sie mit den größten zu erwartenden Bereichen und wählen Sie einen der Zahl-Datentypen aus. Beachten Sie auch, dass manche nur ganze Zahlen und manche auch Dezimalzahlen akzeptieren. Bei Datums- und Währungsangaben fällt die Auswahl nicht schwer: Die Datentypen Datum/Zeit und Währung helfen weiter. Verwenden Sie für Währungsangaben niemals den Datentyp Double, da dies in Berechnungen zu Rundungsfehlern führen kann.
58
Tabellen und Datenmodellierung
Für Primärschlüssel verwenden Sie in der Regel einen Autowert mit dem Untertyp Long Integer. Nur, wenn Sie mit verteilten Anwendungen arbeiten und die Daten zusammenführen müssen, macht der Untertyp Replikations-ID Sinn. Binäre Daten wie Dateien (insbesondere Bilder) können Sie mit Access 2007 nicht nur in OLE-Objekt-Feldern, sondern auch in Feldern des Datentyps Anlage speichern. Die Vor- und Nachteile erfahren Sie im Kapitel 11, »Bilder und binäre Dateien«. Wenn Sie für ein Feld ein Nachschlagefeld bereitstellen wollen, mit dem der Benutzer Daten aus einer vorgegebenen Liste oder einer anderen Tabelle auswählen soll, können Sie den Nachschlage-Assistenten für die Einrichtung verwenden. Im Grunde ist Nachschlage-Assistent aber kein Datentyp, als Datentyp richtet Access den zuvor ausgewählten Datentyp ein. Sie erfahren aber weiter unten, wie Sie die notwendigen Eigenschaften für den Einsatz von Nachschlagefeldern (auch mehrwertiger) von Hand einrichten können. Es gibt Sonderfälle, bei denen Sie einen anderen Datentyp als den offensichtlichen verwenden: Wenn Sie etwa Postleitzahlen eingeben und nur die reinen Zahlenwerte ohne das Länderkürzel (also 47137 statt D-47137) verwenden, sollten Sie ein Textfeld und nicht etwa den Felddatentyp Zahl (Long) auswählen. Der Grund: Manche Post leitzahlen enthalten eine führende 0, und die fällt in Zahlenfeldern natürlich unter den Tisch. Außerdem können Sie, wenn Sie etwa deutsche und österreichische Post leitzahlen gemischt verwenden, immer noch nach der Anfangszahl sortieren, während in einem Zahlenfeld nach dem Wert der Zahl sortiert würde.
Felddatentyp
VBA-Datentyp
Beschreibung
Text
String
Kurze Texte, max. 255 Zeichen, 2 Byte je Zeichen
Memo
String
Lange Texte, max. 65.536 Zeichen bei Eingabe über die Benutzeroberfläche, 2 GB bei Eingabe per Code, 2 Byte je Zeichen
Zahl (Byte)
Byte
Zahlen von 0 bis 255, 1 Byte
Zahl (Integer)
Integer
Zahlen von -32768 bis 32.767, 2 Byte
Zahl (Long Integer)
Long
Zahlen von -2.147.483.648 bis 2.147.483.647, 4 Byte
Zahl (Single)
Single
Gleitkommazahlen einfacher Genauigkeit von -3,402823E38 bis -1,401298E-45 für negative Zahlen, 1,401298E-45 bis 3,402823E38 für positive Zahlen und 0, 4 Byte
Zahl (Double)
Double
Gleitkommazahlen doppelter Genauigkeit von -1,79769313486232E308 bis -4,94065645841247E-324 für negative Zahlen, 4,94065645841247E-324 bis 1,79769313486232E308 für positive Werte und 0, 8 Byte
Tabelle 2.1: Datentypen beim Anlegen von Tabellen in der Entwurfsansicht
59
Kapitel 2 Felddatentyp
VBA-Datentyp
Beschreibung
Zahl (Replikations-ID)
Benutzerdefi nierter Typ
16 Byte
Zahl (Dezimal)
Variant
Zahlen als Potenzen zur Basis 10. Für Zahlen ohne Nachkommastellen +/-79.228.162.514.264.337.593.543.9 50.335, für Zahlen mit 28 Nachkommastellen +/-7,92281 62514264337593543950335
Datum/Uhrzeit
Date
Datums- oder Zeitangaben, 1.1.100 bis 31.12.9999, 8 Byte
Währung
Currency
Skalierte Ganzzahl von -922.337.203.685.477,5808 bis 922.337.203.685.477,5807, 8 Byte
Autowert (Long Integer)
Long
Zahlen von -2.147.483.648 bis 2.147.483.647, 4 Byte
Autowert (Replikations-ID)
Benutzerdefi nierter Typ
16 Byte
OLE-Objekt
Byte-Array
Enthält OLE-Objekte und binäre Daten, 1 Gigabyte
Hyperlink
String
Enthält vier Zeichenketten mit maximal 2.048 Zeichen (Anzeigetext, Hyperlink, Unterhyperlink, Hilfetext). Die einzelnen Bestandteile trennt man durch das RauteZeichen (#).
Anlage
Byte-Array
Speichert Dateien im binären Format. Genaue Informationen finden Sie in Kapitel 12, »Bilder und binäre Daten«. Maximale Größe: keine Informationen verfügbar, Experimente ergaben kein klares Ergebnis.
Nachschlageassistent
Verschiedene
Stellt die Feldeigenschaften für Felder so ein, dass diese als Nachschlagefeld für Wertlisten, Tabellen/Abfragen oder komplexe Datentypen verwendet werden können.
Tabelle 2.1: Datentypen beim Anlegen von Tabellen in der Entwurfsansicht (Fortsetzung)
Wenn Sie die Feldnamen und Felddatentypen ermittelt haben, können Sie diese in den Tabellenentwurf eintragen (siehe Abbilddung 2.2). Fügen Sie die Felder einfach von oben nach unten ein. Wenn Sie die Reihenfolge der Felder ändern möchten, markieren Sie einfach das zu verschiebende Feld durch einen Klick auf das graue Kästchen links daneben und ziehen es dann bei gedrückter linker Maustaste an die gewünschte Stelle. Das Löschen und Einfügen von Zeilen erledigen Sie am einfachsten mit den passenden Einträgen des Kontextmenüs der Entwurfsansicht – experimentieren Sie halt einfach mal. Aber Achtung: Das Löschen eines Feldes löscht unwiderruflich auch die enthaltenen Daten. Mit den im unteren Bereich angezeigten Eigenschaften können Sie die einzelnen Felder weiter anpassen. Weitere Informationen zu den Eigenschaften finden Sie weiter unten.
60
Tabellen und Datenmodellierung
Abbildung 2.2: Eintragen von Feldnamen, Felddatentypen und Beschreibungen in der Tabellen entwurfsansicht
Besonderheiten des Felddatentyps Memo Seit Access 2007 können Memofelder Rich-Text aufnehmen. Eigentlich konnten sie das schon vorher (mit den entsprechenden Format-Tags), aber nun unterstützt Access auch die Anzeige in Formularen und Berichten. Um ein Memo-Feld mit Rich-Text zu füllen, stellen Sie vorab die Eigenschaft Textformat auf den Wert Rich-Text ein (siehe Abbildung 2.3).
Abbildung 2.3: Vorbereiten eines Memo-Felds auf die Eingabe von Text im Rich-Text-Format
61
Kapitel 2
Direkt im Anschluss können Sie in der Datenblattansicht der Tabelle auf die RichText-Formatierung zugreifen. Dazu müssen Sie nur mit der Maus den zu formatierenden Text markieren und die passende Formatierung aus dem nun erscheinenden Popup-Menü auswählen (siehe Abbildung 2.4). Dieses Menü erscheint übrigens nicht, wenn Sie Text mit der Tastatur markieren; in dem Fall müssen Sie auf die passenden Formatierungs-Schaltflächen im Ribbon unter Start|Schriftart und Start|Rich-Text zurückgreifen.
Abbildung 2.4: Formatieren von Rich-Text-Format direkt in der Datenblattansicht einer Tabelle
Access stellt die Rich-Text-Formatierungen übrigens nicht über das etwa in RTF-Dateien verwendete Format, sondern über HTML her. Den verwendeten HTML-Code können Sie nicht direkt über die Benutzeroberfläche, aber über eine kleine VBA-Anweisung ermitteln. Diese setzen Sie im Direktfenster des VBA-Editors ab, den Sie mit Strg + G öffnen: Debug.Print DLookup("BeispielRTF", "tblBeispielRTF")
Für das erste Rich-Text-Feld aus der Tabelle in Abbildung 2.4 sieht der HTML-Code etwa so aus:
<strong>Dies ist Text im Rich-Text-Format. <em>Nach dem Markieren von Text erscheint eine kleine Leiste mit Formatierungsoptionen, wie Sie im nächsten Datensatz sehen können. Sie können aber auch die passenden Schaltflächen im Ribbon verwenden.
Den Inhalt eines Memofeldes im Rich-Text-Format können Sie auch per VBA einfügen; zu beachten ist hier, dass nicht alle HTML-Elemente, sondern nur eine Auswahl verfügbar ist. Um welche es sich genau handelt, ist zum Zeitpunkt der Erstellung dieses Buches nicht dokumentiert; es sind aber mehr, als das zum Formatieren verfügbare Popup-Menü anbietet.
62
Tabellen und Datenmodellierung
Weitere Informationen über den Einsatz von Rich-Text-Feldern finden Sie in Kapitel 5, »Steuerelemente«, in Abschnitt 5.1.1, »Rich-Text in Textfeldern«.
Besonderheiten des Felddatentyps Zahl mit Format Prozent Access 2003 und älter brachte eine etwas gewöhnungsbedürftige Behandlung von Prozentzahlen mit sich: Zwar gab es für den Felddatentyp Zahl mit der Formateinstellung Prozent entsprechend formatierte Werte aus, jedoch interpretierte es die Eingabe von 1 als 100% und 100 als 10.000%, was zu vielen Eigenentwicklungen zur Behandlung von Prozentzahlen führte. Access 2007 zeigt sich da wesentlich entwicklerfreundlicher, da es die Eingabe von 1 auch als 1% interpretiert und 100 als 100%.
Besonderheiten des Felddatentyps Datum/Uhrzeit Eingaben in Felder des Felddatentyps Datum/Uhrzeit speichert Access intern als DoubleWerte. Dabei entspricht der ganzzahlige Anteil dem Datum (dabei steht 1 für den 31.12.1899) und der Teil hinter dem Komma der Uhrzeit (in Bruchteilen eines Tages). Bei der Eingabe zweistelliger Jahreszahlen interpretiert Access alle Jahre von 00 bis 29 als Jahre des 21. Jahrhunderts (15 entspricht also dem Jahr 2015) und alle Jahre von 30 bis 99 als Jahre des 20. Jahrhunderts (71 entspricht somit dem Jahr 1971). Ab Access 2007 steht für Datumsfelder eine Eigenschaft zur Verfügung, mit der Sie einen Dialog zur Auswahl von Datumsangaben aktivieren können. Verantwortlich hierfür ist die Eigenschaft Datumsauswahl anzeigen, für die Sie den Wert Für Datumsangaben einstellen müssen (siehe Abbildung 2.5).
Abbildung 2.5: Für Datumsfelder steht ab jetzt ein Dialog zur Datumsauswahl bereit
63
Kapitel 2
Abbildung 2.6 zeigt diesen Dialog im Einsatz. Beim Klick auf ein Datumsfeld erscheint neben dem Feld eine Schaltfläche, mit der Sie den Dialog zur Datumsauswahl öffnen können. Leider ist diese Lösung etwas halbherzig; wer damit Termine auswählen möchte, die etwa weit zurückliegen, muss endlos klicken, bis er zum gewünschten Jahr gelangt.
Abbildung 2.6: Einblenden des Dialogs zur Auswahl von Datumsangaben
Besonderheiten des Felddatentyps Anlage Das Feld Anlage gibt es erst seit Access 2007. Es kann eines oder mehrere Dateien speichern und bringt einen Dialog zu ihrer Auswahl mit. Abbildung 2.7 zeigt eine solche Tabelle mit einem leeren Feld. Wie in der Abbildung zu sehen ist, zeigt Access für Anlage-Felder keinen Feldnamen in der Kopfzeile an; wenn Sie hier einen Feldnamen sehen möchten, müssen Sie diesen für die Eigenschaft Beschriftung des Feldes eintragen.
Abbildung 2.7: Eine Tabelle mit einem leeren Feld des Datentyps Anlage
Ein Doppelklick auf ein Anlage-Feld aktiviert den Dialog aus Abbildung 2.8, mit dem Sie die in diesem Feld enthaltenen Dateien verwalten können. Der mit der Schaltfläche Hinzufügen geöffnete Dialog ist ein gewöhnlicher Dateiauswahl-Dialog, mit dem Sie eine oder mehrere Dateien auswählen können. Sie können als Dateiname auch Links zu Daten im Internet angeben, Access lädt diese dann herunter.
64
Tabellen und Datenmodellierung
Die übrigen Schaltfächen des Anlagen-Dialogs sind selbsterklärend; mit ihnen können Sie die enthaltenen Dateien wieder entfernen, diese öffnen oder auf der Festplatte speichern.
Abbildung 2.8: Mit diesem Dialog verwalten Sie die in einem Anlage-Feld enthaltenen Dateien
Was sich hinter einem Anlage-Feld verbirgt, können Sie im Beziehungen-Fenster oder im Abfrageentwurf entdecken, wenn Sie dort eine Tabelle mit einem solchen Feld einfügen (siehe Abbildung 2.9). Es enthält intern drei weitere Felder, die mit den Binärdaten, dem Dateinamen (ohne Verzeichnis) und dem Datentyp beziehungsweise der Dateiendung gefüllt werden.
Abbildung 2.9: Abfrage einer Tabelle mit einem Anlage-Feld
Das Ergebnis einer solchen Abfrage sieht wie das einer Abfrage mit zwei per 1:nBeziehungen verknüpften Tabellen aus. Die Daten der übergeordneten Tabelle (tblAnlagenBeispiel) werden dabei für jeden Datensatz der untergeordneten Tabelle (hier die
65
Kapitel 2
interne Tabelle mit den in jedem Anlage-Feld gespeicherten Daten) wiederholt ausgegeben (siehe Abbildung 2.10).
Abbildung 2.10: Abfrage über die Datensätze einer Tabelle mit einem Anlage-Feld mit jeweils mehreren gespeicherten Dateien
Eine Besonderheit ergibt sich bei Bilddateien: Für diese können Sie in den AccessOptionen unter Aktuelle Datenbank|Anwendungsoptionen|Bildeigenschaften-Speicherformat einstellen, ob sie im Quellbildformat oder im Bitmap-Format gespeichert werden sollen (siehe Abbildung 2.11). In älteren Versionen war nur Letzteres möglich, was zu vielen Workarounds führte.
Abbildung 2.11: Eigenschaft zum Einstellen des Formats von in Anlage-Feldern gespeicherten Bilddateien
66
Tabellen und Datenmodellierung
Anlage-Felder nehmen möglicherweise nicht alle Dateien auf, die Sie darin speichern möchten. Dabei entscheidet sich anhand der Dateiendung, ob eine Datei aufgenommen werden kann oder nicht. Welche Dateiendungen gesperrt sind, erfahren Sie in der Onlinehilfe des VBA-Editors unter »Attachment-Objekt«. Interessanterweise prüft Access nur die Dateiendungen; wenn Sie eine Datei mit einer nicht erlaubten Endung in eine mit einer erlaubten Endung umbenennen, akzeptiert Access diese ohne Probleme. Generell stellt sich die Frage, warum man nicht standardmäßig beliebige Dateien in Anlage-Feldern speichern kann. Weitere Informationen zum Thema Bilder und binäre Dateien in Access finden Sie in Kapitel 11, »Bilder und binäre Dateien«. Dort erfahren Sie etwa, wie Sie Dateien per VBA in Anlage-Feldern speichern und wie Sie darin enthaltene Bilder in Formularen und Berichten anzeigen.
Tabellen nach Vorlage erstellen Wie bereits oben erwähnt, liefert Access 2007 die Möglichkeit, den Tabellenentwurf in der Datenblattansicht zu verändern. Darüber hinaus können Sie komplette vorgefertigte Tabellen anlegen lassen oder zumindest fertige Felder in Ihre Tabellen einfügen. Beim Anlegen von Tabellen wählen Sie dazu den Ribbon-Eintrag Erstellen|Tabellenvor lagen| aus, wobei für verschiedene Tabellen wie Kontakte, Aufgaben, Probleme, Ereignisse oder Posten steht (siehe Abbildung 2.12). Leider ist dieses Feature ziemlich nutzlos: Im Gegensatz zu den weiter unten beschriebenen Feldvorlagen lässt sich die Auswahl vorhandener Tabellen nämlich nicht über die Benutzeroberfläche erweitern oder anpassen.
Abbildung 2.12: Auswahl einer vorgefertigten Tabelle
67
Kapitel 2
Felder in der Datenblattansicht hinzufügen Haben Sie bereits eine Tabelle erstellt, können Sie dort in der Datenblattansicht fertige Felder einfügen. Dazu betätigen Sie einfach die Ribbon-Schaltfläche Datenblatt|Felder und Spalten|Neues Feld und wählen aus dem dann erscheinenden Dialog Feldvorlagen das gewünschte Feld aus (siehe Abbildung 2.13).
Abbildung 2.13: Hinzufügen vordefinierter Felder in der Datenblattansicht
Nun ist es aber leider so, dass die Felder nicht unbedingt auf professionelle Bedürfnisse zugeschnitten sind und in den meisten Fällen noch angepasst werden müssen. Die Zeitersparnis ist also praktisch Null. Das können Sie aber ändern, denn die Informationen über die vordefinierten Felder sind – zumindest zum Teil – in handelsüblichen XMLDateien mit der Endung .accfl gespeichert. Sie finden diese Dateien im Verzeichnis C:\ Programme\Microsoft Office\Templates\1031\Access. Wenn Sie die vorhandenen Einträge aus der Liste entfernen möchten, können Sie dies ganz einfach tabellenweise erledigen, indem Sie die passenden Dateien aus dem angegebenen Verzeichnis entfernen. Zum Erweitern der Liste gehen Sie folgendermaßen vor: Erstellen Sie eine Tabelle mit oft benötigten Feldern und fügen stellen Sie deren Ei genschaften ein. Speichern Sie die Tabelle dann unter dem gewünschten Namen.
68
Tabellen und Datenmodellierung
Klicken Sie im Navigationsbereich mit der rechten Maustaste auf die soeben erstellte Tabelle und wählen Sie dort den Eintrag Exportieren|XML-Datei aus. Geben Sie im Exportieren-Dialog als Dateinamen C:\Programme\Microsoft Office\ Templates\1031\Access\.xml an, wobei entsprechend zu ersetzen ist. Aktivieren Sie im Dialog XML exportieren lediglich die Option Schema der Daten (XSL) und behalten Sie die weiteren Einstellungen bei. Nach dem Beenden des Assistenten müssen Sie die so erzeugte Datei nur noch derart umbenennen, dass die Datei auf .accfl endet. Fertig! Sie müssen nun nur noch Access neu starten, eine Tabelle in der Datenblattansicht öffnen, die Ribbon-Schaltfläche Datenblatt|Neues Feld betätigen und die neuen Feldvor lagen in die Tabelle einfügen. Nun ist es nicht so, dass man hier nicht noch etwas verbessern könnte: Schön wäre etwa, wenn man dieses Feature auch in der doch übersichtlicheren Entwurfsansicht einsetzen könnte. Außerdem ist es in der vorliegenden Version von Access noch ein wenig buggy: Textfelder etwa werden unabhängig von den Einstellungen immer mit 255 Zeichen an gelegt.
2.1.3 Schlüssel festlegen Access bietet verschiedene Schlüsselarten an. Schlüssel haben gemeinsam, dass sie das Anlegen eines Index nach sich ziehen. Einen Index können Sie sich als eine Tabelle vorstellen, die alle Daten eines Schlüsselfeldes in der angegebenen Reihenfolge sowie den Primärschlüssel der Tabelle enthält. Beim Sortieren der Tabelle oder beim Suchen nach einem bestimmten Wert aus dem Schlüsselfeld zieht Access dann die Index-Tabelle zu Rate und kommt somit schneller zum Ziel. Im Gegenzug verursachen Indizes beim Anlegen von Datensätzen zusätzliche Arbeit für den Rechner, da der Datensatz ja nicht nur in der eigentlichen Tabelle, sondern auch noch in der Index-Tabelle gespeichert werden muss (oder auch in mehreren). Genaue Informationen über die Auswirkungen und den Einsatz von Indizes finden Sie in Kapitel 14, »Performance«, in Abschnitt 14.1.2, »Indizes«. In den folgenden Abschnitten erfahren Sie zunächst einmal, wie Sie diese festlegen. Daneben haben Schlüssel noch weitere Eigenschaften, die sie von normalen Feldern abheben – auch diese beschreiben die folgenden Abschnitte.
Primärschlüssel Sie sollten für jede Tabelle einen Primärschlüssel festlegen. Ein Primärschlüssel ist ein eindeutiger Index, mit dem Sie – wie der Name schon sagt – die Eindeutigkeit der Da tensätze einer Tabelle sicherstellen. So können Sie zwei ansonsten völlig identische
69
Kapitel 2
Datensätze immer noch aufgrund des Feldes mit dem Primärschlüssel unterscheiden. Den Primärschlüssel legen Sie fest, indem Sie im Datenbankentwurf das passende Feld markieren und den Ribbon-Eintrag Entwurf|Tools|Primärschlüssel betätigen. Access mar kiert das Feld dann etwa wie das Feld MitarbeiterID in Abbildung 2.2. Meist hat das Pri märschlüsselfeld den Datentyp Autowert; damit stellen Sie automatisch sicher, dass niemals ein doppelter Wert für dieses Feld angelegt wird – zumindest nicht, wenn Sie die Daten immer in diese Tabelle eingeben. Alternativ können Sie hier den Unterdatentyp Replikations-ID verwenden; das macht beispielsweise Sinn, wenn Mitarbeiter unabhängig voneinander neue Datensätze in verschiedenen Kopien der Datenbank anlegen und diese zu einem bestimmten Zeitpunkt zusammengeführt werden sollen. Bis Access 2003 war diese Vorgehensweise unter dem Stichwort Replikation bekannt. Datenbanken, die im Format von Access 2007 erstellt wurden, können Sie nicht replizieren. Warum ist es nun so wichtig, dass jede Tabelle ein Feld mit eindeutigen Werten enthält? Eine der wichtigsten Eigenschaften relationaler Datenbanksysteme wie Microsoft Access ist die Möglichkeit, Beziehungen zwischen Tabellen herzustellen. So kann man etwa einen Kunden einem Projekt zuordnen. Und diese Zuordnung erfolgt über die Zu weisung des Primärschlüsselfeldes der Projektetabelle zu einem Fremdschlüsselfeld der Kundentabelle; das heißt, man trägt den Primärschlüsselwert in ein passendes Feld der Kundentabelle ein. Und wenn das Primärschlüsselfeld keine eindeutigen Werte enthält, wäre auch keine eindeutige Zuordnung der Datensätze dieser beiden Tabellen möglich. Sie können nur einen Primärschlüssel je Tabelle anlegen, der allerdings nicht nur eines, sondern auch mehrere Felder enthalten kann. Dies macht etwa Sinn, wenn Sie eine Verknüpfungstabelle zur Herstellung einer m:n-Beziehung verwenden und die darin enthaltenen Verknüpfungsfelder zu einem Primärschlüssel zusammenfassen. Weitere Informationen zu diesem Thema erhalten Sie in Abschnitt 2.5.5, »m:n-Beziehungen«. Primärschlüssel dürfen keine Null-Werte enthalten, nur für Felder mit den Datentypen Autowert, Zahl, Text, Datum/Uhrzeit und Währung angelegt werden und nur für Felder, die noch keine doppelten Werte enthalten. Theoretisch ginge das auch für Ja/Nein-Felder, aber das macht selbstverständlich keinen Sinn.
Sekundärschlüssel Neben dem Primärschlüssel können Sie weitere Schlüssel anlegen, die zumindest dafür sorgen, dass Sie schneller nach Daten in den mit einem Schlüssel versehenen Feld suchen oder diese abfragen können. Sie definieren einen Sekundärschlüssel für ein Feld, indem Sie dessen Eigenschaft Indiziert entweder auf Ja (Duplikate möglich) oder auf Ja (Ohne Duplikate) einstellen. Im letzteren Fall darf dieses Feld wie ein Primärschlüsselfeld nur eindeutige Werte enthalten. Kandidaten für Sekundärschlüsselfelder sind etwa Fremdschlüsselfelder in Beziehungen (weitere Informationen in Abschnitt 2.1.5, »Bezie hungen herstellen«) oder solche Felder, in denen Sie vermutlich oft nach Daten suchen.
70
Tabellen und Datenmodellierung
Anzeigen aller Schlüssel Das Bearbeiten des Primärschlüssels und der nachfolgend vorgestellten Sekundärschlüs sel ist auch in einem speziellen Dialog möglich, den Sie in der Entwurfsansicht einer Tabelle mit dem Ribbon-Eintrag Entwurf|Einblenden/Ausblenden|Indizes öffnen. Der nun erscheinende Dialog Indizes zeigt eine Liste mit allen Schlüsselfeldern der Tabelle an (siehe Abbildung 2.14).
Abbildung 2.14: Dieser Dialog zeigt sämtliche Indizes einer Tabelle
Hier finden Sie zum jeweils markierten Index die drei Eigenschaften Primärschlüssel, Eindeutig und Nullwerte ignorieren. Die Eigenschaft Primärschlüssel können Sie wie oben erwähnt nur für einen Schlüssel auf Ja einstellen. Wenn ein Schlüssel aus mehreren Feldern zusammengesetzt sein soll, fügen Sie unterhalb der Zeile mit der Schlüs selbezeichnung eine weitere Zeile hinzu, deren Feld Indexname leer bleibt und die im Feld Feldname den Namen des weiteren zum Schlüssel gehörenden Feldes enthält (siehe Abbildung 2.15).
Abbildung 2.15: Beispiel für einen zusammengesetzten Schlüssel im Indizes-Dialog
71
Kapitel 2
Wenn Sie die Eigenschaft Nullwerte ignorieren auf Ja einstellen, ignoriert Access Datensät ze mit Nullwerten in dem angegebenen Feld bei der Bildung des Index, was bei Feldern mit vielen Nullwerten im Schlüsselfeld Speicherplatz spart.
Automatisch angelegte Indizes Access legt Felder, deren Feldnamen bestimmte Zeichenfolgen enthalten, automatisch als Index fest. Welche dies sind, können Sie im Dialog Access-Optionen unter Objekt-Desig ner|Tabellenentwurf|AutoIndex beim Importieren/Erstellen festlegen (siehe Abbildung 2.16).
Abbildung 2.16: Für Felder, deren Feldname eine der unter AutoIndex beim Importieren/ Erstellen angegebenen Zeichenfolgen enthält, legt Access automatisch einen Index an
2.1.4 Eigenschaften festlegen Mit den Eigenschaften der Tabelle und der Felder können Sie beispielsweise die Optik und die Ergonomie der Tabelle auch im Hinblick auf die spätere Verwendung als Datensatzquelle für Formulare und Berichte verbessern. Die Bedeutung und die Werte der einzelnen Eigenschaften können Sie der Onlinehilfe entnehmen, die folgenden Abschnitte stellen deren Anwendung für spezielle Fälle vor.
72
Tabellen und Datenmodellierung
Autowertfelder anpassen Mit der Eigenschaft Neue Werte können Sie festlegen, wie Access Autowerte für neue Datensätze bestimmt: Entweder ermittelt Access den aktuell größten Wert und zählt 1 dazu (Inkrement) oder wählt einen zufälligen Wert aus (Zufall).
Feldgrößen voreinstellen Die Feldgröße legt fest, welche maximale Größe (Textfelder) oder welchen Wertebereich (Zahlenfelder) ein Feld hat. Beachten Sie, dass Sie damit immer den maximalen Wert festlegen – das birgt einige Gefahren: Bei Textfeldern sollten Sie etwa immer die maximale Größe 255 verwenden, außer Sie möchten die Anzahl der verwendeten Zeichen einschränken (etwa auf 7 für Postleitzahlen wie D-47137). Das ist insofern wichtig, als der Irrglaube herrscht, dass die Feldgröße bei Textfeldern die Anzahl der reservierten Zeichen repräsentiert, was nicht richtig ist. Bei Zahlen ist das anders: Diese belegen immer die in der Tabelle 2.1 angegebene Anzahl Bytes; die Feldgröße von Zahlenfeldern sollten Sie daher immer so auslegen, dass die größten zu erwartenden Werte soeben hineinpassen. In dem Zusammenhang ist interessant, dass Sie in den Access-Optionen unter ObjektDesigner|Tabellenentwurf den Standardfeldtyp, die Standardtextfeldgröße und die Standard zahlenfeldgröße festlegen können. Wenn Sie hier etwa für die Standardtextfeldgröße den Wert 255 einstellen, kann zumindest kein Benutzer zu Ihnen kommen und sich beschweren, dass mal wieder nur 50 Zeichen in ein Feld zur Eingabe von Internetadressen passen – nur, weil Sie vielleicht einmal zu sparsam waren (50 Zeichen war übrigens in älteren Access-Versionen mal die voreingestellte Standardeinstellung, jetzt sind es 255 Zeichen).
Richtiges Format anzeigen In Formularen und Berichten können Sie für Steuerelemente mit der Format-Eigenschaft festlegen, in welchem Format ihr Inhalt angezeigt werden soll. Da der Inhalt in der Regel immer im gleichen Format erscheinen soll, können Sie eine Menge Arbeit sparen, indem Sie das Format des Feldes direkt im Tabellenentwurf festlegen. Dazu verwenden Sie ebenfalls die Format-Eigenschaft. Je nach dem gewählten Felddatentyp stehen unterschiedliche vordefinierte Formate zur Verfügung, zu denen Sie in der Onlinehilfe ausgiebige Informationen erhalten (etwa über F1|Suche nach: Format Datum). Sie können auch eigene Formate entwerfen: Mehr dazu erfahren Sie in Abschnitt 2.4.2, »Format der Werte (semantische Integrität)«.
73
Kapitel 2
Beschriftungen in Formularen und Berichten vorbereiten Wenn Sie später Tabellenfelder in Formulare und Berichte einfügen, verwendet Access automatisch den Tabellennamen als Beschriftungsfeld des jeweiligen Steuerelements. In manchen Fällen ist das nicht gewünscht, dass Sie beispielsweise in Feldnamen keine Umlaute verwenden sollten oder dort Unterstriche und Abkürzungen einsetzen (mehr über optimale Feldnamen in Abschnitt 2.2.2, »Feldnamen«). Wenn Sie später immer wieder die Feldnamen in lesbare Steuerelementbeschriftungen ändern müssen, kann das auf Dauer nerven. Mit der Eigenschaft Beschriftung können Sie jedoch global den Text festlegen, der statt des Feldnamens als Beschriftungstext zum Einsatz kommt. Geben Sie hier also etwa für das Feld GeaendertAm den Wert Geändert am an.
Globale Validierung Für die Validierung von Daten können Sie durch passend programmierte Formulare und Steuerelemente sorgen, oder Sie legen einige dafür geeignete Eigenschaften von Tabellen und Feldern fest. Diese werden bei jeder Änderung von Daten über die Be nutzeroberfläche herangezogen, also nicht nur beim Ändern der Daten direkt in der Tabelle. Die Eigenschaften Gültigkeitsregel und Gültigkeitsmeldung dienen dazu, Regeln für den Wertebereich der einzugebenen Daten aufzustellen. Mehr zu diesem Thema erfahren Sie in Abschnitt 2.4.1, »Integrität der Werte (Wertbereichsintegrität)« und in der On linehilfe unter den entsprechenden Begriffen. Eine weitere Möglichkeit der globalen Validierung liefert die Eigenschaft Eingabeformat. Damit geben Sie an, wie der einzugebende Ausdruck im Detail aufgebaut sein muss. Weitere Informationen zu dieser Eigenschaft finden Sie in Abschnitt 2.4.2, »Format der Werte (semantische Integrität)«.
Standardwerte von Tabellenfeldern Eine weitere Vereinfachung bei der Eingabe von Daten ist das Bereitstellen von Stan dardwerten. So können Sie etwa einen statischen Wert vergeben oder eine Funktion zur Ermittlung eines Standardwertes heranziehen. In beiden Fällen tragen Sie den passenden Ausdruck in das Feld für die Eigenschaft Standardwert ein. Beispiele: 0: füllt das Feld mit dem Wert 0. Datum(): füllt das Feld eines neuen Datensatzes mit dem aktuellen Datum
74
Tabellen und Datenmodellierung
Die Einstellungen für die Eigenschaft Standardwert wirken sich auch auf den Einsatz des Feldes in Formularen aus. Access 2007 stellt im Gegensatz zu den Vorgängerversionen den Standardwert von Long-Feldern nicht mehr automatisch auf 0 ein.
Unterdatenblätter Unterdatenblätter sind unter Umständen ein performance-fressendes Feature, das mit Access 2000 eingeführt wurde. Diese bieten die Möglichkeit, innerhalb der Datenblattan sicht einer Tabelle alle zu einem Datensatz gehörenden Datensätze aus einer verknüpften Tabelle anzuzeigen. Da Sie Daten mit Formularen/Unterformularen meist wesentlich übersichtlicher darstellen können, sollten Sie diese Option meist abschalten. Dazu stellen Sie die Eigenschaft Unterdatenblattname einer jeden Tabelle in der Entwurfsansicht auf [Keines] ein. Die Funktion hat aber auch Vorteile: Wenn Sie beispielsweise die Un terdatensätze zu mehreren Datensätzen gleichzeitig anzeigen möchten, ist dies ein sehr einfacher Weg.
2.1.5 Beziehungen herstellen Das Herstellen einer Beziehung zwischen zwei Tabellen setzt voraus, dass die Datenbank eine Tabelle enthält, die auf die Daten einer anderen Tabelle verweisen soll. Erstere ent hält dazu ein Fremdschlüsselfeld und heißt Detailtabelle. Das Fremdschlüsselfeld nimmt später Daten auf, die aus dem Primärschlüsselfelds der anderen Tabelle stammen, der so genannten Mastertabelle. Nun können Sie ohne irgendwelche weiteren Maßnahmen Daten aus dem Primärschlüsselfeld der Mastertabelle in das Fremdschlüsselfeld der Detailtabelle eintragen und stellen damit schon eine Beziehung zwischen den beiden Tabellen her. Dies von Hand zu erledigen ist jedoch ziemlich aufwändig, also bietet Access einige Hilfsmittel, die sowohl die Auswahl der verknüpften Datensätze als auch die Prüfung der Konsistenz übernehmen.
Gleiche Datentypen Grundvoraussetzung für das Herstellen einer Beziehung mit referentieller Integrität (was das ist, erfahren Sie weiter unten) zwischen zwei Tabellen ist, dass das Fremd schlüsselfeld der Detailtabelle und das Primärschlüsselfeld der Mastertabelle den gleichen Datentyp haben. In der Regel ist dies der Datentyp Zahl (Long). Wenn Sie keine referentielle Integrität benötigen, können Sie auch andere Kombinationen einsetzen.
Nachschlage-Assistent Die einfachste Möglichkeit besteht darin, dem Verknüpfungsfeld den Wert NachschlageAssistent aus der Liste der Datentypen zuzuweisen. Dies startet einen Assistenten, der
75
Kapitel 2
alle notwendigen Informationen abfragt. Dieses Buch will Ihnen aber nicht den Umgang mit Assistenten, sondern Techniken und Hintergründe beibringen und deshalb folgt nun eine Beschreibung, wie Sie das, was der Nachschlage-Assistent so produziert, selbst auf die Beine stellen.
Beziehung herstellen Das Herstellen der Beziehung erledigen Sie im Dialog Beziehungen (auch BeziehungenFenster genannt). Sie öffnen dieses Fenster mit dem Ribbon-Eintrag Datenbanktools|Ein blenden/Ausblenden|Beziehungen (siehe Abbildung 2.17). In dieser Abbildung übernimmt die Tabelle tblKunden die Rolle der Mastertabelle und tblProjekte die der Detailtabelle. Das Feld KundeID der Tabelle tblProjekte soll einmal die Inhalte des Feldes KundeID der Tabelle tblKunden aufnehmen – und das auf möglichst einfache Weise.
Abbildung 2.17: Das Beziehungen-Fenster mit zwei verknüpften Tabellen
Der Kontextmenüeintrag Tabelle anzeigen öffnet einen Dialog mit allen in der Datenbank enthaltenen Tabellen. Wählen Sie hier die Tabellen aus, zwischen denen Sie eine Bezie hung herstellen möchten (siehe Abbildung 2.18). Wenn Sie alle Tabellen im Beziehungen-Fenster anzeigen möchten, die bereits Teil einer Beziehung sind, können Sie dies auch über den Eintrag Alle anzeigen aus dem Kon textmenü erledigen. Sie können die gewünschten Tabellen auch direkt aus dem Navigationsbereich in das Beziehungen-Fenster ziehen, wobei auch eine Mehrfachauswahl möglich ist (das Datenbankfenster älterer Access-Versionen bot diese Möglichkeit nicht). Zum Herstellen der Beziehung ziehen Sie nun das Primärschlüsselfeld der Mastertabelle auf das Fremdschlüsselfeld der Detailtabelle und lassen es dort fallen. Access legt eine passende Verbindungslinie zum Kennzeichnen der Beziehung an.
76
Tabellen und Datenmodellierung
Wenn Sie nun mit der rechten Maustaste auf diese Linie klicken und den Kontextmenü eintrag Beziehung bearbeiten auswählen, zeigt Access einen Dialog an, mit dem Sie referen tielle Integrität, Lösch- und Aktualisierungsweitergabe einstellen können. Was das alles ist, erfahren Sie weiter unten unter 2.4.5, »Referentielle Integrität«.
Abbildung 2.18: In diesem Dialog wählen Sie die im Beziehungen-Fenster anzuzeigenden Tabel len aus
Auswahl von per 1:n-Beziehung verknüpften Daten Das Herstellen einer Beziehung ist nur ein Teil der Arbeit, die Ihnen normalerweise der Nachschlage-Assistent abnehmen würde. Der Großteil besteht im Anlegen eines Nachschlagefeldes, mit dem Sie die Daten aus der verknüpften Tabelle bequem auswählen können. Alles, was der Nachschlage-Assistent dazu beiträgt, ist das Anpassen weniger Eigenschaften – und das können Sie auch von Hand erledigen. Im obigen Beispiel mit der Kunden- und der Projektetabelle legen Sie die erforderlichen Eigenschaften für das Feld KundeID der Tabelle tblProjekte fest. Öffnen Sie diese Tabelle in der Entwurfsansicht (vom Beziehungen-Fenster geht dies schnell über das Kontextmenü der Tabelle) und markieren Sie das Feld KundeID. Im unteren Bereich wechseln Sie nun zur Registerseite Nachschlagen. Dort finden Sie zunächst nur eine einzige Eigenschaft namens Steuerelement anzeigen vor, was sich aber ändert, wenn Sie deren Wert auf Kombinationsfeld einstellen (siehe Abbildung 2.19). Mit einigen der anderen Eigenschaften sorgen Sie nun für die Bevölkerung Ihres Nach schlagefeldes:
77
Kapitel 2
Abbildung 2.19: Die Eigenschaften eines Verknüpfungsfeldes, das als Nachschlagefeld ausgelegt werden soll
Herkunftstyp: Stellen Sie hier den Wert Tabelle/Abfrage ein. Wozu der Wert Wertliste dient, erfahren Sie weiter unten. Datensatzherkunft: Legt fest, aus welcher Tabelle oder Abfrage die Daten stammen. Üblicherweise gibt man hier einen SQL-Ausdruck an beziehungsweise generiert diesen über die Abfrage-Entwurfsansicht, die Sie mit der Schaltfläche mit den drei Punkten (...) öffnen. Prinzipiell muss die Abfrage zunächst das Primärschlüsselfeld der zu verknüpfenden Tabelle enthalten, weil dessen Inhalt gespeichert und da rüber die Beziehung hergestellt wird. Access erlaubt zusätzlich die Anzeige weiterer Felder und das Ausblenden des Primärschlüsselwertes. Sie brauchen also den Primärschlüssel zum Speichern und ein oder mehrere weitere Felder zum Anzeigen, also sieht die Datensatzherkunft in diesem Beispiel so aus: SELECT KundenID, Kunde FROM tblKunden;
Gegebenenfalls sortieren Sie die Daten alphabetisch nach den Kunden, was die folgende Datensatzherkunft nach sich ziehen würde: SELECT KundenID, Kunde FROM tblKunde ORDER BY Kunde;
Gebundene Spalte: Diese Eigenschaft legt fest, welche Spalte der Datensatzherkunft gespeichert werden soll. Standardmäßig ist dies die erste Spalte, was auch in diesem Beispiel passt. Spaltenanzahl: Gibt an, wie viele Spalten der Datensatzherkunft von links aus gesehen angezeigt werden sollen.
78
Tabellen und Datenmodellierung
Spaltenbreiten: Legt die Breite der angezeigten Spalten fest. Wenn die erste die gebundene Spalte ist und diese nicht angezeigt werden soll (was üblicherweise so ist) und es nur noch eine weitere Spalte gibt, tragen Sie hier den Wert 0cm ein. Damit wird die erste Spalte mit dem Primärschlüssel quasi ausgeblendet und die nächste Spalte nimmt den kompletten verfügbaren Platz ein. Zeilenanzahl: Hiermit legen Sie fest, wie viele Zeilen beim Ausklappen angezeigt werden sollen (standardmäßig seit Access 2007 auf 16 eingestellt, alter Standardwert war 8). Listenbreite: Legt die Gesamtbreite der Liste fest. Es gibt eine Reihe von Varianten bei der Gestaltung von Nachschlagefeldern auf Basis einer Tabelle oder Abfrage, die mit der Vorgehensweise beim Erstellen von Kombina tionsfeldern identisch sind. Für weitere Informationen lesen Sie daher Kapitel 5, »Steu erelemente«, in Abschnitt 5.3, »Kombinationsfelder«. Wenn Sie ein Nachschlagefeld definiert haben, können Sie damit eine Menge anfangen: Access zeigt es nicht nur in der Datenblattansicht der jeweiligen Tabelle oder auf dieser Tabelle aufbauenden Abfragen an (aber dort soll der Anwender ja ohnehin keine Daten ändern), sondern übernimmt die Nachschlagefelder auch standardmäßig in Formulare, wenn Sie dort das passende Feld anlegen. Dazu richtet Access einfach ein Kombinationsfeld mit den passenden Eigenschaften ein. Manchmal hat dies aber auch Nachteile: Wenn Sie einmal die tatsächliche ID des verknüpften Datensatzes ermitteln möchten, müssen Sie die Einstellungen für das Nachschlagefeld entsprechend ändern. Wenn Sie dies verhindern möchten und die Fremdschlüsselfelder in der Tabellendefinition als einfache Textfelder festlegen, müssen Sie diese in Formularen bei Bedarf manuell in Kombinationsfelder umwandeln.
Bearbeiten der Einträge eines Nachschlagefeldes Bisher mussten Sie, wenn Sie die Einträge eines Nachschlagefeldes für die Auswahl von per 1:n-Beziehung verknüpften Daten anpassen wollten, eigene Funktionen bereitstellen. Access 2007 versucht, dies zu vereinfachen, indem es die Möglichkeit zum direkten Öffnen eines geeigneten Formulars bietet. Alles, was Sie dazu veranlassen müssen, ist das Erstellen eines geeigneten Formulars (am besten mit dem Wert Wahr für die Eigenschaft Popup, damit dieses Formular vor dem Weiterarbeiten mit anderen Formularen wieder geschlossen werden muss), hier auf Basis der Tabelle tblKunden sowie das Einstellen zweier Eigenschaften: Die Eigenschaft Wertlistenbearbeitung zulassen erhält den Wert Ja und die Eigenschaft Bearbeitungsformular für Listenelemente den Namen des dafür vorgesehenen Formulars (hier: frmKunden). Wenn Sie das Nachschlagefeld nun in der Datenblattansicht öffnen,
79
Kapitel 2
erscheint eine Schaltfläche wie in Abbildung 2.20, die per Mausklick das angegebene Formular öffnet. Diese Funktion ist natürlich auch in Formularen verfügbar. Für das schnelle Ändern der zugrunde liegenden Daten ist diese Funktion ausreichend, aber nicht für eine professionelle Anwendung: Das angezeigte Formular zeigt nämlich weder direkt den aktuell im Nachschlagefeld ausgewählten Datensatz an (außer, es ist zufällig gerade der erste Datensatz ausgewählt), noch aktualisiert es das ausgewählte Feld auf den zuletzt im Formular angezeigten Datensatz. Falls Sie mehr benötigen, sollten Sie also doch lieber manuell eine passende Funktion erstellen – und wie das funktioniert, erfahren Sie in Kapitel 4, Abschnitt 4.6, »Von Formular zu Formular«.
Abbildung 2.20: Beim Öffnen eines Nachschlagefeldes erscheint ein Symbol zum Anzeigen eines Formulars zum Bearbeiten der zugrunde liegenden Daten
Auswahl von per m:n-Beziehung verknüpfte Daten Die hier vorgestellte Vorgehensweise zum Vereinfachen der Auswahl der Daten verknüpfter Tabellen ist nicht neu. Anders verhält es sich mit der Tatsache, dass Sie hier eine Mehrfachauswahl vornehmen können, ohne eine Verknüpfungstabelle zu erstellen, wie es sonst bei der Umsetzung von m:n-Beziehungen erforderlich ist. Dazu stellen Sie einfach die Eigenschaft Mehrere Werte zulassen auf den Wert Ja ein. Access zeigt dann zusätzlich zu den Listeneinträgen jeweils ein Kontrollkästchen an, mit dem Sie die enthaltenen Einträge auswählen können. So brauchen Sie nun etwa für die Auswahl der Mitarbeiter eines Projekts theoretisch keine m:n-Beziehung mehr, wie folgendes Beispiel zeigt. Die Tabelle tblProjekte enthält dabei ein Feld namens MitarbeiterID, mit dem sich mehrere Mitarbeiter auswählen lassen. Dazu stellen Sie die Feldeigenschaften wie in Abbil dung 2.21 ein. Der nicht komplett sichtbare Inhalt der Eigenschaft Datensatzherkunft sieht so aus: SELECT tblMitarbeiter.MitarbeiterID, [Nachname] & ", " & [Vorname] AS Mitarbeiter FROM tblMitarbeiter ORDER BY [Nachname] & ", " & [Vorname];
80
Tabellen und Datenmodellierung
Damit legen Sie fest, dass Nachname und Vorname als ein Feld in die Datensatzquelle aufgenommen werden.
Abbildung 2.21: Einstellungen für ein Nachschlagefeld, mit dem Sie mehrere Datensätze gleich zeitig auswählen können
In der Datenblattansicht ergibt sich dann das Bild aus Abbildung 2.22. Das Nachschlage feld zeigt beim Aufklappen Kombinationsfelder an, mit denen Sie einen oder mehrere Datensätze auswählen können.
Abbildung 2.22: Auswahl mehrerer Mitarbeiter zu einem Projekt
Nach der Auswahl werden die angezeigten Werte (nicht die gebundenen) in einer semikola-separierten Liste angezeigt (siehe Abbildung 2.23). Natürlich gibt es einige Besonderheiten bei der Abfrage von Tabellen mit mehrwertigen Nachschlagefeldern. Mehr darüber erfahren Sie in Kapitel 3, Abschnitt 3.2, »Abfragen mit Anlagen und mehrwertigen Feldern«.
81
Kapitel 2
Abbildung 2.23: Anzeige von mehrwertigen Feldern mit mehr als einem enthaltenen Datensatz
Bearbeiten der Daten in mehrwertigen Feldern Die in einem solchen mehrwertigen Feld angezeigten Daten können Sie genau wie bei üblichen Nachschlagefeldern mit einem Formular bearbeiten, wenn Sie die Eigenschaft Wertlistenbearbeiten zulassen auf den Wert Ja einstellen und für die Eigenschaft Bearbei tungsformular für Listenelemente den Namen eines geeigneten Formulars angeben – alles Weitere wurde bereits weiter oben erläutert. Bei ungebundenen Datensatzherkünften, bei denen Sie die Eigenschaft Herkunftstyp auf Wertliste einstellen und unter Datensatzherkunft eine semikolonseparierte Liste der anzuzeigenden Daten anlegen, können Sie auch einen eingebauten Dialog zur Bearbeitung der Daten verwenden; mehr dazu im folgenden Abschnitt.
2.1.6 Nachschlagefelder mit Wertliste Die oben vorgestellten Nachschlagefelder können Sie auch ohne den Einsatz einer Tabelle realisieren. Dazu speichern Sie die zu verknüpfenden Daten in einer so genannten Wertliste, indem Sie diese durch Semikola voneinander getrennt auflisten. Sinnvoll ist dies, wenn die mit dem Nachschlagefeld auszuwählenden Daten überschaubar sind und der Bestand sich nicht oder nur selten ändert (warum dies so ist, erfahren Sie weiter unten). In allen anderen Fällen verwenden Sie besser eine Tabelle als Datensatzherkunft. Ein Beispiel ist etwa das Geschlecht von Personen. Diese können Sie in einer eigenen Ta belle speichern, aber da voraussichtlich in nächster Zeit zu männlich und weiblich nicht viele zusätzliche Einträge hinzukommen werden, können Sie hierfür auch eine Wert liste verwenden. Die Eigenschaften für ein Nachschlagefeld namens Geschlecht sehen so aus wie in Abbildung 2.24. Im Vergleich zu den vorgegebenen Werten brauchen Sie nur die Eigen schaft Steuerelement anzeigen auf Kombinationsfeld und dann Herkunftstyp auf Wertliste und Datensatzherkunft auf "männlich";"weiblich" einzustellen; die übrigen Werte können Sie beibehalten.
82
Tabellen und Datenmodellierung
Abbildung 2.24: Einstellungen für ein Nachschlagefeld mit Wertliste
Die Eingabe der Wertlisten-Elemente ist mit Access 2007 noch einfacher geworden (vo rausgesetzt, die Eigenschaft Wertlistenbearbeitung zulassen ist aktiviert und die Wertliste ist einspaltig): Sie müssen nur noch auf die beim Aktivieren des Textfelds erscheinende Schaltfläche mit den drei Punkten (...) klicken und im folgenden Dialog die einzelnen Einträge in jeweils eine eigene Zeile zu schreiben (siehe Abbildung 2.25). Auch einen Standardwert können Sie dort auswählen; dieser wird direkt in die passende Eigenschaft Standardwert auf der Registerseite Allgemein eingetragen.
Abbildung 2.25: Eingabe von Listenelementen einer Wertliste per Dialog
Dieses Feature ist insofern nett, als es die Eingabe der zur Verfügung stehenden Werte erleichtert. Sie können es aber auch deaktivieren, indem Sie die Eigenschaft
83
Kapitel 2
Wertlistenbearbeitung zulassen auf Nein einstellen. Und das ist – sobald Sie die gewünschten Werte eingetragen haben – dringend anzuraten: Auch, wenn Sie dem Anwender keine Möglichkeit geben, direkt auf die Datenblattansicht von Tabellen oder Abfragen zuzugreifen, erhält er Zugriff auf diesen Dialog, wenn er in einem Formular ein Kombinationsfeld aufklappt, dass an ein Feld mit den oben beschriebenen Nachschla gefeld-Eigenschaften gebunden ist. Es erscheint dann ein kleines Symbol unterhalb der Liste, mit dem der Anwender diesen Dialog öffnen kann. Prinzipiell nimmt Ihnen Access damit eine Menge Arbeit ab, aber das Hinzufügen von Einträgen auf diese Weise hat einen entscheidenden Nachteil: Sie haben keine Möglichkeit, die hinzugefügten Werte zu validieren – so kann der Benutzer dort etwa mehrere leere Zeilen einfügen oder bereits verwendete Listeneinträge aus der Liste entfernen. Wenn der Benutzer Daten hinzufügen können soll, verwenden Sie besser ein Kombinationsfeld, das beim Eintragen eines nicht vorhandenen Datensatzes eine entsprechende Prozedur aufruft, mit der Sie die Daten validieren können (weitere Informationen hierzu siehe Kapitel 4, »Formulare«, Abschnitt 4.8, »Eingabevalidierung«). Der zweite Nachteil ist, dass Sie keine mehrspaltigen Wertlisten bearbeiten können, die in der ersten Spalte etwa eine nicht angezeigte ID und in der zweiten Spalte den tatsächlichen Wert anzeigen.
m:n-Beziehung für Wertlisten Access 2007 vereinfacht auch die Auswahl mehrerer Einträge einer Wertliste als Inhalt eines einzigen Feldes. Im Grunde sind diese Felder die m:n-Beziehungs-Variante der oben vorgestellten 1:n-Beziehungs-Wertlisten. Der einzige Unterschied ist, dass Sie hiermit nicht nur einen, sondern mehrere Einträge der angegebenen Wertliste auswählen können. Das ist ganz einfach: Sie müssen lediglich einige Eigenschaften auf der Nachschlagen-Registerseite der Feldeigenschaften des betroffenen Feldes ändern. Die wichtigste Eigenschaft lautet Mehrere Werte zulassen: Stellen Sie diese auf Ja ein. Als Beispiel dient die Tabelle aus Abbildung 2.26, mit der man zu jedem Artikel mehrere Lieferanten auswählen können soll. Anschließend können Sie – vorausgesetzt, es sind Daten vorhanden – mit der Auswahl mehrerer Datensätze für nur ein einziges Feld beginnen (siehe Abbildung 2.27). Abbildung 2.28 zeigt, wie Access den Feldinhalt nach dem Bearbeiten anzeigt. Im Prinzip ist dies eine vereinfachte Form der m:n-Beziehung, mit der Ausnahme, dass Sie nicht den üblichen Zugriff auf die verknüpften Daten in einer weiteren Tabelle haben. Laut Microsoft speichert Access dieses Daten aber dennoch in passenden, versteckten Tabellen, auf die der Anwender aber über die Benutzeroberfläche keinen direkten Zugriff wie auf die anderen Tabellen erhält.
84
Tabellen und Datenmodellierung
Abbildung 2.26: Einstellungen für ein Nachschlagefeld mit Mehrfachauswahl
Abbildung 2.27: Ein mehrwertiges Nachschlagefeld
Abbildung 2.28: Anzeige der in einem mehrwertigen Feld ausgewählten Daten
2.1.7 Tabelleninformationen im Überblick Wenn Sie einmal einen Überblick über die Tabellen, Felder und ihre Eigenschaften benötigen, um sich etwa in eine Tabelle eines anderen Entwicklers einzuarbeiten, können Sie sich eine solche Übersicht mit dem Datenbankdokumentierer erstellen lassen.
85
Kapitel 2
Diesen starten Sie mit dem Ribbon-Eintrag Datenbanktools|Analysieren|Datenbankdoku mentierer.
2.2 Namenskonventionen für Tabellen und Felder Die Namenskonventionen für Tabellen enthalten Vorschläge für die Benennung von Tabellen und der enthaltenen Felder. Die nachfolgenden Abschnitte fassen dabei Elemente der ungarischen Notation (http://www.xoc.net/standards/rvbanc.asp) und einige weitere Regeln zusammen, die sich bewährt haben und von vielen Entwicklern so oder ähnlich berücksichtigt werden. Details zur ungarischen Notation finden Sie unter dem oben genannten Link; ein Abdruck der recht umfangreichen Tabellen ist aus Platzgründen leider nicht möglich. Grundsätzlich gelten für die Vergabe von Namen an Access-Objekte, Feldnamen und Steuerelemente folgende Regeln: Der Name darf aus maximal 64 Zeichen bestehen. Der Name kann aus beliebigen Zeichen mit Ausnahme des Punktes (.), Ausrufezei chens (!), Gravis-Akzents (`), einfachen (') und doppelten Anführungszeichens (") und eckigen Klammern ([]) bestehen. Leerzeichen dürfen nicht am Anfang oder am Ende des Namens stehen. Zu empfehlen ist darüber hinaus, dass Namen von Access-Objekten, Feldern und Steuer elementen gar keine Sonderzeichen enthalten, da Namen mit Sonderzeichen an manchen Stellen eine Spezialbehandlung erfordern – beispielsweise müssen Sie diese in SQLAusdrücken und in VBA in eckige Klammern einfassen. Verwenden Sie außerdem besser keine Namen, die bereits für ein reserviertes Wort innerhalb von Access, VBA oder referenzierten Objektbibliotheken benutzt werden. Abschreckendes Beispiel ist die Verwendung von »Name« als Feldname in Tabellen mit Kontaktdaten – diese Bezeichnung ist bereits als Eigenschaft diverser Objekte vergeben. Access 2007 zeigt allerdings direkt beim Anlegen eines Tabellenfeldes oder von Objekten wie Tabellen, Abfragen, Formularen oder Berichten eine Warnmeldung an, wenn die Bezeichnung bereits reserviert ist. Natürlich können Sie auch Sonderzeichen, Leerzeichen oder bereits als Schlüsselwort verwendete Namen aufgreifen. Sie müssen dann allerdings einige Regeln beachten – etwa, dass Sie die Bezeichnungen von Feldern in SQL-Ausdrücken und in VBA in eckige Klammern setzen, wenn diese nicht ohnehin in Anführungszeichen stehen.
Warum sollen Sie eine Namenskonvention verwenden? Grundsätzlich können Sie Tabellen und die enthaltenen Felder nennen, wie Sie möchten. Abhängig davon, ob Sie eine Anwendung nur für das stille Kämmerlein oder für jemand anderen entwickeln, sollten Sie jedoch folgende Punkte beachten:
86
Tabellen und Datenmodellierung
Eine feste Systematik bei der Vergabe von Namen für Tabellen und Felder erleichtert Ihnen beim Programmieren das Leben. Wenn Ihre Tabellennamen etwa grundsätzlich mit »tbl« beginnen und der Plural der darin beschriebenen Objekte angefügt ist, müssen Sie sich beim Referenzieren dieser Tabelle aus Formularen, Berichten oder aus VBA-Modulen niemals Gedanken machen, wie Sie die Tabelle genannt haben, wenn Sie nur wissen, welches Objekt darin beschrieben wird. Präfixe bei Objektnamen erlauben Ihnen, auf einfache Weise ein Objekt am Namen zu erkennen und gleiche Basisnamen für mehrere unterschiedliche Objekttypen zu verwenden. Wenn Sie eine Tabelle beispielsweise nur Bestellungen nennen, können Sie schon keine Abfrage gleichen Namens mehr verwenden. Daher wählen Sie für die Tabelle den Namen tblBestellungen und für die Abfrage den Namen qryBestellungen. Wenn mehrere Entwickler die gleiche Konvention verwenden, erleichtert dies anderen Entwicklern das Analysieren und Bearbeiten der Anwendung und des enthaltenen Codes. Sie würden wahrscheinlich verrückt werden, wenn Sie eine Anwendung weiterentwickeln müssten, die eine völlig andere Konvention als die Ihnen vertraute verwendet – oder auch gar keine.
Verwenden alle Access-Entwickler die gleiche Konvention? Wenn Sie eine Anwendung oder eine Beispieldatenbank eines halbwegs professionellen Access-Entwicklers in die Hände bekommen, werden Sie feststellen, dass sich dort die ungarische Notation durchgesetzt hat. Natürlich gibt es hier und da Abweichungen, die persönlichen Vorlieben oder einfach der Macht der Gewohnheit unterliegen, aber mit diesen kann man in der Regel gut leben. Sicher kommt ein Entwickler damit klar, wenn ein Bericht nicht das Präfix »rpt« hat, sondern mit »rep« beginnt. Auch nennen manche Entwickler eine Datensatzgruppe »rs…« und nicht »rst…«, aber es versteht trotzdem jeder, was gemeint ist.
2.2.1 Tabellennamen Tabellen haben genau wie alle anderen Objekte unter Access ein bestimmtes Präfix, damit man sie von anderen Objekten unterscheiden kann. Dieses Präfix lautet tbl. Der Rest des Tabellennamens soll möglichst gut beschreiben, was die Tabelle enthält. Dabei wählt man für diesen Teil des Tabellennamens in der Regel die Pluralform. Der Grund dafür ist, dass die meisten Tabellen auch mehrere Datensätze und damit In formationen über das betroffene Objekt enthalten. Man kann nicht oft genug erwähnen, dass jede »normale« Tabelle Informationen über ein reales Objekt enthalten sollte, wie beispielsweise Personen, Fahrzeuge, Projekte, Termine, Gebäude oder Artikel.
87
Kapitel 2
Tabellen mit realen Objekten Tabellen, die reale Objekte beschreiben, heißen beispielsweise folgendermaßen: tblArtikel tblProjekte tblPersonen tblFahrzeuge
Verknüpfungstabellen Wenn schon von »normalen« Tabellen die Rede ist, sollen auch die anderen Typen vorgestellt werden: m:n-Beziehungen erfordern die Verwendung von Verknüpfungstabellen, die mindestens jeweils das Primärschlüsselfeld der zu verknüpfenden Tabellen enthalten. In manchen Fällen lässt sich für solche Tabellen ein erstklassiger Name finden, doch eine Kombination der Namen der beteiligten Tabellen macht meist schon deutlich, was die Tabelle für Informationen enthält. Beispiele: tblFahrzeugeSonderausstattungen tblBestellungenArtikel (besser: tblBestelldetails) tblArtikelFirmen (besser: tblLieferanten)
Detailtabellen in 1:1-Beziehungen Und da wären noch die Tabellen, die zusätzliche Daten zu anderen Tabellen enthalten und per 1:1-Beziehung mit diesen verknüpft sind. Mit ein wenig objektorientierter Sicht weise bilden die »Basistabelle« und die »Erweiterungstabelle« eine neue Tabelle, die quasi von der »Basistabelle« erbt. Wenn Sie beispielsweise eine Tabelle tblPersonen haben, die Kunden und Mitarbeiter zusammenfassen soll, können Sie die spezifischen Daten der beiden Personenarten in weiteren Tabellen speichern, die Sie 1:1 mit der »Basistabelle« verknüpfen. Diese Tabellen könnten Sie tblKunden oder tblMitarbeiter nennen, aber daraus würde nicht deutlich, dass diese Tabellen lediglich Detaildaten zu einer weiteren Tabelle enthalten. Besser wären Bezeichnungen wie tblPersonenKunden und tblPersonenMitarbeiter.
Lookup-Tabellen Die letzte Gruppe Tabellen, die nicht reale Objekte aus dem wirklichen Leben nachbilden, sind so genannte »Lookup-Tabellen«. Diese Tabellen enthalten Informationen, die eigentlich zu den »normalen« Tabellen gehören, aber im Zuge der Normalisierung
88
Tabellen und Datenmodellierung
in eigene Tabellen ausgegrenzt wurden. Beispiele dafür sind Anrede, Geschlecht oder Kontaktart. Für diese Tabellen gelten die gleichen Regeln wie für »normale« Tabellen, also beispielsweise folgende: tblAnreden tblGeschlechter (der Plural liest sich merkwürdig, ist aber hier konsequent) tblKontaktarten
Temporäre Tabellen Gelegentlich benötigen Sie eine Tabelle nur kurze Zeit und löschen diese nach der Verwendung wieder. Um diese Tabellen von anderen unterscheiden zu können, vor allem aber, damit Sie einen Überblick behalten, welche Tabellen nur vorübergehend benötigt werden, können Sie diese Tabellen kennzeichnen, indem Sie ihnen das Suffix Tmp oder Temp anhängen, etwa tblImportTemp. Eine Einsatzmöglichkeit für solche Tabellen ist etwa das Speichern des Ergebnisses von aufwändigen Abfragen (gegebenenfalls mit eingebauten und benutzerdefinierten Funktionen). Wenn die Ermittlung des Abfrageergebnisses lange dauert und das Ergeb nis beziehungsweise die zugrunde liegenden Daten sich selten ändern, macht das Spei chern des Ergebnisses in einer temporären Tabelle sehr viel Sinn.
2.2.2 Feldnamen Entgegen der bei Variablen üblichen Verwendung von Präfixen, durch die sich eine Aus sage über den Datentyp der Variablen treffen lässt, verwendet man für Feldnamen im Allgemeinen kein Präfix. Das ist natürlich Geschmackssache; man findet jedoch kaum Datenbanken, in denen auch die Feldnamen mit einem entsprechenden Präfix versehen sind.
Primärschlüsselfelder Der Name des Primärschlüsselfeldes setzt sich aus der Bezeichnung des durch die Tabelle abgebildeten Objekts im Singular und dem Suffix ID zusammen. Beispiele: tblArtikel: ArtikelID tblPersonen: PersonID In Verknüpfungstabellen kommt es darauf an, ob die Tabelle einen eigenen Primär schlüssel hat oder ob sie aus den Fremdschlüsselfeldern zur Verknüpfung mit den jewei ligen Tabellen zusammengesetzt wird. Im ersteren Fall gibt es vermutlich auch einen sinnvollen Tabellennamen, der nicht aus den Namen der beiden verknüpften Tabellen
89
Kapitel 2
besteht, wie etwa tblBestelldetails. Hier würde der Primärschlüssel (soweit vorhanden) BestelldetailID heißen. In Tabellen, die zusätzliche Daten zu einer anderen Tabelle enthalten und per 1:1-Bezie hung mit dieser verknüpft sind, verwendet man normalerweise den gleichen Namen für das Primärschlüsselfeld wie in der Basistabelle. Gegebenenfalls ist das Verknüpfungsfeld der Erweiterungstabelle nicht das Primärschlüsselfeld der Erweiterungstabelle, sondern lediglich ein eindeutiges Feld. In diesem Fall greift wieder die erstgenannte Regel: Primärschlüsselname = enthaltenes Objekt im Singular + ID.
Fremdschlüsselfelder Für die Benennung von Fremdschlüsselfeldern gibt es in der Praxis zwei Ansätze: Der einfachere verwendet einfach den Namen des Primärschlüsselfeldes der verknüpften Tabelle. Der zweite Ansatz macht noch ein wenig deutlicher, dass es sich bei diesem Feld definitiv um ein Fremdschlüsselfeld handelt, indem er dem Namen dieses Feldes noch das Präfix ref verpasst. Die zweite Variante hat Vorteile, wenn es um m:n-Beziehungen und 1:1-Beziehungen geht: Eine Tabelle mit zwei Feldern, die beide das Präfix ref enthalten, lässt sich zweifelsfrei als Verknüpfungstabelle identifizieren; und auch eine Tabelle, deren Primärschlüsselfeld das Präfix ref enthält, ist schnell als Erweiterungsteil einer 1:1-Beziehung enttarnt.
Allgemein Sowohl für Feldnamen von Primärschlüsselfeldern, Fremdschlüsselfeldern als auch für alle anderen Felder gilt, dass der Name des Feldes sorgfältig gewählt sein will. Am besten ist es, wenn er grobe Informationen über den Datentyp enthält: Text: Vorname, Nachname Datum: Geburtsdatum, Einstellungsdatum, Erscheinungstermin Zahlen: Anzahl, Meldebestand, Lagerbestand Währung: Einzelpreis Boolean: MehrwertsteuerAusweisbar, Aktiv, InReihenfolge
Unterstrich — ja oder nein? Ob Sie in Tabellen- und Feldnamen den Unterstrich als Trennung zwischen einzelnen Wörtern verwenden oder die einzelnen Wörter einfach groß beginnen, ist reine Ge schmackssache (in diesem Buch finden Sie übrigens ausschließlich letztere Variante). Wichtig ist, dass Sie es überhaupt hervorheben, wenn ein Tabellen- oder Variablenname aus mehr als einem Wort besteht.
90
Tabellen und Datenmodellierung
Beispiele: Anzahl_Datensaetze oder AnzahlDatensaetze Fahrzeug_ID oder FahrzeugID Man sollte es aber nicht übertreiben: Dem Autor sind schon Varianten untergekommen, in denen nicht nur jedes einzelne Wort, sondern fast jede Silbe großgeschrieben wurde (etwa MehrwertSteuersatz) – wenn ein Wort im Deutschen zusammengeschrieben wird, sind auch keine Großbuchstaben erforderlich. Einen deutlichen Vorteil hat die Verwendung von Großbuchstaben zur Unterteilung mehrerer Wörter: Sie müssen sich nicht merken, was Sie groß und was Sie klein geschrieben haben. Access ist äußerst nachsichtig, was die Groß-/Kleinschreibung von Objektnamen angeht. Einen Unterstrich zu viel oder zu wenig wird Access Ihnen hingegen nicht verzeihen.
Lang oder kurz — mit oder ohne Abkürzung? Die Zeiten, in denen die beschränkte Länge von Variablennamen die Fantasie der Programmierer beflügelte, sind zum Glück vorbei – und Gleiches gilt für Tabellenund Feldnamen. Die in der Zwischenüberschrift gestellte Frage nach der Länge von Feldnamen ist leicht zu beantworten: So lang wie nötig und so kurz wie möglich. Der Feldname sollte nicht in die Irre führen, nur um ein paar Zeichen zu sparen, andererseits lassen sich zu lange Feldnamen nicht gut merken. Wenn Sie für die Zusammenstellung jeder einzelnen SQL-Anweisung erst das Beziehungsfenster öffnen müssen, wissen Sie, dass Sie an den Tabellen- und Feldnamen arbeiten müssen. Für Tabellen- wie Feldnamen gilt außerdem: Die Länge kann sich negativ auf die Per formance auswirken. Wann immer Zeichenketten im Spiel sind, sollten Sie sich auf das Notwendigste beschränken.
2.3 Normalisierung Unter Normalisierung versteht man die Überführung des Datenmodells in einen bestimmten Zustand. Dieser Zustand wird durch die Nummer der so genannten Nor malform unterschieden. Meistens genügt das Erreichen der dritten Normalform, um Redundanzen und Inkonsistenzen vorzubeugen und dadurch die Wartung der enthalte nen Daten zu vereinfachen. Die Möglichkeit, Redundanzen und Inkonsistenzen zu vermeiden, ist eine der Haupt eigenschaften von relationalen Datenbanksystemen. Jeder, der schon einmal eine Da tenbank für einen Kunden entwickeln sollte, der die betroffenen Daten zuvor mit Excel
91
Kapitel 2
verwaltet hat, und das Vergnügen hatte, auch den Import dieser Daten vorzunehmen, weiß, was Redundanzen und Inkonsistenzen sind (das soll keine Verunglimpfung der Möglichkeiten von Excel sein – aber dessen Stärken liegen eher woanders). Ein gern gesehenes Beispiel ist die Verwaltung von Rechnungen in einer einzigen Tabelle. Dort finden sich unter Umständen alle Rechnungs- und Kundendaten zu einer Rechnung in einer Zeile. Sobald zwei Rechnungen für den gleichen Kunden gespeichert werden, gibt es auch zwei Kopien der Kundendaten in der Tabelle. Ändern sich die Kundendaten, werden diese Änderungen möglicherweise nur in einer neuen Zeile vorgenommen. Sobald ein anderer Mitarbeiter eine Rechnung für diesen Kunden stellen soll, steht er vor mindestens zwei verschiedenen Kunden-Datensätzen und das Unheil nimmt seinen Lauf. Um solches Ungemach zu verhindern, gibt es relationale Datenbanken, die Normalformen und die referentielle Integrität.
Halbautomatisches Normalisieren In den folgenden Abschnitten lernen Sie die unterschiedlichen Normalformen kennen und finden Beispiele für das Umwandeln nicht normalisierter Tabellen in die jeweilige Normalform. Die Zwischenüberschrift bezieht sich darauf, dass jeder Normalisierungsschritt nicht vollautomatisch abläuft. Genau genommen besteht jeder Schritt aus drei Teilen: Der ers te passt das Datenmodell an und der zweite sorgt für die Umorganisierung der vorhandenen Daten. Der dritte Schritt räumt auf und löscht eventuell nicht mehr benötigte Felder. Das Anpassen des Datenmodells und damit des Tabellenentwurfs erfolgt manuell. Das Umorganisieren der Daten kann – bei kleinen Datenmengen – ebenfalls manuell erfolgen, aber das ist sicher keine Arbeit für einen Entwickler: Der baut sich eine kleine Routine, die diesen Vorgang mit ein paar Aktionsabfragen oder im schlimmsten Fall ein paar ADO- oder DAO-Anweisungen automatisch durchführt.
Warum nicht direkt normalisieren? Der Grund für den Einsatz der Normalisierung liegt meist in Altlasten bezogen auf die Organisation der Daten vor der Erstellung einer Datenbankanwendung. Viele Datenbanken werden neu erstellt, weil bestehende Daten auf die bisherige Art und Weise nicht mehr verwaltet werden können – entweder es sind keine Erweiterungen mehr möglich, es wurden immer wieder neue Tabellen und Felder an das bestehende Datenmodell angestückelt und die Anwendung läuft nicht mehr schnell genug oder die Daten liegen in einem nicht für diesen Zweck geeigneten Format vor – etwa in Form von Excel-Tabellen. In diesen Fällen müssen Sie ein bestehendes Datenmodell normalisieren. Das bedeutet nicht, dass Sie mit spitzen Fingern an der Originaldatenbank herumschrauben müssen
92
Tabellen und Datenmodellierung
– meist werden Sie eher eine neue Datenbank erstellen und ein neues Datenmodell aufsetzen, das alle in dem vorhandenen Datenmodell enthaltenen Informationen beinhaltet. Anschließend werden Sie die Daten in die neue Datenbank importieren – natürlich entsprechend umorganisiert. Es kann natürlich auch sein, dass an einer bestehenden Datenbank hier und da ein kleines Problem besteht – in dem Fall werden Sie kein komplett neues Datenmodell ersinnen, sondern punktuelle Änderungen vornehmen. Auch wenn Sie die folgenden Regeln nicht unter dem Schlagwort »Normalisierung« kennen, werden Sie die eine oder andere vermutlich bereits anwenden – allein, weil sie einfach logisch erscheint und weil die meisten Entwickler, die Beispiele für Datenmodelle und damit Vorlagen für Gleichgesinnte veröffentlichen, wissen, wo es hapert. Damit Sie die Normalisierungsregeln gut verinnerlichen, finden Sie im Anschluss einige Beispiele für Datenmodelle, die diesen Regeln widersprechen; außerdem werden praktische Wege aufgezeigt, um solche Daten zu normalisieren.
2.3.1 Die erste Normalform Die erste Normalform fordert, dass jede in einem Feld gespeicherte Information atomar ist und nicht mehr in weitere Informationen unterteilt werden kann. Dadurch erreichen Sie, dass Sie die enthaltenen Werte einfach abfragen oder sortieren können. Beispiele für nicht atomare Informationen sind folgende in einem einzigen Feld gespeicherte Informationen: Vorname und Nachname Straße, Hausnummer, PLZ und Ort Wenn eine Tabelle ein Feld mit der Bezeichnung Name enthält und dieses sowohl den Vor- und den Nachnamen (in dieser Reihenfolge) wiedergibt, können Sie beispielsweise nur schwer nach dem Nachnamen sortieren. Vor- und der Nachname sind daher unbedingt in zwei Feldern zu speichern. Bei der Adresse bietet sich ein ähnliches Bild: Nach Datensätzen mit einem bestimmten Ort oder einer bestimmten PLZ wird oft gesucht. Diese Informationen sollten Sie daher in separaten Feldern speichern (siehe Abbildung 2.29 und Abbildung 2.30). Etwas anders sieht es bei der Straße und der Hausnummer aus: Die Angabe einer Straße hat ohne die Hausnummer zwar noch einen gewissen Informationsgehalt (wenn Sie zum nächsten IKEA fahren möchten, kommen Sie wahrscheinlich ohne die Angabe der Hausnummer aus), aber andersherum lässt sich mit einer Hausnummer allein wenig anfangen. Und das Sortieren danach macht in den meisten Fällen auch keinen Sinn. Fazit: Straße und Hausnummer sind quasi atomar und gehören deshalb normalerweise
93
Kapitel 2
in ein Feld. Und dort, wo diese Regel durch eine Ausnahme bestätigt wird, werden die Anforderungen schon durchblicken lassen, dass die Informationen besser in getrennte Felder gehören.
Abbildung 2.29: Tabelle mit Personendaten vor ...
Abbildung 2.30: ... und nach der Atomisierung
Um den Inhalt des Feldes Name aus Abbildung 2.29 in die beiden Felder Vorname und Nachname aus Abbildung 2.30 zu überführen, ist zumindest eine regelmäßige Anordnung des zusammengesetzten Feldes erforderlich – also entweder immer oder , . Dann lassen sich die Daten leicht von der einen in die andere Tabelle überführen, etwa mit folgender Prozedur: Public Sub NameAufteilen() Dim Dim Dim Dim Dim Dim Dim Dim
db As DAO.Database rst As DAO.Recordset longPos As Long strName As String strVorname As String strNachname As String strSQL As String lngLastSpace As Long
Set db = CurrentDb Set rst = db.OpenRecordset("tblPersonenNichtNormalisiert", _ dbOpenDynaset) Do While Not rst.EOF strName = rst!Name
94
Tabellen und Datenmodellierung lngLastSpace = InStrRev(strName, " ") strVorname = Mid(strName, 1, lngLastSpace) strNachname = Mid(strName, lngLastSpace + 1) strSQL = "INSERT INTO tblPersonenNormalisiert(Vorname, " _ & "Nachname) VALUES('" & strVorname & "','" & strNachname & "')" db.Execute strSQL rst.MoveNext Loop End Sub Listing 2.1: Extrahieren der Bestandteile des Feldes Name in Vor- und Nachname
Die Prozedur geht davon aus, dass die Reihenfolge ist und dass der Nachname keine Leerzeichen enthält. Sie schreibt den Teil des Namens hinter dem letzten Leerzeichen in das Feld Nachname und den Rest in das Feld Vorname. Wenn der Name auch mal in der anderen Reihenfolge und durch Komma getrennt angegeben ist, können Sie dies durch eine entsprechende Verfeinerung der Prozedur abfangen. Aber Unarten, wie zuerst den Nachnamen und dann den Vornamen zu schreiben, ohne dazwischen ein Komma einzufügen, sind leider ebenfalls gängig; hier hilft wohl nur manuelles Nacharbeiten. Ein anderes Beispiel, das nach der ersten Normalform schreit, sind mehrere gleichartige Informationen in Listenform in einem einzigen Feld wie in Abbildung 2.31.
Abbildung 2.31: Beispiel für nicht atomare Informationen
Ein erster naiver Ansatz, die im Feld Lieferanten enthaltenen Daten in atomare Feldinhalte umzuwandeln, sieht wie in Abbildung 2.32 aus. Das ist eine oft zu beobachtende Variante, um gleichartige Informationen zu einem Datensatz zu speichern. Leider birgt diese Variante mindestens drei Schwächen:
95
Kapitel 2
Früher oder später gibt es einen Artikel, der mehr Lieferanten als dafür vorgesehene Felder hat. Dann heißt es: Felder anfügen, Abfragen anpassen, Formulare anpassen, Code anpassen. Wenn ein Artikel weniger Lieferanten als dafür vorgesehene Felder hat, bleiben diese leer und verschwenden unnötig Platz. Wenn man nach Artikeln mit einem bestimmten Lieferanten sucht, muss man alle dafür vorgesehenen Felder durchgehen.
Abbildung 2.32: Atomar, aber nicht optimal: Felder mit gleichartigen Informationen
In diesem Fall gibt es nur eine Lösung: Da theoretisch jeder Artikel von jedem Lieferanten geliefert werden kann, muss eine m:n-Beziehung her. Das bedeutet, dass die Lieferanten in einer eigenen Tabelle gespeichert werden. Welcher Lieferant welchen Artikel liefert, speichern Sie in einer Verknüpfungstabelle, die jeweils die Nummer des Artikels und des Lieferanten aufnimmt. Das aus der Tabelle in Abbildung 2.32 entstehende Datenmodell sieht wie in Abbildung 2.33 aus.
Abbildung 2.33: Manchmal führt Atomatisieren zu m:n-Beziehungen
Um Daten wie aus der Tabelle tblArtikel_1 in Abbildung 2.32 in die Tabellen einer solchen m:n-Beziehung zu überführen, verwenden Sie etwa den Code aus folgendem Listing.
96
Tabellen und Datenmodellierung Public Sub AtomizeIntoMNRelation() Dim Dim Dim Dim Dim Dim
db As DAO.Database rstArtikel As DAO.Recordset rstLieferanten As DAO.Recordset strLieferant As String fld As DAO.Field lngLieferantID As Long
Set db = CurrentDb Set rstArtikel = db.OpenRecordset("tblArtikel_1", dbOpenDynaset) 'Alle Datensätze der Datensatzgruppe rstArtikel durchlaufen Do While Not rstArtikel.EOF 'Für alle Felder der Tabelle... For Each fld In rstArtikel.Fields '...kontrolliere, ob der Name mit 'Lieferant' beginnt If Left(fld.Name, 9) = "Lieferant" _ And Not IsNull(fld.Value) Then 'Prüfen, ob schon ein Lieferant mit diesem Namen 'vorhanden ist strLieferant = fld.Value Set rstLieferanten = db.OpenRecordset _ ("SELECT * FROM tblLieferanten WHERE Lieferant = '" _ & DoubleQuotes(strLieferant) & "'", dbOpenDynaset) 'Wenn noch nicht vorhanden, Lieferant 'in tblLieferanten anlegen If rstLieferanten.RecordCount = 0 Then rstLieferanten.AddNew rstLieferanten!Lieferant = strLieferant 'LieferantID merken lngLieferantID = rstLieferanten!LieferantID rstLieferanten.Update Else lngLieferantID = rstLieferanten!LieferantID End If End If 'Neuen Datensatz in Verknüpfungstabelle anlegen db.Execute "INSERT INTO tblArtikelLieferanten(ArtikelID, " _ & "LieferantID) VALUES(" & rstArtikel!ArtikelID _ & ", " & lngLieferantID & ")" Next fld
97
Kapitel 2 rstArtikel.MoveNext Loop End Sub Listing 2.2: Aufbrechen nicht atomarer Informationen in eine m:n-Beziehung
Das Resultat dieser Prozedur für die Daten aus der in Abbildung 2.32 gezeigten Tabelle finden Sie in Abbildung 2.34. Die Prozedur durchläuft alle Datensätze der ursprünglichen Tabelle und unterzieht die Inhalte aller Felder, deren Feldname mit Lieferant be ginnt (also Lieferant1, Lieferant2 …) und deren Feldinhalt nicht leer ist, einer gesonder ten Behandlung: Zunächst wird überprüft, ob die Tabelle tblLieferanten bereits einen Lieferanten mit dem angegebenen Namen enthält. Falls nein, wird ein entsprechender Datensatz in dieser Tabelle angelegt. Der Wert des Feldes LieferantID wird in jedem Fall festgehalten, um in einer abschließenden Aktionsabfrage einen neuen Datensatz in der Verknüpfungstabelle tblArtikelLieferanten anzulegen. Fast für jede Vorgehensweise gibt es Alternativen. Ein passendes Beispiel für das Anfü gen verknüpfter Daten finden Sie in Kapitel 13, Abschnitt 13.3.8 »Funktionale Fehlerbe handlung«.
Abbildung 2.34: Diese Daten entsprechen der ersten Normalform
2.3.2 Die zweite Normalform Die zweite Normalform besagt, dass alle Felder einer Tabelle vom Primärschlüssel be ziehungsweise vom ganzen Primärschlüssel abhängig sein müssen. »Ganzer« Primär schlüssel bezieht sich auf Tabellen mit mehreren Primärschlüsseln – das hört sich im ersten Augenblick unlogisch an, aber folgendes Beispiel wird verdeutlichen, wie das gemeint ist.
98
Tabellen und Datenmodellierung
Im Beispiel aus Abbildung 2.35 verwaltet jemand Kunden und Projekte in einer ExcelTabelle. Jeder Datensatz dieser Tabelle ist durch die Kombination der Felder KundeID und ProjektID eindeutig identifizierbar. Wenn man die Felder der Tabelle auf ihre Abhängigkeit vom Primärschlüssel untersucht, stellt man schnell fest, dass nicht alle vom »ganzen«, also zusammengesetzten Pri märschlüssel, abhängen. Die Kundendaten sind zwar vom Primärschlüsselfeld KundeID abhängig, aber nicht vom Feld ProjektID. Dadurch kann derselbe Kunde mehrmals in der Tabelle auftreten. Der in Abbildung 2.35 dargestellte Zustand heißt Redundanz. Unter diesen Bedingungen ist die Konsistenz der Daten gefährdet. Sobald man Informationen zu einem Kunden nur in einem der Da tensätze ändert, ist die Konsistenz dahin und die Integrität der Daten verloren.
Abbildung 2.35: Tabelle mit zwei Primärschlüsseln
Wie leicht aus einer Tabelle mit redundanten Daten Inkonsistenzen entstehen können, zeigt Abbildung 2.36. Hier wurde in einem neuen Datensatz der Name des Kunden falsch geschrieben. Die Inkonsistenz würde sich hier bemerkbar machen, wenn man alle Projekte nach dem Kunden »Addison-Wesley« filtert. Der letzte Eintrag mit dem Kunden namen ohne Bindestrich würde nicht angezeigt.
Überführung der Daten in die zweite Normalform Die Gefahr von Redundanzen und Inkonsistenzen lässt sich beheben, indem Sie die Daten so auf mehrere Tabellen aufteilen, dass alle Felder der Tabellen vom jeweiligen Primärschlüsselfeld beziehungsweise vom aus mehreren Feldern zusammengesetzten Primärschlüssel abhängen. Im obigen Beispiel gibt es zwei Primärschlüssel – KundeID
99
Kapitel 2
und ProjektID –, die beide abhängige Felder aufweisen. Die Notwendigkeit einer zweiten Tabelle ist offensichtlich. Die beiden Tabellen sehen wie in Abbildung 2.37 aus.
Abbildung 2.36: Tabelle mit inkonsistenten Daten
Abbildung 2.37: Tabellen mit ausschließlich vom Primärschlüssel abhängigen Feldern
Jetzt fehlt allerdings die Information, welches Projekt zu welchem Kunden gehört. Da in der Regel jedes Projekt nur für einen Kunden durchgeführt wird, halten Sie diese Information in der Tabelle tblProjekte fest, indem Sie per Fremdschlüsselfeld auf den je weiligen Primärschlüssel der Tabelle tblKunden verweisen. Abbildung 2.38 zeigt die Tabelle tblProjekte mit dem neuen Fremdschlüsselfeld, das mit dem Primärschlüsselfeld der Tabelle tblKunden verknüpft ist.
100
Tabellen und Datenmodellierung
Abbildung 2.38: Beziehung zwischen den Tabellen tblKunden und tblProjekte
2.3.3 Die dritte Normalform Die dritte Normalform sorgt dafür, dass es keine transitiven Abhängigkeiten innerhalb einer Tabelle gibt. Alle Nicht-Schlüssel-Felder müssen direkt vom Primärschlüssel der Ta belle abhängig sein. Oder andersherum: Es darf kein Feld geben, das Detailinformationen über ein anderes Feld enthält. Um sicherzugehen, dass eine Tabelle der dritten Normalform entspricht, prüfen Sie, ob Sie die Daten aller Felder mit Ausnahme des Primärschlüssels einzeln ändern können, ohne dass ein weiteres Feld in dieser Tabelle davon betroffen ist. Beispiel: Die Tabelle aus Abbildung 2.39 enthält neben dem Primärschlüsselfeld einige Felder, die von diesem abhängig sind. Im Falle des Feldes Verkaufsleiter besteht allerdings nur eine indirekte Abhängigkeit, da der Verkaufsleiter zunächst vom Feld Hersteller abhängt. Lösung: Der Hersteller samt Verkaufsleiter wird in eine eigene Tabelle ausquartiert. Das Ergebnis sieht wie im Datenmodell aus Abbildung 2.40 aus.
Abbildung 2.39: Diese Tabelle beruht nicht auf der dritten Normalform
Abbildung 2.40: Die Tabelle aus Abbildung 2.38 in der dritten Normalform
101
Kapitel 2
Felder mit berechneten Werten widersprechen der dritten Normalform Eine schlechte Angewohnheit und nur in ganz wenigen Fällen sinnvoll ist das Speichern von berechneten Werten in einer Tabelle wie im folgenden Beispiel (siehe Abbildung 2.41). Hier ist der Bruttopreis das Produkt aus Einzelpreis und Mehrwertsteuer. Solch ein Tabellenentwurf ist sehr anfällig für Inkonsistenzen. Wenn Sie nur einen der drei Werte ändern, ohne die Abhängigkeit zu berücksichtigen, stimmt die Berechnung nicht mehr.
Abbildung 2.41: Der Bruttopreis berechnet sich aus dem Einzelpreis und der Mehrwertsteuer
Die Änderung des Datenmodells fällt hier vergleichsweise einfach aus: Entfernen Sie das Feld Bruttopreis und verwenden Sie zu dessen Ermittlung eine Abfrage wie die folgende (siehe Abbildung 2.42). Das aus der Tabelle entfernte Feld wird durch ein in der Abfrage berechnetes Feld ersetzt. Sie brauchen lediglich das Format des berechneten Feldes auf Währung einzustellen, um ein mit der Ursprungstabelle identisches Ergebnis zu erhalten.
Abbildung 2.42: Realisieren eines berechneten Feldes per Abfrage
102
Tabellen und Datenmodellierung
2.3.4 Weitere Normalformen Die übrigen Normalformen sind eher akademischer Natur und werden deshalb hier nicht behandelt.
2.3.5 Das richtige Maß treffen Wenn man die Normalisierung aus Sicht der Performance betrachtet, verhält es sich so wie mit der Verwendung von Indizes auf Tabellenfeldern (mehr dazu in Kapitel 14, »Per formance«) – manchmal ist weniger mehr. Je mehr Tabellen und Beziehungen in einer Abfrage referenziert werden, desto langsamer wird die Abfrage. Patentrezepte für das richtige Maß an Normalisierung gibt es nicht. Es gibt aber für die meisten Fälle bereits in der Praxis erprobte Datenmodelle, nach denen Sie sich bei der Modellierung der eige nen Datenbank richten können.
2.4 Integritätsregeln Mit Integritätsregeln sorgen Sie dafür, dass die Tabellen einer Datenbank nur die für den jeweiligen Anwendungszweck gültigen Werte enthalten. Es gibt eine Menge unterschiedliche Arten von Integritätsregeln, die entsprechend auf unterschiedlichste Weise umgesetzt werden.
2.4.1 Integrität der Werte (Wertbereichsintegrität) Mit dem Datentyp eines Feldes schränken Sie die Menge der möglichen Eingaben schon relativ weit ein. Weitere Möglichkeiten bestehen in der Verwendung von Feldeigenschaf ten wie beispielsweise Gültigkeitsregel. Damit geben Sie eine Regel an, mit der die Gültigkeit der vorhandenen Daten geprüft wird. Dabei lassen sich durchaus flexible Ausdrücke eingeben. Beispiel: Es sollen keine Personen aufgenommen werden, deren Alter unter 18 Jahren liegt. Dazu stellen Sie die Eigenschaft Gültigkeitsregel auf den folgenden Ausdruck ein: <=DatAdd("jjjj";-18;Datum())
Verwenden Sie außerdem die Eigenschaft Gültigkeitsmeldung, damit Access bei Nichteinhalten der Regel eine entsprechende Meldung ausgibt (siehe Abbildung 2.43). Auch die beiden Eigenschaften Eingabe erforderlich und Leere Zeichenfolge sorgen für die Integrität der eingegebenen Werte. Die beiden Werte kombiniert liefern unterschiedliche Anforderungen an die Werte für ein Feld. Interessant zum Einschränken des Wertebereichs sind die Kombinationen aus Tabelle 2.2.
103
Kapitel 2
Abbildung 2.43: Anwendung einer Gültigkeitsregel
Eingabe erforderlich
Leere Zeichenfolge
Resultat
Ja
Nein
Wert darf nicht NULL sein.
Ja
Ja
Wert kann eine leere Zeichenfolge sein.
Tabelle 2.2: Relevante Kombinationen der Eigenschaften »Eingabe erforderlich« und »Leere Zeichenfolge«
2.4.2 Format der Werte (semantische Integrität) Im Tabellenentwurf können Sie für jedes Feld ein Format vorgeben, das bei der Eingabe eingehalten werden muss. Dies ist hilfreich, wenn die Werte ein bestimmtes Format haben müssen – etwa wenn eine Postleitzahl mit führender Länderkennung und mit Bindestrich angegeben werden muss (beispielsweise D-47137). In diesem Fall verwenden Sie folgenden Ausdruck für die Eigenschaft Eingabeformat: >L\-00009
Das Größer-Zeichen (>) sorgt dafür, dass alle enthaltenen Buchstaben automatisch groß gesetzt werden. Der Backslash signalisiert ein nachfolgendes Literal, die vier Nullen sind Pflichtzahlen und die Neun kennzeichnet eine freiwillige Zahl. Alle möglichen Zeichen und ihre Beschreibung finden Sie in Tabelle 2.3 (Quelle: Microsoft Access 2007, Onlinehilfe).
104
Tabellen und Datenmodellierung Zeichen
Beschreibung
0
Ziffer (0 bis 9, Eingabe erforderlich, Plus- [+] und Minuszeichen [–] sind nicht erlaubt).
9
Ziffer oder Leerzeichen (Eingabe nicht erforderlich, Plus- und Minuszeichen sind nicht erlaubt).
#
Ziffer oder Leerzeichen (Eingabe nicht erforderlich, Leerzeichen werden als Leerzeichen im Bearbeitungsmodus angezeigt, aber beim Speichern der Daten entfernt, Plus- und Minuszeichen sind erlaubt).
L
Buchstabe (A bis Z, Eingabe erforderlich).
?
Buchstabe (A bis Z, Eingabe optional).
A
Buchstabe oder Ziffer (Eingabe erforderlich).
a
Buchstabe oder Ziffer (Eingabe nicht erforderlich).
&
Ein beliebiges Zeichen oder ein Leerzeichen (Eingabe erforderlich).
C
Ein beliebiges Zeichen oder ein Leerzeichen (Eingabe nicht erforderlich).
.,:;–/
Platzhalter für Dezimaltrennzeichen sowie Tausender-, Datums- und Zeit-Trennzei chen (das tatsächlich verwendete Zeichen hängt von den Einstellungen im Dialogfeld Eigenschaften von Ländereinstellungen in der Systemsteuerung von Windows ab).
<
Alle Buchstaben werden in Kleinbuchstaben umgewandelt.
>
Alle Buchstaben werden in Großbuchstaben umgewandelt.
!
Bewirkt, dass die Anzeige im Eingabeformat von rechts nach links anstatt von links nach rechts erfolgt. Eingegebene Zeichen füllen das Eingabeformat immer von links nach rechts aus. Sie können das Ausrufezeichen-Symbol an jeder beliebigen Stelle im Eingabeformat einfügen.
\
Das folgende Zeichen wird als Literal angezeigt, die Wirkung als Sonderzeichen wird ggf. dadurch aufgehoben (zum Beispiel wird \A als A angezeigt).
Tabelle 2.3: Beschreibung der Zeichen für Eingabeformate
2.4.3 Abhängigkeit von Feldinhalten (Attributintegrität) Sie können bei der Datendefinition auch festlegen, dass bestimmte Abhängigkeiten zwischen den Inhalten verschiedener Felder einzuhalten sind. Wenn Sie etwa sicherstellen möchten, dass der maximale Rabatt eines Artikels vom Preis abhängt, verwenden Sie die Eigenschaften Gültigkeitsregel und Gültigkeitsmeldung der Tabelle selbst. Das Beispiel aus Abbildung 2.44 sorgt dafür, dass für Preise kleiner als EUR 1.000 nicht mehr als fünf Prozent Rabatt gewährt werden dürfen.
2.4.4 Eindeutige Datensätze (Entitätsintegrität) Weiter oben haben Sie bereits erfahren, dass jeder Datensatz einer Tabelle eindeutig sein sollte – dafür sorgen Sie unter Access im einfachsten Fall durch die Verwendung eines Primärschlüsselfeldes. Damit Sie sich keine Sorgen um die Auswahl eindeutiger Werte machen müssen, gibt es in Access den Datentyp Autowert. Neue Werte lassen sich
105
Kapitel 2
entweder inkrementell oder per Zufall generieren – für die entsprechende Einstellung ist die Eigenschaft Neue Werte zuständig (siehe Abbildung 2.45). Üblich ist bei einem Autowert die Verwendung inkrementeller neuer Werte.
Abbildung 2.44: Gültigkeitsregel für abhängige Feldinhalte
Abbildung 2.45: Inkrementelle oder zufällige neue Werte?
106
Tabellen und Datenmodellierung
2.4.5 Referentielle Integrität Referentielle Integrität sorgt für die Integrität der Beziehungen zwischen den Datensät zen verknüpfter Tabellen. Damit stellen Sie beispielsweise sicher, dass der Benutzer kein Projekt anlegen kann, ohne einen Kunden ausgewählt zu haben. Die Erstellung einer Beziehung mit referentieller Integrität erfordert, dass eines der beteiligten Felder das Primärschlüsselfeld seiner Tabelle oder zumindest ein eindeutiger Index ist, dass beide Felder kompatible Datentypen aufweisen und dass die beiden beteiligten Tabellen sich in der gleichen Datenbank befinden. Durch die Definition referentieller Integrität stellen Sie sicher, dass die in den Tabellen enthaltenen Daten in folgenden Punkten konsistent sind: Es gibt zu jedem Datensatz der Detailtabelle einen passenden Datensatz in der Mastertabelle. Datensätze der Mastertabelle, die mit mindestens einem Datensatz der Detailtabelle verknüpft sind, können standardmäßig nicht gelöscht werden. Access sorgt dafür, dass diese Grundsätze eingehalten werden, und gibt bei Verletzung dieser Regeln eine entsprechende Meldung aus. Optional lassen sich unter Access noch zwei weitere Automatismen einrichten: Löschweitergabe: Wenn ein Datensatz der Mastertabelle gelöscht wird, werden automatisch alle verknüpften Datensätze der Detailtabelle gelöscht. Aktualisierungsweitergabe: Wenn das Verknüpfungsfeld der Mastertabelle geändert wird, ändert Access automatisch das Verknüpfungsfeld aller Datensätze der Detailtabelle, die mit diesem Datensatz verknüpft sind. Referentielle Integrität legen Sie im Beziehungen-Fenster der Datenbank-Anwendung fest. Das Fenster zeigt die benötigten Tabellen und eventuell bereits bestehende Beziehun gen an. Um für eine Beziehung referentielle Integrität zu definieren, klicken Sie auf den Beziehungspfeil zwischen den beteiligten Tabellen und wählen per Kontextmenü den Eintrag Beziehung bearbeiten… aus. Dort finden Sie die an der Beziehung beteiligten Felder, können referentielle Integrität definieren und Aktualisierungs- und Löschweitergabe festlegen. Außerdem finden Sie hier die Angabe des Beziehungstyps, der sich aus der Art der verknüpften Felder ergibt, und die Möglichkeit, den Verknüpfungstyp anzupassen (siehe Abbildung 2.46). Zu all diesen Optionen erfahren Sie weiter unten mehr.
2.5 Beziehungen Mit Beziehungen legen Sie die Verknüpfungen zwischen den Tabellen einer Datenbank fest. Sie sind das A und O bei relationalen Datenbanken, denn Sie können damit nicht
107
Kapitel 2
nur festlegen, welche Felder der einen Tabelle mit den entsprechenden Feldern der anderen Tabelle verknüpft sind. Access bietet die Möglichkeit, etwa referentielle Inte grität zu definieren und dabei unterschiedliche Eigenschaften für die Beziehung festzulegen.
Abbildung 2.46: Bearbeiten einer Beziehung zwischen zwei Tabellen
Voraussetzung für das Erstellen einer Beziehung ist, dass mindestens eines der Felder das Primärschlüsselfeld seiner Tabelle ist. Die Tabelle mit dem Primärschlüsselfeld spielt die Rolle der Mastertabelle (auch Parent-Tabelle genannt) der Beziehung. Das Verknüpfungsfeld der anderen Tabelle heißt Fremdschlüsselfeld, die Tabelle mit dem Fremdschlüsselfeld ist die Detailtabelle (auch Child-Tabelle genannt). Eigentlich sollte man meinen, dass die Tabelle mit dem Fremdschlüsselfeld die Masterta belle sei und über die per Fremdschlüsselfeld referenzierte Tabelle Details enthielte und dementsprechend Detailtabelle hieße. Das erscheint zumindest bei jenen verknüpften Tabellen sinnvoll, die im Rahmen der Normalisierung ausgegliederte Informationen enthalten – also beispielsweise Anreden, Geschlecht oder Titel. Tatsächlich ist es aber umgekehrt – die Tabelle mit dem Primärschlüsselfeld in der Beziehung heißt Master- und die mit dem Fremdschlüsselfeld Detailtabelle. Beziehungen werden im Beziehungsfenster abgebildet und können dort auch erzeugt und bearbeitet werden. Es gibt aber auch die Möglichkeit, Beziehungen durch einen Assistenten erstellen zu lassen.
108
Tabellen und Datenmodellierung
2.5.1 Benennen von Primär- und Fremdschlüsselfeldern Einen Vorschlag für die Namen von Primärschlüsselfeldern haben Sie bereits weiter oben kennen gelernt – demnach soll das Primärschlüsselfeld aus dem Singular der Be zeichnung des mit den Tabellenfeldern beschriebenen Objekts plus der angehängten Zeichenkette »ID« bestehen – also etwa »ProjektID«, »MitarbeiterID«, »KundeID« oder »ArtikelID«. Das Fremdschlüsselfeld einer Tabelle enthält einen Wert, der dem Wert des Primärschlüs selfeldes der zu verknüpfenden Tabelle entspricht – deshalb sollten Sie es auch genauso nennen. Ein klassisches Beispiel sind Projekte und Kunden (siehe Abbildung 2.47). Die Projekte-Tabelle enthält hier ein Fremdschlüsselfeld, für das man den Wert des Primär schlüsselfeldes eines in der Tabelle tblKunden enthaltenen Datensatzes eintragen kann.
Abbildung 2.47: Beziehung zwischen Kunden und Projekten
Natürlich gibt es auch hier Ausnahmen – beispielsweise kommt es vor, dass eine Detail tabelle zwei Fremdschlüsselfelder enthält, die auf das gleiche Feld der Mastertabelle verweisen. Ein gutes Beispiel ist die Beziehung zwischen Artikeln und Firmen: Dort kann der eine Eintrag der Firmentabelle als Lieferant herhalten, während die andere Firma der Hersteller des Artikels ist (siehe Abbildung 2.48). HerstellerID und LieferantID scheinen hier adäquate Bezeichnungen zu sein, genauer wären allerdings HerstellerFirmaID und LieferantFirmaID – auf diese Weise ließe sich deutlicher erkennen, wohin die Ver knüpfung geht. Die Beziehung aus Abbildung 2.48 zeigt das Beziehungen-Fenster übrigens nicht automatisch an; nachdem Sie die Artikel-Tabelle einmal und die Firmen-Tabelle zweimal in diese Ansicht eingefügt haben, wird lediglich eine der beiden Beziehungen angezeigt. Die zweite fügen Sie zu Fuß hinzu, indem Sie das Feld LieferantID auf das Feld FirmaID der Tabelle tblFirmen_1 ziehen. Die zweite Inkarnation der FirmenTabelle wird im Beziehungen-Fenster nur mit anderem Namen angezeigt, um Ver wechslungen zu vermeiden – tatsächlich existiert nur eine Tabelle namens tblFirmen in der Datenbank.
109
Kapitel 2
Abbildung 2.48: Zwei Beziehungen zu der gleichen Tabelle erfordern unterschiedliche Fremdschlüsselnamen
2.5.2 Halbautomatisches Festlegen von Beziehungen Es gibt eine Möglichkeit, Access dazu zu bringen, automatisch eine Beziehung zwischen zwei Tabellen festzulegen. Das ist immer dann der Fall, wenn Sie mit dem NachschlageAssistenten eine Verknüpfung zwischen zwei Tabellen erstellen. Wenn Sie beispielsweise eine Kunden- und eine Projekte-Tabelle miteinander verknüpfen und dabei das Feld KundeID für die Auswahl des dem Projekt zugeordneten Kunden verwendet werden soll, gehen Sie folgendermaßen vor: Öffnen Sie die Tabelle tblKunden in der Entwurfsansicht. Wählen Sie für das Feld, über das die Beziehung hergestellt werden soll, den Datentyp Nachschlage-Assistent aus. Damit öffnen Sie den Nachschlage-Assistenten. Zum Herstellen der Beziehung zu einer anderen Tabelle wählen Sie im ersten Schritt die erste Option aus: Das Nachschlagefeld soll die Werte einer Tabelle oder Abfrage entnehmen. Wählen Sie im nächsten Schritt die Tabelle aus, mit der Sie das Feld verknüpfen möchten – in diesem Fall die Tabelle tblKunden. Im folgenden Schritt legen Sie fest, welche Felder der verknüpften Tabelle im Nachschlagefeld angezeigt werden sollen. Normalerweise wählt man dort den Pri märindex der Tabelle sowie ein Feld, dessen Inhalt den enthaltenen Datensatz am besten charakterisiert. Hier ist das der Name des Kunden, der im Feld Kunde gespeichert wird. Diese Einstellung können Sie später problemlos ändern. In den letzten drei Schritten können Sie noch eine Sortierung festlegen, angeben, ob die Spalte mit dem Primärschlüsselwert ausgeblendet werden soll, und einen Namen für das Fremdschlüsselfeld eingeben.
110
Tabellen und Datenmodellierung
Nach der Eingabe der benötigten Informationen nimmt der Assistent folgende Änderun gen am Fremdschlüsselfeld der Tabelle vor (siehe Abbildung 2.49): Ändern der Eigenschaft Steuerelement anzeigen auf den Eintrag Kombinationsfeld Einstellen einer Datensatzherkunft für dieses Steuerelement, hier: SELECT tblKunden.KundeID, tblKunden.Kunde FROM tblKunden;
Einstellen der Eigenschaften Spaltenanzahl und Spaltenbreiten auf die Werte 2 und 0cm;2,54cm. Dadurch wird nur die zweite Spalte der Datensatzherkunft angezeigt, während das in der Eigenschaft Gebundene Spalte angegebene erste Feld unsichtbar bleibt.
Abbildung 2.49: Der Nachschlage-Assistent ändert einige Eigenschaften eines Tabellenfeldes und der Tabelle
Außerdem fügt der Assistent die Beziehung zwischen dem Fremdschlüsselfeld der bearbeiteten Tabelle und dem Primärschlüsselfeld der verknüpften Tabelle hinzu (siehe Abbildung 2.50).
2.5.3 Festlegen referentieller Integrität Referentielle Integrität für die Beziehung zwischen zwei Tabellen können Sie ebenfalls im Beziehungen-Fenster festlegen. Dazu markieren Sie die Verbindungslinie zwischen den beteiligten Tabellen und wählen aus dem Kontextmenü den Eintrag Beziehung be-
111
Kapitel 2
arbeiten… aus. Alternativ können Sie auch einfach doppelt auf den Beziehungspfeil klicken.
Abbildung 2.50: Diese Verknüpfung wurde durch den Nachschlage-Assistenten erstellt
Im nun erscheinenden Dialog Beziehungen bearbeiten (siehe Abbildung 2.51) aktivieren Sie mindestens die Option Mit referentieller Integrität. Falls Tabellen, für deren Verknüpfung referentielle Integrität definiert werden soll, bereits Datensätze enthalten, kann es an dieser Stelle zu einer Fehlermeldung kommen, falls die Daten nicht der referentiellen Integrität entsprechen. Entweder Sie leben damit und passen anschließend eventuell zu Null geänderte Inhalte von Fremdschlüsselfeldern an oder kümmern sich vorher um die Integrität der Daten. Die Option Aktualisierungsweitergabe an verwandte Felder sorgt dafür, dass Änderungen am Primärschlüssel der Mastertabelle der Beziehung auf das Fremdschlüsselfeld der Detailtabelle übertragen werden. Mit der Option Löschweitergabe an verwandte Datensätze legen Sie fest, dass beim Löschen eines Datensatzes der Mastertabelle auch alle Datensätze der Detailtabelle gelöscht werden. Im Beispiel Kunden und Projekte bedeutet das Folgendes: Wenn Sie einen Kunden löschen, entfernt Access auch alle dazugehörenden Projekte.
Abbildung 2.51: Festlegen referentieller Integrität per Dialog
112
Tabellen und Datenmodellierung
2.5.4 1:n-Beziehungen Die 1:n-Beziehung ist die Mutter aller Beziehungen. Alle weiteren Beziehungstypen sind Sonderfälle der 1:n-Beziehung, wie Sie weiter unten erfahren werden. Deshalb ist es auch kein Zufall, dass gerade dieser Beziehungstyp für die einführenden Abschnitte zum Thema Beziehungen ausgewählt wurde. Da Sie dort bereits die wichtigsten Grundlagen zu diesem Thema kennen gelernt haben, werden in diesem Abschnitt lediglich einige Beispiele für den Einsatz von 1:n-Beziehungen vorgestellt. In anderen Büchern, Fachbeiträgen oder im Internet stoßen Sie vielleicht auch auf die Bezeichnung n:1-Beziehung. Prinzipiell ist daran nichts auszusetzen, da hier lediglich die Reihenfolge vertauscht wurde. In der in diesem Buch verwendeten Terminologie macht das allerdings sehr wohl einen Unterschied: Hier ist eine 1:n-Beziehung die Beziehung zwischen zwei Tabellen, die Eigenschaften eines Objekts enthalten – also beispielsweise Kunden, Projekte, Artikel, Produkte, Unternehmen oder Fahrzeuge. Wenn Sie eine Beziehung zwischen zwei solchen Tabellen herstellen, heißt sie in diesem Buch 1:n-Beziehung. Die nachfolgend vorgestellten n:1-Beziehungen verknüpfen eine der eben genannten Tabellen mit ausgegliederten Daten wie Anrede, Geschlecht, Titel bei Adressen, Kategorie bei Artikeln oder Fahrzeugart bei Fahrzeugen. Eine andere nachfolgend verwendete Bezeichnung für diese Tabellen ist Lookup-Tabelle.
Beispiele für 1:n-Beziehungen Im Folgenden einige Beispiele für 1:n-Beziehungen: Unternehmen und Personen als Ansprechpartner Projekte und Kunden Projekte und Mitarbeiter als Projektleiter Artikel und Firmen als Lieferanten Artikel und Firmen als Hersteller Diese Liste ließe sich beliebig fortsetzen. Weitere Beispiele finden Sie weiter unten in Abschnitt 2.7, »Datenmodell-Muster«.
2.5.5 n:1-Beziehungen oder Lookup-Beziehungen n:1-Beziehungen verknüpfen Tabellen, deren Eigenschaften Objekte beschreiben, mit jenen, die ausgegliederte Eigenschaften dieses Objekts enthalten. Wenn Sie sich etwa eine Adresstabelle vorstellen, die ein Textfeld zur Angabe der Anrede enthält, würden Sie vermutlich ungern zu jedem Eintrag »Herr« oder »Frau« manuell
113
Kapitel 2
hinzufügen wollen. Stattdessen finden Sie in der Regel ein Kombinationsfeld vor, mit dem sich der gewünschte Eintrag auswählen lässt. Das hat zwei entscheidende Vorteile: Erstens sparen Sie Tipparbeit bei der Eingabe dieses Feldes und zweitens sorgen Sie so dafür, dass nur die für das Feld vorgesehenen Einträge eingegeben werden können. Gleiches gilt für die Angabe des Geschlechts: In jeden Datensatz »männlich« oder »weiblich« einzutragen, ist eine sehr mühselige Arbeit, die Auswahl der Werte aber durchaus zumutbar. Bei der manuellen Eingabe dürfte sich außerdem früher oder später ein Tippfehler einschleichen, den Sie mit der Auswahlmöglichkeit ausschließen. Und diese Tippfehler können sich durchaus auswirken: Wenn Sie beispielsweise alle Datensätze der Tabelle ausgeben möchten, in denen das Feld Geschlecht den Wert »weiblich« hat, oder sich nur die Anzahl dieser Felder anzeigen lassen, sorgt eine einzige un richtige Schreibweise für ein falsches Ergebnis. Daher sollten Sie immer, wenn ein Feld häufig den gleichen Wert annimmt, ein Kombinationsfeld zur Eingabe der Daten in Erwägung ziehen, das aus den Daten einer verknüpften Tabelle gefüttert wird. Es gibt auch die Möglichkeit, in der Felddefinition eine Wertliste als Datensatzherkunft für ein Kombinationsfeld anzugeben. Dazu stellen Sie die in Abbildung 2.52 abgebildeten Ei genschaften ein.
Abbildung 2.52: Kombinationsfeldeinträge per Wertliste
Ausgliedern eines Feldes in eine separate Tabelle Folgendes Beispiel zeigt, wie Sie ein Feld aus einer Tabelle in eine neue Tabelle ausgliedern. Ausgangspunkt ist die Tabelle aus Abbildung 2.53, die das Feld Anrede für die
114
Tabellen und Datenmodellierung
manuelle Eingabe bereitstellt. Um das Feld auszugliedern, erstellen Sie zunächst eine neue Tabelle namens tblAnreden mit den beiden Feldern AnredeID und Anrede. Legen Sie außerdem in der Tabelle tblAdressen ein neues Feld namens AnredeID mit dem Datentyp Zahl zum Herstellen der Verknüpfung an.
Abbildung 2.53: Adressentabelle mit »hart codierten« Anreden
Nun brauchen Sie nur noch die verschiedenen Einträge des Feldes Anrede der Tabelle tblAdressen in die Tabelle tblAnreden zu übertragen und die dort verwendeten Werte für das Feld AnredeID in das gleichnamige neue Feld der Tabelle tblAdressen einzutragen. Keine Frage, dass Sie das für wenige Datensätze von Hand erledigen können, aber wenn die Adressen-Tabelle mehrere hundert Datensätze enthält, verwenden Sie vielleicht besser zwei Aktionsabfragen oder die folgende VBA-Prozedur. Der Aufruf für den hier vorliegenden Fall lautet folgendermaßen: FeldAusgliedern "tblAdressen", "tblAnreden", "AnredeID", "Anrede"
Die Prozedur erwartet den Namen der Ausgangstabelle und der Zieltabelle sowie den Namen des Primärschlüsselfeldes der Zieltabelle und des Zielfeldes der auszugliedernden Daten. Diese beiden Felder heißen hier AnredeID und Anrede und müssen in der Ziel- und in der Ausgangstabelle gleich benannt sein: Public Sub FeldAusgliedern(strAusgangstabelle As String, strZieltabelle _ As String, strSchluesselfeld As String, strFeldname As String) Dim db As DAO.Database Dim rst As DAO.Recordset Dim lngSchluesselfeld As String Set db = CurrentDb Set rst = db.OpenRecordset(strAusgangstabelle, dbOpenDynaset) Do While Not rst.EOF 'Ermitteln, ob Eintrag schon in Lookuptabelle vorhanden ist lngSchluesselfeld = Nz(DLookup(strSchluesselfeld, _ strZieltabelle, strFeldname & " = '" & rst(strFeldname) _ & "'"), 0)
115
Kapitel 2 'Falls nicht, diesen Eintrag hinzufügen... If lngSchluesselfeld = 0 Then db.Execute "INSERT INTO " & strZieltabelle & "(" _ & strFeldname & ") VALUES('" & rst(strFeldname) & "')" '...und Primärschlüssel ermitteln lngSchluesselfeld = Nz(DLookup(strSchluesselfeld, _ strZieltabelle, strFeldname & " = '" & rst(strFeldname) _ & "'"), 0) End If 'Verweisfeld auf Lookuptabelle mit Wert füllen rst.Edit rst(strSchluesselfeld) = lngSchluesselfeld rst.Update rst.MoveNext Loop Set rst = Nothing Set db = Nothing End Sub Listing 2.3: Ausgliedern von Daten in eine Lookup-Tabelle
Abbildung 2.54 zeigt das Ergebnis der Ausgliederung. Die im Feld Anrede vorhandenen Werte wurden in die Tabelle tblAnreden eingetragen und die Werte des dortigen Primärschlüsselfeldes in das neue Fremdschlüsselfeld AnredeID der Tabelle tblAdressen. Zur Kontrolle ist das alte Feld Anrede noch in der Ausgangstabelle vorhanden, dieses können Sie aber ohne Bedenken löschen.
Abbildung 2.54: Ergebnis der Ausgliederung eines Feldes in eine zusätzliche Tabelle
Damit Sie die Werte auch per Kombinationsfeld aus der Lookup-Tabelle auswählen können, legen Sie mit dem Nachschlage-Assistenten eine Beziehung zwischen den bei-
116
Tabellen und Datenmodellierung
den Tabellen an. Wie das funktioniert, haben Sie bereits in Abschnitt 2.5.2, »Halbautoma tisches Festlegen von Beziehungen« erfahren.
2.5.6 m:n-Beziehungen m:n-Beziehungen sind nichts weiter als zwei 1:n-Beziehungen, die zwei Tabellen über eine Hilfstabelle miteinander verknüpfen. Im Gegensatz zu einer einzelnen 1:nBeziehung, mit der sich beliebig viele Datensätze der einen Mastertabelle mit einem Datensatz der Detailtabelle verknüpfen lassen, ist das Ziel der m:n-Beziehung, dass sich jeder Datensatz der ersten Tabelle mit beliebig vielen Datensätzen der zweiten Tabelle verknüpfen lässt und umgekehrt. Beispiele für solche Beziehungen gibt es viele. Das bekannteste ist wohl die Verknüpfung der Bestellungen-Tabelle mit der Artikel-Tabelle über eine Bestelldetails-Tabelle wie in der Nordwind-Datenbank (siehe Abbildung 2.55).
Abbildung 2.55: Klassisches Beispiel einer m:n-Beziehung: Bestellungen und Artikel in der Nord wind-Datenbank
Diese Variante ist zugleich eine kompliziertere Form der m:n-Beziehung, die in der Ver knüpfungstabelle zusätzliche Daten speichert.
m:n-Beziehung am Beispiel von Fahrzeugen und Sonderausstattungen Ein einfacheres Beispiel sind Ausstattungsmerkmale von Fahrzeugen. Fahrzeuge haben einige unveränderliche Eigenschaften wie Marke, Modell, Leistung und so weiter. Außerdem besitzt jedes Fahrzeug verschiedene Ausstattungsmerkmale, die aber bei dem einen vorhanden und bei dem anderen nicht vorhanden sind. Ein nicht normalisiertes Datenmodell würde aus einer einzigen Tabelle mit einigen Dutzend Ja/Nein-Feldern für die einzelnen Ausstattungsmerkmale bestehen. Das ist legitim, kann aber zu Problemen
117
Kapitel 2
führen: Zwar ändern sich die gängigen Ausstattungsmerkmale nur alle Jubeljahre, aber sie ändern sich, und damit müssten Sie auch die komplette Datenbank von der Tabelle bis zu den Formularen, Berichten und VBA-Modulen anpassen. Außerdem wird für jede Ausstattung, die nicht vorhanden ist, Speicherplatz verschwendet. Also legen Sie eine Tabelle mit sämtlichen Ausstattungsmerkmalen an und sorgen mit einer Verknüp fungstabelle dafür, dass Sie alle Fahrzeuge mit allen Ausstattungsmerkmalen kombinieren können (siehe Abbildung 2.56).
Abbildung 2.56: m:n-Beziehung am Beispiel von Fahrzeugen und ihrer Ausstattung
Die Verknüpfungstabelle ist dabei nichts anderes als eine Tabelle mit zwei Fremd schlüsselfeldern, die die beiden Primärschlüsselfelder der zu verknüpfenden Tabellen referenzieren. In der Entwurfsansicht sieht die Verknüpfungstabelle wie in Abbildung 2.57 aus. Dort wird Ihnen vermutlich zuerst auffallen, dass es dort zwei als Primärschlüs sel gekennzeichnete Felder gibt. Genau genommen ist dies ein zusammengesetzter Pri märschlüssel, der verhindert, dass eine Kombination der beiden Felder zweimal einge geben wird. Schließlich soll jedes Ausstattungsmerkmal jedem Fahrzeug nur einmal zugewiesen werden.
Abbildung 2.57: Entwurfsansicht einer m:n-Verknüpfungstabelle
118
Tabellen und Datenmodellierung
Verknüpfungstabellen mit zusätzlichen Daten: Bestellungen und Artikel Nun können Sie sich der Bestelldetails-Tabelle der Nordwind-Datenbank zuwenden, die bereits oben kurz vorgestellt wurde. Dort befinden sich neben den beiden Fremdschlüs selfeldern zum Herstellen der Beziehung noch weitere Felder zum Speichern von Einzel preis, Anzahl und Rabatt des jeweiligen Artikels. Dabei handelt es sich um individuelle Daten für jede Kombination aus Bestellung und Artikel. Dass die Anzahl flexibel sein muss, ist klar, aber warum Einzelpreis und Rabatt? Der Einzelpreis ist zwar bereits in der Tabelle tblArtikel festgelegt, es aber kann durchaus sein, dass der Preis sich einmal ändert. Und wenn Sie diesen dann nur in der Artikel-Tabelle gespeichert haben und ihn dort aktualisieren, dann wirkt sich das auch auf die Rechnungsbeträge aller bisherigen Bestellungen aus. Daher muss der Preis unbedingt in Zusammenhang mit der Kombination aus Bestellung und Artikel gespeichert werden. Und der Rabatt ist ohnehin eine Größe, die je nach Kunde oder je nach Angebot flexibel gestaltet wird – daher macht auch die Aufnahme dieses Feldes in die Verknüpfungstabelle Sinn.
Alternative Verwaltung von Daten aus m:n-Beziehungen In einfachen Fällen wie bei den Ausstattungsmerkmalen für Fahrzeuge können Sie auch ein mehrwertiges Feld verwenden. Weitere Informationen zu mehrwertigen Feldern fin den Sie weiter oben.
Weitere Beispiele für m:n-Beziehungen m:n-Beziehungen treten an vielen Stellen auf. Hier finden Sie zwei weitere Beispiele: Verteiler: Jeder Verteiler basiert auf einer m:n-Beziehung. Die beiden zu verknüpfen den Tabellen enthalten die Publikation auf der einen und die Adressaten auf der anderen Seite. Die Verknüpfungstabelle ist einfach, es sind außer den beiden Fremd schlüsselfeldern keine weiteren Felder notwendig. Projektteams: Jedes Projektteam besteht aus einem oder mehreren Mitarbeitern, und jeder Mitarbeiter gehört zu einem oder mehreren Projektteams. Der Verknüpfungs tabelle könnte man eine dritte Verknüpfung hinzufügen, um die Funktion des jeweiligen Teammitglieds festzulegen. Abbildung 2.58 zeigt das Datenmodell dieser Verknüpfung, bei der es sich eigentlich sogar um eine m:n:o-Verknüpfung handelt.
2.5.7 1:1-Beziehungen 1:1-Beziehungen trifft man relativ selten an, obwohl sie sehr hilfreich sein können. Eine 1:1-Beziehung verknüpft jeden Datensatz mit nur einem Datensatz der verknüpften
119
Kapitel 2
Tabelle und umgekehrt. Wozu soll das nun hilfreich sein? Solche Daten kann man doch auch in eine Tabelle schreiben? Diese Fragen sind durchaus berechtigt. Deshalb lernen Sie nun die Gründe und Einsatzmöglichkeiten für 1:1-Beziehungen kennen. Es gibt häufig Fälle, in denen Tabellen aus endlos vielen Feldern bestehen. Das liegt oft daran, dass die Tabellen recht verschiedenartige Daten enthalten sollen, von denen jede Art eigene Eigenschaften besitzt und damit neue Felder erzeugt.
Abbildung 2.58: m:n-Verknüpfung mit dritter Verknüpfung
Beispiel: Unterschiedliche Mitarbeiterarten Ein Unternehmen sammelt die Daten aller Mitarbeiter in einer Tabelle und unterscheidet dabei zunächst nicht zwischen fest angestellten und freien Mitarbeitern. Die Tabelle sieht wie in Abbildung 2.59 aus.
Abbildung 2.59: Entwurf einer Tabelle mit teilweise nicht benötigten Feldern
Enthält die Tabelle einen Angestellten, wird das Feld Stundensatz nicht benötigt, weil der Angestellte ein Gehalt bekommt. Handelt es sich um einen freien Mitarbeiter, sind die
120
Tabellen und Datenmodellierung
Felder Personalnummer, Gehalt und AbteilungID überflüssig. Eine reale Mitarbeitertabelle enthält sicher noch viele weitere nützliche Felder für den Angestellten und den freien Mitarbeiter, die aber im jeweils anderen Fall nicht benötigt werden. Oder konkret ausgedrückt: Hier wird einiges an Speicherplatz verschenkt.
Tabellen aufteilen und wieder verknüpfen Um dies zu verhindern, trennt man einfach die nur für den Angestellten oder den freien Mitarbeiter vorgesehenen Felder aus der Tabelle heraus und fügt diese in zwei weitere Tabellen namens tblAngestellte und tblFreieMitarbeiter ein. Die Tabellen sehen nun so wie in Abbildung 2.60 aus. Die Tabelle tblMitarbeiter enthält nur noch die Daten, die für beide Mitarbeiterarten gelten. Die Tabelle tblFreieMitarbeiter enthält ein eigenes Primär schlüsselfeld, ein eindeutiges Feld namens PersonID zur Herstellung der 1:1-Beziehung und ein Feld mit den speziellen Informationen zu freien Mitarbeitern – hier den Stun densatz. Die Tabelle tblAngestellte ist genauso aufgebaut, enthält aber die angestelltenspezifischen Informationen.
Abbildung 2.60: Aufteilung einer Tabelle in eine Haupt- und zwei Untertabellen
Voraussetzung: Eindeutige Schlüsselfelder auf beiden Seiten der Beziehung 1:1-Beziehungen verbinden zwei Tabellen über eindeutige Felder. Primärindexfelder sind eindeutig und auch andere Felder können Sie als eindeutig festlegen. Dazu erstellen Sie einfach einen entsprechenden Index über den Indizes-Dialog. Diesen zeigen Sie
121
Kapitel 2
an, indem Sie die gewünschte Tabelle in der Entwurfsansicht öffnen und den RibbonEintrag Entwurf|Einblenden/Ausblenden|Indizes auswählen. Nach dem Öffnen dieses Dialogs werden Sie vermutlich verwundert sein, dass Access nicht nur für das Primärschlüsselfeld, sondern auch noch für einige andere Felder scheinbar willkürlich Indizes angelegt hat (siehe Abbildung 2.61). Die Willkür hält sich aber in Grenzen: Access legt standardmäßig für alle Felder, deren Name eine der Zeichenketten »ID«, »Schlüssel«, »Code« oder »Nummer« enthält, einen Index an. Diese Einstellung können Sie in den Access-Optionen unter Objekt-Designer|Tabellenentwurf| Autoindex beim Importieren/Erstellen anpassen (siehe Abbildung 2.62).
Abbildung 2.61: Einstellen eines eindeutigen Index
Wenn Sie in beiden Tabellen einen eindeutigen Index für das Feld PersonID angelegt haben, können Sie zum Verknüpfen schreiten. Dazu zeigen Sie wie gewohnt die zu verknüpfenden Tabellen im Beziehungen-Fenster an. Nun kommt der wichtigste Schritt und hier müssen Sie besonders auf die Reihenfolge achten: Ziehen Sie das Feld PersonID der Ausgangstabelle, also der Tabelle tblPersonen, in das Feld PersonID der Tabelle tblAngestellte. Definieren Sie referentielle Integrität und aktivieren Sie die Option Löschweitergabe an verwandte Datensätze. Hierfür ist die Reihenfolge wichtig: Der Dialog Beziehungen bearbeiten zeigt unter Tabelle/Abfrage die Ausgangs tabelle und unter Verwandte Tabelle/Abfrage die Zieltabelle einer Löschweitergabe an. Die Löschweitergabe soll von der Tabelle tblPersonen ausgehen und nicht umgekehrt, daher muss auch der Beziehungspfeil von dieser Tabelle ausgehen (siehe Abbildung 2.63).
122
Tabellen und Datenmodellierung
Übrigens: Sollte der Dialog Beziehungen bearbeiten im unteren Bereich nicht 1:1 als Be ziehungstyp anzeigen, müssen Sie nochmals die Eindeutigkeit der beteiligten Tabellen prüfen.
Abbildung 2.62: Anpassen der Feldnamen, die das automatische Anlegen eines Index forcieren
Abbildung 2.63: 1:1-Beziehung zwischen zwei Tabellen
123
Kapitel 2
Nach dem Anlegen der beiden 1:1-Beziehungen sieht das Ergebnis wie in Abbildung 2.64 aus.
Abbildung 2.64: Tabelle mit zwei 1:1-Beziehungen
Wie arbeitet man mit per 1:1-Beziehung verknüpften Tabellen? Eine solche Beziehung können Sie prinzipiell wie eine ganz normale Tabelle behandeln – Sie müssen nur eine geeignete Abfrage anlegen, um die Daten wieder zusammenzufüh ren. Wie dies funktioniert und wie Sie Formulare nutzen, um solche Daten zu bearbeiten, erfahren Sie in Kapitel 3, »Abfragen«, Abschnitt 3.7 und in Kapitel 4, »Formulare«, Abschnitt 4.5.5.
Wo kommen 1:1-Beziehungen sonst noch zum Einsatz? Neben der »Spezialisierung« von Tabellen durch Anhängen von Tabellen mit weiteren Informationen gibt es noch weitere Gründe für den Einsatz von 1:1-Beziehungen: Eine Tabelle hat mehr als 255 Felder. Sicher gibt es Objekte, die so viele Eigenschaften mitbringen. In der Regel sollte man aber das Datenmodell einer genaueren Prüfung unterziehen, wenn eine Tabelle derart viele Felder besitzt. Eine Tabelle enthält eine Menge Felder, die aber nur selten benötigt werden. Diese gliedert man wie im obigen Beispiel in eine weitere Tabelle aus und gibt dort bei Bedarf Daten ein. Beispiel: Ein Ja/Nein-Feld, das Datensätze beispielsweise zum Dru cken festlegt. Dieses Feld benötigt man nie, außer wenn man zu druckende Daten sätze festlegen oder die ausgewählten Datensätze drucken möchte. Also erstellen Sie einfach eine eigene Tabelle und verknüpfen diese mit der Zieltabelle.
2.5.8 Reflexive Beziehungen Reflexive Beziehungen sind Beziehungen, die Datensätze einer Tabelle mit Datensätzen der gleichen Tabelle verknüpfen. Dabei enthalten die verknüpften Datensätze meist un-
124
Tabellen und Datenmodellierung
terschiedliche Rollen, etwa Vorgesetzter und Untergebener. Dabei handelt es sich prinzipiell um eine klassische 1:n-Beziehung – es befindet sich lediglich die gleiche Tabelle auf beiden Seiten. Reflexive Beziehungen (manchmal auch rekursive Beziehungen genannt) können auch über eine Zwischentabelle hergestellt werden. Auf diesem Wege erhalten Sie dann eine reflexive m:n-Beziehung, bei der Sie jeden Datensatz der Tabelle mit beliebigen anderen Datensätzen der gleichen Tabelle verknüpfen können.
Reflexive 1:n-Beziehungen Die Tabelle in Abbildung 2.65 greift das oben genannte Beispiel der Beziehung zwischen Mitarbeitern und Vorgesetzten auf. Dazu enthält die Tabelle tblMitarbeiterMitVorgesetzte ein Feld namens VorgesetzterID mit dem Datentyp Zahl.
Abbildung 2.65: Entwurf einer Tabelle mit reflexiver Beziehung
Wer nun versucht, den Nachschlage-Assistenten zum Erstellen der gewünschten Bezie hung zu bewegen, wird feststellen, dass dieser nur andere Tabellen zum Erstellen von Beziehungen anbietet, aber nicht die, für die eine Beziehung erstellt werden soll. Hier ist also Handarbeit angesagt: Öffnen Sie also den Beziehungen-Dialog und fügen Sie die Tabelle tblMitarbeiterMitVorgesetzten hinzu. Da sich auch dieser Dialog etwas bo ckig anstellt, wenn man die Tabelle mit sich selbst verknüpfen möchte, hilft nur noch ein Trick: Fügen Sie eine zweite Instanz der Tabelle hinzu, indem Sie die Tabelle tblMitarbeiterMitVorgesetzten noch einmal einfügen. Anschließend können Sie die Beziehung wie bei einer normalen 1:n-Beziehung hinzusetzen; auch referentielle Integrität lässt sich problemlos festlegen (siehe Abbildung 2.66). Das Aktivieren der Löschweitergabe ist übrigens nicht zu empfehlen – wenn beim Lö schen des Chefs auch gleich alle Untergebenen aus der Datenbank verschwinden, wird der neue Chef nicht besonders glücklich sein … Informationen über die Anzeige von Datensätzen, die in reflexiver Beziehung zueinander stehen, finden Sie in Kapitel 4, »Formulare«, Abschnitt 4.5.12.
125
Kapitel 2
Abbildung 2.66: Rekursive Beziehungen lassen sich im Beziehungen-Fenster nur über mehrere Instanzen derselben Tabelle anlegen
Reflexive m:n-Beziehungen Weniger bekannt, da sehr selten verwendet, sind reflexive m:n-Beziehungen. Eines der rar gesäten Beispiele sind die Teile einer Produktdatenbank. Ein Produkt besteht aus mehreren Teilen, die wiederum aus anderen Teilen zusammengesetzt sind. Hier werden also nicht nur Endprodukte verwaltet, die aus hunderten von Einzelteilen bestehen, sondern auch Baugruppen, die Bestandteil anderer Baugruppen sind und wiederum weitere Baugruppen enthalten können. Warum reicht hier eine reflexive 1:n-Beziehung nicht aus? Ganz einfach: Weil ein Teil oder eine Baugruppe nicht einer anderen Baugruppe, sondern mehreren Baugruppen als Bestandteil zur Verfügung stehen soll. Und wie realisieren Sie eine reflexive m:nBeziehung? Wie eine ganz normale m:n-Beziehung! Der einzige Unterschied ist, dass Sie zwei Instanzen der Tabelle tblProdukte statt zwei unterschiedliche Tabellen verwenden (siehe Abbildung 2.67).
Abbildung 2.67: Reflexive m:n-Beziehung
Die Verknüpfungstabelle enthält – da die Namen der Primärschlüsselfelder beider verknüpften Tabellen gleich sind – Fremdschlüsselfelder mit Namen, die den Datensätzen
126
Tabellen und Datenmodellierung
der verknüpften Tabellen gleichzeitig die Rolle in der Verknüpfung zuweisen. In diesem Fall ist das übergeordnete Element die Baugruppe und das untergeordnete Element ein Teil. Zusätzlich enthält die Verknüpfungstabelle ein Feld namens Anzahl, damit man festlegen kann, wie viele Teile einer Sorte die Baugruppe enthält.
2.6 Autowerte als Long oder GUID? In Access ist die Verwendung von Autowerten als Primärschlüssel praktisch als Stan dard anzusehen. In manchen Fällen leisten GUIDs allerdings wertvolle Dienste, die Autowerte nicht leisten können. So macht es beispielsweise Sinn, Tabellen mit einer GUID als Autowert zu versehen, deren Daten gelegentlich archiviert und dazu in eine andere Tabelle übertragen und aus der ursprünglichen Tabelle gelöscht werden – gegebenenfalls befindet sich die Archivtabelle sogar in einer anderen Datenbank. Sollten Sie diese Daten noch einmal in der Originaltabelle benötigen, müssen Sie sicherstellen, dass die Daten unter dem alten Primärschlüssel eingetragen werden können. Das ist mit herkömmlichen Autowerten nicht möglich. Wenn Sie etwa den neuesten Datensatz einer Tabelle in die Archivdatenbank übertragen und die Originaldatenbank komprimieren, wird der Autowertzähler so eingestellt, dass er als Nächstes die Zahl verwendet, die um eins größer als die bisher größte verwendete Zahl ist. Das heißt, dass unter Umständen der Primärschlüsselwert des archivierten Datensatzes bereits vergeben ist. Bei der Verwendung von GUIDs als Primärschlüssel können Sie archivierte Datensätze problemlos wieder einfügen. Das liegt daran, dass ein Wert des Typs GUID weltweit einzigartig ist. Wenn Sie sich ein Beispiel für einen solchen Wert ansehen, verstehen Sie, warum das tatsächlich wahr sein kann: {B9FF24C2-C32D-4053-B5FB-FCAF8AC8C7FC}
Eine GUID besteht aus 32 Zeichen, von denen jedes Zeichen mit einer Zahl von 0 bis 9 oder einem Buchstaben von A bis F gefüllt werden kann – also mit einer hexadezimalen Zahl. Das ergibt insgesamt 3,4 × 1038 Möglichkeiten. Ein weiterer Anwendungszweck für GUIDs ist die Replikation und Synchronisation. Bei der Replikation kann man eine oder mehrere Kopien einer Datenbank erstellen, die dann unabhängig voneinander geändert und anschließend synchronisiert werden kön nen. Dazu gehört auch, dass man in den unterschiedlichen Replikationen neue Datensätze anlegt. Auch dort werden GUIDs zur eindeutigen Kennzeichnung der Datensätze ver wendet.
127
Kapitel 2
Nachteile von GUIDs Da GUID-Werte 16 Byte lang sind, wirkt sich das natürlich negativ auf die Performance bei der Suche und Abfrage solcher Daten aus. Außerdem ist der Umgang mit GUIDWerten in Abfragen und unter VBA nicht ganz einfach, weil sie dort in Strings umgewandelt werden müssen.
2.7 Datenmodell-Muster Auch die Kenntnis der einzelnen Beziehungstypen und der Normalisierung garantiert noch lange kein perfektes Datenmodell. Dazu gehört auch eine Menge Erfahrung oder eine Vorlage, von der man weiß, dass sie bereits erfolgreich in der Praxis eingesetzt wurde. Die gute Nachricht ist, dass es bei den meisten Anwendungen nur einen Weg gibt, um das Datenmodell umzusetzen – natürlich unter Berücksichtigung der Normali sierung. Den muss man allerdings erst einmal finden – und dabei soll die nachfolgende Sammlung von Datenmodell-Mustern helfen. Es handelt sich dabei um grundlegende Datenmodelle für verschiedene Anwendungsfälle – wobei nicht nur geschäftliche Themen betrachtet werden, sondern auch die eine oder andere Heimanwendung unter die Lupe genommen wird.
2.7.1 Adressen-/Kundenverwaltung Wer mit Access arbeitet, hat in den meisten Fällen auch schon einmal eine Adressver waltung programmiert, wenn er sich nicht sogar am Beispiel einer Adressverwaltung in Access einarbeiten durfte. So trivial wie diese Anwendung zunächst scheint, so kompliziert kann sie in bestimmten Fällen werden. Das gilt insbesondere dann, wenn nicht nur Adressen mit den üblichen Daten wie Name, Straße, PLZ, Ort und den Kontaktdaten wie Telefon oder E-Mail gefragt sind, sondern auch die Unternehmen der jeweiligen Per sonen ins Spiel kommen. All diese Daten lassen sich leicht in einer Tabelle unterbringen, die beispielsweise tblPersonen heißt. Unter Umständen hängt an dieser Tabelle noch eine Lookup-Tabelle mit den Anreden (siehe Abbildung 2.68). Was auffällt, sind die vielen Kontaktmöglichkeiten via Telefon oder E-Mail. Diese lassen sich leicht in eine weitere Tabelle ausgliedern, die per 1:n-Beziehung mit der Tabelle tblPersonen_1 verknüpft wird (siehe Abbildung 2.69). Wenn Sie aber mehr aus den Adressen machen möchten – etwa um sich in Richtung Customer Relation Management zu bewegen – wird es komplizierter. Der erste Schritt in diese Richtung ist, dass Sie die im Feld Firma gespeicherten Unternehmen in einer eigenen Tabelle speichern und von der Tabelle tblPersonen auf diese Tabelle verweisen.
128
Tabellen und Datenmodellierung
Dadurch können Sie alle Unternehmensdaten in einer einzigen Tabelle unterbringen und diese konsistent halten.
Abbildung 2.68: Datenmodell einer einfachen Adressverwaltung
Abbildung 2.69: Adressentabelle mit ausgegliederten Kontaktmöglichkeiten
Abbildung 2.70 zeigt, wie das Datenmodell nach dieser weiteren Änderung aussieht. Die Unternehmensdaten sind komplett in der Tabelle tblUnternehmen_2 untergebracht, auf die nun von der Tabelle tblPersonen_2 verwiesen wird. Dies ist ein Zustand, auf dem man eine CRM-Anwendung aufbauen kann – Unternehmen und Personen befinden sich in einzelnen, miteinander verknüpften Tabellen.
129
Kapitel 2
Damit ist sichergestellt, dass nicht zwei Datensätze der Personen-Tabelle unterschiedliche Firmendaten enthalten, wie das noch in der Fassung in Abbildung 2.69 möglich war. Außerdem können Sie jedem Unternehmen beliebig viele Personen zuordnen.
Abbildung 2.70: Personen und Unternehmen
Das Ende der Fahnenstange ist damit allerdings noch lange nicht erreicht. Die Unter nehmen lassen sich noch zu Konzernen zusammenfassen, was eine weitere Tabelle erfordern würde. Auch über die Beziehung zwischen Personen und Unternehmen ist noch nicht das letzte Wort gesprochen: Was ist beispielsweise mit freien Mitarbeitern, die Sie schließlich auch unter einer beruflichen Telefonnummer erreichen möchten? Freie Mitarbeiter sind ja gerade deshalb »frei«, weil sie nicht nur für ein einziges Unternehmen arbeiten. Theoretisch müssten Sie also zwischen Personen und Unternehmen eine m:n-Beziehung erstellen. Und wie gehen Sie vor, wenn Sie eine Liste nicht nur aller Unternehmen oder aller Personen, sondern etwa eine Gesamtliste von Unternehmen und Personen ausgeben möchten? Gegebenenfalls könnten Sie die gewünschten Daten per UNION-Abfrage zusammenfassen (was eine UNION-Abfrage ist, erfahren Sie in Kapitel 3, »Abfragen«, Abschnitt 3.5). Wie Sie sehen, ist die Verwendung der Adressdaten von Unternehmen und Personen keine triviale Angelegenheit. Die genannten Konfigurationen sind Beispiele, die Sie beim Erstellen einer Adressenverwaltung nach Sichtung der individuellen Gegebenheiten berücksichtigen können.
130
Tabellen und Datenmodellierung
Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter \Kap_02\Adressverwaltung.accdb.
2.7.2 Rezepteverwaltung Um zu ermitteln, welche Daten eine Rezepteverwaltung enthalten muss, schlagen Sie einfach ein beliebiges Kochbuch auf und schauen sich an, welche Informationen dort pro Rezept enthalten sind. Jedes Rezept enthält die Angabe der benötigten Zutaten mit Menge und Einheit, die Beschreibung, die Zubereitungsdauer, die Anzahl der Personen, die sich an dem Ergebnis laben kann, und vielleicht noch ein Foto. Da ein Kochbuch meist von einem einzigen Autor stammt, wird dieser nicht explizit für jedes Rezept angegeben – das wäre das einzige Feature, das man seinem Datenmodell noch zusätzlich gönnen sollte. Vermutlich möchten Sie Rezepte verschiedener Autoren in einer Datenbank sammeln. Diese Informationen werden allerdings bereits im ersten Ansatz auf insgesamt sieben Tabellen aufgeteilt (siehe Abbildung 2.71) – und hier sind noch Verfeinerungen möglich. Doch zunächst zu dieser Version des Datenmodells: Informationen wie die Beschreibung, die Dauer der Zubereitung, die Anzahl Portionen, der Autor und der Pfad zu einer Abbildung befinden sich in der Haupttabelle tblRezepte. Da mehrere Rezepte vom gleichen Autor stammen können, wird dessen Name noch in eine Lookup-Tabelle namens tblAutoren ausgegliedert. Eine nützliche Geschichte für eine Rezeptesammlung ist die Angabe einer oder mehrerer Kategorien wie Fleischgerichte, vegetarische Gerichte, Salate, Nudelgerichte oder Suppen. Für ausreichend Flexibilität – etwa, wenn sich einmal ein Gericht nicht eindeutig zuordnen lässt – verknüpfen Sie die Tabelle tblKategorien nicht direkt mit der Tabelle tblRezepte, sondern über eine Zwischentabelle namens tblRezepteKategorien. Mit dieser m:n-Beziehung lassen sich jedem Rezept mehrere Kategorien zuordnen. Das Wichtigste sind natürlich die Zutaten: Diese werden zunächst in einer eigenen Ta belle namens tblZutaten erfasst. Über die Zwischentabelle tblRezepteZutaten werden nicht nur die Zutaten zu einem Rezept, sondern auch noch Menge und Einheit festgelegt. Die Einheiten sollten – wie der Name schon sagt – immer einheitlich gewählt werden, weshalb Sie diese in die Tabelle tblEinheiten auslagern und mit der Tabelle tblRezepteZutaten verknüpfen. Dieses Datenmodell ermöglicht nicht nur die Ausgabe von Rezepten mit den angegebenen Informationen, sondern auch noch die Berechnung von Rezepten für eine andere Anzahl hungriger Esser als im Feld AnzahlPortionen angegeben. Um solch ein alternatives Rezept zu berechnen, müssen Sie lediglich die in der Tabelle tblRezepteZutaten ange-
131
Kapitel 2
gebene Menge je Zutat durch die Anzahl zu verköstigenden Personen teilen und mit der Zahl der gewünschten Mahlzeiten multiplizieren.
Abbildung 2.71: Datenmodell einer Rezepteverwaltung
Erweiterungsmöglichkeiten Das Datenmodell bietet noch die Erweiterungsmöglichkeit, mehrere Bilder zu einem Rezept zu speichern oder die einzelnen Schritte der Rezeptbeschreibung in einer eigenen Tabelle zu speichern. Wie grob man dies vornimmt, bleibt jedem selbst überlassen – sinnvoll könnte aber auf jeden Fall die Aufteilung in einzelne Elemente wie »Fleischzu bereitung«, »Beilagen« und »Saucen« sein. Das führt aber spätestens bei Gerichten, in denen zur Optimierung der Zubereitungszeit mehrere Elemente gleichzeitig zubereitet werden, zu Problemen – das Speichern der Beschreibung in einem einzigen Feld scheint also sinnvoller zu sein. Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter \Kap_02\Rezeptverwaltung.accdb.
2.7.3 Artikelverwaltung Mit der guten alten Nordwind-Datenbank, die bis Access 2003 mit Access geliefert wurde, bietet Microsoft ein Beispiel für eine einfache Artikelverwaltung, die sich von
132
Tabellen und Datenmodellierung
Version zu Version jeglicher Namenskonvention widersetzte (die Nordwind-Datenbank von Access 2007 kommt mit einem wesentlich umfangreicheren Datenmodell, dessen Beschreibung hier den Rahmen sprengen würde – aber es widersetzt sich immer noch den gängigen Namenskonventionen). Immerhin liefert das Datenmodell Beispiele für fast alle gebräuchlichen Beziehungstypen mit Ausnahme der 1:1-Beziehung. Nicht auf den ersten Blick zu erkennen ist die reflexive Beziehung in der Personal-Tabelle. Dort lässt sich als Wert des Feldes Vorgesetzte(r) ein Eintrag der gleichen Tabelle auswählen. Das Datenmodell aus Abbildung 2.72 sieht dem der Nordwind-Datenbank sehr ähnlich, unterscheidet sich jedoch in einigen Details: Die Tabellennamen weisen nämlich ordnungsgemäß das Präfix tbl auf und Feldnamen sind von Sonderzeichen befreit.
Abbildung 2.72: Das (überarbeitete) Datenmodell der Nordwind-Datenbank
Das Datenmodell ist für eine Beispieldatenbank durchaus in Ordnung, der tägliche Einsatz dürfte jedoch noch einige Erweiterungen verlangen. So wäre es beispielsweise praktisch, wenn man die bestellten Artikel direkt in die Bestandsverwaltung einbeziehen könnte. Diese würde also nicht nur die Ausgänge der bestellten und anschließend verkauften Artikel verwalten, sondern auch den Wareneingang, und gegebenenfalls notwendige Umbuchungen erfassen, etwa zurückgelieferte Ware oder bei Inventuren festgestellte Fehlmengen. Natürlich könnten Sie dies mit zwei weiteren Tabellen erledigen, die ähnlich wie die Tabelle tblBestelldetails aufgebaut sind, und für Bestandserfassungen die bewegten Waren per UNION-Abfrage zusammenfassen.
133
Kapitel 2
Eine andere Möglichkeit ist die aus Abbildung 2.73. Mit diesem Datenmodell gehen Sie das Problem von einer anderen Warte an: Hier werden alle Bewegungen in einer einzigen Tabelle namens tblBestandsaenderungen erfasst. Die Tabelle ist per 1:1-Beziehung etwa mit einer Tabelle namens tblPositionen verknüpft. Zusammen enthalten diese beiden Tabellen genau die gleichen Daten wie die Tabelle Bestelldetails in der NordwindDatenbank. Arbeiten können Sie mit diesen Daten – wie bereits weiter oben erwähnt – indem Sie diese einfach per Abfrage zusammenfassen. Der Vorteil dieser Vorgehensweise ist, dass sich die für die Erfassung der Wareneingänge und der Umbuchungen benötigten Informationen in Form der Zusatztabellen tblWareneingang und tblUmbuchungen ebenfalls per 1:1-Beziehungen an die Tabelle tblBestandsaen derungen anfügen lassen. Die Bestandsänderungen sind dennoch alle in einer einzigen Tabelle verfügbar. Unterschiede zwischen Ein- und Ausgängen markiert das Feld Vor zeichen, das bei Ausgängen den Wert –1 enthält und bei Eingängen den Wert 1.
Abbildung 2.73: Erweiterung der Bestellverwaltung um Wareneingang und Umbuchungen
Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter \Kap_02\Bestellverwaltung.accdb.
2.7.4 CD-Verwaltung »In welchem Fach von welchem Schrank ist noch mal die Depeche Mode-CD mit dem Song Photographic versteckt?« – so oder ähnlich fragen begeisterte CD-Sammler. Und
134
Tabellen und Datenmodellierung
wer seine Sammlung sicher verwalten und jederzeit die gewünschten Titel finden möchte, baut natürlich eine eigene CD-Verwaltung auf Access-Basis auf. Und dabei muss man noch nicht einmal alle CDs und Tracks selbst erfassen, denn im Internet finden sich Datenserver, die zu einer eindeutigen ID einer CD online alle Da ten zur Verfügung stellen. Ein Beispiel für diesen Service findet sich unter der Internet adresse http://freedb.org; das OCX-Steuerelement UFreeDB.ocx, das Sie mit dem auf der Buch-CD enthaltenen Setup auf Ihrem System installieren können, liefert die für den Zugriff notwendigen Methoden und Eigenschaften. Diesem System lehnt sich auch das folgende Datenmodell an: Es enthält lediglich drei Tabellen, wobei die CDs und die Tracks in je einer Tabelle gespeichert werden und beide die Interpreten aus einer Lookup-Tabelle beziehen (siehe Abbildung 2.74).
Abbildung 2.74: Datenmodell einer CD-Verwaltung
Zwischen CDs und Tracks ergibt sich eine klassische 1:n-Beziehung, da jeder Track sich genau einer CD zuordnen lässt, jede CD aber aus mehreren Tracks besteht. Aber ist das wirklich so? Kann nicht ein Track auf einem Album, auf einer Maxi-CD und vielleicht noch auf verschiedenen Samplern vorhanden sein? Natürlich ist das möglich, aber da sich hier meist auch noch die Spieldauer und die Version unterscheiden, scheint eine m:n-Beziehung hier doch unangemessen. Die Felder sind im Gegensatz zu den sonstigen Gepflogenheiten dieses Buchs nicht mit deutschen Namen versehen. Namensgeber war in diesem Fall das Objektmodell der uFreeDB-Bibliothek.
Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter \Kap_02\CD-Verwaltung.accdb.
135
Kapitel 2
2.7.5 Projektverwaltung Wer keine professionelle Software für die Verwaltung von Projekten verwenden möchte, kann sich mit einer passenden Access-Anwendung helfen. Abbildung 2.75 zeigt ein rudimentäres Datenmodell für eine solche Projektverwaltung. Es basiert darauf, dass jedes Projekt von einem Kunden in Auftrag gegeben wird, wobei für interne Projekte einfach das eigene Unternehmen als Kunde eingetragen werden kann. Jedem Kunden können Sie mehrere Projekte zuweisen, daher sind die Tabellen tblKunden und tblProjekte mit einer 1:n-Beziehung verknüpft. Projekte haben einen Na men, eine Dauer, eine Leitung und vor allem einzelne Projektphasen (mindestens aber eine). Die Projektphasen haben wiederum eine Bezeichnung, eine Leitung sowie ein Start- und ein Enddatum. Konkreter wird es auf der nächsten Ebene: Projektphasen gliedern sich in Tätigkeiten, die in der Tabelle tblTaetigkeiten gespeichert werden. Diese sind immer auf einen Tag begrenzt, daher enthält die Tabelle ein Datumsfeld sowie zwei weitere Felder zur Angabe der Start- und der End-Uhrzeit. Außerdem werden hier eine Tätigkeitsbeschreibung und der Tätigkeitstyp eingegeben.
Abbildung 2.75: Datenmodell einer Projektverwaltung
Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter \Kap_02\Projektverwaltung.accdb.
2.7.6 Mitarbeiterverwaltung Das Datenmodell aus Abbildung 2.76 zeigt ein rudimentäres Abbild dessen, was Sie bei einer Mitbearbeiterverwaltung berücksichtigen müssen. Wichtig ist hier vor allem die
136
Tabellen und Datenmodellierung
Aufteilung der Informationen über einen Mitarbeiter auf die Mitarbeiterdaten und auf die Beschäftigungsdaten. Die Daten in der Tabelle tblMitarbeiter können sich ändern, ohne dass sich dies auf das Beschäftigungsverhältnis auswirkt – der Mitarbeiter kann seinen Wohnsitz, seine Bank verbindung, seine Telefonnummern wechseln und es werden einfach die aktuellen Da ten weiter verwendet. Für die Personalabteilung ist es viel interessanter, wann der Mitar beiter in welcher Position und in welcher Abteilung gearbeitet hat. Deshalb finden Sie im Datenmodell eine Tabelle namens tblBeschaeftigungen, die alle Informationen über die einzelnen Beschäftigungsverhältnisse enthält. Neben Abteilung und Position finden sich auch Details darüber, in welchen Räumlichkeiten der Mitarbeiter sein Unwesen treibt und wer sein Vorgesetzter ist. Das Feld VorgesetzterID ist übrigens auch mit der Tabelle tblMitarbeiter verknüpft; aus Platzgründen wurde diese Tabelle jedoch nicht noch einmal zusätzlich abgebildet. In die beiden Felder Eintrittsdatum und Austrittsdatum tragen Sie ein, wie lange die jeweiligen Beschäftigungsverhältnisse gedauert haben. Erweiterungsmöglichkeiten bieten sich hier in ausreichender Menge – so können Sie Daten zu Lohn/Gehalt ebenfalls in der Tabelle tblBeschaeftigungen unterbringen oder auch Verweise auf die Standorte der jeweiligen Arbeitsverträge hinterlegen.
Abbildung 2.76: Datenmodell einer Mitarbeiterverwaltung
Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter \Kap_02\Mitarbeiterverwaltung.accdb.
137
Kapitel 2
2.7.7 Literaturverwaltung Wenn ein Unternehmen Fachbücher, Magazine und sonstige Literatur zentral einkauft und lagert, macht eine Literaturverwaltung Sinn. Im Mittelpunkt des Datenmodells einer solchen Verwaltung steht die Tabelle tblLiteratur. Sie enthält die wichtigsten In formationen über die einzelnen Werke wie Titel, Erscheinungsjahr, Schlagwörter oder einen Abstract. Weitere Informationen wie Verlag und Dokumenttyp befinden sich in Lookup-Tabellen (siehe Abbildung 2.77). Für die Angabe der Autoren ist hingegen eine m:n-Beziehung erforderlich, denn ers tens kann es mehrere Autoren je Werk geben und zweitens schreiben Autoren unter Umständen für mehr als eine Publikation. Zusätzlich liefert das Datenmodell den Komfort, dass man der Kombination aus Veröffentlichung und Autor noch die Funktion des Autors hinzufügen kann – unter Umständen gibt es Haupt- und Co-Autoren, die Sie ebenfalls in der Datenbank speichern möchten. Wichtig ist vor allem in größeren Unternehmen die Verwaltung der Standorte: Wenn Sie schon den Literaturbestand in einer Datenbank verwalten, möchten Sie vielleicht auch wissen, wo sich die einzelnen Werke zu einem bestimmten Zeitpunkt befinden. Dazu legen Sie in der Tabelle tblStandorte alle vorhandenen Standorte fest und verknüpfen diese wiederum mit einer m:n-Beziehung mit der Tabelle tblLiteratur. Warum nun mit einer m:n-Beziehung – eine Veröffentlichung hat doch in der Regel auch nur einen Standort? Das ist richtig, aber wer zum Beispiel Zeitschriften in der Literaturverwaltung hütet, möchte vielleicht auch deren Rundlauf durch die Abteilungen verfolgen, bevor diese ihren endgültigen Platz finden. Wer es den Benutzern ganz besonders angenehm machen möchte, kann auch noch die in einem Buch enthaltenen Verweise auswerten und eine reflexive Verknüpfung erstellen. In diesem Fall wird dort noch die Tabelle tblVerweise zwischengeschaltet, um gegebenenfalls Bemerkungen unterzubringen (zum Beispiel »Das Buch … liefert weiterführende Informationen zum Thema …«). Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter \Kap_02\Literaturverwaltung.accdb.
2.7.8 Mitgliederverwaltung Wegen der hohen Anzahl Vereine in Deutschland sollte man meinen, dass Mitglieder verwaltungen eine der meistgebrauchten Anwendungen überhaupt sind. Mit einem gewissen Grundstock können Sie Mitgliederverwaltungen für alle möglichen Vereine erstellen.
138
Tabellen und Datenmodellierung
Abbildung 2.77: Datenmodell einer Literaturverwaltung
Abbildung 2.78 liefert eine solche Grundausstattung: Im Mittelpunkt steht hier die Ta belle tblMitglieder, in der prinzipiell alle Daten erfasst werden. Neben dieser Tabelle gibt es nur einige Lookup-Tabellen zur Auswahl von Daten. Wichtig ist: Mitglieder – vor allem von Sportvereinen – wollen sich untereinander immer und überall erreichen können, um sich zu Wettkämpfen und/oder gesellschaftliche nAnlässen zu verabreden. Im Gegensatz zur Kontaktverwaltung in Unternehmen sollen hier möglichst die private, die geschäftliche und die mobile Telefonnummer gepflegt werden. Da hierfür eine ganze Menge Felder draufgehen können, sind diese Daten in zwei weitere Tabellen ausgelagert. Dabei dient die Tabelle tblTelefonnummern als Verknüpfungstabelle zwischen den Tabellen tblMitglieder und tblTelefonnummerarten. Letztere enthält Einträge wie Privat (Festnetz), Privat (Mobil), Privat (Fax), Geschäftlich (Festnetz), Geschäftlich (Mobil) oder Geschäftlich (Fax). Die Tabelle tblTelefonnummern speichert das Mitglied und die Telefonnummerart sowie die eigentliche Telefonnummer. Die Lookup-Tabellen sind weitgehend selbsterklärend. Die Tabelle tblZahlungsarten enthält Informationen wie Bankeinzug oder Überweisung, die Tabelle tblFunktionen die Aufgabe innerhalb des Vereins (erster Vorsitzender, Schriftführer, Jugendwart etc.), die
139
Kapitel 2
Tabelle tblBeitragsklassen Einträge wie Jugendlicher oder Erwachsener und die Tabelle tblMitgliedsarten gibt an, ob es sich um ein aktives oder passives Mitglied handelt.
Abbildung 2.78: Datenmodell einer Mitgliederverwaltung
Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter \Kap_02\Vereinsverwaltung.accdb.
2.7.9 Urlaubsverwaltung Die Verwaltung von Urlaub ist gerade in der Sommerzeit eine knifflige Angelegenheit. Mit einer passenden Access-Anwendung haben Sie nicht nur die verbleibenden Urlaubs tage im Griff, sondern können sich auch per Bericht ausgeben lassen, wo es besonders eng wird. Eine weitere mögliche Funktion ist das Festlegen von Stellvertretern für die Zeit des Urlaubs eines Mitarbeiters. Das Datenmodell aus Abbildung 2.79 sorgt hier für die Grundlage. Im Mittelpunkt stehen die Mitarbeiter, die in der Tabelle tblMitarbeiter gespeichert werden. Dazu gehören Informationen wie Name, Position, Abteilung, Kontakt- und Firmenzugehörigkeitsdaten. Mit den beiden Tabellen tblAbwesenheiten und tblAbwesenheitsarten pflegen Sie nicht nur die Urlaubstage, sondern auch übrige Abwesenheitszeiten durch Krankheit oder
140
Tabellen und Datenmodellierung
Fortbildungen – weiteren Variationen öffnen Erweiterungen der Tabelle tblAbwesenheitsarten Tür und Tor. Durch eine zusätzliche Verknüpfung von der Tabelle tblAbwesenheiten zur Tabelle tblMitarbeiter – in der Abbildung durch die Tabelle tblMitarbeiter_1 repräsentiert – ermöglichen Sie das Zuweisen eines Stellvertreters für die Zeit der Abwesenheit. Und damit niemand mehr Urlaub nimmt, als er darf, speichert die Tabelle tblUrlaubsanspruch den individuellen Anspruch pro Mitarbeiter und pro Jahr. Das Feld JahrID dieser Tabelle und die Verknüpfung zur Tabelle tblJahre sorgen dafür, dass Sie den Mitarbeitern über die Jahre eine unterschiedliche (in der Regel steigende) Anzahl Urlaubstage zuweisen können. So lassen sich auch im Nachhinein die verfallenen oder ins Folgejahr übertragenen Urlaubstage genau nachhalten.
Abbildung 2.79: Datenmodell einer Urlaubsverwaltung
Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter \Kap_02\Urlaubsverwaltung.accdb.
2.7.10 Aufgabenverwaltung Für Entwickler – egal, ob als Einzelgänger oder im Rudel – kann eine Aufgabenverwaltung eine sehr sinnvolle Sache sein. Damit lassen sich Aufgaben in eine Datenbank eintragen und mit wichtigen Informationen versehen: eine Priorität für das Abarbeiten in der richtigen
141
Kapitel 2
Reihenfolge, ein Status für den Projektmanager, Informationen über den Urheber und den Ausführenden der Aufgabe, das geplante Enddatum für die Fertigstellung und mehr. All diese Informationen werden in der Tabelle tblAufgaben gespeichert (siehe Abbildung 2.80). Von dort aus gibt es zwei Verknüpfungen zur Tabelle tblBenutzer: eine, um den Benutzer festzulegen, der die Aufgabe erstellt hat, und eine, um den Ausführenden zu kennzeichnen. Jede Aufgabe lässt sich noch in einzelne Aktionen zerlegen, weshalb eine weitere Tabelle namens tblAktionen mit einer 1:n-Beziehung an die Tabelle tblAufgaben angehängt wird. Hier finden sich einige Felder der Tabelle tblAufgaben wieder. Interessant ist hier vor allem das Feld Verbrauchte Zeit. Über die Summe der Zeiten aller Aktionen einer Aufgabe lässt sich ermitteln, ob die in der Tabelle tblAufgaben gemachte Angabe über die erwartete Dauer realistisch war oder nicht und wo es gegebenenfalls gehakt hat.
Abbildung 2.80: Datenmodell einer Aufgabenverwaltung
Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter \Kap_02\Aufgabenverwaltung.accdb.
142
Tabellen und Datenmodellierung
2.7.11 Projektzeitverwaltung In eine ähnliche Richtung wie die Aufgabenplanung geht die Projektzeitverwaltung. Allerdings ist diese ein wenig projektorientierter und zielt konkret auf die Ermittlung des zeitlichen Aufwands zu Abrechnungszwecken ab. Im Mittelpunkt stehen die Pro jektzeiten – das sind die Zeiten, die ein Mitarbeiter mit dem Bearbeiten eines bestimmten Projekts verbringt. Die Tabelle tblProjektzeiten erfasst das Projekt, die Mitarbeiter, Startzeit und Endzeit sowie Tätigkeitsbeschreibung und Tätigkeitsart (siehe Abbildung 2.81). Die Projekte und Mitarbeiter stammen aus den verknüpften Tabellen tblProjekte und tblMitarbeiter. An der Tabelle tblProjekte hängt vorsichtshalber direkt die Tabelle mit den Kunden – falls Sie einmal das Budget überschreiten und den Kunden benachrichtigen müssen, haben Sie die notwendigen Informationen sofort zur Hand …
Abbildung 2.81: Datenmodell einer Projektzeitverwaltung
Die Tabelle der Projekte enthält eine Projektbezeichnung und eine Beschreibung, Startund Enddatum sowie die Angabe, welcher Mitarbeiter Projektleiter und damit verantwortlich für die Zuweisung der Zeiten ist. Die Erfassung von Zeiten macht natürlich nur Sinn, wenn die Mitarbeiter diese kontinuierlich pflegen – und das machen sie vermutlich lieber, wenn dies schnell geht und nicht viel Zeit kostet. Dazu sollte der Mitarbeiter nicht erst lange nach »seinen« Projekten suchen müssen, sondern alle Projekte, an denen er beteiligt ist, direkt vorliegen haben und möglichst selbst sortieren können. Die Voraussetzung dafür schaffen Sie mit der Tabelle
143
Kapitel 2
tblProjekteMitarbeiter, mit der Sie erstens überhaupt festlegen, welcher Mitarbeiter Pro jektzeiten für welche Projekte anlegen kann. Zweitens können die Mitarbeiter »ihre« Projekte mit dem Feld Aktiviert ein- oder ausblenden und mit dem Feld ReihenfolgeID nach ihren eigenen Wünschen anordnen. Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter \Kap_02\Projektzeitverwaltung.accdb.
2.7.12
Kunden und Weihnachtsgeschenke
Alle Jahre wieder nähert sich das Weihnachtsfest und Hektik macht sich breit – und das nicht nur in Innenstädten und Einkaufszentren, sondern auch in bestimmten Abteilungen der Unternehmen. Dort sollen nämlich die alljährlichen Weihnachtsgeschenke zusam mengestellt werden – und zwar möglichst nicht jedes Jahr das Gleiche und nach Beliebt heit der Kunden sortiert, sprich nach dem Umsatz. Das folgende Datenmodell liefert nicht nur die Ansprechpartner der einzelnen Unter nehmen (tblPersonen und tblUnternehmen), sondern auch noch zwei Tabellen zum Ver walten der Präsente (siehe Abbildung 2.82).
Abbildung 2.82: Datenmodell einer Präsenteverwaltung
144
Tabellen und Datenmodellierung
Dabei ist die Tabelle tblPersonenPraesente eine Verknüpfungstabelle zur Realisierung einer m:n-Beziehung zwischen den Tabellen tblPersonen und tblPraesente. Auf diese Weise lässt sich mehreren Personen das gleiche Präsent zuweisen, aber auch einer Person mehrere Präsente. Der Clou ist das zusätzliche Feld Jahr in der Verknüpfungstabelle: Darüber halten Sie zusätzlich nach, wer in welchem Jahr womit beglückt wurde – nicht, dass jemand denkt, er sei in der Gunst gesunken, nur weil er eine Flasche Wein weniger bekommt. Als Präsent können Sie hier im Übrigen auch die Weihnachtskarten erfassen. Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter \Kap_02\Praesenteverwaltung.accdb.
2.7.13
Fahrtenbuch
Ein Fahrtenbuch zu führen, ist eigentlich kein Problem. Man fährt, trägt seinen Namen, die Strecke und ein paar weitere Informationen ein und schon ist man fertig. Arbeit hat dann derjenige, der die im Fahrtenbuch enthaltenen Informationen für den Arbeitgeber oder das Finanzamt auswerten muss. Einfacher geht das mit einem elektronischen Fahrtenbuch – natürlich auf Access-Basis. Die Tabelle tblFahrten umfasst den größten Teil der benötigten Informationen. Sie enthält zwei Verknüpfungen zu den Lookup-Tabellen tblFahrer und tblNutzungsarten und ist per 1:n-Beziehung mit der Tabelle tblFahrzeuge verknüpft. Diese sorgt dafür, dass das Datenmodell mehr als ein Fahrzeug verträgt und Fahrtenbücher für einen beliebig großen Fuhrpark verwalten kann (siehe Abbildung 2.83). In Zusammenhang mit den Fahrzeugen sind noch weitere Informationen interessant: Zum Beispiel Ausgaben für Reparaturen, Autowäsche und sonstiges Zubehör. Unter Ausgaben fallen eigentlich auch die Tankvorgänge. Wegen der vielen speziellen Informationen werden diese allerdings in einer eigenen Tabelle namens tblTankvorgaenge gespeichert. Zu diesen Informationen zählen das Datum, der Kilometerstand, die Anzahl Liter und der Preis je Liter. Durch das Boolean-Feld Vollgetankt lässt sich später der durchschnittliche Verbrauch zwischen mehreren Vollbetankungen ermitteln. Achtung: Wenn Sie mit dem Gedanken spielen, künftig mit einem per Rechner erstellten Fahrtenbuch beim Finanzamt vorstellig zu werden, erkundigen Sie sich dort auf jeden Fall vorher, welche Bedingungen dabei genau zu erfüllen sind. Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter \Kap_02\Fahrtenbuch.accdb.
145
Kapitel 2
Abbildung 2.83: Datenmodell eines Fahrtenbuchs
146
3 Abfragen Abfragen sind die Schnittstelle zwischen den in den Tabel len enthaltenen Daten und den für die Bearbeitung und Anzeige zuständigen Formularen und Berichten sowie für die Weiterverarbeitung per VBA. Genau wie Tabellen sollten die Benutzer einer professionellen Datenbank auch eine Abfrage nie zu Gesicht bekommen. Sie dient lediglich dazu, die Daten für die eigentliche Bearbei tung aufzubereiten. Dabei gibt es verschiedene Möglichkeiten: Einschränken der Felder einer Tabelle: Mit einer Abfra ge können Sie die Felder auswählen, deren Inhalte als Abfrageergebnis angezeigt werden sollen. Einschränken der Daten einer Tabelle: Genau wie die Felder können Sie auch die anzuzeigenden Datensätze einer Tabelle per Abfrage einschränken. Dazu verwenden Sie ein geeignetes Kriterium für eines oder mehrere Felder. Zusammenführen der Daten verschiedener Tabellen: Nach der Normalisierung liegen Daten in vielen Fäl len in mehreren, miteinander verknüpften Tabellen vor. In einer Abfrage können Sie die Felder der ver knüpften Tabellen wieder zusammenführen und – in den meisten Fällen – wie eine einzige Tabelle verwen den. Zusammenführen der Daten gleichartiger Tabellen: Auch wenn man gleichartige Daten aus verschiedenen Tabellen benötigt – etwa die Namen der Mitarbeiter aus
Kapitel 3
der Mitarbeitertabelle und diejenigen aus der Kunden-Tabelle –, hilft eine Abfrage weiter (siehe Abschnitt 3.5, »UNION-Abfragen«). Spezielle Aufbereitung von Daten: Mit Hilfe von Kreuztabellenabfragen lassen sich Daten wie beispielsweise die Verkaufszahlen von Produkten in bestimmten Zeiträu men in einer Art Matrix ausgeben. Berechnungen auf Basis der Felder der zugrunde liegenden Tabellen ausführen: Be rechnete Werte in Tabellenfeldern sind bekanntlich tabu, da diese zu redundanten Daten führen. Daher gehören Berechnungen in Abfragen. Abfragen werden Sie überall antreffen: Als Datensatzquelle von Formularen und Berich ten, als Datensatzherkunft von Kombinations- und Listenfeldern, als Datenquelle von Recordset-Objekten in VBA-Code und vielleicht auch als Bestandteil einer weiteren Ab frage. Weil Abfragen so wichtig sind, stellt Access eine mächtige Entwurfsansicht dafür bereit, die nur wenig Wünsche offen lässt. Zwar lassen sich dort keine UNION-Abfragen oder PassThrough-Abfragen eingeben; dafür ist aber eine zusätzliche SQL-Ansicht vorhanden, in die man nicht nur diese Abfragen eingeben, sondern mit der man auch den SQLText der anderen Abfragen ausgeben kann. Wie Sie vielleicht zu Beginn des Buches gelesen haben, setzt dies grundlegende Kenntnis se im Umgang mit Access voraus. Daher finden Sie im Folgenden auch keine detaillierte Beschreibung für die Anwendung der Abfrage-Entwurfsansicht, sondern lediglich eine kurze Einführung in das Anlegen von Abfragen mit Access 2007 und – wesentlich umfangreicher als die Einführung – Informationen zu oft benötigten Vorgehensweisen im Zusammenhang mit Abfragen. Dabei findet gelegentlich ein Vorgriff auf die in Kapitel 8, »Access-SQL«, enthaltene umfassende Beschreibung der Abfragesprache SQL statt. Beispiele auf CD: Sie finden alle Code-Beispiele dieses Kapitels auf der Buch-CD unter \Kap_03\Abfragen.accdb. Die Datenbankdatei enthält auch die für die Beispiele verwendeten Tabellen.
3.1 Anlegen von Abfragen mit Access 2007 Prinzipiell gibt es nur zwei mögliche Startpunkte für das Anlegen von Abfragen in Access 2007: den Assistenten, den Sie im Ribbon mit Erstellen|Andere|Abfrage-Assistent aufrufen, sowie den direkten Sprung zur Entwurfsansicht, zu erreichen mit Erstellen|An dere|Abfrageentwurf. Da dieses Buch keine Assistenten behandelt (die meisten davon sind selbsterklärend; probieren Sie sie ruhig aus!), bleibt nur noch die Beschreibung der Entwurfsansicht (siehe Abbildung 3.1).
148
Abfragen
Abbildung 3.1: Eine leere Abfrage mit dem Dialog zum Auswählen der gewünschten Tabellen
Access 2007 liefert kaum neue Features, was das Erstellen und das Handling von Ab fragen angeht – außer, dass sich die dazu notwendigen Steuerelemente wie auch alle anderen nicht mehr an der von Access 2003 und älter gewohnten Stelle befinden. Da die Chancen gut stehen, dass Sie schon wissen, wie Sie eine Auswahlabfrage erstellen, was Kriterien sind, wie Sie Aktionsabfragen wie Aktualisierungs-, Anfüge-, Lösch- und Tabellenerstellungsabfragen erstellen und dass Sie für manche Abfragetypen wie etwa UNION-Abfragen die SQL-Ansicht heranziehen müssen, spart dieses Buch die diesbezüglichen Grundlagen aus und verweist auf die Onlinehilfe. Wenn Sie sich noch in die Grundlagen von Abfragen einarbeiten müssen, bietet der dortige Artikel »Erstellen einer einfachen Auswahlabfrage« einen guten Einstieg. Am Ende des genannten Artikels finden Sie eine Reihe von Verweisen auf weitere Hilfethemen zum Bereich Abfragen, die alle notwendigen Grundlagen liefern.
3.2 Abfragen mit Anlage-Feldern und mehrwertigen Feldern Eine Neuerung gibt es in Access 2007 übrigens doch, die auch die Abfragen betrifft: Abfragen bieten nämlich eine Möglichkeit, auf die in mehrwertigen Feldern und in Anlage-Feldern gespeicherten Daten zuzugreifen. Dazu fügen Sie einfach der Abfrage eine Tabelle mit einem solchen Feld hinzu und erkennen bereits in der Entwurfsansicht, dass nicht nur das Feld selbst, sondern auch die in der damit verknüpften, verborgenen Tabelle enthaltenen Felder angezeigt werden (siehe Abbildung 3.2).
149
Kapitel 3
Abbildung 3.2: Abfrage auf eine Tabelle mit einem mehrwertigen Feld
Wenn Sie die obige Abfrage auf die Tabelle aus Abbildung 3.3 ausführen, würden Sie normalerweise vermutlich zwei Datensätze als Ergebnis erwarten. Aber weit ge fehlt: Wie Abbildung 3.4 zeigt, enthält die Abfrage für jede Kombination aus Rezeptbe zeichnung und Zutat einen einzelnen Datensatz. Im Prinzip ist dies das Gleiche, als wenn Sie die entsprechenden Felder der drei an einer m:n-Beziehung beteiligten Tabellen abfragen.
Abbildung 3.3: Eine Abfrage auf eine Tabelle mit einem mehrwertigen Feld wie dieser ...
Abbildung 3.4: ... zeigt mitnichten die gleiche Anzahl Datensätze an
150
Abfragen
3.3 Verwendung von Abfragen als Datensatzquelle oder Datensatzherkunft Formulare und Berichte beziehen ihre Daten aus der unter der Eigenschaft Datensatzquelle angegebenen Tabelle oder Abfrage, bei Kombinations- und Listenfeldern heißt die entsprechende Eigenschaft Datensatzherkunft. Zum Füllen dieser beiden Eigenschaften gibt es verschiedene Techniken, die nachfolgend erläutert werden. Die einfachste ist das Setzen der entsprechenden Eigenschaft auf eine bestehende Tabelle oder Abfrage. Der Einsatz einer Tabelle ist dabei nur sinnvoll, wenn alle Felder und alle Datensätze der Tabelle benötigt werden. Wenn nicht alle Felder der Tabelle Verwendung finden oder nicht alle Datensätze angezeigt werden sollen, verwenden Sie eine Abfrage. Das kommt auch der Performance Ihrer Anwendung zu Gute. Die Herkunft der Daten heißt in Formularen und Berichten Datensatzquelle und in Steuer elementen wie dem Kombinationsfeld oder Listenfeld Datensatzherkunft. Wenn es im Folgenden nicht explizit um die Datensatzherkunft solcher Steuerelemente geht, wird verallgemeinernd der Begriff Datensatzquelle verwendet.
3.3.1 Tabelle als Datensatzquelle Die einfachste Art der Datensatzquelle ist eine Tabelle. In vielen Fällen haben Tabellen aber mehr Felder oder enthalten mehr Datensätze als tatsächlich angezeigt werden sollen. Lookup-Tabellen, die nur aus einem Primärschlüsselfeld und einem weiteren Feld bestehen, können aber durchaus ohne Verwendung einer Abfrage eingesetzt werden – etwa als Datensatzherkunft von Kombinationsfeldern. Erst wenn die enthaltenen Daten auch noch sortiert werden sollen, ist eine Abfrage erforderlich. Beispiel für diese und die folgenden Möglichkeiten zur Bestückung von Formularen und Steuerelementen mit Datenherkünften ist ein Formular, das die Abwesenheit von Mitarbeitern nach Jahren filtert (siehe Abbildung 3.5). Ein gutes Beispiel für die Verwendung einer Tabelle als Datensatzherkunft ist das Kombinationsfeld cboJahr. Wenn die Daten in der gewünschten Reihenfolge vorliegen, können Sie guten Gewissens eine Tabelle statt einer Abfrage als Datensatzherkunft verwenden. Das ist hier der Fall: Das Feld JahrID dient als nicht sichtbares, gebundenes Feld und der Inhalt des Feldes Jahr wird im Kombinationsfeld angezeigt (siehe Abbildung 3.6). Eine Tabelle oder Abfrage, die als Datensatzherkunft von Kombi nationsfeldern oder Listenfeldern dient, kann auch mehr Felder besitzen, als das Steuerelement anzeigt. Das Steuerelement fragt automatisch nur die benötigten Felder ab.
151
Kapitel 3
Abbildung 3.5: Beispiel für das Zuweisen der Datensatzherkunft
Abbildung 3.6: Tabelle als Datensatzherkunft
3.3.2 SQL-Ausdruck als Datensatzquelle Die zweite Möglichkeit sind reine SQL-Ausdrücke. Diese können statt des Namens der Tabelle oder der Abfrage für die Datensatzquelle- oder Datensatzherkunft-Eigenschaft angegeben werden. In vielen Fällen geht es einfach schneller, wenn man mal eben eine kurze SQL-Anweisung für die entsprechende Eigenschaft einträgt, als wenn man zunächst eine Abfrage erstellt, diese speichert und dann die Abfrage als Wert der jeweiligen Eigenschaft einträgt. Im Beispielformular ist das Kombinationsfeld cboMitarbeiter mit einem solchen SQL-Ausdruck ausgestattet. Dieser lautet folgendermaßen: SELECT tblMitarbeiter.MitarbeiterID, [Nachname] & ", " & [Vorname] AS Mitarbeiter FROM tblMitarbeiter;
152
Abfragen
Dieser Ausdruck wurde über die Entwurfsansicht für Abfragen erstellt, aber nicht sicht bar als Abfrage gespeichert (die Abfrage wird nicht im Navigationsfenster angezeigt, ist aber in der Systemtabelle MSysObjects unter einem Namen wie ~sq_cfrmAbwesenheiten~sq_ ccboMitarbeiter aufgeführt). In diesem Fall wird der reine SQL-Ausdruck in die Eigen schaft Datensatzherkunft des Kombinationsfeldes eingetragen.
3.3.3 Gespeicherte Abfrage als Datensatzquelle Die einfachste, weil ohne VBA- und SQL-Kenntnisse zu bewältigende und daher auch für Einsteiger geeignete Möglichkeit zur Erstellung einer Abfrage bietet die dafür vorgesehene Entwurfsansicht für Abfragen. Damit ist die Einschränkung der Datensatzquelle sowohl bezüglich der Felder als auch der Datensätze möglich und auch die Verwendung von Parametern ist relativ einfach. Die Datensatzherkunft des Kombinationsfeldes cboMitarbeiter lässt sich natürlich ebenso mit einer gespeicherten Abfrage wie mit einem SQL-Ausdruck füllen. Um den SQLAusdruck in eine im Navigationsbereich sichtbar gespeicherte Abfrage zu überführen, klicken Sie einfach auf die Schaltfläche mit den drei Punkten (…) und speichern die nun in der Entwurfsansicht angezeigte Abfrage ab – beispielsweise unter dem Namen qryFrmAbwesenheitenCbo-Mitarbeiter. Auf diese Weise erkennen Sie später schnell, wofür Sie diese Abfrage benötigen. Verwenden Sie nun besser einen SQL-Ausdruck oder eine gespeicherte Abfrage als Datensatzquelle? Performancetechnisch betrachtet besteht kein großer Unterschied – in beiden Fällen wird die Abfrage beim ersten Aufruf kompiliert und damit optimiert (sie he auch Kapitel 14, Abschnitt 14.2.1, »Abfragen und die ACE-Engine«). Bleiben zwei Gründe, die für das Speichern der Abfrage sprechen: Entweder benötigen Sie die Abfra ge an mehreren Stellen oder Sie möchten die Abfrage testen beziehungsweise optimieren, während das Formular in der Formularansicht angezeigt wird. In allen anderen Fällen scheint die Verwendung eines SQL-Ausdrucks naheliegender, zumal der ohnehin spärliche Platz im Navigationsbereich sonst ziemlich schnell überfüllt sein dürfte und seine Suchfunktion zum Einsatz käme.
3.3.4 Datensatzquelle per VBA zuweisen Die Eigenschaften Datensatzquelle und Datensatzherkunft stehen auch unter VBA zur Verfügung – dort verwendet man die Eigenschaftsnamen RecordSource (Formular) und RowSource (Kombinations- und Listenfelder). Sie können diesen Eigenschaften von Formularen sowie Kombinations- und Listenfeldern per VBA den Namen einer Tabelle oder Abfrage oder auch einen SQL-Ausdruck zuweisen. Was macht das für einen Sinn? Manchmal weiß man noch nicht genau, wie die anzuzeigenden Daten gestaltet sind. Das ist meist bei Suchformularen der Fall: Ein Formular bietet mehrere
153
Kapitel 3
Such- und Sortierkriterien an, die der Benutzer mit den gewünschten Werten füllen kann. Man könnte die Kriterien einfach in Form von Parametern an die der Suche zugrunde liegende Abfrage übergeben, aber je nach Komplexität und Anzahl der enthaltenen Tabellen gerät die Abfrage recht komplex. Eine Abfrage über die drei per m:n-Beziehung verknüpften Tabellen tblBestellungen, tblBestelldetails und tblArtikel, deren Suchkriterien sich über die äußeren Tabellen erstrecken, benötigt auch alle Tabellen der Abfrage. Soll eine Suche allerdings alle Artikel liefern, deren Suchkriterien sich lediglich auf die Tabellen tblBestelldetails und tblArtikel erstrecken, kann man die Tabelle tblBestellungen und die notwendige Verknüpfung und damit Zeit und Ressourcen sparen – vorausgesetzt, das Abfrageergebnis gibt keine darin enthaltenen Felder zurück. Hier würde dann eine VBA-Routine zum Einsatz kommen, die einen SQL-Ausdruck mit den benötigten Tabellen zusammensetzt.
3.3.5 Parameter statt Zusammensetzen von SQL-Ausdrücken Auch in anderen, einfacheren Fällen, in denen eine Abfrage lediglich aus einer einzigen Tabelle besteht und nur ein Parameter eingesetzt werden muss, verwenden viele Entwickler VBA, um einen SQL-String zusammenzusetzen und diesen als Datensatzquelle zu verwenden. Das sieht dann beispielsweise so aus: Private Sub ListenfeldAktualisieren() Dim strSQL As String Dim strSQLSelect As String Dim strSQLWhere As String 'Basisabfrage (SELECT-Teil) strSQLSelect = "SELECT AbwesenheitID, StartDatum, EndDatum, " _ & " Abwesenheitsart FROM tblAbwesenheitsarten " _ & "INNER JOIN tblAbwesenheiten " _ & "ON tblAbwesenheitsarten.AbwesenheitsartID = " _ & "tblAbwesenheiten.Abwesenheitart " 'Erstes Kombinationsfeld auswerten If Not Nz(Me!cboMitarbeiter, 0) = 0 Then strSQLWhere = "MitarbeiterID = " & Me!cboMitarbeiter End If 'Zweites Kombinationsfeld auswerten If Not Nz(Me!cboJahr, 0) = 0 Then If Len(strSQLWhere) > 0 Then strSQLWhere = strSQLWhere & " AND " End If
154
Abfragen strSQLWhere = strSQLWhere _ & "Year(tblAbwesenheiten.StartDatum) = " _ & Me!cboJahr.Column(1) End If 'SELECT-Teil zum SQL-Ausdruck hinzufügen strSQL = strSQLSelect 'Falls WHERE-Bedingung vorhanden, 'WHERE-Teil zum SQL-Ausdruck hinzufügen If Len(strSQLWhere) > 0 Then strSQL = strSQL & " WHERE " & strSQLWhere End If 'Neue Datensatzherkunft zuweisen und Listenfeld aktualisieren Me!lstAbwesenheiten.RowSource = strSQL Me!lstAbwesenheiten.Requery End Sub Listing 3.1: Datensatzherkunft für ein Listenfeld per zusammengesetztem SQL-Ausdruck ermit teln
Die Prozedur wird von den beiden Prozeduren aufgerufen, die durch die Ereigniseigen schaft Nach Aktualisierung der beiden Kombinationsfelder cboMitarbeiter und cboJahr ausgelöst werden. Die hier ermittelte SQL-Anweisung ist bei keiner Ausführung kompi liert. Eine Alternative ist die Verwendung einer gespeicherten Abfrage mit Parametern. Die Parameter, die normalerweise die Anzeige eines Dialogs zum Eingeben des Parameters hervorrufen, füllen Sie ebenfalls per VBA. Beim späteren Aufruf weisen Sie dem Listen feld ein Recordset zu, das auf der kompilierten Abfrage inklusive Parametern basiert. Dies funktioniert übrigens erst ab Access XP. Unter Access 2000 und älteren Versionen von Access haben Kombinations- und Listenfelder noch keine Recordset-Eigenschaft. Die Abfrage sieht wie in Abbildung 3.7 aus. Die ersten vier Felder der Abfrage werden angezeigt, die letzten beiden sind lediglich Kriterienfelder. Als Kriterien dienen die per VBA zu füllenden Parameter [cboMitarbeiter] und [cboJahr]. Ersterer wird direkt mit dem Inhalt des Feldes MitarbeiterID verglichen, Letzterer mit dem Ausdruck, der durch die Anwendung der Jahr-Funktion auf dem Inhalt des Feldes StartDatum erzeugt wird. Dabei handelt es sich um die dem Datum entsprechende Jahreszahl. Nun fehlt noch die Prozedur, mit der die Parameter per Code gefüllt werden und das Ergebnis der Abfrage dem Listenfeld zugewiesen wird. Diese Prozedur erstellt ein QueryDef-Objekt auf Basis der Abfrage qryFrmAbwesenheitenLstAbwesenheitParameter. Dieses Objekt enthält eine Auflistung namens Parameters, mit der Sie die in der Abfrage
155
Kapitel 3
gespeicherten Parameter referenzieren und die gewünschten Werte zuweisen können. Der Parameter [cboMitarbeiter] soll mit dem gebundenen Feld der Datensatzherkunft des Kombinationsfeldes cboMitarbeiter gefüllt werden, der Parameter [cboJahr] wird mit dem im Kombinationsfeld cboJahr angezeigten Wert bestückt. Beachten Sie, dass der angezeigte Wert nicht mit dem Wert des gebundenen Feldes übereinstimmt, sondern das Jahr und nicht dessen ID enthält!
Abbildung 3.7: Abfrage mit Parametern
Nach dem Füllen der Parameter wird die Abfrage mit der OpenRecordset-Methode ausgeführt und das Ergebnis in ein Recordset-Objekt geschrieben, das schließlich der entsprechenden Eigenschaft des Listenfeldes zugewiesen wird. Die Prozedur verwendet einige Objekte, Methoden und Eigenschaften der DAO-Biblio thek von Access. Detaillierte Informationen finden Sie in Kapitel 9, »DAO«. Private Sub ListenfeldAktualisierenParameter() Dim db As DAO.Database Dim qdf As DAO.QueryDef Dim rst As DAO.Recordset 'Database- und Querydef-Objekt festlegen Set db = CurrentDb Set qdf = db.QueryDefs("qryFrmAbwesenheitenLstAbwesenheitenParameter") 'Parameter [cboMitarbeiter] mit der im Kombinationsfeld 'cboMitarbeiter ausgewählten MitarbeiterID füllen qdf.Parameters("cboMitarbeiter").Value = Me!cboMitarbeiter
156
Abfragen 'Parameter [cboJahr] mit dem im Kombinationsfeld angezeigten 'Jahr füllen qdf.Parameters("cboJahr").Value = Me!cboJahr.Column(1) 'Abfrage ausführen und Ergebnis in Recordset-Objekt ablegen Set rst = qdf.OpenRecordset 'Recordset der gleichnamigen Eigenschaft des Listenfelds zuweisen Set Me!lstAbwesenheiten.Recordset = rst Set rst = Nothing Set qdf = Nothing Set db = Nothing End Sub Listing 3.2: Listenfeld mit Parameterabfrage füllen
Der erste Test mit dieser Routine läuft nur zufrieden stellend, wenn in beiden Kom binationsfeldern ein Wert ausgewählt ist. Eigentlich soll das Listenfeld beim Öffnen des Formulars alle Abwesenheiten anzeigen, beim Auswählen eines Mitarbeiters ohne Jahr alle Abwesenheiten dieses Mitarbeiters für alle Jahre, um beim Auswählen lediglich eines Jahres alle Abwesenheiten dieses Jahres für alle Mitarbeiter zu berücksichtigen. Das funktioniert deshalb nicht, weil die Abfrage beispielsweise bei fehlender Auswahl des Mitarbeiters den Wert Null als Parameter übergibt. Und da es keine Mitarbeiter mit der MitarbeiterID Null gibt, werden auch keine Abwesenheiten angezeigt. Das Gleiche gilt für die Auswahl des Jahres. Die Verwendung von Null als Standardwert bei fehlender Eingabe eines Parameters ist übrigens nicht zwingend, sondern in diesem Fall durch die Verwendung des Standarddatentyps Variant bedingt. Sie können für einen Parameter in der Abfrage definition durchaus andere Datentypen angeben; folglich werden dann auch die entsprechenden Standardwerte bei fehlendem Wert verwendet (etwa 0 bei Zahlentypen oder eine leere Zeichenkette bei String-Variablen). Sie müssen also dafür sorgen, dass die Parameter im Falle einer fehlenden Auswahl neutralisiert werden. Folgender Trick hilft dabei weiter: Fügen Sie in die Kriterienspalte der betroffenen Felder die folgenden abgewandelten Ausdrücke ein. Ein Datensatz wird angezeigt, wenn das Kriterium wahr ist – und das ist entweder bei passendem Para meterwert oder bei der Übergabe des Wertes Null der Fall: [cboMitarbeiter] Oder [cboMitarbeiter] Ist Null [cboJahr] Oder [cboJahr] Ist Null
Interessant ist, was Access nach dem Schließen und erneuten Öffnen aus den Kriterien macht (siehe Abbildung 3.8).
157
Kapitel 3
Abbildung 3.8: Zwei harmlose Kriterienausdrücke nach der Überarbeitung durch Access
3.3.6 Abfragen mit Parameter oder zusammengesetzte SQL-Ausdrücke? Welche der beiden Varianten Sie verwenden, hängt von der Menge der Parameter ab. Je mehr Parameter vorkommen, desto langsamer wird die Abfrage und umso komplizierter wird der Abfrageentwurf. Wenn Sie sich den Abfrageentwurf aus Abbildung 3.8 ansehen und sich vorstellen, wie eine Abfrage mit vier oder mehr Parametern aussehen wird, können Sie sich vermutlich ausmalen, wie viel Spaß eine nachträgliche Änderung am Abfrageentwurf machen wird. Für Abfragen mit mehreren Parametern empfiehlt sich daher eher die Verwendung eines per VBA zusammengesetzten SQL-Ausdrucks.
3.3.7 Probleme mit Kriterienausdrücken bei SQL-Ausdrücken in VBA Viele Fehler bei der Verwendung von SQL-Ausdrücken unter VBA passieren im Zusam menhang mit den Kriterien. Mal meldet Access das Problem, dass zu wenig Parameter übergeben wurden (siehe Abbildung 3.9), ein anderes Mal funktionieren die Vergleiche mit übergebenen Datumsangaben nicht.
3.3.8 Zeichenkette oder Zahlenwert? Der Fehler aus Abbildung 3.9 resultiert fast immer aus dem Fehlen von Anführungszei chen im SQL-Ausdruck beim Verwenden von Zeichenketten als Kriterium.
158
Abfragen
Abbildung 3.9: Fehlermeldung beim Verwenden einer SQL-Anweisung per VBA
Das folgende Listing zeigt einen solchen Fehler: Public Function MitarbeiterSuchen(strNachname As String) … Set rst = CurrentDB.OpenRecordset("SELECT MitarbeiterID, " _ & "Vorname, Nachname FROM tblMitarbeiter WHERE Nachname = " _ & strNachname) … End Function Listing 3.3: Falsche Verwendung einer Zeichenkette als Kriterium
In dieses Listing wurden keine Anführungszeichen für die Zeichenkette integriert. Der SQL-Ausdruck sieht für den Aufruf MitarbeiterSuchen "Müller" wie folgt aus: SELECT MitarbeiterID, Vorname, Nachname FROM tblMitarbeiter WHERE Nachname = Müller
»Müller« wird hierbei nicht als Zeichenkette, sondern als Parameter ausgelegt. Da für diesen kein Wert vorliegt, erscheint obige Fehlermeldung. Die Lösung des Problems ist einfach. Fassen Sie den Parameter einfach in Anführungszeichen oder Hochkommata ein (in einer Zeile): Set rst = db.OpenRecordset("SELECT MitarbeiterID, Vorname, Nachname FROM tblMitarbeiter WHERE Nachname = '" & strNachname & "'")
oder besser Set rst = db.OpenRecordset("SELECT MitarbeiterID, Vorname, Nachname FROM tblMitarbeiter WHERE Nachname = """ & strNachname & """")
159
Kapitel 3
3.3.9 Probleme mit Datumsangaben Auch Datumsangaben führen immer wieder zu Problemen. Die folgende Routine soll beispielsweise Informationen über Abwesenheiten ausgeben, deren Beginn in einem bestimmten Zeitraum liegt, der durch die Parameter datStart und datEnde angegeben werden kann. Public Function AbwesenheitenZeitraum(datStart As Date, datEnde As Date) … Set rst = db.OpenRecordset("SELECT * FROM tblAbwesenheiten " _ & "WHERE Startdatum BETWEEN " & datStart & " AND " & datEnde, _ dbOpenDynaset) … End Function Listing 3.4: Ermitteln von Abwesenheiten in einem bestimmten Zeitraum
Wenn Sie die Routine mit folgendem Aufruf starten, erscheint die Fehlermeldung aus Abbildung 3.10. Auf den ersten Blick scheinen hier die Anführungszeichen zu fehlen. AbwesenheitenZeitraum "1.1.2007", "31.1.2007"
Abbildung 3.10: Fehlermeldung bei der Verwendung von Datumsangaben in SQL-Ausdrücken
Ändern Sie den Aufruf der SQL-Anweisung wie folgt um, gibt es allerdings eine andere Fehlermeldung (siehe Abbildung 3.11): Set rst = db.OpenRecordset("SELECT * FROM tblAbwesenheiten WHERE Startdatum BETWEEN '" & datStart & "' AND '" & datEnde & "'", dbOpenDynaset)
Diesmal hat Access Probleme mit dem Datentyp – ein String scheint das Bedürfnis nach einem Wert des Typs DATETIME nicht zu befriedigen. Mit dem Wissen, dass Access Datumsangaben intern als Zahlenwerte behandelt, erscheint dies schnell logisch. Dabei
160
Abfragen
gibt es zwei Möglichkeiten: Verwenden Sie den Datentyp Double, um Datumsangaben inklusive Uhrzeit zu verwalten, dann entspricht die Zahl vor dem Komma der Anzahl Tage seit dem 31.12.1899 und die Zahl nach dem Komma der Anzahl Sekunden, die am angegebenen Tag verstrichen sind. Für Datumsangaben ohne Uhrzeit reicht dementsprechend der Datentyp Long aus.
Abbildung 3.11: Auch die Zeichenkette taugt nicht als Datumskriterium
Nun müssen Sie aber nicht alle Datumsangaben explizit in einen Zahlen-Datentyp umwandeln. Es reicht, wenn Sie ein standardisiertes Format verwenden. Dieses hat die Form yyyy-mm-dd. Zusätzlich fassen Sie diesen Ausdruck in der Abfrage in RauteZeichen (#) ein. Im obigen Code sieht das Ganze dann wie folgt aus (in einer Zeile): Set rst = db.OpenRecordset("SELECT * FROM tblAbwesenheiten WHERE Startdatum BETWEEN #" & Format(strStart, "yyyy-mm-dd") & "# AND #" & Format(strEnde, "yyyy-mm-dd") & "#", dbOpenDynaset)
Das Formatieren des Datums und das Einfassen in Rauten lässt sich auch per Funktion erledigen: Public Function SQLDatum(strDatum As String) As String SQLDatum = Format(strDatum, "\#yyyy-mm-dd\#") End Function Listing 3.5: Funktion zum Standardisieren von Datumsangaben
Die Zeile aus obiger Routine sähe dann so aus (in einer Zeile): Set rst = db.OpenRecordset("SELECT * FROM tblAbwesenheiten WHERE Startdatum BETWEEN " & SQLDatum(strStart) & " AND " & SQLDatum(strEnde), dbOpenDynaset)
161
Kapitel 3
3.3.10 Verweis auf Steuerelemente Access-SQL erlaubt direkte Verweise auf Formulare und Steuerelemente. Das ist hilfreich, wenn Sie etwa den Feldinhalt eines Formulars als Abfragekriterium verwenden möchten. Eine solche Abfrage sieht beispielsweise wie in Abbildung 3.12 aus.
Abbildung 3.12: Abfrage mit einem Verweis auf ein Formularsteuerelement
Ein Blick in die entsprechende SQL-Anweisung zeigt, dass der Ausdruck tatsächlich im gleichen Format wie in VBA in die Abfrage integriert wurde: SELECT MitarbeiterID, Nachname, Vorname, AbteilungID FROM tblMitarbeiter WHERE AbteilungID=[Forms]![frmMitarbeiter]![cboAbteilungen];
Dies funktioniert sogar, wenn Sie mit anderen Backends wie etwa MySQL arbeiten. Beim Zusammensetzen von SQL-Abfragen via VBA bietet es sich allerdings an, die entsprechenden Ausdrücke direkt auszulesen und als festen Parameterwert in die Abfrage zu integrieren. Die Verwendung einer fest in eine Abfrage eingebauten Referenz bringt den Nachteil mit sich, dass Sie diese Abfrage nicht einsetzen können, wenn Sie einen anderen als diesen Parameter verwenden möchten. Die Variante, eine Abfrage ohne Formularreferenzen zu erstellen und derartige Kriterien erst per VBA oder direkt in einer Datensatzquelle- oder Datensatzherkunft-Eigenschaft zuzuweisen, ist flexibler.
3.4 Aktualisierbarkeit von Abfragen Je nachdem, wie die in einer Abfrage enthaltenen Tabellen beschaffen sind und wie Sie diese zusammensetzen, sind Abfragen nicht aktualisierbar, das heißt, dass Sie über die
162
Abfragen
Datenblattansicht und damit auch über die Anzeige in Formularen keine Datensätze bearbeiten oder hinzufügen können. Es ist sicher jedem Access-Entwickler schon einmal passiert, dass er per VBA einen Datensatz einer Abfrage ändern oder hinzufügen wollte und eine entsprechende Fehlermeldung erschien, für die es scheinbar keine Erklärung gab.
3.4.1 Wie erkennen Sie, ob das Abfrageergebnis aktualisierbar ist? Wenn Sie eine Abfrage in der Datenblattansicht öffnen, erkennen Sie recht schnell, ob diese aktualisierbar ist – die Schaltfläche zum Springen auf einen neuen Datensatz ist ausgeblendet und unter dem letzten Datensatz befindet sich keine leere Zeile zum Anlegen eines neuen Datensatzes (siehe Abbildung 3.13).
Abbildung 3.13: Eine nicht aktualisierbare Abfrage erkennt man an der deaktivierten Schalt fläche »Neuer Datensatz« und an der fehlenden Zeile mit einem leeren neuen Datensatz
3.4.2 Nicht aktualisierbare Abfragen Nachfolgend finden Sie Beispiele für Abfragen, die niemals aktualisierbar sind: Abfragen mit zwei nicht verknüpften Tabellen. Beispiele sind alle Abfragen, die alle Kombinationen aus den Datensätzen zweier Tabellen anzeigen sollen (siehe Abbildung 3.14). Abfragen mit drei oder mehr Tabellen, deren innere Tabelle die 1-Seite für die beiden äußeren Tabellen stellt. Beispiel: Sie möchten zu einem Mitarbeiter gleichzeitig die Abwesenheiten und den Urlaubsanspruch ausgeben (siehe Abbildung 3.15).
163
Kapitel 3
Abfragen mit Gruppierungen und Aggregatfunktionen Abfragen, bei denen die Eigenschaft Keine Duplikate auf Ja eingestellt ist Weitere Möglichkeiten sind folgende: Abfragen, deren Eigenschaft Recordsettyp den Wert Snapshot besitzt PassThrough-Abfragen UNION-Abfragen Sollte sich einmal eine Abfrage als nicht aktualisierbar erweisen, von der Sie es eigentlich nicht erwarten, prüfen Sie diese zunächst auf die oben genannten Eigenschaften.
Abbildung 3.14: Diese Abfrage liefert kein aktualisierbares Ergebnis
Abbildung 3.15: Auch diese Abfrage ist nicht aktualisierbar
164
Abfragen
3.5 UNION-Abfragen UNION-Abfragen bieten die Möglichkeit, die Daten mehrerer gleichartig aufgebauter Tabellen mit einer Abfrage zu vereinen. Dazu erstellen Sie zwei oder mehr gleichartig aufgebaute Abfragen und verketten diese mit dem UNION-Schlüsselwort. Entscheidend ist, dass alle beteiligten Abfragen die gleiche Anzahl Felder haben und dass die jeweils an der gleichen Stelle befindlichen Felder den gleichen Datentyp besitzen. Haben die Daten verschiedene Datentypen, konvertiert die ACE in den meisten Fällen beide in einen Variant-Wert und gibt anschließend einen String-Wert aus. Probleme gibt es hier, wenn GUIDs und andere Datentypen gemischt werden. Weitere Grundlagen zu UNION-Abfragen finden Sie in Kapitel 8, Abschnitt 8.3.9, unter »Zusammenfassen von Abfrageergebnissen mit UNION«.
3.5.1 UNION-Abfragen zur Optimierung von Kombinationsfeldern Sie können eine UNION-Abfrage beispielsweise dazu verwenden, Kombinationsfelder zu optimieren. Wenn Kombinationsfelder keinen Eintrag enthalten, zeigen diese ein leeres Feld an. Praktischer und eine eindeutige Aufforderung an den Benutzer wäre es, wenn Kombinationsfelder ohne Wert etwa die Zeichenkette anzeigen würden (siehe Abbildung 3.16).
Abbildung 3.16: Vorgefülltes Kombinationsfeld
Als Datensatzherkunft des Kombinationsfeldes dient dabei die folgende SQL-Abfrage, die Sie direkt in die SQL-Ansicht der Abfrage eingeben müssen:
165
Kapitel 3 SELECT 0 AS AbteilungID, '' AS Abteilung FROM tblAbteilungen UNION SELECT AbteilungID, Abteilung FROM tblAbteilungen;
Dies ist ein gutes Beispiel für die Zweckentfremdung einer UNION-Abfrage, denn der aus dem ersten Teil der Abfrage stammende Wert ist eigentlich gar nicht in der Tabelle vorhanden. Deshalb gibt man dort nicht nur die Feldnamen, sondern die konkreten Werte an. Der Übersicht halber versieht man die einzelnen Feldwerte noch mit dem AS-Schlüsselwort und fügt den eigentlichen Feldnamen hinzu. Letzteres ist aber nicht unbedingt notwendig. Wenn Sie einen Dummy-Datensatz wie im ersten Teil der obigen UNION-Abfrage benötigen, brauchen Sie in allein stehenden Tabellen nur den ersten Teil der Abfrage zu verwenden: SELECT 0 AS AbteilungID, '' AS Abteilung. Die Angabe einer Ursprungstabelle mit FROM tblAbteilungen ist nur in Zusammenhang mit UNIONAbfragen erforderlich. Wichtig ist bei diesem Beispiel, dass der im ersten Abfrageteil verwendete Wert für die gebundene Spalte der zukünftigen Datensatzherkunft kleiner ist als alle Werte, die aus der oder den anderen Tabellen noch hinzukommen. Anderenfalls lässt sich nur schwer eine sinnvolle Sortierung festlegen – es sei denn, man fügt noch ein individuelles Sortierfeld hinzu. Wenn Sie hingegen nach dem angezeigten Feld sortieren möchten, müssen Sie erstens das Sortierkriterium an den letzten Teil der UNION-Abfrage anhängen und zweitens dafür sorgen, dass der ohne Auswahl angezeigte Datensatz der erste unter der angegebenen Sortierung ist: SELECT 0 AS AbteilungID, '' AS Abteilung FROM tblAbteilungen UNION SELECT AbteilungID, Abteilung FROM tblAbteilungen ORDER BY Abteilung;
Da das Kleiner-Zeichen (<) im ASCII-Code vor den Buchstaben angeordnet ist, sind hier keine weiteren Maßnahmen erforderlich. Wollen Sie hingegen nur den Eintrag Auswählen ohne spitze Klammern verwenden, müssten Sie ein zusätzliches Sortierfeld anhängen, bei dem Sie für den ersten Teil der Abfrage einen Wert angeben, der auf jeden Fall vor allen anderen liegt. Außerdem legen Sie dieses Sortierfeld als ORDER BY-Kriterium fest: SELECT 0 AS AbteilungID, 'Auswählen' AS Abteilung, 'AAAA' AS Sortierung FROM tblAbteilungen UNION SELECT AbteilungID, Abteilung, Abteilung As Sortierung FROM tblAbteilungen ORDER BY Sortierung;
166
Abfragen
3.5.2 Eindeutige Schlüssel mit UNION-Abfragen Einen gravierenden Nachteil haben UNION-Abfragen: Wenn die zumeist aus mehreren Tabellen zusammengeführten Daten eindeutig identifiziert werden sollen, um etwa einen ausgewählten Datensatz zu löschen, ist »Hängen im Schacht«. Das Problem ist, dass auch die Primärschlüssel aus mehreren Tabellen zusammengeführt werden und diese daher nicht zwangsläufig eindeutig sind, denn es können durchaus mehrere Tabellen etwa den Schlüsselwert 1 besitzen. Die einfachste Lösung ist die Verwendung von GUIDs als Primärschlüssel. Diese Werte sind nicht nur über mehrere Tabellen, sondern sogar weltweit eindeutig – sofern sie mit der entsprechenden Systemfunktion erzeugt wurden (weitere Informationen finden Sie in Kapitel 2, Abschnitt 2.6, »Autowerte als Long oder GUID?«). Selbst das Löschen eines Datensatzes, der aus einer UNION-Abfrage ausgewählt wurde, ist bei Vorhandensein einer GUID einfach: Löschen Sie einfach aus allen beteiligten Tabellen den Datensatz mit der betroffenen GUID – irgendwo werden Sie schon den richtigen treffen und falsche Datensätze löschen Sie damit auch nicht. Ohne GUID ist die eindeutige Identifikation von Daten aus UNION-Abfragen noch schwieriger. Als Beispiel dient die folgende Abfrage: SELECT KundeID, Vorname, Nachname FROM tblKunden UNION SELECT MitarbeiterID, Vorname, Nachname FROM tblMitarbeiter;
Abbildung 3.17 zeigt auf, was passieren kann: Der Wert 3 kommt in je einem Datensatz der beteiligten Tabellen vor.
Abbildung 3.17: UNION-Abfragen garantieren keinen eindeutigen Index
Um dies zu verhindern, verwenden Sie einen kombinierten Wert aus KundeID beziehungsweise MitarbeiterID und dem jeweiligen Tabellennamen. Die UNION-Abfrage sieht nun so aus:
167
Kapitel 3 SELECT 'Kunde' & KundeID AS PersonID, Vorname, Nachname FROM tblKunden UNION SELECT 'Mitarbeiter' & MitarbeiterID AS PersonID, Vorname, Nachname FROM tblMitarbeiter;
Das Ergebnis überzeugt ebenfalls, die Datensätze verfügen nun über ein eindeutiges Feld (siehe Abbildung 3.18).
Abbildung 3.18: Eine UNION-Abfrage mit eindeutigem Feld
Gegenüber der Variante mit dem GUID-Wert als Primärschlüssel fällt diese Variante jedoch deutlich ab. Allein das Handling ist wesentlich aufwändiger: Wenn Sie etwa einen mit dieser UNION-Abfrage ausgewählten Datensatz löschen möchten, müssten Sie zunächst über den zusammengesetzten Schlüssel ermitteln, aus welcher Tabelle der Datensatz stammt, und diesen dann dort löschen. Daraus resultiert letzten Endes die Empfehlung, Daten, die gleich aufgebaut sind, auch in einer einzigen Tabelle zu speichern. Wenn die Daten gelegentlich unterschiedliche Eigenschaften aufweisen und daher unterschiedliche Felder benötigen, lässt sich dies über 1:1-Beziehungen vermutlich leichter realisieren.
3.5.3 INSERT INTO mit UNION-Abfragen Abfragen sind nun einmal nichts Beständiges und deshalb sind auch die mit einer UNION-Abfrage ermittelten Daten grundsätzlich erst einmal nicht zur Weitergabe geeignet. Jeglicher Versuch, die Daten per INSERT INTO-Abfrage einfach in eine temporäre Tabelle zu schreiben, schlägt fehl: SQL mag keine INSERT-Queries mit UNIONAbfragen als Datensatzquelle (siehe Abbildung 3.19). Die Lösung ist, wie in Abbildung 3.20 die UNION-Abfrage zu speichern und die Anfügeabfrage auf diese Abfrage zugreifen zu lassen. Das Ganze kann man wiederum auch in eine Abfrage schreiben: INSERT INTO tblMitarbeiterUndKunden(MitarbeiterUndKundenID, Vorname, Nachname) SELECT * FROM (SELECT 'Kunde' & KundeID AS MitarbeiterUndKundenID, Vorname, Nachname FROM tblKunden UNION SELECT 'Mitarbeiter' & MitarbeiterID AS MitarbeiterUndKundenID, Vorname, Nachname FROM tblMitarbeiter)
168
Abfragen
Abbildung 3.19: UNION-Abfragen in INSERT INTO-Statements funktionieren nicht
Abbildung 3.20: INSERT INTO mit UNION-Abfrage
3.6 Suchen in m:n-Beziehungen Wenn Sie die Anleitungen zur Normalisierung in Kapitel 2 sorgfältig berücksichtigen, dann haben Sie beispielsweise eine Menge triviale Eigenschaften, zu denen jederzeit neue hinzukommen können, in eine m:n-Beziehung ausgelagert. Das dortige Beispiel bezog sich auf Fahrzeuge und ihre Ausstattungsmerkmale. Nach diesem Schritt haben Sie zwar ein sauberes Datenmodell, aber dummerweise lässt sich nicht mehr so einfach per Abfrage ermitteln, welche Autos beispielsweise mit einer Klimaanlage und Servolenkung ausgestattet sind, aber etwa kein Navigationssystem besitzen. Hätte man in der Version mit allen Eigenschaften in einer einzigen Tabelle nur die entsprechenden Felder mit Kriterien wie True oder False ausstatten müssen, ist nun ein wenig mehr Aufwand notwendig.
169
Kapitel 3
Suchen nach Rezepten mit bestimmten Zutaten Als Beispiel dient nun aber eine neue Variante: Die Zuordnung von Zutaten zu einzelnen Rezepten. Stellen Sie sich vor, was Sie alles anstellen könnten, wenn Sie nur noch in Kühl- und Vorratsschrank schauen, die dort vorhandenen Zutaten in ein Formular eingeben (ok, nicht alle, aber zumindest die, auf die Sie gerade Appetit haben) und dann umgehend alle Rezepte erhalten, die sich mit diesen Zutaten zaubern lassen. Betrachtet man das Datenmodell aus Abbildung 3.21, fällt einem nur leider gerade kein Rezept ein, auf dessen Basis man die passende Abfrage basteln könnte.
Abbildung 3.21: Aufbau der Beziehung zwischen Rezepten und Zutaten
Mit einer Zutat jedenfalls funktioniert der Ansatz aus Abbildung 3.22. Das Ergebnis liefert alle Rezepte, welche die Zutat »Pfeffer« enthalten – das ist einfach.
Abbildung 3.22: Abfrage zur Ermittlung aller Rezepte mit einer bestimmten Zutat
Jetzt fügen Sie eine Zutat hinzu: Mal sehen, welche Rezepte übrig bleiben, wenn auch noch Salz enthalten sein soll. Nur wohin mit dem neuen Kriterium? Wenn man es ganz
170
Abfragen
naiv als »Oder«-Kriterium in die Spalte Zutat einfügt, enthält das Abfrageergebnis mehr Datensätze als vorher – das war abzusehen und ist falsch. Schließlich sollte eine weitere Zutat die Ergebnismenge einschränken oder maximal gleich viele Ergebnisse zurückliefern. Also verwenden Sie als Kriterium nun ="Pfeffer" Und "Salz". Das Ergebnis? Es enthält gar keine Datensätze mehr. Gibt es keine Rezepte, die Pfeffer und Salz enthalten? Doch, natürlich, aber es gibt bei keinem Rezept ein Zutatenfeld, das zwei Zutaten gleichzeitig enthält. Schauen Sie sich noch einmal genauer das Ergebnis der Variante mit »Oder« an (siehe Abbildung 3.23). Logischerweise sind dort alle Rezepte, die beide angegebenen Zutaten enthalten, zweimal aufgeführt; alle Rezepte, die nur eine der beiden Zutaten enthalten, findet man nur einmal.
Abbildung 3.23: Ergebnis der »Oder«-Variante der Zutaten-Suche
Damit lässt sich doch etwas anfangen. Gleiche Einträge in einer Abfrage lassen sich gruppieren und zu einer Gruppierung lässt sich auch die Anzahl der enthaltenen Datensätze ausgeben. Passen Sie also die Abfrage wie in Abbildung 3.24 an: Blenden Sie mit dem Ribbon-Eintrag Entwurf|Einblenden/Ausblenden|Summen bei aktivierter Entwurfsansicht die Funktionszeile ein. Stellen Sie für das Feld RezeptID die Funktion Anzahl ein. Das Feld Zutat dient nur als Bedingung und darf nicht angezeigt werden. Dazu deaktivieren Sie die Zeile Anzeigen und stellen als Funktion den Wert Bedingung ein. Das Ergebnis aus Abbildung 3.25 überzeugt: Die Rezepte, die beide Zutaten enthalten, werden mit der Anzahl 2 ausgegeben. Damit erhalten Sie nicht nur die Rezepte mit allen gewünschten Zutaten, sondern auch andere – und zwar nach der Qualität des Treffers sortiert.
171
Kapitel 3
Abbildung 3.24: Diese Abfrage sortiert die Rezepte nach der Anzahl der enthaltenen Wunschzu taten
Abbildung 3.25: Rezepte und die Anzahl der enthaltenen Wunschzutaten
3.7 Handhabung von 1:1-Beziehungen In Kapitel 2, Abschnitt 2.5.6, »1:1-Beziehungen«, haben Sie erfahren, wie Sie Daten von Tabellen in zwei oder mehr per 1:1-Beziehung verknüpfte Tabellen aufteilen. Offen ist noch, wie Sie mit solchen Tabellen arbeiten, wenn es um die Eingabe, das Bearbeiten und Löschen von Daten geht. Mit einer geeigneten Abfrage lassen sich die Daten in per 1:1-Beziehung verknüpften Tabellen genauso bearbeiten wie die Daten einer einzelnen Tabelle. Als Beispiel dient die in Kapitel 2 vorgestellte Beziehung, bei der die erste Tabelle der 1:1-Beziehung Personendaten und die zweite Tabelle Erweiterungsdaten zu den Daten der ersten Tabelle enthält (siehe Abbildung 3.26).
172
Abfragen
Abbildung 3.26: 1:1-Beziehung zwischen Personen auf der einen und Angestellten und freien Mitarbeitern auf der anderen Seite
Für die Bearbeitung ist lediglich interessant, ob Sie Personen mit der Ausprägung »Angestellter« oder mit der Ausprägung »freier Mitarbeiter« behandeln möchten. Wenn Sie die Daten der Angestellten bearbeiten wollen, verwenden Sie die in Abbildung 3.27 abgebildete Abfrage. Die Abfrage enthält alle Felder der beiden Tabellen mit Ausnahme des Primärschlüssel feldes der Tabelle tblAngestellte. Auch das Fremdschlüsselfeld dieser Tabelle muss nicht angezeigt werden, aber zu Beispielzwecken sollten Sie es einbauen. In der Abbildung enthält die Abfrage aus Platzgründen nur die wichtigsten Felder der zugrunde liegenden Tabellen.
Abbildung 3.27: Zusammenführen der Tabellen einer 1:1-Beziehung per Abfrage
173
Kapitel 3
Wenn Sie die Abfrage in der Datenblattansicht anzeigen, können Sie dort Daten wie in einer ganz normalen Tabelle einfügen. Dabei kann allerdings folgendes Problem entstehen: Geben Sie einmal nur Daten in Felder der Tabelle tblPersonen ein (also etwa Vorname und Nachname), schließen Sie die Abfrage und öffnen Sie diese erneut. Der Datensatz scheint verschwunden, zumindest ist er im Sinne der 1:1-Beziehung nicht vorhanden. Der Grund ist ganz einfach: Mit der getätigten Eingabe wird in der Tabelle tblAngestellte kein Datensatz angelegt und die Abfrage zeigt nur jene Kombinationen von Datensätzen der Tabellen tblPersonen und tblAngestellte an, bei denen der Inhalt des Feldes PersonID in beiden Tabellen gleich ist. Den Beweis liefert ein kurzer Blick in die Tabelle tblPersonen: Dort findet sich nämlich der frisch angelegte Datensatz, allein das Pendant in der Tabelle tblAngestellte fehlt. Geben Sie nun einen kompletten Datensatz ein und füllen Sie die Felder von links nach rechts, werden Sie feststellen, dass das Feld PersonID der Tabelle tblAngestellte automa tisch mit dem gleichen Wert wie in der Tabelle tblPersonen gefüllt wird, sobald Sie die Eingabe in eines der Felder der Tabelle tblAngestellte abgeschlossen haben (siehe Abbil dung 3.28).
Abbildung 3.28: Das Eingeben von Daten
Wenn Sie zuerst ein Feld der Tabelle tblAngestellte füllen, erhält das Feld PersonID dieser Tabelle zunächst den Wert 0. Erst wenn Sie mindestens ein Feld der Tabelle tblPersonen gefüllt haben, erhalten beide PersonID-Felder den über die Tabelle tblPersonen generierten Autowert. Bleibt die letzte Möglichkeit: Sie füllen lediglich die Felder der Tabelle tblAngestellte und versuchen, den Datensatz zu speichern. Dies lässt Access nicht zu: Es weist mit einer entsprechenden Meldung darauf hin, dass wegen aktivierter referentieller Integrität der Beziehung zwischen den beiden Tabellen zunächst ein passender Datensatz in der Tabelle tblPersonen angelegt werden muss (siehe Abbildung 3.29). Wie sorgen Sie nun dafür, dass vor dem Speichern alle benötigten Daten eingegeben werden, ohne dass Access seine eigenen Meldungen anzeigt? In der Abfrage funktio-
174
Abfragen
niert dies gar nicht: Es besteht keine Möglichkeit, die Meldung aus Abbildung 3.25 abzufangen – weder durch Setzen der Eingabe erforderlich-Eigenschaft noch durch die Verwendung der Gültigkeitsregel. Das ist aber auch nicht schlimm, denn wie bereits erwähnt, sollen Tabellen und Abfragen ohnehin nicht zur direkten Dateneingabe verwendet werden. Und in den dafür vorgese henen Formularen gibt es sowieso ganz andere Mittel. Mehr dazu erfahren Sie in Kapi tel 4, Abschnitt 4.5.5, »1:1-Beziehungen«.
Abbildung 3.29: Diese Meldung erscheint, wenn Sie unvollständige Daten in eine Abfrage mit zwei per 1:1-Beziehung verknüpften Daten eingeben
Behandlung von 1:1-Beziehungen mit ergänzenden Feldern Die oben vorgestellte 1:1-Beziehung dient dem »Vererben« der in der Tabelle tblPersonen gespeicherten Eigenschaften an speziellere Personentypen wie Angestellte oder freie Mitarbeiter. Es gibt auch 1:1-Beziehungen mit wesentlich weniger anspruchsvollem Hintergrund. Die 1:1-Beziehung aus Abbildung 3.30 dient beispielsweise nur dazu, einer Tabelle ein Drucken-Feld hinzuzufügen, ohne dass die Tabelle tatsächlich erweitert wird. Die passende Verknüpfung sieht wie in Abbildung 3.30 aus. Wenn Sie die beiden Tabellen so wie in Abbildung 3.31 in den Abfrageentwurf übernehmen, werden Sie nach dem Wechsel in die Datenblattansicht ein leeres Abfrageergebnis vorfinden (zumindest, wenn Sie noch keine Datensätze in der Tabelle tblMitarbeiterDrucken angelegt haben). Der Grund ist einfach: Die Abfrage zeigt nur Daten, für die in beiden Tabellen ein Datensatz vorliegt. Da die Tabelle tblMitarbeiterDrucken im Urzustand zunächst keine Daten enthält, ist das Abfrageergebnis noch leer. Leider lässt sich über diese Abfrage auch kein Datensatz zur Tabelle tblMitarbeiterDrucken hinzufügen. Um dies zu ermöglichen, ändern Sie in der Abfrage den Beziehungstyp wie in Abbildung 3.32 und erzeugen somit einen LEFT JOIN. Damit werden nun definitiv alle Datensätze der Tabelle tblMitarbeiter angezeigt, auch wenn es nicht für alle einen verknüpften Datensatz in der Tabelle tblMitarbeiterDrucken gibt.
175
Kapitel 3
Abbildung 3.30: 1:1-Beziehung zum Anfügen eines einzelnen Feldes
Abbildung 3.31: Abfrage mit Drucken-Feld in der Entwurfsansicht
Nun sieht die Datenblattansicht der Abfrage schon wesentlich erfreulicher aus: Zu jedem Mitarbeiter wird das Drucken-Feld angezeigt, obwohl es eigentlich gar keine verknüpften Datensätze gibt. In der Tabelle tblMitarbeiterDrucken wird dann auch tatsächlich erst ein Datensatz angelegt, wenn Sie in der Abfrage einen Haken in das entsprechende Feld setzen (siehe Abbildung 3.33). Wenn Sie das Feld in der Abfrage wieder deaktivieren, bleibt der Datensatz in der Tabelle tblMitarbeiterDrucken allerdings vorhanden. Um Platz in der Datenbank zu schaffen, empfiehlt es sich daher, regelmäßig alle Datensätze aus der Tabelle tblMitarbeiterDrucken zu entfernen, deren Feld Drucken den Wert False enthält.
176
Abfragen
Abbildung 3.32: Ändern der Verknüpfungseigenschaften
Abbildung 3.33: Das Drucken-Feld ist für alle Mitarbeiter verfügbar
3.8 Extremwerte per Abfrage ermitteln Unterabfragen sind oft ein bewährtes Mittel, um Kriterien für Abfragen zu ermitteln – sei es zur Ermittlung nur eines oder auch mehrerer Kriterien (IN-Operator). Besonders interessant ist die Möglichkeit, in einer Unterabfrage auf den Inhalt des in der Haupt abfrage enthaltenen Datensatzes zuzugreifen. So lassen sich beispielsweise Informatio nen ermitteln, die auf eine Gruppe von Datensätzen bezogen sind.
3.8.1 Extremwert einer Gruppierung ermitteln Ein Beispiel sind Extremwerte von Gruppierungen. Wenn Sie in den Tabellen Artikel und Kategorien der Nordwind-Datenbank den teuersten Artikel einer Kategorie ermitteln
177
Kapitel 3
möchten, ist das kein Problem. Die Abfrage aus Abbildung 3.34 hilft dann weiter – hier wird beispielsweise der höchste Preis eines Artikels der Kategorie Getränke ermittelt.
Abbildung 3.34: Ermitteln des maximalen Preises von Artikeln der Kategorie Getränke
Leider gibt die Abfrage nur den Preis, aber keine weiteren Informationen zum Namen des Artikels aus, da das Ergebnis über eine Gruppierung herbeigeführt wurde. Sie können noch nicht einmal das Abfrageergebnis als Unterabfrage einer Abfrage verwenden, die dann alle gewünschten Daten enthält, da diese Unterabfrage kein eindeutiges Feld enthielte.
3.8.2 Extremwert per TOP und ORDER BY Also tricksen Sie ein wenig: Verwenden Sie statt der Gruppierung eine nach dem Preis sortierte Abfrage und die TOP-Option der SELECT-Klausel, um nur den ersten und damit teuersten Datensatz zurückzugeben (siehe Abbildung 3.35).
3.8.3 Extremwerte per Unterabfrage Diese Abfrage liefert das richtige Ergebnis einschließlich Artikelinformationen zurück. Im nächsten Schritt sollen nun in einer Abfrage alle teuersten Artikel ihrer Kategorie ausgegeben werden – und hier kommen die korrelierten Haupt- und Unterabfragen zum Zuge. Zum besseren Verständnis findet noch ein Zwischenschritt statt: Die Abfrage aus Abbildung 3.36 enthält eine Unterabfrage zur Ermittlung des teuersten Artikels, dessen Detaildaten in der Hauptabfrage angezeigt werden.
178
Abfragen
Abbildung 3.35: Variante 2 zur Ermittlung des teuersten Artikels der Kategorie Getränke
Abbildung 3.36: Variante zum Ermitteln des teuersten Artikels über eine Unterabfrage
3.8.4 Extremwerte von Gruppierungen Von dieser Abfrage aus gelangen Sie mit wenigen Änderungen zu einer Variante, die die teuersten Artikel einer jeden Kategorie anzeigt. Dazu müssen Sie eine zusätzliche Beziehung zwischen der Haupt- und der Unterabfrage einbauen, die dazu führt, dass die Unterabfrage jeweils den teuersten Artikel der Kategorie des aktuellen Datensatzes des Hauptformulars zurückgibt. Hört sich kompliziert an, ist es aber nicht: Dazu müs-
179
Kapitel 3
sen Sie lediglich die Tabellen in Haupt- und Unterabfrage mit einem Alias-Namen versehen und die Inhalte des Feldes Kategorie-Nr der beiden Abfragen gleichsetzen. Der SQL-Ausdruck dieser Abfrage sieht wie folgt aus: SELECT t1.[Artikel-Nr], t1.Artikelname, t1.Einzelpreis FROM Artikel AS t1 WHERE t1.[Artikel-Nr]=( SELECT TOP 1 t2.[Artikel-Nr] FROM Artikel AS t2 WHERE t1.[Kategorie-Nr] = t2.[Kategorie-Nr] ORDER BY t2.Einzelpreis DESC );
In der Entwurfsansicht sieht die Abfrage wie in Abbildung 3.37 aus. Das Ergebnis ist in Abbildung 3.38 abgebildet.
Abbildung 3.37: Korrelierte Haupt- und Unterabfrage zur Ermittlung des teuersten Artikels je Kategorie
Abbildung 3.38: Ergebnis der korrelierten Unterabfrage
180
Abfragen
3.9 Datensätze mehrfach anzeigen Für manche Aufgaben kann es erforderlich sein, Datensätze mehr als einmal auszugeben – das Paradebeispiel ist wohl die Ausgabe eines Berichtes mit Adressetiketten. Es gibt einige Lösungen, die dieses Problem per VBA-Code angehen. Es funktioniert aber auch mit einer ausgefeilten Abfrage. Voraussetzung ist, dass sich ein Feld mit der auszugebenden Anzahl in der Tabelle befindet. Außerdem benötigen Sie eine Tabelle, die lediglich ein einziges Feld namens Anzahl enthält, das mit den Zahlen von 0 bis 10 (oder der maximalen Anzahl vorgesehener Exemplare) gefüllt ist (siehe Abbildung 3.39).
Abbildung 3.39: Die Kombination dieser beiden Tabellen liefert die im Feld Anzahl der Mit arbeitertabelle angegebenen Exemplare eines jeden Datensatzes
Abbildung 3.40 zeigt, wie Sie die Tabellen zusammenführen müssen. Beim Hinzufügen der beiden Tabellen erstellt Access gegebenenfalls eine Verknüpfung zwischen den gleichnamigen Feldern (Anzahl), diese entfernen Sie umgehend wieder. Dadurch zeigt Access alle Kombinationen der Datensätze aus der ersten und der Datensätze aus der zweiten Tabelle an. Das sind natürlich ein paar zu viel: Daher schränken Sie die Ergebnismenge noch ein wenig ein. Ziehen Sie neben den auszugebenden Daten das Feld Anzahl der Tabelle tblAnzahl in das Entwurfsraster – nicht jedoch das der Mitarbeitertabelle (!) – und fügen Sie diesem Feld das Kriterium aus der Abbildung hinzu. Somit enthält das Abfrageergebnis alle Kombinationen der Datensätze, für die das Feld Anzahl der Tabelle tblAnzahl kleiner als die in der Mitarbeitertabelle angegebene Anzahl ist. Abbildung 3.41 zeigt das nach dem Feld MitarbeiterID sortierte Abfrageergebnis.
181
Kapitel 3
Abbildung 3.40: Abfrage zur Anzeige einer bestimmten Anzahl je Datensatz
Abbildung 3.41: Das Abfrageergebnis enthält jeden Datensatz in der gewünschten Anzahl. Die letzte Spalte zeigt den jeweiligen Wert des Feldes Anzahl der Tabelle tblAnzahl
3.10 Nummerierung von Datensätzen Felder wie der Primärindex taugen in den seltensten Fällen zum Bereitstellen einer lü ckenlosen Nummerierung von Datensätzen. Erstens sorgt jeder gelöschte Datensatz für eine Lücke (sofern es sich nicht um den letzten Datensatz handelt) und zweitens sollte eine Nummerierung auch mal eine Umsortierung oder das Setzen eines Kriteriums mitmachen.
182
Abfragen
Da hierzu eine gewisse Dynamik erforderlich ist, funktioniert dies nur in Verbindung mit einer Abfrage auf Basis der zu nummerierenden Tabelle. Die Abfrage verwendet einen Ausdruck, der die Anzahl der vor dem aktuellen Datensatz liegenden Datensätze berechnet (siehe Abbildung 3.42). Das Ergebnis der Abfrage finden Sie in Abbildung 3.43.
Abbildung 3.42: Nummerieren von Daten ohne Sortierung und Gruppierung
Abbildung 3.43: Nummerierte Datensätze per Abfrage
3.10.1 Alternative: Nummerieren per Unterabfrage Die gleiche Abfrage können Sie mit einem anderen Nummerierungsfeld ausstatten. Diesmal verwenden Sie eine Unterabfrage: Nummer: (SELECT Count(*) FROM Artikel As Temp WHERE Temp.[Artikel-Nr] < Artikel.[Artikel-Nr])+1
183
Kapitel 3
3.10.2 Nummerierung von Abfrageergebnissen mit alternativen Sortierungen Wenn die Daten nach einem anderen Feld als dem Primärschlüsselfeld der Tabelle sortiert werden sollen, geben Sie im WHERE-Teil der Unterabfrage einfach auf beiden Seiten der Bedingung das gewünschte Feld an und legen die Sortierung auch in der Hauptabfrage fest.
3.10.3 Nummerierung von Abfrageergebnissen mit eingeschränkten Ergebnismengen Wenn das zu nummerierende Abfrageergebnis Kriterien enthält, fügen Sie diese auch der Unterabfrage hinzu. Um alle Artikel, deren Artikelname mit »C« beginnt, zu nummerieren, verwenden Sie die Abfrage aus Abbildung 3.44. Der dort aus Platzgründen nicht vollständig abgebildete Nummer-Ausdruck lautet folgendermaßen: Nummer: (SELECT Count(*) FROM Artikel As Temp WHERE (Temp.Artikelname LIKE "C*") AND (Temp.[Artikel-Nr] < Artikel.[Artikel-Nr]))+1
Abbildung 3.44: Nummerierung einer sortierten und eingeschränkten Abfrage
3.11 Reflexive 1:n-Beziehungen In Kapitel 2, Abschnitt 2.5.7, »Reflexive Beziehungen«, haben Sie die Grundlagen zu re flexiven Abfragen erhalten. Die Anzeige und Behandlung der Daten reflexiver Beziehungen mit Abfragen ist relativ eingeschränkt, da es sich bei Access-SQL nicht um eine prozedurale Sprache handelt. Die meisten Aufgaben in Zusammenhang mit reflexiven Beziehungen lassen sich nur mit VBA und passenden rekursiven Funktionen erledigen.
184
Abfragen
Für einige Aufgaben ist jedoch auch eine Abfrage geeignet. Wenn Sie etwa ermitteln möchten, in welcher Ebene der Hierarchie sich ein Mitarbeiter befindet, können Sie die Abfrage aus Abbildung 3.45 verwenden. Wichtig ist dabei, dass Sie jeweils einen LEFT JOIN zwischen den Tabellen einrichten. Das Feld Ebene enthält den folgenden Ausdruck: Ebene: Anzahl([tblMitarbeiterVorgesetzter].[VorgesetzterID]) +Anzahl([tblMitarbeiterVorgesetzter_1].[VorgesetzterID]) +Anzahl([tblMitarbeiterVorgesetzter_2].[VorgesetzterID]) +1
Die Abfrage geht von der Tabelle tblMitarbeiterVorgesetzter aus. Die weiteren Tabellen sind Kopien der ersten Tabelle, die Sie durch wiederholtes Ziehen der Tabelle tblMitarbeiterVorgesetzter zum Abfrageentwurf hinzufügen. Jeder Anzahl(…)-Ausdruck ermittelt, ob das Feld VorgesetzterID in den verknüpften Tabellen einen Wert enthält. Ist das der Fall, liefert die Anzahl-Funktion den Wert 1.
Abbildung 3.45: Ermittlung der Anzahl der Hierarchieebenen eines Mitarbeiters
Abbildung 3.46 hilft beim Verständnis dieser Abfrage. Die letzten drei Spalten sind im Abfrageentwurf nicht zu sehen. Sie enthalten den direkten Vorgesetzten, den Vorgesetz ten dieses Vorgesetzten und dessen Vorgesetzten. Der Nachteil solcher Abfragen ist, dass Sie diese in der Regel schlecht statisch anlegen können, da Sie nie wissen, ob nicht einmal eine Ebene hinzukommt oder wegfällt. Daher sollten Sie Vorgänge im Zusammenhang mit reflexiven Beziehungen in der Regel mit VBA und rekursiven Funktionen durchführen.
185
Kapitel 3
Abbildung 3.46: Hierarchieebene der Mitarbeiter und Anzeige der jeweiligen Vorgesetzten
3.12 Reflexive m:n-Beziehungen Mit reflexiven m:n-Beziehungen verhält es sich genauso wie mit 1:n-Beziehungen. Am flexibelsten lassen sich die enthaltenen Daten auswerten, wenn Sie dies mit rekursiven Funktionen unter VBA erledigen.
186
4 Formulare Formulare sind das wichtigste Element der Benutzer oberfläche von Access-Anwendungen. Sie sorgen dafür, dass der Benutzer bei der Dateneingabe nicht mit ellen langen Tabellen oder Abfragen mit Tausenden von Daten sätzen hantieren muss, sondern die Daten übersichtlich dargestellt bekommt – je nach Anforderung als Liste oder Detailansicht. Mit Kombinationsfeldern, Listenfeldern und Unterformu laren lassen sich auch die Daten verknüpfter Tabellen problemlos darstellen. Der große Vorteil von Access-Formularen gegenüber denen von Visual Basic oder den .NET-Programmiersprachen liegt in den speziellen Funktionen für die Verwendung mit Tabellen und Abfragen als Datensatzquelle. Diese Techni ken sind mit dafür verantwortlich, dass Access heute das am weitesten verbreitete Datenbank-Managementsystem für Windows-Rechner ist. Abgerundet wird dies durch die Möglichkeit, Formulare und die enthaltenen Steuerelemente zu programmieren und dabei die zahlreichen Ereignisse zu nutzen. In diesem Kapitel erfahren Sie zunächst, welche Neue rungen Access 2007 in Zusammenhang mit Formularen mitbringt und erhalten eine kleine Einführung in Formulare. Dann geht es ans Eingemachte: Sie lernen, wie Sie Daten auf Basis einzelner oder verknüpfter Tabellen in Abfragen darstellen können und wie Sie VBA zur Realisierung wichtiger Funktionen wie etwa zur Suche von Datensätzen verwenden.
Kapitel 4
Das folgende Kapitel geht dann genauer auf die verschiedenen Steuerelemente ein, die Sie in Formularen einsetzen können. Dort geht es nicht nur um die Standardsteuerele mente von Access, sondern auch um Steuerelemente wie das ListView-, TreeView- oder ImageList-Steuerelement. Da alle in diesem Kapitel beschriebenen Funktionen mehr oder weniger den Einsatz von VBA und Ereigniseigenschaften von Formularen und Steuerelementen benötigen, finden Sie zunächst eine Beschreibung der wichtigsten Ereignisse und ihre Abfolge für das Formular selbst und einige Steuerelemente. Die Beispiele zu diesem Kapitel finden Sie auf der Buch-CD in der Datenbank \Kap_04\Formulare.accdb sowie in weiteren Beispieldatenbanken – dazu an anderer Stelle mehr.
4.1 Formulare in Access 2007 Microsoft hat sich eine Menge Features einfallen lassen, die das Erstellen von Formula ren für Einsteiger vereinfacht. Es gibt aber auch Neuerungen, die dem alteingesessenen Access-Entwickler das Leben erleichtern. Beim Gang durch das Entwerfen von Formularen lernen Sie alles Neue kennen.
4.1.1 Anlegen eines Formulars Das schnelle Anlegen von Formularen erfolgt in Access 2007 mit dem passenden Rib bon-Befehl auf Basis des aktuell markierten Tabellen- oder Abfrage-Objekts. Die RibbonEinträge befinden sich sämtlich im Bereich Erstellen|Formulare. Die einfachste Variante mit der Bezeichnung Formular erstellt automatisch ein einfaches Formular in der Ansicht Einzelnes Formular. Dabei berücksichtigt diese Funktion eventuelle Unterdatenblätter und legt dafür passende Unterformulare auf Basis der verknüpften Tabellen an. Mit dem Eintrag Erstellen|Formulare|Geteiltes Formular legen Sie ein geteiltes Formular an, das gleichzeitig eine Liste der Datensätze der zugrunde liegenden Tabelle oder Ab frage und die Detailansicht des aktuell ausgewählten Datensatzes anzeigt. Mehr zu geteilten Formularen erfahren Sie weiter unten in 4.1.3, »Geteilte Formulare«. Weitere Ribbon-Schaltflächen verheißen das schnelle Anlegen der gängigen Ansichten. Wer's manuell mag, der klickt direkt auf den Ribbon-Eintrag Erstellen|Formulare|Formu larentwurf und legt selbst Hand an.
188
Formulare
Gebundene und ungebundene Formulare Üblicherweise verwendet man in Access Formulare, um Daten aus den zugrunde liegenden Tabellen anzuzeigen, und das bedeutet, dass man das Formular an eine Tabelle oder Abfrage bindet. Dazu stellt man die Eigenschaft Datensatzquelle entweder auf den Namen einer Tabelle, den Namen einer Abfrage oder einen passenden SQL-Ausdruck ein. Natürlich kann es auch ungebundene Formulare geben – etwa, um den Benutzer mit einem Startdialog einige nützliche Hinweise mit auf den Weg zu geben, dem Benutzer statt eines Ribbons ein Übersichtsformular zum Steuern der Anwendung zu bieten oder Informationen abzufragen – beispielsweise Such- oder Filterkriterien für die Anzeige von Daten in Formularen oder Berichten. Solche Formulare unterscheiden sich lediglich durch das Leerlassen der Eigenschaft Datensatzquelle von gebundenen Formularen.
Gebundene Felder über die Feldliste anlegen Wenn Sie sich entschieden haben, ein Formular von Grund auf selbst anzulegen (also ohne eine der Funktionen zum automatischen Einfügen etwa von Feldern oder der Datensatzquelle), gehen Sie folgendermaßen vor: Nach dem Auswählen der Datensatzquelle können Sie die darin enthaltenen Felder auf dem Formular platzieren. Wie Sie diese anordnen, hängt im Wesentlichen davon ab, wie Sie die Daten darstellen möchten und welchen Zweck das Formular hat. Grundsätzlich geschieht das aber immer gleich: Aktivieren Sie die Entwurfs- oder die Layoutansicht des Formulars. Klicken Sie auf den Ribbon-Eintrag Entwurf|Tools|Vorhandene Felder hinzufügen, um die Feldliste anzuzeigen. Ziehen Sie die benötigten Felder aus der Feldliste (auch per Mehrfachauswahl) auf den gewünschten Bereich des Formulars. Welche Bereiche es gibt und wozu diese dienen, erfahren Sie weiter unten. Die Feldliste zur Auswahl der anzuzeigenden Felder hat sich im Vergleich zu älteren Access-Versionen stark verändert. Zwar zeigt sie noch die Felder der ausgewählten Da tensatzquelle an, aber anders als gewohnt (siehe Abbildung 4.1): Die eigentliche Feld liste im oberen Bereich enthält nicht nur die Felder der Datensatzquelle, sondern auch noch die Angabe der passenden Tabelle. Im mittleren Bereich zeigt Access direkt mit der Datensatzquelle verknüpfte Tabellen und ihre Felder an (falls Sie im BeziehungenFenster entsprechende Beziehungen angelegt oder den Nachschlageassistenten damit betraut haben) und im unteren Bereich die übrigen Tabellen. Falls die Feldliste nur die Felder der aktuellen Datensatzquelle anzeigt, können Sie die Ansicht mit einem Klick auf die Schaltfläche Alle Tabellen anzeigen erweitern.
189
Kapitel 4
Abbildung 4.1: Auswahl der im Formular anzuzeigenden Felder
Bei Feldern aus verknüpften Tabellen fügt Access diese Tabellen einfach der Datensatz quelle hinzu, indem es den passenden SQL-Ausdruck in der Eigenschaft Datensatzquelle anpasst, bei nicht verknüpften Tabellen ist noch ein wenig Handarbeit notwendig: Sie müssen dann in einem Dialog angeben, über welches Feld ein Bezug hergestellt werden soll (siehe Abbildung 4.2). Prinzipiell stellen Sie mit diesem Dialog nachträglich eine Verknüpfung her (wenn auch nur auf Datensatzquellen-Ebene), die in der zugrunde liegenden Abfrage gespeichert wird. Das ist ein interessantes Werkzeug, wenn man mal eben ein Feld nachreichen möchte, das man beim Anlegen der Datensatzquelle vergessen hat. Die Frage ist, ob dies bei Tabellen, die noch nicht verknüpft sind, sinnvoll ist – und gerade dazu ist der Dialog wohl vorgesehen. Das Hinzufügen von Feldern aus bereits verknüpften Tabellen hingegen kann eine echte Erleichterung sein: Wie oft passiert es, dass man Felder etwa aus Lookup-Tabellen wie einer Anrede-Tabelle beim Zusammenstellen der Datensatzquelle außer Acht lässt und diese sonst nachträglich anpassen müsste. Das geht nun schon wesentlich schneller, indem man einfach das passende Feld aus dem mittleren Bereich der Feldliste in den Entwurf zieht.
190
Formulare
Abbildung 4.2: Nachträgliches Hinzufügen von Feldern aus nicht verknüpften Tabellen
Felder über Steuerelemente anlegen Der zweite Weg, ein gebundenes Feld anzulegen, ist ein wenig aufwändiger. Er ist sinnvoll, wenn Sie kein Textfeld für die Anzeige des Feldinhaltes benötigen, sondern auf ein anderes Steuerelement zurückgreifen möchten. Bei Kombinations- und Listenfeldern kann man dem vorbeugen, indem man die Nachschlage-Eigenschaften des zugrunde liegenden Feldes entsprechend vorbereitet (mehr dazu in Kapitel 2, Abschnitt 2.1.5, »Beziehungen herstellen« und folgende). Auch für Boolean-Felder legt Access automatisch ein Kontrollkästchen an. Vielleicht möchten Sie aber auch einmal eine Optionsgruppe anlegen, um die Werte eines Zahlenfeldes abzubilden, oder Sie benötigen ein Kombinations- oder Listenfeld, ohne dass Sie die Nachschlage-Eigenschaften des Tabellenfelds entsprechend eingestellt haben. Dann klicken Sie einfach auf die passende Schaltfläche im Ribbon-Bereich Entwurf|Steuerelemente und platzieren das Steuerelement dann im Formular. Um das Steuerelement an das gewünschte Feld zu binden, stellen Sie seine Eigenschaft Steuerelementinhalt auf den passenden Feldnamen der Datensatzquelle ein. Weitere Informationen hierzu finden Sie in Kapitel 5, »Steuerelemente«.
Formularbereiche Der meistverwendete Formularbereich ist vermutlich der Detailbereich. Er enthält in der Regel die an die Datensatzquelle gebundenen Steuerelemente – eben jene Felder, die Sie aus der Feldliste auf das Formular ziehen können. Die hier enthaltenen Felder zeigt Access in allen Ansichten an – in manchen sogar nur diese Felder und keine Steuerelemente der anderen Bereiche.
191
Kapitel 4
Formulare können außerdem einen Formularkopf und einen Formularfuß aufweisen. Der Formularkopf ist beispielsweise gut für die Anzeige von Überschriften, berechneten Werten wie dem Systemdatum oder Steuerelementen wie Schaltflächen geeignet. Der Formularfuß enthält oft Ergebnisse von Berechnungen wie etwa der Anzahl der im Detailbereich angezeigten Datensätze oder einer Rechnungssumme. Dort können Sie auch – wie in vielen Windows-Anwendungen – die Schaltflächen zum Steuern des Formulars unterbringen. Viele Benutzer sind es gewohnt, dort Schaltflächen mit Be schriftungen wie OK oder Abbrechen zu finden. Die Bereiche Seitenkopf und Seitenfuß sind nur interessant, wenn Sie Formulare ausdru cken wollen. Für das Ausdrucken von Daten sind unter Access aber Berichte verantwortlich; sie bieten auch wesentlich bessere Möglichkeiten zur optimalen Darstellung der Daten. Daher behandelt dieses Buch diese Bereiche an dieser Stelle nicht weiter. Das Ein- und Ausblenden der verschiedenen Formularbereiche erledigen Sie entweder mit den Kontextmenüeinträgen Seitenkopf/-fuß und Formularkopf/-fuß oder mit den Ribbon-Einträgen im Bereich Anordnen|Einblenden/Ausblenden.
4.1.2 Formularansichten Bei einem Formular, das der Eingabe von Daten dient, werden Sie vermutlich nur jeweils einen einzigen Datensatz anzeigen. Sie stellen dann die Eigenschaft Standardansicht auf Einzelnes Formular ein. Das ist nicht nur die Basis zur Eingabe, sondern auch zur Anzeige von Daten im Detail sowie zum Bearbeiten von Datensätzen. Generell können Sie die Ansicht eines Formulars im Ribbon unter Start|Ansichten oder über das Kontextmenü der Titelleiste eines Formulars oder des Formularhintergrunds einstellen.
Entwurfsansicht In der Entwurfsansicht eines Formulars passen Sie das Formular an: Sie fügen Steuerele mente hinzu, stellen Eigenschaften wie Höhe, Breite oder Farben ein und ordnen die Ele mente wie gewünscht an. Es würde den Rahmen dieses Kapitels sprengen, wenn es alle Einzelheiten in Zusammenhang mit der Entwurfsansicht beschreiben sollte. Deshalb beschränkt es sich auf die Neuheiten von Access 2007, die sich vor allem auf die verbesserte Ausrichtung von Steuerelementen beziehen (siehe weiter unten in Abschnitt 4.1.4, »Hilfreiche Funktionen für den Formularentwurf«. Wenn Ihnen die Grundlagen beim Erstellen von Formularen mit der Entwurfsansicht nicht geläufig sind, liefert die Onlinehilfe von Access allerdings gutes Material: Geben Sie dort als Suchbegriff einfach »Formulare« ein und Sie erhalten eine umfangreiche Liste mit allen möglichen Themen rund um die Erstellung von Formularen.
192
Formulare
Layoutansicht Die Layoutansicht ist ein neues Feature von Access 2007 und stellt eine Art Zwischenstufe zwischen der Entwurfsansicht und der Formularansicht dar. In dieser Ansicht zeigt ein Formular die zugrunde liegenden Daten an und lässt sich gleichzeitig wie in der Entwurfsansicht bearbeiten. Der wichtigste Vorteil der Layoutansicht ist, dass Sie Größe, Position und Formatierung der Steuerelemente auf die enthaltenen Daten abstimmen können. Bisher war dazu ein stetiges Wechseln zwischen Entwurfs- und Formularansicht notwendig. Der Wert der Eigenschaft Layoutansicht zulassen (VBA: LayoutView) entscheidet, ob Sie ein Formular grundsätzlich in der Layoutansicht anzeigen können. Wenn diese Voraussetzung geschaffen ist, können Sie sie problemlos einsetzen, um wie in Abbildung 4.3 die Breite von Steuerelementen in der Endlosansicht eines Formulars den vorhandenen Inhalten anzupassen.
Abbildung 4.3: Anpassen der Breite von Steuerelementen in der Layoutansicht
Sie können nicht alle Entwurfsaufgaben in der Layoutansicht erledigen – so lassen sich etwa nur Steuerelemente über die Feldliste hinzufügen, nicht aber über die Steuerele ment-Schaltflächen im Ribbon-Bereich Entwurf|Steuerelemente. Dieser Bereich ist in der Layoutansicht schlicht nicht vorhanden. Außerdem können Sie auch keine Steuerele mente duplizieren oder Steuerelemente mit den Cursor-Tasten in Kombination mit der Umschalt- und der Strg-Taste fein positionieren oder ihre Größe anpassen.
Datenblattansicht Die Datenblattansicht zeigt die im Detailbereich befindlichen gebundenen Felder genau wie in der von Tabellen und Abfragen bekannten Datenblattansicht an. Steuer elemente im Kopf- und Fußbereich stellt die Datenblattansicht nicht dar. Aus dem Detailbereich fallen auch einige Steuerelementtypen weg. Angezeigt werden nur Text felder, Kombinationsfelder, Listenfelder (als Kombinationsfeld), Optionsfelder und Kontrollkästchen, die nicht in Optionsgruppen organisiert sind, Optionsgruppen (aller-
193
Kapitel 4
dings nur als Textfeld, nicht mit Optionsfeldern), Umschaltflächen (als Wert) und einige andere Steuerelemente. Bezeichnungsfelder, die an eines der genannten Steuerelemente gebunden sind, zeigt die Datenblattansicht als Spaltenkopf an. In der Praxis werden Sie die Datenblattansicht wohl nie für eigenständige Formulare einsetzen, sondern nur als Unterformular – auch, wenn Sie eigentlich nur Daten in der Datenblattansicht anzeigen möchten. Der Grund ist ganz einfach: Die Datenblattansicht ist sehr egoistisch und lässt keinen Platz für andere Steuerelemente. Wenn Sie eine solche Ansicht etwa mit einem kleinen Suchfeld oder mit Schaltflächen zum Schließen oder Abbrechen versehen möchten, müssen Sie eine Art »Rahmen-Formular« verwenden und das Formular in der Datenblattansicht dort als Unterformular einbetten. Wie das aussieht, sehen Sie in Abbildung 4.4. Weitere Informationen zu dieser Ansicht finden Sie in Abschnitt 4.5.3 unter »Einfache Daten in der Übersicht als Datenblatt«.
Abbildung 4.4: Ein Formular in der Datenblattansicht mit Formular-Rahmen zum Hinzufügen von Schaltflächen
Die Datenblattansicht kommt auch oft zum Einsatz, wenn es um die Darstellung von 1:n- oder m:n-Beziehungen geht. Dabei zeigt dann ein Hauptformular die Daten der Detailtabelle und das Unterformular die Daten der Mastertabelle in der Datenblattan sicht an. Ein Beispiel sehen Sie in Abbildung 4.5, weitere Informationen zu dieser Dar stellung finden Sie in Abschnitt 4.5.8, »1:n-Beziehung per Unterformular und Datenblatt ansicht«.
Einfaches Formular Diese Ansicht ist für die Anzeige der Details eines Datensatzes geeignet. Sie zeigt jeweils einen einzigen Datensatz an. Im Gegensatz zur Datenblattansicht können Sie die Steuerelemente ganz nach Ihren Wünschen auf die Bereiche des Formulars aufteilen und – noch wichtiger – Sie können alle Steuerelemente einsetzen, die Access hergibt. Besonders interessant sind dabei Listenfelder und Unterformulare, mit denen Sie die strukturierte Anzeige von Daten aus mehreren verknüpften Tabellen anzeigen können.
194
Formulare
Abbildung 4.5: Für Unterformulare bietet sich oft die Datenblattansicht an
Endlosformular Endlosformulare sind prinzipiell einfache Formulare, von denen Sie aber mehrere gleichzeitig (und zwar untereinander) anzeigen können. Damit können Sie einige Nachteile der Datenblattansicht ausmerzen: So lassen sich auch hier beliebige Steuerelemente anzeigen. Eine Ausnahme sind Unterformulare: Diese können Sie nicht in der Endlosansicht eines Formulars einsetzen. Um mit einem Endlosformular ein Formular in der Datenblattansicht nachzubauen, brauchen Sie nur die Steuerelemente genauso wie in der Datenblattansicht anzuordnen, passende Beschriftungsfelder im Formularkopf zu platzieren und die Höhe des Detail bereichs entsprechend zu verkleinern. Das kann etwa so wie in Abbildung 4.6 aussehen – weitere Informationen zu dieser Ansicht finden Sie in Abschnitt 4.5.2, »Einfache Daten in der Übersicht mit Endlosformularen«. Eine solche Endlosansicht können Sie natürlich auch als Unterformular einsetzen, um eine 1:n- oder m:n-Beziehung darzustellen.
Abbildung 4.6: Ein Endlosformular als Ersatz für die Datenblattansicht
195
Kapitel 4
PivotTable- und PivotChart-Ansicht Diese beiden Ansichten bietet Access seit der Version XP an. Sie bieten eine recht gute Entwurfsansicht, mit der sich schnell ansprechende Ergebnisse erzielen lassen. Aller dings sind diese beiden Ansichten in Zusammenhang mit größeren Datenmengen recht langsam. Da das Interesse seitens der Access-Entwickler an diesen Ansichten relativ gering ist, geht dieses Buch nicht näher darauf ein.
4.1.3 Geteilte Formulare Access 2007 liefert einige Eigenschaften, mit denen Sie Formulare zur Anzeige von Daten in Listen- und Detailansicht aufteilen können. Sicher haben Sie schon einmal ein Unterformular oder ein Listenfeld in einem Formular verwendet, um eine Über sicht über Datensätze – etwa von Mitarbeiterdaten – anzuzeigen und darüber die Aus wahl eines Datensatzes zur Anzeige der Detaildaten in einem anderen Bereich des Formulars einzublenden. Hierzu ist VBA-Programmierung notwendig, und Microsoft hat mit den so genannten »Split Forms« eine Möglichkeit geschaffen, dies durch einfaches Anpassen einiger Eigenschaften zu erreichen. (Natürlich können Sie dies auch per Assistenten durchführen, aber diese kleinen Helfershelfer will dieses Buch ja beiseite lassen ...) Grundvoraussetzung für das Teilen eines Formulars ist das Einstellen der Eigenschaft Standardansicht auf den neuen Wert Geteiltes Formular (siehe Abbildung 4.7).
Abbildung 4.7: Mit der richtigen Einstellung für die Eigenschaft Standardansicht erreichen Sie die Anzeige eines geteilten Formulars
Haben Sie diese Eigenschaft eingestellt, brauchen Sie eigentlich nur noch eine passende Datensatzquelle auszuwählen und die Felder in der gewünschten Reihenfolge anzuordnen. Das kann dann, in einem sehr einfachen Fall, wie in Abbildung 4.8 aussehen. Ein Wechseln in die Formularansicht sorgt schließlich für die Anzeige des geteilten For mulars (siehe Abbildung 4.9). Mit einem Klick auf einen der Datensätze in der Hälfte mit
196
Formulare
der Datenblattansicht zeigen Sie den jeweils angeklickten Datensatz im Detailbereich des Formulars an.
Abbildung 4.8: Dieser Entwurf soll den Detailbereich eines geteilten Formulars ausmachen
Abbildung 4.9: Die geteilte Ansicht eines Formulars mit Datenblatt und Detailansicht
Natürlich können Sie für geteilte Formulare einige Einstellungen vornehmen. Welche dies sind, zeigt Tabelle 4.1. Weitere Möglichkeiten bietet das Kontextmenü des Datenblatt-Bereichs des geteilten Formulars: Hier haben Sie alle Möglichkeiten, die Sie auch sonst in der Datenblattansicht haben. Die Formatierung des Datenblattbereichs können Sie mit den Steuerelementen im Ribbon unter Start|Schriftart anpassen – vorher müssen Sie allerdings den Fokus auf das Datenblatt setzen. Die passenden Eigenschaften unter VBA heißen DatasheetAlter nateBackColor, DatasheetBackColor, DatasheetBorderLineStyle, DatasheetCellsEffect, Datasheet ColumnHeaderUnderlineStyle, DatasheetFontHeight, DatasheetFontItalic, DatasheetFontName, DatasheetFontUnderline, DatasheetFontWeight, DatasheetForeColor, DatasheetGridlinesBeha viour und DatasheetGridlinesColor – für weitere Informationen siehe Onlinehilfe.
197
Kapitel 4 Eigenschaft (deutsch)
Eigenschaft (VBA)
Beschreibung
Größe des geteilten Formulars
SplitFormSize
Legt die Höhe des Detailbereichs des Formulars fest. Einheit in VBA: Twips (567 Twips entsprechen 1 cm).
Orientierung des geteilten Formulars
SplitFormOrientation
Gibt die Position des Datenblatts an. Mögliche Werte: 0 (acDatasheetOnTop, oben, Standard) 1 (acDatasheetOnBottom, unten) 2 (acDatasheetOnLeft, links) 3 (acDatasheetOnRight, rechts)
Teilerleiste des geteilten Formulars
SplitFormSplitterBar
Gibt an, ob der Benutzer die Größe der Bereiche mit einer Teilerleiste einstellen kann. Boolean.
Datenblatt des geteilten Formulars
SplitFormDatasheet
Gibt an, ob der Benutzer Daten im Datenblatt bearbeiten darf. Mögliche Werte: 0 (acDatasheetAllowEdits, editieren erlaubt) 1 (acDatasheetReadOnly, schreibgeschützt)
Drucken des geteilten Formulars
SplitFormPrinting
Gibt an, welcher Bereich des Formulars gedruckt wird. Mögliche Werte: 0 (acFormOnly, Detailansicht) 1 (acGridOnly, Datenblatt)
Position der Teiler leiste speichern
SplitFormSplitterBarSave
Gibt an, ob die Position der Teilerleiste beim Schließen gespeichert wird. Boolean.
Tabelle 4.1: Eigenschaften zum Anpassen der geteilten Formularansicht
4.1.4 Hilfreiche Funktionen für den Formularentwurf Access 2007 bietet einige neue Funktionen zum Entwerfen von Formularen.
Automatische Layouts Wenn Sie Steuerelemente und ihre Beschriftungsfelder in Tabellenform anordnen möchten, können Sie einfach alle betroffenen Elemente markieren und dann einen der RibbonEinträge Anordnen|Layout bestimmen|Tabelle oder Anordnen|Layout bestimmen|Gestapelt auswählen. Diese Funktionen stehen in der Entwurfs- und der Layoutansicht zur Ver fügung. Access ordnet die Steuerelemente dann wie in Abbildung 4.10 (Ausrichtung: Tabelle) oder wie in Abbildung 4.11 (Ausrichtung: gestapelt) an. Es handelt sich hierbei aber nicht nur um das automatische Anordnen, denn Access fügt gleichzeitig eine Art Rahmen hinzu, der die Positionen der enthaltenen Steuerelemente wahrt. Außerdem bietet dieser Rahmen die Möglichkeit, dass Sie die Reihenfolge der Steuerelemente neu sortieren, ohne sich um die Wahrung der Abstände und der Po
198
Formulare
sition kümmern müssen. Markieren Sie dazu einfach das zu verschiebende Feld und ziehen Sie es an die gewünschte Stelle. Eine orange Linie zeigt dabei an, wo Access das Steuerelement platziert. Bei der tabellarischen Ausrichtung übernimmt Access automatisch die Aufgabe, die Feldbezeichnungen als Spaltenköpfe in den Formularkopf zu kopieren. Wenn Sie einzelne Elemente einer Gruppe von gemeinsam ausgerichteten Steuerelemen ten individuell anpassen möchten, müssen Sie mit Einschränkungen leben: In der tabellarischen Ausrichtung können Sie nur die Breite und in der gestapelten Ausrichtung nur die Höhe der einzelnen Steuerelemente anpassen. Das ist aber nicht schlimm: Wenn Sie die Steuerelemente mit einer der Ausrichten-Funk tionen positioniert haben, können Sie den Layoutrahmen wieder entfernen, indem Sie diesen markieren und den Ribbon-Eintrag Anordnen|Layout bestimmen|Entfernen betätigen. Danach können Sie manuell Hand an die Steuerelemente anlegen, um etwa die Breite des PLZ-Felds in Abbildung 4.11 zu verringern.
Abbildung 4.10: Felder mit tabellarischer Ausrichtung
Abbildung 4.11: Felder in der »gestapelten« Ausrichtung
199
Kapitel 4
Den Layoutrahmen markieren Sie im Übrigen mit einem Klick auf das kleine Kreuz, das nach einem Klick auf eines der enthaltenen Steuerelemente erscheint (siehe Abbil dung 4.12).
Abbildung 4.12: Mit dem Kreuz links oben markieren Sie das komplette Layout
Sie können dem Layout auch nachträglich einzelne Steuerelemente hinzufügen oder Steuerelemente daraus entfernen. Zum Hinzufügen ziehen Sie einfach das passende Steuerelement an die gewünschte Stelle im Layout, zum Entfernen markieren Sie das betroffene Steuerelement und klicken anschließend auf die Ribbon-Schaltfläche An ordnen|Layout bestimmen|Entfernen. Nach dem Entfernen verbleibt das entfernte Steuerele ment an Ort und Stelle und die daneben oder darunter befindlichen Steuerelemente rücken an dessen Platz. Sie müssen auf einem Layout entfernte Steuerelemente also noch manuell verschieben, damit diese nicht mit den Steuerelementen des Layouts überlappen. Die Layoutansicht sollte dem Entwickler einer Datenbank vorbehalten sein. Sie sollten daher entweder die Eigenschaft Layoutansicht zulassen für die gewünschten Formulare auf Nein einstellen oder die Ansicht für die komplette Datenbank deaktivieren, indem Sie in den Access-Optionen die Eigenschaft Aktuelle Datenbank|Anwendungsoptionen|Layout ansicht für diese Datenbank aktivieren auf Ja einstellen. In VBA können Sie dieses Feature mit der Eigenschaft AllowLayoutView des Formulars aktivieren und deaktivieren.
Berechnungen in der Datenblattansicht In Tabellen, Abfragen und Formularen steht nun für die Datenblattansicht eine neue Funktion zum Durchführen von Berechnungen und zur Anzeige ihrer Ergebnisse bereit.
200
Formulare
Da der Benutzer Ihrer Datenbank keinen Kontakt mit der Datenblattansicht der Tabellen und Abfragen haben soll, ist dies ein Fall für das Formulare-Kapitel. Wie weiter oben erwähnt, erscheinen Daten in der Datenblattansicht meist in Form von Unterformularen in der Datenblattansicht – ein Formular in der Datenblattansicht allein macht wenig Sinn, da man beispielsweise keine zusätzlichen Steuerelemente wie Schaltflächen dort unterbringen kann. Ob nun in einem Formular oder in einem Unterformular: Sie können einem Datenblatt eine Zeile mit Berechnungen hinzufügen, indem Sie es in der Entwurfs- oder Layout ansicht markieren und im Ribbon die Schaltfläche Start|Datensätze|Summen anklicken. Schon erscheint die gewünschte Zeile in der Tabelle und Sie müssen nur noch das Feld und die Berechnungsmethode festlegen (siehe Abbildung 4.13). Praktischerweise bleibt die Berechnungszeile am unteren Rand des Datenblatts »kleben« und wird nicht nach unten weggeschoben, wenn das Datenblatt mehr Datensätze enthält, als es gleichzeitig anzeigen kann.
Abbildung 4.13: Auswahl einer Berechnungsart für das Feld »Betrag«
Access stellt je nach dem Datentyp des Feldes unterschiedliche Funktionen zur Verfügung. Bei Zahlen stehen naturgemäß mehr Varianten bereit als für Textfelder – hier gibt es lediglich die Möglichkeit, die Anzahl der Einträge auszugeben. Wie für viele neue Features von Access 2007 gilt: Probieren Sie es einfach aus, es geht ganz leicht.
4.1.5 Sonstige Neuerungen Es gibt noch einige weitere kleine Neuheiten, die eine Erwähnung wert sind.
201
Kapitel 4
Alternierender Hintergrund in Datenblättern Access 2007 bietet die Möglichkeit, die Datenblattansicht von Tabellen, Abfragen und Formularen mit einer alternierenden Hintergrundfarbe zu versehen. Eine Access-weite Option finden Sie in den Access-Optionen unter Datenblatt|Standardfarben|Alternative Hintergrundfarbe. Den Effekt können Sie sich in zahlreichen Abbildungen dieses Kapitels ansehen. Wenn Sie eine individuelle Färbung für einzelne Objekte vornehmen möchten, verwenden Sie in der Datenblattansicht den Ribbon-Eintrag Start|Schriftart|Alternative Füllung/Hintergrundfarbe. Alternativ können Sie dies auch mit VBA erreichen; die passenden Eigenschaft heißt DatasheetAlternateBackColor.
Steuerelemente mit Abstand Mit den folgenden vier Eigenschaften stellen Sie den Abstand von Steuerelementen zur nächsten Gitternetzlinie in einem der Layouts Tabelle oder Geschachtelt fest. Die Eigen schaften sind für fast alle Steuerelemente verfügbar: Textabstand oben (TopPadding) Textabstand unten (BottomPadding) Textabstand links (LeftPadding) Textabstand rechts (RightPadding)
Steuerelemente verankern Mit dem Ribbon-Eintrag Anordnen|Schriftgrad|Anker können Sie Steuerelemente mit dem Seitenrand des Formulars verankern (fragen Sie nicht, warum diese Funktion aktuell in der Gruppe Schriftgrad zu finden ist). Alternativ stellen Sie die Eigenschaften Horizontaler Anker (HorizonalAnchor) und Vertikaler Anker (VerticalAnchor) des betroffenen Steuerelements ein. Die Standardeinstellungen sind Links für den horizontalen Anker und Oben für den vertikalen Anker. Das bedeutet, dass sich das Steuerelement beim Vergrößern oder Verkleinern des Formulars nicht ändert. Wie sich die unterschiedlichen Einstellungen auswirken, probieren Sie am besten selbst aus – und zwar in der Layoutansicht, die dafür perfekt geeignet ist. Einen guten Ein druck vermittelt das Beispiel in Abbildung 4.14 und Abbildung 4.15.
Beschriftung der Navigationsleiste Links von der Navigationsleiste von Formularen können Sie von nun an einen Text anzeigen. Diesen legen Sie mit der Eigenschaft Navigationsbeschriftung fest (in VBA: Na vigationCaption). Abbildung 4.16 zeigt ein Beispiel für eine auf diese Weise beschriftete
202
Formulare
Navigationsleiste. Da die Länge begrenzt und die Anzeige unauffällig ist, scheint der Nutzen dieser Funktion eingeschränkt zu sein.
Abbildung 4.14: Verankerte Elemente eines Formulars im normalen ...
Abbildung 4.15: ... und im vergrößerten Zustand
Abbildung 4.16: Die Beschriftung der Navigationsleiste darf nicht sehr lang sein
Wenn Sie den Benutzer mit der Beschriftung unbedingt auf die Navigationselemente aufmerksam machen möchten, stellen Sie für die Eigenschaft Zeitgeberintervall des Formulars den Wert 500 ein und hinterlegen für die Ereigniseigenschaft Bei Zeitgeber die folgende Routine:
203
Kapitel 4 Private Sub Form_Timer() Static bol As Boolean bol = Not bol If bol Then Me.NavigationCaption = "Hier navigieren!->" Else Me.NavigationCaption = "->Hier navigieren!" End If End Sub Listing 4.1: Wechselnde Navigationsleistenbeschriftungen per Zeitgeber
Dies lässt die Beschriftung »Hier navigieren« hin- und herspringen. Beachten Sie, dass die verwendeten Zeichen immer die gleiche Breite einnehmen, sonst springt auch die eigentliche Navigationsleiste hin- und her. (Kleiner Tipp: Nehmen Sie nicht alles ernst, was in diesem Buch geschrieben steht.)
Formular-Ribbon Sie können für jedes Formular eine der in der Datenbank gespeicherten Ribbon-Defini tionen festlegen. Damit können Sie beispielsweise dafür sorgen, dass bei Aktivierung eines Formulars nur formularspezifische Ribbon-Elemente angezeigt werden oder ein fach das Ribbon um ein spezielles Tab-Element für das Formular erweitern. Sie legen das Ribbon mit der Formular-Eigenschaft Name der Multifunktionsleiste (unter VBA: RibbonName) fest. Dies funktioniert nicht bei Popup- und geteilten Formularen. Weitere Informationen zum Thema Ribbons finden Sie in Kapitel 12, »Ribbons«.
Autoformat Sie können mit Access 2007 eigene Autoformate definieren. Das heißt, dass Sie das For mat der einzelnen Steuerelementtypen in einem Formular und das Format des Formulars selbst einmal festlegen und dann die Einstellungen in Form eines Autoformats speichern. Den passenden Ribbon-Eintrag finden Sie im Formular- oder Berichtsentwurf oder der passenden Layoutansicht unter Anordnen|Autoformat. Klappen Sie die Gallerie auf und wählen Sie den untersten Eintrag Autoformat-Assistent aus. Hier klicken Sie auf Anpassen, wählen im folgenden Dialog die erste Option zum Erstellen eines neuen Autoformats aus und klicken auf OK. Dann nur noch den Namen für das Autoformat eingeben und fertig ist das Autoformat. Dieses können Sie dann für alle weiteren Formulare anwenden.
4.1.6 Formularvorlage Nicht neu, aber möglicherweise nicht jedem bekannt sind die folgenden beiden Features, die das Erstellen von Formularen vereinfachen.
204
Formulare
Mit der Zeit werden Sie sich einen gewissen Stil angewöhnen, was das Aussehen von For mularen angeht. Vielleicht verwenden Sie eine bestimmte Hintergrundfarbe für Kopf-, Fuß- und Detailbereich oder stellen weitere Eigenschaften immer auf die gleichen Werte ein. Wenn Sie solche Standardeinstellungen öfter benötigen, können Sie ein Formular mit all den gewünschten Eigenschaften anlegen und es unter dem Formularnamen Normal speichern. Von nun an besitzen alle neu erstellten Formulare standardmäßig diese Eigenschaften. Erst wenn Sie das Formular Normal entfernen oder umbenennen, verwendet Access wieder die ursprünglichen Standardeinstellungen beim Anlegen von Formularen. Das Gleiche gilt übrigens auch für Berichte, auch hier verwenden Sie standardmäßig den Berichtsnamen Normal für die Vorlage. Wenn Sie einen anderen Formular- beziehungsweise Berichtsnamen als Normal einsetzen möchten, können Sie dies in den AccessOptionen unter Objekt-Designer|Formular/Berichte|Formularvorlage/Berichtsvorlage einstel len.
Beim Öffnen filtern und sortieren Mit den beiden Eigenschaften Beim Laden filtern (VBA: FilterOnLoad) und Beim Laden sortieren (VBA: OrderByOnLoad) legen Sie fest, ob eine in den Eigenschaften Filter (Filter) oder Sortiert nach (OrderBy) angegebene Filterung beziehungsweise Sortierung direkt beim Laden des Formulars ausgeführt werden soll.
An Bildschirmgröße anpassen Mit der gleichnamigen Eigenschaft bestimmen Sie, ob Formulare, die zu groß für die aktuelle Access-Fensterbreite sind, dem verfügbaren Platz angepasst werden sollen. Unter VBA lautet der Name dieser Eigenschaft FitToScreen – obwohl dies streng genommen keine Anpassung an den Bildschirm, sondern an den freien MDI-Bereich von Access ist.
Keine Entwurfsänderungen in der Formularansicht Die Eigenschaft Entwurfsänderungen zulassen (AllowDesignChanges) entfällt. Sie bewirkte in älteren Versionen, dass Formulare nur in der Entwurfsansicht das Eigenschaftenfenster angezeigt haben. In Access 2007 können Sie das Eigenschaftenfenster grundsätzlich nur noch in der Entwurfs- und der Layoutansicht einblenden.
4.2 Formulare öffnen Formulare öffnen Sie in der Regel mit der DoCmd.OpenForm-Anweisung. Es geht zwar auch über das Instanzieren des entsprechenden Formularobjekts und anschließendes
205
Kapitel 4
Einstellen der Eigenschaft Visible auf den Wert True, das ist aber eher Spezialfällen vor behalten (siehe Kapitel 16, Abschnitt 16.2.3, »Öffnen mehrerer Instanzen eines Formu lars«). Da die DoCmd.OpenForm-Anweisung allgemein bekannt sein dürfte (ansonsten liefert die Onlinehilfe einen guten Überblick), soll sie hier nicht im Detail beschrieben werden. Im Hinblick auf die nachfolgenden Beispiele sei nur erwähnt, dass die dortigen Aufrufe »benannte Parameter« verwenden. Das bedeutet, dass die optionalen Parameter nicht in der vorgegebenen Reihenfolge durch Kommata getrennt an die Anweisung angefügt werden, sondern unter Angabe des jeweiligen Parameternamens: DoCmd.OpenForm "frmKontakteDetailansicht", DataMode:=acFormAdd, _ WindowMode:=acDialog
Mit einer herkömmlichen Parameterliste hätte diese Anweisung so ausgesehen: DoCmd.OpenForm "frmKontakteDetailansicht", , , , acFormAdd, acDialog
4.3 Ereignisse in Formularen und Steuerelementen Formulare lassen sich wie auch Berichte bis zu einem gewissen Grad ohne den Einsatz von Ereigniseigenschaften und VBA verwenden. Sobald Sie aber auch nur eine OKSchaltfläche hinzufügen möchten, ist es so weit: Die Programmierung der ersten Ereignisprozedur steht an. In diesem Abschnitt erfahren Sie, welche der zahlreichen Ereignisse von Formularen und der enthaltenen Steuerelemente oft zum Einsatz kommen und wie Sie diese optimal einsetzen – dazu gehört auch, dass Sie sich mit der Reihenfolge der Ereignisse vertraut machen.
4.3.1 Ereignisse in Formularen Formulare bieten eine unüberschaubare Menge Ereignisse. In Access 2007 sind zum Glück keine mehr hinzugekommen, lediglich das neue Anlage-Steurelement bringt einige neue Ereignisse mit. Weitere Informationen zu diesem Steuerelement finden Sie in Kapitel 5, »Steuerelemente«, Abschnitt 5.7.
Im Rahmen dieses Kapitels können leider nur die wichtigsten Ereignisse besprochen werden. Für Informationen zu speziellen Ereignissen klicken Sie einfach in das Feld der gewünschten Eigenschaft im Eigenschaftsfenster und betätigen anschließend die Taste F1. Die Onlinehilfe steht Ihnen dann mit Rat und Tat zur Seite.
206
Formulare
Anlegen eines Ereignisses Abbildung 4.17 zeigt die Ereigniseigenschaften von Formularen – hier aus Platzgründen auf zwei Fenster verteilt.
Abbildung 4.17: Übersicht der Ereigniseigenschaften eines Formulars
Zum Anlegen eines Ereignisses klicken Sie doppelt in das entsprechende Textfeld, sodass dieses den Wert [Ereignisprozedur] erhält, und betätigen dann die Schaltfläche mit den drei Punkten (…) wie in Abbildung 4.18. Wenn Sie es noch einfacher haben möchten, aktivieren Sie in den Access-Optionen im Bereich Objekt-Designer|Formulare/Berichte den Eintrag Immer Ereignisprozeduren verwenden. Dann müssen Sie nur noch auf die beim Aktivieren des Feldes erscheinende Schaltfläche anklicken, um die Ereignisprozedur anzulegen. Wenn Sie dies beispielsweise mit der
207
Kapitel 4
ersten aufgeführten Ereigniseigenschaft Beim Anzeigen machen, öffnet sich der VBAEditor und zeigt direkt den Rumpf der gewünschten Prozedur an: Private Sub Form_Current() End Sub Listing 4.2: Leerer Rumpf einer frisch angelegten Ereignisprozedur
Abbildung 4.18: Anlegen einer Ereignisprozedur
Neben den Ereignisprozeduren können Sie auch Access-Makros und Funktionen per Ereignis auslösen. Dazu tragen Sie einfach den Namen des Access-Makros oder der Funktion ein. Access-Makros bleiben in diesem Buch weitgehend außen vor; der Einsatz von Funktionen kann jedoch durchaus sinnvoll sein. So können Sie beispielsweise für OK-Schaltflächen eine globale Funktion schreiben, die eine Anweisung zum Schließen des Formulars enthält, oder eine Funktion im Formularmodul anlegen, die durch mehrere Ereignisse des gleichen Formulars ausgelöst wird.
Ereigniseigenschaft und Ereignisprozedur — ein unzertrennliches Paar Damit ein Ereignis durch die entsprechende Aktion – also etwa Betätigen einer Schalt fläche – ausgelöst wird, muss die Ereigniseigenschaft den Eintrag [Ereignisprozedur] enthalten und eine Ereignisprozedur mit dem für dieses Ereignis vorgesehenen Namen
208
Formulare
im Klassenmodul des Formulars oder des Berichts vorliegen – beispielsweise cmdOK_ Click.
4.3.2 Abfolge und Bedeutung der Ereignisse beim Öffnen und Schließen eines Formulars Besonders wichtig beim Umgang mit Ereigniseigenschaften ist die Reihenfolge ihrer Abarbeitung und der Zusammenhang mit den im Hintergrund ausgelösten internen Pro zessen wie Laden der angezeigten Daten, Übergeben von Öffnungsargumenten und der gleichen in gebundenen Formularen. Selbst wenn Sie eine Access-Anwendung völlig ohne Dokumentation programmieren müssen, können Sie sich hier selbst weiterhelfen: Schreiben Sie einfach für alle Ereignis eigenschaften, die Sie interessieren, eine kleine Prozedur, die den jeweiligen Ereignis namen ausgibt. Wenn Sie dann das Formular öffnen oder die gewünschte Aktion durchführen, können Sie anschließend oder währenddessen beobachten, in welcher Reihenfolge die Ereignisse ablaufen. Wenn Sie beispielsweise herausfinden, welche Sequenz von Ereignissen beim einfachen Öffnen und Schließen eines Formulars abläuft, legen Sie die Ereignisprozeduren aus folgendem Listing an: Private Sub Form_Activate() Debug.Print "Beim Aktivieren" End Sub Private Sub Form_Close() Debug.Print "Beim Schließen" End Sub Private Sub Form_Current() Debug.Print "Beim Anzeigen" End Sub Private Sub Form_Deactivate() Debug.Print "Beim Deaktivieren" End Sub Private Sub Form_Load() Debug.Print "Beim Laden" End Sub Private Sub Form_Open(Cancel As Integer) Debug.Print "Beim Öffnen" End Sub
209
Kapitel 4 Private Sub Form_Resize() Debug.Print "Bei Größenänderung" End Sub Private Sub Form_Unload(Cancel As Integer) Debug.Print "Beim Entladen" End Sub Listing 4.3: Ausgabe von Meldungen beim Auslösen unterschiedlicher Ereignisprozeduren
Als Ergebnis erhalten Sie beim Öffnen des Formulars folgende Abfolge: Beim Öffnen: Tritt beim Öffnen ein. Bietet die Möglichkeit, das Öffnen abzubrechen, wenn beispielsweise keine Daten vorhanden sind – diese werden dementsprechend bereits vor dem Beim Öffnen-Ereignis eingelesen. Beim Laden: Tritt ein, wenn das Formular einschließlich Steuerelementen geöffnet, aber noch nicht sichtbar ist. Eignet sich für Aktionen wie Setzen von Standardwerten oder Werten von Steuerelementen. Bei Größenänderung: Wird beim Öffnen und bei der Änderung der Größe eines Formulars ausgelöst. Kann zum Anpassen der Größe von Steuerelementen an die Größe des Formulars verwendet werden – falls Sie nicht zufällig von den Möglichkeiten rund um die Verankerung von Steuerelementen unter Access 2007 Gebrauch machen. Beim Aktivieren: Wird ausgelöst, wenn das Formular den Fokus erhält. Beim Anzeigen: Wird beim Anzeigen, bei jedem Datensatzwechsel und beim Aktuali sieren ausgelöst. Beim Schließen sieht der Ablauf so aus: Beim Entladen: Wird durch einen Klick auf eine Schließen-Schaltfläche oder die DoCmd. Close-Anweisung ausgelöst, findet aber vor dem eigentlichen Schließen statt. Der Schließen-Vorgang kann an dieser Stelle durch Setzen des Cancel-Parameters auf den Wert True unterbrochen werden. Beim Deaktivieren: Wird ausgelöst, wenn das Formular den Fokus verliert. Beim Schließen: Wird im Moment des Schließens ausgelöst.
4.3.3 Abfolge und Bedeutung der Ereignisse beim Bearbeiten von Datensätzen Das Bearbeiten eines Datensatzes beginnt mit der ersten Änderung an einem der Felder und endet mit dem Speichervorgang des Datensatzes.
210
Formulare
Ändern von Feldinhalten Änderungen von Datensätzen beginnen mit dem Ändern des Inhalts mindestens eines Feldes. Dies löst eines oder mehrere der folgenden Ereignisse aus: Vor Eingabe: Wird vor der Eingabe des ersten Zeichens ausgelöst, aber nur in Verbin dung mit neuen Datensätzen. Kann abgebrochen werden. Bei Geändert: Wird beim ersten Eingeben oder Löschen eines Zeichens in einem der Datensätze ausgelöst. Kann abgebrochen werden.
Speichern der Änderungen Wird die Änderung gespeichert, werden in der Regel die folgenden drei Ereignisse ausgelöst. Das erste Ereignis Vor Aktualisierung kann den Ablauf allerdings abbrechen, etwa wenn eine Validierung fehlschlägt. Vor Aktualisierung: Wird nach dem Auslösen des Speicherns, aber vor dem eigentlichen Speichervorgang ausgelöst. Ist ein gängiger Platz für die Durchführung von Validierungen. Folgendes Beispiel zeigt, wie Sie die Aktualisierung abbrechen, wenn bestimmte Daten nicht vorhanden sind (weitere Informationen zum Validieren siehe Abschnitt 4.8.2, »Validieren vor dem Speichern«): If IsNull(Me!Telefon) And IsNull(Me!EMail) Then MsgBox "Bitte geben Sie eine Telefonnummer oder eine " _ & "E-Mail-Adresse ein.", vbExclamation + vbOKOnly, _ "Fehlende Daten" Me!Telefon.SetFocus Cancel = True End If
Nach Aktualisierung: Wird nach dem Speichern des Datensatzes ausgelöst. Nach Eingabe: Wird nach dem Speichern eines neuen Datensatzes ausgelöst; gilt nicht für bestehende Datensätze. Beim Anzeigen: Wird beim Wechseln beziehungsweise Speichern des Datensatzes auf gerufen.
Abbruch des Änderungsvorgangs Neben dem Speichern bietet sich nach Änderungen an Feldern eines Datensatzes auch die Möglichkeit, den Vorgang abzubrechen und die vorgenommenen Änderungen am aktuellen Datensatz zu verwerfen. Dies löst das Bei Rückgängig-Ereignis des Formulars aus. Das Ereignis kann durch Setzen des Cancel-Parameters auf den Wert True abgebrochen werden.
211
Kapitel 4
Löschen eines Datensatzes Das Löschen von Datensätzen ist keine triviale Geschichte. Wenn Sie auf eine LöschenSchaltfläche klicken, ist der Datensatz noch lange nicht gelöscht. Zunächst wird die Anzeige aktualisiert, die auf einem temporären, zwischengespeicherten Datenbestand basiert, und dann noch abgewartet, ob die Ereignisse Bei Löschbestätigung oder Nach Löschbestätigung Änderungen bringen. Erst dann überträgt Access die Änderungen in die Tabellen der Datenbank. Beim Löschen: Wird beim Aufrufen des Löschvorgangs und unmittelbar vor dem Löschen ausgelöst. Wenn mehrere Datensätze gleichzeitig gelöscht werden, wird dieses Ereignis für jeden Datensatz einmal aufgerufen. Beim Anzeigen: Wird nach dem Löschen, aber vor dem Übernehmen der Änderungen in die Tabelle ausgelöst. Bei Löschbestätigung: Wird nur ausgelöst, wenn Access eine Bestätigung der Daten satzänderung anzeigt – diese Option lässt sich im Dialog aus Abbildung 4.19 einstellen. Die Bestätigungsmeldung kann unterbunden und durch eine eigene Meldung ersetzt werden. Beispiel: If MsgBox("Möchten Sie den Datensatz wirklich löschen?", _ vbExclamation + vbOKCancel, _ "Löschbestätigung") = vbCancel Then Cancel = True End If Response = acDataErrContinue
Nach Löschbestätigung: Wird nur ausgelöst, wenn eine angezeigte Bestätigung einer Datensatzänderung auch bestätigt wurde beziehungsweise wenn der Löschvorgang nach einer benutzerdefinierten Meldung im Ereignis Bei Löschbestätigung nicht abgebrochen wurde.
4.4 Ereignisse von Steuerelementen Neben dem Formular selbst lösen auch die Steuerelemente Ereignisse aus. Nachfolgend finden Sie einige wichtige Ereigniseigenschaften von Steuerelementen.
Unterformulare Wenn Sie mit Unterformularen arbeiten, sollten Sie wissen, in welcher Reihenfolge die Ereignisse beim Öffnen und Schließen von Unterformularen in Bezug auf die Ereignisse des Hauptformulars ausgelöst werden.
212
Formulare
Die Abfolge sieht folgendermaßen aus: Unterformular Beim Öffnen Unterformular Beim Laden Unterformular Bei Größenänderung Unterformular Beim Anzeigen Hauptformular Beim Öffnen Hauptformular Beim Laden Hauptformular Bei Größenänderung Hauptformular Bei Aktivierung Hauptformular Beim Anzeigen
Abbildung 4.19: Aktivieren der Anzeige einer Meldung beim Ändern von Datensätzen
Das Unterformular wird in der Tat komplett vor dem Hauptformular geladen. Lediglich das Filtern der Datensätze des Unterformulars in Abhängigkeit von den für das Haupt formular vorgesehenen Datensätzen erfolgt noch vor dem Beim Öffnen-Ereignis des Un terformulars.
213
Kapitel 4
Beim Schließen des Formulars wird zuerst das Hauptformular und dann das Unterfor mular geschlossen: Hauptformular Beim Entladen Hauptformular Bei Deaktivierung Hauptformular Beim Schließen Unterformular Beim Entladen Unterformular Beim Schließen
Textfelder Beim Setzen der Einfügemarke in ein Textfeld, beim Ändern des Inhalts und beim anschließenden Verlassen treten die folgenden Ereignisse auf: Beim Hingehen: Beim Eintreten in das Feld Bei Fokuserhalt Bei Geändert: Beim Ändern des ersten Zeichens. Stellt die Eigenschaft Dirty auf den Wert True ein. Bei Änderung: Beim Ändern jedes Zeichens Vor Aktualisierung: Bei jeder Aktion, die zum Verlassen des Feldes führt. Dieses Ereignis bietet die Möglichkeit, feldbezogene Validierungen durchzuführen und die Aktualisierung abzubrechen. Beispiel: If IsNumeric(Left(Me.Projekt, 1)) Then MsgBox "Der Projektname darf nicht mit einer Zahl " _ & "beginnen.", vbOKOnly + vbExclamation, _ "Fehlerhafte Eingabe" Cancel = True End If
Nach Aktualisierung: Nach dem Ereignis Vor Aktualsisierung, wenn dieses nicht abgebrochen wurde. Bietet die Möglichkeit, den eingegebenen Wert weiter zu verwerten oder zu ändern. Beim Verlassen: Nach Abarbeitung der Ereignisse Vor Aktualisierung und Nach Aktualisierung Bei Fokusverlust: Beim Setzen des Fokus auf ein anderes Steuerelement In Zusammenhang mit diesen Ereignissen sind drei Eigenschaften von Textfeldern wichtig: Value, OldValue und Text. OldValue enthält während des ganzen Änderungsvorgangs
214
Formulare
den vorherigen Wert des Feldes. Text enthält den aktuell im Textfeld angezeigten Wert und Value den alten Wert, bis es mit dem Auslösen des Ereignisses Vor Aktualisierung den aktuellen Inhalt des Feldes beziehungsweise der Eigenschaft Text zugewiesen bekommt.
Kombinationsfelder Das Auswählen eines Eintrags und das anschließende Verlassen eines Kombinationsfel des löst die folgenden Ereignisse aus (wenn dieses noch nicht den Fokus hat). In runden Klammern finden Sie Ereignisse, die nur bei der manuellen Eingabe von Zeichen ausgeführt werden, in eckigen Klammern die Ereignisse, die nur bei manueller Eingabe nicht vorhandener Listeneinträge ausgelöst werden. Beim Hingehen Bei Fokuserhalt Bei Änderung: Beim Auswählen eines Eintrags oder bei der ersten Eingabe eines Zeichens (Bei Geändert: Bei der manuellen Eingabe von Zeichen) [Bei nicht in Liste: Nach der manuellen Eingabe eines Wertes, der nicht in der Liste enthalten ist. Hier können Sie eine Prozedur hinterlegen, die nicht in der Liste enthaltene Einträge in der zugrunde liegenden Datensatzherkunft anlegt und den neuen Wert im Kombinationsfeld festlegt. In diesem Fall folgen die übrigen Ereignisse, sonst wird der Änderungsvorgang unterbrochen.] Vor Aktualisierung Nach Aktualisierung Beim Klicken Bei Geändert Beim Verlassen Bei Fokusverlust
Weitere Steuerelemente Die übrigen Steuerelemente wie Listenfelder, Kontrollkästchen oder Optionsgruppen haben je nach Typ unterschiedliche Ereignisse. Nachdem Sie erfahren haben, wie Sie die Abfolge der Ereignisse von Formularen und Steuerelementen ermitteln, soll an dieser Stelle nicht weiter auf die Ereignisse der noch nicht besprochenen Steuerelemente und ihre Abfolge eingegangen werden.
215
Kapitel 4
4.5 Abbildung verschiedener Beziehungsarten Die vorhandene Fachliteratur beschränkt sich weitgehend auf die Darstellung von Daten aus einzelnen oder aus per 1:n-Beziehung verknüpften Tabellen. Daher erhalten Sie in diesem Abschnitt des Kapitels einen Überblick über die Realisierung der verschiedenen Beziehungsarten in Formularen.
4.5.1 Einfache Daten in der Detailansicht Die Daten aus einzelnen Tabellen oder einfachen Abfragen lassen sich mit wenigen Schritten in einem Formular darstellen. Üblicherweise sind hier zwei Darstellungen gefragt: Detailansichten oder Übersichtslisten. Mit »einfachen Abfragen« sind hier Abfragen gemeint, deren Daten quasi wie eine Tabelle gehandhabt werden – also etwa Abfragen, die zwei per 1:1-Beziehung verknüpfte Tabellen darstellen oder die eine einzelne Tabelle mit einem oder mehreren Lookup-Feldern beinhalten. Detailansichten dienen dazu, die sich oft über viele Felder erstreckenden Daten in einer Form anzuzeigen, die die Daten einerseits vernünftig strukturiert und andererseits eine komfortable Bearbeitung ermöglicht. Übersichtslisten dienen selten dazu, die Daten kompletter Tabellen anzuzeigen – es sei denn, diese enthalten nicht besonders viele Felder. Wenn der Benutzer scrollen muss, um alle Felder eines Formulars sehen zu können, ist die Menge der angezeigten Felder zu groß. Meist dienen solche Übersichtslisten dazu, wichtige Informationen zu den angezeigten Datensätzen sowie die Anzeige einer Detailansicht zu bieten. Übersichtlisten gibt es in drei Formen: als Endlosformular, Datenblattansicht oder in geteilten Formularen.
Detailansicht einfacher Daten in Formularen Die Erstellung einer Detailansicht einfacher Daten läuft in folgenden Schritten ab: Anlegen eines neuen, leeren Formulars Festlegen der Datensatzquelle im Eigenschaftsfenster Einfügen der benötigten Felder (Ribboneintrag Entwurf|Tools|Vorhandene Felder hinzufügen, siehe Abbildung 4.20) Anpassen und Ausrichten der Felder und Anpassen der Beschriftungen
216
Formulare
Das ist im Prinzip einfach. Wichtig ist lediglich, dass Sie beim Anlegen der Datensatzquelle in Schritt 2 darauf achten, dass die Datensatzquelle nur die Felder enthält, die Sie auch wirklich benötigen.
Abbildung 4.20: Das Hinzufügen der Einträge der Feldliste in das Formular kann durch Ziehen mit der Maus erfolgen
Muss man in Detailansichten blättern können? Die oben erstellte Detailansicht enthält einige Elemente, die die Navigation in den Datensätzen des Formulars erleichtern: Bildlaufleisten Datensatzmarkierer Navigationsschaltflächen Das Formular aus Abbildung 4.21 enthält all diese Elemente. Die Frage ist nun: Welche davon benötigen Sie in einer Detailansicht? Was zu einer anderen Frage führt: Was macht der Benutzer eigentlich mit dieser Detailansicht? Nun, er soll die Details eines Datensatzes betrachten und gegebenenfalls die enthaltenen Daten ändern beziehungs weise neue Datensätze anlegen oder bestehende löschen können. Soll er mit der Na vigationsleiste arbeiten, um innerhalb der Datensätze zu navigieren? Seit Access 2007 vielleicht: Immerhin befindet sich dort ein Suchen-Feld.
217
Kapitel 4
Optimal wäre es trotzdem, wenn der Benutzer die oben genannten Elemente gar nicht benötigt, sondern mit anderen Mitteln zu einem gesuchten oder einem neuen Datensatz gelangt – immerhin ist nicht jeder Benutzer mit diesen Steuerelementen vertraut und intuitiv zu bedienen sind sie auch nicht unbedingt.
Abbildung 4.21: Die Formularansicht des Detailformulars
Angenommen, der Benutzer benötigt diese zusätzlichen Elemente gar nicht: Dann zeigen Sie diese doch gar nicht an. Drei Mausklicks im Eigenschaftsfenster der Entwurfsansicht des Formulars, einer noch, wenn das Formular beim Öffnen zentriert angezeigt werden soll, was immer Sinn macht, und noch eine Beschriftung hinzufügen – blitzschnell sieht das Eigenschaftsfenster wie in Abbildung 4.22 und das Formular wie in Abbildung 4.23 aus.
Abbildung 4.22: Eigenschaften eines Formulars …
218
Formulare
Abbildung 4.23: … ohne unnötigen Schnickschnack (»frmKontakteDetailansicht«)
Navigation und Aktion in Detailformularen Nun fehlen noch die Möglichkeiten zum Auswählen eines Datensatzes, zum Anlegen eines neuen und Löschen des bestehenden Datensatzes. Nur: Müssen diese Elemente auf das Detailformular? Das ist sicher Geschmackssache, aber Sie sollten sich für eine der folgenden beiden Möglichkeiten entscheiden: Entweder Sie verwenden nur eine OKund eine Abbrechen-Schaltfläche oder Sie fügen neben einer Möglichkeit zur Auswahl von Datensätzen auch noch je eine Schaltfläche zum Löschen und zum Anlegen von Datensätzen hinzu. Nachfolgend finden Sie eine Beschreibung der ersten Variante. Die Schaltflächen zum Auswählen, Anlegen und Löschen eines Datensatzes werden in Zusammenhang mit den unten beschriebenen Übersichtsformularen in der Endlos- und der Datenblattansicht beschrieben.
OK- und Abbrechen-Schaltflächen Diese beiden Schaltflächen lösen jeweils Prozeduren mit nur einer Zeile VBA-Code aus. Legen Sie die beiden Schaltflächen an, stellen Sie die Eigenschaft Beschriftung auf die Werte OK und Abbrechen und die Eigenschaft Name auf die Werte cmdOK und cmdAbbrechen ein. Legen Sie dann für beide je eine Prozedur für die Ereigniseigenschaft Beim Klicken an (siehe Abbildung 4.24): Private Sub cmdOK_Click() DoCmd.Close acForm, Me.Name End Sub Private Sub cmdAbbrechen_Click() Me.Undo DoCmd.Close acForm, Me.Name End Sub Listing 4.4: Schließen mit und ohne Übernahme der Änderungen
219
Kapitel 4
Die OK-Schaltfläche sorgt in der Regel nur dafür, dass ein Formular geschlossen wird. Es gibt aber auch Ausnahmen: Wenn ein Formular geöffnet wurde, um Daten zu ermit teln, die in der aufrufenden Routine weiter verarbeitet werden sollen, müssen diese vor dem Schließen natürlich erst abgefragt werden. Wie dies funktioniert, erfahren Sie weiter unten in Abschnitt 4.6, »Von Formular zu Formular«.
Abbildung 4.24: Anlegen (»frmDetailansicht«)
der
Beim
Klicken-Ereigniseigenschaft
für
eine
Schaltfläche
4.5.2 Einfache Daten in der Übersicht mit Endlosformularen Ein Formular zur Anzeige der Übersicht von Datensätzen enthält die gleiche Datensatzquelle wie das Formular zur Anzeige der Detailansicht, in der Regel jedoch mit weniger Feldern. Für Tabellen, die so wenige Felder enthalten, dass diese leicht nebeneinander angezeigt werden können, braucht prinzipiell gar keine Detailansicht erstellt zu werden. Die Felder sind meist wie in Abbildung 4.25 nebeneinander angeordnet. In der Abbildung finden Sie direkt die Elemente zum Steuern von Aktionen wie Neuanlegen, Löschen und Bearbeiten von Datensätzen. Die OK-Schaltfläche dient wie die Schaltfläche des zuvor beschriebenen Formulars lediglich dem Schließen des Formulars. Die Neu-Schaltfläche soll das oben beschriebene Formular zur detaillierten Ansicht eines Datensatzes öffnen und einen leeren Datensatz anzeigen. Dafür ist die folgende Ereigniseigenschaft verantwortlich, die durch das Beim Klicken-Ereignis der Schaltfläche ausgelöst wird.
220
Formulare
Abbildung 4.25: Entwurfsansicht eines Übersichtsformulars (»frmKontakteEndlosformular«)
Formular mit leerem Datensatz öffnen Die Routine verwendet die OpenForm-Methode des DoCmd-Objekts zum Öffnen des For mulars frmKontakteDetailansicht. Der Parameter DataMode erhält dabei den Wert acForm Add, damit das Formular beim Öffnen direkt einen leeren Datensatz anzeigt.
Formular als modalen Dialog öffnen Der Wert des Parameters WindowMode liefert die Voraussetzung dafür, dass das aufrufende Formular – die Übersicht – den angezeigten Datenbestand direkt nach dem Eingeben des neuen Datensatzes und Schließen des Detailformulars aktualisieren kann (siehe Abbildung 4.26). Durch den Wert acDialog wird das Formular frmKontakteDetailansicht als modaler Dialog geöffnet, was bedeutet, dass innerhalb der Access-Anwendung nichts mehr geht, solange dieses Formular geöffnet ist – selbst der aufrufende Code wird währenddessen angehalten. Erst wenn das Formular den Fokus verliert, also geschlossen oder unsichtbar gemacht wird, läuft die aufrufende Routine weiter. Dadurch kann die Übersicht direkt nach dem Schließen der Detailansicht aktualisiert werden. Private Sub cmdNeu_Click() DoCmd.OpenForm "frmKontakteDetailansicht", DataMode:=acFormAdd, _ WindowMode:=acDialog Me.Requery End Sub Listing 4.5: Aufrufen des Detailformulars zum Anlegen eines neuen Datensatzes
Löschen von Datensätzen Die nächste Schaltfläche dient dem Löschen des aktuell markierten Datensatzes. Die einfachste Variante des benötigten Codes sieht wie folgt aus:
221
Kapitel 4 Private Sub cmdLoeschen_Click() On Error Resume Next DoCmd.RunCommand acCmdDeleteRecord End Sub Listing 4.6: Löschen eines Datensatzes
Abbildung 4.26: Öffnen eines Eingabeformulars vom Übersichtsformular aus (»frmKontakte Endlosformular«, »frmKontakteDetailansicht«)
Die Routine verwendet die RunCommand-Methode des DoCmd-Objekts mit dem Parame ter acCmdDeleteRecord, um den aktuell markierten Datensatz zu löschen. Da hier eigent lich nur der Fehler auftreten kann, dass kein Datensatz markiert ist, verhindern Sie diesen mit der On Error Resume Next-Anweisung im Vorhinein. Alternativ können Sie den aktuellen Datensatz im Übrigen auch mit folgender Anweisung löschen: Me.Recordset.Delete
Etwas weniger rudimentär gestalten Sie das Abfangen dieses Fehlers, wenn Sie die On Error Resume Next-Anweisung durch folgenden Code ersetzen. Dieser prüft, ob aktuell kein Datensatz ausgewählt ist, und bricht in diesem Fall mit einer entsprechenden Meldung ab: If IsNull(Me!KontaktID) Then MsgBox "Bitte wählen Sie zunächst einen Datensatz aus." Exit Sub End If
Sollte die Anzeige von Warnmeldungen bei Datensatzänderungen aktiviert sein (siehe Ab bildung 4.19), erscheint hier allerdings eine unschöne Meldung (siehe Abbildung 4.27).
222
Formulare
Abbildung 4.27: Access-Meldung beim Löschen eines Datensatzes
Wenn Sie diese Meldung durch eine eigene Meldung ersetzen möchten, verwenden Sie die folgende Prozedur. Die SetWarnings-Methode des DoCmd-Objekts deaktiviert dabei die Anzeige der eingebauten Meldung und aktiviert diese anschließend wieder. Private Sub cmdLoeschen_Click() On Error Resume Next DoCmd.SetWarnings False If MsgBox("Möchten Sie den Kontakt '" & Me!Vorname & " " _ & Me!Nachname & "' wirklich löschen?", vbYesNo + vbExclamation, _ "Löschbestätigung") = vbYes Then DoCmd.RunCommand acCmdDeleteRecord End If DoCmd.SetWarnings True End Sub Listing 4.7: Löschen eines Datensatzes mit benutzerdefinierter Warnmeldung
Anzeigen der Details zu einem Datensatz Die letzte Schaltfläche des Übersichtsformulars dient dem Anzeigen der Detailansicht des ausgewählten Datensatzes. Die dadurch ausgelöste Routine prüft zunächst, ob ein Datensatz markiert ist. Falls ja, öffnet sie das Detailformular wiederum mit der DoCmd. OpenForm-Methode. In diesem Fall kommt für den Parameter DataMode jedoch der Wert acFormEdit zum Zuge und der anzuzeigende Datensatz wird mit dem Parameter WhereCondition festgelegt. Dieser erwartet als Wert – wie der Name schon sagt – eine Bedingung, die den anzuzeigenden Datensatz festlegt. Hier lautet die Bedingung, dass der Wert des Feldes KontaktID dem des ausgewählten Datensatzes im Übersichtsformular entsprechen muss. Private Sub cmdBearbeiten_Click() If IsNull(Me!KontaktID) Then MsgBox "Bitte wählen Sie zunächst einen Datensatz aus."
223
Kapitel 4 Exit Sub End If DoCmd.OpenForm "frmKontakteDetailansicht", DataMode:=acFormEdit, _ WindowMode:=acDialog, WhereCondition:="KontaktID = " & Me!KontaktID End Sub Listing 4.8: Öffnen eines Datensatzes in der Detailansicht
4.5.3 Einfache Daten in der Übersicht als Datenblatt Neben der Endlosansicht ist auch die Datenblattansicht von Formularen für die Verwen dung als Übersichtsformular geeignet. Die Datenblattansicht hat den Vorteil, dass der Benutzer dort selbst die Spaltenbreite einstellen kann – das ist bei Endlosformularen mitunter ein Problem, wenn wenig Platz vorhanden oder die maximale Länge der anzuzeigenden Zeichenketten nicht bekannt ist. Auch die Performance ist in dieser Ansicht besser. Allerdings hat auch das Endlosformular Vorteile. Warum das so ist, erfahren Sie, wenn Sie das Formular frmKontakteUebersicht einmal in der Datenblattansicht anzeigen (siehe Abbildung 4.28). Das Datenblatt ist zwar als Übersicht akzeptabel, aber Kopf- und Fußbereich des Formulars sind völlig verschwunden. Tatsache ist: Beide können in der Datenblattansicht schlicht nicht verwendet werden.
Abbildung 4.28: Übersichtsformular in der Datenblattansicht (»sfmKontakteDatenblattansicht«)
Datenblattansicht im Unterformular Wer dennoch ein Formular in der Datenblattansicht verwenden möchte, setzt dies als Unterformular in ein Hauptformular ein, das die Navigationselemente zum Anlegen, Löschen und Bearbeiten der Einträge bereitstellt. Das Unterformular enthält einfach nur die Felder, die in der Datenblattansicht angezeigt werden sollen (siehe Abbildung 4.29). Es gibt lediglich drei Punkte, auf die Sie achten sollten:
224
Formulare
Stellen Sie die Eigenschaft Standardansicht auf Datenblatt ein. Versehen Sie die Bezeichnungsfelder der einzelnen Steuerelemente mit ordentlichen Beschriftungen, da diese später als Feldüberschriften dienen. Sorgen Sie für die richtige Aktivierreihenfolge, da diese festlegt, in welcher Reihen folge die einzelnen Felder später angezeigt werden (Dialog öffnen über den Eintrag Aktivierreihenfolge des Steuerelement-Kontextmenüs).
Abbildung 4.29: Der Entwurf eines Formulars in der Datenblattansicht braucht keine besonderen optischen Ansprüche zu erfüllen.
Hauptformular als Container für ein Unterformular in der Datenblattansicht Bereiten Sie dann das Hauptformular vor. Dazu legen Sie ein neues, leeres Formular an und kopieren die bereits erstellten Schaltflächen des Formulars frmKontakteEndlosfor mular in den Detailbereich des neuen Formulars. Lassen Sie dabei ein wenig Platz nach oben. Ziehen Sie dann das neue Unterformular aus dem Navigationsbereich in den De tailbereich des Formulars (siehe Abbildung 4.30).
Anpassen der Datenblattansicht eines Formulars Das Erste, was Ihnen beim Blick auf die Formularansicht auffallen wird, ist, dass standardmäßig fast alle Steuerelemente in Formularen in Schriftgröße 11 dargestellt werden, das Unterformular in der Datenblattansicht aber mit Schriftgröße 10 aufwartet. Außerdem ist die Breite der Felder des Unterformulars natürlich nicht den Inhalten angepasst. Beides holen Sie nach – allerdings nicht in der Entwurfsansicht, sondern in der Formularansicht des Unterformulars. Dieses müssen Sie dafür allerdings noch einmal separat öffnen. Wenn Tabellen, Abfragen und Formulare in der Datenblattansicht grundsätzlich in einer anderen Schriftgröße oder in anderem Layout geöffnet werden sollen, können Sie die gewünschten Einstellungen in den Access-Optionen im Bereich Datenblatt anpassen. Dort finden Sie auch die neue Option zum Einstellen einer alternativen Hintergrundfarbe.
225
Kapitel 4
Abbildung 4.30: Ziehen des Unterformulars aus dem Navigationsbereich in den Detailbereich des neuen Hauptformulars
Wenn Sie die dortigen Einstellungen je Formular individuell vergeben möchten, können Sie dies entweder mit dem Ribbon-Eintrag Entwurf|Schriftart|Alternative Füllung/Hinter grundfarbe oder per VBA durchführen. Dazu hinterlegen Sie die folgende Prozedur für die Ereigniseigenschaft Beim Laden: Private Sub Form_Load() Me.DatasheetAlternateBackColor = &HDDDDFF End Sub Listing 4.9: Individuelles Anpassen der alternativen Hintergrundfarbe eines Formulars in der Datenblattansicht
Die Farbe können Sie statt im hexadezimalen Format auch mit der RGB-Funktion angeben – zum Beispiel so: Me.DatasheetAlternateBackColor=RGB(255,240,204)
Wenn Sie die Farbe des aktuell angezeigten Datenblatts – unabhängig davon, ob es sich um eine Tabelle, Abfrage oder ein Formular handelt – ändern möchten, können Sie diesen Aufruf verwenden (beispielsweise im Direktfenster):
Die übrigen Einstellungen des Optionen-Dialogs können Sie ebenfalls auf diese Weise vornehmen. Die Namen der passenden Eigenschaften beginnen alle mit »Datasheet...«. Die Spaltenbreiten bändigen Sie durch Ziehen des vertikalen Trennstriches zwischen zwei Spalten und die Schriftgröße passen Sie in einem Dialog an, den Sie per Kontextme nü der Titelleiste des Formulars öffnen. Nachdem Sie im Hauptformular auch noch die Bildlaufleisten, Datensatzmarkierer, Na vigationsschaltflächen und Trennlinien deaktiviert haben, sieht das Formular schon viel besser aus (siehe Abbildung 4.31).
Abbildung 4.31: Datenblattansicht als Übersicht (»frmKontakteDatenblattansicht«)
Bezug auf Steuerelemente im Unterformular Nun fehlen noch die Funktionen der Schaltflächen. Da sich die betroffenen Daten nun in einem Unterformular befinden, können Sie nicht ohne Weiteres die Prozeduren aus dem Formular frmKontakteEndlosformular einsetzen – aber fast. Die Prozedur hinter der Schaltfläche OK können Sie komplett übernehmen. Die Prozeduren zum Anlegen, Löschen und Bearbeiten des aktuell markierten Datensatzes sehen nun folgendermaßen aus: Private Sub cmdBearbeiten_Click() If IsNull(Me!sfmKontakteDatenblatt!KontaktID) Then MsgBox "Bitte wählen Sie zunächst einen Datensatz aus." Exit Sub End If DoCmd.OpenForm "frmKontakteDetailansicht", DataMode:=acFormEdit, _ WindowMode:=acDialog, WhereCondition:="KontaktID = " _ & Me!sfmKontakteDatenblatt!KontaktID Me.Requery End Sub
227
Kapitel 4 Private Sub cmdLoeschen_Click() If IsNull(Me!sfmKontakteDatenblatt.Form!KontaktID) Then MsgBox "Bitte wählen Sie zunächst einen Datensatz aus." Exit Sub End If DoCmd.SetWarnings False If MsgBox("Möchten Sie den Kontakt '" _ & Me!sfmKontakteDatenblatt!Vorname & " " _ & Me!sfmKontakteDatenblatt!Nachname & "' wirklich löschen?", _ vbYesNo + vbExclamation, "Löschbestätigung") = vbYes Then Me!sfmKontakteDatenblatt.SetFocus DoCmd.RunCommand acCmdDeleteRecord End If DoCmd.SetWarnings True End Sub Private Sub cmdNeu_Click() DoCmd.OpenForm "frmKontakteDetailansicht", DataMode:=acFormAdd, _ WindowMode:=acDialog Me!sfmKontakteDatenblatt.Requery End Sub Listing 4.10: Änderungen an den Prozeduren zum Löschen und Bearbeiten von Datensätzen im Detailformular
Die Änderungen gegenüber den Prozeduren des Formulars mit der Endlosansicht sind fett gedruckt. Dabei wird jeweils auf die im Unterformular befindlichen Steuerelemente Bezug genommen, die im Formular frmKontakteEndlosformular alle im gleichen Formular wie die Schaltflächen angesiedelt waren. Bei den Steuerelementen wird statt Me!KontaktID etwa Me!sfmKontakteDatenblatt!Kontakt ID verwendet. Stattdessen könnte man auch ausführlicher schreiben: Me!sfmKontakteDatenblatt.Form!KontaktID
oder Me.Controls("sfmKontakteDatenblatt").Form.Controls("KontaktID")
Vorteil Datenblattansicht Ein klarer Vorteil der hier konstruierten erweiterten Datenblattansicht gegenüber der Endlosansicht ist, dass nach dem Bearbeiten von Daten im Detailformular und der Aktualisierung der überarbeiteten Daten im Übersichtsformular nicht der Datensatz gewechselt wird. Im Formular frmKontakteEndlosformular springt der Datensatzzeiger nach dem Aktualisieren mit der Requery-Methode immer wieder auf den ersten Daten satz. Das ist in der Datenblattansicht nicht der Fall. Hier ist darüber hinaus nur eine
228
Formulare
Aktualisierung per Requery nach dem Anlegen eines neuen Datensatzes erforderlich, im Detailformular vorgenommene Änderungen werden automatisch übernommen. Das Gleiche gilt natürlich auch für die Datenblattansicht.
4.5.4 Daten in der Übersicht als Listenfeld Die dritte Möglichkeit der Darstellung von Daten in der Übersicht ist die Verwendung eines Listenfeldes. Wie Sie bemerkt haben werden, kann man mit den beiden zuvor beschriebenen Varianten auch bereits in der Übersicht Daten verändern, wenn man nicht gerade die einzelnen Steuerelemente sperrt oder die Eigenschaft RecordsetTyp des Formulars auf Snapshot einstellt. Hier bringt das Listenfeld Vorteile: Es ist keine direkte Bearbeitung möglich und außerdem scheint das Markieren zu bearbeitender oder zu löschender Datensätze intuitiver zu sein. Davon abgesehen kann man den Doppelklick für die schnelle Anzeige des Detailformulars auslegen. Mit den folgenden Schritten haben Sie rasch ein Listenfeld als Übersicht erstellt: Legen Sie ein Listenfeld in einem leeren Formular an und vergrößern Sie es auf etwa 12 cm Breite. Stellen Sie die Eigenschaft Name auf lstKontakte ein. Legen Sie als Datensatzherkunft eine Abfrage an, die auf der Tabelle tblKontakte basiert und nur die wichtigsten Felder KontaktID, Nachname, Vorname, Telefonnummer und EMail enthält. Gegebenenfalls können Sie auch eine Sortierung festlegen – etwa nach dem Nachnamen. Stellen Sie die Eigenschaften Spaltenanzahl und Spaltenbreiten auf die Werte 5 und 0cm;2cm:2cm;3cm ein. Dadurch wird die gebundene Spalte KontaktID ausgeblendet (Primärschlüsselfelder soll man, wie oben bereits erläutert, nur anzeigen, wenn diese geschäftliche Informationen enthalten). Das Feld muss aber dennoch in der Abfrage und auch im Listenfeld enthalten sein, da sonst kein Bezug zum entsprechenden Datensatz hergestellt werden kann. Die weiteren Spalten werden in der entsprechenden Breite angezeigt und die letzte Spalte füllt den restlichen Platz aus. Je nach Geschmack stellen Sie die Eigenschaft Spaltenüberschriften auf Ja ein oder legen selbst Beschriftungsfelder oberhalb des Listenfeldes an. Bedenken Sie, dass die Spaltenüberschriften – falls angezeigt – eine Zeile belegen und damit der Index des ersten angezeigten Datensatzes nicht 0, sondern 1 ist. Wenn Sie noch die obligatorischen Schaltflächen wie in den beiden vorherigen Beispielen hinzufügen und die Eigenschaften Bildlaufleisten, Datensatzmarkierer, Navigationsschalt flächen und Trennlinien auf Nein einstellen, sieht das Formular wie in Abbildung 4.32 aus.
229
Kapitel 4
Abbildung 4.32: Übersicht per Listenfeld (»frmKontakteListenfeld«)
Nun fehlen noch die dem Listenfeld angepassten Prozeduren zum Anlegen, Löschen und Bearbeiten des aktuell ausgewählten Datensatzes. Hierbei ist zu beachten, dass nach Änderungen immer die Requery-Methode des Listenfeld-Steuerelements ausgeführt werden muss. Die fett gedruckten Zeilen enthalten die Anweisungen, die sich speziell auf das Listenfeld beziehen. Um den Wert des Feldes KontaktID für den aktuellen Listenfeldeintrag zu ermitteln, lesen Sie einfach den Wert des Listenfeldes aus. Das Feld KontaktID ist das gebundene Feld der Datensatzherkunft und dadurch mit dem Wert des Listenfeldes identisch. Eine weitere Änderung ist zum Löschen des markierten Datensatzes notwendig: Mit DoCmd.RunCommand acCmdDeleteRecord ist einem Eintrag im Listenfeld nicht beizukom men, hier müssen Sie direkt mit einer DELETE-Anweisung auf die zugrunde liegende Tabelle zugreifen. Private Sub cmdBearbeiten_Click() If IsNull(Me!lstKontakte) Then MsgBox "Bitte wählen Sie zunächst einen Datensatz aus." Exit Sub End If DoCmd.OpenForm "frmKontakteDetailansicht", DataMode:=acFormEdit, _ WindowMode:=acDialog, WhereCondition:="KontaktID = " _ & Me!lstKontakte Me!lstKontakte.Requery End Sub Private Sub cmdLoeschen_Click() If IsNull(Me!lstKontakte) Then MsgBox "Bitte wählen Sie zunächst einen Datensatz aus."
230
Formulare Exit Sub End If DoCmd.SetWarnings False If MsgBox("Möchten Sie den Kontakt '" & Me!lstKontakte & " " _ & Me!lstKontakte & "' wirklich löschen?", _ vbYesNo + vbExclamation, "Löschbestätigung") = vbYes Then CurrentDb.Execute "DELETE FROM tblKontakte WHERE KontaktID = " _ & Me!lstKontakte Me!lstKontakte.Requery End If DoCmd.SetWarnings True End Sub Private Sub cmdNeu_Click() DoCmd.OpenForm "frmKontakteDetailansicht", DataMode:=acFormAdd, _ WindowMode:=acDialog Me!lstKontakte.Requery End Sub Listing 4.11: Anlegen, Löschen und Bearbeiten von Datensätzen eines Listenfeldes
4.5.5 1:1-Beziehungen Die Daten aus 1:1-Beziehungen lassen sich genau wie die oben beschriebenen »einfachen Daten« in Formularen abbilden. Sie können eine Abfrage über die an der 1:1-Beziehung beteiligten Tabellen erstellen und genau die gleichen Formulare wie oben verwenden. Sie können aber auch beispielsweise im Übersichtsformular nur die Daten der einen Seite der Beziehung anzeigen und die Details erst im Detailformular offenbaren. Einen kleinen Trick müssen Sie anwenden, wenn Sie sicherstellen wollen, dass auch in der Erweiterungstabelle zu jedem Datensatz der Haupttabelle ein passender Datensatz angelegt wird – auch ohne, dass der Benutzer explizit Daten für die Erweiterungstabelle eingibt. Dazu legen Sie eine kleine Prozedur an, die durch die Ereigniseigenschaft Vor Aktualisierung des Formulars ausgelöst wird. Dieses Ereignis stellt einfach den Wert des Verknüpfungsfeldes der Erweiterungstabelle auf den Wert des Primärschlüsselfeldes der Haupttabelle ein: Private Sub Form_BeforeUpdate(Cancel As Integer) Me!tblAngestellte_PersonID = Me!tblPersonen_PersonID End Sub Listing 4.12: Synchronisieren von Haupt- und Erweiterungstabelle im Formular
231
Kapitel 4
Das Beispielformular finden Sie nicht in der Datenbank zu diesem Kapitel, sondern unter \Kap_03\Abfragen.accdb unter dem Formularnamen frmAngestellte.
4.5.6 n:1-Beziehungen n:1-Beziehungen bestehen aus einer Detailtabelle, von der ein Feld in Form einer Mastertabelle ausgelagert wurde – das einfachste Beispiel ist wohl das Feld Anrede in einer Kontakt-, Adress- oder Mitarbeitertabelle. Die Darstellung und Auswahl der Daten aus der Mastertabelle erfolgt im Formular über ein Kombinationsfeld. Wer gute Vorarbeit leistet und per Nachschlage-Assistent oder von Hand bereits beim Datenentwurf dafür sorgt, dass die Daten aus verknüpften Tabellen in einem Kombi nationsfeld ausgewählt werden können, hat beim Erstellen eines Formulars für diese Beziehungsart leichtes Spiel: Das Kombinationsfeld wird dann automatisch angelegt (siehe Abbildung 4.33). Wie Sie eine Tabelle mit einem Kombinationsfeld zur Darstel lung einer n:1-Beziehung erstellen, erfahren Sie in Kapitel 2, Abschnitt 2.5.4, »n:1-Bezie hungen oder Lookup-Beziehungen«.
Abbildung 4.33: Darstellung einer n:1-Beziehung im Formular
Sollte es aus irgendwelchen Gründen nicht gewünscht sein, dass das Kombinationsfeld direkt in der Tabelle angelegt wird, müssen Sie selbst Hand anlegen. In diesem Fall zeigt das Formular nach dem Hinzufügen der Felder aus der Feldliste zunächst ein herkömmliches Textfeld statt eines Kombinationsfeldes an. Wenn Sie wissen, aus welcher Datei das Fremdschlüsselfeld seine Daten beziehen soll, gehen Sie folgendermaßen vor: Konvertieren Sie das Textfeld in ein Kombinationsfeld, indem Sie aus dessen Kon textmenü den Eintrag Ändern zu|Kombinationsfeld auswählen. Stellen Sie die Eigenschaft Datensatzherkunft des frisch gebackenen Kombinationsfel des auf die Tabelle tblAnreden ein.
232
Formulare
Legen Sie für die Eigenschaften Spaltenanzahl und Spaltenbreiten die Werte 2 und 0 fest. Es sollen beide Felder der Tabelle enthalten sein, aber nur die zweite mit den Anreden selbst angezeigt werden. Das Kombinationsfeld zeigt nun die vorhandenen Einträge wie in Abbildung 4.33 an.
4.5.7 1:n-Beziehungen 1:n-Beziehungen trifft man während des Entwicklerlebens vermutlich am häufigsten an, vor allem, wenn man n:1-Beziehungen auch darunter einstuft. Ein klassisches Beispiel für eine 1:n-Beziehung sind Projekte und Kunden, wobei die Projekte in der Detailtabelle und die Kunden in der Mastertabelle gespeichert werden (siehe Abbildung 4.34).
Abbildung 4.34: 1:n-Beziehung zwischen Projekten und Kunden
Die Gestaltung des Formulars für die Anzeige der Projekte entspricht vom Aufbau her dem Formular aus Abbildung 4.34 – es enthält die Daten zum jeweiligen Projekt und der Kunde kann mit einem Kombinationsfeld ausgewählt werden. Interessanter ist die Darstellung eines Kunden und seiner Projekte. Der Kunde wird dabei wie in Abschnitt 4.5.1, Teilabschnitt »Detailansicht einfacher Daten in Formularen« angezeigt. Zusätzlich sollen aber nun die Projekte des Kunden im Überblick dargestellt werden – und dazu gibt es wiederum verschiedene Möglichkeiten. Auch diese Möglich keiten kennen Sie bereits – wenn auch in vereinfachter Form – aus den Abschnitten 4.5.2, »Einfache Daten in der Übersicht mit Endlosformularen«, 4.5.3, »Einfache Daten in der Übersicht als Datenblatt«, 4.5.4, »Daten in der Übersicht als Listenfeld« und 4.13, »Geteilte Formulare«. Dabei müssen Sie in diesem Fall sowohl das Endlosformular als auch die Datenblatt ansicht in ein Unterformular ausgliedern; lediglich das Listenfeld können Sie direkt in das Kunden-Formular integrieren.
233
Kapitel 4
Verknüpfte Daten direkt bearbeiten oder nicht? Welche Variante Sie verwenden, hängt zunächst einmal davon ab, ob Sie die verknüpften Daten – also die Projekte – direkt im Kundenformular bearbeiten möchten oder nicht. Falls ja, verwenden Sie ein Unterformular mit den Projekten in einem Endlosformular oder der Datenblattansicht, ansonsten entscheiden Sie sich für ein Unterformular mit dem Recordset-Typ Snapshot oder ein Listenfeld. Da die Unterformular-Varianten technisch ähnlich sind und beide in einfacher Form bereits ausführlich erläutert wurden, soll hier nur eine der beiden Möglichkeiten beschrieben werden – die etwas einfacher zu realisierende Datenblattansicht.
4.5.8 1:n-Beziehung per Unterformular und Datenblattansicht Das Unterformular zur Anzeige der Projekte in der Datenblattansicht ist genauso wie das aus Abschnitt 4.5.3, »Einfache Daten in der Übersicht als Datenblatt«, aufgebaut. Es basiert allerdings auf einer Abfrage, die alle Felder der Tabelle tblProjekte enthält und diese zusätzlich nach dem Startdatum sortiert (siehe Abbildung 4.35).
Abbildung 4.35: Datensatzquelle des Unterformulars zur Anzeige der Projekte eines Kunden (»sfmProjekte«)
Vorbereiten des Unterformulars Das Unterformular selbst enthält lediglich die drei Felder Projekt, Startdatum und End datum. Die anderen Felder werden zwar für die Funktionalität benötigt, sollen aber nicht angezeigt werden – und die Datenblattansicht zeigt immer alle im Entwurf enthaltenen Felder an. Daran ändert auch das Einstellen der Sichtbar-Eigenschaft auf den Wert Nein
234
Formulare
nichts. Die einzige Möglichkeit, ein Feld »unsichtbar« zu machen, ist das Verkleinern der Spaltenbreite auf den Wert 0. Dies erreichen Sie mit dem Kontextmenü-Eintrag Format|Spalten ausblenden des Spaltenkopfes oder mit der VBA-Eigenschaft Column Width. Vergessen Sie nicht, die Eigenschaft Standardansicht auf Datenblatt einzustellen und die Spaltenbreiten und Schriftgröße Ihren Bedürfnissen anzupassen. Speichern Sie das Formular unter dem Namen sfmProjekte (siehe Abbildung 4.36).
Abbildung 4.36: Das Unterformular zur Anzeige der Projekte in der Entwurfsansicht
Erstellen des Hauptformulars Das Hauptformular basiert auf der Tabelle tblKunden und zeigt alle darin enthaltenen Felder an. Ordnen Sie die Felder wie in Abbildung 4.37. Anschließend fügen Sie das Unterformular in das Hauptformular ein, indem Sie dieses aus dem Navigationsbereich in den Entwurf des Hauptformulars ziehen. Damit das Unterformular nur die Projekte des im Hauptformular angezeigten Kunden ausgibt, müssen zwei Eigenschaften des Unterformularsteuerelements entsprechend eingestellt werden, was beim Einfügen des Unterformulars normalerweise automatisch geschieht. Die beiden Eigenschaften heißen Verknüpfen von und Verknüpfen nach und erwarten den Namen des Fremdschlüsselfeldes der Datensatzquelle des Unterformulars und den Namen des Primärschlüsselfeldes der Datensatzquelle des Hauptformulars. Diese beiden Felder haben üblicherweise den gleichen Namen – in diesem Fall KundeID. Die Verknüpfungsfelder müssen nicht zwangsläufig mit dem Primärschlüsselfeld des Hauptformulars und dem Fremdschlüsselfeld des Unterformulars gefüllt werden. Es gibt auch Fälle, in denen beispielsweise der Wert eines Kombinationsfeldes im Hauptformular zur Auswahl der passenden Datensätze im Unterformular he rangezogen wird – hier würde man einfach den Namen des Kombinationsfeldes als Wert der Eigenschaft Verknüpfen nach verwenden.
235
Kapitel 4
Um die Eigenschaften einzusehen oder anzupassen, markieren Sie das Unterformular steuerelement – dieses ist keinesfalls mit dem Unterformular selbst zu verwechseln! Am einfachsten markieren Sie es, wenn Sie erst ein anderes Steuerelement des Hauptfor mulars markieren und dann einfach auf das Unterformular klicken. Wenn das Eigenschaftsfenster im Register Daten die beiden Eigenschaften anzeigt, liegen Sie richtig.
Abbildung 4.37: Haupt- und Unterformular zur Anzeige von Kunden und Projekten
Wenn Sie das Formular nun in der Formularansicht öffnen, sieht dieses etwa wie in Abbildung 4.38 aus. Beim Wechseln zu einem anderen Datensatz des Hauptformulars zeigt das Unterformular automatisch die passenden Projekte an.
Abbildung 4.38: Das fertige Formular zur Anzeige von Kunden und Projekten (»frmKunden«)
236
Formulare
Anlegen, bearbeiten und löschen im Detailformular Eine Projekttabelle wird in der Regel nicht mit derart wenig Feldern auskommen. Sie werden also normalerweise wie weiter oben in Abschnitt 4.5.3, »Einfache Daten in der Übersicht als Datenblatt«, ein Detailformular zur Anzeige der Projekte benötigen. Um dieses anzuzeigen, neue Projekte anzulegen oder Projekte zu löschen, können Sie die ebenfalls in dem genannten Abschnitt vorgestellten Schaltflächen einschließlich VBACode für das Formular frmKunden anpassen. Die Prozeduren zum Löschen eines Datensatzes und für die Anzeige zum Bearbeiten sind prinzipiell mit den oben beschriebenen identisch; bei Bedarf finden Sie diese im Klassenmodul des Formulars frmKunden. Eine wichtige Neuerung liefert das Neuanlegen per Detailformular. In der Routine, die durch das Anklicken der Schaltfläche cmdNeu ausgelöst wird, enthält die DoCmd.OpenAnweisung einen bislang nicht verwendeten Parameter. Mit OpenArgs kann man einen einzelnen Wert, auch Öffnungsparameter genannt, an das aufgerufene Formular übergeben. In diesem Fall handelt es sich dabei um den Wert des Feldes KundeID. Private Sub cmdNeu_Click() DoCmd.OpenForm "frmProjekteDetailansicht", DataMode:=acFormAdd, _ WindowMode:=acDialog, OpenArgs:=Me!KundeID Me.Requery End Sub Listing 4.13: Aufruf des Detailformulars mit Öffnungsargument
Der Hintergrund ist, dass neue Datensätze im Detailformular zur Anzeige der Projekte direkt mit dem richtigen Kunden ausgestattet werden sollen (siehe Abbildung 4.39). Damit das Detailformular den Öffnungsparameter auch auswertet, legen Sie dort für die Ereigniseigenschaft Beim Öffnen die folgende Prozedur an. Die Prozedur prüft, ob ein Öffnungsparameter übergeben wurde (das ist notwendig, da das Formular etwa zum Bearbeiten auch ohne Öffnungsparameter geöffnet werden soll – das Fehlen des Öffnungsarguments würde sonst einen Fehler auslösen) und weist diesen dann der Ei genschaft DefaultValue des Feldes KundeID zu. Dadurch wird der entsprechende Eintrag im Kombinationsfeld zur Anzeige der verknüpften Werte voreingestellt. Private Sub Form_Open(Cancel As Integer) If Not IsNull(Me.OpenArgs) Then Me!KundeID.DefaultValue = Me.OpenArgs End If End Sub Listing 4.14: Auswerten des Öffnungsarguments beim Öffnen eines Formulars
237
Kapitel 4
Abbildung 4.39: Das Kunden-Formular und die Detailansicht für Projekte mit einem neuen Daten satz (»frmKunden«, »frmProjekteDetailansicht«)
4.5.9 1:n-Beziehung per Listenfeld Wenn Sie ohnehin ein Formular für die Anzeige der Details der verknüpften Datensätze des Hauptformulars verwenden, müssen beziehungsweise sollten Sie im Hauptformular nicht die Möglichkeit zur Bearbeitung der verknüpften Daten bieten.In diesem Fall reicht ein Listenfeld zu deren Anzeige aus; außerdem bietet es die Möglichkeit, Datensätze zur Bearbeitung und zum Löschen auszuwählen – alles wie bereits in Abschnitt 4.5.4, »Daten in der Übersicht als Listenfeld« beschrieben. Eine Besonderheit gibt es jedoch: Die im Listenfeld angezeigten Daten hängen nun von dem im Hauptformular angezeigten Datensatz ab. Dementsprechend muss bei einem Wechsel des Hauptformulars auch der Inhalt des Listenfeldes aktualisiert werden (siehe Abbildung 4.40). Damit das Listenfeld jeweils die Projekte zu dem im Hauptformular angezeigten Kunden ausgibt, legen Sie für die Ereigniseigenschaft Beim Anzeigen die folgende Routine an. Diese stellt einen SQL-Ausdruck mit einer Abfrage zusammen, die alle anzuzeigenden Felder der Tabelle tblProjekte ausgibt und dabei nur jene Datensätze berücksichtigt, deren Feld KundeID mit dem Wert des Formularfeldes KundeID übereinstimmt. Diesen SQL-Ausdruck weist die Routine dann der Eigenschaft RowSource des Listenfeldes zu: Private Sub Form_Current() Dim strSQL As String strSQL = "SELECT ProjektID, Projekt, Startdatum, Enddatum " _ & "FROM tblProjekte WHERE KundeID = " & Me!KundeID
238
Formulare Me!lstProjekte.RowSource = strSQL End Sub Listing 4.15: Synchronisieren des Listenfeldinhalts mit dem Hauptformular
Abbildung 4.40: Darstellung einer 1:n-Beziehung per Listenfeld (»frmKundenListenfeld«)
Projekt anzeigen per Doppelklick Wenn Sie dem Benutzer zusätzlichen Komfort bieten möchten, legen Sie für das Listenfeld eine Prozedur an, die beim Doppelklick auf das Listenfeld den aktuell markierten Eintrag im Detailformular anzeigt. Dazu verwenden Sie die Ereigniseigenschaft Beim Doppelklicken: Private Sub lstProjekte_DblClick(Cancel As Integer) If IsNull(Me!lstProjekte) Then MsgBox "Bitte wählen Sie zunächst einen Datensatz aus." Exit Sub End If DoCmd.OpenForm "frmProjekteDetailansicht", DataMode:=acFormEdit, _ WindowMode:=acDialog, WhereCondition:="ProjektID = " & Me!lstProjekte Me!lstProjekte.Requery End Sub Listing 4.16: Anzeigen der Detailansicht eines Projekts
Die Routine hat exakt den gleichen Inhalt wie die Ereignisprozedur, die durch einen Klick auf die Schaltfläche Bearbeiten ausgelöst wird. Den enthaltenen Code extrahieren Sie am besten in eine separate neue Routine, die Sie von beiden Ereignisprozeduren aufrufen. Wie das aussieht, können Sie dem Klassenmodul des Formulars frmKundenListenfeld in der Beispieldatenbank \Kap_04\Formulare.accdb auf der Buch-CD entnehmen.
239
Kapitel 4
4.5.10 m:n-Beziehungen in Haupt- und Unterformular Wie bereits erwähnt, bieten Listenfelder zur Verwaltung von Daten aus m:n-Beziehungen nicht den Komfort, dass man die im Listenfeld angezeigten Daten direkt bearbeiten kann. Dafür ist eine andere Lösung wesentlich sinnvoller: Die Darstellung einer m:nBeziehung in einer Kombination aus Formular und Unterformular. Dabei zeigt das Hauptformular die Daten der einen Seite der Beziehung an, während das Unterformular die Daten der Verknüpfungstabelle und der anderen Seite der Bezie hung enthält. Das bekannteste Beispiel für eine solche Darstellung ist vermutlich das Formular Bestel lungen aus der Nordwind-Datenbank (siehe Abbildung 4.41).
Abbildung 4.41: Beispiel für die Verwaltung von Daten in einer m:n-Beziehung
Zu Demonstrationszwecken reicht eine einfache m:n-Beziehung mit den notwendigsten Feldern wie in Abbildung 4.42. Die beiden Tabellen tblProjekte und tblMitarbeiter werden über die Verknüpfungstabelle tblProjektzeiten miteinander verbunden. Interessant wird dieses Beispiel durch die Tatsache, dass auch die Verknüpfungstabelle einige Felder enthält – in diesem Fall sogar die wichtigsten. Die Verknüpfungstabelle dient dem Speichern der Zeiten, die die Mitarbeiter mit den je weiligen Projekten verbracht haben. Um diese Informationen nicht nur quantitativ, son dern auch qualitativ auswerten zu können, wird nicht nur die Zeit, sondern auch das Datum der Tätigkeit gespeichert.
240
Formulare
Beispieldatenbank: Die Tabellen tblProjekte, tblMitarbeiter und tblProjektzeiten, die Abfrage qryProjektzeiten sowie die Formulare frmProjektzeiten und sfmProjektzeiten finden Sie unter \Kap_04\Formulare.accdb. Für die Beziehungen legen Sie referentielle Integrität fest, da so sichergestellt ist, dass für jeden Eintrag in die Tabelle tblProjektzeiten ein Projekt und ein Mitarbeiter ausgewählt werden. Wer dieses Beispiel ausbauen möchte, wird natürlich die Projekt- und die Mitarbeiter tabelle noch um einige Felder erweitern. Aber auch die Verknüpfungstabelle tblProjektzeiten kann noch weitere wichtige Informationen speichern: etwa eine Kurzbeschreibung der jeweiligen Tätigkeit und eine Tätigkeitsart wie »Konzeption«, »Programmierung« oder »Test«.
Abbildung 4.42: Datenmodell der Beispieltabellen
Hauptformular der m:n-Beziehung Bevor Sie sich der Erstellung des Hauptformulars zuwenden, müssen Sie bei m:nBeziehungen jeweils zunächst überlegen, von welcher der beiden Tabellen Sie nur je einen Datensatz im Hauptformular anzeigen möchten und welche Tabelle die Datensätze für das Unterformular liefert. Im vorliegenden Fall sind beide Varianten interessant: Man könnte sich sowohl zu jedem Projekt die Zeiten ansehen, die die einzelnen Mitarbeiter damit verbracht haben, andererseits ist es vielleicht nicht ganz uninteressant, womit der eine oder andere Mitarbeiter seine Zeit verbringt. Wichtiger für die Auswertung von Projekten ist sicher die erste Variante. Das Haupt formular zeigt also die Daten der Tabelle tblProjekte an, die dementsprechend auch als Datensatzquelle des Formulars dient. Wenn Sie aus der Feldliste die beiden Felder Pro jektID und Projekt in den Detailbereich des Formulars ziehen, ist der erste Teil der Arbeit bereits erledigt (siehe Abbildung 4.43).
241
Kapitel 4
Abbildung 4.43: Hauptformular der m:n-Beziehung (»frmProjektzeiten«)
Als Nächstes fügen Sie nun noch das Unterformular zur Anzeige der Mitarbeiter und der jeweiligen Projektzeiten hinzu.
Unterformular der m:n-Beziehung Das Unterformular soll anzeigen, welcher Mitarbeiter an welchem Datum wie viele Stunden mit einem Projekt verbracht hat. Dazu legen Sie für das Unterformular die Abfrage aus Abbildung 4.44 als Datensatzquelle an.
Abbildung 4.44: Datensatzquelle des Unterformulars der m:n-Beziehung
Der Aufbau dieser Abfrage ist für alle Unterformulare von Formularen zur Darstellung von m:n-Beziehungen gleich. Die Abfrage enthält jeweils die Verknüpfungstabelle und
242
Formulare
die Tabelle, deren Daten nicht im Hauptformular angezeigt werden – in diesem Fall also die Tabellen tblProjektzeiten und tblMitarbeiter. Auch für die Felder gibt es feste Regeln. Sie benötigen immer folgende Felder: Fremdschlüsselfeld der Verknüpfungstabelle zur Tabelle, die im Hauptformular angezeigt wird (hier ProjektID). Fremdschlüsselfeld der Verknüpfungstabelle zur anderen Tabelle der m:n-Beziehung (hier MitarbeiterID). Dieses Feld wird in der Regel als Kombinationsfeld ausgeführt, damit ein Datensatz dieser Tabelle ausgewählt werden kann. Alle sonstigen Felder der Verknüpfungstabelle und der im Unterformular anzuzeigenden Tabelle, die im Unterformular angezeigt werden sollen (hier die Felder Datum und Zeit der Tabelle tblProjektzeiten und Telefon der Tabelle tblMitarbeiter). Im Unterformular stellen Sie dann die Eigenschaft Datensatzherkunft auf die soeben erstellte Abfrage ein. Im Detailbereich des Unterformulars finden nicht alle Felder der zugrunde liegenden Abfrage Platz: Das Fremdschlüsselfeld, das auf den entsprechenden Datensatz des Hauptformulars verweist, muss nicht angezeigt werden. Lediglich das Feld zur Auswahl des Mitarbeiters sowie einige weitere Felder sollen später sichtbar sein (siehe Abbildung 4.45).
Abbildung 4.45: Das Unterformular der m:n-Beziehung in der Entwurfsansicht
Datenblatt- oder Endlosansicht? Da das Unterformular mehrere Datensätze anzeigen soll, stehen die Datenblatt- und die Endlosansicht zur Verfügung. Wenn Sie keine besonderen Ansprüche an das Layout der Steuerelemente im Unterformular haben und außer Text- und Kombinationsfeldern keine Steuerelemente benötigen, sind Sie mit der Datenblattansicht gut bedient. In diesem Beispiel ist das der Fall; stellen Sie daher die Eigenschaft Standardansicht auf Datenblattansicht ein.
243
Kapitel 4
Das Unterformular sieht in der Datenblattansicht nun wie in Abbildung 4.46 aus. Wenn Sie bereits Daten in die Tabelle tblMitarbeiter eingegeben haben oder die Tabellen und Formulare aus der Beispieldatenbank verwenden, können Sie das Unterformular bereits testen. Die Auswahl eines Mitarbeiters per Kombinationsfeld sorgt automatisch für das Füllen der übrigen Felder, die Sie aus der Tabelle tblMitarbeiter in das Unterformular übernommen haben. Fehlt noch eine Eingabe in die beiden Felder Datum und Zeit und das Speichern des Datensatzes. Letzteres schlägt wegen der Regeln zur referentiellen Integrität freilich fehl, da noch kein Wert für das Feld ProjektID der Abfrage qryProjektzeiten angegeben wurde – was ja auch gar nicht geht, da das Feld überhaupt nicht im Formular angezeigt wird.
Abbildung 4.46: Das Unterformular in der Datenblattansicht
Wie sollen Sie also Datensätze im Unterformular anlegen, wenn Sie gar keinen Zugriff auf eines der wichtigsten Felder haben? Ganz einfach: Sie automatisieren die Eingabe der Werte für dieses Feld. Das Unterformular ist ja eigentlich für den Einsatz im Haupt formular frmProjektzeiten gedacht, in das Sie es nun einfügen können. Es gibt mehrere Möglichkeiten, ein Formular als Unterformular in ein anderes Formular einzubauen. Am schnellsten geht es aber vermutlich, indem Sie das Ziel formular in der Entwurfsansicht öffnen und das zukünftige Unterformular aus dem Navigationsbereich einfach an die gewünschte Stelle ziehen. Nachdem Sie das Unterformular in das Hauptformular eingefügt haben, werfen Sie einen Blick auf die Eigenschaften Verknüpfen von und Verknüpfen nach (siehe Abbildung 4.47). Dort sollte Access beim Einfügen des Unterformulars automatisch jeweils den Wert ProjektID eingetragen haben. Diese Einstellung gewährleistet Folgendes: Das Feld ProjektID wird bei neuen Datensätzen im Unterformular automatisch mit dem entsprechenden Wert des Hauptformulars gefüllt.
244
Formulare
Das Unterformular zeigt nur Datensätze an, deren ProjektID dem entsprechenden Wert des Hauptformulars entspricht.
Abbildung 4.47: Verknüpfen von Haupt- und Unterformular
Ein Wechsel in die Formularansicht demonstriert die Funktion des Formulars. Sie können nun einen neuen Datensatz im Hauptformular anlegen und im Unterformular die gewünschten Projektzeiten eintragen. Dazu wählen Sie dort einfach den Mitarbeiter aus und geben Datum und Zeit ein – um den Inhalt des Feldes ProjektID kümmert sich Access selbst (siehe Abbildung 4.48).
Abbildung 4.48: m:n-Beziehung im Einsatz
245
Kapitel 4
4.5.11 m:n-Beziehungen per Listenfeld Die Verwaltung von Daten aus m:n-Beziehungen erfolgt verhältnismäßig selten mit Hilfe von Listenfeldern. Der Grund ist, dass Listenfelder keine direkte Bearbeitung der angezeigten Daten ermöglichen. Daher sind Listenfelder eher zum übersichtlichen Zu weisen von Datensätzen der n-Seite zu den Datensätzen der m-Seite einer Beziehung einsetzbar. Einsatzzwecke für die Anwendung von Listenfeldern in Zusammenhang mit m:n-Be ziehungen gibt es genug: Die Verwaltung von Verteilerlisten, die das Zuordnen von Empfängern zu einer Publikation ermöglichen, Benutzer und Benutzergruppen (wie im obigen Beispiel) oder Fahrzeuge und Sonderausstattungen. Das folgende Formular soll anhand des Beispiels der Fahrzeuge und Ausstattungsmerk male hergeleitet werden. Zum Nachvollziehen benötigen Sie drei Tabellen: Die beiden Tabellen tblFahrzeuge und tblAusstattungen werden über die Tabelle tblFahrzeugeAusstattungen miteinander verknüpft (siehe Abbildung 4.49). Für die beiden Beziehungen legen Sie jeweils referentielle Integrität mit Löschweitergabe fest.
Abbildung 4.49: Datenmodell der Beispieltabellen
Beispieldatenbank: Die Tabellen tblFahrzeuge, tblFahrzeugeSonderausstattungen, tblAus stattungen und das Formular frmFahrzeuge finden Sie unter \Kap_04\Formulare.accdb.
Das Formular zur Verwaltung der enthaltenen Daten besitzt als Datensatzquelle die Tabelle tblFahrzeuge und enthält die folgenden Steuerelemente (siehe Abbildung 4.50): FahrzeugID und Fahrzeug: Textfelder, gebunden an die Datensatzquelle des Formulars. lstVorhandeneAusstattung: Zeigt alle Datensätze der Tabelle tblAusstattungen an, die über die Tabelle tblFahrzeugeAusstattungen mit der Tabelle tblFahrzeuge verknüpft sind.
246
Formulare
lstNichtVorhandeneAusstattung: Zeigt alle Datensätze der Tabelle tblAusstattungen an, die das Listenfeld lstVorhandeneAusstattung nicht anzeigt. cmdEntfernen: Verschiebt das aktuell im linken Listenfeld markierte Ausstattungs merkmal in das rechte Listenfeld. cmdAlleEntfernen: Verschiebt alle Einträge des linken Listenfeldes in das rechte Lis tenfeld. cmdHinzufuegen: Verschiebt das aktuell im rechten Listenfeld markierte Ausstattungs merkmal in das linke Listenfeld. cmdAlleHinzufuegen: Verschiebt alle Einträge des rechten Listenfeldes in das linke Listenfeld. Zusätzlich zu den bereits erwähnten Funktionen soll ein Doppelklick auf einen Eintrag eines der Listenfelder den betroffenen Eintrag in das jeweils andere Listenfeld verschieben.
Abbildung 4.50: Entwurfsansicht des Formulars zum Festlegen der Ausstattungsmerkmale eines Fahrzeugs
Datensatzherkunft der Listenfelder Die Beschreibung der Steuerelemente des Formulars hat den Inhalt der beiden Listenfel der bereits scharf umrissen. Da der Inhalt des rechten Listenfeldes vom Inhalt des linken Listenfeldes abhängt, beginnen Sie mit dem linken Listenfeld. Es soll alle Datensätze der Tabelle tblAusstattungen enthalten, die über die Tabelle tblFahrzeugeAusstattungen mit der Tabelle tblFahrzeuge verknüpft sind. Das aktuell angezeigte
247
Kapitel 4
Fahrzeug lässt sich über das Feld FahrzeugID identifizieren. Da die Verknüpfungstabelle den entsprechenden Wert bereits enthält, besteht die Abfrage für die Datensatzherkunft aus den beiden Tabellen tblFahrzeugeAusstattungen und tblAusstattungen (siehe Abbil dung 4.51). Das Listenfeld soll die Bezeichnung des Ausstattungsmerkmals anzeigen, aber das Feld AusstattungID als gebundene Spalte verwenden; das Feld FahrzeugID ist nur als Kriterium vorgesehen und muss gar nicht angezeigt werden. Damit das Listenfeld die erste Spalte mit dem Feld AusstattungID ausblendet, stellen Sie die Eigenschaften Spaltenanzahl und Spaltenbreite auf die Werte 2 beziehungsweise 0cm ein.
Wenn Sie die Tabellen aus der Beispieldatenbank verwenden oder bereits selbst Beispiel daten eingegeben haben, können Sie nun ausprobieren, ob das Listenfeld die gewünschten Daten anzeigt. Beim ersten angezeigten Fahrzeug sollte dies funktionieren, beim Blättern zum nächsten Fahrzeug verändert sich der Inhalt des Listenfeldes allerdings nicht. Das liegt daran, dass seine Datensatzherkunft nicht aktualisiert wird. Legen Sie also die folgende Prozedur an, die durch das Ereignis Beim Anzeigen des Formulars ausgelöst wird: Private Sub Form_Current() 'Aktualisieren des linken Listenfeldes Me!lstVorhandeneAusstattung.Requery End Sub Listing 4.17: Diese Prozedur sorgt für die Aktualisierung des Listenfeldes beim Datensatz wechsel
248
Formulare
Legen Sie nun die Abfrage an, die als Datensatzherkunft für das zweite Listenfeld dient. Das Listenfeld soll alle Ausstattungen anzeigen, die nicht zu einem Fahrzeug gehören und die nicht über die Tabelle tblFahrzeugeAusstattungen mit der Tabelle tblFahrzeuge verknüpft sind. Das sind alle Datensätze der Tabelle tblAusstattungen, die nicht im linken Listenfeld angezeigt werden – also formulieren Sie die Abfrage auch einfach so. Diese enthält zunächst lediglich die beiden Felder der Tabelle tblAusstattungen. Den Bezug zu der Abfrage, die als Datensatzherkunft des linken Listenfeldes dient, erstellen Sie über das Kriterium für das Feld AusstattungID: Dort schließen Sie über das Schlüsselwort NOT IN alle Ausstattungen der Abfrage des linken Listenfeldes aus. Allerdings müssen Sie die dortige Abfrage noch ein wenig bearbeiten, da die für IN-Bedingungen verwendeten Unterabfragen nur ein Feld ausgeben dürfen. Den in Abbildung 4.52 nicht komplett zu erkennenden Ausdruck für die Bedingung finden Sie hier: Nicht In (SELECT tblAusstattungen.AusstattungID FROM tblAusstattungen INNER JOIN tblFahrzeugeAusstattungen ON tblAusstattungen.AusstattungID = tblFahrzeugeAusstattungen.AusstattungID WHERE tblFahrzeugeAusstattungen. FahrzeugID=[Forms]![frmFahrzeuge]![FahrzeugID])
Abbildung 4.52: Datensatzherkunft des Listenfeldes der nicht vorhandenen Ausstattungsmerk male (»frmFahrzeuge«)
Nun zeigen die beiden Listenfelder bereits die gewünschten Daten an (siehe Abbil dung 4.53). Als Nächstes bringen Sie ein wenig Leben in die Schaltflächen und Listenfel der, um das Formular zum Hinzufügen und Entfernen von Datensätzen in der Tabelle tblFahrzeugeAusstattungen verwenden zu können. Damit auch das rechte Listenfeld beim Datensatzwechsel aktualisiert wird, ergänzen Sie die Ereignisprozedur Form_Current wie im folgenden Listing: Private Sub Form_Current() 'Aktualisieren des linken Listenfeldes Me!lstVorhandeneAusstattung.Requery
249
Kapitel 4 'Aktualisieren des rechten Listenfeldes Me!lstNichtVorhandeneAusstattung.Requery End Sub Listing 4.18: Aktualisieren der Listenfelder
Abbildung 4.53: Die Listenfelder zeigen bereits die richtigen Daten an (»frmFahrzeuge«)
Hinzufügen eines Ausstattungsmerkmals Das Formular bietet zwei Möglichkeiten zum Hinzufügen eines einzelnen Ausstattungs merkmals zu einem Fahrzeug: per Doppelklick auf den gewünschten Eintrag im Lis tenfeld lstNichtVorhanden oder durch Markieren des Eintrags im selben Listenfeld und anschließendes Betätigen der Schaltfläche cmdHinzufuegen. Da in beiden Varianten die gleiche Aktion ausgelöst werden soll, legen Sie diese in einer separaten Prozedur an, die von der jeweiligen Ereignisprozedur der beiden Steuerele mente aus aufgerufen wird. Für das Anlegen des neuen Datensatzes in der Tabelle tblFahrzeugeAusstattungen müssen Sie die zukünftigen Werte der Felder FahrzeugID und AusstattungID kennen. Diese ermitteln Sie in den beiden Ereignisprozeduren Beim Klicken der Schaltfläche cmdHinzufuegen und Beim Doppelklicken des Listenfeldes lstNichtVorhanden und rufen von dort aus die Prozedur Hinzufuegen auf. Sollte einer der beiden Werte nicht vorhanden sein, was der Fall ist, wenn entweder kein hinzuzufügender Eintrag des Listenfeldes markiert ist oder das Formular einen neu angelegten Fahrzeug-Datensatz enthält, wird die Hinzufuegen-Prozedur nicht aufgerufen. Private Sub cmdHinzufuegen_Click() If Not IsNull(Me!FahrzeugID) And _ Not IsNull(Me!lstNichtVorhandeneAusstattung) Then
250
Formulare Hinzufuegen Me!FahrzeugID, Me!lstNichtVorhandeneAusstattung End If End Sub Private Sub lstNichtVorhandeneAusstattung_DblClick(Cancel As Integer) If Not IsNull(Me!FahrzeugID) And _ Not IsNull(Me!lstNichtVorhandeneAusstattung) Then Hinzufuegen Me!FahrzeugID, Me!lstNichtVorhandeneAusstattung End If End Sub Listing 4.19: Aufrufen der Prozedur zum Anlegen eines neuen Datensatzes in der Tabelle tblFahrzeugeAusstattungen
Die Prozedur Hinzufuegen erwartet als Parameter die Fahrzeug-ID und die AusstattungsID des zu erstellenden Datensatzes. Nach der Durchführung der Aktionsabfrage aktualisiert die Prozedur die beiden Listenfelder. Private Sub Hinzufuegen(lngFahrzeugID As Long, _ lngAusstattungID As Long) Dim db As DAO.Database Dim strSQL As String Set db = CurrentDb 'Zusammen der Anfügeabfrage strSQL = "INSERT INTO tblFahrzeugeAusstattungen" _ & "(FahrzeugID, AusstattungID) VALUES(" _ & lngFahrzeugID & ", " & lngAusstattungID & ")" 'Ausführen der Anfügeabfrage db.Execute strSQL 'Aktualisieren der Listenfelder Me!lstVorhandeneAusstattung.Requery Me!lstNichtVorhandeneAusstattung.Requery Set db = Nothing End Sub Listing 4.20: Prozedur zum tblFahrzeugeAusstattungen
Hinzufügen
eines
Datensatzes
zur
Tabelle
Entfernen eines Ausstattungsmerkmals Zum Entfernen eines Ausstattungsmerkmals eines Fahrzeugs gibt es ebenfalls zwei Möglichkeiten. Entweder Sie markieren den zu entfernenden Datensatz und klicken auf die Schaltfläche zum Entfernen eines Datensatzes oder Sie klicken doppelt auf den zu entfernenden Eintrag im Listenfeld. Der Aufbau der dadurch ausgelösten Prozeduren ist identisch mit dem der Prozeduren zum Hinzufügen eines Ausstattungsmerkmals
251
Kapitel 4
und die Prozedur Entfernen ist das Pendant zur Prozedur Hinzufuegen. Lediglich die ver wendete SQL-Anweisung hat ein anderes Aussehen: Private Sub Entfernen(lngFahrzeugID As Long, lngAusstattungID As Long) Dim db As DAO.Database Dim strSQL As String Set db = CurrentDb strSQL = "DELETE FROM tblFahrzeugeAusstattungen " _ & "WHERE FahrzeugID = " & lngFahrzeugID _ & " AND AusstattungID = " & lngAusstattungID db.Execute strSQL Me!lstVorhandeneAusstattung.Requery Me!lstNichtVorhandeneAusstattung.Requery Set db = Nothing End Sub Listing 4.21: Prozedur zum Entfernen eines Ausstattungsmerkmals eines Fahrzeugs
Hinzufügen oder Entfernen aller Ausstattungsmerkmale Die beiden Schaltflächen mit dem doppelten Kleiner- beziehungsweise Größer-Zeichen bewegen jeweils alle Ausstattungen eines Fahrzeugs von einem Listenfeld ins andere. Aus Datensicht bedeutet das, dass entweder für jedes Ausstattungsmerkmal in Kombi nation mit dem aktuell angezeigten Fahrzeug ein Datensatz in der Tabelle tblFahrzeuge Ausstattungen angelegt wird oder dass alle vorhandenen Einträge für ein Fahrzeug entfernt werden. Das Hinzufügen aller Ausstattungsmerkmale erledigt die Prozedur, die durch das Ereignis Beim Klicken der Schaltfläche cmdAlleHinzufuegen ausgelöst wird. Im Gegensatz zu den Prozeduren zum Hinzufügen eines einzelnen Datensatzes braucht hier nicht geprüft zu werden, ob ein Eintrag des Listenfeldes markiert ist: Private Sub cmdAlleHinzufuegen_Click() Dim db As DAO.Database Dim strSQL As String If Not IsNull(Me!FahrzeugID) Then Set db = CurrentDb strSQL = "INSERT INTO tblFahrzeugeAusstattungen " _ & "SELECT " & Me!FahrzeugID & " AS FahrzeugID, " _ & "AusstattungID FROM tblAusstattungen" db.Execute strSQL Me!lstVorhandeneAusstattung.Requery Me!lstNichtVorhandeneAusstattung.Requery Set db = Nothing End If End Sub Listing 4.22: Hinzufügen aller vorhandenen Ausstattungsmerkmale zu einem Fahrzeug
252
Formulare
Das Entfernen aller Ausstattungen eines Fahrzeugs geschieht in der folgenden Prozedur. Der wesentliche Unterschied zur vorherigen Prozedur liegt in der SQL-Anweisung: Private Sub cmdAlleEntfernen_Click() Dim db As DAO.Database Dim strSQL As String If Not IsNull(Me!FahrzeugID) Then Set db = CurrentDb strSQL = "DELETE FROM tblFahrzeugeAusstattungen " _ & "WHERE FahrzeugID = " & Me!FahrzeugID db.Execute strSQL Me!lstVorhandeneAusstattung.Requery Me!lstNichtVorhandeneAusstattung.Requery Set db = Nothing End If End Sub Listing 4.23: Entfernen aller Ausstattungsmerkmale eines Fahrzeugs
4.5.12 Reflexive Beziehungen Für die Darstellung der Daten aus reflexiven Beziehungen bietet sich das TreeviewSteuerelement an. Mit ein wenig Fantasie lassen sich hierarchische Daten zwar auch in Listenfeldern oder gar in Textfeldern anzeigen, aber ihre Anwendung macht nicht wirklich Spaß. Daher finden Sie nachfolgend eine Anleitung zum Erstellen eines Formulars mit einen Treeview-Steuerelement zur Darstellung der Hierarchie von Mitarbeitern und ihren Vorgesetzten. Eine ausführliche Vorstellung der Funktionen und Möglichkeiten des Treeview-Steuerele ments finden Sie in Kapitel 5, »Steuerelemente«. Das Treeview-Steuerelement legen Sie über den Dialog ActiveX-Steuerelement einfügen an, den Sie mit dem Ribbon-Eintrag Entwurf|Steuerelemente|ActiveX-Steuerelement einfügen aufrufen (siehe Abbildung 4.54). Benennen Sie das neu hinzugefügte Steuerelement ctl Treeview. Anschließend können Sie direkt den für das Füllen des Treeview-Steuerelements benötigten Code schreiben.
Füllen des Treeview-Steuerelements Zum Speisen des Treeview-Steuerelements mit Daten aus einer Tabelle mit einer reflexiven Beziehung benötigen Sie eine rekursive Prozedur. Das bedeutet, dass sich die Pro zedur immer wieder selbst aufruft, solange die Datensatzquelle tiefer verschachtelte Elemente enthält, und erst dann die folgenden Elemente der übergeordneten Ebenen abarbeitet. Den Start macht allerdings eine herkömmliche Routine. Sie legt eine modulweit gültige Variable namens objTreeview mit einem Verweis auf das Steuerelement an.
253
Kapitel 4
Abbildung 4.54: Einfügen des Treeview-Steuerelements
Anschließend durchläuft die Routine alle Datensätze eines Recordsets mit allen Mitar beitern, die keinen Vorgesetzten haben, und fügt je einen Knoten zum Treeview-Steuer element hinzu. Dabei legt sie als Beschriftung den Nachnamen und den Vornamen des aktuellen Mitarbeiters an. Außerdem verwendet sie die Key-Eigenschaft des neuen Ele ments, um den Wert des Feldes MitarbeiterID des aktuellen Datensatzes hinzuzufügen. Da Werte der Eigenschaft Key mit einem Buchstaben beginnen müssen, stellt die Routine ein »x« voran. Nach dem Anlegen des Knotens ruft sie die Routine zum Anlegen der untergeordneten Mitarbeiter auf – dazu weiter unten mehr. Im Anschluss daran werden weitere Datensätze – soweit vorhanden – auf die gleiche Art abgearbeitet. Dim objTreeview As TreeView Dim db As DAO.Database Private Sub Form_Load() Dim rst As DAO.Recordset Dim objNode As Node Dim objListItem As ListItem Set objTreeview = Me!ctlTreeview.Object objTreeview.Nodes.Clear Set db = CurrentDb Set rst = db.OpenRecordset( _ "SELECT * FROM tblMitarbeiterMitVorgesetzten " _ & "WHERE VorgesetzterID IS NULL", dbOpenDynaset) Do While Not rst.EOF Set objNode = objTreeview.Nodes.Add With objNode .Text = rst!Nachname & ", " & rst!Vorname .Key = "x" & rst!MitarbeiterID
254
Formulare End With AddChilds rst!MitarbeiterID rst.MoveNext Loop End Sub Listing 4.24: Diese Routine fügt die Elemente der ersten Ebene in das Treeview-Steuerelement ein
Die Routine AddChilds erwartet die MitarbeiterID des übergeordneten Mitarbeiters als Parameter. Per OpenRecordset legt die Routine eine Datensatzgruppe an, die alle Mitar beiter-Datensätze enthält, deren Vorgesetzter der Mitarbeiter mit der übergebenen Mit arbeiterID ist. Anschließend wird für jeden »Untergebenen« ein Element unterhalb des Vorgesetzten angelegt. Um das Element des Vorgesetzten zu ermitteln, setzt die Routine den Buch staben »x« und die MitarbeiterID zu dem Wert zusammen, den die aufrufende Routine als Key für den übergeordneten Mitarbeiter angelegt hat, und findet so das passende Element. Für jeden »Untergebenen« wird diese Routine rekursiv aufgerufen, um auch die »Unter gebenen« der »Untergebenen« zu finden und entsprechende Elemente anzulegen. Private Dim Dim Set
Sub AddChilds(lngMitarbeiterID As Long) rst As DAO.Recordset objNode As Node rst = db.OpenRecordset( _ "SELECT * FROM tblMitarbeiterMitVorgesetzten " _ & "WHERE VorgesetzterID = " _ & lngMitarbeiterID, dbOpenDynaset) Do While Not rst.EOF Set objNode = objTreeview.Nodes.Add(Relative:="x" _ & lngMitarbeiterID, Relationship:=tvwChild) With objNode .Text = rst!Nachname & ", " & rst!Vorname .Key = "x" & rst!MitarbeiterID End With AddChilds rst!MitarbeiterID rst.MoveNext Loop End Sub
Listing 4.25: Rekursiver Teil der Prozeduren zum Füllen des Treeview-Steuerelements
Auf diese Weise werden alle Datensätze durchlaufen und dem Treeview-Steuerelement hinzugefügt.
255
Kapitel 4
Anzeigen des Detailformulars zu einem Element Natürlich sollen Sie auch etwas mit dem gefüllten Treeview anfangen können. Daher finden Sie nachfolgend eine kleine Routine, die nach einem Klick auf eine dafür vorgesehene Schaltfläche ein Detailformular mit den Daten des aktuell im Treeview-Steuerelement markierten Eintrags anzeigt: Private Sub cmdDetails_Click() Dim lngMitarbeiterID As Long lngMitarbeiterID = CLng(Mid(objTreeview.SelectedItem.Key, 2)) DoCmd.OpenForm "frmMitarbeiterDetail", _ WhereCondition:="MitarbeiterID = " & lngMitarbeiterID End Sub Listing 4.26: Code zum Öffnen eines Detailformulars zum aktuellen Element des TreeviewSteuerelements
Das gefüllte Treeview-Steuerelement mit Detailformular für einen der Einträge sieht wie in Abbildung 4.55 aus.
Abbildung 4.55: Treeview-Steuerelement mit Detailformular (»frmMitarbeiterMitVorgesetzten«, »frmMitarbeiterDetail«)
4.6 Von Formular zu Formular Weiter oben haben Sie bereits einige Möglichkeiten kennen gelernt, mit denen Sie von einem Formular aus ein weiteres Formular aufrufen und diesem bestimmte Informationen übergeben können.
256
Formulare
Bei der Anwendung dieser Möglichkeiten sind einige Voraussetzungen zu beachten, damit Sie länger Spaß an den auf diese Weise »verknüpften« Formularen haben. Die wichtigste Regel ist: Sorgen Sie für möglichst wenig Abhängigkeiten zwischen aufrufendem und aufgerufenem Formular. Das bedeutet in diesem Fall, dass Sie die Abhängigkeit erstens unidirektional auslegen und zweitens an bestimmten Punkten konzentrieren sollten – etwa auf das Öffnen und das Schließen des aufgerufenen For mulars. Zur besseren Verständlichkeit heißt das aufrufende Formular in den nächsten Abschnit ten »Parent« und das aufgerufene Formular »Child«. Unidirektionale Abhängigkeit bedeutet, dass zwar das Parent-Formular das ChildFormular kennen muss, aber nicht umgekehrt. In der Praxis sieht das folgendermaßen aus: Das Parent-Formular kennt den Namen des Child-Formulars und ruft dieses da rüber auf. Das Parent-Formular kennt die Datensatzquelle des Child-Formulars und weiß, wie eine Bedingung zur Einschränkung der Datensatzquelle aussehen muss. Das Parent-Formular weiß, welche Werte es dem Child-Formular mit dem Öffnungsargument übergeben kann. Umgekehrt weiß das Child-Formular nichts vom Parent-Formular – es wertet lediglich die vom Parent-Formular übergebenen Informationen aus, ist aber nicht von der Liefe rung dieser Informationen abhängig. Andersherum soll das Parent-Formular neben den beim Aufruf übergebenen Infor mationen nach dem Schließen des Child-Formulars Werte von dort auslesen können. Daher darf das Child-Formular nicht geschlossen, sondern nur unsichtbar gemacht werden. Außer diesen zwei Kontakten – Parameterübergabe beim Aufruf und Auslesen des Child-Formulars vor dem Schließen – finden im Optimalfall keine Kontakte statt. Das heißt insbesondere, dass das Child-Formular nicht auf das Parent-Formular zugreift. Es gibt natürlich die Möglichkeit, Variablen, die zwischen Parent- und Child-Formular hin- und hergereicht werden sollen, in globalen Variablen zu speichern; man könnte auch vom Child-Formular aus lesend und schreibend auf das Parent-Formular zugreifen. Ersteres bedingt, dass beide Formulare die globalen Variablen kennen müssen, und Letzteres, dass das Parent-Formular das Child-Formular kennen muss und umgekehrt und diese damit voneinander abhängig sind. Seit Access 2007 gibt es mit den TempVars noch eine weitere Möglichkeit. Nähere Informationen dazu finden Sie in Kapitel 1, »Warum Access 2007?«.
257
Kapitel 4
Formulare aufrufen und vor dem Schließen auslesen VBA-technisch sieht das wie folgt aus. Der Aufruf des Child-Formulars erfolgt mit der DoCmd.OpenForm-Methode. Damit das Parent-Formular das Child-Formular nach dem Ausblenden und vor dem Schließen auslesen kann, muss das Child-Formular als modaler Dialog geöffnet werden. Das bedeutet, dass keine anderen Aktionen in der Access-Anwendung möglich sind, solange das Child-Formular geöffnet ist. Auf diese Weise verhindern Sie Wechselwirkungen zwischen Child-Formular und anderen Elementen der Benutzeroberfläche. Um ein Formular mit der OpenForm-Methode als modalen Dialog zu öffnen, verwendet man den Parameter WindowMode mit dem Wert acDialog: DoCmd.OpenForm "frmChild", WindowMode:=acDialog
Wichtigster Nebeneffekt des Öffnens als modaler Dialog ist, dass auch die aufrufende Prozedur angehalten wird. Diese läuft erst dann weiter, wenn das Child-Formular durch Setzen der Eigenschaft Visible auf den Wert True oder durch Schließen den Fokus verliert. Die aufrufende Routine weiß dann bei der nächsten Zeile zumindest sicher, dass das Child-Formular entweder geschlossen oder ausgeblendet ist. Erstrebenswert ist natürlich Letzteres, denn sonst könnten Sie von dort keine Informationen mehr erlangen. Also sorgen Sie dafür, dass das Formular nicht auf herkömmlichem Wege geschlossen, sondern nur über eine spezielle Schaltfläche ausgeblendet werden kann. Die passende Er eignisprozedur der Schaltfläche mit dem Namen cmdOK sieht dann folgendermaßen aus: Private Sub cmdOK_Click() Me.Visible = True End Sub Listing 4.27: Ausblenden eines Formulars
In der aufrufenden Prozedur sollten Sie auf jeden Fall prüfen, ob das Formular noch geöffnet ist. Dazu können Sie die Eigenschaft IsLoaded des entsprechenden Elements der AllForms-Auflistung verwenden: If CurrentProject.AllForms("").IsLoaded = True Then
Aber auch das verschafft noch keine Sicherheit, denn das Formular könnte ja auch in der Entwurfsansicht geöffnet sein. Also folgt eine weitere Prüfung: If CurrentProject.AllForms("").CurrentView = _ acCurViewFormBrowse Then …
Anschließend kann das Child-Formular in Ruhe ausgelesen werden, bevor es geschlossen wird.
258
Formulare
Beispiel für das Aufrufen eines weiteren Formulars Wenn Sie ein Kombinationsfeld etwa für die Auswahl der Kategorie eines Artikels verwenden, sollten Sie die Möglichkeit bieten, leicht weitere Kategorien anzulegen. Im folgenden Beispiel wird bei der Eingabe eines Eintrags, der in der Datensatzherkunft eines Kombinationsfeldes noch nicht vorhanden ist (siehe Abbildung 4.56), ein weiteres Formular zum Anlegen eines neuen Datensatzes in der Kategorien-Tabelle angezeigt (siehe Abbildung 4.57).
Abbildung 4.56: Anlegen einer neuen Kategorie (»frmArtikel«) …
Abbildung 4.57: … in einem eigenen Formular (»frmKategorien«)
Ob der Benutzer einen noch nicht in der Datensatzherkunft eines Kombinationsfeldes vorhandenen Datensatz eingefügt hat, stellen Sie daran fest, dass das Ereignis Bei nicht in Liste ausgelöst wird. Dieses Ereignis machen Sie sich zu Nutze und fragen darin den Benutzer, ob er den neuen Eintrag tatsächlich anlegen möchte – vielleicht hat er sich ja auch nur vertippt. Anderenfalls öffnet die Routine das Formular frmKategorien (siehe Abbildung 4.57) mit einem neuen Datensatz, der automatisch mit der neuen Kategorie gefüllt wird – dazu später mehr. Nach der Eingabe und dem Ausblenden des Formulars prüft die Routine, ob das Formular noch in der Formularansicht geöffnet ist, und liest gegebenenfalls die KategorieID des neuen Eintrags ein. Nach dem Schließen des aufgerufenen Formulars,
259
Kapitel 4
das zu diesem Zeitpunkt nur ausgeblendet, aber nicht geschlossen ist, stellt die Routine das Kombinationsfeld auf den neuen Wert ein. Private Sub KategorieID_NotInList(NewData As String, Response As Integer) Dim lngKategorieID As Long If MsgBox("Möchten Sie diese Kategorie anlegen?", _ vbYesNo + vbExclamation, "Neue Kategorie") = vbYes Then DoCmd.OpenForm "frmKategorien", DataMode:=acFormAdd, _ OpenArgs:=NewData, WindowMode:=acDialog If CurrentProject.AllForms("frmKategorien").IsLoaded = True Then If CurrentProject.AllForms("frmKategorien").CurrentView = _ acCurViewFormBrowse Then lngKategorieID = Forms!frmKategorien!KategorieID DoCmd.Close acForm, "frmKategorien" Me!KategorieID = lngKategorieID Me!KategorieID.Requery End If End If End If Response = acDataErrContinue End Sub Listing 4.28: Aufrufen eines weiteren Formulars zum Eingeben verknüpfter Daten
4.7 Besonderheiten von Unterformularen Unterformulare können auf verschiedene Arten eingesetzt werden. Meist gibt es hier und da Probleme – etwa, wenn Daten im Unterformular eingegeben werden, ohne dass ein Datensatz im Hauptformular existiert, oder wenn Änderungen rückgängig gemacht werden sollen, obwohl diese Änderungen im Unterformular bereits gespeichert sind.
4.7.1 Eingabe von Daten ohne Detaildatensatz In Abschnitt 4.5.10 haben Sie ein Formular mit Unterformular zur Eingabe von Projek ten und Projektzeiten kennen gelernt. Wenn Sie dort im Hauptformular auf einen neuen Datensatz springen und im Unterformular Daten anlegen, bekommen Sie folgendes Problem: Die Datensätze im Unterformular sind dann nicht mit einem Datensatz im
260
Formulare
Hauptformular verknüpft und verschwinden in die ewigen Jagdgründe – zumindest aus Sicht dieses Formulars, denn es zeigt im Unterformular nur Datensätze an, die mit dem im Hauptformular befindlichen Datensatz verknüpft sind (siehe Abbildung 4.58). Das ist zumindest das Standardverhalten: Seit Access 2007 können Sie mit der Eigen schaft Leeren Hauptentwurf filtern (VBA: FilterOnEmptyMaster) des Unterformular-Steuer elements festlegen, ob im Falle eines leeren Datensatzes im Hauptformular alle Daten sätze der Datensatzquelle des Unterformulars oder keiner angezeigt werden soll.
Abbildung 4.58: Eingabe von Daten in ein Unterformular ohne verknüpften Datensatz im Haupt formular (»frmProjektzeiten«)
Um diese Probleme zu vermeiden, sollten Sie verhindern, dass der Benutzer mit der Be arbeitung von Daten im Unterformular beginnt, bevor er nicht mindestens ein Feld im Hauptdatensatz ausgefüllt hat. Dazu legen Sie für die Ereigniseigenschaft Beim Hingehen die folgende Prozedur an: Private Sub sfmProjektzeiten_Enter() If Me.NewRecord Then If Not Me.Dirty Then MsgBox "Bitte legen Sie zuerst einen Projektnamen an." Me!Projekt.SetFocus End If End If End Sub Listing 4.29: Kein Datensatz im Unterformular ohne Pendant im Hauptformular
Die Prozedur prüft, ob das Hauptformular einen neuen Datensatz enthält und ob dieser gegebenenfalls bereits bearbeitet wurde – womit der benötigte Primärschlüsselwert vorhanden wäre. Falls nicht, fordert die Routine den Benutzer auf, zunächst ein Projekt im Hauptformular anzulegen.
261
Kapitel 4
4.7.2 Undo in Haupt- und Unterformular Viele Formulare zeigen nicht nur selbst Daten an, sondern enthalten auch noch ein Un terformular zur Bearbeitung von Daten aus verknüpften Tabellen. Wie in gebundenen Formularen üblich, gilt auch hier: Wechselt man den Datensatz, wird er auch in der zugrunde liegenden Tabelle gespeichert. Bis dahin lassen sich Änderungen am aktuell bearbeiteten Datensatz noch relativ leicht durch Auslösen des Undo-Ereignisses rückgängig machen, etwa durch Betätigen der Esc-Taste. Beispieldatenbank: Die Formulare frmProjektzeiten_Undo und sfmProjektzeiten_Undo finden Sie unter \Kap_04\Formulare.accdb. Viele Formulare enthalten eine Implementierung einer Undo-Funktion in Form einer Abbrechen-Schaltfläche. Den unbedarften Benutzer täuscht man freilich damit, denn diese Schaltfläche macht lediglich die Änderungen an dem im Hauptformular angezeigten Datensatz rückgängig. Die Änderungen im Unterformular sind und bleiben gespeichert. Abbildung 4.59 zeigt, wie solch ein Formular aussieht. Es handelt sich um das bereits weiter oben hergeleitete Formular mit zunächst zwei (sichtbaren) Erweiterungen: einer OK- und einer Abbrechen-Schaltfläche.
Abbildung 4.59: Formular mit Unterformular und Abbrechen-Funktion (»frmProjektzeiten_Un do«)
Der Code, der durch das Beim Klicken-Ereignis der jeweiligen Schaltfläche ausgelöst wird, sieht etwa wie folgt aus:
262
Formulare Private Sub cmdOK_Click() 'Formular schließen DoCmd.Close acForm, Me.Name End Sub Private Sub cmdAbbrechen_Click() 'Prüfen, ob Datensatz seit letzter Speicherung geändert wurde If Me.Dirty = True Then 'Falls ja, Änderungen rückgängig machen Me.Undo End If 'Formular schließen DoCmd.Close acForm, Me.Name End Sub Listing 4.30: Ereignisprozeduren der OK- und der Abbrechen-Schaltfläche
Eine Änderung im Unterformular wird auf jeden Fall gespeichert, da das Verlassen des Unterformulars das Speichern des aktuellen Datensatzes zur Folge hat. Wenn Sie nun einmal eine Korrektur an den Projektzeiten eines Projekts vornehmen und dabei aus Versehen einige Datensätze löschen, weil Sie sich in einem anderen Projekt wähnten, gibt es keine Undo-Funktion: Solche Änderungen führen Sie ohne Netz und doppelten Boden aus. In den folgenden Abschnitten erfahren Sie, wie Sie den Benutzern Ihrer Datenbank (und vielleicht auch sich selbst) das Leben in dieser Hinsicht erleichtern können. Dabei machen Sie Gebrauch von einer Transaktion, die bei der ersten Änderung an einem Datensatz des Hauptformulars oder der verknüpften Datensätze im Unterformular gestartet und mit Speichern des Datensatzes im Hauptformular (etwa durch Betätigen der OK-Schaltfläche oder den Wechsel zu einem anderen Datensatz) beendet wird – was zum endgültigen Speichern der Änderungen im Haupt- und Unterformular führt. Auch die Abbrechen-Schaltfläche kann die Transaktion beenden – allerdings sollen dabei alle seit Beginn der Transaktion durchgeführten Änderungen verworfen werden. Normalerweise ist eine Transaktion ein gängiges Mittel, um mehrere Datenbankopera tionen zu bündeln und je nach Verlauf der Operationen komplett durchzuführen oder zu verwerfen. Das kann doch bei den Aktionen innerhalb eines Formulars nicht viel anders sein: Also startet man die Transaktion einfach beim Öffnen des Formulars und beendet sie mit Betätigen der OK- oder der Abbrechen-Schaltfläche. Oder doch nicht? Nein, ganz so einfach ist es nicht. Access führt nämlich die in Zusammenhang mit ge-
263
Kapitel 4
bundenen Formularen anfallenden Datenbankzugriffe nicht im Kontext des aktuellen Workspace aus. Wenn Sie also einen Datensatz ändern und speichern, bekommt die Transaktion davon überhaupt nichts mit. Abhilfe schafft überhaupt erst die mit Access 2000 eingeführte Recordset-Eigenschaft von Formularen. Damit lässt sich ein Formular an ein zuvor erstelltes Recordset-Objekt binden, das wiederum sehr wohl unter die Kontrolle einer Transaktion fallen kann. Es sind also folgende Maßnahmen für die Verwendung einer Transaktion zur Zusam menfassung von Datenbankoperationen in einem Formular mit Unterformular notwendig: Binden des Haupt- und des Unterformulars an Recordset-Objekte Starten der Transaktion beim Ändern des Datensatzes im Hauptformular oder in einem der verknüpften Datensätze im Unterformular Durchführen der Transaktion beim Klick auf die OK-Schaltfläche oder beim Wechseln des Datensatzes im Hauptformular Verwerfen der Operationen der Transaktion beim Betätigen der Abbrechen-Schalt fläche Die letzten beiden Punkte setzen voraus, dass die entsprechenden Prozeduren Infor mationen über den aktuellen Status haben – ist bereits eine Transaktion gestartet worden und wurde diese bereits beendet? Diese Information muss vom Haupt- und vom Unterformular aus schreib- und lesbar sein, da Datenbankoperationen in beiden Formularen eine Transaktion in Gang setzen können. Das ist deshalb wichtig, weil das Beenden einer Transaktion, die noch nicht gestartet wurde, zu einem Fehler führt. Genauso wichtig ist es, keine zweite Transaktion zu starten, wenn bereits eine Transaktion gestartet wurde. Dies würde als »verschachtelte« Transaktion gewertet werden; die Betätigung der OK- oder Abbrechen-Schaltfläche würde möglicherweise nur die innere von mehreren verschachtelten Transaktionen beenden und zu einem unvorhersehbaren Ergebnis führen.
Öffentliche Variablen für den Transaktionsstatus Es gibt mehrere Möglichkeiten, den Status bezüglich der Transaktion zu speichern und in Haupt- und Unterformular zugänglich zu machen. Globale Variablen verwendet man besser nicht, da diese allzu leicht von einer anderen Stelle aus geändert werden können. Besser ist die Verwendung von Membervariablen in einem der beiden Formulare und ihre Veröffentlichung über entsprechende Property-Prozeduren. Der folgende Quellcode zeigt, wie der Kopf des Klassenmoduls des Hauptformulars aussieht:
264
Formulare Option Compare Database Option Explicit Dim Dim Dim Dim
mDirtyForm As Boolean mDeletedForm As Boolean db As DAO.Database wrk As DAO.Workspace
Public Property Get DirtyForm() As Boolean DirtyForm = mDirtyForm End Property Public Property Let DirtyForm(bolDirtyForm As Boolean) mDirtyForm = bolDirtyForm End Property Listing 4.31: Kopf des Klassenmoduls des Formulars frmProjektzeiten_Undo
Die Eigenschaft mDirtyForm machen Sie durch die nachfolgenden Property-Prozeduren von außerhalb schreib- und lesbar. Auf diese Weise können Sie auf die wichtigen Infor mationen über den Zustand der Transaktion vom Haupt- und auch vom Unterformular aus zugreifen. Die Verwendung von Property Get- und Property Let-Prozeduren ist ein kleiner Vorgriff auf Kapitel 15, »Objektorientierte Programmierung«. Weitere Informationen über die Verwendung dieser Prozeduren finden Sie in diesem Kapitel.
Recordset-Objekt als Datensatzquelle Weiter oben wurde bereits erwähnt, dass Transaktionen von den Vorgängen im Formu lar und im Unterformular nur etwas mitbekommen, wenn die Datensatzquelle ein Re cordset-Objekt ist. Für das Hauptformular weisen Sie diese Eigenschaft am besten direkt beim Öffnen zu. Die Prozedur, die durch die Ereigniseigenschaft Beim Öffnen ausgelöst wird, hat folgendes Aussehen: Private Sub Form_Open(Cancel As Integer) 'Recordset-, Database- und Workspace-Objekt deklarieren Dim rst As DAO.Recordset Set db = DBEngine(0)(0) Set wrk = DBEngine.Workspaces(0) 'Recordset-Objekt öffnen... Set rst = db.OpenRecordset("tblProjekte", dbOpenDynaset)
265
Kapitel 4 '...und der entsprechenden Eigenschaft des Formulars zuweisen Set Me.Recordset = rst End Sub Listing 4.32: Ereignisprozedur Form_Open des Hauptformulars frmProjektzeiten_Undo
Da Sie dem Formular nun dynamisch die Datensatzquelle zuweisen, können Sie die Eigenschaft Datensatzquelle leeren. Wichtig ist, dass die Felder weiterhin an die entsprechenden Felder der Datensatzquelle gebunden sind. Anderenfalls können diese natürlich nicht die gewünschten Daten anzeigen. Neben dem Zuweisen der Datensatzquelle per Recordset-Eigenschaft ist die Deklaration des Workspace-Objekts wrk die zweite Besonderheit in dieser Prozedur. Ein WorkspaceObjekt können Sie anschaulich als Arbeitsbereich betrachten, in dem die aktuelle AccessSitzung abläuft.
Kein neues Formular-Recordset während einer Transaktion Wenn eine Transaktion einmal gestartet ist, können Sie dem Formular kein neues Recordset-Objekt zuweisen. Diese Regel bedeutet kein besonderes Problem, da Formular und Unterformular durchaus vor der ersten Änderung der Daten und damit dem Starten der Transaktion mit den benötigten Recordsets versehen werden können. Es ergibt sich nur eine Einschränkung: Sie können während einer Transaktion nicht den Datensatz im Hauptformular wechseln. Warum nicht? Weil bei der Anzeige eines neuen Datensatzes im Hauptformular auch die im Unterformular angezeigten Daten aktualisiert werden müssen. Und das geht wiederum nur durch das Zuweisen eines neuen Recordsets, das die mit dem Datensatz im Hauptformular korrespondierenden Daten enthält. Das ist aber nicht schlimm, denn dass beim Wechseln des Datensatzes die Daten des vorherigen Datensatzes gespeichert werden, sollte dem Benutzer klar sein. Die technische Umsetzung indes ist nicht ganz einfach. Fest steht nur, dass beim Wechsel des Datensatzes, der das Ereignis Beim Anzeigen des Hauptformulars auslöst, dem Unterfor mular ein neues Recordset zugewiesen werden muss. Um den Rest kümmern Sie sich später. Die Prozedur Form_Current sieht nun folgendermaßen aus: Private Sub Form_Current() Dim strSQL As String Dim rst As DAO.Recordset 'Zusammenstellen des SQL-Strings für das Recordset strSQL = "SELECT tblProjektzeiten.ProjektID, " _ & "tblProjektzeiten.ProjektzeitID, " _ & "tblProjektzeiten.MitarbeiterID, " _
'Öffnen des Recordset Set rst = db.OpenRecordset(strSQL, dbOpenDynaset) 'Zuweisen des Recordset an das Unterformular Set Me!sfmProjektzeiten_Undo.Form.Recordset = rst End Sub Listing 4.33: Quellcode der Prozedur Form_Current des Hauptformulars
Alternativ können Sie auch die gespeicherte Abfrage qryProjektzeitenMitarbeiter verwenden (siehe Abbildung 4.44). Die Prozedur sähe dann folgendermaßen aus: Private Sub Form_Current() Dim rst As DAO.Recordset 'Öffnen des Recordset Set rst = db.OpenRecordset("SELECT * FROM " _ & "qryProjektzeitenMitarbeiter WHERE ProjektID = " _ & Nz(Me!ProjektID, 0), dbOpenDynaset) 'Zuweisen des Recordset an das Unterformular Set Me!sfmProjektzeiten_Undo.Form.Recordset = rst End Sub Listing 4.34: Vereinfachte und performantere Version der Prozedur Form_Current
Dadurch, dass die im Unterformular angezeigten Datensätze ohnehin nach dem im Hauptformular angezeigten Projekt gefiltert werden, sind die beiden Eigenschaften Verknüpfen von und Verknüpfen nach im Unterformularsteuerelement nicht mehr notwen dig. Leeren Sie also diese beiden Eigenschaften. Dadurch müssen Sie beim Anlegen neuer Datensätze im Unterformular dafür sorgen, dass diese Datensätze irgendwie mit der ProjektID des Datensatzes im Hauptformular versehen werden. Dazu jedoch später mehr. Sie kommen trotzdem nicht umhin, dafür zu sorgen, dass keine Datensätze im Unterfor mular angelegt werden, ohne dass sich ein Datensatz im Hauptformular befindet. Dazu können Sie die Routine aus Listing 4.29 verwenden.
267
Kapitel 4
Transaktionen in Formularen Nun geht es an das eigentliche Problem: Das Einfassen von Änderungen im Haupt- und Unterformular in eine Transaktion, um getätigte Änderungen für Haupt- und Unterfor mular komplett revidieren zu können. Dazu veranschaulichen Sie sich zunächst, welche Datensatzänderungen überhaupt berücksichtigt werden müssen, und leiten davon ab, in welchen Ereignisprozeduren die Transaktion gestartet, durchgeführt oder abgebrochen wird. Hinzufügen eines Datensatzes im Hauptformular: löst Form_Dirty im Hauptformular aus. Ändern eines Datensatzes im Hauptformular: löst Form_Dirty im Hauptformular aus. Löschen eines Datensatzes im Hauptformular: kann nicht rückgängig gemacht werden, da das Formular unmittelbar nach dem Löschen auf einen anderen Datensatz springt. Hinzufügen eines Datensatzes im Unterformular: löst Form_Dirty im Unterformular aus. Ändern eines Datensatzes im Unterformular: löst Form_Dirty im Unterformular aus. Löschen eines Datensatzes im Unterformular: löst Form_Delete im Unterformular aus. Damit stehen bereits die Ereignisse fest, die eine Transaktion starten sollen. Schauen Sie sich die dadurch ausgelösten Prozeduren an. Den Anfang macht die Prozedur Form_ Dirty des Hauptformulars: Private Sub Form_Dirty(Cancel As Integer) 'Wenn aktueller Datensatz noch nicht geändert... If Me.DirtyForm = False Then 'Eigenschaft DirtyForm auf True setzen Me.DirtyForm = True 'Transaktion starten wrk.BeginTrans End If End Sub Listing 4.35: Start einer Transaktion beim Ändern oder Hinzufügen eines Datensatzes im Haupt formular
Im Unterformular muss die Prozedur Form_Dirty noch ein wenig mehr leisten. Sie sorgt nicht nur dafür, dass im Falle der ersten Änderung die Transaktion gestartet und die
268
Formulare
Eigenschaft DirtyForm des Hauptformulars auf True gesetzt wird. Zusätzlich prüft sie, ob es sich bei der Änderung um einen neuen Datensatz handelt. Wie weiter oben erwähnt, sind Haupt- und Unterformular nicht über die entsprechenden Werte für die Eigenschaften Verknüpfen von und Verknüpfen nach miteinander verknüpft. Der Wert des Verknüpfungsfeldes ProjektID wird nicht automatisch gefüllt. Die Prozedur übernimmt daher auch diese Aufgabe und weist dem Feld ProjektID des Unterformulars den entsprechenden Wert des Hauptformulars zu. Damit im Unterformular überhaupt eine Transaktion gestartet werden kann, sind noch einige Zeilen Code notwendig. So befindet sich im Modulkopf neben den obligatorischen Zeilen noch die Deklaration des Workspace-Objekts für das Unterformular: Option Compare Database Option Explicit Dim wrk As DAO.Workspace
Dieses stellt das Form_Open-Ereignis des Unterformulars auf den gleichen Workspace wie im Hauptformular ein: Private Sub Form_Open(Cancel As Integer) Set wrk = DBEngine.Workspaces(0) End Sub Listing 4.36: Zuweisen des Workspace-Objekts
Die Prozedur Form_Dirty ist eine von zwei Prozeduren, die vom Unterformular aus eine Transaktion starten können: Private Sub Form_Dirty(Cancel As Integer) 'Wenn es sich um einen neuen Datensatz handelt, 'Wert für die Herstellung der Verknüpfung über das 'Feld ProjektID zuweisen If Me.NewRecord Then Me!ProjektID = Me.Parent!ProjektID End If 'Wenn noch keine Änderung im Haupt- oder Unterformular 'vorliegt, DirtyForm auf True setzen und Transaktion starten If Me.Parent.DirtyForm = False Then Me.Parent.DirtyForm = True wrk.BeginTrans End If End Sub Listing 4.37: Form_Dirty-Prozedur des Unterformulars sfmProjektzeiten_Undo
269
Kapitel 4
Die Prozedur, die durch die Beim Löschen-Eigenschaft des Unterformulars ausgelöst wird, muss sich nicht um die ProjektID scheren. Sie startet lediglich die Transaktion, falls dies noch nicht geschehen ist: Private Sub Form_Delete(Cancel As Integer) 'Wenn noch keine Änderung im Haupt- oder Unterformular... If Me.Parent.DirtyForm = False Then Me.Parent.DirtyForm = True wrk.BeginTrans End If End Sub Listing 4.38: Die Prozedur Form_Delete startet gegebenenfalls eine Transaktion
Abbrechen der Transaktion Das Abbrechen der Transaktion erreichen Sie durch Betätigen der Abbrechen-Schaltfläche. Die dadurch ausgelöste Prozedur prüft, ob Änderungen im Haupt- oder Unterformular durchgeführt wurden und damit eine Transaktion gestartet wurde. Falls ja, werden die Änderungen durch die Rollback-Methode des Workspace-Objekts wieder verworfen. Private Sub cmdAbbrechen_Click() 'Prüfen, ob Daten geändert wurden If Me.DirtyForm = True Then 'Transaktion abbrechen wrk.Rollback End If 'Formular schließen DoCmd.Close acForm, Me.Name End Sub Listing 4.39: Die Abbrechen-Schaltfläche verwirft alle Änderungen der aktuellen Daten des Haupt- und Unterformulars
Durchführen der Transaktion Die Durchführung der Transaktion erfolgt durch Speichern des Datensatzes im Haupt formular. Dazu gibt es zum Beispiel folgende Möglichkeiten: Schließen des Formulars mit der OK-Schaltfläche Wechseln zu einem anderen Datensatz im Hauptformular
270
Formulare
Schließen des Formulars auf anderem Wege (beispielsweise Betätigen des SchließenButtons oben rechts im Formular) In diesem Beispiel werden nur die beiden ersten Möglichkeiten zum Speichern eines Datensatzes beschrieben. Zum Schließen des Formulars mit der OK-Schaltfläche ist die Prozedur verantwortlich, die durch das Ereignis Beim Klicken der Schaltfläche ausgelöst wird. Die Prozedur prüft anhand der Eigenschaft DirtyForm, ob überhaupt Änderungen im Haupt- oder Unterformular durchgeführt wurden und dementsprechend eine Transaktion gestartet wurde. Falls ja, schließt sie die Transaktion mit der Methode CommitTrans ab. Private Sub cmdOK_Click() 'Speichern der Änderungen DoCmd.RunCommand acCmdSaveRecord 'Wenn Änderungen vorliegen und damit eine 'Transaktion gestartet wurde: If Me.DirtyForm = True Then 'Transaktion durchführen wrk.CommitTrans End If 'Formular schließen DoCmd.Close acForm, Me.Name End Sub Listing 4.40: Das Schließen des Formulars mit der OK-Schaltfläche bewirkt die Durchführung der Transaktion
Der Wechsel zu einem anderen Datensatz im Formular löst das Ereignis Beim Anzeigen aus. Die erste Version dieser Prozedur (siehe Listing 4.33) sieht noch keine Funktion zum Abschließen einer Transaktion vor. Die folgende Variante holt dies nach: Private Sub Form_Current() 'Wenn Daten geändert und Transaktion gestartet... If Me.DirtyForm = True Then 'aktuellen Datensatz speichern DoCmd.RunCommand acCmdSaveRecord 'Transaktion durchführen wrk.CommitTrans 'Formular als "gespeichert" kennzeichnen Me.DirtyForm = False End If If mDeletedForm = False Then
271
Kapitel 4 'Prozedur zum Aktualisieren des Unterformulars aufrufen UnterformularAktualisieren Else mDeletedForm = False End If End Sub Listing 4.41: Abschließen der Transaktion beim Datensatzwechsel im Hauptformular
Die eigentliche Funktionalität zum Aktualisieren der Datensatzquelle des Unterformulars wird dabei in eine eigene Prozedur ausgegliedert, weil diese noch von einer weiteren Ereignisprozedur aufgerufen werden soll: Private Sub UnterformularAktualisieren() Dim strSQL As String Dim rst As DAO.Recordset 'Zusammenstellen des SQL-Strings strSQL = "SELECT * FROM qryProjektzeitenMitarbeiter " _ & "WHERE ProjektID = " & Nz(Me!ProjektID, 0) 'Öffnen des Recordset Set rst = db.OpenRecordset(strSQL, dbOpenDynaset) 'Zuweisen des Recordset an das Unterformular Set Me!sfmProjektzeiten_Undo.Form.Recordset = rst End Sub Listing 4.42: Prozedur zum Aktualisieren des Unterformulars
Löschen des aktuellen Datensatzes im Hauptformular Bleibt noch ein Problem: Wenn Sie den aktuell im Hauptformular angezeigten Datensatz löschen möchten, erscheint der Fehler aus Abbildung 4.60. Das Merkwürdige ist, dass beim Löschen gar keine Transaktion per VBA gestartet wurde – woher kommt nun diese Transaktion? Offensichtlich schließt Access den Löschvorgang automatisch in eine Transaktion ein. Normalerweise würden Sie davon gar nichts mitbekommen, da innerhalb dieser Transaktion keine unerlaubten Aktionen erfolgen wie das hier bemängelte Zuweisen eines Recordset-Objekts an die entsprechende Eigenschaft. Hier liegt der Fall anders und bei genauer Betrachtung des Ablaufs der Ereignisse beim Löschen eines Datensatzes wird Einiges klarer. Die Ereignisse haben folgende Reihenfolge:
272
Formulare
Form_Delete Form_Current Form_BeforeDelConfirm Form_AfterDelConfirm Dabei werden die beiden letzten Ereignisse nur ausgelöst, wenn die Einstellung Erwei tert|Bearbeiten|Bestätigen|Datensatzänderungen in den Access-Optionen aktiviert ist.
Abbildung 4.60: Fehler beim Löschen des Datensatzes im Hauptformular
Wichtig ist zunächst, dass nach dem Delete-Ereignis der zu löschende Datensatz bezie hungsweise die zu löschenden Datensätze zunächst temporär gespeichert werden, bis feststeht, dass der Löschvorgang nicht durch das Ereignis Vor Löschbestätigung des For mulars abgebrochen wird. Und genau dieses temporäre Speichern erledigt die Transak tion, die sich nicht mit dem Zuweisen des Recordset-Objekts zum Unterformular verträgt. Das Problem lösen Sie, indem Sie den Zeitpunkt der Zuweisung des Recordset-Objekts einfach nach hinten verschieben – vom Form_Current- ins Form_AfterDelConfirm-Ereignis. Dazu verwenden Sie die folgenden Prozeduren. Die Prozedur Form_Delete stellt die Eigenschaft mDeletedForm auf den Wert True ein. Diese Eigenschaft wird in der Prozedur Form_Current ausgewertet; wenn sie den Wert True enthält, wird das Aktualisieren des Unterformulars unterbunden (siehe auch Lis ting 4.41). Private Sub Form_Delete(Cancel As Integer) mDeletedForm = True End Sub Listing 4.43: Setzen einer Eigenschaft, an der andere Prozeduren den laufenden Löschvorgang erkennen können
273
Kapitel 4
Die Prozedur Form_BeforeDelConfirm können Sie theoretisch auch weglassen. In diesem Fall unterbindet sie die Standardmeldung von Access beim Löschen des Datensatzes (siehe Abbildung 4.61). Private Sub Form_BeforeDelConfirm(Cancel As Integer, Response As Integer) Response = acDataErrContinue End Sub Listing 4.44: Verhindern der Rückfrage beim Löschen
Abbildung 4.61: Standardmeldung von Access beim Löschen von Daten
Die Prozedur Form_AfterDelConfirm ist wiederum sehr wichtig: Sie wird nur ausgelöst, wenn Sie wie in Listing 4.42 die Access-Meldung unterbinden oder wenn Sie den Löschvorgang im Fall der Anzeige bestätigen. Die Prozedur ruft die Routine auf, die das Unterformular aktualisiert, und holt damit genau den Vorgang nach, der durch Setzen der Variablen mDeletedForm im Form_Current-Ereignis unterbunden wurde. Private Sub Form_AfterDelConfirm(Status As Integer) UnterformularAktualisieren End Sub Listing 4.45: Aktualisieren des Unterformulars nach vollendetem Löschvorgang
Restarbeiten Damit das Beispiel wie beschrieben funktioniert, brauchen Sie lediglich die Eigenschaften der Beziehungen zwischen den im Haupt- und Unterformular angezeigten Tabellen so anzupassen, dass beim Löschen des Datensatzes im Hauptformular direkt die entsprechenden Unterformulareinträge mitgelöscht werden. Dazu reicht das Setzen der Eigenschaft Löschweitergabe an verwandte Datensätze im Eigenschaftsfenster der passenden Beziehung (siehe Abbildung 4.62).
Einsatz in eigenen Formularen Dem Einsatz dieser Technik in Ihren eigenen Formularen steht prinzipiell nichts im Wege. Sie müssen lediglich alle in den beiden Formularmodulen enthaltenen Dekla rationen und Prozeduren in Ihre Formulare übertragen und einige Zeilen Code den
274
Formulare
Gegebenheiten Ihrer eigenen Datenbank anpassen – etwa die Datenherkünfte für Haupt- und Unterformular und die Zeile, die beim Anlegen eines neuen Datensatzes im Unterformular den Wert für den Fremdschlüssel festlegt (siehe Listing 4.35).
Abbildung 4.62: Aktivieren der Löschweitergabe zwischen verknüpften Tabellen
4.8 Eingabevalidierung Zur defensiven Programmierung gehört, dass Sie keine ungültigen Daten zulassen. Das fängt bei der Eingabe von Daten in Formularen an. Dieser Teil beschreibt die unterschiedlichen Möglichkeiten zur Validierung der Eingabe. Die wichtigste Frage bei der Validierung ist nicht, wie diese erfolgen soll, sondern zu welchem Zeitpunkt. Die folgenden drei Regeln sind praxiserprobt: Fehlerhafte Eingaben wie Zeichenketten in Datumsfelder werden sofort geahndet. Fehlende Eingaben werden erst beim Bestätigen des Datensatzes bemängelt. Die Handhabung von abhängigen Feldern ist Geschmackssache – Beispiel Startdatum und Enddatum. Am einfachsten ist eine Prüfung beim Bestätigen des Datensatzes.
4.8.1 Validieren direkt bei der Eingabe Das Validieren unmittelbar nach der Eingabe erfolgt in einer Prozedur, die durch die Ereigniseigenschaft Vor Aktualisierung des jeweiligen Steuerelements ausgeführt wird. Eine solche Routine könnte beispielsweise wie folgt aussehen:
275
Kapitel 4 Private Sub Projekt_BeforeUpdate(Cancel As Integer) If IsNumeric(Left(Me!Projekt, 1)) Then MsgBox "Der Projektname muss mit einem Buchstaben beginnen.", _ vbOKOnly + vbExclamation, "Eingabefehler" Cancel = True Exit Sub End If End Sub Listing 4.46: Validierung bei der Eingabe
Die Routine prüft, ob die in das Feld Projekt eingegebenen Werte nicht mit einer Zahl anfangen. Sollte dies doch der Fall sein, erscheint eine entsprechende Meldung (siehe Abbildung 4.63).
Abbildung 4.63: Projektbezeichnungen, die mit einer Zahl beginnen, sind nicht erlaubt (»frm ProjekteDetailansicht«)
4.8.2 Validieren vor dem Speichern Vor dem Speichern werden vor allem Pflichtfelder geprüft. Falls im obigen Formular die Felder Projekt und Startdatum Pflichtfelder wären, würde die Prüfung wie folgt aussehen: Private Function Validierung() As Boolean If IsNull(Me!Projekt) Then MsgBox "Bitte geben Sie einen Projektnamen ein.", _ vbOKOnly + vbExclamation, "Fehlende Eingabe" Me!Projekt.SetFocus Exit Function End If If IsNull(Me!Startdatum) Then
276
Formulare MsgBox "Bitte geben Sie das Startdatum ein.", _ vbOKOnly + vbExclamation, "Fehlende Eingabe" Me!Startdatum.SetFocus Exit Function End If
'
If Not IsNull(Me!Startdatum) And Not IsNull(Me!Enddatum) Then If Me!Startdatum > Me!Enddatum Then MsgBox "Das Enddatum darf nicht hinter " _ & "dem Startdatum liegen.", _ vbOKOnly + vbExclamation, "Falsche Datumsangabe" Me!Enddatum.SetFocus Exit Function End If End If Validierung = True
End Function Listing 4.47: Validierung vor dem Speichern des Datensatzes
Die Prozedur prüft von oben nach unten alle Pflichtfelder. Sobald sie auf eines trifft, das leer ist, gibt sie eine Fehlermeldung aus, setzt den Fokus auf das Feld mit dem fehlenden Inhalt und bricht die Prozedur ab. Außerdem wird der Funktionswert nur auf True gesetzt, wenn alle Validierungen erfolgreich verlaufen sind.
Abhängige Felder prüfen Im letzten Teil der Routine aus Listing 4.45 finden Sie ein Beispiel, wie sich abhängige Felder prüfen lassen. In diesem Beispiel wird sichergestellt, dass das Startdatum nicht hinter dem Enddatum liegt.
Aufruf der Validierung vor dem Speichern Nun müssen Sie diese Funktion nur noch von geeigneter Stelle aus aufrufen. Leider reicht es nicht, dies mit der Ereigniseigenschaft Vor Aktualisierung abzudecken. Die dazugehörende Ereignisprozedur enthält auch den passenden Cancel-Parameter, der das Speichern des Datensatzes bei falschen oder fehlenden Daten abbrechen könnte. Es gibt allerdings eine Schwachstelle: Theoretisch wird diese Routine zwar durch alle relevanten Vorgänge wie Wechsel des Datensatzes oder Schließen des Formulars aufgerufen – aber wenn das Schließen einmal ausgelöst wurde, hält auch das Setzen des CancelParameters der Form_BeforeUpdate-Prozedur das Schließen nicht mehr auf. Somit würde in diesem Fall ein Datensatz mit falschen oder fehlenden Daten gespeichert.
277
Kapitel 4
Daher rufen Sie die Funktion Validierung von zwei Prozeduren aus auf: Von der Form_ BeforeUpdate-Prozedur sowie von der cmdOK_Click-Prozedur aus. Um auszuschließen, dass das Formular anders als mit der OK-Schaltfläche geschlossen wird, stellen Sie noch die Eigenschaft Schließen-Schaltfläche auf den Wert Nein ein. Die beiden Ereignisprozeduren sehen schließlich wie folgt aus: Private Sub cmdOK_Click() If Validierung = True Then DoCmd.Close acForm, Me.Name End If End Sub Private Sub Form_BeforeUpdate(Cancel As Integer) If Validierung = False Then Cancel = True End If End Sub Listing 4.48: Diese beiden Routinen rufen die Funktion Validierung auf
Alternativ können versierte Benutzer das Formular natürlich noch mit Strg + F4 schließen. Um dem vorzubeugen, verwenden Sie eine Prozedur, die beim Ereignis Bei Taste ausgelöst wird und das Betätigen dieser Tastenkombination unterbindet. Gleichzeitig müssen Sie die Eigenschaft Tastenvorschau auf den Wert Ja einstellen: Private Sub Form_KeyDown(KeyCode As Integer, Shift As Integer) If KeyCode = 115 And Shift = 2 Then KeyCode = 0 End If End Sub Listing 4.49: Unterbinden der Tastenkombination zum Schließen eines Formulars
Tastenkombinationen sperren und bereitstellen mit dem AutoKeys-Makro Davon abgesehen ist das eine gute Gelegenheit, hier das AutoKeys-Makro vorzustellen: Das ist nämlich neben dem AutoExec-Makro das zweite Makro, das aufgrund seines Namens automatisch zu bestimmten Gelegenheiten ausgeführt wird – in diesem Fall beim Eingeben von Tastenkombinationen. Sie können damit nämlich beliebige Tastenkombinationen festlegen. Legen Sie einfach ein neues Makro an und fügen Sie pro Tastenkombination eine Anweisung hinzu. Die Tastenkombination tragen Sie dabei in der Spalte Makroname ein und verwenden dabei die Syntax der SendKeys-Anweisung von VBA – die Kürzel für die Tastenkombinationen finden Sie in der VBA-Onlinehilfe. Die Tastenkombination Strg + F4 fügen Sie dort etwa so ein: ^{F4}. Lassen Sie die Spalte
278
Formulare
Aktion leer, veranlasst Access beim Betätigen dieser Tastenkombination schlicht nichts mehr – genau wie gewünscht. Natürlich können Sie auf diese Weise auch Tastenkombinationen für richtige Funktionen festlegen. Das beschriebene Makro finden Sie in der Beispieldatenbank.
4.8.3 Sonderfälle beim Validieren Mit der oben genannten Vorgehensweise können Sie nicht alle möglichen Eingabefehler abfangen. Wenn im Tabellenentwurf festgelegte Einschränkungen (siehe Kapitel 2, Ab schnitt 2.4, »Integritätsregeln«) bei der Eingabe oder beim Speichern des Datensatzes nicht berücksichtigt werden, werden die dadurch hervorgerufenen Meldungen vor dem Auslösen der Vor Aktualisierung-Ereignisprozeduren von Steuerelementen und For mularen angezeigt. Das ist zum Beispiel der Fall, wenn Sie einen Text in ein Datumsfeld eingeben (siehe Abbildung 4.64).
Abbildung 4.64: Access-eigene Validierungsmeldung
Wenn Sie diese Meldungen umgehen möchten, verwenden Sie die Bei Fehler-Ereignisei genschaft des Formulars. Hier müssen Sie zwar ein wenig mehr Aufwand betreiben als bei den oben beschriebenen Validierungsfunktionen, aber dafür ersetzen Sie damit auch die Access-eigenen Validierungsmeldungen. Im Formular frmProjekteDetailansicht kann es etwa passieren, dass eines der Felder Startdatum oder Enddatum mit einem Wert bestückt wird, der nicht den Datentyp Datum/Uhrzeit enthält, oder dass kein Kunde für das Projekt ausgewählt wird. Beide Fehler würden die Integritätsregeln verletzen und eine Access-eigene Meldung heraufbeschwören. Diese Meldungen können Sie nur im Bei Fehler-Ereignis abfangen. Die folgende Routine wird durch dieses Ereignis ausgelöst und prüft, ob eines der beiden Datumsfelder einen ungültigen Wert enthält. Falls ja, gibt es eine entsprechende Meldung aus; der Fokus wird dann automatisch auf das betroffene Steuerelement gesetzt. Damit Sie auf den richtigen Fehler reagieren, prüfen Sie in einer Select Case-Verzweigung die in DataErr enthaltene Fehlernummer und verzweigen entsprechend. Mit der Anwei sung Screen.ActiveControl ermitteln Sie, bei welchem Steuerelement der Fehler ausgelöst wurde, und behandeln den Fehler dem betroffenen Feld entsprechend.
279
Kapitel 4
Ähnlich ist es beim Speichern des Datensatzes ohne Füllen des Pflichtfeldes KundeID. Die Fehlernummer lautet 3201 und Sie ermitteln ganz einfach, ob das Feld KundeID für diesen Fehler verantwortlich ist, indem Sie dessen Inhalt überprüfen. Ist dieser Null, muss der Benutzer den entsprechenden Kunden noch nachreichen. Wenn Sie nicht sicher sind, welche Fehlernummer sich hinter den eingebauten Fehlermeldungen verbirgt, lassen Sie sich in dieser Routine einfach den Wert DataErr mit der Debug.Print-Methode ausgeben. Private Sub Form_Error(DataErr As Integer, Response As Integer) Dim ctl As Control Select Case DataErr Case 2113 Set ctl = Screen.ActiveControl Select Case ctl.Name Case "Startdatum", "Enddatum" MsgBox "Bitte geben Sie ein Datum im Format " _ "'dd.mm.jjjj' ein.", vbOKOnly + vbExclamation, _ "Eingabefehler" Response = acDataErrContinue Exit Sub End Select Case 3201 If Me!KundeID = 0 Then MsgBox "Bitte wählen Sie einen Kunden für dieses " _ & "Projekt aus.", vbOKOnly + vbExclamation, _ "Fehlende Eingabe" Response = acDataErrContinue Me!KundeID.SetFocus Exit Sub End If End Select End Sub Listing 4.50: Behandlung von Verletzungen der Integrität bei der Dateneingabe
4.9 Suchen in Formularen Die Suche nach bestimmten Daten erfordert je nach Anwendungsfall unterschiedliche Maßnahmen. In den folgenden Abschnitten finden Sie zwei Möglichkeiten, die sich für viele Anwendungen eignen – eine Schnellauswahl per Kombinationsfeld und ein Text feld zum schnellen Filtern von Listenfeldern.
4.9.1 Schnelles Suchen in Formularen Für die Suche in Tabellen, Abfragen und Formularen bietet Access ein unscheinbares Textfeld im Navigationsbereich dieser Objekte. Hier geben Sie einfach den gesuchten
280
Formulare
Begriff Buchstabe für Buchstabe ein und Access sucht die jeweils nächste Fundstelle in den Daten der Datensatzquelle des Formulars (siehe Abbildung 4.65). Das Betätigen der Eingabetaste bewirkt das Aufsuchen der nächsten Fundstelle. Der Nachteil dieses Suchfelds ist, dass es die Suche nicht auf bestimmte Datenfelder begrenzt, sondern einfach in allen Steuerelementen des Formulars sucht – und das ist nicht immer unbedingt erwünscht.
Abbildung 4.65: Die eingebaute Suche von Access
4.9.2 Schnelles Filtern in der Datenblattansicht Access 2007 liefert eine Filter-Funktion für die Datenblattansicht von Tabellen, Abfragen und Formularen mit. Sie rufen diese Funktion über die kleine Schaltfläche mit dem Pfeil nach unten in den Spaltenköpfen der Datenblattansicht auf (siehe Abbildung 4.66).
Abbildung 4.66: Filtern von Datenblättern
281
Kapitel 4
Die Filterkriterien unterscheiden sich je nach Datentyp. Sehr nützlich ist der Datumsfilter: Er bietet Vergleichskriterien wie morgen, heute, gestern, nächste Woche, diese Woche, letzte Woche und so weiter. Dieses Feature liefert einen guten Grund, dem Benutzer Daten in der Datenblattansicht statt in einem Endlosformular zu präsentieren. Die Bedie nung dieses Filters ist im Übrigen selbsterklärend.
4.9.3 Schnellauswahl per Kombinationsfeld Eine der einfachsten Möglichkeiten zum Einbau einer Suche ist ein Kombinationsfeld zur Auswahl von Werten aus einem oder aus mehreren kombinierten Feldern. Das folgende Beispiel basiert auf einem Formular zum Bearbeiten von Kontakten.
Beispieldatenbank: Die Tabelle tblKontakte und das Formular frmSchnellsuchePerKombifeld finden Sie unter \Kap04\Formulare.accdb. Die Schnellauswahl per Kombinationsfeld ist nur dann sinnvoll, wenn die gewünschten Datensätze nach einem einzigen Kriterium durchsucht werden – beispielsweise nach dem Nachnamen eines Kontaktes. Diese Variante finden Sie im folgenden Beispiel: Das Formular aus Abbildung 4.67 enthält alle Felder der Tabelle tblKontakte, zwei Schaltflächen mit der Beschriftung OK und Abbrechen sowie ein Kombinationsfeld namens cboSchnellsuche.
Abbildung 4.67: Formular mit Schnellauswahl in der Entwurfsansicht
282
Formulare
Das Kombinationsfeld verwendet die Abfrage aus Abbildung 4.68 als Datensatzherkunft. Die Abfrage enthält ein Feld mit dem Primärschlüsselfeld der Tabelle tblKontakte sowie ein aus den beiden Feldern Nachname und Vorname zusammengesetztes Feld, wobei die beiden Werte durch ein Komma getrennt werden.
Abbildung 4.68: Datensatzherkunft des Kombinationsfeldes zur Schnellauswahl von Kontakten
Damit lassen sich mit dem Kombinationsfeld alle Einträge der Tabelle tblKontakte in der Form , anzeigen. Damit das Formular auch den ausgewählten Datensatz einblendet, fügen Sie der Ereigniseigenschaft Nach Aktualisierung des Kombinationsfeldes die folgende Prozedur hinzu: Private Sub cboSchnellauswahl_AfterUpdate() Dim rst As DAO.Recordset 'Kopie der aktuellen Formulardaten im Recordset speichern Set rst = Me.RecordsetClone 'Im Recordset nach dem im Kombinationsfeld ausgewählten 'Kontakt suchen rst.FindFirst "KontaktID = " & Me!cboSchnellauswahl 'Im Formular zum gefundenen Recordset springen Me.Bookmark = rst.Bookmark Set rst = Nothing End Sub Listing 4.51: Code für die Schnellsuche per Kombinationsfeld
283
Kapitel 4
Diese Variante ist auch für ältere Access-Versionen universell einsetzbar; in AccessVersionen, die für Formulare die Recordset-Eigenschaft bereitstellen, ist folgende Variante deutlich kürzer: Private Sub cboSchnellauswahl_AfterUpdate() Me.Recordset.FindFirst "KontaktID = " & Me!cboSchnellauswahl End Sub Listing 4.52: Datensatz suchen für Formulare mit Recordset-Eigenschaft
Das Auswählen eines Eintrags des Kombinationsfeldes führt nun zur Anzeige des gewünschten Datensatzes im Formular (siehe Abbildung 4.69). Es fehlt nur noch die Aktualisierung der Datensatzherkunft des Kombinationsfeldes beim Ändern, Hinzufügen oder Löschen von Datensätzen im Formular. Diese Aufgabe übernimmt die folgende Prozedur, die durch die Ereigniseigenschaft Beim Anzeigen des Formulars ausgelöst wird: Private Sub Form_Current() 'Aktualisieren des Inhalts des Kombinationsfeldes Me!cboSchnellauswahl.Requery End Sub Listing 4.53: Aktualisieren des Kombinationsfeldes
Abbildung 4.69: Die Schnellsuche im Einsatz (»frmSchnellsuchePerKombinationsfeld«)
4.9.4 Schnelles Filtern von Listenfeldern Listenfelder kommen oft zum Einsatz, wenn Datensätze in einer Übersicht zur Auswahl angeboten werden sollen. Wenn viele Datensätze zur Auswahl bereitstehen, macht eine Möglichkeit zum Filtern der Datensätze Sinn.
284
Formulare
Die üblichen Filter-Funktionen bestehen aus verschiedenen Textfeldern zur Eingabe der Filterkriterien und einer Schaltfläche zum Auslösen des Filtervorgangs mit den neuen Parametern. Je mehr Felder man mit einer solchen Filter-Funktion abdeckt, desto mächtiger ist diese – dafür benötigt man aber auch entsprechend viele Steuerelemente, durch die sich der Benutzer erst einmal hindurchkämpfen muss. In vielen Fällen ist das gar nicht notwendig; dort lässt sich eine viel handlichere Lösung einsetzen: Stellen Sie dem Benutzer einfach ein Textfeld bereit, das in beliebig vielen Feldern des Listenfeldes sucht und unmittelbar nach der Eingabe eines jeden Buchstabens die angezeigten Datensätze filtert. Dazu ist neben ein wenig Routinearbeit noch ein kleiner Trick notwendig – sehen Sie einfach selbst. Abbildung 4.70 zeigt das fertige Formular in der Entwurfsansicht. Es enthält ein Listenfeld zur Anzeige der Datensätze, eine Schaltfläche zum Anzeigen der Detailansicht des markierten Datensatzes und ein Textfeld zur Eingabe des Suchbegriffs.
Abbildung 4.70: Listenfeld mit Schnellfilter
Das erste Textfeld namens txtSuche dient der Eingabe des Suchbegriffs. Der Inhalt des Listenfeldes lstKontakte soll nach jeder Änderung des im Textfeld txtSuche enthaltenen Suchbegriffs aktualisiert werden. Das Listenfeld bezieht seine Daten aus der Abfrage qryKontakteListenfeld (siehe Abbil dung 4.71). Die Abfrage enthält die anzuzeigenden Felder in der gewünschten Reihen folge und Sortierung. Damit das Listenfeld nach jeder Änderung des Inhalts des Textfeldes txtSuche aktualisiert wird, verwenden Sie eine sonst recht stiefmütterlich behandelte Ereigniseigenschaft. Die Ereigniseigenschaft Bei Änderung des Feldes zur Eingabe des Suchbegriffs wird bei jeder Änderung des Inhalts ausgelöst. Für die entsprechende Ereignisprozedur fügen Sie den folgenden Code ein:
285
Kapitel 4
Abbildung 4.71: Datensatzherkunft des Listenfeldes zur Anzeige der Kontakte
Private Sub txtSuche_Change() Dim strSuchbegriff As String 'Suchbegriff in Variable speichern strSuchbegriff = Me!txtSuche.Text 'Neue Datensatzherkunft zuweisen Me!lstKontakte.RowSource = "SELECT * FROM qryKontakteListenfeld " _ "WHERE Nachname LIKE '" & strSuchbegriff & "*'" 'Inhalt des Listenfeldes aktualisieren Me!lstKontakte.Requery End Sub Listing 4.53: Prozedur zum Filtern des Listenfeldes
Der Trick ist, dass Sie nicht den Inhalt des Textfeldes über die Eigenschaft Value auswerten, sondern die Text-Eigenschaft verwenden. Die Value-Eigenschaft wird nämlich erst nach dem Aktualisieren des Textfeldes auf den tatsächlichen Inhalt eingestellt. Der Abfrageausdruck ermittelt alle Datensätze der Abfrage qryKontakteListenfeld, in denen der Nachname mit dem im Textfeld angegebenen Ausdruck beginnt. Der Vollständigkeit halber finden Sie hier noch den Code zum Anzeigen des im Listenfeld ausgewählten Kontaktes. Der Kontakt soll im Formular aus dem vorherigen Beispiel angezeigt werden. Zwei Aktionen sollen dazu führen: ein Doppelklick auf das Listenfeld und ein Klick auf die Schaltfläche cmdAnzeigen. Die entsprechenden Ereignisse sollen prinzipiell die gleiche Aktion auslösen – daher legen Sie dafür eine neue Prozedur an und rufen diese über die beiden Ereignisprozeduren auf:
286
Formulare Private Sub cmdAnzeigen_Click() Anzeigen End Sub Private Sub lstKontakte_DblClick(Cancel As Integer) Anzeigen End Sub Private Sub Anzeigen() If Not IsNull(Me!lstKontakte) Then DoCmd.OpenForm "frmSchnellsuchePerKombifeld", _ WhereCondition:="KontaktID = " & Me!lstKontakte, _ WindowMode:=acDialog End If End Sub Listing 4.54: Prozeduren zum Anzeigen der Details eines Kontaktdatensatzes
287
5 Steuerelemente Das vorhergehende Kapitel, »Formulare«, setzt die Kennt nis der Standardsteuerelemente voraus. So ist es auch in diesem Kapitel. Es erweitert Ihre Grundkenntnisse um interessante Techniken zur optimalen Verwendung von Steuerelementen. Aber es beschränkt sich nicht auf die Standardsteuerelemente, sondern zeigt auch, was Sie mit ActiveX-Steuerelementen wie dem TreeView-, dem List View- oder dem ImageList-Steuerelement anfangen kön nen. Und natürlich stellt es die Neuigkeiten vor, die Access 2007 in Zusammenhang mit Steuerelementen liefert – neben einigen Detailverbesserungen gibt es übrigens sogar ein ganz neues Steuerelement. Um Sie mit den wichtigsten Informationen zu versorgen, lassen wir das eine oder andere Steuerelement aus: Hier sollten Sie sich die grundlegenden Techniken mit Hilfe der Onlinehilfe aneignen.
5.1 Textfelder Textfelder als meistgenutztes Steuerelement sind in Access 2007 natürlich nicht von Änderungen verschont geblieben. Wie Sie grundsätzlich mit Textfeldern umgehen, beschreibt die Onlinehilfe im Artikel »Hinzufügen eines Text feld-Steuerelements zu einem Formular oder zu einem Bericht«.
5.1.1 Rich-Text in Textfeldern In Tabellen können Sie für Memofelder nun das Textformat Rich-Text einstellen. Wenn Sie ein Textfeld auf Basis eines
Kapitel 5
solchen Feldes in einem Formular verwenden, können Sie die Eigenschaft Textformat auch im Formular auf Rich-Text einstellen (bei »normalen« Textfeldern erzeugt dies eine Meldung, dass die Einstellung ungültig sei). Die Beispiele dieses Kapitels finden Sie auf der Buch-CD in der Datenbankdatei \Kap_05\Steuerelemente.accdb. Textfelder im Rich-Text-Modus zeigen beim Markieren von Textpassagen mit der Maus ein zunächst transparentes Menü an, aus dem Sie mit der Maus die gewünschte Formatierung auswählen können (siehe Abbildung 5.1). Sie können aber auch die Ribbon-Schaltflächen aus den Bereichen Start|Schriftart und Start|Rich-Text für einzelne Formatierungsoptionen verwenden.
Abbildung 5.1: Ein Textfeld im Rich-Text-Modus
Die formatierten Texte speichert Access im HTML-Format. Den Quelltext können Sie normalerweise nicht einsehen, es sei denn, Sie stellen die Eigenschaft Textformat eines darauf basierenden Textfelds auf Nur Text ein (erzeugt eine vorherige Warnung). Per VBA legen Sie diese Eigenschaft so fest: Me!Textbox1.TextFormat = acTextFormatPlain Me!Textbox1.TextFormat = acTextFormatHTMLRichText
Sie können auch per VBA auf den Inhalt zugreifen. Dazu reicht eine einfache DLookupAnweisung aus. Alternativ können Sie sich den HTML-Text mit folgenden Zeilen – etwa beim Anzeigen des aktuellen Datensatzes eines Formulars – anzeigen lassen: Private Sub Form_Current() Debug.Print Me!Beschreibung Debug.Print Application.PlainText(Me!Beschreibung) End Sub Listing 5.1: Ausgeben des HTML- und des angezeigten Texts
290
Steuerelemente
In dem Zusammenhang sind auch zwei neue Methoden des Application-Objekts erwähnenswert: HTMLEncode und PlainText. HTMLEncode erzeugt beispielsweise folgende Ausgabe: Debug.Print Application.HtmlEncode ("
Dies ist ein Text
")
Dies ist ein Text
Den umgekehrten Weg beschreitet PlainText: Debug.Print Application.PlainText("
Dies ist ein Text
")
Dies ist ein Text
5.1.2 Datum auswählen Eine weitere Neuerung, die speziell das Textfeld betrifft, ist der DatePicker. Diesen können Sie für Textfelder aktivieren, die an Datumsfelder gebunden sind. Die passende Eigenschaft heißt Datumsauswahl (VBA: ShowDatePicker) und akzeptiert die beiden Werte Für Datumsangaben oder Nie. Abbildung 5.2 zeigt das beim Eintritt in ein Datumsfeld mit aktiviertem DatePicker angezeigte Symbol zum Öffnen des DatePickers. Mit einem Klick auf diese Schaltfläche öffnen Sie den DatePicker (siehe Abbildung 5.3).
Abbildung 5.2: Die Schaltfläche zum Öffnen des DatePickers
Abbildung 5.3: Der DatePicker in Aktion
291
Kapitel 5
Der DatePicker hat einen entscheidenden Nachteil: Er bietet keine Möglichkeit zur direkten Auswahl des Monats und des Jahres. Wenn Sie Daten eingeben möchten, die weit in der Vergangenheit liegen, bemühen Sie am besten doch die Tastatur oder eine alternative Lösung eines Drittanbieters. Per VBA können Sie den DatePicker so anzeigen: RunCommand acCmdShowDatePicker
5.1.3 Texte als Hyperlink anzeigen Mit der Eigenschaft Als Hyperlink anzeigen können Sie festlegen, dass auch Daten, die kein Hyperlink sind, als Hyperlink angezeigt werden (siehe Abbildung 5.4). Die VBAVariante dieser Eigenschaft heißt DisplayAsHyperlink. Beachten Sie, dass ein Klick auf ein so markiertes Element keinesfalls automatisch wie ein Hyperlink funktioniert.
Abbildung 5.4: Anzeigen von Daten als Hyperlink
5.1.4 Abgeschnittene Zahlenfelder Nicht direkt eine Steuerelementeigenschaft, aber doch in diesem Zusammenhang interessant ist die Eigenschaft Aktuelle Datenbank|Anwendungsoptionen|Auf abgeschnittene Zahlenfelder prüfen in den Access-Optionen. Damit legen Sie fest, ob Zahlenwerte, die zu lang für ihr Textfeld sind, wie in Excel durch eine Reihe Rauten (#) ersetzt werden.
5.2 Schaltflächen Wie Sie Schaltflächen anlegen, diese mit Ereignissen versehen und mehr erfahren Sie in der Onlinehilfe im Artikel »Verwenden einer Befehlsschaltfläche zum Starten einer Aktion oder einer Reihe von Aktionen«, hier vor allem in den hinteren Abschnitten. Neu ist bei den Schaltflächen vor allem das lang ersehnte gleichzeitige Anzeigen eines Symbols und einer Beschriftung. Damit braucht man zu diesem Zweck nicht mehr das
292
Steuerelemente
etwas unkomfortable CommandButton-Steuerelement der Bibliothek Microsoft Forms 2.0 einzusetzen. Ein Icon fügen Sie ganz einfach über die Eigenschaft Bild hinzu – dort öffnet sich ein Dialog, mit dem Sie eines der eingebauten oder ein benutzerdefiniertes Icon auswählen können. Sie können auch andere Dateiformate als die vom Dialog angezeigten .bmp und .ico verwenden – nämlich alle von GDI+ verarbeitbaren Formate, also .jpg, .png, .gif, .tif, .emf und .wmf. Abbildung 5.5 zeigt drei Beispiele für die Gestaltungsmöglichkeiten mit den neuen Schaltflächeneigenschaften – Ihrer Fantasie sind hier aber (fast) keine Grenzen gesetzt.
Abbildung 5.5: Verschiedene Schaltflächen-Designs
Außerdem bieten Schaltflächen folgende neue Möglichkeiten: Wahlweise normaler oder transparenter Hintergrund (Eigenschaft Hintergrundart, VBA: BackStyle, Einstellungen: Normal (1), Transparent (2)). Bei transparentem Hinter grund können Sie beispielsweise ein andersfarbiges Rechteck hinter die Schaltfläche legen, um eine alternative Hintergrundfarbe zu erzeugen. Beschriftung zum Bild anordnen (Eigenschaft Anordnung der Bildbeschriftung, VBA: PictureCaptionArrangement, Werte: acNoPictureCaption (0), acGeneral (1), acTop (2), acBottom (3), acLeft (4), acRight (5)) Bild/Beschriftung ausrichten (Eigenschaft Ausrichtung, VBA: Alignment, Werte: All gemeine Ausrichtung (0), links (1), zentriert (2, Standardwert), rechte (3), verteilt (4)) Beim Überfahren einer Schaltfläche kann der Mauszeiger in eine Hand umgewandelt werden (Eigenschaft Cursor beim Bewegen, VBA: CursorOnHover, Werte: acCursor OnHoverDefault (0), acCursorOnHoverHyperlinkHand (1))
5.3 Kombinationsfelder Den Inhalt von Kombinationsfeldern können Sie ab jetzt per eingebautem Dialog editieren, wenn folgende Bedingungen erfüllt sind: Erstens muss das Kombinationsfeld ungebunden sein, zweitens darf die Wertliste nur eine Spalte haben und drittens muss
293
Kapitel 5
die Eigenschaft Wertlistenbearbeitung zulassen (AllowValueListEdits) den Wert Ja haben. Für ein Standardkombinationsfeld müssen Sie dazu nur die Eigenschaft Herkunftsart (RowSourceType) auf Wertliste einstellen. Abbildung 5.6 zeigt die Schaltfläche, die beim Ausklappen des Kombinationsfeldes erscheint.
Abbildung 5.6: Ein (noch) leeres Kombinationsfeld
Mit dieser Schaltfläche öffnen Sie den Dialog aus Abbildung 5.7. Dort können Sie Einträ ge hinzufügen (jeden in eine Zeile) und einen Standardwert festlegen. Damit ändern Sie allerdings eine Eigenschaft und damit auch den Entwurf des Formulars – was bedeutet, dass Sie beim Schließen das Speichern des Formulars bestätigen müssen. Sie können dies umgehen, indem Sie das Formular mit einer eigenen Schaltfläche schließen, deren Beim Klicken-Ereignisprozedur die folgende Zeile enthält: Docmd.Close acForm, Me.Name, acSaveYes
Abbildung 5.7: Bearbeiten der Wertliste eines Formulars
Das Erweitern einer Wertliste von Kombinationsfeldern und Listenfeldern ist somit in einer Runtime-Version der Datenbank nicht verfügbar; hier müssen Sie eine eigene Lö sung programmieren.
294
Steuerelemente
5.3.1 Wertliste erben Wenn Sie Verknüpfungsfelder mit einem Nachschlagefeld ausstatten, das die schnelle Auswahl der Daten ermöglicht (etwa zur Auswahl der Anrede aus einer Wertliste), übernimmt Access dies beim Einfügen eines solchen Feldes aus der Feldliste in ein Formular. Mit der Eigenschaft Wertliste erben (InheritValueList) können Sie festlegen, ob Änderungen, die Sie im Kombinationsfeld an der Wertliste vornehmen, in die Auswahldaten des Feldes übertragen werden. Gleichzeitig muss natürlich die Eigenschaft Wertlistenbearbeitung zu lassen aktiviert sein.
5.3.2 Formular zum Bearbeiten anzeigen Für die Eigenschaft Bearbeitungsformular für Listenelemente können Sie ein Formular angeben, mit dem man die Datensatzherkunft des Kombinationsfelds bearbeiten kann. Access öffnet das Formular dann auf die gleiche Weise wie den Dialog zum Eingeben von Wertlisteneinträgen. Leider erkennt Access nicht automatisch, welcher Datensatz gerade im Listenfeld angezeigt wird, und öffnet das Bearbeitungsformular ungefiltert. Nach dem Bearbeiten aktualisiert Access die Datensatzherkunft des Kombinationsfeldes. Die VBA-Bezeichnung für diese Eigenschaft ist ListItemsEditForm.
5.3.3 Wachsen und Schrumpfen Sie können für Kombinationsfelder in Berichten die Eigenschaften Vergrößerbar und Ver kleinerbar einstellen. Die Eigenschaft wirkt genau wie bei Textfeldern. VBA-Bezeichnun gen: CanGrow und CanShrink.
5.3.4 Hyperlinks Genau wie bei Textfeldern können Sie auch für Kombinationsfelder entscheiden, ob auch Inhalte, die kein Hyperlink sind, als Hyperlink formatiert werden sollen (Als Hy perlink anzeigen/DisplayAsHyperlink).
5.3.5 Mehrwertige Felder Kombinations- und Listenfelder liefern einige Eigenschaften, die Sie in Zusammenhang mit mehrwertigen Feldern einsetzen können: ItemsSelected, Selected und SeparatorCharac ters. Die Technik scheint noch unausgegoren; wenn sie möchten, experimentieren Sie selbst mit diesen Eigenschaften. Empfehlung: Verwenden Sie die bekannten Techniken zur Anzeige von Daten in m:n-Beziehungen – speichern Sie alle Daten in Tabellen, ver knüpfen Sie diese ebenfalls über eine Tabelle und setzen Sie dann die in Kapitel 4, »For mulare«, vorgestellten Techniken ein.
295
Kapitel 5
5.4 Kombinationsfeld-Techniken In den folgenden Abschnitten finden Sie einige oft benötigte Techniken für den Einsatz von Kombinationsfeldern.
5.4.1 Kombinationsfeld aufklappen Wenn Sie ein Kombinationsfeld beim Öffnen des Formulars oder zu einer anderen Gele genheit aufklappen möchten, verwenden Sie die DropDown-Methode des Kombinations feldes (siehe Abbildung 5.8). Vorher müssen Sie diesem Steuerelement noch den Fokus zuweisen: Private Sub Form_Current() Me!KategorieID.SetFocus Me!KategorieID.Dropdown End Sub Listing 5.2: Aufklappen eines Kombinationsfeldes
5.4.2 Auswählen-Eintrag hinzufügen Leere Kombinationsfelder sehen nicht schön aus. Mit der folgenden Datensatzherkunft können Sie den enthaltenen Positionen den Eintrag voranstellen: SELECT 0, '' AS Firma FROM tblLieferanten UNION SELECT LieferantID, Firma FROM tblLieferanten ORDER BY Firma;
Nun müssen Sie noch dafür sorgen, dass der künstlich hinzugefügte Eintrag automatisch ausgewählt wird, wenn das Feld noch keinen Wert hat. Und das geht ganz einfach:
296
Steuerelemente
Stellen Sie einfach den Standardwert des zugrunde liegenden Feldes in der Tabelle oder des Steuerelements auf 0 ein (Abbildung 5.9).
Abbildung 5.9: Bei neuen Datensätzen zeigt das Kombinationsfeld mit den Lieferanten einen passenden Eintrag an
5.4.3 Abhängige Kombinationsfelder Früher oder später brauchen Sie einmal zwei Kombinationsfelder, bei denen der Wert des zweiten vom aktuellen Wert des ersten Kombinationsfeldes abhängt. Gehen Sie dann folgendermaßen vor: Erstellen Sie zwei Kombinationsfelder auf Basis der Tabellen tblKategorien und tblArtikel der Beispieldatenbank, die jeweils an die ID gebunden sind und die Kategorie beziehungsweise den Artikel anzeigen (siehe Abbildung 5.10). Legen Sie die folgenden beiden Ereignisprozeduren sowie die Prozedur ArtikelAktu alisieren an. Private Sub cboKategorien_AfterUpdate() ArtikelAktualisieren End Sub Private Sub Form_Load() ArtikelAktualisieren End Sub Private Sub ArtikelAktualisieren() Me!cboArtikel.RowSource = "SELECT ArtikelID, Artikelname " _ & "FROM tblArtikel WHERE KategorieID = " & Nz(Me!cboKategorien,0) End Sub Listing 5.3: Diese Routinen benötigen Sie für abhängige Kombinationsfelder
Die beiden Ereignisprozeduren sorgen beim Öffnen sowie nach dem Aktualisieren des Kategorien-Kombinationsfelds für das erneute Abfragen der passenden Artikel.
297
Kapitel 5
Abbildung 5.10: Abhängige Kombinationsfelder
5.4.4 Bestimmten Eintrag auswählen Gerade beim vorherigen Beispiel ist es sinnvoll, beim Öffnen einen Eintrag aus dem Listenfeld auszuwählen. Dazu verwenden Sie die Eigenschaft ItemData. Passen Sie die folgenden zwei der im vorherigen Beispiel vorgestellten Routinen an: Private Sub Form_Load() Me!cboKategorien = Me!cboKategorien.ItemData(0) ArtikelAktualisieren End Sub Private Sub ArtikelAktualisieren() Me!cboArtikel.RowSource = "SELECT ArtikelID, Artikelname " _ & "FROM tblArtikel WHERE KategorieID = " & Me!cboKategorien Me!cboArtikel = Me!cboArtikel.ItemData(0) End Sub Listing 5.4: Diese Routinen sorgen für die Vorauswahl des jeweils ersten Eintrags eines Kombinationsfelds
Das Formular aus Abbildung 5.11 zeigt die beim Laden ausgewählten Einträge an. Wichtig ist, dass Sie die Datensatzherkunft des zweiten, abhängigen Kombinationsfeldes zunächst aktualisieren und dann erst den ersten Eintrag auswählen.
Abbildung 5.11: Formular mit zuvor ausgewählten Datensätzen
298
Steuerelemente
5.4.5 Aktuell markierten Eintrag auslesen Beim Auslesen von Werten aus Kombinationsfeldern besteht das Problem meist im Zu griff auf die einzelnen Spalten. Hier enthält die erste Spalte die ID und wird nicht angezeigt, die zweite Spalte schließlich liefert den angezeigten Wert (siehe Abbildung 5.12).
Abbildung 5.12: Auslesen des aktuell markierten Eintrags
Mit der folgenden Ereignisprozedur zeigen Sie per Mausklick die erste und die zweite Spalte des aktuell ausgewählten Eintrags des Kombinationsfeldes an: Private Sub cmdEintragAnzeigen_Click() MsgBox "ID: " & Me.LieferantID & vbCrLf & " Angezeigter Wert: " & Me.LieferantID.Column(1) End Sub Listing 5.5: Anzeigen verschiedener Spalten des aktuellen Eintrags eines Kombinationsfelds
5.4.6 Wert zu einem gebundenen Kombinationsfeld hinzufügen Wenn Sie dem Benutzer erlauben wollen, über ein Kombinationsfeld Werte einzutippen, die dann in der zugrunde liegenden Datenbank gespeichert werden, legen Sie die folgende Prozedur für das Ereignis Nicht in Liste an: Private Sub cboKategorien_NotInList(NewData As String, Response As Integer) Dim db As DAO.Database Dim rst As DAO.Recordset2 Set db = CurrentDb Set rst = db.OpenRecordset("tblKategorien", dbOpenDynaset) rst.AddNew rst!Kategoriename = NewData
299
Kapitel 5 rst.Update Response = acDataErrContinue rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 5.6: Hinzufügen eines Tabelleneintrags per Kombinationsfeld
Das Ganze können Sie natürlich auch noch ausbauen – etwa, indem Sie eine Rückfrage einbauen, die den Benutzer vor dem Speichern fragt, ob er tatsächlich einen neuen Datensatz anlegen möchte. Außerdem fehlt natürlich noch eine Fehlerbehandlung.
5.4.7 Weitere Techniken Eine weitere Technik in Zusammenhang mit Kombinationsfeldern finden Sie in Kapitel 4, Abschnitt 4.4, »Von Formular zu Formular«. Dort erfahren Sie, wie Sie die von einem Kombinationsfeld angezeigten Daten mit Hilfe eines Formulars bearbeiten können. In der Onlinehilfe finden Sie im Übrigen noch Hinweise zu den interessanten Funktionen zum Hinzufügen und Entfernen von Listeneinträgen: AddItem und RemoveItem.
5.5 Listenfelder Listenfelder bieten wesentlich weniger neue Features als Kombinationsfelder – und gar keine, die Sie nicht schon vom Kombinationsfeld her kennen. Also stellen die folgenden Abschnitte einige bewährte Listenfeld-Techniken vor. Dabei ist anzumerken, dass die meisten in Zusammenhang mit Kombinationsfeldern genannten Techniken auch für Listenfelder gelten (also auf verschiedene Spalten des aktuellen Eintrags zugreifen, abhängige Listenfelder erstellen, einen bestimmten Eintrag festlegen und so weiter).
5.5.1 Mehrfachauswahl auslesen Im Gegensatz zu Kombinationsfeldern (mit Ausnahme dieser neumodischen mehrwertigen Felder natürlich ...) können Sie mit Listenfeldern einen oder mehrere Einträge auswählen. Dazu stellen Sie die Eigenschaft Mehrfachauswahl entweder auf Einzeln oder Erweitert ein. Die ausgewählten Einträge gibt eine Routine mit identischem Aufbau für beide Varianten aus (siehe Abbildung 5.13): Private Dim Dim For
300
Sub cmdAusgabeMehrfachauswahlEinzeln_Click() var As Variant str As String Each var In Me.lstMehrfachauswahlEinfach.ItemsSelected
Steuerelemente str = str & var & vbCrLf Next var MsgBox str End Sub Listing 5.7: Auslesen eines Listenfeldes mit Mehrfachauswahl
Abbildung 5.13: Mehrfachauswahl einfach und erweitert auslesen
Wenn Sie nicht nur die erste, sondern auch die folgenden Spalten lesen möchten, greifen Sie über die Column-Eigenschaft auf die weiteren Spalten zu. Wenn Sie die Eigenschaft Mehrfachauswahl auf Keine einstellen, können Sie einfach über die Column-Eigenschaft auf den ausgewählten Wert zugreifen. Achten Sie darauf, dass kein Wert ausgewählt sein könnte und prüfen Sie den Wert des Listenfeldes zuvor mit IsNull auf einen Null-Wert.
5.5.2 Ja/Nein-Felder im Listenfeld anzeigen Ja/Nein-Felder sehen im Listenfeld immer etwas daneben aus: Immerhin ist die passende Spalte mit den Werten 0 und -1 gefüllt. Um dies zu ändern, passen Sie die entsprechend an, und zwar so, dass das Ja/Nein-Feld in eine IIf-Funktion gepackt wird, die für den Wert True die Zeichenfolge x und für den Wert False eine leere Zeichenfolge ausgibt (siehe Abbildung 5.14): SELECT tblArtikel.ArtikelID, tblArtikel.Artikelname, IIf([Auslaufartikel], "x","") AS [Läuft aus] FROM tblArtikel;
301
Kapitel 5
Abbildung 5.14: Hübsches Ja/Nein-Feld im Listenfeld
5.5.3 Weitere Techniken In Kapitel 4, »Formulare«, finden Sie weitere Listenfeld-Techniken: Abschnitt 4.5.4, »Da ten in der Übersicht als Listenfeld«, Abschnitt 4.5.9 »1:n-Beziehung per Listenfeld« und Abschnitt 4.5.11, »m:n-Beziehungen per Listenfeld«.
5.6 Unterformulare Unterformulare kommen mit einer neuen Eigenschaft: Leeren Hauptentwurf filtern (VBA: FilterOnEmptyMaster). Mit der Eigenschaft legen Sie fest, ob das Unterformular bei einem leeren Datensatz im Hauptformular alle Datensätze (Einstellung: Nein) oder keinen (Standard, Einstellung: Ja) anzeigen soll (siehe Abbildung 5.15).
Abbildung 5.15: Die neue Eigenschaft von Unterformular-Steuerelementen
302
Steuerelemente
5.7 Das Anlagen-Steuerelement Sicher erwarten Sie in diesem Kapitel eine großartige Abhandlung über das seit langem erste neue Steuerelement von Access. Aber weit gefehlt! Die Vorstellung des AnlagenSteuerelements befindet sich in Kapitel 11, »Bilder und binäre Daten«. Der Grund ist, dass dieser Themenkomplex mehrere Objektarten stark ineinander verwickelt – hier gehören Tabellen, Formulare, Berichte und natürlich auch VBA-Module dazu.
5.8 Optionsgruppe, Umschaltfläche, Kontrollkästchen, Bildsteuerelement und Co. Informationen zu diesen Steuerelementen finden Sie in der Onlinehilfe von Access. Im Vergleich zu den bereits detaillierter beschriebenen Steuerelementen werden diese weniger genutzt und bieten nicht so viele interessante Techniken. Es gibt übrigens noch eine neue Art, ein Bildsteuerelement zum Formular hinzuzufügen: Wenn Sie im Ribbon unter Entwurf|Steuerelemente ganz links auf die Schaltfläche Logo klicken, fügt Access dem Formularkopf ein Bildsteuerelement hinzu, für das Sie direkt beim Anlegen ein Bild auswählen müssen. Dieser Vorgang lässt sich nicht wie beim Anlegen eines Bildsteuerelements abbrechen, um so ein leeres Bildsteuerelement zu erzeugen. Statt des Bildsteuerelements sollten Sie im Übrigen besser das Anlage-Feld oder das Image-Steuerelement der MSForms-Bibliothek verwenden (mehr dazu in Kapitel 11, »Bil der und binäre Daten«)
5.9 Weitere Steuerelementeigenschaften Einige Eigenschaften sind für mehrere Steuerelemente hinzugekommen. Die folgenden Absätze erläutern, worum es sich dabei handelt.
5.9.1 Steuerelemente verankern Eine der interessantesten Neuerungen von Access 2007 ist das Verankern von Steuer elementen: Damit ist die Ereigniseigenschaft Bei Größenänderung quasi obsolet, zumindest was das Anpassen der Steuerelementgröße angeht. Die Abbildungen 5.16 und 5.17 zeigen, wie sich die verankerten Steuerelemente eines Formulars bei einer Größenänderung des Formulars mit verändern.
303
Kapitel 5
Abbildung 5.16: Ein Formular mit verankerten Steuerelementen im Ausgangszustand ...
Abbildung 5.17: ... und im vergrößerten Zustand
Das Verankern der Steuerelemente können Sie über die Benutzeroberfläche oder per VBA vornehmen. In der Benutzeroberfläche finden Sie die passenden Schaltflächen des Ribbons nach Aktivieren des betroffenen Steuerelements in der Entwurfsansicht im Bereich Anordnen|Schriftgrad|Anker (fragen Sie nicht, was das mit dem Schriftgrad zu tun hat ...). Hier können Sie auswählen, wie ein Objekt verankert und wohin es sich ausdehnen soll. Im Eigenschaftsfenster legen Sie die notwendigen Einstellungen mit
304
Steuerelemente
den Eigenschaften Horizontaler Anker und Vertikaler Anker (VBA: HorizontalAnchor und VerticalAnchor, Werte: acHorizontalAnchorLeft (0), acHorizontalAnchorRight (1) und acHorizontalAnchorBoth (2) für den horizontalen Anker, acVerticalAnchorTop (0), acVerticalAnchorBottom (1), acHorizontalAnchorBoth (2)) fest. Dazu eine kurze Erläuterung: Wenn Sie Horizontaler Anker für ein Steuerelement auf Links und Vertikaler Anker auf Oben festlegen, tut sich beim Ändern der Formulargröße gar nichts. Stellen Sie Horizontaler Anker auf Rechts ein, bewegt sich das Steuerelement beim Vergrößern der Breite entsprechend nach rechts. Die Einstellung Beide sorgt dafür, dass der Abstand vom linken und vom rechten Rand gleich bleibt und das Steuerelement seine Breite entsprechend dem Formular anpasst. Gleiches gilt für das vertikale Verankern: Oben bedeutet, dass der obere Rand des Steuerele ments auf jeden Fall seine vertikale Position beibehält. Unten heißt, dass das Steuerelement sich mit dem unteren Formularrand nach unten bewegt, und die Einstellung Beide führt zum Vergrößern der Höhe des Steuerelements. Eine Vergrößerung der Breite oder Höhe erfolgt übrigens nicht proportional zur Vergrößerung des Formulars, sondern absolut – das heißt, wenn Sie das Formular um zwei Zentimeter verbreitern, wirkt sich das im gleichen Maße auch auf Steuerelemente aus, für die Sie Horizontaler Anker auf Beide eingestellt haben. In den Formularen frmListenfeldMehrfachauswahl und frmListViewMitDetaildaten der Beispiel datenbank finden Sie weitere Beispiele für das Verankern von Steuerelementen.
5.9.2 Layout oder nicht? Die Eigenschaft Layout (nur VBA) gibt an, ob das Steuerelement zu einem der beiden Lay outs Tabelle oder Gestapelt gehört. Da ein Formular mehrere Layouts aufweisen kann, gibt die Eigenschaft LayoutID (ebenfalls nur VBA) zusätzlich die ID des Layouts zurück. Beide Eigenschaften sind schreibgeschützt. Layout kann die Werte 0 (acLayoutNone, kein Layout), 1 (acLayoutStacked, gestapeltes Layout) oder 2 (acLayoutTabular, Tabellen-Layout) annehmen.
5.9.3 Gitternetzlinien In den beiden Layouts Tabelle und Gestapelt können Sie Gitternetzlinien aktivieren (RibbonEintrag Format|Gitternetzlinien|Gitternetzlinien). Die folgenden vier Eigenschaften (VBAVersion in Klammern) Linienart für Gitternetzlinien oben (GridlineStyleTop), Linienart für Gitternetzlinien unten, (GridlineStyleBottom), Linienart für Gitternetzlinien links (GridlineStyleLeft) und Linienart für Gitternetzlinien rechts (GridlineStyleRight) sowie
305
Kapitel 5
Gitternetzlinienbreite oben (GridlineWidthTop), Gitternetzlinienbreite unten (GridlineStyleBottom), Gitternetzlinienbreite links (GridlineStyleLeft) und Gitternetzlinienbreite rechts (GridlineStyleRight) legen fest, wie das Gitter aussieht. Fehlt noch die Farbe: Dafür ist die Eigenschaft Gitter netzlinienfarbe (GridlineColor) zuständig. Mit dem oben genannten Ribbon-Eintrag erstellen Sie Gitternetzlinien auf die Schnelle, daneben finden Sie noch Einträge zum Einstellen der Breite, Formatvorlage (Linienart) und Farbe per Mausklick.
5.9.4 Textabstand Mit den folgenden Eigenschaften legen Sie die Abstände zu den benachbarten Steuerele menten in den beiden Layouts Tabelle und Gestapelt fest: Textabstand oben (TopPadding) Textabstand unten (BottomPadding) Textabstand links (LeftPadding) Textabstand rechts (RightPadding)
5.10 Das TreeView-Steuerelement Mit dem TreeView-Steuerelement können Sie hierarchische Daten in Formularen anzeigen und diese – entsprechende Programmierung vorausgesetzt – dort auch bearbeiten. Die wichtigste Rolle spielen dabei die so genannten Knoten (nodes). Jedes Element im TreeView entspricht einem Knoten. Damit die Knoten hierarchisch angeordnet werden können, weist man untergeordneten Knoten den jeweiligen Parent-Knoten zu. Das TreeView-Steuerelement liefert viele Möglichkeiten zum Anpassen seines Aussehens. So lassen sich die folgenden Möglichkeiten leicht kombinieren: Text Symbole Plus-/Minus-Zeichen zum Auf- und Zuklappen von Verzweigungen Linien Kontrollkästchen zum Anhaken von Elementen
306
Steuerelemente
Herkunft Das TreeView-Steuerelement und die übrigen Steuerelemente der Bibliothek Microsoft Windows Common Controls sind fester Bestandteil einer Office 2007-Installation.
Alternativen Es gibt zwar einige Alternativen zum TreeView-Steuerelement von Microsoft, unter anderem solche, die entweder ebenfalls als .ocx-Datei kommen oder keine zusätzlichen Da teien erfordern und komplett auf API basieren, aber diese sind meist nicht kostenlos.
Version der ActiveX-Steuerelemente Sie sollten generell immer die gleiche Version verwenden, wenn Sie mehrere Steuerele mente der Common Controls-Sammlung einsetzen – am besten die jeweils neuesten. Die Steuerelemente der aktuellen Version sind mit der Versionsnummer 6.0 gekennzeichnet.
5.10.1 TreeView anlegen Sie fügen ein TreeView-Steuerelement in ein Formular ein, indem Sie die Ribbon-Schalt fläche Entwurf|Steuerelemente|ActiveX-Steuerelement einfügen betätigen und aus dem Dia log aus Abbildung 5.18 den gewünschten Eintrag auswählen.
Abbildung 5.18: Hinzufügen eines ActiveX-Steuerelements
307
Kapitel 5
Anschließend klicken Sie im Formular auf die passende Stelle, um das TreeViewSteuerelement einzufügen. Das neue TreeView-Steuerelement erscheint zunächst als unscheinbares weißes Kästchen im Formular, offenbart aber direkt nach dem Vergrößern des Rahmens seinen wahren Charakter (siehe Abbildung 5.19). Benennen Sie das Steuerelement um, damit Sie anschließend per VBA komfortabel darauf zugreifen können. Verwenden Sie dabei einen Namen wie tvwTreeView. Zusammen mit dem TreeView-Steuerelement legt Access gleichzeitig einen Verweis auf die Herkunftsbibliothek dieses Steuerelements an (siehe Abbildung 5.20).
Abbildung 5.19: Ein frisch angelegtes TreeView-Steuerelement
Abbildung 5.20: Die Bibliothek des TreeView-Steuerelements im Verweise-Dialog
308
Steuerelemente
Die Objekte, Methoden und Eigenschaften dieser Bibliothek können Sie sich in aller Ruhe im Objektkatalog ansehen. Diesen öffnen Sie mit der Taste F2. Wählen Sie aus dem oberen Kombinationsfeld den Eintrag MSComctlLib aus und markieren links die Klasse TreeView, um die Eigenschaften und Methoden dieser Klasse anzuzeigen (siehe Abbildung 5.21).
Abbildung 5.21: Die Objekte, Methoden und Eigenschaften der Bibliothek MsComctlLib im Objektkatalog
IntelliSense für ActiveX-Steuerelemente TreeView-Steuerelemente s können Sie ausschließlich per VBA füllen. Dummerweise bietet IntelliSense nicht die TreeView-spezifischen Eigenschaften, Methoden und Ereignisse an, wenn Sie wie folgt auf das Steuerelement verweisen: Me!ctlTreeView
Wenn Sie IntelliSense verwenden möchten, was empfehlenswert ist, deklarieren Sie eine Objektvariable für das TreeView-Steuerelement im Modulkopf des Formulars und instanzieren diese an geeigneter Stelle. Die Deklaration sieht etwa wie folgt aus: Dim objTreeView As MSComctlLib.TreeView
Zum Instanzieren eignet sich am besten eine der Ereignisprozeduren, die beim Öffnen des Formulars ausgeführt werden. Da mit dem Beim Öffnen-Ereignis Probleme mit man-
309
Kapitel 5
chen ActiveX-Steuerelementen auftreten können, verwenden Sie am besten immer das Beim Laden-Ereignis: Private Sub Form_Load() Set objTreeView = Me!ctlTreeView.Object End Sub Listing 5.8: Instanzieren einer TreeView-Objektvariablen
Wenn Sie in weiteren Routinen auf das Objekt objTreeView zugreifen, statt das Steuerele ment ctlTreeView zu referenzieren, stellt IntelliSense die TreeView-spezifischen Elemente zur Verfügung (siehe Abbildung 5.22).
Abbildung 5.22: Mit der passenden Objektvariablen hilft IntelliSense beim Programmieren von ActiveX-Steuerelementen
Diese Vorgehensweise ist allerdings nur für die Zeit der Entwicklung anzuraten. Vor Fertigstellung der Anwendung sollten Sie diese Objekte entfernen und direkt auf das Steuerelement im Formular zugreifen. Anderenfalls kann es Probleme geben, wenn etwa ein Fehler auftritt: Die Objektvariable verliert dann die Referenz auf das Steuerelement und der nächste Zugriff führt automatisch zum Fehler. Es gibt noch einen alternativen Weg. Dabei deklarieren Sie zunächst eine Variable, die einen Verweis auf das TreeView-Steuerelement enthalten soll: Private mTreeView As MSComctlLib.TreeView
Außerdem fügen Sie dem Formular-Modul die folgende Property Get-Routine hinzu, die zunächst prüft, ob mTreeView schon einen Verweis auf das Steuerelement enthält, und diesen gegebenenfalls herstellt. Den Verweis gibt die Routine dann mit dem Parameter
310
Steuerelemente
objTreeView zurück. Auf diese Weise können Sie immer auf objTreeView zugreifen, ohne es explizit zu instanzieren. Property Get objTreeView() As MSComctlLib.TreeView If mTreeView Is Nothing Then Set mTreeView = Me.tvwTreeView.Object End If Set objTreeView = mTreeView End Property Listing 5.9: Get-Eigenschaft zum Referenzieren eines Steuerelements
5.10.2 Eigenschaften des TreeView-Steuerelements Wenn Sie in der Entwurfsansicht des Formulars mit der rechten Maustaste auf das TreeView-Steuerelement klicken, können Sie im Kontextmenü den Eintrag TreeCtrlObjekt|Eigenschaften auswählen. Mit diesem Dialog lassen sich die meisten Eigenschaften des TreeView-Steuerelements einstellen (siehe Abbildung 5.23).
Abbildung 5.23: Eigenschaften des TreeView-Steuerelements
5.10.3 Erzeugen eines Baumes Die wichtigste Methode beim Erzeugen eines Baumes im TreeView-Steuerelement ist die Add-Methode des Node-Elements. Die Methode erwartet sechs Parameter, die allesamt optional sind: Relative: Erwartet den Index (Datentyp Integer) oder Schlüssel (String) eines Knotens, zu dem der neue Knoten in einer bestimmten Beziehung stehen soll. Relationship: Beziehung, in der der neue und der unter Relative angegebene Knoten stehen. Mögliche Werte: tvwChild, tvwFirst, tvwLast, tvwNext, tvwPrevious.
311
Kapitel 5
Key: Zeichenfolge als Schlüssel des neuen Knotens. Dient zum Referenzieren des Knotens und muss zwingend mit einem Buchstaben beginnen. Text: Angezeigter Text Image: Index eines Symbols, das vor dem Text angezeigt wird, wenn die Einstellung der Style-Eigenschaft des TreeView-Objekts dies erlaubt. Weitere Informationen zur Verwendung von Symbolen in TreeView-Objekten finden Sie weiter unten. SelectedImage: Index eines Symbols, das statt des unter Image angegebenen Symbols angezeigt wird, wenn der Knoten markiert ist. Alle Eigenschaften können Sie nach dem Erzeugen des Knotens per VBA ändern. Wenn Sie beim Erzeugen noch gar keine Eigenschaften zuweisen möchten (etwa aus Gründen der Übersicht), können Sie einfach die folgende Anweisung verwenden: objTreeView.Nodes.Add
Wenn Sie, wie weiter oben beschrieben, ein Formular mit einem TreeView-Steuerelement angelegt, dieses in ctlTreeView umbenannt und ein entsprechendes Objekt im Formu larmodul deklariert und instanziert haben, können Sie direkt das erste Element im TreeView anlegen. Dazu erweitern Sie einfach das obige Listing wie folgt. Beachten Sie, dass die Eigenschaften Key und Text direkt beim Anlegen mit den entsprechenden Werten gefüllt werden: Private Sub Form_Load() Set objTreeView = Me.ctlTreeView.Object With objTreeView .Nodes.Add , , "tvw01", "Erster Knoten" End With End Sub Listing 5.10: Anlegen des ersten Elements
Öffnen Sie das Formular, das nun wie in Abb. 5.24 aussehen sollte.
Abbildung 5.24: Ein einfaches Beispiel für ein TreeView-Steuerelement
312
Steuerelemente
Mit der leicht erweiterten Fassung aus folgendem Listing legen Sie drei Unterelemente an, die wie in Abbildung 5.25 angeordnet sind. Die Routine löscht alle Elemente, bevor es diese neu anlegt, da das Schreiben von zwei Elementen mit gleichem Key-Wert einen Fehler auslöst. Bevor Sie die Unterelemente sehen, müssen Sie noch doppelt auf das Wurzelelement klicken. Private Sub Form_Load() With objTreeView .Nodes.Clear 'Basisknoten anlegen .Nodes.Add , , "tvw01", "Erster Knoten" 'Knoten unterhalb des Basisknotens anlegen .Nodes.Add "tvw01", tvwChild, "tvw0101", "Erster Unterknoten" 'Knoten hinter einem existierenden Knoten anlegen .Nodes.Add "tvw0101", tvwNext, "tvw0102", "Zweiter Unterknoten" 'Knoten als ersten Knoten in der Ebene des angegebenen Knoten 'anlegen .Nodes.Add "tvw0101", tvwFirst, "tvw0100", "Nullter Unterknoten" End With End Sub Listing 5.11: Anlegen von Wurzel- und drei Child-Elementen
Abbildung 5.25: TreeView mit Knoten auf zwei Ebenen
5.10.4 Stil einstellen Wenn Sie keinen bestimmten Stil festlegen, verwendet Access standardmäßig den Stil tvwTreelinesText wie in obiger Abbildung. Wenn Sie einen anderen Stil verwenden möchten, stellen Sie die Eigenschaft Style des TreeView-Objekts etwa mit folgender Anweisung ein: objTreeView.Style = tvwPictureText
Das Aussehen der verschiedenen Stile können Sie sich ansehen, wenn Sie dem Formular ein Kombinationsfeld namens cboStil hinzufügen und seine Eigenschaften Spaltenanzahl
313
Kapitel 5
auf 2, Spaltenbreiten auf 0, Herkunftsart auf Wertliste und Datensatzherkunft auf die folgende Liste einstellen: 0;"tvwTextOnly"; 1;"tvwPictureText";2;"tvwPlusMinusText"; 3;"tvwPlusPictureText";4;"tvwTreelinesText";5;"tvwTreelinesPictureText"; 6;"tvwTreelinesPlusMinusText";7;"tvwTreelinesPlusMinusPictureText"
Wenn Sie dann noch die folgende Prozedur für die Ereigniseigenschaft Nach Aktualisierung des Kombinationsfeldes hinterlegen, können Sie die einzelnen Stile bequem ausprobieren (siehe Abbildung 5.26): Private Sub cboStil_AfterUpdate() objTreeView.Style = CLng(Me!cboStil) End Sub
Abbildung 5.26: Auswahl des Stils per Kombinationsfeld
5.10.5 Element-Eigenschaften per VBA zuweisen Mit der Add-Methode der Nodes-Auflistung können Sie bereits einige Eigenschaften eines neuen Elements festlegen. Es gibt aber noch wesentlich mehr Eigenschaften, die Sie anpassen können, wenn Sie das neue Element direkt beim Anlegen einer Objektvariablen zuweisen. Dazu deklarieren Sie zunächst eine entsprechende Objektvariable: Dim objNode As MSComctlLib.Node
Das Anlegen eines Elements mit passender Objektvariablen und anschließender Anpas sung der Eigenschaften sieht wie folgt aus: Set objNode = objTreeView.Nodes.Add(, , "tvw01", "Erster Knoten") With objNode 'Fette Schrift verwenden .Bold = True 'Hintergrundfarbe .BackColor = 255
314
Steuerelemente 'Schriftfarbe .ForeColor = 128 'Unterknoten anzeigen .Expanded = True End With Listing 5.12: Referenzieren eines neuen Elements und Zuweisen einiger Eigenschaften
5.10.6 Symbole im TreeView Richtig hübsch wird ein TreeView-Steuerelement erst, wenn Sie die einzelnen Elemente mit Symbolen versehen. Dabei können Sie jedem Element ein anderes Symbol verpassen; es ist sogar möglich, zusätzliche Symbole für unterschiedliche Zustände eines Elements anzugeben. Die dazu verwendeten Symbole speichern Sie in einem ImageListSteuerelement. Umfangreiche Informationen zum ImageList-Steuerelement und zum Füllen von TreeView-Elementen mit den enthaltenen Bildern finden Sie weiter unten in Abschnitt 5.12, »Das ImageList-Steuerelement«. Ein Beispiel finden Sie im Formular frmTreeViewMitIcons – dort können Sie auch die verschiedenen Stile ansehen (siehe Abbildung 5.27).
Abbildung 5.27: Die verschiedenen Stile des TreeView-Steuerelements mit Icons
5.10.7 Daten aus Tabellen im TreeView-Steuerelement darstellen Das TreeView-Steuerelement macht nur wenig Sinn, wenn man es nicht dynamisch mit Daten aus Tabellen füllen kann. Dabei kann man damit nicht nur Daten aus mehreren
315
Kapitel 5
verknüpften Tabellen (etwa Kunden und ihre Bestellungen), sondern auch solche aus reflexiven Beziehungen (Vorgesetzte und Mitarbeiter) anzeigen.
5.10.8 Daten aus verknüpften Tabellen anzeigen Beim Anzeigen von Daten aus verknüpften Tabellen werden die in den Tabellen enthaltenen Datensätze entsprechend der Anzeige im TreeView-Steuerelement durchlaufen. Bei zwei Tabellen bedeutet dies, dass in einer äußeren Schleife die Datensätze der Haupttabelle und in einer inneren Schleife die Datensätze der untergeordneten Tabelle durchlaufen werden. Das Füllen des TreeView-Steuerelements erfolgt am besten in der Ereignisprozedur, die beim Laden des Formulars ausgelöst wird. Als Basis für das folgende Beispiel dienen die Tabellen tblKunden und tblBestellungen sowie ein Formular mit einem TreeView-Steuerelement namens tvwKundenUndBestellungen. Damit Sie das TreeViewSteuerelement jederzeit mit dem Ausdruck objTreeView referenzieren können, fügen Sie folgende Zeilen in den Kopf des Formular-Moduls ein (Sie kennen diese Zeilen schon, aber diesmal verwenden diese einen anderen Namen für das TreeView-Steuerelement – ein bisschen Abwechslung muss sein): Private mTreeView As MSComctlLib.TreeView Property Get objTreeView() As MSComctlLib.TreeView If mTreeView Is Nothing Then Set mTreeView = Me.tvwKundenUndBestellungen.Object End If Set objTreeView = mTreeView End Property
Die folgende Routine füllt das TreeView-Steuerelement mit den Daten der beiden Tabel len: Private Sub Form_Load() Dim db As DAO.Database Dim rstKunden As DAO.Recordset2 Dim rstBestellungen As DAO.Recordset2 Dim objNode As MSComctlLib.Node Set db = CurrentDb Set rstKunden = db.OpenRecordset("tblKunden") objTreeView.Nodes.Clear Do While Not rstKunden.EOF Set objNode = objTreeView.Nodes.Add(, , "Kunde" _ & rstKunden!KundeID, rstKunden!Firma) Set rstBestellungen = db.OpenRecordset("SELECT * FROM " _ & "tblBestellungen WHERE KundeID = " & rstKunden!KundeID) Do While Not rstBestellungen.EOF
316
Steuerelemente objTreeView.Nodes.Add "Kunde" & rstKunden!KundeID, _ tvwChild, "Bestellung" & rstBestellungen!BestellungID, _ rstBestellungen!BestellungID & "/" _ & rstBestellungen!Bestelldatum rstBestellungen.MoveNext Loop rstKunden.MoveNext Loop Set objNode = Nothing rstKunden.Close rstBestellungen.Close Set rstKunden = Nothing Set rstBestellungen = Nothing Set db = Nothing End Sub Listing 5.13: Diese Routine füllt das TreeView-Steuerelement mit Daten aus den zwei verknüpften Tabellen tblKunden und tblBestellungen
Dabei deklariert und instanziert die Routine zunächst ein Database- und zwei Recordset2Objekte zum Referenzieren der beiden Tabellen tblKunden und tblBestellungen. Die erste Do While-Schleife durchläuft die Kunden. Dabei legt die Routine für jeden Kun den ein Element im TreeView an. Innerhalb dieser Schleife wird außerdem das Recordset2Objekt rstBestellungen mit den Bestellungen gefüllt, die zum aktuellen Kunden gehören. Diese werden in einer weiteren Do While-Schleife unterhalb des aktuellen Kunden in das TreeView-Steuerelement eingefügt. Zu beachten ist dabei die Benennung der Key-Eigenschaft eines jeden Knotens. Dieser muss wie gehabt aus einer Zeichenkette mit einem Buchstaben am Anfang bestehen und eindeutig sein. Außerdem sollte man aus dieser Eigenschaft möglichst den zugrunde liegenden Datensatz identifizieren können – etwa über den Primärschlüssel. Diesen integriert man daher in den Wert der Key-Eigenschaft. Normalerweise könnte man einfach einen beliebigen Buchstaben (etwa »a«) verwenden und hinten den Wert des Primärschlüsselfeldes der Tabelle anhängen: "a" & rst!ID
Wenn Sie es aber wie im vorliegenden Fall mit mehr als einer Tabelle zu tun haben, ist die Eindeutigkeit der Primärschlüsselfelder nicht mehr gewährleistet: Es kann ja durchaus in beiden Tabellen einen Datensatz geben, dessen Primärschlüssel den Wert 1 aufweist. Also lassen Sie den Key-Wert für jede Tabelle mit einer alternativen Zeichenfolge beginnen – etwa mit »Kunde...« und »Bestellung...«. Das Ergebnis der Routine sieht wie in Abbildung 5.28 aus. Per Doppelklick auf einen der Kunden zeigen Sie die zu dem Kunden gehörenden Bestellungen an.
317
Kapitel 5
Abbildung 5.28: Kunden und ihre Bestellungen im TreeView-Steuerelement
5.10.9 Reflexive Daten im TreeView-Steuerelement Die Tabelle tblPersonal der Beispieldatenbank liefert ein gutes Beispiel für eine reflexive Beziehung. Einige Datensätze sind über das Feld Vorgesetzter mit Feldern der gleichen Tabelle verknüpft und liefern damit Informationen über die Hierarchie der Mitarbeiter. Diese lässt sich natürlich in einem TreeView-Steuerelement weitaus besser veranschaulichen als in einer Tabelle (siehe Abbildung 5.29).
Abbildung 5.29: Mitarbeiter und ihre Untergebenen
Für das Füllen des TreeView-Steuerelements mit Daten aus einer reflexiven Beziehung ist eine rekursive Funktion erforderlich, das heißt, dass eine Funktion sich entsprechend der Hierarchie immer wieder selbst aufruft und dabei untergeordnete Objekte abarbeitet, bis schließlich alle Daten berücksichtigt wurden.
318
Steuerelemente
Das folgende Listing zeigt die für diesen Fall notwendigen Routinen. Die Beim Laden-Er eignisprozedur leert das TreeView-Steuerelement und ruft zum ersten Mal die Routine TreeViewRekursivFuellen auf. Der einzige Parameter der Routine erhält hier den Wert 0. Private Sub Form_Load() objTreeView.Nodes.Clear TreeViewRekursivFuellen 0 End Sub Private Sub TreeViewRekursivFuellen(lngPersonalID As Long) Dim db As DAO.Database Dim rst As DAO.Recordset2 Dim objNode As MSComctlLib.Node Set db = CurrentDb If lngPersonalID = 0 Then Set rst = db.OpenRecordset("SELECT * FROM tblPersonal " _ & "WHERE Vorgesetzter IS NULL") Else Set rst = db.OpenRecordset("SELECT * FROM tblPersonal " _ & "WHERE Vorgesetzter = " & lngPersonalID) End If Do While Not rst.EOF If lngPersonalID = 0 Then Set objNode = objTreeView.Nodes.Add(, , "Personal" _ & rst!PersonalID, rst!Vorname & " " & rst!Nachname) Else Set objNode = objTreeView.Nodes.Add("Personal" _ & lngPersonalID, tvwChild, "Personal" & rst!PersonalID, _ rst!Vorname & " " & rst!Nachname) End If TreeViewRekursivFuellen rst!PersonalID rst.MoveNext Loop Set objNode = Nothing rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 5.14: Füllen eines TreeView-Steuerelements mit einer rekursiven Funktion
Beim ersten Aufruf erzeugt die Routine TreeViewRekursivFuellen eine Datensatzgruppe, die alle Mitarbeiter ohne Vorgesetzten enthält. Dafür sorgt der Parameter lngPersonalID; hat dieser den Wert 0, wird die entsprechende SELECT-Abfrage ausgeführt. Die Routine legt dann zunächst einen Knoten für den ersten Mitarbeiter an (für lngPersonalNr = 0 ohne Referenz auf bestehende Knoten) und ruft sich selbst dann mit einem anderen Wert für den Parameter lngPersonalID auf – nämlich der Personalnummer des soeben angelegten Mitarbeiters.
319
Kapitel 5
Diesmal prüft die Routine, ob es Mitarbeiter gibt, die dem Mitarbeiter mit der übergebe nen Personalnummer untergeordnet sind. Ist das der Fall, werden diese ebenfalls angelegt. Dabei wird allerdings eine Referenz auf das übergeordnete Mitarbeiter-Element erzeugt. Anschließend wird die Routine erneut mit der aktuellen Personalnummer aufgerufen – dieser Vorgang wiederholt sich, bis es keine weiteren untergeordneten Mitarbeiter mehr gibt. In diesem Fall arbeitet die aufrufende Instanz der Routine TreeViewRekursivFuellen die übrigen Datensätze der Datensatzgruppe rst weiter ab.
5.10.10 TreeView füllen bei großen Datenbeständen Das Füllen von TreeView-Steuerelementen kann je nach Menge der anzuzeigenden Da ten eine Weile dauern. Es gibt verschiedene Möglichkeiten, den Aufbau des Baumes zu beschleunigen. Bei bekannter Verschachtelungstiefe können Sie etwa folgende Va riante verwenden: Die Anzeige von Kunden und Bestellungen aus dem weiter oben beschriebenen Beispiel erzeugt beispielsweise für jeden Kunden ein neues RecordsetObjekt mit den passenden Bestellungen. Dies können Sie sich sparen, indem Sie direkt alle benötigten Informationen in einer einzigen Abfrage zusammenfassen. Diese enthält im vorliegenden Fall die beiden Tabellen tblKunden und tblBestellungen und die in Abbildung 5.30 abgebildeten Felder, wobei diese nach den Primärschlüsselfeldern der beiden Tabellen sortiert sind – das ist für die folgende Vorgehensweise besonders wichtig.
Abbildung 5.30: Diese Abfrage liefert alle Bestellungen samt Kundeninformationen
Die folgende Routine sorgt für das Füllen des TreeView-Steuerelements auf Basis der Abfrage qryKundenUndBestellungen:
320
Steuerelemente Private Sub Form_Load() Dim db As DAO.Database Dim rst As DAO.Recordset Dim objNode As MSComctlLib.Node Dim lngVorherigerKunde As Long Set db = CurrentDb Set rst = db.OpenRecordset("qryKundenUndBestellungen") objTreeView.Nodes.Clear Do While Not rst.EOF If Not rst!KundeID = lngVorherigerKunde Then Set objNode = objTreeView.Nodes.Add(, , _ "Kunde" & rst!KundeID, rst!Firma) End If Set objNode = objTreeView.Nodes.Add("Kunde" & rst!KundeID, _ tvwChild, "Bestellung" & rst!BestellungID, rst!BestellungID _ & "/" & rst!Bestelldatum) lngVorherigerKunde = rst!KundeID rst.MoveNext Loop Set objNode = Nothing rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 5.15: Diese Routine füllt ein TreeView-Steuerelement mit den Daten der Abfrage qryKundenUndBestellungen
Die Routine durchläuft alle in der Abfrage qryKundenUndBestellungen enthaltenen Daten sätze. Dabei speichert sie jeweils die KundenID des aktuellen Kunden in der Variablen strVorherigerKunde zwischen. Im ersten Durchlauf ist lngVorherigerKunde noch leer, also wird das Kunden-Element angelegt. Im gleichen Zuge legt die Routine das erste Bestellungs-Element an – und das alles mit den Daten des ersten Datensatzes. Beim zweiten Datensatz stellt die Routine fest, dass die Kunden-ID dem Wert von lngVor herigerKunde entspricht, und legt kein neues Element für diesen Kunden, sondern nur die im zweiten Datensatz enthaltene Bestellung an.
5.10.11 Elemente erst bei Bedarf anlegen Wenn der Aufbau des Baumes definitiv zu lange dauert, könnte man auch dafür sorgen, dass nicht sichtbare oder untergeordnete Knoten erst bei Verwendung nachgeladen werden. Diese Variante finden Sie im Formular frmTreeViewFuellen1_n_BeiBedarf. Die enthaltene Funktionalität entspricht prinzipiell der aus dem Formular frmTreeView Fuellen1_n mit dem Unterschied, dass der Teil, in dem die Routine die Bestellungen
321
Kapitel 5
zu einem Kunden im TreeView-Steuerelement anlegt, in eine weitere Ereignisprozedur ausgelagert wurde. Diese heißt NodeClick und wird beim Klicken auf ein Element ausgelöst. Die Ereignisprozedur Form_Load füllt das TreeView-Steuerelement zunächst mit allen Kunden: Private Sub Form_Load(Cancel As Integer) Dim db As DAO.Database Dim rstKunden As DAO.Recordset2 Dim rstBestellungen As DAO.Recordset2 Dim objNode As MSComctlLib.Node Set db = CurrentDb Set rstKunden = db.OpenRecordset("tblKunden") objTreeView.Nodes.Clear Do While Not rstKunden.EOF Set objNode = objTreeView.Nodes.Add(, , "Kunde" _ & rstKunden!KundeID, rstKunden!Firma) rstKunden.MoveNext Loop Set objNode = Nothing rstKunden.Close Set rstKunden = Nothing Set db = Nothing End Sub Listing 5.16: Einlesen der Kunden ohne Bestelldaten
Die per Klick auf einen Kunden ausgelöste NodeClick-Prozedur besitzt einen Parameter, der einen Objektverweis auf das angeklickte Element liefert. Die Eigenschaft Children dieses Objekts enthält die Anzahl der Child-Elemente des angeklickten Elements. Ist diese gleich 0, wurden die Bestellungen zu diesem Kunden noch nicht eingelesen – Zeit, dies nachzuholen. Mit der Key-Eigenschaft des Node-Objekts, die beispielsweise den Wert Kunde7 enthält, können Sie per Replace schnell die Kunden-ID ermitteln. Mit dieser öffnen Sie ein Recordset mit allen Bestellungen zu dem angeklickten Kunden und fügen die Bestellung-Elemente zum Baum hinzu: Private Sub tvwKundenUndBestellungen_NodeClick(ByVal Node As Object) Dim objNode As MSComctlLib.Node Dim rstBestellungen As DAO.Recordset2 Dim db As DAO.Database Dim lngKundeID As Variant Set objNode = Node If objNode.Children = 0 Then Set db = CurrentDb lngKundeID = Replace(objNode.Key, "Kunde", "")
322
Steuerelemente If Len(lngKundeID) > 8 Then Exit Sub Set rstBestellungen = db.OpenRecordset("SELECT BestellungID, " _ & "Bestelldatum FROM tblBestellungen WHERE KundeID = " _ & lngKundeID, dbOpenDynaset) Do While Not rstBestellungen.EOF objTreeView.Nodes.Add "Kunde" & lngKundeID, tvwChild, _ "Bestellung" & rstBestellungen!BestellungID, _ rstBestellungen!BestellungID & "/" _ & rstBestellungen!Bestelldatum rstBestellungen.MoveNext Loop rstBestellungen.Close Set rstBestellungen = Nothing Set db = Nothing End If Set objNode = Nothing End Sub Listing 5.17: Diese Routine liest die Bestelldaten zu einem mit der Maus angeklickten KundenElement im TreeView-Element ein
5.10.12 Neuzeichnen des Baumes verhindern Ein weiteres Problem bei vielen Einträgen im Baum ist, dass das TreeView-Steuerelement sich beim Hinzufügen jedes Knotens komplett neu zeichnet, was umso mehr Zeit kostet, je länger der Baum wird. Dieses Neuzeichnen lässt sich mit einer API-Funktion verhindern (in einer Zeile): Private Declare Function LockWindowUpdate Lib "user32.dll" (ByVal hwndLock As Long) As Long
Die Funktion wird vor dem Füllen des TreeView-Steuerelements mit dem Handle des TreeView-Objekts als Parameter aufgerufen, um das Neuzeichnen zu deaktivieren. Zum Deaktivieren erfolgt ein Aufruf der gleichen Funktion mit dem Wert 0& als Pa rameter: LockWindowUpdate objTreeView.hWnd '... Füllen des TreeViews LockWindowUpdate 0&
Dieser Trick beschleunigt das Füllen des TreeView-Steuerelements bei vielen Datensätzen um ein Vielfaches. Es ist dabei allerdings darauf zu achten, dass die Zeile LockWindowUpdate 0& unbedingt abschließend ausgeführt wird, selbst wenn ein Fehler im Code zum Befüllen des Baums auftreten sollte, weil andernfalls Access einfriert – das bedeutet also, dass eine Fehlerbehandlung in dieser Prozedur angeraten ist.
323
Kapitel 5
5.10.13 Drag and Drop im TreeView-Steuerelement Nachdem Sie nun wissen, wie Sie Ihre Daten in ein TreeView-Steuerelement bringen, möchten Sie vielleicht auch noch weitere Vorzüge dieses Steuerelements nutzen: Das TreeView-Steuerelement unterstützt Drag and Drop und ermöglicht damit das Zuweisen von hierarchischen Daten mit der Maus. Drag-and-Drop-Operationen erfordern je nach Anwendungsfall einige Zeilen Code. Voraussetzung ist, dass Sie die Einstellung für die Eigenschaften OLEDragMode auf ccOLEDragAutomatic und OLEDropMode auf cc OLEDropManual festlegen. Das können Sie im Eigenschaftsdialog des TreeView-Steuer elements erledigen (siehe Abbildung 5.31) oder per Code: objTreeView.OLEDragMode = ccOLEDragAutomatic objTreeView.OLEDropMode = ccOLEDropManual
Abbildung 5.31: Einstellungen der für Drag and Drop notwendigen Eigenschaften
5.10.14 VBA-Ereignisprozeduren für Drag and Drop einrichten Außerdem sind einige Ereignisprozeduren mit Leben zu füllen. Diese lassen sich nicht wie bei den üblichen Access-Steuerelementen über das Eigenschaftsfenster anlegen, sondern müssen direkt im VBA-Editor erstellt werden. Nachdem Sie ein passendes TreeView-Steuerelement im Formular angelegt haben, können Sie die beiden Kombinationsfelder im VBA-Editor zum Anlegen von Ereignis eigenschaften für das TreeView-Steuerelement verwenden. Wählen Sie dazu im linken Kombinationsfeld den Namen des Steuerelements aus. Dies führt zum automatischen
324
Steuerelemente
Anlegen des Prozedurrumpfs, der durch das Updated-Ereignis ausgelöst wird. Weitere Ereignisprozeduren legen Sie durch Auswahl des gewünschten Ereignisses aus dem rechten Kombinationsfelds an (siehe Abbildung 5.32).
Abbildung 5.32: Anlegen von Ereignisprozeduren im VBA-Codefenster
Auf diese Weise sind die nachfolgend benötigten drei Prozeduren schnell erstellt. Das Ereignis OLEStartDrag wird ausgelöst, wenn Sie den Mauszeiger auf einem Eintrag positionieren, die linke Maustaste gedrückt halten und den Mauszeiger dabei bewegen. Das Ereignis OLEDragOver wird nach dem Ereignis OLEStartDrag in kurzen Abständen ausgelöst – es könnte eigentlich OLEDragMouseMove heißen –, bis Sie die Maustaste loslassen. Dies wiederum ruft das Ereignis OLEDragDrop auf den Plan. Damit hätten Sie das Werkzeug beisammen – mit Ausnahme einiger nützlicher Eigenschaften des TreeView-Steuerelements: HitTest(x,y): Liefert einen Verweis auf den Knoten mit den Koordinaten x und y. SelectedItem: Legt den markierten Knoten fest oder liest diesen aus. DropHighlight: Markiert den Knoten, über dem sich der Mauszeiger bei einer Dragand-Drop-Operation befindet, sofern dies nicht der Ausgangsknoten ist. Aber der Beginn der Drag-and-Drop-Operation hat es in sich: Die Operation bezieht sich nämlich auf das aktuell markierte Element. Wenn Sie ein Element mit der Maus
325
Kapitel 5
markieren, passiert dies jedoch erst beim Loslassen der Maustaste. Beim Ziehen lassen Sie die Taste aber erst zum Ablegen wieder los. Wenn nun wie in Abbildung 5.33 vor Beginn des Ziehens ein anderes Element markiert ist, denkt Access, Sie wollten dieses aktuell markierte Element ziehen, aber nicht das, auf das Sie mit der Maus geklickt haben (das gestrichelte Element ist das aktuell markierte, das blau hinterlegte das zu ziehende Element). Sobald Access den Start einer Drag-and-Drop-Operation erkennt, soll also das aktuelle Element demarkiert und das tatsächlich zu ziehende Element markiert werden. Den ersten Schritt erledigt der Einzeiler aus folgender Routine: Private Sub tvwTreeView_OLEStartDrag(Data As Object, _ AllowedEffects As Long) objTreeView.SelectedItem = Nothing End Sub Listing 5.18: Aufheben der aktuellen Markierung
Abbildung 5.33: Zu Beginn einer Drag-and-Drop-Operation ist noch ein anderes als das zu bewegende Element markiert
Die Markierung des zu ziehenden Elements erfolgt im OLEDragOver-Ereignis, das immer wieder während des Drag-and-Drop-Vorgangs ausgelöst wird. Das Markieren soll aber nur einmal zu Beginn erfolgen, weshalb die folgende Routine abfragt, ob ein Element markiert ist, und gegebenenfalls dasjenige markiert, über dem sich aktuell der Mauszeiger befindet. Private Sub tvwTreeView_OLEDragOver(Data As _ Object, Effect As Long, Button As _ Integer, Shift As Integer, x As Single, _ y As Single, State As Integer) If objTreeView.SelectedItem Is Nothing Then Set objTreeView.SelectedItem = objTreeView.HitTest(x, y) End If Set objTreeView.DropHighlight = objTreeView.HitTest(x, y) End Sub Listing 5.19: Diese Routine wird immer wieder während des Drag-and-Drop-Vorgangs aus geführt
326
Steuerelemente
Bei jedem Aufruf der OLEDragOver-Prozedur passiert allerdings Folgendes: Die Rou tine markiert das Element, über dem sich der Mauszeiger aktuell befindet, mit der Eigenschaft DropHighlight und kennzeichnet es so als potenzielles Ziel der Drag-andDrop-Operation. Das große Finish folgt dann in der Ereignisprozedur OLEDragDrop. Diese wird beim Loslassen der Maustaste ausgelöst und sorgt dafür, dass das Element an die neue Stelle verschoben wird. Aber nicht nur das: Das Abbild der Daten im TreeView-Steuerelement und die in den Tabellen befindlichen Daten werden natürlich nicht automatisch synchro nisiert. Also müssen Sie auch noch dafür sorgen, dass der entsprechende Datensatz der neuen Situation im TreeView-Steuerelement angepasst wird. Das hört sich zwar erst einmal recht kompliziert an, ist aber letzten Endes reine Fleiß arbeit. Im vorliegenden Beispiel sieht die Routine wie im folgenden Listing aus. Private Sub tvwTreeView_OLEDragDrop(Data As Object, Effect As Long, _ Button As Integer, _ Shift As Integer, x As Single, y As Single) Dim db As DAO.Database Dim lngID As Long Dim lngParentID As Long Dim strKey As String Dim strText As String Set db = CurrentDb 'zu verschiebenden Datensatz ermitteln lngID = Mid(objTreeView.SelectedItem.Key, 9) strKey = objTreeView.SelectedItem.Key strText = objTreeView.SelectedItem.Text 'Wurde an eine freie Stelle oder auf einen anderen Knoten gezogen? If objTreeView.HitTest(x, y) Is Nothing Then 'Knoten entfernen objTreeView.Nodes.Remove objTreeView.SelectedItem.Index 'Knoten neu anlegen objTreeView.Nodes.Add , , strKey, strText 'Änderung in die Datenbank übertragen db.Execute "UPDATE tblPersonal SET Vorgesetzter = NULL " _ & "WHERE PersonalID = " & lngID 'Untergeordnete Elemente des verschobenen Knotens wiederherstellen TreeViewRekursivFuellen lngID Else If objTreeView.SelectedItem <> objTreeView.DropHighlight Then 'neuen übergeordneten Datensatz ermitteln lngParentID = Mid(objTreeView.DropHighlight.Key, 9) 'Zielknoten als neuen übergeordneten Knoten festlegen Set objTreeView.SelectedItem.Parent = objTreeView.DropHighlight
327
Kapitel 5 'Änderung in die Datenbank übertragen db.Execute "UPDATE tblPersonal SET Vorgesetzter = " _ & lngParentID & " WHERE PersonalID = " & lngID End If Set objTreeView.DropHighlight = Nothing End If End Sub Listing 5.20: Durchführen der Drag-and-Drop-Operation
Es gibt beim Drag and Drop grundsätzlich zwei zu unterscheidende Fälle: Der Knoten wird in den leeren Raum gezogen und soll so in die oberste HierarchieEbene eingeordnet werden. Der Knoten wird auf einen anderen Knoten gezogen und soll so diesem untergeordnet werden. Nach dem Ermitteln einiger Informationen des Knotens, der verschoben werden soll (Primärschlüsselwert des entsprechenden Datensatzes, Key, angezeigter Text), zweigt eine If Then-Bedingung die Routine entsprechend den beiden oben genannten Fällen auf. Beim Ziehen in den leeren Raum wird zunächst der Knoten entfernt und neu angelegt. Leider nicht ohne Reibungsverluste, denn eventuell untergeordnete Knoten gehen so ver loren. Um diese wiederherzustellen, wird die Änderung zunächst in der Tabelle tblPersonal durchgeführt: Der Wert des Feldes Vorgesetzter des verschobenen Datensatzes wird auf NULL gesetzt. Anschließend ruft die Routine die Prozedur TreeViewRekursivFuellen (siehe Listing 5.13) mit dem neuen Knoten als Parameter auf, um die untergeordneten Knoten wiederherzustellen. Zieht der Benutzer den Knoten auf einen anderen Knoten, ist etwas weniger zu tun: Nach einer Prüfung, ob der Knoten nicht auf sich selbst abgelegt werden soll, ermittelt die Routine zunächst den Primärschlüsselwert des neuen übergeordneten Knotens. Anschließend weist man der Parent-Eigenschaft des verschobenen Knotens den neuen übergeordneten Knoten zu. Damit werden auch automatisch alle untergeordneten Kno ten mitgezogen. Fehlt noch die Übernahme der Änderung in die Datenbank mit der entsprechenden UPDATE-Abfrage – auch dies erledigt obige Routine.
5.11 ListView Wer mitAccess arbeitet, vermisst manchmal sicher etwas anspruchsvollere Steuerelemente. Beispiel Listenfeld: Dort wäre es doch schön, wenn es eine einfache Möglichkeit zum Sortieren nach den enthaltenen Spalten anböte oder wenn man auch Icons darin un-
328
Steuerelemente
terbringen könnte. Das funkioniert sehr gut mit dem ListView-Steuerelement, das die folgenden Abschnitte vorstellen.
5.11.1 Möglichkeiten des ListView-Steuerelements Im Gegensatz zu den eingebauten Steuerelementen von Access hat das ListViewSteuerelement wie das TreeView-Steuerelement den Nachteil, dass man es nicht so einfach an eine Datenherkunft binden kann, sondern es per VBA-Code mit Daten füllen muss. Dafür bietet das ListView-Steuerelement einige andere Vorteile, die seinen Einsatz lohnenswert machen: per Mausklick nach einzelnen Spalten sortieren Icons anzeigen Bilder anzeigen umfangreiche Anpassungen am Aussehen vornehmen die Spaltenbreite manuell zur Laufzeit verändern Drag and Drop verwenden (zumindest) den Inhalt der ersten Spalte manuell ändern
Hinzufügen des ListView-Steuerelements Das ListView-Steuerelement fügen Sie genau wie das TreeView-Steuerelement über den Dialog ActiveX-Steuerelement einfügen hinzu, den Sie über den Ribbon-Eintrag Ent wurf|Steuerelemente|ActiveX-Steuerelement einfügen öffnen. Nach dem Hinzufügen des Steuerelements (Microsoft ListView Control, Version 6.0) geben Sie ihm direkt einen Na men wie etwa lvwPersonen. Für den komfortablen Zugriff auf das Steuerelement per VBACode deklarieren Sie entweder eine passende Objektvariable und verweisen damit auf das Steuerelement oder Sie verwenden eine Property Get-Routine, um wie bereits beim TreeView-Steuerelement immer eine passende Objektvariable griffbereit zu haben: Private mListView As MSComctlLib.ListView Property Get objListView() As MSComctlLib.ListView If mListView Is Nothing Then Set mListView = Me!lvwPersonen.Object End If Set objListView = mListView End Property Listing 5.21: Bereitstellen eines Objektverweises auf ein ListView-Steuerelement per Property Get-Routine
329
Kapitel 5
5.11.2 Füllen des ListView-Steuerelements Füllen lässt sich das ListView-Steuerelement nur via VBA-Code. Zum Füllen des Steuer elements beim Laden des Formulars eignet sich am besten das Beim Öffnen-Ereignis. Mit der folgenden Routine fügen Sie direkt das erste Element ein: Private Sub Form_Load(Cancel As Integer) Dim objListItem As ListItem Set objListView = Me.lvwPersonen.Object With objListView Set objListItem = .ListItems.Add(, "a1", "André") End With End Sub Listing 5.22: Hinzufügen eines ersten Elements zum ListView-Element
Die Elemente eines ListView-Steuerelements werden in der ListItems-Auflistung des Steuerelements erfasst. Mit der Add-Methode dieser Auflistung fügen Sie ein neues Element hinzu. Als Parameter dienen dabei ein Schlüssel, der mit einem Buchstaben beginnen und eindeutig sein muss, sowie der anzuzeigende Inhalt des Elements. In der Objektvariablen objListItem speichern Sie einen Verweis auf das neue Listenelement, damit Sie später darauf zugreifen können. Der erste Blick auf das Steuerelement samt erstem Eintrag in der Formularansicht ernüchtert: Besonders attraktiv ist das nicht geworden (siehe Abbildung 5.34).
Abbildung 5.34: Das ListView-Steuerelement mit einem ersten Element – zugebenermaßen recht unattraktiv
Natürlich gibt es ausreichend Möglichkeiten, das Aussehen anzupassen – dazu jedoch später mehr. Erstmal fügen Sie dem bestehenden Element weitere Zeilen hinzu. Hier kommt die Objektvariable objListItem mit dem Verweis auf das erste Element zum Einsatz: Das ListView-Steuerelement ist nämlich nicht wie beispielsweise das Listenfeld aufgebaut, bei dem jeder Eintrag aus mehreren gleich zu behandelnden Spalten besteht.
330
Steuerelemente
Beim ListView-Steuerelement entspricht eine Zeile einem ListItem-Objekt, das selbst nur den Wert für die erste Spalte speichert. Der Inhalt weiterer Spalten wird in Elementen der ListSubItems-Auflistung des Listenelements verwaltet. Daher benötigen Sie auch einen Verweis auf das Listenelement, um weitere Spalten hinzuzufügen. Die folgende Anweisung fügt eine zweite Spalte hinzu: objListItem.ListSubItems.Add , , "Minhorst"
Im Vergleich zu den »Hauptlistenelementen« benötigt ein ListSubItem nur die Angabe des enthaltenen Textes. Die anderen Parameter können Sie zunächst außer Acht lassen. Möchten Sie sich nun den neuen Eintrag in der Formularansicht ansehen, werden Sie enttäuscht: Das ListView-Steuerelement zeigt nach wie vor nur eine einzige Spalte an. Nun wird es Zeit, sich um die Eigenschaften des ListView-Steuerelements zu kümmern.
5.11.3 Eigenschaften des ListView-Steuerelements ActiveX-Steuerelemente haben meist ein zusätzliches Eigenschaftsfenster, das sich über das Kontextmenü öffnen lässt. Beim ListView-Steuerelement sieht dies wie in Abbildung 5.35 aus.
Abbildung 5.35: Eigenschaften eines ListView-Steuerelements
Hier ändern Sie den Wert der Eigenschaft View auf 0-lvwReport, damit die Daten wie etwa in der Dateiliste des Windows Explorers angezeigt werden. In der Formularansicht zeigt das ListView-Steuerelement nun gar keine Daten mehr an. Der Grund ist, dass auf der Registerseite Spaltenköpfe noch keine Spalten angelegt sind (siehe Abbildung 5.36).
331
Kapitel 5
Abbildung 5.36: Ein frisch eingefügtes ListView-Steuerelement zeigt nur die Basisspalte an; weitere Spalten müssen Sie erst noch einfügen
Klicken Sie also auf die Schaltfläche Spalte einfügen. Von den nun aktivierten Textfeldern brauchen Sie nur zwei: Mit Text legen Sie die Spaltenüberschrift fest und mit Width stellen Sie die gewünschte Breite der zweiten Spalte ein. Legen Sie zwei Spalten für die Anzeige des Vornamens und des Nachnamens an und konfigurieren Sie die Breite entsprechend den vorhandenen Daten. Nun sieht das ListView-Steuerelement schon besser aus (siehe Abbildung 5.37).
Abbildung 5.37: ListView-Steuerelement mit zwei Spalten inklusive Spaltenköpfen
5.11.4 Sortieren von ListView-Einträgen Ein großer Vorteil von ListView-Steuerelementen gegenüber dem Access-Listenfeld ist die Möglichkeit, per Klick auf den Spaltenkopf nach dem Inhalt der Spalte zu sortieren. Um dies zu demonstrieren, erweitern Sie zunächst den Code so, dass er dem ListView
332
Steuerelemente
einige weitere Einträge hinzufügt (wie Sie dies mit Daten aus einer Tabelle durchführen, erfahren Sie weiter unten): Private Sub Form_Load(Cancel As Integer) Dim objListItem As ListItem Set objListItem = objListView.ListItems.Add(, objListItem.ListSubItems.Add , , "Minhorst" Set objListItem = objListView.ListItems.Add(, objListItem.ListSubItems.Add , , "Trowitzsch" Set objListItem = objListView.ListItems.Add(, objListItem.ListSubItems.Add , , "Forster" Set objListItem = objListView.ListItems.Add(, objListItem.ListSubItems.Add , , "Flechler" Set objListItem = Nothing
End Sub Listing 5.23: ListView-Steuerelement mit mehreren Einträgen füllen
Das Sortieren nach dem Inhalt einer bestimmten Spalte soll durch einen Klick auf den entsprechenden Spaltenkopf ausgelöst werden. Das ListView-Steuerelement stellt dazu die Ereigniseigenschaft ColumnClick zur Verfügung. Sie legen ein solches Ereignis genau wie beim TreeView-Steuerelement direkt im VBA-Editor über die beiden Kombinations felder des Codefensters an. Bei einem Klick auf den Spaltenkopf sollen die enthaltenen Einträge immer aufsteigend sortiert werden. Einzige Ausnahme: Wenn die Spalte unmittelbar vorher schon aufsteigend sortiert war, soll nun eine absteigende Sortierung vorgenommen werden. Die folgende Routine hat zwei statische Variablen – das sind als Static deklarierte Variablen, deren Gültigkeit auch nach Verlassen der Prozedur erhalten bleibt –, die sich sowohl die zuletzt sortierte Spalte als auch die verwendete Sortierreihenfolge merken. Private Sub lvwPersonen_ColumnClick(ByVal ColumnHeader As Object) Static intAktuelleSortierspalte As Integer Static bolAufsteigend As Boolean objListView.SortKey = ColumnHeader.Index - 1 If intAktuelleSortierspalte = ColumnHeader.Index - 1 Then bolAufsteigend = Not bolAufsteigend objListView.SortOrder = Abs(bolAufsteigend) Else objListView.SortOrder = lvwAscending End If objListView.Sorted = True intAktuelleSortierspalte = ColumnHeader.Index - 1 End Sub Listing 5.24: Sortieren der Spalten im ListView-Steuerelement
333
Kapitel 5
Das ListView-Steuerelement übergibt der Ereignisprozedur beim Klicken auf einen der Spaltenköpfe ein ColumnHeader-Objekt, das unter anderem die Eigenschaft Index enthält. Diese gibt an, auf welche Spalte der Benutzer geklickt hat. Dies ist die erste Information, die Sie zum Sortieren nach einer Spalte benötigen. Die Routine weist den Spaltenindex der Eigenschaft SortKey des ListView-Steuerelements zu: objListView.SortKey = ColumnHeader.Index - 1
Im nächsten Schritt prüft die Routine, ob die aktuell zu sortierende Spalte auch die zuletzt sortierte Spalte ist. Falls ja, wird die Sortierreihenfolge umgedreht, in der statischen Variablen bolAufsteigend gespeichert und die Spalte nach der neuen Sortierreihenfolge sortiert. Ist diese Spalte nicht als letzte Spalte sortiert worden, erfolgt auf jeden Fall eine aufsteigende Sortierung. Mit der folgenden Anweisung aktiviert die Routine schließlich die Sortierung: objListView.Sorted = True
Damit die Routine bei der nächsten Sortierung noch weiß, welche Spalte in diesem Durchgang sortiert wurde, erfolgt eine entsprechende Zuweisung an die statische Variable intAktuelleSortierspalte.
5.11.5 Einträge des ListView-Steuerelements auswählen Eine Liste wollen Sie natürlich nicht nur zum Ansehen und Sortieren von Daten verwenden, sondern auch zum Auswählen von Daten. Standardmäßig lässt sich dabei nur der erste Eintrag des ListViews markieren. Ein Klick auf die anderen Spalten wirkt sich nicht auf die Markierung aus. Wenn die komplette Zeile markiert werden und auch ein Klick auf andere als die erste Spalte eine Markierung bewirken soll, aktivieren Sie die Eigenschaft FullRowSelect.
Aktuell markierten Eintrag ermitteln Den Schlüssel des aktuell markierten Eintrags erhalten Sie über folgenden Ausdruck: .SelectedItem.Key
Mehrere Einträge gleichzeitig markieren Wenn Sie das gleichzeitige Markieren mehrerer Einträge zulassen möchten, stellen Sie die Eigenschaft MultiSelect auf True ein (VBA) oder aktivieren den entsprechenden Eintrag im Eigenschaftsfenster. Das Steuerelement verfügt dann über die gleiche Mimik wie etwa der Windows-Explorer: Bei gedrückter Umschalttaste können Sie mit zwei Mausklicks auf den ersten und den letzten Eintrag eine Gruppe zusammenhängender Einträge markieren. Einzelne Einträge lassen sich bei gedrückter Strg-Taste aus- oder abwählen.
334
Steuerelemente
Auslesen mehrerer markierter Einträge Zum Auslesen der markierten Einträge in einem ListView mit aktivierter Mehrfachaus wahl durchläuft man alle Elemente und prüft jeweils anhand der Eigenschaft Selected, ob das Element markiert ist. Im Code sieht das wie in der folgenden Routine aus: Private Sub cmdMarkierteEintraegeAnzeigen_Click() Dim objListItem As ListItem For Each objListItem In lvwPersonen.ListItems If objListItem.Selected Then Debug.Print objListItem.Key, objListItem.Text, _ objListItem.ListSubItems(1).Text End If Next End Sub Listing 5.25: Mehrere markierte Einträge auslesen
Spalten eines bestimmten Eintrags lesen In vielen Fällen ist nicht der Schlüssel eines ListView-Elements interessant, sondern einer der Spaltenwerte. Auf diese greifen Sie über die Text-Eigenschaft zu. Allerdings gibt es Unterschiede beim Zugriff auf die erste und die folgenden Spalten. Die erste Spalte enthält das Standardelement (ListItem), das bereits beim Anlegen des Eintrags mit Text gefüllt wird. Sie lässt sich direkt über die Eigenschaft Text des Eintrags auslesen. Für alle weiteren Spalten müssen Sie die Auflistung ListSubItems mit dem entsprechenden Index bemühen, wobei der Index bei 1 beginnt. Wenn Sie in der obigen Routine nicht nur den Key, sondern auch den Inhalt der beiden Spalten ausgeben möchten, ersetzen Sie die Anweisung in der For Each-Schleife durch die folgende: Debug.Print objListItem.Key, objListItem.Text, _ objListItem.ListSubItems(1).Text
Einträge zur Laufzeit hinzufügen Der bisherige Beispielcode geht davon aus, dass die Einträge beim Öffnen des jeweiligen Formulars angelegt werden. Sie können natürlich auch neue Einträge zur Laufzeit hinzufügen. Dazu legen Sie beispielsweise drei Textfelder wie in Abbildung 5.38 an und hinterlegen für die entsprechende Schaltfläche die folgende Ereignisprozedur: Private Sub cmdNeuerEintrag_Click() Dim objListItem As ListItem Set objListItem = lvwPersonen.ListItems.Add(, Me.txtKey, Me.txtVorname) objListItem.ListSubItems.Add , , Me.txtNachname
335
Kapitel 5 Set objListItem = Nothing End Sub Listing 5.26: Hinzufügen eines neuen Eintrags per Code
Abbildung 5.38: Hinzufügen neuer Einträge zum ListView-Steuerelement
Werte direkt im ListView ändern Standardmäßig können Sie die erste Spalte eines Eintrags ändern, indem Sie wie im Windows Explorer erst die Zeile markieren und dann nach einer kurzen Wartezeit erneut darauf klicken. Die erste Spalte des Eintrags verwandelt sich daraufhin in ein Eingabefeld, in das Sie den neuen Wert eintragen können. Für eventuelle Aktionen in Zusammenhang mit einer solchen Änderung können Sie die Ereignisse BeforeLabelEdit und AfterLabelEdit verwenden. Wie das funktioniert, erfahren Sie weiter unten in Ab schnitt 5.11.6, »ListView-Steuerelement mit Daten füllen«. Andere Spalten als die erste lassen sich leider nicht anpassen.
ListView-Steuerelement stylen Zum Anpassen des Layouts stellen die Eigenschaften eines ListView-Steuerelements einige Möglichkeiten zur Verfügung. Experimentieren Sie einfach mit einzelnen Einstel lungen des Eigenschaftsfensters – das Steuerelement lässt sich sehr flexibel anpassen. Als Starthilfe finden Sie hier eine Auflistung wichtiger Eigenschaften: BorderStyle (Registerseite Allgemein): legt fest, wie der Rand aussieht. Diese Eigen schaft ist nicht mit der im eigentlichen Eigenschaftsfenster identisch, die den Rand des umgebenden OLE-Rahmens festlegt. Appearance (Registerseite Allgemein): legt fest, ob das Steuerelement vertieft oder eben angezeigt wird.
336
Steuerelemente
Gridlines (Registerseite Allgemein): zeigt ein Gitterraster an, wenn aktiviert. Alignment (Registerseite Spaltenköpfe): Legt die Ausrichtung des Spalteninhalts fest. Width (Registerseite Spaltenköpfe): Legt die Breite der Spalten fest. Symbolindex (Registerseite Spaltenköpfe): Legt fest, welches Symbol aus dem unter Abbildungslisten/Spaltenkopf angegebenen ImageList-Steuerelement der Spaltenkopf enthält.
5.11.6 ListView-Steuerelement mit Daten füllen Wie das TreeView-Steuerelement bietet das ListView keine Möglichkeit der direkten Bindung an eine Datenherkunft wie eine Tabelle oder Abfrage. Die einzelnen Datensätze der zugrunde liegenden Datenherkunft müssen Stück für Stück programmatisch zum ListView hinzugefügt werden. Das folgende Beispiel zeigt, wie Sie ein ListView-Steuerelement mit den in einer Tabelle enthaltenen Daten füllen. Als Datenherkunft dient die Tabelle tblPersonal der Beispiel datenbank. Das ListView-Steuerelement heißt lvwPersonal und soll für jeden Mitarbeiter Nachname, Vorname und die Büro-Durchwahl anzeigen.
Layout der Liste Bevor Sie das ListView-Steuerelement mit Daten füllen, nehmen Sie einige Einstellungen am Layout des Steuerelements vor. Als Erstes stellen Sie die Eigenschaft View auf der Registerseite Allgemein des Eigenschaftsfensters auf den Wert 3 – lvwReport ein. Anschlie ßend bereiten Sie das ListView für die Anzeige von drei Spalten je Zeile vor. Dazu wechseln Sie im Eigenschaftsfenster auf die Seite Spaltenköpfe und fügen die drei Einträge Nachname, Vorname und Durchwahl hinzu.
Füllen per VBA Dann können Sie schon die VBA-Prozedur für das Füllen des ListView-Steuerelements anlegen. Das ListView soll direkt beim Öffnen des Formulars gefüllt werden, also verwenden Sie die Beim Laden-Ereigniseigenschaft. Die fertige Routine deklariert zu nächst die für den Datenzugriff benötigten Objekte und instanziert diese. In einer Do While-Schleife durchläuft sie dann alle in der Datensatzgruppe enthaltenen Daten sätze. Und hier kommt der für das ListView interessante Teil: Für jeden Eintrag legt die Routine ein neues ListItem-Element an und referenziert es mit der Objektvariablen obj ListItem. Das war es schon – das ListView sollte sich nun etwa wie in Abbildung 5.39 präsentieren.
337
Kapitel 5 Dim objListView As ListView Private Sub Form_Load() Dim db As DAO.Database Dim rst As DAO.Recordset Dim objListItem As ListItem Set db = CurrentDb Set rst = db.OpenRecordset("tblPersonal", dbOpenSnapshot) Set objListView = Me!lvwPersonal.Object Do While Not rst.EOF Set objListItem = objListView.ListItems.Add(, "a" _ & rst![PersonalID], rst!Nachname) With objListItem .ListSubItems.Add , , rst!Vorname .ListSubItems.Add , , rst!DurchwahlBuero End With rst.MoveNext Loop rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 5.27: ListView mit Tabellendaten füllen
Markierten Datensatz ermitteln Ist das ListView gefüllt, lässt sich daraus leicht der Schlüssel des jeweils markierten Eintrags ermitteln. Dies erledigt man am besten immer beim Klick auf einen der Einträge. Die passende Ereignisprozedur, die übrigens auch beim Navigieren mit den Pfeiltasten durch die Liste ausgelöst wird, heißt ItemClick. Mit der Routine aus folgendem Listing geben Sie beim Klick auf einen Eintrag den Schlüssel des aktuellen Elements aus. Private Sub lvwPersonen_ItemClick(ByVal Item As Object) Debug.Print lvwPersonen.SelectedItem.Key End Sub Listing 5.28: Ausgeben des Schlüsselwertes beim Mausklick auf einen Eintrag
338
Steuerelemente
Der Key eines Elements setzt sich in diesem Fall aus dem Buchstaben »a« und dem Primärschlüsselwert des entsprechenden Datensatzes zusammen. Um hieraus den Primärschlüssel zu ermitteln, würde man etwa folgenden Ausdruck verwenden: Mid(lvwPersonen.SelectedItem.Key, 2)
Primärschlüssel in eigener Spalte speichern Alternativ zum Auslesen des Schlüssels und zum Ermitteln des Primärschlüssels aus dem Elementschlüssel per Textfunktion kann man auch direkt den Primärschlüssel in einer nicht sichtbaren Spalte des ListView-Steuerelements speichern. Dazu fügen Sie der Routine aus Listing 5.26 einfach noch eine Zeile zum Erzeugen eines weiteren Unterelements je Eintrag hinzu: .ListSubItems.Add , , rst!PersonalID
Natürlich brauchen Sie auch noch eine weitere Spalte im ListView-Steuerelement, das Sie am schnellsten über sein Eigenschaftsfenster einrichten können – nennen Sie es einfach ID. Der Name tut aber ohnehin nichts zur Sache, da Sie gleich über den Spaltenindex auf das Feld zugreifen werden.
Abbildung 5.39: ListView mit Tabellendaten
Zum Auslesen dieses Unterelements müssen Sie seinen Index kennen. Der Index der Einträge der Listenunterelemente beginnt mit 1. Da der Primärschlüssel in der Prozedur aus Listing 5.26 als letztes Element angelegt wird, hat der Index den Wert 3. Mit folgendem Ausdruck erhalten Sie den (versteckten) Index: lvwPersonen.SelectedItem.ListSubItems.Item(3).Text
Wenn Sie das ListView-Steuerelement beispielsweise dazu verwenden möchten, wie in Abbildung 5.40 im gleichen Formular die Detaildaten des ausgewählten Mitarbeiters anzuzeigen, kommen Sie mit folgendem Dreizeiler aus:
339
Kapitel 5 Private Sub lvwPersonen_ItemClick(ByVal Item As Object) Dim lngKey As Long lngKey = lvwPersonen.SelectedItem.ListSubItems.Item(3).Text Me.Recordset.FindFirst "PersonalID = " & lngKey End Sub Listing 5.29: Aktualisieren des im Formular angezeigten Datensatzes nach Auswahl im ListViewSteuerelement
Voraussetzung ist natürlich, dass das Formular an die Tabelle tblPersonal gebunden ist.
Abbildung 5.40: Das ListView-Steuerelement zur Auswahl von Mitarbeiterdetails
5.11.7 Icons im ListView-Steuerelement Einer der großen Vorteile des ListView-Steuerelements gegenüber dem Listenfeld von Access ist die Möglichkeit, Images und Icons anzuzeigen. Damit lassen sich sowohl An sichten ähnlich der Miniaturansicht im Windows Explorer als auch normale Listenein träge mit kleinen Icons anzeigen.
ListView-Steuerelement mit Bildern Das ListView-Steuerelement bietet mit seinen verschiedenen Ansichten unterschiedliche Möglichkeiten zur Darstellung von Bildern. Interessant ist dies etwa für Anwendungszwecke wie dem Anzeigen von Thumbnails oder zum Hinzufügen von Anwendungssymbolen zu Dateilisten. Diese beiden Fälle lernen Sie in den folgenden Abschnitten kennen.
340
Steuerelemente
Keine Bilder ohne ImageList Das ListView-Steuerelement zeigt keine Bilder ohne ImageList-Steuerelement an. Das ImageList-Steuerelement ist ein Container für Bilddateien, der entweder im Entwurf oder auch zur Laufzeit mit Bilddateien gefüllt werden kann. Die Bilder eines ImageListSteuerelements werden von Windows intern auf einer unsichtbaren Grafikfläche untergebracht, die man sich als ein rechteckiges Raster von Einzelbildern vorstellen kann – so wie die Zellen einer Tabelle. Damit es ein Einzelbild anhand des Index ermitteln kann, müssen die Zellen alle gleich groß sein. Die Gesamtfläche der Grafik ist durch die Ressourcen des Windows-GDI je nach Be triebssystem auf ca. 32 MB begrenzt, was bedeutet, dass die Grafik bei 32-Bit-Auflösung etwa unter WinXP eine maximale Ausdehnung von ca. 2.800 x 2.800 Pixel haben kann. Ein ImageList-Steuerelement kann also nur eine begrenzte Menge von Einzelbildern aufnehmen. Weitere Informationen zum ImageList-Steuerelement finden Sie weiter unten unter 5.12, »Das ImageList-Steuerelement«. Im Beispiel sollen die Dateien eines Verzeichnisses in einem ListView-Steuerelement an gezeigt werden – einschließlich des Icons für den jeweiligen Dateityp. Das Ergebnis soll wie in Abbildung 5.41 aussehen.
Abbildung 5.41: Das ListView-Steuerelement zeigt eine Liste von Dateien an
Die Daten zu diesem Beispiel stammen aus der Tabelle tblDateien, die lediglich das Verzeichnis und den Dateinamen einer Datei speichert (siehe Abbildung 5.42). Das ImageList- und das ListView-Steuerelement dieses Beispiels heißen lvwDateien und ctlIcons. Die Eigenschaft View des ListView-Steuerelements muss für die Anzeige von Textspalten inklusive Icon auf 3-lvwReport eingestellt werden (diese Einstellungen nehmen Sie entweder im Eigenschaften-Dialog des Steuerelements oder per VBA vor). Damit Sie Dateien zum Löschen markieren können, aktivieren Sie außerdem die Eigen schaft FullRowSelect.
341
Kapitel 5
Abbildung 5.42: Aufbau der Tabelle tblDateien
Das Formular enthält eine Schaltfläche namens cmdDateiHinzufuegen, die mit der Proze dur aus dem folgenden Listing einen Dialog zum Auswählen einer oder mehrerer Da teien öffnet und die Dateien über die Routine DateiSpeichern (siehe Listing 5.29) in der Tabelle tblDateien speichert. Die Funktion zum Anzeigen des Dateidialogs finden Sie im Modul mdlTools. Nach dem Speichern der Dateiinformationen ruft die Ereignisprozedur cmdDateiHinzufuegen_Click noch die Routine AnzeigeAktualisieren auf (siehe Listing 5.30). Diese Routine enthält die Funktionalität zum Füllen des ListView-Steuerelements. Private Sub cmdDateiHinzufuegen_Click() Dim strDateiname As String strDateiname = OpenFileName(CurrentProject.path, , _ "Alle Dateien(*.*)", , True) If Len(strDateiname) > 0 Then DateiSpeichern strDateiname AnzeigeAktualisieren End If End Sub Listing 5.30: Hinzufügen einer Datei zur Tabelle tblDateien
Private Sub DateiSpeichern(strPfad As String) Dim strVerzeichnis As String Dim strDateiname As String Dim intPositionLetzterBackslash intPositionLetzterBackslash = InStrRev(strPfad, "\") strVerzeichnis = Left(strPfad, intPositionLetzterBackslash) strDateiname = Mid(strPfad, intPositionLetzterBackslash + 1)
342
Steuerelemente CurrentDb.Execute "INSERT INTO tblDateien(Verzeichnis, " _ & "Dateiname) VALUES('" & strVerzeichnis & "','" _ & strDateiname & "')" End Sub Listing 5.31: Speichern von Verzeichnis und Dateiname einer Datei in der Tabelle tblDateien
Außerdem legen Sie auf der Registerseite Spaltenköpfe des Eigenschaftsfensters des List Views drei Spalten an. Die erste hat die Breite 200 und keine Spaltenüberschrift, die beiden weiteren die Breite 2000 und die Überschrift Verzeichnis beziehungsweise Datei name. Die Routine funktioniert ähnlich wie die aus dem vorherigen Beispiel. Nach dem De klarationsteil und dem Instanzieren beziehungsweise Erstellen der Verweise für die einzelnen Objektvariablen initialisiert die Routine das ListView-Objekt, indem es die Eigenschaft SmallIcons auf Nothing setzt. Das ist notwendig, weil das ListView nicht nur beim Öffnen, sondern auch nach dem Hinzufügen einer neuen Datei aktualisiert wird. Gleiches gilt für das ImageList-Objekt: Auch dieses wird initialisiert, allerdings mit der Clear-Methode der ListItems-Auflistung. Das Füllen der Steuerelemente erfolgt wieder in zwei Durchläufen durch die Datensätze der zugrunde liegenden Tabelle: Im ersten Durchlauf fügt die Routine die benötigten Icons im ImageList-Objekt ein. Das Icon für die Dateien zugeordnete Anwendung lässt sich relativ leicht ermitteln: Es reicht ein Aufruf der Funktion GetIconPic, die sich im Modul mdlOGL2007 befindet, das in Kapitel 11, »Bilder und binäre Dateien« ausführlich diskutiert wird. Private Sub AnzeigeAktualisieren() Dim db As DAO.Database Dim rst As DAO.Recordset Dim i As Integer Dim objListItem As ListItem Set objListView = Me!lvwDateien.Object Set objImageList = Me!imlIcons.Object Set db = CurrentDb Set rst = db.OpenRecordset("tblDateien", dbOpenDynaset) 'ListView initialisieren Set objListView.SmallIcons = Nothing 'ImageList initialisieren objImageList.ListImages.Clear objImageList.ImageHeight = 16 objImageList.ImageWidth = 16 objListView.ListItems.Clear If Not rst.EOF Then 'ImageList mit Icons füllen Do While Not rst.EOF
343
Kapitel 5 objImageList.ListImages.Add , "a" & rst!DateiID, _ GetIconPic(rst!Verzeichnis & rst!Dateiname) rst.MoveNext Loop 'ListView mit Icons und Dateiinformationen füllen rst.MoveFirst Set objListView.SmallIcons = objImageList objListView.ListItems.Clear Do While Not rst.EOF i = objImageList.ListImages("a" & rst!DateiID).Index Set objListItem = objListView.ListItems.Add(, "a" _ & rst!DateiID, , , i) objListItem.ListSubItems.Add , , rst!Verzeichnis objListItem.ListSubItems.Add , , rst!Dateiname rst.MoveNext Loop End If rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 5.32: Füllen des ListViews mit Dateiinformationen inklusive Icon
Nachdem die Icons im ImageList-Steuerelement untergebracht und mit einem Key versehen sind, der das spätere Zuordnen zum passenden Datensatz ermöglicht, geht es an das Füllen des ListView-Steuerelements. Die erste Spalte ist für das Icon reserviert. Dieses weist man mit dem letzten Parameter der Add-Methode der ListItems-Auflistung namens SmallIcon hinzu. Dieser Parameter erwartet den Index des zuzuordnenden Eintrags des ImageList-Objekts. Diesen ermitteln Sie eine Zeile vorher über die IndexEigenschaft des Eintrags mit dem entsprechenden Key. Die Methode ListSubItems eines frisch angelegten Elements im ListView fügt schließlich das zum Datensatz passende Verzeichnis und den Dateinamen ein. Damit das ListView-Steuerelement direkt beim Öffnen des Formulars gefüllt wird, legen Sie noch eine Routine an, die beim Laden des Formulars ausgelöst wird: Private Sub Form_Load() AnzeigeAktualisieren End Sub Listing 5.33: Aktualisieren des ListView-Steuerelements beim Laden des Formulars
Fehlt noch eine Schaltfläche, mit der Sie einen markierten Eintrag wieder herauswerfen können. Diese nennen Sie cmdDateiLoeschen und versehen sie mit folgender Ereignis prozedur:
344
Steuerelemente Private Sub cmdDateiLoeschen_Click() CurrentDb.Execute "DELETE FROM tblDateien WHERE DateiID = " _ & Replace(objListView.SelectedItem.Key, "a", "") AnzeigeAktualisieren End Sub Listing 5.34: Löschen der aktuell im ListView-Steuerelement ausgewählten Datei
5.11.8 Drag and Drop mit dem ListView-Steuerelement Im Gegensatz zu Listenfeldern können Sie im ListView-Steuerelement auch Drag and Drop einsetzen. Wie das funktioniert, erfahren Sie im folgenden Beispiel: Dabei sollen Daten aus zwei ListView-Steuerelementen hin- und hergeschoben werden – natürlich per Drag and Drop und nicht, wie sonst bei Listenfeldern üblich, per Doppelklick oder mit passenden Schaltflächen. Die Daten kommen dabei aus einer m:n-Beziehung zwischen zwei Tabellen mit Publikationen und Empfängern. Zusätzlich zu den dazu notwendigen Tabellen tblPublikationen und tblEmpfaenger brauchen Sie eine Tabelle zum Verknüpfen der Datensätze der beiden Tabellen namens tblVerteiler. Im Beziehungsfenster sieht das wie in Abbildung 5.43 aus.
Abbildung 5.43: Datenmodell des Verteiler-Beispiels
Das Formular soll in zwei ListView-Steuerelementen die Empfänger und die Nicht-Emp fänger der Publikation anzeigen (siehe Abbildung 5.44). Als Datensatzquelle des For mulars dient die Tabelle tblPublikationen. Das Formular zeigt beide Felder der Tabelle im Detailbereich an. Außerdem fügen Sie zwei ListView-Steuerelemente namens lvwEmpfaenger und lvwKeinEmpfaenger hinzu. Stellen Sie für beide die Eigenschaft View auf 3 – lvwReport ein. Fügen Sie außerdem auf der Registerseite Spaltenköpfe des Eigenschaftsfensters der ListView-Steuerelemente jeweils eine Spalte mit den Bezeichnungen Empfänger und Kein Empfänger hinzu. Für das Füllen beider ListView-Steuerelemente ist die folgende Routine verantwortlich, die jeweils mit einem Objektverweis auf das entsprechende ListView-Steuerelement und einer passenden Datenherkunft aufgerufen wird.
345
Kapitel 5
Abbildung 5.44: So soll das Formular zum Hin- und Herschieben von Empfängern aussehen
Private Sub ListeAktualisieren(objListView As ListView, strSQL As String) Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb Set rst = db.OpenRecordset(strSQL, dbOpenDynaset) objListView.ListItems.Clear Do While Not rst.EOF objListView.ListItems.Add , "a" & rst(0), rst(1) rst.MoveNext Loop rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 5.35: Diese Routine füllt eine Liste mit den Daten einer Tabelle
Den Aufruf dieser Routine übernimmt die Prozedur, die durch das Anzeigen eines Da tensatzes im Formular ausgelöst wird – so ist gewährleistet, dass die beiden ListViewSteuerelemente immer die Daten zur aktuellen Publikation anzeigen. Die Beim AnzeigenEreignisprozedur sieht wie folgt aus: Private Sub Form_Current() Dim strSQLEmpfaenger As String Dim strSQLKeinEmpfaenger As String If Not IsNull(Me.Publikation) Then strSQLEmpfaenger = "SELECT tblEmpfaenger.EmpfaengerID, " _ & "tblEmpfaenger.Empfaenger FROM tblEmpfaenger INNER JOIN " _ & "tblVerteiler ON tblEmpfaenger.EmpfaengerID = " _ & "tblVerteiler.EmpfaengerID WHERE " _ & "tblVerteiler.PublikationID = " _ & Me.PublikationID & " ORDER BY tblEmpfaenger.Empfaenger"
346
Steuerelemente strSQLKeinEmpfaenger = "SELECT tblEmpfaenger.EmpfaengerID, " _ & "tblEmpfaenger.Empfaenger FROM tblEmpfaenger WHERE " _ & "tblEmpfaenger.EmpfaengerID NOT IN " _ & "(SELECT tblEmpfaenger.EmpfaengerID FROM tblEmpfaenger " _ & "INNER JOIN tblVerteiler ON tblEmpfaenger.EmpfaengerID " _ & "= tblVerteiler.EmpfaengerID WHERE " _ & " tblVerteiler.PublikationID = " & Me.PublikationID _ & ") ORDER BY tblEmpfaenger.Empfaenger" ListeAktualisieren Me.lvwZugeordnet.Object, strSQLEmpfaenger ListeAktualisieren Me.lvwNichtZugeordnet.Object, _ strSQLKeinEmpfaenger End If End Sub Listing 5.36: Aufruf der Routine zum Füllen der beiden ListView-Steuerelemente
Die Routine stellt für jedes ListView-Steuerelement eine SQL-Anweisung zusammen, die aus den verknüpften Tabellen jeweils die Empfänger auswählt, die einer Publikation zugeordnet sind, und jene, die nicht mit der aktuellen Publikation verknüpft sind. Dann ruft sie die Prozedur aus Listing 5.35 auf und übergibt einen Objektverweis auf das jeweilige ListView-Steuerelement und die passende SQL-Anweisung. Die zweite SQL-Anweisung verwendet dabei fast die gleiche Anweisung wie die erste – allerdings nur als Unterabfrage. Damit ermittelt diese Abfrage alle Empfänger-Daten sätze, die nicht durch die erste SQL-Anweisung erfasst werden und dementsprechend nicht über die Tabelle tblVerteiler mit der aktuell angezeigten Publikation verknüpft sind.
Daten verschieben Um die Empfänger komfortabel verschieben zu können, stellen Sie noch die Eigenschaft Multiselect auf True ein – so lassen sich auch mehrere Empfänger gleichzeitig verschieben. Außerdem setzen Sie für beide ListView-Steuerelemente die Eigenschaft OLEDragMode auf den Wert 1 – ccOLEDragAutomatic und OLEDropMode auf 1 – ccOLEDropManual. Beim Drag and Drop mit ungebundenen Steuerelementen, wie es die ListViews nun einmal sind, ist wie auch beim TreeView-Steuerelement vor allem eines zu beachten: Neben dem Verschieben des Eintrags zwischen den Listen müssen auch die dahinter liegenden Datenbanktabellen aktualisiert werden. Für das Ziehen eines Eintrags von einem zum anderen ListView benötigen Sie nur zwei Ereignisprozeduren – vorausgesetzt, Sie verzichten auf jeglichen Schnickschnack: OLEStartDrag beim Ausgangs-ListView-Steuerelement OLEDragDrop beim Ziel-ListView-Steuerelement
347
Kapitel 5
Die OLEStartDrag-Prozedur durchläuft in einer Schleife alle Elemente des ListViewSteuerelements und prüft, welche Elemente markiert sind. Alle markierten Elemente werden an eine String-Variable angehängt – und zwar in der Form: ¦; Wozu das alles? Ganz einfach: Die Parameterliste der Ereignisprozedur enthält ein Objekt namens Data, das zunächst mit der Clear-Methode geleert und dann über die SetData-Methode mit der soeben zusammengestellten Liste gefüllt wird. Dieses Objekt (OLE-DataObject) ist eine Art Zwischenablage für Drag-and-Drop-Operationen und wird im Ziel-Steuerelement ausgelesen. Private Sub lvwNichtZugeordnet_OLEStartDrag(Data As Object, _ AllowedEffects As Long) Dim objListItem As ListItem Dim strData As String Dim strDataItems() As String For Each objListItem In lvwKeinEmpfaenger.ListItems If objListItem.Selected = True Then strData = strData & objListItem.Key & "¦" _ & objListItem.Text & ";" End If Next Data.Clear Data.SetData strData, ccCFText End Sub Listing 5.37: Der Drag-and-Drop-Vorgang startet im ListView lvwKeinEmpfaenger …
Dies geschieht dann in der OLEDragDrop-Ereignisprozedur des Ziel-ListView-Steuer elements: Hier taucht das Objekt Data erneut in der Parameterliste auf. Die GetDataMethode gibt den Inhalt dieses Objekts preis. In der String-Variablen strData angekommen, werden die Daten direkt weiterverarbeitet: Nach der Prüfung, ob mindestens ein »Pipe«-Zeichen (¦) in der übergebenen Zeichenkette enthalten ist, teilen die folgenden Anweisungen die Zeichenkette zunächst in die durch Semikola (;) getrennten Bestand teile auf und speichern sie in einem String-Array. Dieses wird in einer For Next-Schleife über alle Elemente durchlaufen, wobei die Split-Anweisung die Elemente vor und hin ter dem »Pipe«-Zeichen in die Variablen strKey und strText extrahiert. Und damit lässt sich natürlich leicht ein neues Element zum ListView-Steuerelement hinzufügen. Glei ches gilt auch andersherum: Nachdem die Add-Methode der ListItems-Auflistung des Ziel-ListView-Steuerelements das neue Element hinzugefügt hat, entfernt die RemoveMethode selbiges aus dem Ausgangs-ListView-Steuerelement: Private Sub lvwEmpfaenger_OLEDragDrop(Data As Object, Effect As Long, _ Button As Integer, Shift As Integer, x As Single, y As Single) Dim strData As String Dim strDataItems() As String Dim strText As String
348
Steuerelemente Dim strKey As String Dim i As Integer strData = Data.GetData(ccCFText) If InStr(1, strData, "¦") <> 0 Then If Right(strData, 1) = ";" Then strData = Left(strData, Len(strData) - 1) End If strDataItems = Split(strData, ";") For i = 0 To UBound(strDataItems) strKey = Split(strDataItems(i), "¦")(0) strText = Split(strDataItems(i), "¦")(1) If IsNull(DLookup("EmpfaengerID", "tblVerteiler", _ "PublikationID = " & Me.PublikationID _ & " AND EmpfaengerID = " & Mid(strKey, 2))) Then lvwEmpfaenger.ListItems.Add , strKey, strText lvwKeinEmpfaenger.ListItems.Remove strKey CurrentDb.Execute "INSERT INTO tblVerteiler" _ & "(PublikationID, EmpfaengerID) " & "VALUES(" _ & Me.PublikationID & ", " & Mid(strKey, 2) & ")" End If Next i End If End Sub Listing 5.38: … und endet im ListView lvwZugeordnet
Mit diesen beiden Routinen können Sie nun schon Elemente von rechts nach links, aber noch nicht von links nach rechts ziehen. Das lässt sich aber leicht nachholen: Legen Sie einfach die passenden Ereignisprozeduren analog für das jeweils andere ListViewSteuerelement an – die kleinen Änderungen können Sie in der Beispieldatenbank einsehen. Beim Probieren fällt allerdings eines auf: Nach ein paar Drag-and-Drop-Operationen und einem Wechsel zu einem anderen Publikations-Datensatz im Formular und wieder zurück enthält jedes ListView-Steuerelement wieder die ursprünglichen Elemente. Das liegt daran, dass die Datenbankseite bisher außen vor gelassen wurde: Sie können zwar lustig Elemente zwischen den ListView-Steuerelementen hin- und herschieben, aber die entsprechenden Änderungen an der Verknüpfungstabelle tblVerteiler erfolgen nicht. Der Aufwand hierfür ist allerdings vergleichsweise gering: Für das Hinzufügen eines Kontakts zur Liste der Empfänger müssen Sie schließlich nur einen Datensatz mit der PublikationID und der EmpfaengerID zur Tabelle tblVerteiler hinzufügen; zum Entfernen des Empfängers aus der Liste löschen Sie einfach den passenden Eintrag aus der Tabelle tblVerteiler. Ein Platz für die SQL-Anweisungen ist auch schnell gefunden: Da die Änderungen je weils für alle markierten Elemente vorzunehmen sind, gehört die entsprechende An
349
Kapitel 5
weisung selbstverständlich in eine der For Next-Schleifen, die alle zu bewegenden Ele mente durchläuft. Soll ein Eintrag aus dem Verteiler entfernt werden, fügen Sie diese Anweisung in die Routine lvwNichtZugeordnet_OLEDragDrop ein (in einer Zeile): CurrentDb.Execute "DELETE FROM tblVerteiler WHERE PublikationID = " & Me.PublikationID & " AND EmpfaengerID = " & Mid(strKey, 2)
Die entsprechende Anweisung in der Routine lvwZugeordnet_OLEDragDrop sieht so aus: CurrentDb.Execute "INSERT INTO tblVerteiler(PublikationID, Empfaenge-rID) VALUES(" & Me.PublikationID & ", " & Mid(strKey, 2) & ")"
5.11.9 Reihenfolge per Drag and Drop einstellen Neben dem Bewegen von Elementen zwischen zwei ListView-Steuerelementen ist auch das Einstellen der Reihenfolge von Elementen ein häufig gefragtes Thema. Dazu ist zunächst ein Feld in der Herkunftstabelle vorzusehen, das einen die Reihenfolge festlegenden Wert enthält – typischerweise heißt dieses Feld dann auch Reihenfolge. Wichtig ist die Überlegung, wann die Änderungen der Reihenfolge im ListView in die zugrunde liegende Tabelle übertragen werden. Wenn man dies nach jeder Änderung erledigt, ist schon eine Menge Code notwendig. Daher beschränkt sich dieses Beispiel auf das Übertragen der Änderungen der Reihenfolge auf das Schließen des Formulars – das heißt, dass dann einmalig alle Elemente des ListViews durchlaufen und ihre neue Reihenfolge im Feld Reihenfolge der entsprechenden Tabelle vermerkt wird.
Aufbau des Formulars zum Einstellen der Reihenfolge per Drag and Drop Das Formular frmListViewReihenfolge enthält ein ListView-Steuerelement namens lvwRei henfolge, das mit Daten aus der Tabelle tblPersonal gefüllt werden soll. Diese Tabelle enthält neben den eigentlichen Feldern ein Feld namens Reihenfolge. Das ListView-Steuerelement im Formular zeigt die Daten wie gehabt in der Ansicht lvwReport an. OLEDragMode und OLEDropMode stellen Sie auf 1 – ccOLEDragAutomatic beziehungsweise 1 – ccOLEDropManual ein. Da die Elemente mit der Maus verschoben werden sollen, wäre es gut, wenn man die Elemente über die ganze Spalte „greifen“ könnte – dazu aktivieren Sie die Eigenschaft FullRowSelect. Das ListView-Steuerelement soll zwei Spalten anzeigen, deren Köpfe die Texte Nachname und Vorname enthalten.
Füllen des ListView-Steuerelements Das Füllen des Steuerelements erfolgt beim Öffnen des Formulars in der Ereignispro zedur Form_Load. Die Routine erzeugt für jeden Datensatz ein Element im ListView, wo-
350
Steuerelemente
bei die Startreihenfolge über die im Feld Reihenfolge festgelegten Zahlenwerte festgelegt ist. Der Inhalt dieses Feldes ist interessanterweise nicht Bestandteil der Elemente des ListView-Steuerelements. Der Grund ist, dass ein ListItem mit seinem Index bereits die aktuelle Reihenfolgeposition enthält. Private Dim Dim Dim Set Set
Sub Form_Load() db As DAO.Database rst As DAO.Recordset objListitem As ListItem db = CurrentDb rst = CurrentDb.OpenRecordset("SELECT * FROM tblPersonal " _ & "ORDER BY Reihenfolge", dbOpenDynaset) Do While Not rst.EOF Set objListitem = objListView.ListItems.Add(, "a" _ & rst!PersonalID, rst!Nachname) objListitem.ListSubItems.Add , , rst!Vorname rst.MoveNext Loop End Sub
Listing 5.39: Das ListView wird beim Öffnen des Formulars mit den Datensätzen der Tabelle tblPersonen gefüllt
Start des Drag-and-Drop-Vorgangs Der Drag-and-Drop-Vorgang beginnt mit dem Markieren eines Elements mit der Maus und dem anschließenden Bewegen des Mauszeigers. Dies erkennt das Steuerelement und löst die Ereignisprozedur OLEStartDrag aus. Die Routine verwendet die Eigenschaft SelectedItem, um das markierte Element zu ermitteln. Dessen Daten werden durch ein »Pipe«-Zeichen (¦) getrennt in eine String-Variable geschrieben und dem Data-Parameter der Ereignisprozedur übergeben. Private Sub lvwReihenfolge_OLEStartDrag(Data As Object, _ AllowedEffects As Long) Dim strKey As String Dim objListitem As ListItem Set objListitem = objListView.SelectedItem strKey = objListitem.Key & "¦" & objListitem.Text & "¦" _ & objListitem.ListSubItems(1).Text _ & "¦" & objListitem.Index '& "¦" & objListitem.ListSubItems(2).Text Data.Clear Data.SetData strKey End Sub Listing 5.40: Ermitteln des markierten Elements und Zwischenspeichern seiner Daten
351
Kapitel 5
Von dort kann die Ereignisprozedur OLEDragDrop die Zeichenkette wieder auslesen – dazu später mehr. Erstmal tritt die Ereignisprozedur OLEDragOver auf den Plan. Diese wird regelmäßig während des Ziehens ausgelöst. Hier sorgt sie dafür, dass das Element, über dem sich der Mauszeiger aktuell befindet, markiert wird. So kann der Benutzer genau erkennen, wo das zu verschiebende Element landet: Private Sub lvwReihenfolge_OLEDragOver(Data As Object, Effect As Long, _ Button As Integer, Shift As Integer, x As Single, y As Single, _ State As Integer) Set objListView.DropHighlight = objListView.HitTest(x, y) End Sub Listing 5.41: Diese Routine sorgt für das Markieren des Elements, über dem sich der Mauszeiger aktuell befindet
Drag-and-Drop-Vorgang abschließen Das Loslassen der Maustaste beziehungsweise das »Droppen« löst die Ereignisprozedur OLEDragDrop aus. Zu diesem Zeitpunkt sind nun alle für die Operation notwendigen In formationen bekannt – nämlich das zu verschiebende Element und das Element, dessen Position es einnehmen soll. Die Daten des zu verschiebenden Elements entnimmt die Routine dem Data-Parameter der Ereignisprozedur – unter anderem auch den Index der vorherigen und der Zielposition. Unter Umständen versucht der Benutzer, den Eintrag auf sich selbst zu ziehen – in dem Fall soll natürlich keine Änderung der Reihenfolge vorgenommen werden. Anderenfalls entfernt die Routine das Ausgangselement und legt es an der gewünschten Stelle neu an. Außerdem wird die Boolean-Variable bolReihenfolgeGeaendert, die Sie im Übrigen modul weit und nicht in der Routine deklarieren, auf den Wert True gesetzt. Wozu diese Variable notwendig ist, erfahren Sie weiter unten. Private Sub lvwReihenfolge_OLEDragDrop(Data As Object, Effect As Long, Button As Integer, Shift As Integer, x As Single, y As Single) Dim strData As String Dim strKey As String Dim strNachname As String Dim strVorname As String Dim lngReihenfolgeAlt As Long Dim lngIndex As Long Dim lngReihenfolgeNeu As Long Dim objListitem As MSComctlLib.ListItem strData = Data.GetData(ccCFText) strKey = Split(strData, "¦")(0)
352
Steuerelemente strNachname = Split(strData, "¦")(1) strVorname = Split(strData, "¦")(2) lngReihenfolgeAlt = CLng(Split(strData, "¦")(3)) lngIndex = objListView.HitTest(x, y).Index lngReihenfolgeNeu = CLng(objListView.HitTest(x, y).Index) If Not lngReihenfolgeNeu = lngReihenfolgeAlt Then objListView.ListItems.Remove strKey Set objListitem = objListView.ListItems.Add(lngIndex, strKey, _ strNachname) objListitem.ListSubItems.Add , , strVorname bolReihenfolgeGeaendert = True End If End Sub Listing 5.42: Diese Routine führt die eigentliche Drag-and-Drop-Operation durch
Neue Reihenfolge speichern Fehlt noch der letzte Schritt: Die neue Reihenfolge soll beim Schließen des Formulars gespeichert werden. Damit dies nicht unnötig geschieht, also wenn etwa gar keine Än derung der Reihenfolge vorgenommen wurde, gibt es die oben erwähnte Boolean-Varia ble bolReihenfolgeGeaendert. Diese wird nur bei einer Änderung der Reihenfolge auf den Wert True gesetzt. Die Ereignisprozedur Form_Close fragt diese Variable beim Schließen des Formulars ab und speichert die neue Reihenfolge nur, wenn diese Variable den Wert True enthält. Das Speichern ist ein Rundumschlag: Da die Anwendung nicht nachvollzieht, welche Elemente ihre Reihenfolge verändert haben, muss die Reihenfolge aller Elemente auf den neuesten Stand gebracht werden. Dazu durchläuft die Routine in einer For EachSchleife alle Elemente und führt jeweils eine SQL-Update-Anweisung aus: Private Sub Form_Close() Dim db As DAO.Database Dim objListitem As ListItem If bolReihenfolgeGeaendert = True Then Set db = CurrentDb For Each objListitem In objListView.ListItems db.Execute "UPDATE tblPersonal SET Reihenfolge = " _ & objListitem.Index & " WHERE PersonalID = " _ & Mid(objListitem.Key, 2) Next objListitem End If Set db = Nothing End Sub Listing 5.43: Speichern der neuen Reihenfolge in der zugrunde liegenden Tabelle der Datenbank
353
Kapitel 5
5.12 Das ImageList-Steuerelement Die Einträge in Steuerelementen wie dem TreeView-Steuerelement oder dem ListViewSteuerelement werden aus optischen und praktischen Gründen gerne mit Icons versehen. Dies funktioniert am einfachsten mit dem ListView-Steuerelement, das Sie weiter oben bereits kurz kennen gelernt haben und das selbst nur im Entwurf eines Formulars sichtbar ist.
ImageList einfügen Wenn Sie das TreeView- oder das ListView-Steuerelement in einem Formular verwenden und die Listeneinträge mit Icons versehen möchten, benötigen Sie zusätzlich ein ImageList-Steuerelement. Dieses Steuerelement fungiert als eine Art »Container« für Images und stellt diese bei Bedarf zur Verfügung. Ausgehend von einem Formular, das bereits das gewünschte ListView- oder TreeViewSteuerelement enthält, müssen Sie das ImageList-Steuerelement zunächst hinzufügen. Dazu verwenden Sie den Dialog ActiveX-Steuerelement einfügen, den Sie über den Rib bon-Eintrag Entwurf|Steuerelemente|ActiveX-Steuerelement einfügen anzeigen. Die Positi on des eingefügten ImageList-Steuerelements (Microsoft ImageList Control, version 6.0) müssen Sie nicht anpassen, da es in der Formularansicht ohnehin nicht sichtbar ist. Ver geben Sie einen Namen wie imlBilder. Achten Sie wie bei den anderen ActiveX-Steuerelementen immer darauf, die aktuelle Version des Steuerelements zu verwenden – in diesem Fall ist das die Version 6.0.
Füllen des ImageList-Steuerelements Bevor Sie Bilder aus dem ImageList-Steuerelement in einem TreeView- oder einem ähnlichen Steuerelement anzeigen können, müssen Sie diese Bilder im ImageListSteuerelement speichern. Dazu gibt es, wie unter Access üblich, mindestens zwei verschiedene Wege.
Manuelles Füllen der ImageList Die erste Variante erfordert lediglich einen Doppelklick auf das ImageList-Steuerelement in der Entwurfsansicht und die Auswahl der gewünschten Abbildungen auf der zweiten Registerseite des Dialogs Eigenschaften von ImageListControl. Zuvor können Sie auf der ersten Seite festlegen, welche Größe die gespeicherten Abbildungen beim Anzeigen im entsprechenden Steuerelement aufweisen sollen. Diese Einstellung gilt übrigens für alle Abbildungen und lässt sich nicht variieren. Wenn Sie unterschiedlich große Abbildungen benötigen, verwenden Sie mehrere ImageList-Steuerelemente (siehe Abbildung 5.45).
354
Steuerelemente
Abbildung 5.45: Einstellen der Größe der gespeicherten Images
Zum Einfügen einer Abbildung klicken Sie auf der zweiten Registerseite auf die Schalt fläche Bild einfügen… und wählen im nun folgenden Dialog die einzufügende Bilddatei aus. Sollten wie in Abbildung 5.46 bereits Bilder in der Liste enthalten sein, markieren Sie das Bild, hinter dem die neue Abbildung eingefügt werden soll. Dies ist umso wichtiger, wenn Sie bereits Bilder in einem Steuerelement per Ordinalzahl referenzieren – wenn Sie ein neues Bild an erster Stelle einfügen, verschieben sich alle anderen Bilder um eine Position.
Abbildung 5.46: Liste der gespeicherten Abbildungen
Einfügen von Bildern per VBA Natürlich können Sie die Abbildungen auch per VBA in das ImageList-Steuerelement einfügen. Dazu verwenden Sie etwa die folgende Routine. Die Routine können Sie bei-
355
Kapitel 5
spielsweise beim Öffnen des Formulars aufrufen. Sie deklariert zunächst eine Objekt variable des Typs MSComctlLib.ImageList und weisen dieser das im Formular enthaltene ImageList-Steuerelement zu. Dies ist nur dann notwendig, wenn Sie die IntelliSenseFunktion des VBA-Editors verwenden möchten, anderenfalls können Sie auch direkt über den Namen des Steuerelements auf dieses zugreifen. Private Sub LoadImages() Dim objImagelist As MSComctlLib.ImageList Dim i As Integer Set objImagelist = Me.ctlImageList.Object With objImagelist .ImageHeight = 16 .ImageWidth = 16 .ListImages.Clear For i = 1 To 4 .ListImages.Add i, "icon" & Format(i, "000"), _ LoadPicture(CurrentProject.Path & "\icon" _ & Format(i, "000") & ".ico") Next i End With End Sub Listing 5.44: ImageList-Steuerelement mit Icons füllen
Die Routine stellt direkt die Eigenschaften Width und Height der Abbildungen ein. Die Clear-Methode leert das ImageList-Steuerelement vor dem Einfügen der Icons. Die Anweisung in der anschließenden For Next-Schleife füllt die ersten vier Plätze des Steuerelements mit den Icon-Dateien namens Icon001.ico, Icon002.ico und so weiter. Dabei kommt die LoadPicture-Methode zum Einsatz. Die Routine funktioniert nur richtig, wenn die vier Icon-Dateien sich im gleichen Verzeichnis wie die Datenbank befinden, andernfalls müssen Sie die Routine Ihren Bedürfnissen anpassen. Mit der Methode LoadPicture der Access.Application-Bibliothek können Sie nur die folgenden Bilddateiformate laden: ICO, CUR, GIF, BMP, WMF, EMF und JPG.
Icons in anderen Steuerelementen verwenden Zum Anzeigen der Icons aus dem ImageList-Steuerelement in einem anderen Steuerele ment als dem TreeView-Steuerelement ist wiederum der Einsatz von VBA gefragt. Das TreeView-Steuerelement aus Abbildung 5.47 enthält beispielsweise vier verschiedene Icons. Das Zuweisen der Icons erfolgt am Besten direkt mit dem Aufbauen der Knoten des Baums (weitere Informationen hierzu finden Sie weiter vorne in diesem Kapitel). Es gibt zwei Möglichkeiten zum Zuweisen der gewünschten Abbildung:
356
Steuerelemente
direkt beim Anlegen des Knotens mit dem entsprechenden Parameter der AddMethode nach dem Anlegen des Knotens durch Zuweisen des Index-Wertes der Abbildung an die Eigenschaft Image des dem Knoten entsprechenden Node-Objekts In beiden Fällen müssen Sie der Eigenschaft ImageList des TreeView-Steuerelements das ImageList-Steuerelement zuweisen. Jedes Element des TreeView-Steuerelements enthält zusätzlich zwei Eigenschaften namens SelectedImage und ExpandedImage. Diesen Eigenschaften können Sie die Ordi nalzahl der Bilder aus dem ImageList-Steuerelement zuweisen, die beim entsprechenden Zustand des Knotens angezeigt werden sollen. Der Index der beim ausgeklappten Zustand des Elements anzuzeigenden Abbildung kann ebenfalls mit der Add-Methode angegeben werden. Die Eigenschaft SelectedImage kann jedoch nur in Zusammenhang mit einem bestehenden Node-Objekt verwendet werden. Private Sub BeispielPictureText() Dim objNode As MSComctlLib.Node Dim i As Integer Dim j As Integer Dim k As Integer Set objImagelist = Me.ctlImageList.Object objTreeView.ImageList = objImagelist objTreeView.Style = tvwPictureText For i = 1 To 5 Set objNode = objTreeView.Nodes.Add(, , _ "node" & Format(i, "00"), "Eintrag " & Format(i, "00")) objNode.Image = 1 objNode.ExpandedImage = 2 For j = 1 To 5 Set objNode = objTreeView.Nodes.Add("node" _ & Format(i, "00"), tvwChild, "node" & Format(i, "00") _ & Format(j, "00"), "Eintrag " _ & Format(i, "00") & Format(j, "00")) objNode.Image = 3 For k = 1 To 5 Set objNode = objTreeView.Nodes.Add("node" _ & Format(i, "00") & Format(j, "00"), tvwChild, _ "node" & Format(i, "00") & Format(j, "00") _ & Format(k, "00"), "Eintrag " & Format(i, "00") _ & Format(j, "00") & Format(k, "00")) objNode.Image = 4 Next k Next j Next i End Sub Listing 5.45: Einfügen von Bildern aus dem ImageList-Steuerelement in ein TreeViewSteuerelement
357
Kapitel 5
Die Routine füllt den Baum aus Abbildung 5.47, wobei die Abbildungen jeweils explizit mit der jeweiligen Eigenschaft hinzugefügt werden.
Zentrale Aufbewahrung Wenn Sie mehrere Formulare haben, die auf die gleichen Bilder zugreifen, können Sie das ImageList-Steuerelement in einem speziell dafür angelegten – eventuell unsichtbaren – Formular unterbringen. Sie können dann von den anderen Formularen aus zentral auf die Bilder zugreifen und müssen diese nicht mehrfach speichern.
Abbildung 5.47: TreeView-Steuerelement mit drei verschiedenen Icons
5.13 Tipps und Tricks zu Steuerelementen Nachfolgend finden Sie noch einige allgemeine Tipps für den Umgang mit Steuerele menten.
5.13.1 Standardeinstellungen speichern Es ist ja schön, dass Microsoft neue Schriftarten mit ClearType-Unterstützung liefert, aber diese in Steuerelementen mit der Standardgröße 11 anzuzeigen ist doch etwas übertrieben: In älteren Access-Versionen kamen die texthaltigen Steuerelemente mit der Schriftgröße 8 daher, was wiederum oft zu klein wirkte. Wenn Sie automatisch Steuerelemente mit einer kleineren Schriftgröße verwenden möchten, gehen Sie folgendermaßen vor: Legen Sie eine neue Datenbank mit dem Namen leer.accdb an und speichern Sie diese im Verzeichnis c:\Programme\Microsoft Office\Templates\1031\Access (in der Online
358
Steuerelemente
hilfe steht übrigens, dass man den Dateinamen Blank.accdb verwenden soll, was aber nicht funktioniert – hier hat Microsoft wohl zu viel (oder zu wenig) übersetzt ...). Öffnen Sie ein neues Formular in der Entwurfsansicht. Klicken Sie im Ribbon im Bereich Entwurf|Steuerelemente auf das Steuerelement, dessen Schriftgröße Sie anpassen möchten – etwa ein Textfeld. Zeigen Sie das Eigenschaftsfenster von Access an und stellen Sie die Eigenschaft Schriftgrad auf den gewünschten Wert ein. Verfahren Sie genauso für die übrigen anzupassenden Steuerelemente und Eigen schaften. Speichern Sie das Formular unter dem Namen Normal und schließen Sie die Daten bank. Beim nächsten Anlegen einer neuen, leeren Datenbank ist diese nicht mehr leer, sondern enthält die Formularvorlage. Damit können Sie nun direkt loslegen, ohne dass Sie jedesmal die angepassten Eigenschaften einstellen müssen. Diese Vorlage können Sie natürlich noch für viele andere Dinge einsetzen: So können Sie etwa Performance-Brem sen wie die Objektnamen-Autokorrektur, die üblicherweise aktiviert ist, von vornherein deaktivieren.
5.13.2 Autoformate Eine weitere Möglichkeit, sich Arbeit beim Formatieren von Steuerelementen zu sparen, sind Autoformate. Schauen Sie sich einfach an, wie es funktioniert: Öffnen Sie ein Formular, dessen Steuerelemente wie gewünscht formatiert sind. Wählen Sie den Ribbon-Eintrag Anordnen|Autoformat|Autoformat-Assistent aus. Klicken Sie im nun erscheinenden Dialog auf die Schaltfläche Anpassen. Wählen Sie im Dialog Autoformat anpassen die Option Ein neues AutoFormat basierend auf Formular erstellen. Geben Sie einen Namen für das Autoformat ein. Tipp: Stellen Sie einen Unterstrich (_) voran, damit dieses Format ganz oben in der Liste der Autoformate erscheint. Schließen Sie den Dialog. Nun können Sie ein beliebiges vorhandenes Formular in der Entwurfsansicht öffnen und wählen das soeben erstelle Autoformat aus. Gegebenenfalls fehlen noch Formate für Steuerelemente, die im Ausgangsformular nicht enthalten waren, aber im anzupassenden Formular vorhanden sind. Für diese können Sie entweder den Stil eines anderen Steuerelements übernehmen oder es zunächst unverändert lassen. Anschließend aktua-
359
Kapitel 5
lisiert Access die Steuerelemente des Formulars auf die im Autoformat gespeicherten Formate. Per VBA können Sie ein Autoformat mit folgender Anweisung verwenden: DoCmd.RunCommand acCmdApplyAutoFormat1
Die Zahl 1 am Ende der acCmdApplyAutoFormat-Konstanten können Sie dabei durch weitere Zahlenwerte ersetzen, um auf die anderen Autoformate zuzugreifen.
360
6 Berichte Mit Berichten lassen sich Daten in nahezu beliebiger Weise darstellen. Voraussetzung ist, dass Sie genau wissen, wie ein solcher Bericht arbeitet – also wie Sie Gruppierungen und Sortierungen verwenden, wie Sie Unterberichte einset zen, in welcher Reihenfolge der Bericht die einzelnen Er eignisse abarbeitet, wo Sie innerhalb dieser Ereignisse Aktionen etwa zum Ein- oder Ausblenden von Bereichen oder Steuerelementen unterbringen und so weiter. Dieses Kapitel hilft Ihnen, die Funktion von Berichten zu verstehen, und zeigt außerdem, wie sich Daten der gängigen Be ziehungsarten darstellen lassen. Außerdem zeigt es, wie Sie die neuen Features von Access 2007 am besten nutzen können. Und die sind eine kleine Revolution: Sie können damit genau wie mit Formularen in einer Layoutansicht arbeiten und den Bericht bei angezeigten Daten anpassen und – noch besser – Sie können die Daten des Berichts sogar während der Berichtsvorschau filtern und sortieren.
6.1 Berichte erstellen Microsoft verspricht, dass Sie mit Access 2007 noch schneller zu anspruchsvollen Berichten kommen als mit älteren Versionen. Das gilt in erster Linie für Einsteiger, die nor male Ansprüche an das Aussehen und die Funktion eines Berichts haben. Auch erfahrene Access-Entwickler dürften Spaß an dem einen oder anderen Feature haben, das die folgenden Abschnitte vorstellen. Aber wenn es ans Einge machte geht – wie beispielsweise beim Erstellen aufwändiger Berichte wie etwa der Rechnung, die Sie am Ende
Kapitel 6
dieses Kapitels kennen lernen – helfen die neuen Möglichkeiten zum Erstellen von Be richten auch nicht mehr weiter. Die Beispiele dieses Kapitels finden Sie auf der Buch-CD in der Datenbankdatei \Kap_06\Berichte.accdb. In der Beispieldatenbank ist die Option zur Anzeige einzelner Fenster statt der Registeransicht eingestellt (Einstellung in den Access-Optionen unter Aktuelle Datenbank|Anwendungsoptionen|Dokumentfensteroptionen). In dieser Ansicht gibt es ein kleines Problem: Wenn Sie einen Bericht oder ein Formular maximieren, gibt es keine Schließen-Schaltfläche mehr. Sie können die Objekte dann entweder mit der Tastenkombination Strg + F4 oder mit dem Schließen-Eintrag des Kontextmenüs schließen.
Achtung, verwirrende Ansichtsvielfalt! Berichte unter Access 2007 liefern direkt zwei neue Ansichten: Die erste ist die Layout ansicht, die Sie bereits von den Formularen kennen. Die zweite ist die interaktive Be richtsansicht. Diese liefert ein ähnliches Bild wie die bereits von älteren Access-Versionen bekannte Seitenansicht, wartet aber mit Interaktivität auf: Dafür sorgt eine ganze Reihe neuer Ereigniseigenschaften, die Sie samt der Berichtsansicht ganz hinten in diesem Ka pitel kennen lernen. Noch eines ist in diesem Zusammenhang wichtig: Sie können die Ansicht generell über den Ribbon-Eintrag Start|Ansichten ändern, nur in der Seitenansicht nicht. Hier finden Sie die passenden Befehle im Kontextmenü. Und noch ein Hinweis: Sie können die Layoutansicht mit der Berichtseigenschaft Lay outansicht zulassen zur Bearbeitung durch den Benutzer freigeben oder sperren. Diese Ansicht erlaubt es dem Benutzer, schnell größere Änderungen an einem Bericht vorzunehmen und ihn damit möglicherweise unbrauchbar zu machen. Sie sollten ihn darauf hinweisen oder diese Ansicht sperren.
6.1.1 Anlegen eines Berichts Access liefert einige Ribbon-Einträge, über die Sie Berichte erstellen können. Wenn Sie etwa den Eintrag Erstellen|Berichte|Bericht verwenden möchten, um einen einfachen Bericht zu erstellen, müssen Sie zuvor eine Tabelle oder Abfrage mit den anzuzeigenden Daten auswählen. Access erstellt dann einen Bericht, den Sie schnell durch Auswählen eines der zur Verfügung stehenden Autoformate (Ribbon-Eintrag Format|Autoformat) anpassen können. Wenn Sie einen solchen Bericht erstellen, öffnet Access diesen in der Layoutansicht. Da kommt schon ein wenig Excel-Feeling auf, wenn der Bericht mit gestrichelten Linien andeutet, welche Informationen auf welcher Seite ausgedruckt wer-
362
Berichte
den – davon abgesehen ist das aber sehr hilfreich, um die Felder auf das richtige Maß zu stutzen (siehe Abbildung 6.1). Mehr zur Layoutansicht lesen Sie weiter unten.
Abbildung 6.1: Ein automatisch erstellter Bericht in der Layoutansicht
Der Assistent zum Anlegen von Berichten hat sich im Vergleich zu den Vorgängerversio nen nicht wesentlich geändert; probieren Sie ihn einfach aus und entscheiden Sie, ob Sie Ihre Berichte lieber damit oder von Hand erstellen möchten. Beachten Sie dabei, dass der Assistent immer ein Bild im Berichtskopf einbettet, das eine Menge Speicherplatz braucht. Wenn Sie lieber selbst Hand anlegen, klicken Sie einfach auf den Ribbon-Eintrag Er stellen|Berichte|Berichtsentwurf. Sie erhalten dann einen leeren Bericht in der Entwurfs ansicht, den Sie von Grund auf neu gestalten können. Im ersten Schritt wählen Sie eine Tabelle, Abfrage oder einen SQL-Ausdruck als Daten satzquelle für den Bericht aus oder stellen die Daten in einer neuen Abfrage zusammen. Anschließend ziehen Sie die gewünschten Felder aus der Feldliste (Ribbon-Eintrag Entwurf|Tools|Vorhandene Felder hinzufügen) in den Berichtsentwurf. Weiter unten erfahren Sie, in welche Bereiche des Berichts welche Daten gehören, und Abschnitt 4.1.1 des Kapitels »Formulare« liefert weitere Informationen über die Feldliste.
363
Kapitel 6
Natürlich können Sie wie in Formularen auch andere Steuerelemente als die beim Hin zufügen von Elementen der Feldliste entstehenden Textfelder und sonstige einfache Steuerelemente hinzufügen, indem Sie diese im Ribbon-Bereich Entwurf|Steuerelemente markieren und anschließend dem Entwurf hinzufügen. Diese können Sie anschließend durch Festlegen entsprechender Eigenschaften wie Steuerelementinhalt oder Datensatzherkunft an die dem Bericht zugrunde liegenden Daten oder auch an andere Datenquellen binden.
6.1.2 Vereinfachtes Layouten Genau wie bei Formularen liefert Access 2007 einige Features, die eine komfortablere Anordnung der Steuerelemente erlauben. Zum Nachvollziehen der folgenden Techniken erstellen Sie mit dem Ribbon-Eintrag Erstellen|Berichte|Bericht einen Bericht auf Basis einer beliebigen Tabelle. Die Tabellenfelder werden dabei automatisch in tabellarischer Form angeordnet (siehe Abbildung 6.2).
Abbildung 6.2: Ein einfacher Bericht in der Layoutansicht
Außerdem legt Access automatisch das Layout Tabelle für die Steuerelemente fest (das können Sie – wenn Sie die Steuerelemente von Hand in den Bericht gezogen haben – durch Markieren der benötigten Steuerelemente und Betätigen der Ribbon-Schaltfläche Anordnen|Layout bestimmen|Tabelle erreichen). Dieses Layout verschafft einige Vorteile:
364
Berichte
Sie brauchen nur noch ein Element einer Spalte zu markieren und können ihre Breite durch Ziehen des rechten Rands mit der Maus anpassen – und die rechts davon liegenden Steuerelemente verschieben sich direkt mit. In älteren Access-Versionen mussten Sie alle rechts davon liegenden Steuerelemente manuell verschieben. Sie sehen die enthaltenen Daten und können die Breite direkt daran anpassen, ohne wie früher zwischen Entwurfs- und Seitenansicht hin- und herwechseln zu müssen. Sie können die Reihenfolge der Spalten durch eine einfache Maus-Aktion einstellen. Um eine Spalte um eine Position nach rechts zu bewegen, ziehen Sie diese einfach so weit nach rechts, bis an der gewünschten Stelle ein vertikaler Balken angezeigt wird, und lassen die Spalte dann fallen (siehe Abbildung 6.3).
Abbildung 6.3: Ändern der Reihenfolge von Spalten in einem Bericht
Im Layout Gestapelt sieht dies ähnlich aus, nur dass die Felder eines Datensatzes hier in »Päckchenform« angezeigt werden – hier können Sie entsprechend die Höhe der Steuer elemente und ihre vertikale Reihenfolge ändern, ohne dass Sie die von der Änderung betroffenen Steuerelemente manuell anpassen müssen.
6.1.3 Einheitliches Design mit Autoformat Sie können genau wie in Formularen auch in Berichten für ein einheitliches Layout sorgen, indem Sie einen Bericht nach Wunsch gestalten und die vorhandenen Formatierungen in
365
Kapitel 6
Form eines Autoformats speichern. Weitere Informationen dazu finden Sie in Kapitel 5 in Abschnitt 5.13.2, »Autoformate«.
6.1.4 Wechselnde Hintergrundfarbe Im Ribbon-Bereich Format|Schriftart finden Sie zwei Schaltflächen namens Füll-/Hin tergrundfarbe und Alternative Füllung/Hintergrundfarbe. Damit können Sie ganz einfach wechselnde Hintergrundfarben für Berichte festlegen (siehe Abbildung 6.4). Weniger einfach ist es, die gewünschte Zeile zu markieren: Das funktioniert nämlich nur in der Layoutansicht, und dann auch nur, wenn die zu färbende Zeile komplett markiert ist. Das erreichen Sie am einfachsten, wenn Sie einen Mausklick direkt links neben die gestrichelte Linie auf Höhe der zu färbenden Zeile ausführen. Davon abgesehen können Sie für jeden Gruppierungsbereich eigene Hintergrundfarben festlegen.
Abbildung 6.4: Festlegen einer alternativen Hintergrundfarbe
6.1.5 Bedingte Formatierung Neben den wechselnden Hintergrundfarben können Sie Berichtssteuerelemente mit bedingten Formatierungen ausstatten. Dazu markieren Sie das passende Steuerelement,
366
Berichte
klicken Sie auf die Ribbon-Schaltfläche Format|Schriftart|Bedingte Formatierung und legen in dem nun erscheinenden Dialog die Bedingungen und die passenden Formatierungen fest. Wie in den vorherigen Access-Versionen können Sie nur drei verschiedene bedingte Formatierungen angeben. Informationen zum Anwenden bedingter Formatierungen finden Sie in der Onlinehilfe von Access 2007 im Artikel »Formatieren von Daten in Tabellen, Formularen und Berichten« (letzter Abschnitt).
6.1.6 Sonstige Layout-Vereinfachungen Die übrigen Funktionen, wie das Anpassen der Seitenränder, des Abstandes des Texts eines Steuerelements von den Steuerelementrändern, das Ausrichten von Steuerelementen, das Hinzufügen und Entfernen von Steuerelementen in beziehungsweise aus den beiden Layouts, sind intuitiv zu handhaben. Ein echtes Highlight ist dabei die Möglichkeit, einem Layout Gitternetzlinien hinzuzufügen – wer mit älteren Versionen von Access gearbeitet hat, weiß, welchen Spaß man mit vertikalen Linien in Berichten haben kann (Layoutansicht, Ribbon-Eintrag Format|Gitternetzlinien|Gitternetzlinien). Probieren Sie es einfach aus; bei Problemen liefert die Onlinehilfe ausreichend Hilfestellung. Wenn Sie einmal alle Elemente des Layouts markieren möchten, markieren Sie zunächst ein Element und klicken dann auf das links über dem Layout erscheinende Symbol.
6.1.7 Berichtsbereiche Berichte haben standardmäßig drei Bereiche, die Sie mit Steuerelementen füllen können: Der Detailbereich, der Seitenkopf und der Seitenfuß. Hinzu kommen, wenn Sie den Kontextmenüeintrag Berichtskopf/-fuß auswählen, zwei weitere Bereiche namens Berichtskopf und Berichtsfuß. Diese Bereiche haben die folgenden Funktionen: Detailbereich: Dieser Bereich wird für jeden Datensatz der Datensatzherkunft wiederholt. Hier fügen Sie die eigentlichen Daten ein. Seitenkopf und -fuß: Diese Bereiche werden im Kopf und im Fuß jeder Seite angezeigt. Sie können darin Angaben wie Seitenzahlen, Datum oder Seitensummen und -überträge anzeigen. Berichtskopf und -fuß: Diese Bereiche zeigt der Bericht am Anfang und am Ende an. Der Berichtskopf kann Informationen über den Bericht enthalten und gegebenenfalls auch auf eine komplette Seite ausgedehnt und als Deckblatt verwendet werden. Im Berichtsfuß bringen Sie etwa Gesamtsummen unter. Gruppenköpfe und -füße: Weitere Bereiche können Sie durch Gruppierungen erreichen. Wenn Sie etwa die Artikel der Nordwind-Datenbank nach Kategorien gruppieren und jeweils die Kategorie oberhalb einer Gruppe von Artikeln ausgeben möchten, ist eine Gruppierung genau das Richtige. Zu jeder Gruppierung können Sie einen Gruppenkopf
367
Kapitel 6
und einen Gruppenfuß einblenden, um beispielsweise Überschriften oder Gruppen summen einzufügen. Mehr zu Gruppierungen erfahren Sie weiter unten.
6.1.8 Berichtsansichten Seit Access 2007 gibt es vier für die Arbeit mit Berichten geeignete Ansichten: Die Seitenansicht zeigt den Bericht so, wie er auch ausgedruckt wird. Die Entwurfsansicht dient dem Entwerfen des Berichts. Hier können Sie etwa Steuerelemente hinzufügen und ausrichten, Gruppierungen und Sortierungen oder die Größe von Bereichen festlegen. Die Layoutansicht ist eine Mischung aus Seiten- und Entwurfsansicht. Sie liefert ein gutes Bild des beim Ausdruck zu erwartenden Aussehens des Berichts und bietet gleichzeitig die Möglichkeit, den Entwurf von Bericht und Steuerelementen anzupassen. Die Berichtsansicht sieht genauso aus wie die Seitenansicht, liefert aber erheblich mehr Möglichkeiten zur Interaktion mit dem Benutzer. Dazu unterstützt sie eine ganze Reihe von Ereigniseigenschaften, die Sie bisher nur von Formularen her kannten.
6.1.9 Gruppieren und sortieren Eine Gruppierung legen Sie in der Entwurfsansicht im Bereich Gruppieren, Sortieren und Summe an, den Sie mit dem Ribbon-Eintrag Entwurf|Gruppierung und Summen|Gruppieren und Sortieren anzeigen. In der Layoutansicht blenden Sie diesen Bereich mit dem RibbonEintrag Format|Gruppierung und Summen|Gruppieren und sortieren ein. Im Gegensatz zu früheren Access-Versionen ist dieser Bereich fest im unteren Teil des AccessFensters verankert und steht nicht mehr als eigenes Fenster zur Verfügung. Prinzipiell hat sich aber zumindest bezüglich der Gruppierung und Sortierung nicht viel geändert.
6.2 Berichte anzeigen Die Anzeige von Berichten erfolgt meist über die DoCmd.OpenReport-Methode. Die Methode hat folgende Parameter (hier nur mit den gebräuchlichsten Konstanten): ReportName: Name des anzuzeigenden Berichts View: Ansicht beim Öffnen (acViewPreview: Seitenansicht, acViewNormal: Drucken, acViewDesign: Entwurfsansicht, acViewLayout: Layoutansicht, acViewReport: Berichts ansicht)
368
Berichte
FilterName: Name einer Abfrage auf Basis der Datensatzquelle WhereCondition: Teil einer Abfrage hinter der WHERE-Klausel zum Einschränken der enthaltenen Daten WindowMode: Fenstereigenschaften (acDialog: modales Fenster, acWindowNormal: Standard, acIcon: minimiert, acHidden: nicht sichtbar) OpenArgs: Öffnungsargument, mit dem zusätzliche Informationen übergeben werden können (Datentyp Variant) Die Parameter können Sie entweder in der angegebenen Reihenfolge durch Kommata getrennt eingeben. Wenn Sie einen Parameter nicht benötigen, lassen Sie diesen einfach weg, ohne aber die Anzahl der Kommata zu verändern. Hinter dem letzten verwendeten Parameter brauchen Sie allerdings keine Kommata mehr anzuhängen. Alternativ können Sie mit »benannten Argumenten« arbeiten. Dabei geben Sie den Para meternamen gefolgt von Doppelpunkt und Gleichheitszeichen sowie dem Wert an (in einer Zeile): DoCmd.OpenReport "rptKalenderdaten", View:=acViewPreview, FilterName:="qryKalenderdaten"
Zwischen Parameter und Wert dürfen sich keine Leerzeichen befinden. Die Reihenfolge bei der Verwendung benannter Parameter ist beliebig.
BBesonderheit beim Öffnen von Berichten aus modal geöffneten Formularen Wenn ein Formular, von dem aus Sie etwa per Schaltfläche einen Bericht anzeigen möchten, mit dem Parameter WindowMode:=acDialog, also als modaler Dialog geöffnet wurde, müssen Sie auch den Bericht als modales Fenster öffnen. Anderenfalls wird der Bericht direkt im Hintergrund des Formulars angezeigt und Sie können nicht auf diesen zugreifen.
6.3 Filtern und sortieren Berichte enthalten sechs Eigenschaften, mit denen sich Filter- und Sortierkriterien einstellen lassen. Diese können auf unterschiedliche Weise festgelegt werden – beispielsweise auf Basis der zugrunde liegenden Datensatzquelle oder auch per Zuweisung der entsprechenden Eigenschaften mit VBA. Wenn Sie etwa eine Abfrage als Datensatzquelle festlegen, die eine Sortierung und ein Kriterium enthält, werden diese Angaben einfach in den Bericht übernommen, ohne dass Sie dafür eine Eigenschaft einstellen müssen. Wenn Sie beim Öffnen des Berichts eine Abfrage mit dem Parameter FilterName überge-
369
Kapitel 6
ben, die eine Sortierung und ein Kriterium enthält, wird das Kriterium dieser Abfrage in die Eigenschaft Filter übernommen, aber die Eigenschaft Beim Laden Filtern nicht gesetzt. Beim Sortieren funktioniert alles wie erwartet: Die Eigenschaften Sortiert nach und Beim Laden sortieren erhalten die gewünschten Werte (siehe Abbildung 6.5). Trotzdem wird nach dem angegebenen Artikelnamen gefiltert und sortiert: DoCmd.OpenReport "rptArtikelUebersicht", acPreview, "qryArtikel"
Abbildung 6.5: Einstellungen der Filter- und Sortiereigenschaften nach Zuweisen einer Abfrage mit dem Parameter »FilterName« der DoCmd.OpenReport-Methode
Die Eigenschaften in dieser Abbildung lassen sich nebenher auch noch manuell oder per VBA zur Laufzeit einstellen. Per VBA stehen sogar noch zwei weitere Eigenschaften zur Verfügung: FilterOn und SortByOn. Der Unterschied zwischen FilterOnLoad und FilterOn ist folgender: Enthält Filter einen Kriteriumsausdruck und hat FilterOnLoad den Wert True, dann wird der Filter bereits beim Laden des Formulars berücksichtigt. Das dürfte der Normalfall sein. Stellen Sie die Eigenschaft FilterOn zur Laufzeit, also bei geöffnetem Bericht, auf False ein, wird der angegebene Filter deaktiviert. Stellen Sie den Wert wieder auf True, filtert der Bericht die Daten wieder nach dem angegebenen Filterkriterium. Hat FilterOn den Wert True, können Sie auch das Filterkriterium ändern und die Auswirkungen direkt in der Seitenansicht betrachten. Im Gegensatz zu älteren Access-Versionen finden Sie die Eigenschaften FilterOn und OrderByOn nicht mehr im Eigenschaftsfenster (dafür gab es dort die Eigenschaften FilterOnLoad und OrderByOnLoad noch nicht). Die Eigenschaften FilterOn und OrderByOn werden übrigens beide auf den Wert Ja eingestellt, wenn man beim Öffnen des Berichts wie oben eine Abfrage mit Sortierung und Filterkriterien für den Parameter FilterName übergibt. Wenn Sie eine der gleich vorgestellten Gruppierungen oder Sortierungen im Bereich Gruppieren, Sortieren und Summe festlegen, steht die in der Eigenschaft Sortiert nach festgelegte Sortierung ganz hinten an. Wenn Sie in Sortiert nach etwa absteigend nach dem Artikelnamen sortieren und im Bereich Gruppieren, Sortieren und Summe eine aufsteigen-
370
Berichte
de Sortierung nach dem Artikelnamen festgelegt ist, werden die Datensätze aufsteigend nach dem Artikelnamen sortiert. Die Einstellung der Eigenschaft Sortiert nach macht sich erst bemerkbar, wenn im Bereich Gruppieren, Sortieren und Summe keine Sortierung für dieses Feld angegeben ist.
6.3.1 Filtern und Sortieren in der Berichtsvorschau Interessant werden die Eigenschaften Filter, Filter aktiv, Sortierung und Sortierung aktiv, wenn die diesbezüglichen Möglichkeiten im Bericht noch nicht durch sonstige Sortierungen und Gruppierungen erschöpft sind. Ein Beispiel dafür sind einfache Übersichtslisten wie der Bericht rptArtikelUebersicht (siehe Beispieldatenbank). Wenn Sie diesen in der Seitenansicht öffnen, können Sie zur Laufzeit per VBA die Sortierung und den Filter anpassen. Um die Datensätze beispielsweise in absteigender Reihenfolge nach dem Artikelnamen zu sortieren, verwenden Sie die folgende Anweisung: Reports!rptArtikelUebersicht.OrderBy = "Artikelname DESC"
Falls das nicht wirkt, ist die Sortierung noch nicht aktiv. Schieben Sie in diesem Fall noch folgende Anweisung hinterher: Reports!rptArtikelUebersicht.OrderByOn = True
Genauso können Sie auch den Filter einsetzen. Alle Artikel mit dem Anfangsbuchstaben A liefert die folgende Anweisung (wiederum bei aktivierter Seitenansicht): Reports!rptArtikelUebersicht.Filter = "Artikelname LIKE 'A*'"
Falls der Filter nicht greift, müssen Sie auch diesen anschalten: Reports!rptArtikelUebersicht.FilterOn = True
Das Ganze funktioniert seit Access 2007 auch über die Benutzeroberfläche: Sie können per Kontextmenü des gewünschten Feldes einen Filter festlegen.
6.3.2 Filtern, Sortieren und Gruppieren in der Layoutansicht Mit der Layoutansicht erhalten Sie noch viel mächtigere Möglichkeiten, die im Bericht angezeigten Daten zur Laufzeit zu filtern und zu sortieren. Abbildung 6.6 zeigt, wie das aussehen kann: Wenn Sie etwa die Daten in tabellarischer Form ausgeben, können Sie in der Layoutansicht einfach mit der rechten Maustaste auf ein Feld klicken und aus dem Kontextmenü eine ganze Reihe zusätzlicher Funktionen auswählen.
371
Kapitel 6
Abbildung 6.6: Das Kontextmenü eines Feldes in der Layoutansicht liefert Sortier- und Filterfunktionen
6.3.3 Filtern in der Layoutansicht Wenn Sie ein Feld nach bestimmten Kriterien filtern möchten, klicken Sie im Kontextmenü des Feldes entweder einen der Schnellfilter an, die unterschiedliche Vergleichsoperatoren auf den Inhalt des aktuellen Feldes anwenden, oder wählen den für den aktuellen Datentyp angezeigten Eintrag wie Textfilter, Zahlenfilter oder Datums filter und einen der für dieses Feld gültigen Vergleichsoperatoren aus. Diese variieren je nach dem Felddatentyp – für Datumsangaben bietet Access etwa direkt eine ganze Latte von Vergleichsmöglichkeiten. In einem weiteren Dialog geben Sie den Vergleichswert ein; Access filtert den Bericht dann nach den angegebenen Kriterien. Diese Funktion ist intuitiv zu bedienen, probieren Sie es einfach aus.
6.3.4 Sortieren in der Layoutansicht Mit den Kontextmenüeinträgen Von A bis Z sortieren oder Von Z bis A sortieren legen Sie die Sortierung für das aktuell ausgewählte Feld fest. Dies funktioniert leider nur für das aktuell markierte Feld. Wenn Sie nach mehreren Feldern sortieren wollen, müssen
372
Berichte
Sie entsprechende Sortierkriterien im Bereich Gruppieren, Sortieren und Summe festlegen (Ribbon-Eintrag Format|Gruppierung und Summen|Gruppieren und sortieren). Das erledigen Sie mit einem Klick auf die Schaltfläche Sortierung hinzufügen. Die Reihenfolge der Sortierung können Sie dann mit den Pfeil-Schaltflächen am rechten Rand des Bereichs festlegen. Die Daten werden zuerst nach dem zuoberst angegebenen Kriterium sortiert.
6.3.5 Gruppieren in der Layoutansicht Sie können beispielsweise nach einem Feld gruppieren. Im vorliegenden Beispiel macht das etwa für Lieferanten Sinn: Klicken Sie mit der rechten Maustaste auf einen der Einträge in der Spalte Lieferanten und wählen Sie den Kontextmenüeintrag Nach Lieferant gruppieren aus. Das Ergebnis sieht wie in Abbildung 6.7 aus: Access zieht die Spalte mit den Lieferanten heraus und fügt sie in einen Gruppenkopf ein, die passenden Artikel werden darunter aufgelistet. Was da passiert ist, lässt sich prima in der Entwurfsansicht nachvollziehen (siehe Abbildung 6.8): Access hat tatsächlich eine neue Gruppierung erstellt, die Sie noch nach Ihren Wünschen anpassen können – einfacher geht es kaum.
Abbildung 6.7: Die Gruppierung nach Lieferanten ist nicht schön, aber schnell gemacht ...
In der Layoutansicht wie auch in der Berichtsvorschau können Sie einen in Access 2007 neu designten Bereich zum Bearbeiten der Gruppierungs- und Sortierungsebenen anzeigen lassen (Ribbon-Eintrag Format|Gruppierung und Summen|Gruppieren und sortieren). Diese hat leider immer die in Abbildung 6.9 angezeigte Höhe und kann nur in der Breite verändert werden – das ist ein großer Wermutstropfen in Anbetracht der vielen Verbesserung der Berichtsansichten. Gegenüber dem alten Dialog zum Einstellen die-
373
Kapitel 6
ser Eigenschaften hat sich dennoch einiges verbessert: Sie können Gruppierungen und Sortierungen mit einem Klick löschen oder ihre Priorität anpassen.
Abbildung 6.8: ... und kann vor allem als Basis für eine dauerhafte Gruppierung verwendet werden, wie die Entwurfsansicht zeigt
Abbildung 6.9: Die Gruppierungs- und Sortierungseigenschaften lassen sich leichter bearbeiten, allerdings leidet die Übersicht durch die fixe Größe des passenden Bereichs
374
Berichte
Damit Einsteiger nicht mehr mit den verwirrenden Begriffen »aufsteigend« und »absteigend« durcheinanderkommen, lauten die Sortieroptionen nun »vom kleinsten zum größten« und »vom größten zum kleinsten« (siehe Abbildung 6.10). Gruppenkopf und Gruppenfuß heißen nun »Kopfzeilenbereich« und »Fußzeilenbereich«
Abbildung 6.10: Eine Gruppierung mit allen Details
6.3.6 Summen in der Layoutansicht Wenn Sie einem Berichtsfeld in der Layoutansicht eine Summe hinzufügen wollen, wählen Sie aus dem Kontextmenü des passenden Felds den Eintrag Summe aus. Dies liefert genau genommen nicht direkt ein Summenfeld, sondern weitere Menüein träge, mit denen Sie weitere Aggregatberechnungen durchführen können (siehe Abbil dung 6.11). Abbildung 6.12 zeigt das neue Berechnungsfeld im Berichtsfuß.
Abbildung 6.11: Berechnungsfunktionen für einzelne Felder eines Berichts
375
Kapitel 6
Abbildung 6.12: Berechnungsfelder wie diese lassen sich mit Access 2007 per Kontextmenü anlegen
6.4 Berichtsbereiche und Ereignisse Von elementarer Bedeutung für die Arbeit mit Berichten – vor allem mit VBA – ist das Verständnis der einzelnen Bereiche eines Berichts und der Ereignisse dieser Be richtsbereiche. Daher finden Sie zunächst noch einmal eine Zusammenfassung der Informationen über die einzelnen Berichtsbereiche, bevor es an die Beschreibung der Ereignisse dieser Bereiche geht.
6.4.1 Berichtsbereiche Standardmäßig zeigt ein Bericht einen Detailbereich sowie einen Seitenkopf und einen Seitenfuß an. Optional lassen sich noch ein Berichtskopf- und ein Berichtsfuß anzeigen. Letztere werden nur je einmal eingeblendet – am Anfang und am Ende des Berichts – und können beispielsweise für die Gestaltung einer Titelseite verwendet werden. Seitenkopf und -fuß erscheinen am oberen und unteren Ende jeder Seite und legen somit fest, wie viel Platz noch für den Detailbereich und andere Bereiche übrig bleibt. Alle Bereiche lassen sich jedoch nach Bedarf ein- und ausblenden. Dazu später mehr. Eines der wichtigsten Elemente zur Strukturierung von Daten – und hier speziell von verknüpften Daten – liegt in der Möglichkeit, Daten nach bestimmten Kriterien zu gruppieren und zu sortieren. Sortierungen und Gruppierungen legen Sie in dem bereits weiter oben erwähnten Bereich fest. Mit diesem fügen Sie Sortierungen und Gruppierungen zu einem Bericht
376
Berichte
hinzu. Im Gegensatz zu älteren Access-Versionen, in denen Sie zunächst Felder in den Dialog gezogen und dann mit den Eigenschaften Gruppenkopf und Gruppenfuß festgelegt haben, ob es sich nur um eine Sortierung oder eine richtige Gruppierung mit Kopf- oder Fußbereich handelte, scheint es in Access 2007 so zu sein, dass Sie von vornherein festlegen müssen, ob Sie eine Sortierung oder eine Gruppierung anlegen möchten (wobei eine Gruppierung natürlich auch eine Sortierung beinhalten kann). Dazu klicken Sie auf eine der Schaltflächen Gruppe hinzufügen oder Sortierung hinzufügen (siehe Abbildung 6.13) oder ziehen einfach ein Feld aus der Feldliste auf den Gruppierungs-Bereich und stellen dann in der erweiterten Ansicht aus Abbildung 6.14 das zu gruppierende Feld und die übrigen Eigenschaften ein. In der Tat können Sie aber auch eine Sortierung noch zu einer Gruppierung umfunktionieren, indem Sie einfach einen Kopf- oder Fußzeilenbereich hinzufügen. Die Einstellungen aus dieser Abbildung sorgen übrigens dafür, dass die zugrunde lie genden Artikel zunächst nach Kategorien und dann innerhalb der Kategorien nach Lie feranten gruppiert werden. Die Daten jeder Lieferanten-Gruppe werden dann noch einer aufsteigenden Sortierung unterzogen.
Abbildung 6.13: Dialog zum Festlegen von Sortierungen und Gruppierungen
Abbildung 6.14: Festlegen der Bereiche einer Gruppierung
In Abschnitt 6.6, »Wichtige Eigenschaften von Berichten und Berichtsbereichen« lernen Sie die übrigen Einstellungen des Gruppieren, Sortieren und Summe-Bereichs kennen.
6.4.2 Ereignisse in Berichten Berichte bieten seit Access 2007 erheblich mehr Ereignisse für Berichte als in den Vorgän gerversionen – und fast so viele wie für Formulare. Die Möglichkeiten der Interaktion
377
Kapitel 6
sind um einiges besser geworden; so können Sie etwa auf einen Datensatz in einem Bericht klicken, um ein Formular mit den Details zum Bearbeiten zu öffnen. Diese Ereignisse beziehen sich aber alle auf eine weitere neue Berichtsansicht, nämlich die »Berichtsansicht«. Mehr zu dieser neuen interaktiven Ansicht finden Sie weiter unten. Bis ein Bericht überhaupt einmal angezeigt ist, geschieht allerdings schon eine Menge und in vielen Fällen können Sie den Ablauf per Ereignisprozedur beeinflussen. Nachfol gend finden Sie zunächst eine Auflistung der Ereignisse eines Berichts und der Bereiche eines Berichts; im Anschluss lernen Sie einige Anwendungsfälle der Ereignisse kennen.
6.4.3 Zusammenfassung der Berichtsereignisse Der Bericht löst folgende Ereignisse aus, die in den bisherigen Access-Versionen ebenfalls unterstützt wurden und die für die reine Anzeige eines Berichts wichtig sind: Beim Öffnen: Wird beim Öffnen, aber vor dem Erstellen des Berichts ausgelöst. Dient beispielsweise der Übergabe von Parametern oder zum Abbrechen der Ausgabe des Berichts. Bei Aktivierung: Bericht wird aktiviert oder gedruckt. Bei Seite: Wird nach Abschluss der Bei Formatierung- und der Beim Drucken-Ereignisse der Berichtsbereiche, aber vor dem Anzeigen der Seite ausgelöst. Beim Schließen: Wird beim Schließen des Berichts ausgelöst – etwa durch Betätigen der Schließen-Schaltfläche. Bei Deaktivierung: Bericht wird deaktiviert, da ein anderes Objekt den Fokus erhält oder der Bericht geschlossen wird. Bei Ohne Daten: Wird ausgelöst, wenn die Datensatzquelle des Berichts keine Daten enthält. Bei Fehler: Wird beim Auftreten eines Fehlers ausgelöst.
6.4.4 Zusammenfassung der Bereichsereignisse Die Bereiche Berichtskopf, Berichtsfuß, Seitenkopf, Seitenfuß, Kopf und Fuß der einzelnen Gruppierungen und der Detailbereiche lösen die folgenden Ereignisse aus. Dabei werden die Ereignisse Beim Formatieren und Beim Drucken je Element eines jeden Bereichs mindestens einmal aufgerufen: Beim Formatieren: Wird ausgelöst, wenn die Daten ermittelt sind, bevor der Bereich formatiert wird. Dieses Ereignis wird gegebenenfalls mehrere Male ausgelöst. Wenn es sich beim aktuell formatierten Bereich um einen Gruppenkopf handelt, haben Sie Zugriff auf die im Gruppenkopf angezeigten Daten und auf die Daten des ersten
378
Berichte
Datensatzes der Gruppierung im Detailbereich. Beim Gruppenfuß stehen die Daten des Gruppenfußbereichs und des letzten Datensatzes des Bereichs zur Verfügung. Im Detailbereich bietet dieses Ereignis Zugriff auf die Daten des aktuellen Datensatzes. Beim Drucken: Wird ausgelöst, wenn der Bereich formatiert, aber noch nicht gedruckt ist. In diesem Ereignis können Sie je Bereich auf die gleichen Daten zugreifen wie im Beim Formatieren-Ereignis. Mit »Drucken« ist hier nicht die Ausgabe auf Papier gemeint, sondern die grafische Ausgabe in das »Dokument«, das dann auf dem Bildschirm oder auf Papier dargestellt werden kann. Bei Rückname: Wird jedes Mal ausgelöst, wenn Access einen Bereich infolge Posi tionierungsberechnungen neu formatieren muss (steht nicht für den Seitenkopf zur Verfügung). Die genaue Abfolge der Ereignisse eines Berichts ist relativ komplex. Mehr Gruppierun gen oder spezielle Bedingungen wie etwa das Zusammenhalten von Gruppierungen führen zu einer unüberschaubaren Menge von Ereignissen. Allein der Aufruf eines Berichts mit einer Gruppierung, der nur einen Datensatz anzeigt, löst die folgenden Ereignisse in der angegebenen Reihenfolge aus: Bericht Beim Öffnen Bericht Bei Aktivierung Seitenkopfbereich Beim Formatieren Gruppenfuß0 Beim Formatieren Detailbereich Beim Formatieren Gruppenfuß0 Beim Formatieren Seitenfußbereich Beim Formatieren Seitenkopfbereich Beim Formatieren Seitenkopfbereich Beim Drucken Gruppenfuß0 Beim Formatieren Gruppenfuß0 Beim Drucken Detailbereich Beim Formatieren Detailbereich Beim Drucken Gruppenfuß0 Beim Formatieren Gruppenfuß0 Beim Drucken Seitenfußbereich Beim Formatieren
379
Kapitel 6
Seitenfußbereich Beim Drucken Bericht Bei Seite Bericht Beim Schließen Bericht Bei Deaktivierung
6.4.5 Zugriff auf die Berichtsbereiche Der Zugriff auf die Steuerelemente eines Berichts erfolgt genau wie in Formularen. Interessanter sind da die Elemente, die in Formularen nicht vorhanden sind – die einzelnen Bereiche der Berichte. Warum muss man eigentlich wissen, wie man per VBA auf diese Bereiche zugreift? Weil es Situationen gibt, in denen Sie beispielsweise einen Bereich ein- oder ausblenden oder die Eigenschaften eines Bereichs anpassen müssen. Auf einen Berichtsbereich greifen Sie über die Auflistung Section zu. Als Argument geben Sie entweder den Namen, eine Zahl oder – bei den eingebauten Bereichen – eine VBA-Konstante an. Auf den Detailbereich können Sie beispielsweise mit folgenden Anweisungen zugreifen: Debug.Print Debug.Print Debug.Print Debug.print
Alle vier Anweisungen geben in der deutschen Version von Access 2007 den Namen »Detailbereich« aus. Dies ist der voreingestellte Name für den Detailbereich. Sie können den Namen der eingebauten Bereiche wie auch der zusätzlichen Gruppierungen leicht im Eigenschaftsfenster des jeweiligen Bereichs anpassen (siehe Abbildung 6.15). Damit wird deutlich, dass Access für jeden Bereich dynamisch eine Berichts-Eigenschaft mit dem Namen des jeweiligen Bereichs bereitstellt.
Abbildung 6.15: Anpassen des Namens eines Berichtsbereichs
380
Berichte
Die Namen, Zahlenwerte und Konstanten der einzelnen Berichtsbereiche finden Sie in Tabelle 6.1: Die Kopf- und Fußbereiche der bis zu zehn Gruppierungsebenen werden von 5 bis 24 durchnummeriert, wobei die Kopfbereiche jeweils ungerade und die Fußbereiche gerade Zahlen erhalten. Access legt automatisch Namen für die Bereiche an, die jeweils mit Gruppenkopf und Gruppenfuß beginnen und genau wie die anderen Steuerelemente eine eigene Nummer erhalten, die nicht zwangsläufig fortlaufend sein muss. Name
Zahl
Konstante
Detailbereich
0
acDetail
Berichtskopf
1
acHeader
Berichtsfuß
2
acFooter
Seitenkopfbereich
3
acPageHeader
Seitenfußbereich
4
acPageFooter
Tabelle 6.1: Name, Zahlenwert und Konstanten von Berichtsbereichen
6.5 Beispiele für den Einsatz der Berichts- und Bereichsereignisse in der Seitenansicht Die Berichtsereignisse lassen sich teilweise für recht spezielle Aktionen einsetzen. In den folgenden Abschnitten finden Sie einige Beispiele, damit Sie ein Gefühl für das richtige Einsetzen der Berichtsereignisse erhalten.
6.5.1 Beim Öffnen: Auswertung von Öffnungsargumenten Der richtige Zeitpunkt zum Auswerten eines Öffnungsarguments ist das Ereignis Beim Öffnen. Mit dem Öffnungsargument der DoCmd.OpenReport-Anweisung lässt sich beispielsweise ein Parameter zum Filtern der Datensatzquelle übergeben. Da dies aber relativ langweilig ist, finden Sie nachfolgend ein Beispiel, wie Sie die Gruppierung eines Berichts beim Öffnen verändern können. Voraussetzung ist der Bericht rptGruppierungenTauschen aus Abbildung 6.16. Der Clou an diesem Bericht ist, dass Sie mit wenigen Zeilen Code die Gruppierungsebenen vertauschen können. Dazu sind folgende, in der Abbildung nicht sichtbare Eigenschaften einzustellen: Name des Beschriftungsfeldes im Berichtskopf: lblUeberschrift Name des LieferantID-Kopfbereichs: Gruppenkopf0 Name des Beschriftungsfeldes im LieferantID-Kopfbereich: lblUeberschrift Gruppie rung0
381
Kapitel 6
Name des Textfeldes im LieferantID-Kopfbereich: txtUeberschriftGruppierung0 Name des KategorieID-Kopfbereichs: Gruppenkopf1 Name des Beschriftungsfeldes im KategorieID-Kopfbereich: lblUeberschrift Gruppie rung0 Name des Textfeldes im KategorieID-Kopfbereich: txtUeberschriftGruppierung1
Abbildung 6.16: Bericht mit zwei Gruppierungsebenen
Wie bringen Sie nun Dynamik ins Spiel? Betrachten Sie den reinen Ablauf, so rufen Sie den Bericht auf und übergeben mit dem Öffnungsargument Informationen über die im Bericht anzuzeigenden Daten. Das sieht etwa folgendermaßen aus (in einer Zeile): DoCmd.OpenReport "rptGruppierungenTauschen", View:=acViewPreview, OpenArgs:="Artikel nach Kategorien und Lieferanten;KategorieID;LieferantID; Kategorie;Lieferant"
Das Öffnungsargument enthält sogar mehrere Argumente, die durch Semikola voneinander getrennt sind. Das erste enthält den Text, der im Berichtskopf als Überschrift angezeigt werden soll, das zweite und dritte enthalten die Beschriftungen der Bezeichnungsfelder in den beiden Gruppenköpfen und das vierte und fünfte die Felder der Datensatzquelle, nach denen gruppiert werden soll. Fehlt noch eine Routine, die diese Informationen auseinandernimmt und den entsprechenden Eigenschaften zuweist. Diese wird – wer hätte es gedacht – durch das Beim Öffnen-Ereignis des Berichts ausgelöst:
382
Berichte Private Sub Report_Open(Cancel As Integer) Dim strOpenArgs As String Dim strGruppierungen() As String Dim i As Integer If IsNull(Me.OpenArgs) Then Exit Sub End If strGruppierungen() = Split(Me.OpenArgs, ";") Me!lblUeberschrift.Caption = strGruppierungen(0) For i = 1 To 2 Me("lblUeberschriftGruppierung" & i - 1).Caption = _ strGruppierungen(i + 2) Me.GroupLevel(i - 1).ControlSource = strGruppierungen(i) Next i End Sub Listing 6.1: Diese Öffnungsargument
Prozedur
sortiert
die
Gruppierungen
nach
den
Vorgaben
im
Die Prozedur Report_Open zerlegt zunächst die im Öffnungsargument übergebene Liste mit der Split-Funktion und speichert die einzelnen Elemente in einem Array namens strGruppierungen. Warum so umständlich und gleich fünf Parameter mit OpenArgs übergeben? Die Rei henfolge der Gruppierungen wird doch in einem Formular festgelegt und da könnte man beim Öffnen des Berichts die Einstellungen des Formulars auslesen und entsprechend reagieren. Dagegen spricht allerdings ein Grundsatz, den Sie bereits in Ka pitel 4, Abschnitt 4.6, »Von Formular zu Formular«, kennen gelernt haben: Je weniger Abhängigkeiten zwischen zwei Objekten bestehen, desto besser. Und weniger Abhän gigkeit, als alle notwendigen Informationen beim Öffnen zu übergeben, ist fast nicht möglich. Die folgende For Next-Schleife wird genau zweimal durchlaufen – für jede Gruppie rung einmal. Die erste Anweisung weist zunächst dem Beschriftungsfeld der äußeren Gruppierung die erste Gruppierungsbezeichnung zu – in diesem Fall »Kategorie«. Die zweite Anweisung legt das Feld fest, nach der die äußere Gruppierung erfolgen soll – hier »KategorieID«. Das gleiche Spiel wiederholt die Schleife noch für die innere Gruppierung – fertig! Mit dem obigen DoCmd.OpenReport-Befehl erhalten Sie eine nach Kategorien und Lieferanten gruppierte Artikelliste. Lediglich die Angabe der aktuellen Kategorie beziehungsweise des aktuellen Lieferanten im Kopfbereich der Gruppierungen fehlt noch. Dazu verwenden Sie die folgenden Prozeduren und erhalten gleichzeitig ein gutes Anwendungsbeispiel für den Einsatz des Ereignisses Beim Formatieren:
383
Kapitel 6 Private Sub Gruppenkopf1_Format(Cancel As Integer, FormatCount As Integer) Me!txtUeberschriftGruppierung1 = _ Me(Me.GroupLevel(1).ControlSource).Text End Sub Private Sub Gruppenkopf0_Format(Cancel As Integer, FormatCount As Integer) Me!txtUeberschriftGruppierung0 = _ Me(Me.GroupLevel(0).ControlSource).Text End Sub Listing 6.2: Einstellen der Überschriften in den Gruppierungsköpfen
Wie oben erwähnt, können Sie von der Beim Formatieren-Ereignisprozedur aus auf den Inhalt des ersten Detaildatensatzes der Gruppierung zugreifen, was hier sehr nützlich ist: Auf diese Weise lässt sich leicht der angezeigte Text der Felder KategorieID und Liefe rantID auslesen. Wenn Sie die Priorität der Gruppierung ändern möchten, verwenden Sie einfach die folgende Anweisung zum Anzeigen des Berichts: DoCmd.OpenReport "rptGruppierungenTauschen", View:=acViewPreview, OpenArgs:="Artikel nach Lieferanten und Kategorien;LieferantID;KategorieID; Lieferant;Kategorie"
Wenn Sie den Bericht mit geänderter Gruppierung aufrufen möchten, müssen Sie ihn zuvor schließen. Ein Wechsel in die Entwurfsansicht reicht nicht aus. Den Aufruf können Sie übrigens in einem Formular wie dem in Abbildung 6.17 unterbringen. Weitere Details finden Sie in der Beispieldatenbank zu diesem Kapitel (\Kap_06 \Berichte.accdb).
Abbildung 6.17: Formular zum Anzeigen von Berichten mit flexibler Gruppierungsreihenfolge (»frmGruppierungenTauschen«)
6.5.2 Bei Aktivierung und Bei Deaktivierung: Berichtsabhängige Funktionen ein- und ausschalten Wenn Sie einen Bericht geöffnet haben und diesen dann schließen oder den Fokus auf ein anderes Objekt setzen, möchten Sie möglicherweise Elemente des Ribbons aktivie-
384
Berichte
ren oder deaktivieren. Eine Drucken-Schaltfläche macht beispielsweise am meisten Sinn, wenn gerade ein Bericht angezeigt wird.
6.5.3 Bei Ohne Daten: Öffnen leerer Berichte vermeiden Das Ereignis Bei Ohne Daten wird ausgelöst, wenn die Anzahl Datensätze in der Daten satzquelle des Berichts 0 ist. Damit ist die entsprechende Ereignisprozedur prädestiniert, Aufrufe der Seiten- und der Berichtsansicht zu unterbinden, wenn gar keine Daten vorhanden sind. Die Benutzer werden es Ihnen danken, wenn nicht hin und wieder Ausdrucke von eigentlich leeren Berichten vorkommen. Zum Simulieren eines leeren Berichts setzen Sie einfach eine DoCmd.OpenReport-Anweisung mit einem entsprechenden Kriterium ein: DoCmd.OpenReport "rptArtikel", View:=acViewPreview, WhereCondition:="1=0"
Der Aufruf führt zur Anzeige eines bis auf die Bezeichnungsfelder leeren Berichts. Das ist noch zu verschmerzen. Unangenehmer wird es, wenn diese »Blanko«-Variante des Berichts unnötig ausgedruckt wird. Den Druck eines leeren Berichts und die Anzeige einer leeren Vorschau können Sie abfangen, indem Sie eine Prozedur für das Bei Ohne Daten-Ereignis anlegen. Diese soll eine Meldung anzeigen und die Ausgabe abbrechen. Private Sub Report_NoData(Cancel As Integer) MsgBox "Der Bericht enthält keine Daten.", _ vbExclamation Or vbOKOnly, "Keine Daten" Cancel = True End Sub Listing 6.3: Unterbinden der Ausgabe eines leeren Berichts
Leider liefert ein abgebrochener Aufruf der DoCmd.OpenReport-Methode einen Laufzeit fehler. Diesen müssen Sie auch noch behandeln, indem Sie den Aufruf in eine geeignete Fehlerbehandlung einbetten.
6.5.4 Bei Fehler: Fehler abfangen Das Ereignis Bei Fehler hat genau die gleiche Funktion wie bei Formularen. Damit lassen sich Laufzeitfehler behandeln, die nicht durch den VBA-Code im Klassenmodul des Berichts entstehen. Für weitere Informationen siehe Kapitel 13, Abschnitt 13.5, »Fehlerbehandlung in Formularen«.
6.5.5 Bei Seite: Seiten verschönern Wenn das Ereignis Bei Seite eintritt, ist der Großteil der Arbeiten bereits erledigt: Die Seite ist mit Inhalten gefüllt und fertig formatiert. Manchmal bereiten gewisse Feinheiten
385
Kapitel 6
allerdings Probleme, beispielsweise Linien zum Trennen der einzelnen Bereiche oder – und hier wird es spannend – zum Trennen einzelner Spalten beziehungsweise Setzen von vertikalen Rändern. Wenn manch einer wüsste, wie leicht sich das mit VBA erledigen lässt, würde er die »Pixelpopelei« schnell sein lassen … Sie möchten jetzt vielleicht einwerfen, dass man ja seit Access 2007 wie oben angeführt Gitternetzlinien für den Bericht definieren kann. Das funktioniert aber nur, wenn Sie eines der beiden Layouts Tabelle oder Gestapelt verwenden. Wenn Sie in der Layoutansicht einfach nur eines oder mehrere Felder markieren, um Gitternetzlinien zu applizieren, erleben Sie eine Enttäuschung: Der passende Ribbon-Eintrag ist schlicht und einfach deaktiviert. Da Sie aber nun sicher nicht alle Berichte mit den beiden möglichen Layouts versehen werden, müssen Sie hier und da noch von Hand Linien zeichnen – vermutlich mehr, als Ihnen lieb ist. VBA bietet mit der Line-Methode eine Anweisung zum genauen Anlegen von Rahmen und Trennlinien. In den folgenden Abschnitten finden Sie im Schnelldurchlauf einige interessante Beispiele für die Verwendung der Line-Methode, die Sie allesamt in der Ereignisprozedur Report_Page unterbringen können. Die folgenden Elemente der LineAnweisung sind für die nachfolgenden Beispiele ausreichend: Objekt.Line (x1, y1)-(x2, y2)[, [Farbe], [B[F]]]
Für Objekt geben Sie eine Referenz auf den Bericht an, in dem die Linie oder das Rechteck gezeichnet werden soll. Die Koordinaten x1, y1, x2, y2 geben die Position der Eckpunkte der Linie beziehungsweise des Rechtecks an. Der optionale Parameter Farbe enthält einen Wert für die Farbe des zu zeichnenden Elements. Standardmäßig wird hier die Farbe Schwarz verwendet. Verwenden Sie als letzten Parameter das B, wird ein Rahmen gezeichnet, BF liefert einen ausgefüllten Rahmen und wenn Sie den letzten Parameter weglassen, zeichnet Access statt eines Rahmens eine Linie zwischen den beiden angegebenen Punkten.
Seite einrahmen Einen Rahmen um die komplette Seite legen Sie mit folgender Prozedur an: Private Sub Report_Page() Me.ScaleMode = 6 'Millimeter Me.Line (Me.ScaleLeft, Me.ScaleTop)-(Me.ScaleWidth - 0.8, _ Me.ScaleHeight - 0.8), &H0, B End Sub Listing 6.4: Kompletten Bericht einrahmen
Möglicherweise wundern Sie sich wegen des »Abschlags« von 0.8 mm bei den Koordinaten des zweiten Punktes. Die Linien haben eine bestimmte Dicke, um die der
386
Berichte
eingerahmte Bereich vergrößert wird. Dadurch rutschen mitunter die rechte und die untere Linie aus dem Bericht heraus. Daher müssen Sie den rechten unteren Punkt ein wenig nach innen beziehungsweise nach oben rücken. Berücksichtigen Sie dies auch beim Anlegen der Steuerelemente. Die verwendete Einheit legt man mit der Eigenschaft ScaleMode fest. Der Wert 6 steht für Millimeter, 7 für Zentimeter.
6.5.6 Beim Formatieren: Layout anpassen Da das Ereignis Beim Formatieren gegebenenfalls mehr als einmal aufgerufen wird, sollten Sie in der entsprechenden Ereignisprozedur nur Code unterbringen, der tatsächlich bei jedem Aufruf dieses Ereignisses ausgeführt werden muss. Um nicht unnötig Rechenkapazität zu vergeuden, können Sie im Beim FormatierenEreignis mit dem Parameter FormatCount prüfen, das wievielte Mal die entsprechende Prozedur aufgerufen wird. Damit Anweisungen nur beim ersten Durchlauf ausgeführt werden, fassen Sie diese etwa in folgendes If Then-Konstrukt ein: If FormatCount = 1 Then 'Code beim Formatieren End If
Während Sie theoretisch viele Aktionen wie etwa das Sichtbar/unsichtbar-Machen von Steuerelementen, Einstellen von Hintergrundfarben oder Ähnliches sowohl im Ereignis Beim Formatieren als auch im Ereignis Beim Drucken durchführen können, sind Aktionen, die das Layout und insbesondere die Größe von Steuerelementen betreffen, nur im Beim Formatieren-Ereignis möglich.
Höhe von Steuerelementen einstellen Wenn Sie beispielsweise die Höhe von Steuerelementen oder Bereichen einstellen möchten, verwenden Sie die Beim Formatieren-Eigenschaft des jeweiligen Bereichs. Wenn Sie etwa einen Wochenkalender ausdrucken möchten, sollen vermutlich Samstage und Sonntage etwas weniger Platz einnehmen (außer Sie sind selbstständig und/oder Autor). Ein Kalender wie in Abbildung 6.18 benötigt neben einer Tabelle mit allen Tagen des Jahres (in der Beispieldatenbank unter tblKalenderdaten zu finden) noch die folgende Prozedur. Diese prüft, ob es sich beim aktuellen Datum um einen Samstag oder Sonntag handelt, und passt davon abhängig die Höhe des Steuerelements und des Detailbereichs an. Letzterer ist ein wenig größer, um einen schicken Zwischenraum zwischen den einzelnen Kalendertagen einzuschieben: Private Sub Detailbereich_Format(Cancel As Integer, FormatCount As Integer) If Weekday(Me!Kalenderdatum) = 7 Or Weekday(Me!Kalenderdatum) = 1 Then
387
Kapitel 6 Me!txtKalenderdatum.Height = 1100 Me.Detailbereich.Height = 1150 Else Me!txtKalenderdatum.Height = 2100 Me.Detailbereich.Height = 2150 End If End Sub Listing 6.5: Unterschiedliche Höhe für Werktage und Wochenenden
Abbildung 6.18: Kalender mit unterschiedlich hohen Detailbereichen
6.5.7 Beim Drucken Das Beim Drucken-Ereignis wird für jeden Bereich mindestens einmal, und zwar nach dem Beim Formatieren-Ereignis, ausgelöst. Es kann also auf fertig formatierte Elemente zugreifen, was sich die folgenden Beispielprozeduren zu Nutze machen.
Tabelle mit Gitternetzlinien Wenn Sie eine Tabelle im Excel-Stil mit Gitternetzlinien drucken möchten, müssen Sie nicht jedes Strichlein einzeln ziehen, sondern können sich der bereits weiter oben vor-
388
Berichte
gestellten Line-Methode bedienen (nochmal zur Erinnerung: Sie können auch eines der in Abschnitt 6.1.2, »Vereinfachtes Layouten« vorgestellten Layouts verwenden und dieses komfortabel mit einem Gitternetz versehen. Wenn Sie aber was Individuelles brauchen ...). Bauen Sie den Detailbereich des zu bearbeitenden Berichts wie in Abbildung 6.19 auf. Es ist empfehlenswert, zwischen oberem und unterem Rand sowie zwischen den Steuerelementen ein Pixel Platz zu lassen.
Abbildung 6.19: Dieser Bericht soll hinter Gitter
Die folgende Ereignisprozedur durchläuft in der For Each-Schleife alle Steuerelemente des Detailbereichs mit Ausnahme des letzten Steuerelements. Es legt rechts neben jedem Steuerelement eine Linie an, die am rechten oberen Eckpunkt des Steuerelements beginnt und am linken unteren Eckpunkt endet. Damit sind bereits Trennstriche zwischen den einzelnen Steuerelementen vorhanden. Mit der folgenden Line-Anweisung zieht die Routine einen Rahmen um den kompletten Detailbereich. Das Ergebnis sieht schließlich wie in Abbildung 6.20 aus. Private Sub Detailbereich_Print(Cancel As Integer, PrintCount As Integer) Dim ctl As Control For Each ctl In Me.Section(acDetail).Controls If Not ctl.Name = "Auslaufartikel" Then With ctl Me.Line (.Left + .Width, 0)-(.Left + .Width, Me.Height),0,B End With End If Next With Me Me.Line (0, 0)-(.Width, .Height), 0, B End With End Sub Listing 6.6: Bericht mit Gitternetzlinien versehen
389
Kapitel 6
Abbildung 6.20: Bericht mit Gitternetzlinien
Datensätze durchstreichen Wenn Sie schon gerade Linien zeichnen: Wie wäre es, nicht mehr verfügbare Artikel einmal als durchgestrichene Datensätze im Bericht anzuzeigen (siehe Abbildung 6.21)? Verwenden Sie einfach folgende Prozedur, die ebenfalls durch das Ereignis Beim Drucken des Detailbereichs ausgelöst wird: Private Sub Detailbereich_Print(Cancel As Integer, PrintCount As Integer) If Me!Auslaufartikel And Lagerbestand = 0 Then Me.Line (0, Me.Height / 2)-(Me.Width, Me.Height / 2) End If End Sub Listing 6.7: Durchstreichen von ausgegangenen Auslaufartikeln
6.6 Wichtige Eigenschaften von Berichten und Berichtsbereichen Berichte und ihre Bereiche besitzen einige erwähnenswerte Eigenschaften, die Ihnen in der Praxis viel Arbeit ersparen können. Einige der Eigenschaften finden Sie im Eigen schaftenfenster, andere im Bereich Gruppieren, Sortieren und Summe. Dieser Bereich ist vermutlich nicht nur für Buch- und sonstige Autoren ein Greuel: Er lässt sich nicht vergrößern (zumindest in der Grundversion von Access 2007 ohne Service Packs – vielleicht geschieht da ja noch was) und zeigt Werte von Eigenschaften ohne den Eigenschaftsnamen
390
Berichte
an. Die folgenden Abschnitte verwenden daher von den Eigenschaftswerten abgeleitete oder die in den bisherigen Access-Versionen im Dialog Sortieren und Gruppieren benutzten Eigenschaftsnamen.
Abbildung 6.21: Nicht mehr verfügbare Artikel werden durchgestrichen
6.6.1 Kopfzeilenbereich und Fußzeilenbereich Mit diesen beiden Eigenschaften legen Sie im Bereich Gruppieren, Sortieren und Summe für Gruppierungen fest, ob zu einer Gruppierung ein Kopf- und/oder ein Fußbereich eingeblendet werden soll. Unter VBA sprechen Sie diese beiden Eigenschaften mit Group Header und GroupFooter an. Es handelt sich dabei um Eigenschaften des GroupLevel-Ob jekts, das Sie innerhalb des Berichts-Klassenmoduls beispielsweise wie im folgenden Listing ansprechen. Die Routine gibt auch noch die übrigen wichtigen Eigenschaften einer Gruppierung aus: Private Sub Report_Current() Dim grp As GroupLevel Set grp = Me.GroupLevel(0) Debug.Print "GroupInterval: " & grp.GroupInterval Debug.Print "ControlSource: " & grp.ControlSource Debug.Print "GroupFooter: " & grp.GroupFooter Debug.Print "GroupHeader: " & grp.GroupHeader Debug.Print "GroupOn: " & grp.GroupOn Debug.Print "KeepTogether: " & grp.KeepTogether
391
Kapitel 6 Debug.Print "SortOrder: " & grp.SortOrder End Sub Listing 6.8: Ausgabe der Eigenschaften einer Gruppierung
6.6.2 Gruppieren nach und Intervall Standardmäßig wird in einer Gruppierung nach jedem Wert des unter Feld/Ausdruck angegebenen Feldes gruppiert (VBA-Eigenschaft ControlSource). Das lässt sich aber auch grober einstellen. Im einfachsten Fall gruppieren Sie etwa nach dem Primärschlüsselfeld, stellen die Eigenschaft Gruppieren nach (VBA: GroupOn) auf den Wert Intervall ein und setzen für die Eigenschaft Intervall (VBA: Interval) den Wert 10 fest. Warum das Ganze? Beispielsweise, um alle paar Zeilen eine zusätzliche Leerzeile einzufügen, um eine Liste besser lesbar zu machen. Ein Beispiel finden Sie im Bericht rptArtikelUebersichtInZehnergruppen (siehe Abbildung 6.22).
Abbildung 6.22: Einstellung zur Anzeige von Daten in Zehnerpäckchen und Layoutansicht des passenden Berichts
392
Berichte
Richtig interessant werden diese Eigenschaften, wenn Sie mit Datumsangaben arbeiten – hier können Sie dann nach Jahren, Quartalen, Monaten, Wochen und Tagen oder auch feinkörniger nach Stunden und Minuten gruppieren.
6.6.3 Zusammenhalten von Daten Innerhalb von Gruppierungen sorgen Sie mit der Eigenschaft Zusammenhalten (VBA: KeepTogether) des Dialogs Gruppieren, Sortieren und Summe dafür, dass entweder die komplette Gruppe (also Gruppenkopf, die enthaltenen Detaildatensätze und Gruppenfuß) oder zumindest der Gruppenkopf und der erste Detaildatensatz der Gruppierung auf einer Seite gedruckt werden. Eine ähnliche Eigenschaft gibt es auch noch einmal unter Gruppe zusammenhalten (VBA: GrpKeepTogether) im Eigenschaftsfenster des Berichts; dort kann diese Eigenschaft auch für den Detailbereich festgelegt werden.
6.6.4 Neue Seite, Zeile oder Spalte Gruppierungen und auch der Detailbereich bieten die Möglichkeit, vor, nach oder vor und nach einem Bereich eine neue Seite, Zeile oder Spalte zu beginnen. Mit der Eigen schaft Neue Seite (VBA: ForceNewPage) geben Sie an, ob vor oder nach dem Bereich oder auch in beiden Fällen eine neue Seite erzeugt werden soll. Ein Beispiel finden Sie in Ab schnitt 6.9.8 unter »Bereiche auf neuer Seite anzeigen«. Die Eigenschaft Neue Zeile oder Spalte (VBA: NewRowOrCol) legt fest, ob Bereiche in mehrspaltigen Berichten in einer neuen Zeile oder einer neuen Spalte angelegt werden sollen. Ob es sich nun um eine neue Zeile oder Spalte handelt, hängt vom Spaltenlayout ab, das Sie im Dialog Seite einrichten festlegen (siehe Abbildung 6.23). Diesen Dialog öffnen Sie über den RibbonEintrag Seite einrichten|Seitenlayout|Spalten.
Abbildung 6.23: Optionen für mehrspaltige Berichte
393
Kapitel 6
6.6.5 Vergrößerbar und Verkleinerbar Manchmal enthalten Felder keine Daten – das kann beispielsweise bei Adressdaten passieren. Da fehlt mal hier ein Ansprechpartner und mal dort eine Straße. Es wäre doch traurig, wenn Access-Berichte hier keine Abhilfe schaffen könnten. Abbildung 6.24 zeigt, wie Sie die Eigenschaft Verkleinerbar der Textfelder eines Adress etiketts auf Ja einstellen. Damit sorgen Sie dafür, dass die Felder, wenn diese keinen Inhalt haben, auf die Höhe 0 verkleinert werden. Der Clou ist, dass die anderen Felder dafür nach oben rücken.
Abbildung 6.24: Verkleinerbare Textfelder im Bericht
Abbildung 6.25 enthält die Seitenansicht des Berichts rptAdressetiketten: Bei Alfreds Futterkiste fehlt der Ansprechpartner, bei Ana Trujillo Emparedadosy helados die Straße. In beiden Fällen wurden die anderen Felder so nach oben verschoben, dass kein Freiraum mehr bleibt.
Einschränkung Das Verkleinern funktioniert allerdings nur, wenn sich die Felder nicht mit anderen Feldern überlappen und auch keine anderen Felder auf der gleichen Höhe liegen. Im Bericht aus Abbildung 6.41 etwa ist das Verkleinern eines Feldes und das Verschieben der darunter liegenden Felder nach oben nicht möglich. Wenn Sie diese Technik verwenden möchten, müssen Sie außerdem bedenken, dass eventuelle Zwischenräume nicht verkleinert werden. Wenn Sie also zwischen dem ersten und zweiten Steuerelement 10 Pixel Zwischenraum haben und zwischen dem zweiten und dritten auch, dann bleiben beim Verkleinern des zweiten Steuerelements immer noch insgesamt 20 Pixel Zwischenraum
394
Berichte
übrig – wenn alle anderen Zwischenräume 10 Pixel betragen, ergibt das ein unschönes Bild. Abhilfe schaffen Sie, indem Sie einfach die Steuerelemente ein wenig größer ziehen (bei Textfeldern geht das problemlos) und statt dessen die Zwischenräume weglassen – etwa durch den Einsatz des gestapelten Layouts.
Abbildung 6.25: Adressetiketten mit geschrumpften Textfeldern
6.6.6 Bereich wiederholen Wenn sich gruppierte Daten mit Feldüberschriften oder ähnlichen Informationen im Gruppenkopf über mehr als eine Seite erstrecken, sollten die Überschriften auch auf den Folgeseiten angezeigt werden. Dies legen Sie fest, indem Sie die Eigenschaft Bereich wiederholen (VBA: RepeatSection des Section-Objekts) der jeweiligen Gruppierung auf den Wert Ja einstellen. Ein Beispiel dafür finden Sie in Abschnitt 6.8.3, »Unterberichte über mehrere Seiten«.
6.7 Darstellung von Daten Noch im vorherigen Kapitel haben Sie erfahren, wie Sie mit Formularen die unterschiedlichen Beziehungsarten anzeigen. Dort gab es Varianten wie Listenfelder, Unterformulare, Detailformulare und sogar das TreeView-Steuerelement wurde zu Hilfe genommen. Und das war nur die Spitze des Eisbergs; auf mehrere Unterformulare im gleichen Formular, Unterformulare auf verschiedenen Registerblättern und Ähnliches wurde dort gar nicht eingegangen.
395
Kapitel 6
Berichte sind da wesentlich anspruchsloser. Die meisten Darstellungen lassen sich mit nur zwei unterschiedlichen Methoden erreichen: Sie fassen alle notwendigen Daten in der als Datensatzquelle dienenden Abfrage zusammen. Alles Weitere erledigen Sie im Bericht durch entsprechende Gruppierungen. Das funktioniert fast immer, außer wenn … … die Haupttabelle des Berichts mit mehr als einer Tabelle per 1:n-Beziehung verknüpft ist und Daten aus diesen drei (oder mehr) Tabellen im Bericht angezeigt werden sollen. Dann helfen auch keine Gruppierungen mehr, hier müssen Unterberichte her. Diese beiden Arten von Berichten werden Sie in den nächsten Abschnitten kennen lernen.
6.7.1 Einzelne Tabellen Daten aus einzelnen Tabellen lassen sich in Berichten wie auch in Formularen als Detailansicht mit je einem Datensatz oder in Listenform anzeigen. Beides ist nicht besonders kompliziert.
Daten einzelner Tabellen in der Detailansicht Als Datensatzquelle für das folgende Beispiel dient die Artikel-Tabelle der NordwindDatenbank. Auf je einer Seite sollen die Eigenschaften eines Artikels angezeigt werden. Um eine Entwurfsansicht wie in Abbildung 6.26 zu erzeugen, sind folgende Schritte notwendig: Stellen Sie die Eigenschaft Datensatzherkunft des Berichts auf die Tabelle Artikel ein. Ziehen Sie alle Felder aus der Feldliste in den Detailbereich des Berichtsentwurfs. Ordnen Sie die Felder sauber an und stellen Sie gegebenenfalls Schriftgröße und/ oder -breite für eine bessere Lesbarkeit ein. Fügen Sie den Eintrag Artikelname der Feldliste ein zweites Mal in den Bericht ein – diesmal allerdings in den Bereich Seitenkopf. Vergrößern Sie Schriftgröße und -breite so, dass dieses Feld Überschriftscharakter erhält. Legen Sie im Bereich Seitenfuß zwei Textfelder an (Bezeichnung: txtSeite und txtDatum, Steuerelementinhalt: ="Seite " & [Seite] & "/" & [Seiten] und =Datum()) Sorgen Sie dafür, dass nur ein Datensatz je Seite angezeigt wird. Dazu vergrößern Sie entweder den Detailbereich, sodass kein zweiter kompletter Detailbereich auf die Seite passt oder besser, Sie stellen die Eigenschaft Neue Seite des Detailbereichs auf den Wert Nach Bereich ein.
396
Berichte
Legen Sie außerdem eine Sortierung nach dem Artikelnamen fest. Dazu blenden Sie den Bereich Gruppieren, Sortieren und Summe ein (Ribbon-Eintrag Format|Gruppierung und Summen|Gruppieren und sortieren) und wählen dort unter Sortieren nach das Feld Artikelname aus.
Abbildung 6.26: Entwurfsansicht des Berichts zur Anzeige der Artikeldetails (»rptArtikel«)
Beschriftungen von Steuerelementen verschieben Beim Positionieren der Steuerelemente stört möglicherweise, dass Beschriftungsfelder und Steuerelemente standardmäßig zusammen verschoben werden. Wenn Sie lieber alle Elemente einzeln positionieren möchten, schneiden Sie jeweils eines der verbundenen Elemente aus und fügen Sie es direkt wieder ein (geht am schnellsten mit der Tastenkombination Strg + C und Strg + V). Anschließend können Sie die Steuerelemente separat verschieben. Das Einzige, was stört, ist das Viereck in der linken oberen Ecke des neu eingefügten Bezeichnungsfeldes. Beim Anklicken des betroffenen Steuerelements erscheint das aufklappbare Element aus Abbildung 6.27 und offenbart einige Möglichkeiten zum Umgang mit dem Bezeichnungsfeld. Den vermeintlichen Fehler können Sie entweder jedes Mal mit dem Menüeintrag Fehler ignorieren unsichtbar machen oder, wenn Sie
397
Kapitel 6
dauerhaft von Hinweisen bezüglich allein stehender Steuerelemente verschont bleiben möchten, die Option Objekt-Designer|Fehlerüberprüfung|Fehlerüberprüfung aktivieren in den Access-Optionen ausschalten.
Abbildung 6.27: Ein Smarttag weist auf »ungebundene« Bezeichnungsfelder hin
Daten einzelner Tabellen in der Übersicht Eine Übersicht der Artikel aus dem oben beschriebenen Bericht lässt sich ebenso leicht erstellen. Legen Sie in einem neuen Bericht wiederum die Tabelle Artikel als Datensatzquelle fest. Ziehen Sie auch hier alle Felder aus der Feldliste in den Detailbereich des Berichts. Anschließend stellen Sie zunächst die Orientierung des Berichts im Ribbon unter Seite einrichten|Seitenlayout auf Querformat ein. Hier können Sie auch die Anzeige der Seitenränder aktivieren und diese einstellen. Die Artikel sollen als Tabelle angezeigt werden, also benötigen Sie eine Tabellenüber schrift je Seite. Dazu schneiden Sie die Beschriftungen der eingefügten Felder komplett aus und fügen diese im Bereich Seitenkopf wieder ein. Ordnen Sie dann die Felder neben einander an, sodass der Entwurf etwa wie in Abbildung 6.28 aussieht. Die Feldüberschriften erscheinen in der Seitenansicht auf jeder Seite – genau wie die im Seitenfuß befindlichen Angaben zu Seitenzahl und Datum. Für eine bessere Optik empfiehlt sich auch die Verwendung von horizontalen Strichen zur Abgrenzung von Seitenkopf und -fuß und den eigentlichen Daten (siehe Abbildung 6.29). Wenn Sie keine besonderen Ansprüche an eine solche Ansicht stellen, können Sie auch das Layout Tabelle verwenden. Sie können damit einfach die Steuerelementbreiten und -reihenfolge anpassen oder Gitternetzlinien hinzufügen. Erstellen Sie einfach einen neuen Bericht, indem Sie die Herkunftstabelle (hier tblArtikel) im Navigationsbereich markieren, den Ribbon-
398
Berichte
Eintrag Erstellen|Berichte|Bericht wählen und mit Anordnen|Layout bestimmen|Tabelle ein Tabellenlayout festlegen. Allerdings können die beiden Layouts auch als Basis für anspruchsvollere Berichte dienen.
Abbildung 6.28: Entwurfsansicht der Artikelübersicht
Abbildung 6.29: Artikelübersicht in der Seitenansicht
Wenn Sie eine Anwendung entwickeln, stehen Ihnen in manchen Fällen bereits umfangreiche Daten des Auftraggebers zur Verfügung. Diese werden Sie vermutlich ohnehin
399
Kapitel 6
im Rahmen der Erstellung des Datenmodells importiert haben. Wenn Sie nicht über solche Daten verfügen, werden Sie wohl oder übel einige hundert Beispieldatensätze anlegen müssen. Viele nachträgliche Änderungen an Datenbankanwendungen resultieren nämlich daraus, dass die einzelnen Formulare und vor allem Berichte nicht mit realistischen Daten getestet wurden. Es zeigt sich dann in der Praxis, dass die Steuerelemente in Formularen und Berichten zu klein für die tatsächlichen Daten dimensioniert wurden oder Berichte ab bestimmten Seitenzahlen ein unerwartetes Verhalten aufweisen. Mit den richtigen Beispieldaten schaffen Sie solche Probleme bereits bei der Entwicklung aus der Welt.
6.7.2 1:n-Beziehungen Zur Darstellung von 1:n-Beziehungen müssen Sie nicht unbedingt, wie bereits weiter oben angedeutet, auf Unterformulare oder Steuerelemente wie Listenfelder zugreifen. In einem Bericht reicht es aus, die verknüpften Tabellen zu einer Datensatzquelle zusam menzufassen und eine entsprechende Gruppierung anzulegen. Natürlich gibt es hier Ausnahmen wie etwa die Anzeige mehrerer mit der Mastertabelle verknüpfter Detail tabellen. Dazu jedoch später mehr. Die Darstellung einer 1:n-Beziehung lässt sich anschaulich an den Artikeln und Kategorien der Nordwind-Datenbank erläutern. Dazu soll einfach der Bericht aus dem vorherigen Beispiel so umgestaltet werden, dass jede Kategorie mit den enthaltenen Artikeln auf je einer Seite erscheint.
Datensatzquelle des 1:n-Berichts Dazu erweitern Sie zunächst die Datensatzquelle des Berichts um die Tabelle Kategorien und ziehen alle Felder der beiden Tabellen in den Abfrageentwurf (siehe Abbildung 6.30).
Gruppierung statt Unterformular Statt des bei der Verwendung von Formularen notwendigen Unterformulars oder Listen feldes zur Darstellung von verknüpften Daten wenden Sie bei Berichten vorzugsweise Gruppierungen an. Im vorliegenden Beispiel sollen eine Kategorie und die dazugehören den Artikel auf einer Seite angezeigt werden. Also gruppieren Sie die Datensatzquelle nach Kategorien. Jede Gruppe erhält einen speziellen Kopfbereich, den Gruppenkopf. In diesem werden Daten wie Name der Kategorie, Beschreibung oder das in der Tabelle enthaltene Bild abgelegt. Um eine Gruppierung anzulegen, klicken Sie im Bereich Gruppieren, Sortieren und Summe auf die Schaltfläche Gruppe hinzufügen und stellen unter Gruppieren nach das Feld ein, nach dem gruppiert werden soll. Außerdem legen Sie hier fest, ob die Gruppe einen Gruppenkopf und/oder -fuß erhalten soll. In diesem Fall reicht ein Gruppenkopf aus (siehe Abbildung 6.31). Fügen Sie außerdem eine Sortierung nach dem Artikelnamen hinzu.
400
Berichte
Abbildung 6.30: Datensatzquelle eines Berichts zur Anzeige der Kategorien und der enthaltenen Artikel
Achten Sie beim Anlegen von Sortierungen und Gruppierungen darauf, dass diese von oben nach unten abgearbeitet werden. In diesem Fall gilt: Gruppierungen müssen nach oben, Sortierungen innerhalb der gruppierten Daten nach unten. Der Detailbereich soll wie im Bericht rptArtikelUebersicht die Artikel der jeweiligen Kategorie in tabellarischer Form anzeigen. Natürlich benötigen Sie auch hier Spaltenüberschriften. Wie Abbildung 6.31 zeigt, befindet sich der Seitenkopf-Bereich über dem Gruppenkopf – dort würden die Feldüberschriften nicht gut aussehen. Also fügen Sie diese einfach im unteren Bereich des Gruppenkopfes der Kategorien ein.
Abbildung 6.31: Entwurf des Berichts zur Anzeige von Artikeln nach Kategorie
401
Kapitel 6
Eine Gruppe je Seite Wenn Sie nun in die Seitenansicht wechseln, werden die Artikel zwar nach Kategorien gruppiert, aber die Kategorien folgen ohne Seitenumbruch direkt aufeinander und ste hen nicht auf je einer Seite. Dabei fällt gleichzeitig auf, dass Kategorien, die mehr Artikel enthalten, als auf eine Seite passen, zwar auf der nächsten Seite fortgesetzt werden, aber ohne Feldüberschriften. Das ist auch logisch, denn diese werden ja bisher nur im Grup penkopf angezeigt. Damit für jede Gruppierung eine neue Seite erzeugt wird, stellen Sie die Eigenschaft Neue Seite des Bereichs auf den Wert Vor Bereich ein (siehe Abbildung 6.32). Damit kennen Sie nun auch alle Möglichkeiten, Eigenschaften der Kopf- und Fußbereiche von Gruppierungen einzustellen – nämlich im Bereich Gruppieren, Sortieren und Summe und im entsprechenden Eigenschaftsfenster.
Abbildung 6.32: Eigenschaften des Gruppenkopfs eines Bereichs
Feldüberschriften auf Folgeseiten von Gruppierungen Fehlen noch die Feldüberschriften für Kategorien, die mehr als eine Seite füllen. Bisher dient der Kopfbereich der Gruppe KategorieID quasi als Seitenkopf; nun benötigen Sie den Seitenkopf selbst. Kopieren Sie hier die Feldüberschriften aus dem KategorienKopfbereich hinein und fügen Sie auch eine Kopie des Feldes Kategoriename aus diesem Bereich hinzu. Löschen Sie den Inhalt der Eigenschaft Steuerelementinhalt dieses Steuer elements; es wird später per VBA gefüllt (siehe Abbildung 6.33). Eine VBA-Prozedur ist es auch, die den Seitenkopf einblendet, wenn der Detailbereich eine Fortsetzung der vorherigen Seite ist. Die entsprechende Prozedur wird durch die
402
Berichte
Ereigniseigenschaft Beim Drucken ausgelöst. Sie fragt die Eigenschaft WillContinue ab, die genau die gesuchte Information liefert, die Sie hier benötigen. Ist der Wert False, handelt es sich nicht um eine Fortsetzung der Artikel einer Kategorie – hier wird dann der Kopfbereich der neuen Kategorie angezeigt. Interessant wird es, wenn WillContinue den Wert True zurückliefert – in diesem Fall wird der Seitenkopf eingeblendet und das Textfeld txtKategoriename mit dem Namen der aktuellen Kategorie und dem Text »(Fortsetzung)« gefüllt (siehe Beispieldatenbank, Bericht rptKategorienUndArtikel). Private Sub Detailbereich_Print(Cancel As Integer, PrintCount As Integer) If Me.Detailbereich.WillContinue = True Then Me.Seitenkopfbereich.Visible = True Me!txtKategoriename = Me!Kategoriename & " (Fortsetzung)" Else Me.Seitenkopfbereich.Visible = False End If End Sub Listing 6.9: Der Seitenkopf wird eingeblendet, wenn der Detailbereich eine Fortsetzung der vorherigen Seite ist
Abbildung 6.33: Der neue Seitenkopf soll nur zum Einsatz kommen, wenn der KategorienKopfbereich nicht angezeigt wird
6.7.3 m:n-Beziehungen Die Darstellung von m:n-Beziehungen in Berichten funktioniert fast genauso wie im vorherigen Beispiel der 1:n-Beziehung. Der wichtigste Unterschied ist, dass die Datensatzquelle nun mindestens drei Tabellen enthält (die beiden verknüpften Tabellen und die Verknüpfungstabelle). Davon abgesehen verwenden Sie genau wie bei der
403
Kapitel 6
1:n-Verknüpfung eine Gruppierung nach den gewünschten Daten der m-Seite der Tabelle und zeigen die anderen Daten entsprechend im Detailbereich an.
6.8 Berichte mit Unterberichten Einer der wenigen Gründe, der für den Einsatz von Unterberichten spricht, ist das Vorhandensein einer Mastertabelle, die mit mehreren Detailtabellen verknüpft ist. Wenn Sie also etwa eine Kunden-Tabelle haben, die mit einer Projekte-Tabelle und einer Ansprechpartner-Tabelle verknüpft ist und Sie diese alle in einem Bericht anzeigen möchten, kommen Sie um den Einsatz von Unterberichten nicht herum.
6.8.1 Unterberichte Beginnen Sie mit den Unterberichten für die Projekte und die Mitarbeiter des Unternehmens. Der Unterbericht srpAnsprechpartner verwendet die Tabelle tblAnsprechpartner als Datensatzquelle. Die Felder werden tabellarisch im Seitenkopf und im Detailbereich aufgebaut (siehe Abbildung 6.34). Der Unterbericht srpProjekte bezieht seine Daten aus der Tabelle tblProjekte und ist prinzipiell genauso aufgebaut wie der Bericht srpAnsprechpartner.
Abbildung 6.34: Unterbericht zur Darstellung der Mitarbeiter
6.8.2 Einbinden der Unterberichte in den Hauptbericht Der Bericht rptKunden dient als Hauptbericht. Er verwendet die Tabelle tblKunden als Datensatzquelle und stellt die dort enthaltenen Daten im oberen Teil des Detailbereichs dar. Damit nur ein Kunde je Seite angezeigt wird, stellen Sie die Eigenschaft Neue Seite dieses Bereichs auf den Wert Vor Bereich ein. Die Unterformulare ziehen Sie genau wie beim Einfügen eines Unterformulars in ein Hauptformular einfach aus dem Navigationsbereich in den Detailbereich. Unterbericht-
404
Berichte
Steuerelemente haben wie Unterformular-Steuerelemente zwei Eigenschaften namens Verknüpfen von und Verknüpfen nach, mit denen die Felder beziehungsweise Steuerele mente festgelegt werden, über die die in Haupt- und Unterbericht anzuzeigenden Daten verknüpft werden. Der wichtigste Unterschied zwischen dem Anlegen von Unterberichten im Vergleich zu Unterformularen ist, dass Sie Letztere bereits im Entwurf genau anpassen müssen. Unterberichte müssen nur die richtige Breite haben, die Höhe wird je nach Anzahl der enthaltenen Datensätze vergrößert – vorausgesetzt Sie stellen die Eigenschaft Vergrößerbar des Unterberichtsteuerelements auf Ja ein. Sie können auch die Eigenschaft Verkleinerbar auf Ja einstellen – in diesem Fall wird der Unterbericht gar nicht beziehungsweise mit der Höhe 0 angezeigt, wenn keine Datensätze enthalten sind.
Abbildung 6.35: Hauptbericht mit zwei Unterberichten
Wenn Sie den Hauptbericht nun in der Seitenansicht öffnen, fehlen allerdings die in den Unterberichten noch vorhandenen Kopfzeilen (siehe Abbildung 6.36). Das ist kein Fehler, denn ein Unterbericht enthält schlicht und einfach keine eigene Seite, auf der ein Seitenkopf untergebracht werden könnte. Also müssen Sie sich mit einem kleinen Trick behelfen. Wenn man keine Feldüberschriften mit einem Seitenkopf erzeugen kann, verwenden Sie einfach einen Kopfbereich einer Gruppierung. Oh, es gibt gar keine Möglichkeit für eine Gruppierung? Kein Problem: Dann verwenden Sie einfach eine fiktive Gruppierung ohne Bezug zu einem der Felder. Legen Sie dazu im
405
Kapitel 6
Bereich Gruppieren, Sortieren und Summe eine neue Gruppierung an und klicken Sie unter Gruppieren nach auf den Link Ausdruck. Es öffnet sich der Ausdruck-Manager: Geben Sie hier den Wert -1 ein und schließen Sie diesen wieder. Legen Sie außerdem einen Kopfzeilenbereich für diese Gruppierung an.
Abbildung 6.36: Unterberichte ohne Kopfbereich
Verschieben Sie dann die Feldüberschriften aus dem Seitenkopfbereich in den Kopfbereich der neuen Gruppierung – fertig sind die Feldüberschriften (siehe Abbildung 6.37).
Abbildung 6.37: Erzeugen des Kopfbereichs einer fiktiven Gruppierung
Abbildung 6.38 zeigt, dass die neuen Feldüberschriften auch im Hauptbericht angezeigt werden.
406
Berichte
Abbildung 6.38: Unterberichte mit Feldüberschriften
6.8.3 Unterberichte über mehrere Seiten Die Unterberichte aus dem vorherigen Beispiel haben einen kleinen Fehler: Wenn ein Kunde einmal so viele Ansprechpartner oder Projekte enthält, dass der Unterbericht sich über zwei Seiten erstreckt, erscheinen wiederum keine Feldüberschriften. Dies zu ändern ist allerdings leicht: Stellen Sie einfach die Eigenschaft Bereich wiederholen des Gruppenkopfes im Entwurf des Unterformulars auf den Wert Ja ein (siehe Abbil dung 6.39).
6.9 Rechnungserstellung mit Berichten Das Ausgeben von Rechnungen ist vermutlich einer der am meisten genutzten Anwen dungsfälle von Berichten in Access. Deshalb und auch weil Rechnungsberichte eine Menge Möglichkeiten zum Vorstellen verschiedener Techniken bieten, finden Sie nachfolgend ein ausführliches Beispiel zum Erstellen von Rechnungen mit Access-Berichten. Als Grundlage verwenden Sie die Bestelldaten aus der Nordwind-Datenbank. Welche Tabellen Sie für die dem Bericht zugrunde liegende Abfrage benötigen, lässt sich mit einem Blick auf eine herkömmliche Rechnung herausfinden. Für die Anschrift des Kun den benötigen Sie die Kunden-Tabelle, den Rest erledigen die in einer m:n-Beziehung
407
Kapitel 6
stehenden Tabellen tblBestellungen, tblBestelldetails und tblArtikel. Damit der Kunde weiß, an wen er sich bei Fragen wenden kann, nehmen Sie noch die Tabelle tblPersonal hinzu, die ebenfalls mit der Tabelle tblBestellungen verknüpft ist. Die Datensatzquelle sieht schließlich wie in Abbildung 6.40 aus.
Abbildung 6.39: Mit dieser auch auf den Folgeseiten
Einstellung
erscheint
der
Abbildung 6.40: Datensatzquelle für den Rechnungsbericht
408
Kopfbereich
der
Gruppierung
Berichte
Den aktuellen Steuergesetzen entsprechend wurde der Tabelle Bestellung noch ein Feld namens Rechnungsnummer hinzugefügt. Das Feld hat den Datentyp Text, um auch Zeichen wie Bindestriche oder Schrägstriche zu ermöglichen. Wie Sie die Rechnungsnummer ermitteln, bleibt Ihnen überlassen. Eine Rechnungsnum mer muss aber auf jeden Fall eindeutig sein. Manch einer verwendet eine bei 1 startende Nummerierung in Zusammenhang mit der Jahreszahl (zum Beispiel »2005-01«); andere lassen die Kundennummer in die Rechnungsnummer einfließen. Wer wichtig wirken möchte, verwendet eine Rechnungsnummer wie »950 752 0642«. In der Beispieltabelle Bestellungen ist die Rechnungsnummer schlicht eine Kombination aus dem Datum der Rechnungstellung und dem Primärschlüssel der Tabelle (etwa »20051009-11077«). So können Sie auch einmal nur einfach in den Ordner mit Rechnungen schauen, wenn ein Kunde (oder der Steuerprüfer) dazu eine Frage hat – vorausgesetzt dort liegen die Rechnungen in chronologischer Reihenfolge vor ... Außerdem wurde die Tabelle Artikel um ein Feld namens Mehrwertsteuer erweitert, das durchgängig den Wert 7% enthält. Die Abfrage selbst wurde um ein berechnetes Feld namens Bruttopreis ergänzt, das den Bruttopreis einer jeden Position enthält (siehe Auflistung). Sie verwenden in diesem Bericht folgende Felder: Tabelle tblArtikel: Artikelname, Liefereinheit, Mehrwertsteuer Tabelle tblBestelldetails: Einzelpreis, Anzahl, Rabatt Tabelle tblBestellungen: BestellungID, Bestelldatum, Versanddatum, VersandUeber, Fracht kosten, Empfaenger, Strasse, Ort, PLZ, Bestimmungsland, Rechnungsnummer Tabelle tblKunden: Firma, Kontaktperson, Strasse, Ort, PLZ, Land Tabelle tblPersonal: Nachname, Vorname, Anrede, DurchwahlBuero Berechnetes Feld Bruttopreis: [tblBestelldetails].[Einzelpreis]*[tblBestelldetails].[Anzahl]* ([tblBestelldetails].[Rabatt]+1)*([tblArtikel].[Mehrwertsteuer]+1) Berechnetes Feld Netto: [tblBestelldetails.Einzelpreis]*[Anzahl]*(1+[Rabatt]) Berechnetes Feld MwStBetrag: [tblBestelldetails.Einzelpreis]*[Anzahl]*(1+[Rabatt])*[Mehr wertsteuer] Die letzten beiden Felder werden nicht in der Auflistung der Positionen angezeigt, sondern dienen nur der Ermittlung der Summe der Nettopreise und der Mehrwertsteuer am Ende der Rechnung.
409
Kapitel 6
6.9.1 Konzept für die Erstellung des Berichts Bevor Sie sich an die Erstellung eines Berichts machen, sollten Sie die Daten in Gedanken (oder auch auf dem Papier) kurz auf die einzelnen Bereiche eines Berichts aufteilen und sich überlegen, welche Daten etwa nur auf der ersten Seite angezeigt werden, was passiert, wenn so viele Datensätze vorhanden sind, dass der Bericht über mehrere Seiten geht, und so weiter. Im vorliegenden Fall haben Sie es mit einem ziemlich großen Berichtskopf zu tun: Dieser enthält den kompletten Briefkopf, die Anschrift des Kunden, Lieferdaten und so weiter. Moment: Ist das wirklich so? Wenn der Berichtskopf bereits kundenspezifische und damit rechnungsspezifische Daten enthält, dann können Sie nur eine Rechnung je Bericht ausgeben. Warum? Weil der Berichtskopf nur einmal je Bericht angezeigt wird. Wenn Sie aber mal einen ganzen Schwung Rechnungen ausdrucken möchten, haben Sie ein Problem: Sie müssen dann schon den gleichen Bericht mehrere Male aufrufen. Natürlich geht das auch, aber in diesem Fall sollen Sie einen Bericht erstellen, der mehrere Rechnungen auf einen Rutsch anfertigen kann. Also benötigen Sie eine Gruppierung über die einzelnen Bestellungen – als Gruppierungsfeld bietet sich das Feld BestellungID an. Im Gruppenkopf bringen Sie nun alles unter, was Sie sonst in den Berichtskopf packen wollten – Briefkopf, Anschrift, Bestelldaten wie Bestellnummer und so weiter. Die einzelnen Positionen gehören – ganz klar – in den Detailbereich. Darüber benötigen Sie Feldüberschriften: Diese können Sie auf der ersten Seite direkt im Gruppenkopf unterbringen, auf den folgenden Seiten im Seitenkopf. Dieser darf dann wiederum nicht auf der ersten Seite angezeigt werden – dazu später mehr. Nach der letzten Rechnungsposition folgt noch die Rechnungssumme. Dafür bietet sich der Berichtsfuß an. Auf der Seite mit dem Berichtsfuß soll wiederum kein Seitenfußbereich angezeigt werden. Wenn Sie dem Kunden ein wenig mehr Komfort bieten möchten, sorgen Sie auch noch für eine Zwischensumme und einen Übertrag. Die Zwischensumme gehört mit in den Seitenfuß, der Übertrag in den Seitenkopf – dieser erscheint aber nur auf den Seiten, die keinen Gruppierungskopf anzeigen. Sie sehen – eine Rechnung ist nicht gerade der trivialste Bericht, der sich mit Access erstellen lässt, und er enthält noch nicht einmal Gruppierungen.
6.9.2 Erstellen des Gruppenkopfs Die Erstellung des Gruppenkopfs einer Rechnung ist im Wesentlichen Design-Arbeit und weniger Denksport. Hier bringen Sie den Briefkopf unter, den Block mit der Empfängeradresse und den Block mit den allgemeinen Rechnungsdaten (siehe Abbildung 6.41).
410
Berichte
Hier gibt es folgende Besonderheiten: Das Feld txtPLZUndOrt fasst die Felder PLZ und Ort der Tabelle Kunden zusammen. Wichtig ist hier wie bei anderen Steuerelementen, dass Sie Tabellennamen und Feldnamen von in mehreren Tabellen vorkommenden Feldern durch Punkt getrennt schreiben und in eckige Klammern einfassen. Dies geschieht noch einmal im Gruppenkopf, und zwar im Textfeld txtAnsprechpartner. Dessen Inhalt lautet: =[Personal.Anrede] & " " & [Personal.Vorname] & " " & [Personal.Nachname]
Die Feldüberschriften werden Sie normalerweise erst im folgenden Schritt beim Füllen des Detailbereichs anlegen, in der Abbildung sehen Sie aber bereits ihre Anordnung.
Abbildung 6.41: Gruppenkopf einer Rechnung in der Detailansicht
6.9.3 Anlegen des Detailbereichs Der Detailbereich ist reine Fleißarbeit – abgesehen von einem Feature, das aber erst später hinzukommt, und der Angabe der Rechnungspositionen. Der Detailbereich enthält die Felder Artikelname, Liefereinheit, Bruttopreis, Einzelpreis, Anzahl, Rabatt, Mehrwertsteuer und Bruttopreis, wobei Letzteres das berechnete Feld ist, das Sie der Abfrage weiter oben hinzugefügt haben. Es fehlt noch das ganz linke Feld in der Entwurfsansicht (siehe Abbildung 6.42). Es heißt txtPosition und besitzt als Steuerelementinhalt den Wert =1. Damit dieses Feld die Position des Artikels für die aktuelle Rechnung, also innerhalb
411
Kapitel 6
der aktuellen Gruppierung, anzeigt, stellen Sie die Eigenschaft Laufende Summe dieses Feldes auf Über Gruppe ein.
Abbildung 6.42: Entwurfsansicht des Detailbereichs mit den Rechnungspositionen
6.9.4 Berechnungen in Berichten oder Berechnungen in Formularen Möglicherweise fragen Sie sich, ob man die Berechnung des Bruttopreises nicht auch innerhalb des Berichts hätte durchführen können. Die Frage ist berechtigt, da das sogar funktionieren würde. Das Problem ist nur, dass Sie keine datensatzübergreifenden Berechnungen über den Bezug auf das Feld mit dem Berechnungsergebnis durchführen könnten. Ein in der Abfrage berechnetes Feld behandelt Access im Bericht aber wie ein normales Tabellenfeld; hier sind Berechnungen wie etwa das Bilden der Summe über alle Datensätze einer Gruppierung möglich. Streng genommen würde man im Bericht auch ohne das in der Abfrage berechnete Feld auskommen, aber dann müsste man im Feld zur Berechnung der Summe über mehrere Datensätze Bezug auf die Berechnungsformel nehmen und nicht auf das Feld mit dem Berechnungsergebnis.
6.9.5 Summenbildung im Fußbereich der Gruppierung Im Fußbereich der Gruppierung heißt es: Abrechnen! Hier wird die Summe über das Feld Bruttopreis des Detailbereichs gebildet und schließlich noch der Frachtkostenanteil hinzuaddiert. Unterhalb der Rechnungssumme ist ein guter Ort, um die Bankverbindung unterzubringen. Diese könnte man zwar auch auf der ersten Seite zu den allgemeinen Rechnungsdaten hinzufügen, aber der Rechnungsempfänger wird sich freuen, wenn er zum Begleichen der Rechnung per Online-Banking nicht noch hin- und herblättern muss (siehe Abbildung 6.43). Der Fußbereich greift auf zwei Felder zurück, die sich zwar in der Abfrage, aber nicht im Detailbereich des Berichts befinden: Netto und MwStBetrag. Diese werden aus Gründen der Übersicht nicht in jedem Datensatz angezeigt, sondern nur aufsummiert am Ende der Rechnung.
412
Berichte
Abbildung 6.43: Der Fußbereich der Gruppierung in der Entwurfsansicht
6.9.6 Feinheiten: Zwischensumme und Übertrag Ist abzusehen, dass sich einige Rechnungen über mehrere Seiten erstrecken, macht die Angabe von Zwischensumme und Übertrag Sinn. Die Zwischensumme platzieren Sie im Bereich Seitenfuß des Berichts (siehe Abbildung 6.44). Zur Ermittlung der laufenden Summe müssen Sie zunächst ein zusätzliches unsichtbares Feld im Detailbereich anlegen. Dieses erhält den Namen txtLaufendeSumme und hat als Steuerelementinhalt den Wert =BruttoPreis. Damit die laufende Summe nur im Rahmen der aktuellen Rechnung ermittelt wird, stellen Sie die Eigenschaft Laufende Summe auf Über Gruppe ein. Dieses Feld enthält jeweils den Inhalt des gleichen Feldes aus dem vorherigen Datensatz zuzüglich des Inhalts des Feldes Bruttopreis des aktuellen Datensatzes. Das Feld txtZwischensumme im Seitenfuß des Berichts enthält den Inhalt des Feldes txtLaufendeSumme, welcher am Seitenende dem Wert des untersten angezeigten Datensatzes entspricht.
Abbildung 6.44: Entwurfsansicht des Seitenfußes mit der laufenden Summe
6.9.7 Überschriften für Folgeseiten und Rechnungsübertrag Fehlen noch die Feldüberschriften für die Folgeseiten in Rechnungen, die über mehr als eine Seite gehen. Diese platzieren Sie genau wie das Feld zur Anzeige des
413
Kapitel 6
Rechnungsübertrags im Seitenkopf des Berichts (siehe Abbildung 6.45). Das Feld zur Anzeige des Übertrags heißt txtUebertrag und ist ungebunden. Damit es immer den Wert anzeigt, den das Feld txtZwischensumme auf der vorherigen Seite hatte, setzen Sie eine Ereigniseigenschaft ein, die direkt nach der Ermittlung der Zwischensumme ausgelöst wird und das Feld txtUebertrag mit dem aktuellen Wert des Feldes txtZwischensumme füllt. Die entsprechende Routine sieht folgendermaßen aus: Private Sub Seitenfußbereich_Print(Cancel As Integer, _ PrintCount As Integer) Me!txtUebertrag = Me!txtZwischensumme End Sub Listing 6.10: Routine zum Aktualisieren des Übertrags
Abbildung 6.45: Der Seitenkopf in der Entwurfsansicht
6.9.8 Rechnungsentwurf im Zusammenhang und Restarbeiten Wenn Sie den Rechnungsbericht, dessen Entwurf Sie in Abbildung 6.46 im Überblick sehen, nun öffnen, werden Sie noch kleinere Schönheitsfehler entdecken. Es beginnt nicht jede Rechnung auf einer neuen Seite, der Seitenkopfbereich erscheint auch auf der ersten Seite einer Bestellung mit dem Gruppenkopf und der Seitenfuß erscheint auch auf der letzten Seite, wo er eigentlich nicht sichtbar sein sollte. Diese Ungereimtheiten räumen Sie jetzt nacheinander aus.
Bereiche auf neuer Seite anzeigen In der aktuellen Fassung beginnt nicht jede Rechnung auf einer neuen Seite, die Rechnungen werden einfach nacheinander weggedruckt. Um die Anzeige jeder Rechnung auf einer neuen Seite zu erreichen, ist nur eine kleine Änderung vonnöten: Stellen Sie die Eigenschaft Neue Seite des Kopfbereichs der Gruppierung BestellungID auf den Wert Vor Bereich ein. Dadurch wird vor jedem Gruppenkopf ein Seitenumbruch eingefügt.
414
Berichte
Abbildung 6.46: Entwurf der Rechnung im Überblick
Seitenkopf und Seitenfuß nur auf bestimmten Seiten anzeigen Nun wenden Sie sich dem Seitenkopf und dem Seitenfuß zu. Der Seitenkopf erscheint dummerweise auch auf der ersten Seite über dem Briefkopf und der Seitenfuß soll nicht auf Seiten angezeigt werden, die den Gruppenfuß enthalten. Würden Sie hier die weiter oben angesprochene Variante des Rechnungsberichts verwenden, bei der jede Rechnung in einem einzelnen Bericht angezeigt wird und sich der Briefkopf mit den allgemeinen Rechnungsdaten im Berichtskopf statt im hier verwendeten Gruppenkopf befindet, wäre das Problem leicht zu lösen: Sie würden dann einfach die Eigenschaft Seitenkopf des Berichts auf den Wert Außer Berichtskopf einstellen (siehe Abbildung 6.47).
415
Kapitel 6
Abbildung 6.47: Seitenkopf nicht mit dem Berichtskopf auf einer Seite anzeigen
Nachdem Sie nun wissen, wie dies normalerweise funktionieren würde, kommen Sie nun zur anspruchsvolleren Variante: Und da ist schon ein wenig Gehirnschmalz notwendig. Die wichtigste Information, die Sie zum Ein- beziehungsweise Ausblenden von Berichtsbereichen haben müssen, ist folgende: Es funktioniert nicht zuverlässig, wenn Sie den Bereich mit der Visible-Eigenschaft sichtbar oder unsichtbar machen. Auf der sicheren Seite sind Sie, wenn Sie die Cancel-Eigenschaft der Beim Formatieren-Eigenschaft des jeweiligen Bereichs auf True setzen, um die Anzeige des Bereichs zu unterbinden. Ob einer der beiden Bereiche Seitenkopf oder Seitenfuß angezeigt werden soll, entscheidet sich freilich nicht in der jeweiligen Beim Formatieren-Eigenschaft, sondern in anderen Ereignisprozeduren. Nun der Reihe nach: Das folgende Listing enthält das komplette Klassenmodul des Berichts. Die Ereignisprozeduren sind nach der Reihenfolge ihres Auftretens geordnet, wobei das Beim Öffnen-Ereignis des Berichts nur einmal ausgelöst wird. Um festzulegen, ob die Anzeige von Seitenfuss oder Seitenkopf unterbunden werden soll, verwenden Sie zwei Boolean-Variablen namens bolCancelSeitenfuss und bolCancelSeitenkopf. Beim Öffnen des Berichts wird das Ereignis Beim Öffnen ausgelöst. Die erste Seite des Berichts enthält logischerweise die erste Seite einer Rechnung. Daher wird hier auf jeden Fall der Gruppenkopf der Rechnung angezeigt; der Seitenkopf soll dann nicht erscheinen – die Variable bolCancelSeitenkopf wird auf True eingestellt. Vor dem Anzeigen des Gruppenkopfs wird dessen Beim Drucken-Ereignis ausgelöst. Hier wird die Variable bolCancelSeitenfuss prophylaktisch auf False eingestellt. Das kann sich allerdings schnell ändern, wenn die Gruppierung so wenige Datensätze enthält, dass der Gruppenkopf noch auf der gleichen Seite angezeigt wird. Das dann ausgelöste Ereignis Beim Drucken des Gruppenfußes stellt die Variable bolCancelSeitenfuss dann auf True ein. Damit steht auch fest, dass auf der nächsten Seite eine neue Rechnung beginnt
416
Berichte
– der Seitenkopf soll also wieder dem Gruppenkopf weichen: Dazu erhält die Variable bolCancelSeitenkopf ebenfalls den Wert True. Geht es dann an das Formatieren des Seitenfußes, prüft das Ereignis Beim Formatieren den Wert bolCancelSeitenfuss und weist diesen dem Cancel-Parameter zu. Ist dieser True, wird der Bereich nicht angezeigt. Soll der Bereich doch angezeigt werden, wird nach dem Beim Formatieren-Ereignis auch noch das Beim Drucken-Ereignis ausgelöst. In diesem stellen Sie dann direkt die Variable bolCancelSeitenkopf auf den Wert False ein, denn wenn noch nicht die letzte Seite der Rechnung erreicht ist, soll auf der Folgeseite auf jeden Fall der Seitenkopf angezeigt werden. Dim bolCancelSeitenfuss As Boolean Dim bolCancelSeitenkopf As Boolean Private Sub Report_Open(Cancel As Integer) 'auf erster Seite wird auf jeden Fall der Gruppenkopf sichtbar, 'also Seitenkopf ausblenden bolCancelSeitenkopf = True End Sub Private Sub Gruppenkopf0_Print(Cancel As Integer, PrintCount As Integer) 'Seitenfuß soll erstmal nicht ausgeblendet werden... bolCancelSeitenfuss = False End Sub Private Sub Gruppenfuß1_Print(Cancel As Integer, PrintCount As Integer) '... außer, der Gruppenfuß wird angezeigt. 'Dann gibt es keinen Seitenfuß. bolCancelSeitenfuss = True 'Und wenn der Gruppenfuß angezeigt wird, kommt auf der nächsten Seite 'eine neue Rechnung, also soll auch der Seitenkopf nicht angezeigt 'werden. bolCancelSeitenkopf = True End Sub Private Sub Seitenfußbereich_Format(Cancel As Integer, _ FormatCount As Integer) 'Abbrechen, wenn bolCancelSeitenfuss True ist Cancel = bolCancelSeitenfuss Me!txtUebertrag = Me!txtZwischensumme End Sub Private Sub Seitenfußbereich_Print(Cancel As Integer, _ PrintCount As Integer) 'Wenn Seitenfuß, dann auf jeden Fall Seitenkopf auf nächster Seite bolCancelSeitenkopf = False End Sub
417
Kapitel 6 Private Sub Seitenkopfbereich_Format(Cancel As Integer, _ FormatCount As Integer) 'Abbrechen, wenn bolCancelSeitenkopf True ist Cancel = bolCancelSeitenkopf End Sub Listing 6.11: Inhalt des Klassenmoduls des Berichts rptRechnungen
Auf diese Weise zeigt der Bericht auch Rechnungen über zwei oder mehr Seiten mit Zwischensumme, Übertrag und Gesamtsumme an (siehe Abbildung 6.48).
Abbildung 6.48: Mehrseitiger Rechnungsbericht
Rechnungen stellen ist mehr als Berichte drucken … Zum Schluss darf der Hinweis nicht fehlen, dass Sie Rechnungen unbedingt dokumen tieren sollten – am besten, indem Sie Rechnungen und die enthaltenen Positionen in separaten Tabellen sichern. Das ist unumgänglich, wenn Sie die Rechnung später noch einmal ausdrucken möchten – ansonsten laufen Sie Gefahr, dass die Rechnung einen völlig anderen Betrag enthält, weil sich beispielsweise in der Zwischenzeit die Mehr wertsteuer erhöht hat.
418
Berichte
6.10 Die Berichtsansicht Neben der Layoutansicht, die neben der Entwurfsansicht zum Entwerfen von Berichten gedacht ist und hier einige sehr nützliche neue Funktionen liefert, gibt es noch eine weitere neue Ansicht, die die Ausgabe von Berichten um einige interaktive Features erweitert. Sie können zum Beispiel ein Beim Klicken-Ereignis mit einer entsprechenden Prozedur für ein Textfeld im Bericht hinterlegen. Wenn Sie den Bericht dann ausdrucken und mit dem Finger auf den Text drücken, öffnet sich etwa ein Formular zum Bearbeiten des aktuellen Datensatzes. Uups! Nein, da ist der Autor mit den vielen Berichtsansichten durcheinandergekommen. Natürlich funktionieren diese Features nur bei der Anzeige auf dem Monitor ... Was hat es nun tatsächlich mit der neuen Berichtsansicht auf sich, die auch noch Berichtsansicht heißt? Grundsätzlich handelt es sich dabei um eine Ansicht ähnlich der Seitenansicht, nur mit erheblich mehr Möglichkeiten. So können Sie beispielsweise eine ganze Menge Ereigniseigenschaften verwenden, die Sie bisher nur in Formularen einsetzen konnten. Die folgenden Ereignisse werden allerdings nicht nur durch die Berichts-, sondern auch durch die Layoutansicht unterstützt: Beim Anzeigen: wird beim Wechseln des aktiven Datensatzes durch einen Klick auf diesen ausgelöst Beim Laden Beim Klicken Bei Fokuserhalt Bei Fokusverlust Beim Doppelklicken Diverse Maus- und Tastaturereignisse Bei Größenänderung Bei Entladen Bei Filter Bei angewendetem Filter Bei Zeitgeber Auch die einzelnen Steuerelemente bringen einige neue Ereignisse mit, die für den Einsatz in dieser Ansicht vorgesehen sind – diese sollen aber hier aus Platzgründen nicht alle aufgelistet werden. Werfen Sie doch einfach einen Blick in das Eigenschaftenfenster der einzelnen Steuerelemente!
419
Kapitel 6
Wozu noch eine Ansicht? Viel interessanter ist aber, was man auch ohne den Einsatz von Ereignissen mit dieser Ansicht alles anfangen kann: Filtern: Die Berichtsansicht bietet die gleichen Filter-Möglichkeiten wie die Layout ansicht. Das heißt, dass Sie das Kontextmenü des Feldes aktivieren, dessen Inhalt Sie filtern möchten, und dann die passenden Filterkriterien zusammenstellen. Suchen: Sie können mit Strg + F den Suchen-Dialog öffnen und den Inhalt der Felder des Berichts nach dem gewünschten Begriff durchsuchen. Klick-Ereignisse: Sie können Steuerelemente mit einer Ereignisprozedur ausstatten, die Sie durch Klicken oder Doppelklicken auf das Steuerelement auslösen. Damit können Sie beispielsweise ein Formular zu dem aktuellen Datensatz öffnen, die Daten bearbeiten und die überarbeiteten Daten in der aktualisierten Berichtsansicht anzeigen. Wie das genau funktioniert, erfahren Sie weiter unten in einem Beispiel. Feldinhalte als Hyperlink formatieren: Mit der Eigenschaft Als Hyperlink anzeigen (VBA: DisplayAsHyperlink, Werte: acDisplayAsHyperlinkAlways (1), acDisplayAsHyperlinkIfHlink (0), acDisplayAsHyperlinkOnScreenOnly (2)) legen Sie fest, ob der Inhalt eines Feldes als Hyperlink formatiert wird. Beim Überfahren des Textes mit der Maus erscheint dann die typische Hyperlink-Hand als Mauszeiger.
6.11 Anwendungsbeispiel für die Berichtsansicht Dem Autor dieses Buchs erfüllt die Berichtsansicht einen großen Wunsch, und wahrscheinlich werden auch viele andere Entwickler dieses Feature begrüßen: Und das nicht unbedingt, weil es Berichte mit Formularfunktionen versieht, sondern weil es eine bisher fehlende Formular-Funktion nachliefert: Nämlich das Verwenden von Unterformularen in der Endlosansicht. Wie schön wäre das Entwicklerleben, wenn man in einem Formular mehrere Datensätze in der Endlosansicht und gleichzeitig mit diesen Datensätzen verknüpfte Daten anzeigen könnte! Und nun baut Microsoft eine neue Berichtsansicht und liefert genau dieses Feature. Um zu verstehen, was gemeint ist, schauen Sie sich doch einmal Abbildung 6.49 an. Sie können damit etwa Projekte und alle in Zusammenhang mit den einzelnen Projekten angefallenen Tätigkeiten auflisten. Das ging vorher nur in der Vorschauansicht von Be richten oder mit zusätzlichen Tools wie Microsoft InfoPath. Nun ist das zwar auch ein Bericht, aber er wird nicht in Form einzelner Seiten, sondern in der Endlosansicht angezeigt und Sie können ihn in einem gewissen Rahmen automatisieren, um beispielsweise Projektzeiten hinzuzufügen, die vorhandenen Datensätze zu bearbeiten oder zu löschen. Das funktioniert zwar nicht direkt in der Berichtsansicht (die Inhalte von gebundenen
420
Berichte
Textfeldern lassen sich nicht bearbeiten, nur von ungebundenen), aber Sie können ja zur Bearbeitung auf ein Formular zurückgreifen.
Abbildung 6.49: Die Projektübersicht in der Berichtsansicht
Erst einmal schauen Sie sich aber in Abbildung 6.50 an, wie der obige Bericht in der Entwurfsansicht aussieht. Hier finden Sie etwa den gleichen Aufbau vor, wie ihn auch ein herkömmlicher Bericht zur Anzeige von Daten in einer 1:n-Beziehung liefern würde – mit Ausnahme der drei Schaltflächen. Die Schaltfläche im Gruppenkopf der Projekte heißt cmdNeueProjektzeit und löst beim Klicken die folgende Routine aus: Private Sub cmdNeueProjektzeit_Click() DoCmd.OpenForm "frmProjektzeiten", WindowMode:=acDialog, _ DataMode:=acFormAdd, OpenArgs:=Me.ProjektID Me.Requery End Sub Listing 6.12: Öffnen eines Formulars zum Anlegen einer neuen Projektzeit
421
Kapitel 6
Abbildung 6.50: Entwurfsansicht des Berichts, der in der Berichtsansicht mit vielen Funktionen glänzen soll
Die Routine öffnet ein Formular namens frmProjektzeiten, das im Entwurf wie in Ab bildung 6.51 aussieht. Es ist an die Tabelle tblProjektzeiten gebunden und enthält zwei Ereignisprozeduren. Die erste löst das Formular beim Öffnen aus und prüft damit, ob die aufrufende Routine ein Öffnungsargument mitgeliefert hat. Falls ja, schreibt das For mular diese als Standardwert in das Feld ProjektID.
Abbildung 6.51: Das Formular zum Bearbeiten von Projektzeiten in der Entwurfsansicht
422
Berichte Private Sub Form_Open(Cancel As Integer) If Not IsNull(Me.OpenArgs) Then Me.ProjektID.DefaultValue = Me.OpenArgs End If End Sub Listing 6.13: Eintragen eines Öffnungsarguments als Standardwert eines Steuerelements
Die zweite Routine wird durch die OK-Schaltfläche ausgelöst und schließt das Formular: Private Sub cmdOK_Click() DoCmd.Close acForm, Me.Name End Sub Listing 6.14: Schließen des Formulars
Die beiden Schaltflächen cmdBearbeiten und cmdLoeschen im Detailbereich des Berichts lösen ebenfalls Ereignisprozeduren aus. Diese sehen wie folgt aus: Private Sub cmdBearbeiten_Click() If Not IsNull(Me.ProjektzeitID) Then DoCmd.OpenForm "frmProjektzeiten", WindowMode:=acDialog, _ DataMode:=acFormEdit, _ WhereCondition:="ProjektzeitID = " & Me.ProjektzeitID Me.Requery End If End Sub Private Sub cmdLoeschen_Click() If Not IsNull(Me.ProjektzeitID) Then CurrentDb.Execute "DELETE FROM tblProjektzeiten " _ & "WHERE ProjektzeitID = " & Me.ProjektzeitID Me.Requery End If End Sub Listing 6.15: Routinen zum Bearbeiten und Löschen vorhandener Projektzeiten
Der Clou ist, dass die Berichtsansicht direkt nach dem Ändern der Daten per Formular aktualisiert werden kann. Leider ist die Berichtsansicht keinesfalls perfekt: So unterstützt sie leider keine Ereignisse, die man zum dynamischen Formatieren der Inhalte verwenden könnte: So brauchen Sie gar nicht erst Prozeduren für Ereignisse wie Beim Formatieren oder Beim Drucken anzulegen, wenn Sie damit in der Berichtsansicht etwas bewirken wollen. Leider kann man dadurch auch keine Bereiche ausblenden, wenn diese keine Daten enthalten. In diesem Fall kann es ja durchaus Projekte geben, für die noch keine Projektzeit angelegt
423
Kapitel 6
wurde. Die Berichtsansicht zeigt dann einen – bis auf die beiden Schaltflächen – leeren Detailbereich an. Deshalb prüfen die obigen Listings auch, ob der aktuelle Detailbereich überhaupt einen Datensatz enthält, bevor das Formular zum Bearbeiten geöffnet oder der Datensatz gelöscht wird.
424
7 VBA VBA ist als Basic-Dialekt eine strukturierte und sehr leicht lesbare sowie gut verständliche Programmiersprache. Das hat Vor- und Nachteile: Es ist sehr einfach, etwas in Basic zu programmieren – selbst Programmiereinsteiger bringen hier schnell erste Resultate zustande. Das verleitet natürlich dazu, einfach drauflos zu programmieren – man benötigt nur eine Prozedur und schreibt dort alles hinein, was die aktuelle Aufgabe erfordert. Auf diese Weise entstehen schnell endlos lange Prozedu ren (so genannter Spaghetti-Code), bei denen man in den letzten Zeilen schon nicht mehr weiß, welche Variablen man zu Beginn der Prozedur deklariert hat, und in denen es vielleicht sogar noch Sprungmarken gibt, zwischen denen munter hin- und hergesprungen wird. Wer seine Aufgaben mit solchen oder ähnlichen Prozeduren löst, hat aber eines möglicherweise bereits geschafft: Er kennt sich ein wenig mit der Sprache aus. Das ist auch die Voraussetzung für dieses Kapitel: Es bietet keine grundlegende Einführung in die Sprache VBA, sondern setzt grundlegende Kenntnisse voraus. Sie sollten also schon wissen, wie eine Prozedur aufgebaut ist, wie Sie Variablen deklarieren, wie Sie diesen Variablen Werte zuweisen und wie die Konstrukte zum Verzweigen und zur Realisierung von Schleifen aussehen. Dieses Kapitel soll vielmehr dabei helfen, die Grundkenntnisse ein wenig auszubauen, und vermitteln, wie Sie Code so strukturiert aufbauen, dass dieser leicht verständlich und damit leicht zu pflegen und zu erweitern ist.
Kapitel 7
Die Beispiele dieses Kapitels finden Sie auf der Buch-CD in der Datenbankdatei \Kap_07\VBA.accdb.
7.1 VBA-Neuigkeiten in Access 2007 Access 2007 bringt zwar eine neue VBA-Version mit (von Version 6.4 auf Version 6.5); es gibt aber keine Änderungen am Sprachumfang der VBA-Bibliothek, sondern lediglich Anpassungen der Oberfläche an die neue Office-Version. Um dies zu präzisieren: DieVBA-Bibliothek liefert das Grundgerüst für die VBA-Programmierung wie etwa die Anweisungen zum Deklarieren von Variablen, Elemente wie Schleifen oder Bedingun gen sowie einige grundlegende Funktionen wie Zeichenkettenfunktionen, mathematische Funktionen oder Datentypkonvertierungen. Access 2007 liefert zwar Neues im VBA-Bereich, die betroffenen Methoden, Eigenschaften und Ereignisse beziehen sich aber auf andere Bibliotheken wie die Access-Bibliothek oder DAO. Einen Überblick über die Neuerungen finden Sie in Kapitel 1, »Access 2007: Neuigkeiten und Benutzeroberfläche«, dort finden Sie auch Verweise auf die Kapitel, in denen die Neuerungen behandelt werden. Eine der wenigen Neuerungen betrifft den VBA-Editor: Microsoft hat es nämlich endlich geschafft, das Scrollen des Codefenster-Inhalts mit der Maus zu implementieren. Die Installation zusätzlicher Tools fällt damit für die Zukunft weg.
7.2 Namenskonventionen in VBA Routinen und Variablen sind die Elemente in VBA, denen der Programmierer nach eigenem Ermessen Namen zuteilen darf. Stopp: Ganz so beliebig geht es hier doch nicht zu. Immerhin gibt es ein paar Konventionen, die bei VBA zu berücksichtigen sind – man darf bestimmte Zeichen nicht verwenden und es dürfen keine Zahlen am Anfang eines Routinen- oder Variablennamens stehen. Um nicht alle Ausnahmen aufzuzählen: Verwenden Sie nur Zahlen, Buchstaben und den Unterstrich (_) und beginnen Sie den Variablennamen mit einem Buchstaben, dann sind Sie auf der sicheren Seite (das gilt übrigens auch für Access-Objekte wie Tabellen, Abfragen und so weiter). Auch bei der Wahl der Namen sollten Sie bestimmte Regelmäßigkeiten einhalten. Das hilft Ihnen zum einen, wenn Sie mal aus dem Kopf ein paar weiter oben in der Prozedur
426
VBA
deklarierte Variablennamen abrufen möchten, zum anderen ist es nützlich, wenn Sie aus dem Namen einer Variable oder einer Routine ableiten können, was diese enthält beziehungsweise bewirkt. Bei Variablen wäre es zudem interessant, nicht nur die Art des Inhalts, sondern auch den Variablentyp aus dem Namen ableiten zu können – spätestens hier stoßen Sie dann auf die ungarische Notation. Diese stammt von Gregory Reddick und liefert einen Vorschlag für eine Konvention zur Benennung von Variablen. Die komplette Konvention finden Sie im Internet unter [1]. Nach dieser Konvention wird ein Variablenname wie folgt aufgebaut: [prefixes]tag[BaseName[Suffixes]]
Dabei legt Basename den eigentlichen Inhalt der Variablen fest und tag ein Präfix, das den Typ der Variablen festsetzt (beispielsweise str für String, obj für Objekte, frm für Formulare). Der erste Teil prefixes enthält spezielle Informationen wie m für private Va riablen (member) oder g für öffentliche Variablen (global). Der Teil Suffixes enthält nähere Informationen zum Inhalt der Variablen wie Min oder Max für Extremwerte. Die ungarische Notation ist quasi Standard bei der Benennung von Variablen- und Objektnamen. Fast jeder verwendet mehr oder weniger bewusst die hier festgelegten Regeln – manch einer übernimmt vermutlich intuitiv diese Konvention aus Codebeispielen. Hier und da gibt es sicher Variationen (auch in diesem Buch), aber im Großen und Ganzen hilft diese Notation ganzen Heerscharen von Programmierern, zumindest die Variablen- und Ob jektnamen ihrer Kollegen zu verstehen.
Keine reservierten Wörter! Wenn Sie keine Präfixe verwenden, sollten Sie zumindest darauf achten, dass Sie keine reservierten Wörter als Variablennamen verwenden – Begriffe wie Name, Field, Links etc. machen früher (wenn der Kompiler sich meldet) oder später Ärger.
7.3 Layout von Code VBA-Code muss gut lesbar und verständlich sein, damit Sie oder andere ihn leicht warten oder erweitern können. Dazu gehört nicht nur die eigentliche Struktur des Codes, sondern auch seine Darstellung oder das Layout. Man könnte hier nun einige Negativbeispiele anführen, aber dafür ist der Platz zu schade. Daher direkt einige Hin weise, die eine Grundlage für die gute Lesbarkeit des Codes liefern.
7.3.1 Funktionalität vor Schönheit? Manch ein Programmierer mag nun denken: Eine Routine muss funktionieren und nicht schön aussehen. Tatsache ist aber: Eine Routine muss nicht nur funktionieren, sondern
427
Kapitel 7
sie soll auch mal gelesen, geändert oder debugged werden, und wenn das dann ein anderer als der Urheber mit seinem geringen ästhetischen Empfinden machen muss, hat dieser sicher nicht gerade helle Freude daran. Tatsache ist, dass schon die Einrückungen bestimmter Teile des Codes diesen sehr viel besser lesbar machen. Allein das Einrücken des eigentlichen Inhalts von Routinen gegenüber der Routinendeklaration und der End-Zeile hilft beim schnellen Durchscrollen, das Ende der einen und den Beginn der nächsten Routine zu finden. Genauso verhält es sich mit Kontrollstrukturen wie Verzweigungen, Schleifen oder auch nur dem With-Statement.
7.3.2 Code einrücken zur Verdeutlichung der logischen Struktur Wenn Sie Code einrücken, verwenden Sie dazu am besten die Tabulator-Taste. Im Op tionen-Dialog der VBA-Entwicklungsumgebung können Sie Ihrem Geschmack entsprechend festlegen, wie viele Leerzeilen die Schrittweite des Tabulators umfassen soll (siehe Abbildung 7.1).
Abbildung 7.1: Optionen-Dialog der VBA-Entwicklungsumgebung
Typische Beispiele für die Einrückung sehen wie folgt aus: Public Sub DatenLesen() Dim db As DAO.Database Dim rst As DAO.Recordset On Error GoTo DatenLesen_Err Set db = CurrentDb Set rst = db.OpenRecordset("tblProjekte", dbOpenDynaset) Do While Not rst.EOF Debug.Print rst!Projekt
428
VBA rst.MoveNext Loop DatenLesen_Exit: On Error Resume Next rst.Close Set rst = Nothing Set db = Nothing DatenLesen_Err: MsgBox "Es ist ein Fehler aufgetreten!" Resume DatenLesen_Exit End Sub Listing 7.1: Beispiele für Einrückungen
Zunächst einmal sind alle Anweisungen außer der ersten und der letzten Zeile der Routine um einen Tabulatorschritt eingerückt. Innerhalb der Do While-Schleife erfolgt eine weitere Einrückung. Die Sprungpunkte DatenLesen_Exit und DatenLesen_Err wiederum werden automatisch an den linken Rand gesetzt, damit man diese leicht identifizieren kann. Der geübte VBA-Programmierer sieht hier auf einen Blick: Der eigentliche Kern dieser Routine befindet sich zwischen der ersten Zeile und der ersten Sprungmarke DatenLesen_Exit. Andere Beispiele für Einrückungen sind (neben vielen anderen): For Next-Schleife: For i = 1 to 10 'Inhalt der Schleife Next i
If Then…Else-Verzweigung If a=b then 'Inhalt der Verzweigung End If
Select Case-Verzweigung Select Case str Case "a" 'Inhalt des Zweiges Case "b" 'Inhalt des Zweiges End Select
Umbrochene Zeilen MsgBox "Es ist schön, wenn man nicht scrollen muss, " _ & "um eine Zeile komplett zu lesen.", _ vbOkOnly + vbExclamation, "Sinnfreie Meldung"
429
Kapitel 7
Wenn Sie einmal mit dem Code anderer Entwickler oder selbst geschriebenem Code arbeiten müssen, der nicht sauber formatiert ist, sollten Sie dies nachholen – aber nicht von Hand. Es gibt praktische Tools, mit denen sich VBA-Code automatisch in eine ansprechende Form bringen lässt. Ein Beispiel ist die Freeware SmartIndenter, die Sie unter [2] finden. Diese formatiert nicht stur nach Schema, sondern bietet auch einige Einstel lungsmöglichkeiten zum Formatieren des Quellcodes.
7.3.3 Leerzeilen für bessere Lesbarkeit Neben Einrückungen sind Leerzeilen ein sinnvolles Mittel zur Verbesserung der Lesbar keit des Codes. Das Beispiel aus Listing 7.1 wäre beispielsweise viel leichter zu lesen, wenn Sie durch Leerzeichen für eine Gruppierung zusammenhängender Codezeilen sorgen. Dies könnte so aussehen: Public Sub DatenLesen() Dim db As DAO.Database Dim rst As DAO.Recordset On Error GoTo DatenLesen_Err Set db = CurrentDb Set rst = db.OpenRecordset("tblProjekte", dbOpenDynaset) Do While Not rst.EOF Debug.Print rst!Projekt rst.MoveNext Loop DatenLesen_Exit: On Error Resume Next rst.Close Set rst = Nothing Set db = Nothing DatenLesen_Err: MsgBox "Es ist ein Fehler aufgetreten!" Resume DatenLesen_Exit End Sub Listing 7.2: Optisches Strukturieren des Codes mit Leerzeilen
Mit den hier eingefügten Leerzeilen wächst die Lesbarkeit deutlich. Die Deklarationszei len werden mit dem Prozedurkopf zusammengefasst und einige weitere Blöcke werden
430
VBA
ebenfalls sinnvoll gruppiert. Wichtig ist bei der Verwendung von Leerzeilen, dass Sie diese nicht einsetzen, um Zeilen voneinander zu trennen, sondern um zusammenhängende Anweisungen zu gruppieren.
7.3.4 Zeilenumbrüche Extrem lange Codezeilen sind im Quellcode eher selten, aber wenn sie dennoch auftauchen, sollten Sie diese umbrechen. So ersparen Sie dem Leser des Codes unnötiges Scrollen. Paradebeispiel für lange Zeilen sind die Deklarationen von API-Funktionen. Ob man diese nun im Detail lesen muss, ist eine andere Frage, das Umbrechen schadet jedenfalls nicht. Wie weiter oben bereits erwähnt, sollten Sie Fortsetzungen umbrochener Zeilen einrücken, um diese als solche kenntlich zu machen. In einem Fall ist sogar eine folgende Leerzeile angezeigt: Wenn der Prozedurkopf einen Zeilenumbruch erfordert, sollten Sie vor der folgenden Zeile eine Leerzeile einfügen. Die Fortsetzungszeile und die eingerückte Zeile lassen sich optisch sonst nur schwer auseinanderhalten. Es gibt zwei Varianten zum Umbrechen von Zeilen: innerhalb und außerhalb von Zeichenketten. Außerhalb einer Zeichenkette können Sie eine Zeile fast überall umbrechen, außer mitten in Schlüsselwörtern. Dazu fügen Sie an der Stelle des gewünschten Umbruchs ein Leerzeichen, einen Unterstrich und einen Zeilenumbruch ein – und vergessen Sie das Einrücken nicht. Die folgenden zwei Zeilen enthalten ein Beispiel für eine Zeile mit allen möglichen Umbrüchen: Set rst = db.OpenRecordset("tblProjekte", dbOpenDynaset)
Die extrem umbrochene Variante sieht so aus – lesbar ist der Code so natürlich nicht mehr, aber er zeigt die Möglichkeiten auf. Allerdings sind auch hier Grenzen gesetzt: Sie können eine Anweisung nicht auf mehr als 25 Zeilen aufteilen: Set _ rst _ = _ db _ . _ OpenRecordset _ ( _ "tblProjekte" _ , _ dbOpenDynaset _ )
Die zweite Variante von Zeilenumbrüchen betrifft Zeichenketten innerhalb von Anfüh rungszeichen. Diese lassen sich an mehr Stellen umbrechen, nämlich nach jedem Buch
431
Kapitel 7
staben. Allerdings sind die Regeln geringfügig umfangreicher. Nehmen Sie die folgende Zeile als Ausgangspunkt: MsgBox "Es ist ein Fehler aufgetreten!"
Der Zeilenumbruch erfolgt zum besseren Verständnis in zwei Schritten. Erst wird die Zeichenkette in zwei Teilzeichenketten aufgeteilt: MsgBox "Es ist ein " & "Fehler aufgetreten!"
Dann fügen Sie wie oben einfach den Umbruch ein: MsgBox "Es ist ein " _ & "Fehler aufgetreten!"
Ob Sie den Umbruch vor oder nach dem Und-Zeichen durchführen, bleibt Ihnen überlassen. Wenn Sie einmal einen sehr langen Text in einer String-Variablen unterbringen möchten, können Sie diesen auch in mehreren Schritten zusammensetzen: Dim str As String str = "Erster Teil" str = str & "Zweiter Teil"
7.3.5 Anweisungen zusammenfassen Es gibt in VBA verschiedene Möglichkeiten, Anweisungen in einer Zeile zusammenzufassen. So können Sie beispielsweise die If Then-Anweisung ohne Else-Teil in eine Zeile schreiben. Ausgangspunkt ist die folgende Zeile: If a = 1 Then Debug.Print "A ist gleich 1" End If
Daraus wird diese Variante: If a = 1 Then Debug.Print "A ist gleich 1"
Und mehrere einzelne Anweisungen lassen sich durch einen Doppelpunkt getrennt in einer einzigen Zeile eingeben: rst.Close: Set rst = Nothing
Diese Varianten sparen zwar Zeilen ein, übersichtlicher und lesbarer machen sie den Code aber nicht unbedingt. Außerdem gibt es Probleme, wenn Sie beim Dokumentieren von Fehlern die Zeilennummer mit einbeziehen – Sie können dann nicht erkennen, welche Anweisung den Fehler ausgelöst hat (weitere Informationen zum Ermitteln der Zei lennummer fehlerhafter Zeilen finden Sie in Kapitel 13, Abschnitt 13.4.1, »Wichtige Feh lerinformationen«).
432
VBA
7.4 Kommentare Mit dem Hochkomma (') leiten Sie einen Kommentar ein. Kommentare können als komplette Zeilen oder als Anhängsel an bestehende Codezeilen eingesetzt werden. Seltener zum Einsatz kommt das Schlüsselwort Rem, das allerdings grundsätzlich am Anfang einer Zeile stehen muss. Wenn Sie Kommentare in einer Routine verwenden, beziehen sich diese meist auf eine einzelne Zeile oder eine Gruppe von Zeilen. Setzen Sie den Kommentar direkt über die betroffenen Zeilen und rücken Sie den Kommentar genauso weit wie die kommentierte Zeile ein. Auf diese Weise machen Sie die Zugehörigkeit gut kenntlich: Public Sub DatenLesen() … 'Alle Datensätze durchlaufen und Projekte ausgeben Do While Not rst.EOF Debug.Print rst!Projekt rst.MoveNext Loop … End Sub Listing 7.3: Kommentare rücken Sie am besten genauso weit wie die kommentierte Zeile ein
Kommentare wie im vorherigen Beispiel benötigen Sie normalerweise nicht. Die kommentierte Do While-Schleife verwendet jeder Access-Entwickler vermutlich täglich. Setzen Sie Kommentare nur dort ein, wo diese wirklich benötigt werden – etwa wenn Sie nicht triviale Methoden oder einen Trick verwenden, um zu einem bestimmten Ergebnis zu gelangen. Ein Beispiel für Kommentare im Anschluss an Zeilen ist der Deklarationsbereich. In vielen Fällen würde das Unterbringen aller benötigten Informa tionen zu lange Variablennamen erfordern – etwa wenn der Variableninhalt eine bestimmte Einheit hat: Dim sngLaenge As Single 'Länge in Meter [m]
Weitere sinnvolle Einsatzmöglichkeiten sind folgende: Hinweise auf noch zu bearbeitende Code-Bereiche: 'ToDo: …
Auskommentieren von Zeilen, etwa um eine alte Version nicht endgültig zu löschen, während man eine neue Variante ausprobiert Kommentierter Bereich im Kopf einer Routine, um deren Eingangs- und Ausgangs parameter, die enthaltene Funktionalität und weitere Informationen etwa zur Ände rungshistorie anzugeben
433
Kapitel 7
Das Kommentieren und Auskommentieren von Code brauchen Sie übrigens nicht zeilenweise von Hand vorzunehmen. Die Menüleiste Bearbeiten (Menüeintrag Ansicht|Sym bolleisten|Bearbeiten) liefert zwei Schaltflächen, mit denen Sie mehrere Zeilen gleichzeitig ein- und auskommentieren können (siehe Abbildung 7.2).
Abbildung 7.2: Menüleiste mit zwei Befehlen zum Kommentieren und Entkommentieren von VBA-Code
7.5 Konstanten Konstanten bieten eine Möglichkeit, Werte, die zur Laufzeit nicht verändert werden, zentral zu speichern und durch einen aussagekräftigen Ausdruck zu ersetzen. VBA und die in Access üblicherweise verwendeten Bibliotheken enthalten Hunderte, wenn nicht Tausende Konstanten. Schauen Sie sich allein die MsgBox-Anweisung an: MsgBox "Meldungstext", vbOKCancel Or vbCritical Or vbDefaultButton1
Hinter den dort verwendeten Konstanten verbergen sich völlig harmlose Zahlenwerte. Ein gutes Einsatzgebiet für Konstanten in Ihren eigenen Anwendungen sind beispielsweise Optionsgruppen, deren Wert Sie nach der Auswahl in einer Select Case-Verzweigung auswerten (siehe Abbildung 7.3).
Abbildung 7.3: Optionsgruppe zur Anzeige von Detailinformationen
Die folgende Prozedur legt für die drei Optionen aussagekräftige Konstanten fest, die in der Routine cmdAuswaehlen_Click eingesetzt werden können. Const pizKlein = 1 Const pizMittel = 2
434
VBA Const pizGross = 3 Private Sub cmdAuswaehlen_Click() Select Case Me!ogrPizzagroesse Case pizKlein MsgBox "Für den kleinen Hunger zwischendurch." Case pizMittel MsgBox "Normale Größe." Case pizGross MsgBox "Nur für Access-Entwickler und Buchautoren." End Select End Sub Listing 7.4: Einsatz benutzerdefinierter Konstanten
Ein anderes sinnvolles Einsatzgebiet von Konstanten ergibt sich, wenn Sie bestimmte Kennzahlen im Code einsetzen – das gilt erst recht, wenn diese Kennzahlen an mehr als einer Stelle vorkommen. Ein gutes Beispiel für D-Mark-Liebhaber wäre der Umrechnungsfaktor zwischen DM und Euro: Public Const EUROFAKTOR = 1.95583
Andere Beispiele, die den Code lesbarer machen, sind folgende: Public Const ANZAHL_MONATE = 12 Public Const ANZAHL_STUNDEN_PRO_TAG = 24
Auch oft verwendete Zeichenketten sollten Sie in Konstanten packen. Wenn Sie beispielsweise eine Anwendung entwickeln und sich über ihren Namen noch nicht im Klaren sind, diesen aber an mehreren Stellen ausgeben wollen, legen Sie einfach eine Konstante mit dieser Information an und verwenden Sie diese an den entsprechenden Stellen statt des Variablennamens: Public Const ANWENDUNGSNAME = "Beispieldatenbank VBA"
Sie können diese Konstante dann im Begrüßungsformular, Meldungsfenster und an anderen Orten anstatt des hart codierten Anwendungsnamens zuweisen und brauchen sie bei Bedarf nur an einer einzigen Stelle zu ändern.
Konvention für Konstanten Die ungarische Notation sieht für Konstanten die gleichen Regeln vor wie für Variablen. An den obigen Beispielen haben Sie schon erkennen können, dass diese Konvention hier keine Beachtung findet. Das ist wiederum Geschmackssache, aber aufgrund der sehr unterschiedlichen Anwendungszwecke verdienen Konstanten eine wesentlich auffälligere Notation als ein zwischengeschobenes »c«, wie in der ungarischen Notation vorgeschlagen.
435
Kapitel 7
Aufzählungstypen Aufzählungstypen sind ein probates Mittel, Konstanten für eine Variable zur Verfü gung zu stellen, deren Wertebereich bekannt und begrenzt ist. Dies ist vor allem für solche Werte interessant, die in oft genutzten Routinen als Parameter zum Einsatz kommen – die möglichen Werte werden dann durch IntelliSense angezeigt (siehe Abbildung 7.4).
Abbildung 7.4: Einsatz einer Enumeration
Damit eine Routine eine solche Liste zur Verfügung stellt, verwenden Sie eine Enumera tion und einen Funktionskopf wie im folgenden Listing: Public Enum ePizzagroesse ePizzagroesse_Klein = 1 ePizzagroesse_Mittel = 2 ePizzagroesse_Gross = 3 End Enum Public Function Teigmenge(intPizzagroesse As ePizzagroesse) As Integer Select Case intPizzagroesse Case ePizzagroesse_Klein Teigmenge = 500 Case ePizzagroesse_Mittel Teigmenge = 750 Case ePizzagroesse_Gross Teigmenge = 1250 End Select End Function Listing 7.5: Bereitstellen einer Enumeration als Parameter einer Routine
Die Mitglieder einer Enumeration werden, wenn Sie keine expliziten Werte angeben, mit dem Wert 1 beginnend durchnummeriert, als Datentyp wird standardmäßig Long angenommen.
436
VBA
7.6 Variablen In diesem Abschnitt erfahren Sie, wie Sie Variablen benennen, wie Sie diese optimal einsetzen und welche speziellen Variablentypen es gibt. Das Wichtigste vorneweg: Sie sollten unbedingt darauf achten, dass Sie ausschließlich Variablen benutzen, die Sie auch deklariert haben. VBA ermöglicht auch den anderen Weg – nämlich einfach eine Variable einzusetzen, die man nicht deklariert hat. Dann erhält diese automatisch den Datentyp Variant – und auch der Compiler meckert beim Kompilieren nicht, wenn die Variable nicht deklariert ist. Damit dies nicht passiert, fügen Sie zu Beginn eines Moduls die folgende Zeile ein: Option Explicit
Wenn Sie nun eine nicht deklarierte Variable verwenden, schlägt Access beim Kompilie ren des Moduls Alarm. Damit man nicht unter Umständen mal vergisst, diese sehr wichtige Zeile an den Anfang des Moduls zu setzen, stellt VBA eine Option bereit, mit der diese Zeile standardmäßig beim Anlegen eines neuen Moduls hinzugefügt wird: Öffnen Sie dazu den OptionenDialog (Extras|Optionen) und aktivieren Sie dort die Option Variablendeklaration erforderlich im Bereich Code-Einstellungen der Registerseite Editor.
7.6.1 Variablennamen Variablennamen sollten zunächst einmal den in Abschnitt 7.2, »Namenskonventionen in VBA«, genannten Konventionen entsprechen. Noch nicht besprochen wurde dort der Teil zwischen Präfix und Suffix, also der eigentlich wichtigste Teil. Dieser Teil kann aus einem Wort oder mehreren Wörtern bestehen. Schreiben Sie jedes neue Wort groß, damit man es als neues Wort erkennen kann, etwa intAnzahlMitarbeiter. Gestalten Sie den Variablennamen so lang wie nötig und so kurz wie möglich, allerdings ohne mit wilden und nicht nachvollziehbaren Abkürzungen zu arbeiten. So ist sngMwStSatz statt sngMehrwertsteuersatz sicher sinnvoll, aber sngMS ist ein wenig kurz und selbst im richtigen Zusammenhang schwer zu deuten. Gleichzeitig sollten Sie eine Variable so benennen, dass man unmittelbar erkennen kann, was diese Variable für einen Wert enthält. Beispiele: strSQL datAktuellesDatum curBetrag rstMitarbeiter
437
Kapitel 7
Diese Variablen weisen noch einen weiteren Vorteil auf: Sie lassen sich allesamt leicht einprägen. Das ist besonders wichtig, denn Sie wollen sicher nicht bei jeder Anwendung einer Variablen zum Deklarationsbereich scrollen, um die genaue Schreibweise dieser Variablen zu ermitteln.
Zahlen in Variablennamen Wenn Sie in einer Routine Variablennamen wie strMitarbeiter1, strMitarbeiter2 oder ähnliche finden, sollten Sie das Design des Codes überprüfen. Entweder ließe sich hier besser ein Array verwenden oder es handelt sich tatsächlich um zwei verschiedene Variablen, die aber mit wesentlich aussagekräftigeren Namen ausgestattet werden sollten.
7.6.2 Spezielle Variablennamen Es haben sich einige Variablennamen eingebürgert, die der weiter oben erwähnten Konvention widersprechen. Dennoch werden sie immer wieder benutzt – eben weil sie gängig sind.
Lauf- oder Zählervariablen Das beste Beispiel ist sicher die Variable i als Zählervariable. Wer nicht mit der Konven tion brechen möchte, mag vielleicht eine Variante wie intZaehler verwenden – das ist letzten Endes Geschmackssache. Ein alternativer Name für eine Laufvariable ist definitiv angezeigt, wenn dieser Wert beispielsweise anschließend weiter verwendet wird. Auch wenn es sich um Laufvariablen in verschachtelten Schleifen handelt, sollte man über eine andere Benennung als i und j nachdenken.
Temporäre Variablen Temporäre Variablen verdienen meist einen aussagekräftigeren Namen als temp oder tmp. Man könnte zumindest die Bezeichnung dessen, was sie beinhalten, vorne anfügen – dann hieße eine temporäre Variable beispielsweise rstMitarbeiterTemp.
Statusvariablen Wenn Sie eine Variable verwenden, um einen Status zu speichern, sollten Sie auch dieser einen brauchbaren Namen geben.Viele Entwickler nennen solche Variablen lieblos »Flag«. Spätestens, wenn Sie mal zwei »Flags« in einer Routine oder in einem Modul benötigen, müssen Sie sich zwei unterschiedliche Namen ausdenken – und dann tun Sie sich selbst den Gefallen und nennen diese nicht »Flag1« und »Flag2«.
438
VBA
7.6.3 Arrays Arrays sind Datenfelder zum Speichern mehrerer Daten gleichen Datentyps. Diese Da tenfelder können auch mehrdimensional ausgelegt werden. Sie deklarieren ein Datenfeld entweder direkt mit der gewünschten Anzahl möglicher Werte oder lassen diesen Parameter offen: Dim strVornamen() As String
oder Dim strVornamen(10) As String
In beiden Fällen können Sie die Anzahl später noch mit der ReDim-Anweisung ändern. Diese gibt es in zwei Ausführungen: Ohne das Schlüsselwort Preserve werden alle enthaltenen Daten gelöscht, mit diesem Schlüsselwort behält das Array die vorhandenen Daten bei, was normalerweise gewünscht sein dürfte: ReDim Preserve strVornamen(15) As String
Arrays beginnen standardmäßig mit dem Index 0. Wenn ein anderer Index gewünscht ist, wobei als einzige Alternative 1 erlaubt ist, verwenden Sie folgende Anweisung im Kopf des Moduls: Option Base 1
Viele Fehler treten dadurch auf, dass auf Array-Elemente zugegriffen wird, die nicht vorhanden sind. Das können Sie – etwa in einer Schleife – mit den beiden Funktionen UBound und LBound verhindern. Diese Funktionen liefern den obersten und den unters ten Index eines Arrays zurück. Folgendes Beispiel fasst die wichtigsten Funktionen von Arrays zusammen: Option Base 1 Public Sub BeispielArray() Dim Dim Dim Dim
strName() As String intUntereGrenze As Integer intObereGrenze As Integer i As Integer
Kapitel 7 'Feld für Nachzügler erweitern ReDim Preserve strName(4) strName(4) = "Sylvia" intUntereGrenze = LBound(strName) intObereGrenze = UBound(strName) For i = intUntereGrenze To intObereGrenze Debug.Print strName(i) Next i End Sub Listing 7.6: Beispiel für die Verwendung eines Arrays
7.6.4 Benutzerdefinierte Typen Benutzerdefinierte Typen fassen ähnlich wie Arrays Daten zusammen. Allerdings bestehen sie aus einer festen Anzahl von Elementen, die dafür aber auch verschiedene Daten typen besitzen können. Sinnvoll ist die Verwendung solcher Typen, wenn Sie bestimmte Variablen immer gemeinsam etwa beim Aufruf anderer Routinen weitergeben. Im folgenden Beispiel werden Informationen zu einem Artikel beispielsweise zum Typ TArtikel zusammengefasst. Die Funktion TypFuellen füllt die einzelnen Elemente der Type-Variablen und gibt diese an eine andere Funktion weiter, die die enthaltenen Infor mationen ausliest. Public Type TArtikel ArtikelNr As Long Artikelname As String Einzelpreis As Currency End Type Public Function TypFuellen() Dim typArtikel As TArtikel With typArtikel .ArtikelNr = 1 .Artikelname = "Das Access 2003 Entwicklerbuch" .Einzelpreis = 49.9 End With TypAusgeben typArtikel End Function Public Function TypAusgeben(typArtikel As TArtikel) With typArtikel Debug.Print .ArtikelNr, .Artikelname, _
440
VBA Format(.Einzelpreis, "0.00") End With End Function Listing 7.7: Beispiel für die Verwendung benutzerdefinierter Typen
7.6.5 Alle Variablen verwenden Sorgen Sie dafür, dass alle deklarierten Variablen auch verwendet werden. Wenn Sie nicht sicher sind, etwa weil die Routine zu umfangreich ist, um dies auf einen Blick zu erkennen, können Sie wie folgt vorgehen: Kommentieren Sie alle Variablen aus und kompilieren Sie den Code so lange, bis Sie durch die entsprechenden Fehlermeldungen alle wirklich benötigten Variablen identifiziert haben. Mit dem Tool aus [3] können Sie eine Liste ausgeben, die beispielsweise auch alle nicht deklarierten, aber nicht verwendeten Variablen enthält. Die passende Funktion erhalten Sie über den Menüeintrag Andere Hilfsmittel|Übersicht Programmcode.
7.6.6 Globale Variablen Globale Variablen sind unbestritten in einigen Fällen nützlich, weil man darin Daten speichern kann, die von überall zugreifbar sind. Sie bergen allerdings auch Gefahren. Oft wird ihr Inhalt unbewusst von mehreren Stellen aus geändert. Bei nicht behandelten Fehlern irgendwo im VBA-Projekt verlieren globale Variablen außerdem in der Regel ihren Inhalt und sind dann leer. Um Risiken bei der Anwendung globaler Variablen zu vermeiden, sollten Sie diese besonders kennzeichnen – die oben beschriebene Namenskonvention schlägt vor, ein »g« voranzustellen – das scheint vernünftig zu sein. Eine alternative Technik zur Verwendung globaler Variablen finden Sie in Kapitel 15, Abschnitt 15.8.1, »Auflistungen selbst gemacht«. Grundsätzlich gilt jedoch: Verwenden Sie globale Variablen nur, wenn es keine andere Möglichkeit gibt.
7.7 Kontrollstrukturen Die Kontrollstrukturen unter VBA bieten eine Menge Möglichkeiten, um unsauber, fehlerhaft oder performancehemmend eingesetzt zu werden. In den folgenden Abschnitten lernen Sie die Strukturen kennen und erfahren, wie Sie diese sinnvoll verwenden.
7.7.1 If...Then-Anweisung Die If...Then-Anweisung dient dem Verzweigen in verschiedene Unterabschnitte aufgrund einer Bedingung. Die Erweiterung in Form einer If...Then…Else-Anweisung erlaubt auch die Verwendung mehrerer Kriterien.
441
Kapitel 7
Die einfachste Variante prüft nur eine Bedingung und führt in Abhängigkeit davon die zwischen If...Then und End If liegenden Anweisungen aus. Befindet sich dort nur eine Anweisung, können Sie die End If-Anweisung weglassen und den If Then-Teil und die auszuführende Anweisung in eine Zeile schreiben (siehe weiter oben Abschnitt 7.3.5, »Anweisungen zusammenfassen«).
Wahrscheinliche Fälle nach oben Wenn die If Then-Anweisung nicht nur eine Bedingung, sondern mehrere und damit entsprechende Else- beziehungsweise ElseIf-Abschnitte enthält, sollten Sie den am wahrscheinlichsten eintretenden Fall nach oben setzen: If bolWahrscheinlicherFall Then 'Wahrscheinlicher Fall Else 'Weniger wahrscheinlicher Fall End If
Das Gleiche gilt für If...Then-Anweisungen mit mehr als einer Else-Bedingung.
Keine leeren Zweige Oft formulieren Entwickler eine Bedingung, die so nur selten oder gar nicht eintritt. Im letzteren Fall bleibt dann die erste Verzweigung völlig leer: If bolSehrUnwahrscheinlich = True Then 'leere Verzweigung Else 'sehr wahrscheinlicher Fall End If
Wenn Sie einmal eine derartige If Then-Anweisung erstellen und erst später bemerken, dass die genannte Bedingung selten oder nie eintritt, formulieren Sie die Bedingung um oder verneinen Sie diese einfach. Alternativ können Sie auch die einzelnen Zweige vertauschen. If bolSehrUnwahrscheinlich = False Then 'sehr wahrscheinlicher Fall Else 'leere Verzweigung End If
oder If Not bolSehrUnwahrscheinlich Then 'sehr wahrscheinlicher Fall Else
442
VBA 'leere Verzweigung End If
Oder-Verknüpfungen in If- oder ElseIf-Bedingungen Im Falle von Oder-Verknüpfungen im If- oder im ElseIf-Abschnitt sollten Sie die Verwen dung einer Select Case-Anweisung in Erwägung ziehen. Diese Anweisung ist wesentlich flexibler bei der Verarbeitung von Oder-Verknüpfungen.
Alle erwarteten Fälle behandeln Bevor Sie eine If...Then-Abfrage programmieren, führen Sie sich alle denkbaren Fälle vor Augen. Legen Sie für all diese Fälle konkrete If/ElseIf-Zweige an. Die schlechtere Alternative wäre, nur die Fälle exakt zu erfassen, die Sie interessieren, und alles andere durch den ElseTeil abzufangen. Der Else-Teil behandelt dann zwar alle nicht definierten Fälle, aber dabei kann es sich durchaus um erwartete und nicht erwartete Ergebnisse handeln: Public Function Sachbearbeiter(strAnfangsbuchstabe As String) As String If strAnfangsbuchstabe Like "[a-k]" Then Sachbearbeiter = "a-k: Herr Müller" Else Sachbearbeiter = "l-z: Frau Meier" End If End Function Listing 7.8: Schlechte Auswertung mit If…Then…Else
Die Routine behandelt alle Fälle richtig, in denen ein Buchstabe als Parameter übergeben wird. Wenn Sie aber aus Versehen eine Zahl eingeben, wird auch die zweite Verzwei gung ausgeführt, was ja in diesem Falle falsch ist. Besser ist folgende Variante: Public Function Sachbearbeiter(strAnfangsbuchstabe As String) As String If strAnfangsbuchstabe Like "[a-k]" Then Sachbearbeiter = "a-k: Herr Müller" ElseIf strAnfangsbuchstabe Like "[l-z]" Then Sachbearbeiter = "l-z: Frau Meier" Else Sachbearbeiter = "Kein Sachbearbeiter gefunden" End If End Function Listing 7.9: Zuverlässige Auswertung des Anfangsbuchstabens
Hier werden die Buchstaben von a bis z ebenfalls ordnungsgemäß zugewiesen. Zufällig falsch eingegebene Zeichen werden aber ebenso richtig mit einem entsprechenden Rückgabewert beantwortet.
443
Kapitel 7
7.7.2 Select Case Die Select Case-Anweisung können Sie oft verwenden, wenn If...Then-Konstrukte zu kom pliziert werden – entweder wenn viele If/ElseIf-Anweisungen hintereinander abgearbei tet werden oder wenn die Bedingung in einzelnen Verzweigungen beispielsweise aus vielen mit Or verknüpften Ausdrücken besteht. Voraussetzung für die Verwendung einer Select Case-Anweisung ist, dass alle Verzwei gungen sich auf einen Ausdruck und dessen unterschiedliche Werte beziehen. Hier gilt das Gleiche wie für If...Then-Verzweigungen: Ordnen Sie die einzelnen CaseZweige so an, dass die wahrscheinlichsten zuerst abgearbeitet werden – das kommt der Performance zu Gute. Wenn keine Bedingung eine höhere Wahrscheinlichkeit als andere erwarten lässt, verwenden Sie eine Sortierung, die gut lesbar ist – beispielsweise nach dem Alphabet. Und die für die If...Then-Konstrukte beschriebene Regel, alle erwarteten Ergebnisse zu berücksichtigen und den Else-Zweig für eine Behandlung unerwarteter Ergebnisse zu verwenden, gilt auch für Select Case-Anweisungen. Hier heißt der für Ausnahmen verantwortliche Zweig allerdings Case Else: Public Function SachbearbeiterMitSelectCase(strAnfangsbuchstabe As _ String) As String Select Case strAnfangsbuchstabe Case "a" To "k" SachbearbeiterMitSelectCase = "a-k: Herr Müller" Case "l" To "z" SachbearbeiterMitSelectCase = "l-z: Frau Meier" Case Else SachbearbeiterMitSelectCase = "Kein Sachbearbeiter gefunden" End Select End Function Listing 7.10: Ausnahmefälle behandeln mit Select Case
Select Case ermöglicht beispielsweise die folgenden Kriterienausdrücke (wobei Sie natürlich auch Variablen und Konstanten statt konkreter Werte einsetzen können): Case Case Case Case Case Case Case
444
1 Is > 1 1, 2, 3 1 To 4 1, 3, 5 To 6 Is > "a" "a" To "z"
Case "abc"
VBA
7.7.3 For...Next-Schleifen Die For...Next-Schleife wird eingesetzt, wenn Sie vor dem ersten Durchlauf wissen, wie oft die Schleife durchlaufen werden soll. Beispiel: Sie möchten für jeden Monat eines Jahres irgendetwas veranlassen – in diesem Fall die Ausgabe des Monatsnamens: Public Sub MonateAusgeben() Dim intMonat As Integer For intMonat = 1 To 12 Debug.Print Format("1." & intMonat & ".2004", "mmmm") Next intMonat End Sub Listing 7.11: Durchlaufen der Monate eines Jahres per For Next-Schleife
Künstlicher Ausgang For...Next-Schleifen müssen nicht grundsätzlich bis zum Ende durchlaufen werden. Mit der Exit-Anweisung können Sie diese an jeder beliebigen Stelle innerhalb der Schleife stoppen. Sie müssen nur die gewünschte Bedingung festlegen: If bolAbbruchbedingung Then Exit For
Sie können eine For...Next-Schleife auch beenden, indem Sie innerhalb der Schleife den Wert der Zählervariablen so verändern, dass er größer als der höchste im Schleifenkopf angegebene Wert ist – das ist allerdings kein guter Programmierstil. Verwenden Sie statt dessen die Exit-Anweisung.
Name der Zählervariablen Versuchen Sie, einen möglichst aussagekräftigen Namen für eine Zählervariable zu verwenden. In einfachen Schleifen reicht i zwar oft aus, ein richtiger Variablenname wie intMonat in diesem Beispiel (noch besser wäre intAktuellerMonat) erhöht aber auf jeden Fall die Lesbarkeit. Richtig interessant wird es, wenn Sie verschachtelte Schleifen verwenden. Vermutlich hat jeder von Ihnen schon einmal i und j bei der Verwendung verschachtelter Schleifen vertauscht.
Große Schritte: Step Wenn Sie nicht jede Zahl im angegebenen Bereich einer For...Next-Schleife durchlaufen möchten, sondern etwa nur jede zweite, können Sie das Step-Schlüsselwort verwenden. Die folgende Variante gibt etwa die Zahlen 1, 3, 5, 7 und 9 im Direktfenster aus:
445
Kapitel 7 For i = 1 To 10 Step 2 Debug.Print i Next i
Step liefert auch die einzige Möglichkeit, eine Schleife rückwärts zu durchlaufen: For i = 10 To 1 Step -1 Debug.Print i Next i
7.7.4 For Each-Schleifen For Each-Schleifen durchlaufen alle Elemente von Auflistungen. Auch hier ist die Anzahl der Durchläufe vor dem ersten Durchlauf bekannt. Damit können Sie beispielsweise die in den unterschiedlichen Objektmodellen enthaltenen Auflistungen durchlaufen. Das folgende Beispiel gibt alle Formulare des aktuellen Projekts aus: Public Sub FormulareAusgeben() Dim frm As AccessObject For Each frm In CurrentProject.AllForms Debug.Print frm.Name Next frm End Sub Listing 7.12: Ausgabe von Auflistungselementen per For Each-Schleife
Damit lassen sich For Each-Schleifen in vielen Fällen alternativ zu For...Next-Schleifen einsetzen. Das ist immer dann der Fall, wenn eine passende Auflistung vorhanden ist. Die obige Funktionalität ließe sich auch mit einer For...Next-Schleife nachbilden: Public Sub FormulareAusgebenMitForNext() Dim intAnzahlFormulare As Integer Dim i As Integer Dim frm As AccessObject intAnzahlFormulare = CurrentProject.AllForms.Count For i = 0 To intAnzahlFormulare - 1 Debug.Print CurrentProject.AllForms(i).Name Next i End Sub Listing 7.13: Auflistung mit einer For Next-Schleife durchlaufen
446
VBA
7.7.5 Do…Loop-Schleifen und Varianten Bei Do Loop-Schleifen kennen Sie bis zum letzten Durchlauf der Schleife nicht die Anzahl der Durchläufe. Ob die Schleife weiterläuft oder nicht, entscheidet allein das Abbruch kriterium. Das bekannteste Beispiel ist vermutlich das Durchlaufen eines Recordsets mit einer Do While...Loop-Schleife: Public Sub AlleDatensaetzeAusgeben() Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb Set rst = db.OpenRecordset("Artikel", dbOpenDynaset) Do While Not rst.EOF Debug.Print rst!Artikelname rst.MoveNext Loop rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 7.14: Durchlaufen einer Schleife mit Abbruchkriterium
Es gibt vier Varianten der Do Loop-Schleife – welche Sie davon verwenden, hängt ganz vom Einsatzzweck ab: Do...Loop mit While-Abbruchbedingung in der ersten Zeile: Do While ... Loop
Do...Loop mit While-Abbruchbedingung in der letzten Zeile Do
... Loop While
Do...Loop mit Until-Abbruchbedingung in der ersten Zeile Do Until ... Loop
447
Kapitel 7
Do...Loop mit Until-Abbruchbedingung in der letzten Zeile Do
... Loop Until
Der wesentliche Unterschied zwischen diesen Varianten ist, dass diejenigen mit Ab bruchbedingung in der letzten Zeile mindestens einmal durchlaufen werden. Auch mit Do While-Schleifen lassen sich andere Schleifentypen nachbilden. Das folgende Beispiel gibt wiederum alle Formularnamen der aktuellen Datenbank aus. Auch hier ist der Programmieraufwand höher als bei der Lösung mit For Each – auch hier sollten Sie immer die For Each-Variante verwenden. Public Sub FormulareAusgebenMitDoWhile() Dim intAnzahlFormulare As Integer Dim i As Integer intAnzahlFormulare = CurrentProject.AllForms.Count Do While i < intAnzahlFormulare Debug.Print CurrentProject.AllForms(i).Name i = i + 1 Loop End Sub Listing 7.15: Schleife mit fester Anzahl Durchläufe mit Do While abarbeiten
Mit dem Exit-Schlüsselwort können Sie eine Schleife vorzeitig verlassen. Das gilt für Schleifen mit den Schlüsselwörtern Do und For; die passenden Anweisungen zum Verlassen lauten dann Exit Do und Exit For.
7.7.6 Exit Die Exit-Anweisung bietet nicht nur die Möglichkeit, vorzeitig aus den unterschiedlichen Schleifen auszusteigen. Sie können damit auch vorzeitig Prozeduren verlassen (und nicht nur Sub- und Function-, sondern auch noch Property-Prozeduren – aber die lernen Sie erst im Kapitel 15, »Objektorientiertes Programmieren«, kennen). Ein gutes Beispiel für einen solchen vorzeitigen Ausstieg sind Validierungen im BeforeUpdateEreignis von Formularen. Wenn eines der vorderen geprüften Steuerelemente nicht ordnungsgemäß ausgefüllt ist, nutzen Sie die Gelegenheit, die Prüfung der anderen Steuerelemente abzubrechen. Anderenfalls würde der Benutzer bei mehreren falsch ausgefüllten Feldern erstmal mehrere Meldungsfenster erhalten – das ist keinesfalls ergonomisch.
448
VBA
Beispiel: ... If IsNull(Me!txtVorname) Then MsgBox "Bitte geben Sie einen Vornamen ein." Me!txtVorname.SetFocus Exit Sub End If ...
Ein anderes Beispiel ist die Fehlerbehandlung einer Routine: Auf diese soll nur gesprungen werden, wenn in der Routine ein Fehler auftritt. Ansonsten soll die Anwendung die am Ende stehenden Anweisungen zur Behandlung von Fehlern niemals erreichen – und dafür sorgt eine Exit-Anweisung. Private Sub ProzedurMitFehlerbehandlung() On Error GoTo ProzedurMitFehlerbehandlung_Err 'Prozedurinhalt ProzedurMitFehlerbehandlung_Exit: 'Restarbeiten Exit Sub ProzedurMitFehlerbehandlung_Err: 'Fehlerbehandlung GoTo ProzedurMitFehlerbehandlung_Exit End Sub Listing 7.16: Die Exit-Anweisung für den Ausstieg vor dem Erreichen der Fehlerbehandlung
Die Exit-Anweisung erfordert immer einen zweiten Teil, der angibt, wo der Abschied erfolgen soll – beispielsweise Exit For, Exit Sub oder Exit Function.
7.7.7 Die GoTo-Anweisung und Sprungmarken In der Fehlerbehandlung des obigen Beispiels haben Sie noch zwei wichtige Elemente von VBA kennen gelernt – die Sie aber in der Tat nur in Fehlerbehandlungen einsetzen sollten, da VBA nicht die von anderen Programmiersprachen bekannte Exception-Hand ling mit Try...Catch...Finally kennt: Mit der GoTo-Anweisung springen Sie dabei zu einer bestimmten Sprungmarke. Sprungmarken wie in der obigen Fehlerbehandlung bestehen immer aus einer Zeichen kette, die den Konventionen für die Benennung von Variablen und Routinennamen entspricht, und einem angehängten Doppelpunkt (:). Solche Zeilen werden automatisch an den linken Rand gesetzt und können so leicht identifiziert werden.
449
Kapitel 7
7.8 Routinen Der Grund, warum die Überarbeitung vieler Access-Anwendungen zur Neverending Story gerät, ist die fehlerhafte Verwendung von Routinen. Sie erhalten keinen aussage kräftigen Namen, haben einen schwachen Zusammenhalt, weil sie mehr Aufgaben erledigen, als der Routinenname verrät, und sind mitunter zu stark aneinandergekop pelt. Dabei sind Routinen das vermutlich wichtigste Element bei der Programmierung von Anwendungen. Ohne Routinen könnten Sie den gleichen Code immer und immer wieder an den verschiedenen Stellen einer riesigen Prozedur einfügen. Die Wartung wäre ein Lebenswerk, Änderungen mit riesigem Aufwand verbunden. Routinen bieten so viele Erleichterungen beim Schreiben von Code, dass man ein Narr wäre, würde man nicht alle ihre Vorteile nutzen. Die folgenden Abschnitte sollen einige wichtige Informationen zur Erstellung guter Routinen liefern und erläutern, welche Vorteile es bringt, einige Regeln beim Erstellen von Routinen zu beachten. Führen Sie sich zunächst einmal vor Augen, welche Vorteile Routinen überhaupt bringen: Routinen kapseln Funktionalität. Eine Routine erhält einen Namen, mit dem diese aufgerufen werden kann, und führt im Optimalfall eine Aktion aus, die mit dem Routinennamen komplett beschrieben wird. Routinen vereinfachen den Code. Wenn Sie eine große Prozedur, die verschiedene Funktionalitäten enthält, in mehrere kleine Routinen aufteilen, die nur noch von der ehemals großen Prozedur aufgerufen werden, machen Sie aus einem unverdaulichen Brocken viele kleine leicht verdauliche Häppchen. Routinen sind meist wieder verwendbar. Sie werden viele Beispiele von Routinen kennen, die immer wiederkehrende Funktionen ausführen. Wenn Sie eigene Rou tinen so gestalten, dass diese genau die im Routinennamen beschriebene Funktion ausführen, werden Sie diese zu einem großen Teil früher oder später wieder verwenden können. Die Wiederverwendbarkeit erleichtert die Wartung des Codes. Änderungen, die Sie sonst an vielen Stellen durchführen müssten, können Sie nach dem Erzeugen einer Routine, die von vielen Stellen aufgerufen wird, an einer Stelle erledigen.
7.8.1 Routinenarten VBA enthält die Routinentypen Function und Sub. Der Unterschied ist, dass FunctionRoutinen einen Rückgabewert haben. Der Unterschied ist aber nicht so gewichtig, weil Sie auch die Parameter von Sub-Routinen verwenden können, um Ergebnisse der Rou tine zurückzuliefern.
450
VBA
Bei Funktionen ist der Funktionsname gleichzeitig der Name der Variablen mit dem Rückgabewert: Public Function Beispielfunktion() As Boolean Beispielfunktion = True End Function
Sie sollten dem Funktionskopf immer den Datentyp des Wertes anhängen, den die Funk tion zurückliefert, da VBA diesen sonst als Variant annimmt. Funktionen können auch Parameter enthalten: Public Function IstGroesserNull(intZahl As Integer) As Boolean If intZahl > 0 Then IstGroesserNull = True 'Der Else-Teil kann entfallen, da Boolean standardmäßig False ist Else IstGroesserNull = False End If End Function
Eine Sub-Prozedur liefert keinen Wert zurück und sieht in einem einfachen Fall so aus: Public Sub Beispielroutine() MsgBox "Ich bin eine Beispielroutine" End Sub
Parameter geben Sie genau wie bei Funktionen an: Public Sub BeispielroutineMitParameter(strText As String) MsgBox "Der Parameter enthält den Text '" & strText & "'" End Sub
Sowohl bei Function- als auch bei Sub-Routinen können Sie die übergebenen Parameter in der Routine ändern und die geänderten Werte in der aufrufenden Routine weiterver arbeiten – dazu jedoch später mehr.
7.8.2 Routinennamen Die Wahl des richtigen Routinennamens ist entscheidend für die Güte einer Routine. Sie dient nicht nur dazu, die richtige Routine für eine bestimmte Aufgabe ausfindig zu machen, sondern zwingt im Optimalfall den Entwickler dazu, die Routine auch nur mit der Funktionalität zu füllen, die der Routinenname vermuten lässt. Meist führt eine Routine eine Aktion auf einem Objekt aus – beispielsweise Erzeugen eines Datensatzes, Ermitteln eines Wertes, Einlesen einer Textdatei, Daten exportieren oder Ähnliches. Dementsprechend setzen sich Routinennamen meist aus einem Verb und einem Substantiv zusammen. Wenn Sie mehr als ein Verb und ein Substantiv für die
451
Kapitel 7
Beschreibung der Funktion einer Routine benötigen, sollten Sie überlegen, ob die Routine nicht zu viele nicht zusammenhängende Aufgaben erledigt und vielleicht aufgeteilt werden sollte. AdressenEinlesenUndDrucken wäre beispielsweise ein wenig viel für eine einzige Routine. Versuchen Sie, eine feste Reihenfolge von Verb und Substantiv einzuhalten – also entweder BenutzerErmitteln oder ErmittleBenutzer, wobei die erste Variante durchweg besser klingt. Wichtig ist, dass Sie aus der Funktion einer Routine auf ihren Namen schließen können und nicht erst nachsehen müssen, wie die Routine nun heißt.
Englisch oder deutsch? Ob man Variablen- und Routinennamen in englischer oder deutscher Sprache verfasst, ist leider nicht nur Geschmackssache. Wenn die Gefahr besteht, dass Ihr Werk irgendwann einmal internationale Sphären erreichen sollte, liegen Sie mit der englischen Sprache sicher nicht verkehrt. In diesem Buch wird meist die deutsche Variante verwendet – allerdings überwiegend aus Gründen der Lesbarkeit. Normalerweise sollte jeder Google-gestählte Leser zumindest mit Variablen- und Routinennamen von ein oder zwei englischen Wörtern klarkommen, aber warum die Sache unnötig verkomplizieren … In der Tat gibt es zwei Ansichten: Die einen sagen, dass man – wenn schon die komplette Programmiersprache englisch ist – auch die Variablen, Routinen und dergleichen mit englischen Bezeichnungen ausstatten sollte. Andererseits werden Sie möglicherweise Objekte wie Tabellen, Feldnamen, Formulare oder Steuerelemente mit deutschen Bezeichnungen versehen – und dann sieht es auch merkwürdig aus, wenn Sie ein Recordset auf Basis der Tabelle tblKontakte mit rstContacts bezeichnen: Set rstContacts = db.OpenRecordset("tblKontakte")
Länge von Routinennamen Hier gilt das Gleiche wie für Variablennamen: So lang wie nötig, aber so kurz wie möglich. Kombinationen aus Verb und Substantiv werden sicher nicht so lang, dass diese unlesbar sind, und Abkürzungen wie zu Zeiten, als Variablen und Routinen noch mit acht Buchstaben abgehandelt werden mussten, sollten ebenfalls vermieden werden.
Keine Zahlen! Wie bei Variablennamen sind Zahlen zur Unterscheidung von Routinen tabu. Wenn Sie mal eine Version einer Routine temporär von Routinenname in Routinenname1 umbenennen, um eine Variation einer Routine zu testen – kein Problem. Aber wer programmieren kann, sollte in der Lage sein, Routinen mit unterschiedlichen Funktionen auch unterschiedlich zu benennen.
452
VBA
7.8.3 Starker Zusammenhalt von Routinen Wenn es Ihnen durchweg gelingt, Routinen mit zwei Wörtern zu benennen und damit die darin enthaltene Funktionalität zu beschreiben, haben Sie vermutlich bereits das erreicht, was man als »starken Zusammenhalt« bezeichnet. Starker Zusammenhalt bezieht sich immer auf eine einzelne Routine und ist gegeben, wenn alle enthaltenen An weisungen nur dem Zweck dienen, der im Routinennamen beschrieben wird. Starken Zusammenhalt in dieser Form zu erreichen, ist ein hohes Ziel. Wenn man sich gängigen Code ansieht, stellt man oft fest, dass es bis zum starken Zusammenhalt noch ein gutes Stück Arbeit ist. In vielen Fällen wird ein starker Zusammenhalt nicht oder nur schwer zu erreichen sein. Wenn Sie sich beispielsweise die Ereignisprozeduren eines Formulars ansehen – etwa das Ereignis Beim Öffnen –, erkennen Sie, dass dort mitunter zahlreiche Aktionen stattfinden, die nicht unbedingt miteinander zusammenhängen. Die Stärke des Zusammenhalts liegt hier eher darin, dass alle Aktionen zu einem bestimmten Zeitpunkt ausgeführt werden müssen.
7.8.4 Lose Kopplung zwischen Routinen Der Begriff lose Kopplung kommt eigentlich aus der objektorientierten Welt, lässt sich aber auch leicht auf Routinen anwenden. Routinen sind lose gekoppelt, wenn die aufrufende Routine keine Implementierungsdetails der aufgerufenen Routine kennen muss, um diese perfekt verwenden zu können. Die einzigen Kenntnisse sind der Routinenname und die Parameter der aufgerufenen Routine. Je weniger Parameter es gibt, desto loser die Kopplung, das heißt desto besser. Schlecht ist es, wenn die Kopplung zwischen zwei Routinen über mehr als den Aufruf und die Übergabe von Parametern und gegebenenfalls das Zurückliefern hergestellt wird. Ein Beispiel ist eine globale Variable, die von der aufrufenden Prozedur mit einem Wert gefüllt und von der aufgerufenen Prozedur ausgelesen wird. In diesem Fall reicht die Kenntnis des Routinennamens und der Parameter (zusammengefasst der Schnittstelle) der Routine nicht mehr aus – man muss sich die aufgerufene Routine zuvor ansehen, um zu erfahren, dass hier noch eine globale Variable eine Rolle spielt.
7.8.5 Parameter und Rückgabewerte einer Routine Sie können für jede VBA-Routine Parameter festlegen. Diese dienen einerseits dazu, In formationen an die aufgerufene Routine zu übergeben. Andererseits können Sie diese auch verwenden, um die Ergebnisse der Routine zurückzuliefern.
453
Kapitel 7
Parameter werden in Pflicht- und optionale Parameter unterteilt. Optionale Parameter befinden sich immer am Ende der Parameterliste und werden mit dem Schlüsselwort Optional gekennzeichnet. Sie können Parameter einfach in der Reihenfolge angeben, in der sie in der Parameterliste aufgeführt werden, oder benannte Parameter verwenden. Dazu übergeben Sie der Routine beim Aufruf eine durch Kommata getrennte Liste von Wertepaaren nach dem Schema <Parameter>:=<Wert>. In der Routine können Sie das Vorhandensein von Werten bei optionalen Parametern mit der IsMissing-Funktion überprüfen. Optionale Parameter müssen den Datentyp Variant besitzen, damit die IsMissing-Funktion funktioniert. Ein andere Alternative, bei der der optionale Parameter auch einen anderen Datentyp haben kann, ist das Zuweisen eines Vorgabewertes mit dem Gleichheitszeichen: Sub BeispielParameter2(str1 As String, Optional lngWert As Long = -1)
In der Prozedur ist dann zu prüfen, ob der Parameter lngWert einen anderen Wert als –1 hat. Letzteres würde ihn als nicht übergeben kennzeichnen. Jede Routine kann maximal 60 Parameter besitzen.
Namen von Parametern Für Parameter von Routinen gelten im Allgemeinen die gleichen Regeln wie für sonstige Variablen.
Parameterwerte nur mit Vorsatz ändern In manchen Fällen bieten die Parameter einer Routine die einzige Möglichkeit, Informa tionen an die aufrufende Routine zurückzugeben. Gut, man könnte ein Array als Funk tionswert oder einen benutzerdefinierten Typen verwenden. Parameter lassen sich aber wesentlich leichter handhaben. Beispiel: Die Routine BeispielRueckgabewert deklariert die beiden Variablen strVorname und strNachname und übergibt diese einschließlich der Personalnummer der gesuchten Person an die Funktion NameErmitteln. Diese liest den entsprechenden Datensatz ein und füllt die Parameter strVorname und strNachname mit den Daten aus der Tabelle. Diese lassen sich in der aufrufenden Routine anschließend ganz normal weiterverwenden. Public Sub BeispielRueckgabeparameter() Dim strVorname As String Dim strNachname As String NameErmitteln 1, strVorname, strNachname
454
VBA Debug.Print strVorname Debug.Print strNachname End Sub Public Function NameErmitteln(lngPersonalID As Long, _ strVorname As String, strNachname As String) Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb Set rst = db.OpenRecordset("SELECT Vorname, Nachname FROM Personal " _ & "WHERE [Personal-Nr] = " & lngPersonalID) strVorname = rst!Vorname strNachname = rst!Nachname rst.Close Set rst = Nothing Set db = Nothing End Function Listing 7.17: Parameter als Rückgabewert
Dies funktioniert, weil unter VBA alle Parameter standardmäßig als Referenztyp übergeben werden. Das bedeutet, dass nicht der eigentliche Wert, sondern eine Referenz auf die Adresse, an der der Wert gespeichert ist, übergeben wird. Ändert die aufgerufene Routine diesen Wert, findet die aufrufende Prozedur in der verwendeten Variablen den geänderten Wert vor. Wenn Sie den Wert selbst statt der Referenz übergeben möchten, verwenden Sie das Schlüsselwort ByVal. Wenn Sie die genannte Technik für die Rückgabe von Ergebnissen verwenden, ordnen Sie die Parameter so an, dass die Rückgabeparameter am Ende der Parameterliste stehen.
Parameterwerte nicht ohne Vorsatz ändern Umgekehrt gilt: Wenn Sie nicht jede Variable explizit als Werttyp deklarieren möchten, dürfen Sie Parameter nicht als Variablen verwenden und diese in der aufgerufenen Routine ändern. Daher weisen Sie Parameter zunächst prozedurinternen Variablen zu und verwenden diese für die weiteren Aktionen.
Rückgabewerte erst am Ende zuweisen Viele VBA-Entwickler neigen dazu, den Rückgabewert einer Routine, also die Variable mit dem Namen der Routine, innerhalb der Routine als Variable zu verwenden und ihren Wert dort nach Belieben zu ändern. Das kann zu Problemen führen, wenn die Routine etwa einmal vorzeitig beendet wird – der Rückgabewert liefert dann unter Umständen ein falsches Ergebnis.
455
Kapitel 7
Besser ist es, wenn Sie den Rückgabewert einer Routine erst kurz vor dem Ende der Routine zuweisen. Tritt dann vorher ein Fehler auf, lässt sich dieser durch den leeren Rückgabewert leicht identifizieren.
Beliebige Anzahl Parameter übergeben Mit dem Schlüsselwort Optional wird die Übergabe von Parametern an eine Prozedur zwar variabler, weil nicht alle Parameter mit einem Wert gefüllt sein müssen. Die Anzahl Parameter ist dennoch festgelegt. Mit dem Schlüsselwort ParamArray ist es jedoch möglich, einer Prozedur eine beliebige, nicht festgelegte Zahl Parameter zu übergeben. Dabei werden die einzelnen übergebenen Parameter in einem Variant-Array gespeichert, das in der Prozedur ausgelesen werden kann: Public Sub BeispielParameterArray(str1 As String, _ ParamArray arrParamMulti()) Dim i As Long Debug.Print "Pflichtparameter: " & str1 For i = 0 To UBound(arrParamMulti) Debug.Print "Optionaler Parameter " & i & ": " & arrParamMulti(i) Next i End Sub
Ein Beispielaufruf dieser Prozedur sieht so aus: BeispielParameterArray "Minhorst", "Alter", 34, "Gewicht", 86.5
7.8.6 Gleichzeitige Rückgabe von Statuswert und Ergebnis In vielen Fällen dürfte eine Funktion für die Ermittlung der gewünschten Information reichen. Sobald Sie aber neben einem Ergebnis auch noch eine Statusmeldung erwarten, sollten Sie die oben genannte Vorgehensweise zur Rückgabe von Ergebnissen per Parameter mit der Rückgabe eines Funktionswerts verknüpfen. Der Rückgabewert der Funktion gibt dann den Statuswert zurück und die Parameter sind für das Ergebnis zuständig. Eine Erweiterung der Routinen aus Listing 7.17 zeigt, wie dies funktioniert: Public Function NameErmittelnMitStatus(lngPersonalID As Long, _ strVorname As String, strNachname As String) As Boolean Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb Set rst = db.OpenRecordset("SELECT Vorname, Nachname FROM Personal " _ & "WHERE [Personal-Nr] = " & lngPersonalID) If Not rst.EOF Then
456
VBA strVorname = rst!Vorname strNachname = rst!Nachname NameErmittelnMitStatus = True Else NameErmittelnMitStatus = False End If rst.Close Set rst = Nothing Set db = Nothing End Function Public Sub BeispielRueckgabeparameterMitStatus() Dim strVorname As String Dim strNachname As String If NameErmittelnMitStatus(1, strVorname, strNachname) = True Then Debug.Print strVorname Debug.Print strNachname Else Debug.Print "Der Mitarbeiter konnte nicht eingelesen werden." End If End Sub Listing 7.18: Gleichzeitige Übergabe von Statuswert und Funktionsergebnis
7.8.7 Alle Routinen verwenden Um Karteileichen zu vermeiden, sollten Sie spätestens vor der Abnahme eines Projekts dafür sorgen, dass alle nicht benötigten Routinen aus den Modulen entfernt werden. Ob eine Routine noch verwendet wird, erfahren Sie ohne Hilfsmittel, wenn Sie im kompletten Projekt nach Aufrufen mit dem Namen dieser Routine suchen. Mit den MZTools [3] können Sie per Kontextmenüeintrag alle Aufrufe für die aktuell markierte Routine anzeigen lassen.
Quellen zu diesem Kapitel [1] Reddick-Convention/ungarische Notation: http://www.xoc.net/standards/rvbanc.asp [2] SmartIndenter: http://www.oaltd.co.uk/Indenter/Default.htm [3] MZTools: http://www.mztools.com/v3/download.htm
457
8 Access-SQL SQL (Structured Query Language, strukturierte Abfrage sprache) ist eine weitgehend standardisierte Abfragespra che für relationale Datenbanken. SQL dient der Auswahl von Daten aus den Tabellen einer relationalen Datenbank und ihrer Manipulation. Der Sprachumfang von Access-SQL ist in zwei Bereiche un terteilt: Die Data Manipulation Language (DML) liefert die Befehle zum Auswählen und Manipulieren von Daten, die Data Definition Language (DDL) die Anweisungen zum Er stellen, Bearbeiten und Löschen des Datenmodells selbst. Die Befehle der DML finden Sie in den Abschnitten 8.3, »Daten auswählen«, und 8.4, »Daten manipulieren«, und die der DDL in Abschnitt 8.5, »Datenmodell erstellen und manipulieren«.
8.1 SQL-Versionen Access-SQL bietet eine Mischung aus den Standards SQL89, SQL-92 und einigen Access-spezifischen Erweiterungen – beispielsweise lassen sich unter Access VBA-Ausdrücke in SQL-Anweisungen integrieren. Damit ist etwa die Verwendung von Standardfunktionen oder Bezügen auf Formulare und Steuerelemente möglich. Die Bestandteile aus SQL-92 werden nur an bestimmten Stellen unterstützt. In den folgenden Abschnitten finden Sie hauptsächlich die Möglichkeiten von Access-SQL, die sowohl in der SQLund Abfrage-Entwurfsansicht als auch unter DAO und ADO ohne Einschränkung verwendet werden können.
Kapitel 8
Die Beispiele zu diesem Kapitel finden Sie auf der Buch-CD unter \Kap_07\ AccessSQL.accdb. Soweit die Erweiterungen die hier beschriebenen Funktionen von SQL betreffen, finden Sie ihre Erläuterung und einen entsprechenden Hinweis auf die Version. Um die erst unter SQL-92 enthaltenen Funktionen einsetzen zu können, müssen Sie eine Einstellung in den Optionen der Datenbank vornehmen (siehe Abbildung 8.1). Diese Änderung hat weit reichende Folgen: Unter anderem ändert sich die Syntax in einigen Punkten – zum Beispiel werden die unter Access verwendeten Platzhalter Sternchen (*) und Fragezeichen (?) nicht mehr unterstützt, sondern das Prozentzeichen (%) und der Unterstrich (_) verwendet.
Abbildung 8.1: Aktivieren der SQL-92-Erweiterungen
Sie können die Erweiterungen auch ohne Ändern dieser Einstellung verwenden, allerdings nicht an allen Stellen. Das Einsatzgebiet beschränkt sich dann auf die Anwendung in VBA-Code und dort auf die ADO-Objektbibliothek. Sie finden an entsprechender Stelle eine Beispielprozedur für den Aufruf von SQL-Anweisungen mit SQL-92-spezifischen Sprachelementen.
8.2 SQL und Access SQL wird in Access an den verschiedensten Stellen eingesetzt – teilweise völlig unbemerkt. Wer mit einem Grundlagenbuch in Access einsteigt, wird unter Umständen erst
460
Access-SQL
in den hinteren Kapiteln – wenn überhaupt – mit SQL in Berührung kommen. Und wenn man es darauf anlegt, kann man sich eine ganze Weile um die erste selbst ge schriebene SQL-Anweisung herumdrücken. Schließlich bietet Access mit der AbfrageEntwurfsansicht ein ausgezeichnetes Hilfsmittel für die Erstellung von Abfragen (siehe Abbildung 8.2).
Abbildung 8.2: Die Entwurfsansicht zur Erstellung von Abfragen
Dieses Werkzeug ermöglicht es, die in einer Abfrage anzuzeigenden Tabellen und Felder auszuwählen, Kriterien direkt oder als Parameter anzugeben, Sortierungen und Grup pierungen festzulegen und Funktionen zur Summierung, Mittelwertbildung, Ermitt lung von Extremwerten oder Anzahlen anzuwenden. Wer bis dato noch keinen Kontakt mit SQL hatte und sich darum vielleicht auch keine größeren Gedanken gemacht hat, ist möglicherweise überrascht, dass alles, was man in der Abfrage-Entwurfsansicht anstellt, in die Erstellung einer SQL-Anweisung mündet. Beweis gefällig? Dann wählen Sie doch einfach aus dem Kontextmenü des Abfrageentwurfs den Eintrag SQL-Ansicht aus – das nun erscheinende Fenster offenbart das wahre Aussehen der Abfrage (siehe Abbildung 8.3). Die Schriftart dieses Fensters können Sie in den Access-Optionen unter Objektdesigner| Abfrageentwurf|Schriftart zum Erstellen von Abfragen anpassen.
8.2.1 Wozu trotz Abfrage-Entwurfsansicht SQL lernen? Warum sollten Sie sich eigentlich mit SQL beschäftigen, obwohl die Entwurfsansicht für Abfragen so eine Erleichterung beim Erstellen von Abfragen ist? Dafür gibt es mehrere Gründe:
461
Kapitel 8
Abbildung 8.3: Abfrage in der SQL-Ansicht
Nicht jede Abfrage lässt sich in der Entwurfsansicht darstellen, darunter UNIONAbfragen, Datendefinitions-Abfragen und PassThrough-Abfragen. Die Erstellung einer Abfrage per SQL-Code geht manchmal einfach schneller. Die Eingabe der folgenden Zeile im Direktfenster bekommt manch einer fixer hin, als die entsprechende Tabelle zu öffnen, alle Datensätze zu markieren, den Menüeintrag Bearbeiten|Löschen auszuwählen und die Tabelle wieder zu schließen: CurrentDB.Execute "DELETE FROM tblKunden"
Gelegentlich ist es aus Performancegründen sinnvoll, eine SQL-Abfrage im VBACode einzuarbeiten und von dort auszuführen – beispielsweise, wenn sich die in den betroffenen Tabellen enthaltenen Daten oft ändern und der Vorteil einer kompi lierten und optimierten gespeicherten Abfrage nicht mehr vorhanden ist (siehe Kapi tel 14, Abschnitt 14.2.3, »Gespeicherte Abfragen versus Ad-hoc-Abfragen«). Manchmal ist es auch unabwendbar, eine SQL-Anweisung im VBA-Code zusammenzusetzen – beispielsweise, weil die Abfrage das Ergebnis einer Suche liefern soll, die je nach Kriterien Felder aus wechselnden Tabellen enthält.
8.2.2 Wo lässt sich SQL überall einsetzen? Von Hand erstellte SQL-Ausdrücke lassen sich praktisch an allen Stellen einsetzen, an denen auch Tabellen oder Abfragen als Datenquelle angegeben werden können. Aber auch »normale« Abfragen lassen sich statt über die Entwurfsansicht über die SQL-Ansicht eingeben und können anschließend wie gewohnt über die Entwurfsansicht angepasst werden. Diese Vorgehensweise bietet sich an, wenn Sie im VBA-Code eine SQL-Abfrage zusammensetzen, die ihren Dienst verweigert. Sie können sich dann die entsprechende Zeichenkette im Direktfenster ausgeben lassen, den SQL-Ausdruck in die SQL-Ansicht einer Abfrage eingeben und dann nach Fehlern suchen. SQL-Ausdrücke lassen sich an folgenden Stellen verwenden: SQL-Ansicht von Abfragen Datensatzquelle von Formularen
462
Access-SQL
Datensatzquelle von Berichten Datensatzherkunft von Kombinationsfeldern Datensatzherkunft von Listenfeldern Datensatzherkunft von Nachschlagefeldern in Tabellen Makro AusführenSQL RunSQL-Anweisung des DoCmd-Objekts in VBA Execute-Anweisung des Database-Objekts (ADO) in VBA Execute-Anweisung des Connection-Objekts (ADO) in VBA Source-Eigenschaft des Recordset-Objekts (ADO) in VBA CommandText des Command-Objekts (ADO) in VBA
8.3 Daten auswählen SQL-Abfragen dienen der Auswahl von Daten aus einer oder mehreren Tabellen. Dabei können Sie sowohl die Anzahl auszugebender Felder als auch der auszugebenden Datensätze einschränken. Die einfachste Form einer Auswahlabfrage liefert den kompletten Inhalt einer Tabelle wie im folgenden Beispiel: SELECT * FROM tblKunden
Das Ergebnis dieser Abfrage entspricht genau dem Bild, das Sie auch beim Öffnen einer Tabelle erhalten. Es enthält alle Felder und alle Datensätze des Originals. Um die Datenblatt-Ansicht dieser und der folgenden SQL-Beispiele anzusehen, gehen Sie folgendermaßen vor: Klicken Sie im Ribbon auf den Eintrag Erstellen|Andere|Abfrageentwurf. Schließen Sie den Dialog Tabelle anzeigen, ohne eine Tabelle auszuwählen. Wählen Sie im nun aktiven Ribbon-Tab namens Entwurf die Schaltfläche SQL aus. Geben Sie im nächsten Fenster den SQL-Ausdruck ein und wählen Sie anschließend statt der SQL-Ansicht die Datenblatt-Ansicht aus oder klicken Sie auf die AusführenSchaltfläche im Ribbon. Wie Sie bereits gesehen haben, besteht die einfachste Form einer SQL-Abfrage aus mindestens zwei Teilen: Die SELECT-Klausel leitet den ersten Teil ein und enthält die Auflistung der auszuwählenden Felder. Die FROM-Klausel legt fest, aus welcher beziehungsweise welchen Tabellen die Daten stammen.
463
Kapitel 8
Die weiteren Teile einer Abfrage dienen dem Einschränken (WHERE-Klausel) und Sor tieren des Ergebnisses (ORDER BY-Klausel). Diese beiden Klauseln sind optional.
Abbildung 8.4: Anzeigen der SQL-Ansicht einer Abfrage
8.3.1 Festlegen der anzuzeigenden Felder Der erste Teil einer Abfrage, der durch die SELECT-Klausel eingeleitet wird, legt fest, welche Felder die Ergebnismenge enthält. Die Felder werden im Anschluss an das SE LECT-Schlüsselwort aufgelistet und durch Kommata voneinander getrennt. Unter Um ständen schiebt man noch einige Elemente zwischen SELECT und Feldliste – dazu spä ter mehr. Die folgende Abfrage gibt die drei Felder KundeID und Firma der Tabelle tblKunden aus: SELECT KundeID, Firma FROM tblKunden;
Eine etwas ausführlichere Variante würde lauten: SELECT tblKunden.KundeID, tblKunden.Firma FROM tblKunden;
Die Angabe des Tabellennamens vor dem jeweiligen Feld ist sinnvoll, wenn die Abfrage ihre Daten aus mehreren Tabellen bezieht, und Pflicht, wenn sich in den zugrunde liegenden Tabellen mehrere Felder gleichen Namens befinden. Wenn Sie einen SQL-Aus druck ohne Angabe der Tabelle zusammen mit den Feldnamen in der SQL-Ansicht einer Abfrage eingeben, anschließend zur Entwurfsansicht wechseln und von dort aus die Abfrage speichern, enthält die SQL-Ansicht beim nächsten Anzeigen jeweils den zu den Feldern gehörenden Tabellennamen. Wenn Sie andersherum die Entwurfsansicht zum Erstellen der Abfrage verwenden, enthält die SQL-Ansicht immer die vollständigen Feldnamen – also mit Tabellennamen.
Alle Felder einer Tabelle ausgeben Um die Inhalte aller Felder einer Tabelle zu ermitteln, geben Sie entweder alle Felder explizit an oder verwenden das Sternchen (*) an Stelle der Auflistung der Felder: SELECT * FROM tblKunden;
464
Access-SQL
oder ausführlicher: SELECT tblKunden.* FROM tblKunden;
Sonderzeichen in Tabellen- und Feldnamen Vom Gebrauch von Sonderzeichen in Tabellen- und Feldnamen ist grundsätzlich abzura ten. Manchmal lässt sich das aber nicht verhindern, weil man etwa eine bestehende Da tenbank weiterentwickelt, bei der das Kind schon in den Brunnen gefallen ist und eine nachträgliche Änderung zu aufwändig wäre. In diesem Fall fassen Sie den Feldnamen in eckige Klammern ein: SELECT [Kunde-ID], Firma FROM tblKunden;
Auch hier die ausführliche Variante mit Tabellennamen: SELECT tblKunden.[Kunde-ID], tblKunden.Firma FROM tblKunden;
Feldnamen ersetzen Wenn Sie für einen bestimmten Zweck einen anderen Feldnamen in der Ausgabe der Abfrage wünschen, verwenden Sie das AS-Schlüsselwort, um einem Feld einen alternativen Namen zuzuweisen: SELECT [Kunde-ID] AS KundeID, Firma AS Kunde FROM tblKunden;
Wie Sie sehen, lassen sich die unbequemen Feldnamen mit Sonderzeichen auf diese Wei se zumindest für den weiteren Gebrauch auf Basis des SQL-Ausdrucks umbenennen. Auch für Felder, die Sie aus anderen Feldern berechnen oder zusammensetzen, verwenden Sie das AS-Schlüsselwort: SELECT MitarbeiterID, Nachname & ", " & Vorname AS Mitarbeiter FROM tblMitarbeiter;
Das Ergebnis dieser Abfrage sieht wie in Abbildung 8.5 aus.
8.3.2 Festlegen der enthaltenen Tabellen Neben den Feldern, die ein SQL-Ausdruck enthält, müssen Sie natürlich auch noch die Tabellen angeben, aus denen die Felder stammen. Das gilt übrigens nicht nur für die angezeigten Felder, sondern auch für die Felder, die als Kriterien- oder Sortierfeld dienen. Die Tabellen listet man unmittelbar hinter der FROM-Klausel auf. Bei einer einzigen Tabelle ist der Ausdruck noch recht übersichtlich: SELECT MitarbeiterID, Vorname, Nachname FROM tblMitarbeiter;
465
Kapitel 8
Abbildung 8.5: Abfrage mit zusammengesetztem Feld
Wenn die Daten aber aus mehreren Tabellen stammen, reicht es unter Umständen nicht aus, einfach nur die beteiligten Tabellen aufzulisten. Der einzige Fall, in dem Sie so verfahren, liefert alle Kombinationen der Felder aus den angegebenen Tabellen: SELECT tblMitarbeiter.MitarbeiterID, tblMitarbeiter.Vorname, tblMitarbeiter. Nachname, tblProjekte.ProjektID, tblProjekte.Projekt FROM tblMitarbeiter, tblProjekte;
Dieser SQL-Ausdruck würde alle Kombinationen der Datensätze der Tabelle tblMitarbeiter und der Tabelle tblProjekte ausgeben. Normalerweise wird man nur bestimmte Kombinationen aus Projekten und Mitarbeitern anzeigen wollen – etwa einen Mitar beiter, der in der Projekt-Tabelle als Projektleiter eingetragen ist. Um einen derartigen Zusammenhang festzulegen, verwendet man entweder eine JOIN-Klausel, die Infor mationen über eine Verknüpfung zwischen den beteiligten Tabellen angibt, oder eine WHERE-Klausel, die festlegt, welche Felder der beiden Tabellen übereinstimmen müs sen, damit eine Kombination ausgegeben wird.
Vereinfachen der Schreibweise Um die Schreibweise und Übersichtlichkeit von SQL-Ausdrücken mit mehreren Tabellen zu vereinfachen, können Sie den Tabellennamen auch alternative Bezeichnungen zuweisen. Diese werden dann im Rest des SQL-Ausdrucks verwendet. Im folgenden Beispiel soll die Tabelle tblMitarbeiter die Bezeichnung t1 erhalten und die Tabelle tblProjekte die Bezeichnung t2: SELECT t1.MitarbeiterID, t1.Vorname, t1.Nachname, t2.ProjektID, t2.Projekt FROM tblMitarbeiter AS t1, tblProjekte AS t2;
In der Entwurfsansicht stellen Sie einen alternativen Namen übrigens über die Eigen schaft Alias ein (siehe Abbildung 8.6).
8.3.3 Festlegen von Bedingungen Bisher haben Sie nur die Felder ausgewählt, die Sie im Abfrageergebnis der SQLAbfrage anzeigen möchten. Nun legen Sie fest, welche Datensätze angezeigt werden sollen. Dazu verwenden Sie die WHERE-Klausel von SQL.
466
Access-SQL
Abbildung 8.6: Eingeben eines Alias-Namens für eine Tabelle
Die WHERE-Klausel enthält einen oder mehrere Ausdrücke, die durch AND oder OR voneinander getrennt sind. Damit können Sie etwa alle Datensätze der Tabelle tblMitarbeiter herausfinden, bei denen das Feld MitarbeiterID den Wert 1 enthält: SELECT * FROM tblMitarbeiter WHERE MitarbeiterID = 1;
Mit Hilfe der OR-Verknüpfung können Sie Datensätze zurückgeben, die mindestens eine von mehreren Bedingungen erfüllen: SELECT * FROM tblMitarbeiter WHERE MitarbeiterID = 1 OR MitarbeiterID = 2;
Mit der AND-Verknüpfung schränken Sie die zurückgegebenen Datensätze so ein, dass diese alle aufgeführten Bedingungen erfüllen: SELECT * FROM tblMitarbeiter WHERE Vorname = 'Klaus' AND Nachname = 'Wild';
Wenn der SQL-Ausdruck mehr als einen Verknüpfungsoperator enthält, können Sie die Reihenfolge der Abarbeitung durch das Setzen von Klammern beeinflussen.
8.3.4 Vergleichsausdrücke Bei der Zusammenstellung der Vergleichsausdrücke sind der Fantasie praktisch keine Grenzen gesetzt. In den folgenden Abschnitten finden Sie die Grundlagen und die wichtigsten Möglichkeiten für Vergleichsausdrücke.
Operatoren Für Vergleiche stellt SQL die folgenden Operatoren zur Verfügung:
467
Kapitel 8
gleich (=) größer als (>) größer als oder gleich (>=) kleiner als (<) leiner als oder gleich (<=) ungleich (<>) Vergleichsumkehr (NOT) zwischen zwei Werten (BETWEEN … AND) Vergleich mit Platzhaltern wie Sternchen (*, beliebig viele Zeichen) oder Fragezeichen (?, ein Zeichen): LIKE Vergleich mit Null-Werten: IS NULL Vergleich mit den Werten einer Menge: IN
Vergleiche mit Zahlen Wenn Sie einen der obigen Vergleichsoperatoren für den Vergleich mit Zahlen verwenden, können Sie alle Varianten außer Vergleichen mit Platzhaltern einsetzen. Beispiele: SELECT * FROM tblArtikel WHERE Preis > 10; SELECT * FROM tblArtikel WHERE Preis BETWEEN 5 AND 15;
Geben Sie Zahlen immer ohne Formatierungen wie Prozentzeichen (%), Währungs angaben (€) oder dergleichen ein. Verwenden Sie nur die nackten Zahlen als Vergleichs ausdrücke und setzen Sie den Punkt als Dezimaltrennzeichen: SELECT * FROM tblArtikel WHERE Mehrwertsteuer IN (0.7, 0.16);
Vergleiche mit Zeichenketten Wichtig für die Verwendung von Zeichenketten ist vor allem, dass Sie den Vergleichs wert in einfache (') oder doppelte (") Anführungszeichen einschließen. Probleme werden Sie jedoch bekommen, wenn innerhalb der Zeichenkette selbst Anführungszeichen enthalten sind, weil der SQL-Interpreter diese dann als Trennzeichen der Zeichenkette interpretiert. Um auf der sichereren Seite zu sein, sind doppelte Anführungszeichen meist günstiger. Dann gibt es auch keine Fehlermeldung, wenn ein Name wie »D'Hondt« ausgewertet wird. Weiterhin sind alle Vergleichsoperationen möglich. Am sinnvollsten ist jedoch der LIKE-Vergleich, da er die Verwendung von Platzhaltern wie Sternchen (*), das stellvertretend für beliebig viele Zeichen steht, und Fragezeichen (?),
468
Access-SQL
das stellvertretend für je ein Zeichen steht, unterstützt. Die folgenden Beispiele verdeutlichen dies: Alle Mitarbeiter, deren Vorname mit A beginnt: SELECT * FROM tblMitarbeiter WHERE Vorname LIKE "A*";
Alle Mitarbeiter, deren Nachname die Zeichenfolge EI enthält: SELECT * FROM tblMitarbeiter WHERE Nachname LIKE "*EI*";
Alle Mitarbeiter, deren Nachname mit einem Buchstaben von A–J beginnt: SELECT * FROM tblMitarbeiter WHERE Vorname >= "A" AND Vorname < "K";
Vergleiche mit Datumsangaben Datumsangaben sind ein fehlerträchtiges Gebiet – vor allem in Abfragen. Auf Nummer Sicher gehen Sie mit den folgenden beiden Datumsformaten: Amerikanisches Datumsformat: #mm/dd/yyyy# ISO-Datumsformat: #yyyy/mm/dd# Verwenden Sie eines dieser Formate als Grundlage für den Vergleichswert. Gehen Sie kein Risiko ein. Lassen Sie sich auch nicht davon irritieren, dass das Datum in der Tabelle ganz anders eingegeben und angezeigt wird. Von dieser Feinheit abgesehen können Sie für Vergleiche mit dem Datum alle Vergleichsvarianten mit Ausnahme des Vergleichs mit Platzhaltern heranziehen. Beispiele: SELECT * FROM tblBestellungen WHERE Bestelldatum = #2005/05/15#; SELECT * FROM tblBestellungen WHERE Bestelldatum In (#5/5/2005#, #5/7/2005#); SELECT * FROM tblBestellungen WHERE Lieferdatum BETWEEN #2005/5/10# AND #2005/5/15#;
Vergleiche mit dem Null-Wert Der Null-Wert besitzt eine besondere Funktion: Liefert ein Vergleich eines Feldes mit dem Wert Null den Wert True, ist das Feld leer.
469
Kapitel 8
Das folgende Beispiel liefert alle Mitarbeiter, die keine E-Mail-Adresse haben: SELECT * FROM tblMitarbeiter WHERE Email IS NULL;
Umgekehrt ergibt das nächste Beispiel alle Mitarbeiter, die eine E-Mail-Adresse besitzen: SELECT * FROM tblMitarbeiter WHERE NOT EMail IS NULL;
Vergleiche mit Funktionen Access-SQL bietet die Möglichkeit, auch Funktionen als Vergleichswert zu verwenden. Eine oft als Vergleichswert verwendete Funktion ist Date(). Die folgende Abfrage ermittelt alle Bestellungen, deren Lieferdatum mit dem aktuellen Datum übereinstimmt: SELECT * FROM tblBestellungen WHERE Lieferdatum = DATE();
Auch berechnete Ausdrücke sind erlaubt. Alle Bestellungen der letzten 30 Tage ermitteln Sie folgendermaßen: SELECT * FROM tblBestellungen WHERE Bestelldatum = DATE() – 30;
Standard-SQL bietet einige Funktionen wie etwa die Aggregatfunktionen Sum, Count, Max oder Min (siehe weiter unten in Abschnitt 8.3.6, »Aggregatfunktionen«). Unter Access-SQL lassen sich wie im Beispiel auch eingebaute VBA-Funktionen, benutzerdefinierte Funktionen und Verweise auf Objekte verwenden. Letzteres ist vor allem im Zusammenhang mit der Einbindung der Steuerelementinhalte von Formularen interessant.
8.3.5 Sortieren von Daten Die ORDER BY-Klausel dient der Angabe der Sortierreihenfolge für beliebig viele Felder. Nach der ORDER BY-Klausel, die sich übrigens immer am Ende der Abfrage befindet, geben Sie den Namen eines oder mehrerer Felder an, nach denen das Abfrageergebnis sortiert werden soll. Mehrere Feldnamen trennt man durch Kommata. Um anzugeben, ob aufsteigende oder absteigende Sortierung verwendet werden soll, ergänzen Sie den Feldnamen um einen der beiden Ausdrücke ASC (aufsteigend) oder DESC (absteigend). Ohne Angabe von ASC oder DESC wird automatisch ASC angenommen. Wenn Sie die Datensätze der Mitarbeitertabelle aufsteigend nach den Nachnamen sortieren möchten, verwenden Sie folgende Abfrage: SELECT * FROM tblMitarbeiter ORDER BY Nachname;
Die nächste Variante legt die Sortierreihenfolge explizit fest: SELECT * FROM tblMitarbeiter ORDER BY Nachname ASC;
470
Access-SQL
Da Nachnamen gegebenenfalls mehrfach vorkommen, macht eine zusätzliche Sortierung nach dem Vornamen Sinn: SELECT * FROM tblMitarbeiter ORDER BY Nachname, Vorname;
Die explizite Variante hieße dann: SELECT * FROM tblMitarbeiter ORDER BY Nachname ASC, Vorname ASC;
8.3.6 Aggregatfunktionen Sie können SQL Aggregatfunktionen verwenden, um Datensätze zu zählen, Mittelwerte zu bilden oder Summen zu ermitteln. Diese Aggregatfunktionen beziehen sich auf das komplette Abfrageergebnis oder auf einzelne Gruppierungen. Mehr über den Einsatz von Aggregatfunktionen auf das komplette Abfrageergebnis erfahren Sie im Anschluss an die Auflistung der Funktionen; der Einsatz von Aggregatfunktionen mit gruppierten Daten wird in Abschnitt 8.3.7, »Gruppieren von Daten« vorgestellt. SQL bietet die folgenden Aggregatfunktionen: Avg(): Berechnet den arithmetischen Mittelwert des in enthaltenen Wertes. Count: Ermittelt die Anzahl von Datensätzen. Es gibt zwei Varianten: Count(*) ermittelt die Anzahl aller enthaltenen Datensätze, und Count() ermittelt die Anzahl aller Datensätze, in denen der nicht den Wert NULL besitzt. First(): Ermittelt den Wert von des ersten Datensatzes des Abfrageergebnisses. Last(): Ermittelt den Wert von des letzten Datensatzes des Abfrageergebnisses. Max(): Ermittelt den größten Wert von des Abfrageergeb nisses. Min(): Ermittelt den kleinsten Wert von des Abfrageergeb nisses. StDev(), StDevP(): Ermittelt die Standardabweichung von bezogen auf die Grundgesamtheit (StDevP) oder eine Stichprobe (StDev). Sum(): Summiert die Werte von , Null-Werte werden ignoriert. Var(), VarP(): Berechnet die Varianz von bezogen auf die Grundgesamtheit (VarP) oder eine Stichprobe (Var).
471
Kapitel 8
Aggregatfunktionen ohne Gruppierung Die Aggregatfunktionen lassen sich ohne Gruppierung auf die komplette Ergebnismenge der Abfrage anwenden. Wenn Sie beispielsweise den Durchschnittspreis von Produkten ermitteln möchten, verwenden Sie etwa folgende SQL-Abfrage: SELECT Avg(Preis) AS Durchschnittspreis FROM tblProdukte;
Das Ergebnis der Abfrage sieht wie in Abbildung 8.7 aus.
Abbildung 8.7: Ergebnis der Abfrage eines Durchschnittswertes
Wichtig ist, dass Sie für den durch die Aggregatfunktion entstandenen Ausdruck einen Alias-Namen angeben – in diesem Fall als AS Durchschnittspreis –, sonst vergibt Access automatisch einen Namen der Form »Durchschnitt von Preis«, »Anzahl von ProduktID«, ...
8.3.7 Gruppieren von Daten Daten lassen sich mit SQL innerhalb einer einzigen Abfrage gruppieren. Dadurch erhalten Sie die Möglichkeit, Aggregatfunktionen auf gruppierte Datensätze mit bestimmten Eigenschaften auszuführen. Bei der Erstellung von Abfragen mit Gruppierungen sind zwei Regeln zu beachten: Gruppierungen können entweder Tabellenfelder, berechnete Felder oder Konstanten enthalten. Jedes Feld, das der SELECT-Bereich der Abfrage enthält, muss entweder mit einer Aggregatfunktion versehen oder ein Element des GROUP BY-Abschnitts sein. Das fällt auch beim Erstellen von Gruppierungen mit der Abfrage-Entwurfsansicht auf: Dort wird für jedes hinzugefügte Feld standardmäßig die Funktion Gruppierung festgelegt. Es ist nicht möglich, in der Spalte Funktion weder eine Gruppierung noch eine Aggregatfunktion auszuwählen.
Einfache Gruppierungen Das folgende Beispiel zeigt eine Abfrage, die Produkte nach Kategorien gruppiert und die Anzahl der Produkte je Kategorie ausgibt:
472
Access-SQL SELECT KategorieID, Count(ProduktID) AS AnzahlProdukte FROM tblProdukte GROUP BY KategorieID;
Das Ergebnis der Abfrage sieht wie in Abbildung 8.8 aus.
Abbildung 8.8: Abfrageergebnis einer Gruppierung von Datensätzen mit der Anzahl Datensätze jeder Gruppe
Sie können auch nach mehreren Feldern gruppieren. Ist die Abfrage aus dem vorherigen Beispiel nicht aussagekräftig genug, können Sie auch noch den Hersteller in die Gruppierung einbeziehen: SELECT KategorieID, HerstellerID, Count(ProduktID) AS AnzahlProdukte FROM tblProdukte GROUP BY KategorieID, HerstellerID;
Diese Variante gruppiert nach allen vorhandenen Kombinationen aus Kategorie und Hersteller, wie das Ergebnis in Abbildung 8.9 zeigt.
Abbildung 8.9: Gruppierung nach einer Kombination aus mehreren Feldern
473
Kapitel 8
Gruppierungen einschränken Es gibt zwei verschiedene Möglichkeiten, das Ergebnis einer gruppierten Abfrage einzu schränken: vor dem Gruppieren der enthaltenen Datensätze und nach dem Gruppieren der enthaltenen Datensätze. Die erste Möglichkeit kennen Sie bereits: Mit der WHERE-Klausel leiten Sie einen Bereich ein, der Kriterien für die anzuzeigenden Datensätze enthält. Den WHERE-Abschnitt fügen Sie dabei hinter dem FROM-Bereich, aber vor dem GROUP BY-, HAVING- oder ORDER BY-Bereich ein. Das folgende Beispiel ermittelt die Anzahl der Produkte teurer als EUR 50,– je Kategorie. Dabei werden durch die WHERE-Bedingung zunächst alle Produkte ermittelt, die mehr als EUR 50,– kosten, und anschließend wird die Gruppierung durchgeführt: SELECT KategorieID, HerstellerID, Count(ProduktID) AS AnzahlProdukte FROM tblProdukte WHERE Preis > 50 GROUP BY KategorieID, HerstellerID;
Die zweite Möglichkeit zum Einschränken einer gruppierten Abfrage führt zunächst die Gruppierung durch und wertet dann das Ergebnis einer Aggregatfunktion als Kriterium aus. Dazu wird – analog zur WHERE-Klausel – die HAVING-Klausel zum Bereitstellen des Kriteriums beziehungsweise der Kriterien verwendet. Im folgenden Beispiel sollen die Produkte nach Kategorie und Hersteller gruppiert und die Anzahl der Produkte je Gruppierung ermittelt werden, bevor die Abfrage diejenigen Kombinationen he rausfiltert, die mit weniger als zwei Produkten vertreten sind. Abbildung 8.10 zeigt das Ergebnis des folgenden SQL-Ausdrucks an: SELECT KategorieID, HerstellerID, Count(ProduktID) AS AnzahlProdukte FROM tblProdukte GROUP BY KategorieID, HerstellerID HAVING Count(ProduktID) > 1;
Abbildung 8.10: Abfrageergebnis eines SQL-Ausdrucks mit HAVING-Klausel
474
Access-SQL
8.3.8 WHERE, GROUP BY, HAVING und ORDER BY im Überblick Die vielfältigen Möglichkeiten zum Einschränken, Gruppieren und Sortieren der Ergeb nismenge eines SQL-Ausdrucks bergen ein Problem: Wie soll man sich die Reihenfolge der unterschiedlichen Bereiche merken? Folgendes hilft vielleicht: WHERE schränkt die Daten ein und kommt daher zuerst. GROUP BY gruppiert die übrig gebliebenen Datensätze. HAVING schränkt ebenfalls Daten ein, aber nur auf Basis von Gruppierungen. ORDER BY kommt zum Schluss, weil alles andere vergeudete Rechenleistung wäre. Diese Reihenfolge scheint intuitiv richtig zu sein – das reicht als Eselsbrücke für den Aufbau von SQL-Anweisungen aus. Tatsächlich werden Abfragen mitunter auch anders abgearbeitet – mehr dazu erfahren Sie in Kapitel 14 im Abschnitt 14.2.1, »Abfragen und die ACE-Engine«.
8.3.9 Verknüpfen von Tabellen in Abfragen Wenn Sie mehrere Tabellen im FROM-Bereich als Datenherkunft eines SQL-Ausdrucks angeben, werden diese von der ACE-Engine als völlig losgelöst von jeglichen zuvor angelegten Beziehungen zwischen den Tabellen betrachtet.
Manuelles Hinzufügen einer Verknüpfung Sie können durchaus eine 1:n-Beziehung mit referentieller Integrität zwischen Tabellen wie tblProjekte und tblMitarbeiter im Beziehungsfenster angelegt haben – sobald Sie diese beiden Tabellen in der folgenden Form in einem SQL-Ausdruck angeben, ist alles ver gessen: SELECT tblProjekte.ProjektID, tblMitarbeiter.MitarbeiterID FROM tblProjekte, tblMitarbeiter;
Die Abfrage-Entwurfsansicht hat da ein etwas besseres Gedächtnis. Sie erkennt direkt bestehende Beziehungen und zeigt diese auch an, wie in Abbildung 8.11. Allerdings können Sie auch diese Möglichkeit ausschalten: Dazu deaktivieren Sie auf der Registerseite Objekt-Designer des Access-Optionen-Dialogs (Office-Menü, Schaltfläche Access-Optionen) die Eigenschaft AutoVerknüpfung aktivieren. Ein Wechsel von hier aus in die SQL-Ansicht zeigt, welchen zusätzlichen Code die Festlegung einer Beziehung zwischen zwei Tabellen mit sich bringt:
475
Kapitel 8
Abbildung 8.11: Die Abfrage-Entwurfsansicht übernimmt bestehende Beziehungen nach dem Hinzufügen von verknüpften Tabellen
SELECT tblMitarbeiter.MitarbeiterID, tblProjekte.ProjektID FROM tblMitarbeiter INNER JOIN tblProjekte ON tblMitarbeiter.MitarbeiterID = tblProjekte.MitarbeiterID;
Es gibt noch eine weitere Möglichkeit, zwei Tabellen innerhalb eines SQL-Ausdrucks zu verknüpfen. Dabei erfolgt die Verknüpfung im WHERE-Teil der Abfrage: SELECT tblMitarbeiter.MitarbeiterID, tblProjekte.ProjektID FROM tblMitarbeiter, tblProjekte WHERE tblMitarbeiter.MitarbeiterID = tblProjekte.MitarbeiterID;
Diese Variante schreibt sich zwar etwas kürzer, kann aber keine OUTER JOIN-Verknüp fungen abbilden – mehr zu dieser Verknüpfungsart später. Noch gravierender ist der Nachteil, dass das Abfrageergebnis nicht aktualisiert werden kann – das zeigt ein Blick auf die Datenblatt-Ansicht einer solchen Abfrage (siehe Abbildung 8.12). Dort findet sich keine Möglichkeit, einen neuen Datensatz anzulegen, und auch eine Änderung der enthaltenen Daten ist nicht möglich.
Abbildung 8.12: WHERE-Verknüpfungen sind nicht aktualisierbar
476
Access-SQL
Im Entwurfsfenster einer solchen Abfrage erscheint übrigens keine Verknüpfungslinie, die WHERE-Bedingung wird wie üblich im Entwurfsraster angezeigt (siehe Abbil dung 8.13).
Abbildung 8.13: Beziehung per Kriterium
Aufbau von Verknüpfungen (INNER JOIN) Einfache Verknüpfungen zwischen zwei Tabellen definiert man in SQL folgendermaßen: INNER JOIN ON . = .;
Kommen weitere Tabellen hinzu, setzt man den Ausdruck für die erste Verknüpfung in Klammern und behandelt ihn für die Erstellung der zweiten Verknüpfung wie eine einzige Tabelle. Die neue Verknüpfung baut man einfach um den bestehenden Ausdruck herum wie im folgenden Beispiel (neue Teile fett gedruckt): INNER JOIN ( INNER JOIN ON . = .) ON .;
Beispiel: Die klassische m:n-Beziehung zwischen Bestellungen, Bestelldetails und Arti keln enthält zwei INNER JOIN-Verknüpfungen (siehe Abbildung 8.14). Der entsprechende SQL-Ausdruck sieht folgendermaßen aus: SELECT tblBestelldetails.ArtikelID, tblArtikel.Artikel, tblBestelldetails. Anzahl, tblBestellungen.Lieferdatum FROM tblBestellungen
477
Kapitel 8 INNER JOIN ( tblArtikel INNER JOIN tblBestelldetails ON tblArtikel.ArtikelID = tblBestelldetails.ArtikelID ) ON tblBestellungen.BestellungID = tblBestelldetails.BestellungID;
Abbildung 8.14: Abfrage mit zwei Verknüpfungen
Weitere Verknüpfungsarten Neben den INNER JOINS, die alle Datensätze ausgeben, bei denen die Inhalte des Ver knüpfungsfeldes gleich sind, gibt es noch weitere Verknüpfungsarten. Ein OUTER JOIN liefert alle Datensätze der ersten Tabelle zurück und nur die Datensätze der zweiten Tabelle, die mit einem Datensatz der ersten Tabelle verknüpft sind. OUTER JOIN-Abfragen treten als LEFT OUTER JOIN und RIGHT OUTER JOIN auf. LEFT beziehungsweise RIGHT legt dabei fest, ob alle Datensätze von der links vom Schlüsselwort JOIN stehenden Tabelle ausgegeben werden oder von der rechts davon stehenden Ta belle. Beispiel: Der folgende SQL-Ausdruck soll alle Mitarbeiter und ihre Projekte ausgeben, aber auch die Mitarbeiter berücksichtigen, denen kein Projekt zugeordnet ist. SELECT Nachname & ", " & Vorname AS Mitarbeiter, tblProjekte.Projekt FROM tblMitarbeiter LEFT JOIN tblProjekte ON tblMitarbeiter.MitarbeiterID=tblProjekte.MitarbeiterID;
Abbildung 8.15 zeigt das Ergebnis dieses SQL-Ausdrucks. Für die Verwendung von OUTER JOIN-Verknüpfungen gibt es zwei wichtige Regeln: Die rechte Tabelle eines LEFT OUTER JOIN kann nicht mit anderen Tabellen per INNER
478
Access-SQL
JOIN verknüpft werden und kann nicht die linke Tabelle eines anderen RIGHT OUTER JOIN oder die rechte Tabelle eines LEFT OUTER JOIN sein. Sie erhalten sonst die Fehler meldung »Abfrage konnte nicht ausgeführt werden, da sie mehrdeutige Inklusionsver knüpfungen enthält«.
Abbildung 8.15: Ergebnis einer Abfrage mit LEFT OUTER JOIN-Verknüpfung
Rekursive Verknüpfungen lassen sich ebenfalls mit SQL realisieren. Allerdings gibt es dafür kein spezielles Sprachkonstrukt. Statt dessen verwenden Sie einfach einen kleinen Trick. Zum Herstellen einer Beziehung kommen Sie definitiv nicht daran vorbei, zwei Tabellen miteinander zu verknüpfen – es ist aber nicht verboten, zweimal die gleiche Tabelle zu nehmen. Sie müssen nur eine der Tabellen – wie weiter oben erläutert – mit dem ASSchlüsselwort umbenennen. Das folgende Beispiel zeigt, wie es funktioniert. Das Abfrageergebnis finden Sie in Abbildung 8.16. SELECT tblMitarbeiter.MitarbeiterID, tblMitarbeiter.Nachname & ", " & tblMitarbeiter.Vorname AS Mitarbeiter, tblVorgesetzte.Nachname & ", " & tblVorgesetzte.Vorname AS Vorgesetzter FROM tblMitarbeiter INNER JOIN tblMitarbeiter AS tblVorgesetzte ON tblMitarbeiter.VorgesetzterID = tblVorgesetzte.MitarbeiterID;
In der Bedingung für die Herstellung der Verknüpfung (den ON-Abschnitt) haben die bisherigen Beispiele immer das Gleichheitszeichen als Vergleichsoperator verwendet. Sie können auch andere Vergleichsoperatoren wie >, >=, <, <=, <> oder BETWEEN einsetzen. Der Anteil von SQL-Ausdrücken mit anderen Vergleichsoperatoren als dem Gleichheitszeichen ist aber relativ gering.
479
Kapitel 8
Abbildung 8.16: Beispiel für die Ausgabe rekursiv verknüpfter Daten
Unterabfragen Weiter oben in Abschnitt 8.3.4 unter »Vergleiche mit Zahlen« haben Sie bereits erfahren, dass man mit dem IN-Operator Vergleiche von Feldinhalten mit einer Gruppe von Werten vornehmen kann. Es wäre sehr unpraktisch, wenn man die Vergleichswerte immer manuell eintragen müsste. Unterabfragen lösen dieses und andere Probleme auf elegante Weise: Sie treten als Abfrage in der Abfrage auf und liefern so die Vergleichswerte für Kriterienausdrücke. Dabei können diese entweder nur einen einzelnen Wert als Vergleichswert zurückliefern oder eine Gruppe von Werten eines Feldes, das mit dem IN-Operator untersucht wird. Beispiel für eine Unterabfrage, die einen Vergleichswert liefert: Der folgende SQL-Aus druck liefert alle Produkte zurück, deren Preis über dem durchschnittlichen Preis aller Produkte liegt. SELECT ProduktID, Produkt, Preis FROM tblProdukte WHERE Preis > (SELECT Avg(Preis) FROM tblProdukte);
Behandlung doppelter Datensätze in Ergebnissen von SQL-Ausdrücken SQL-Ausdrücke, die mehrere Abfragen enthalten, liefern oft doppelte Daten zurück. Das ist zum Beispiel der Fall, wenn Sie eine Projekt- und eine Kundentabelle verknüpfen und alle Kunden ausgeben möchten, denen ein aktuelles Projekt zugewiesen ist: SELECT tblKunden.KundeID, tblKunden.Firma FROM tblKunden INNER JOIN tblProjekte ON tblKunden.KundeID = tblProjekte.KundeID;
480
Access-SQL
Wenn es zwei oder mehr Projekte im Auftrag eines Kunden gibt, wird dieser Kunde im Ergebnis auch mehrfach angezeigt, wie Abbildung 8.17 zeigt. In dieser Abfrage wird implizit das Prädikat ALL für den SELECT-Teil der Abfrage verwendet. Das bedeutet, dass alle Datensätze des Ergebnisses ausgegeben werden. Der folgende SQL-Ausdruck mit dem ALL-Prädikat erfüllt die gleiche Funktion: SELECT ALL tblKunden.KundeID, tblKunden.Firma FROM tblKunden INNER JOIN tblProjekte ON tblKunden.KundeID = tblProjekte.KundeID;
Abbildung 8.17: Mehrfache Anzeige von Kunden
Wenn Sie gleiche Datensätze nicht doppelt anzeigen möchten, können Sie eines der Prädikate DISTINCT oder DISTINCTROW verwenden. Jedes dieser Prädikate geben Sie unmittelbar hinter dem SELECT-Schlüsselwort an. Mit dem DISTINCT-Prädikat sorgen Sie dafür, dass das Abfrageergebnis jede Kombination der ausgegebenen Daten nur einmal anzeigt. Dieses Prädikat hat die glei che Wirkung wie das Einstellen der Eigenschaft Keine Duplikate (in Visual Basic: Unique Values) auf den Wert Ja (True). Im folgenden Beispiel werden also tatsächlich nur die Firmen angezeigt, die mit einem Projekt verknüpft sind: SELECT DISTINCT tblKunden.KundeID, tblKunden.Firma FROM tblKunden INNER JOIN tblProjekte ON tblKunden.KundeID = tblProjekte.KundeID;
Das DISTINCTROW-Prädikat bringt in vielen Fällen das gleiche Ergebnis wie das DISTINCT-Prädikat. Es bezieht sich nicht nur auf die im Abfrageergebnis angezeigten Felder, sondern auf die gesamte Datenherkunft der Abfrage. Das Prädikat hat die gleiche Wirkung wie das Einstellen der Eigenschaft Eindeutige Datensätze (UniqueRecords) auf den Wert Ja (True). Ein weiterer großer Unterschied zwischen DISTINCT und DISTINCTROW ist, dass das Ergebnis einer Abfrage mit dem DISTINCTROW-Prädikat aktualisierbar ist. Außerdem
481
Kapitel 8
ist DISTINCTROW eine Spezialität von Access-SQL, die nicht dem SQL-Standard entspricht. Ein gutes Verständnis für den Unterschied liefert folgendes Beispiel. Die Tabelle aus Abbildung 8.18 enthält Mitarbeiterdaten. Die folgenden beiden Abfragen sollen nur die Vornamen der Mitarbeiter ausgeben und damit zeigen, wie sich die beiden Prädikate auswirken.
Abbildung 8.18: Beispieltabelle für den Einsatz von DISTINCT und DISTINCTROW
Die erste Abfrage verwendet das DISTINCT-Prädikat. Sie liefert das Ergebnis des linken Fensters in Abbildung 8.19 – der Vorname Bernd wird nur einfach ausgegeben, obwohl er zweimal in der zugrunde liegenden Tabelle enthalten ist. Der Grund ist, dass DISTINCT nur die ausgegebenen Felder auf Duplikate untersucht. SELECT DISTINCT Vorname FROM tblMitarbeiter;
Die zweite Abfrage mit dem DISTINCTROW-Prädikat gibt beide Mitarbeiter mit dem Vornamen Bernd aus. Diese Abfrage untersucht die ganze Tabelle tblMitarbeiter. Deren Datensätze sind natürlich alle verschieden, da zumindest der Primärschlüssel einen eindeutigen Wert enthält. SELECT DISTINCTROW Vorname FROM tblMitarbeiter;
Die ersten x oder die ersten x Prozent der Datensätze ausgeben Wenn Sie das Ergebnis einer Abfrage dahingehend einschränken möchten, dass nur die ersten x Datensätze oder die ersten x Prozent der Datensätze ausgegeben werden sollen, verwenden Sie das TOP-Prädikat. Die folgende Beispielabfrage liefert die drei teuersten Produkte einer Tabelle zurück: SELECT TOP 3 ProduktID, Produkt, Preis FROM tblProdukte ORDER BY Preis;
482
Access-SQL
Abbildung 8.19: Ergebnisse der DISTINCT- und der DISTINCTROW-Version einer Abfrage
Wichtig ist bei der Verwendung des TOP-Prädikats natürlich die Sortierung der Daten sätze nach dem gewünschten Kriterium. Der nächste SQL-Ausdruck ermittelt die teuers ten zehn Prozent der Produkte: SELECT TOP 10 PERCENT ProduktID, Produkt, Preis FROM tblProdukte ORDER BY Preis;
Die Prozent-Auswahl gibt »angebrochene« Datensätze mit aus – wenn Sie also zehn Pro zent von 21 Produkten ausgeben möchten, erhalten Sie drei Datensätze als Ergebnis.
Parameter verwenden Wenn Sie eine Abfrage mit Parametern verwenden möchten, wie es auch in der AbfrageEntwurfsansicht in Abbildung 8.20 möglich ist, reicht es aus, die Parameter wie in der Entwurfsansicht in eckigen Klammern als Kriterium anzugeben: SELECT MitarbeiterID, Vorname, Nachname FROM tblMitarbeiter WHERE Vorname=[Vorname eingeben];
Sie können zusätzlich die PARAMETERS-Klausel verwenden, um einen Datentyp für die Parameter festzulegen. Sie beginnt mit dem Schlüsselwort PARAMETERS und ent hält eine durch Kommata getrennte Liste von Wertepaaren, die aus dem in eckigen Klammern gefassten Parameternamen und dem Datentyp bestehen. Das folgende Bei spiel zeigt die vorherige Abfrage mit der PARAMETERS-Auflistung: PARAMETERS [Vorname eingeben] STRING; SELECT MitarbeiterID, Vorname, Nachname
483
Kapitel 8 FROM tblMitarbeiter WHERE Vorname=[Vorname eingeben];
Abbildung 8.20: Abfrage mit Parameter in der Entwurfsansicht
Die Verwendung des PARAMETERS-Schlüsselworts macht nur Sinn, wenn Sie die Ein gabe eines Wertes mit dem richtigen Datentyp erzwingen möchten. In der Regel sollten solche Validierungen aber bereits im Code erfolgen und nicht auf Abfrage-Ebene festge legt werden.
Zusammenfassen von Abfrageergebnissen mit UNION Das UNION-Schlüsselwort von SQL ermöglicht das Zusammenfassen der Ergebnisse mehrerer SELECT-Anweisungen. Damit können Sie beispielsweise die Adressen aus einer Mitarbeiter- und einer Kundentabelle in einen Topf werfen, um eine Verteilerliste für Weihnachtskarten zu erstellen. Der Einsatz von UNION ist ganz einfach: Sie fassen damit zwei Abfragen zusammen, indem Sie zwei oder mehr Abfragen mit diesem Schlüsselwort verbinden: Abfrage1 UNION Abfrage2 [UNION Abfrage 3 […]]
Voraussetzung für den Einsatz einer UNION-Abfrage ist, dass alle beteiligten Abfragen die gleiche Anzahl Felder aufweisen. Der Vorteil von UNION-Abfragen ist, dass auch Daten zusammengeführt werden können, die in den einzelnen Tabellen in unterschiedlicher Form auftreten. Das folgende Beispiel zeigt, wie Sie Vorname und Nachname der Mitarbeitertabelle zusammensetzen und mit den in einem einzigen Feld gespeicherten Namen des Ansprechpartners von Kunden zusammenführen können:
484
Access-SQL SELECT Vorname & " " & Nachname AS Empfaenger FROM tblMitarbeiter UNION SELECT Ansprechpartner FROM tblKunden;
Wenn Sie für das Ergebnis einer UNION-Abfrage eine Sortierung vornehmen möchten, müssen Sie diese im Anschluss an die letzte Abfrage angeben. Sortierkriterien in den vorherigen Abfragen werden nicht berücksichtigt. Als Feldname des Abfrageergebnisses verwendet Access immer den in der ersten Abfrage angegebenen Feldnamen. UNION-Abfragen können nicht in der Entwurfsansicht für Abfragen angelegt werden. Wenn Sie alle Felder einer Abfrage oder Tabelle in einer UNION-Abfrage verwenden möchten, müssen Sie diese nicht alle im Quellcode angeben. Es reicht die Sternchen(*)Syntax: SELECT * FROM tblMitarbeiter. Aber auch diese Schreibweise können Sie noch abkürzen: TABLE tblMitarbeiter liefert genauso alle Felder der Tabelle zurück. Diese Syntax können Sie übrigens auch für Abfragen verwenden. Außerdem ist der Einsatzbereich dieser Abkürzung nicht auf UNION-Abfragen beschränkt.
8.3.10 Zugriff auf externe Datenquellen Access-SQL bietet die Möglichkeit, per angehängter IN-Klausel im Anschluss an die Angabe der beteiligten Tabellen eine externe Datenbank oder Datei als Datenquelle festzulegen. Das sieht dann beispielsweise wie folgt aus: SELECT * FROM tblMitarbeiter IN '\<.accdb-Dateiname>'
Die Technik funktioniert nicht nur mit Access-Datenbanken, sondern auch mit dBase, Excel, Exchange, HTML, Lotus, Outlook, Paradox und Textdateien in den verschiedenen Versionen. Dies ist allerdings nur eine Alternative dazu, die externen Daten als verknüpfte Tabellen in die aktuelle Access-Datenbank einzubinden. Das Einbinden bringt in den meisten Fällen weniger Probleme, da Sie damit die externen Daten an einer zentralen Stelle verwalten. So müssen Sie nicht in jeder Abfrage auf eine externe Datenquelle verweisen, sondern können auf die verknüpfte Tabelle zugreifen.
8.3.11 Zugriff auf Felder des Datentyps Anlage und mehrwertige Felder Die ACE-Engine vereinfacht die Verwaltung von Dateien und m:n-Beziehungen durch einen neuen Datentyp beziehungsweise zusätzliche Feldeigenschaften für Felder mit
485
Kapitel 8
Nachschlage-Feldern. In Anlage-Feldern können Sie eine oder mehrere Dateien speichern, mehrwertige Felder dienen der Auswahl eines oder mehrerer Einträge einer Liste. Dies lässt sich auch per SQL nachbilden. Auf diese Daten können Sie auch per SQLAbfrage zugreifen. Der einfachste Weg, die passende SQL-Abfrage zu erhalten, liegt im Einsatz der Abfrage-Entwurfsansicht: Hier legen Sie einfach eine Abfrage an, die auf einer Tabelle mit einem mehrwertigen Feld oder einem Anlage-Feld basiert, und ziehen die gewünschten Felder in das Entwurfsraster. Der Wechsel in die SQL-Ansicht liefert dann den resultierenden SQL-Ausdruck. Der SQL-Ausdruck für die Abfrage aus Abbildung 8.21 sieht etwa wie folgt aus: SELECT tblRezepteMVF.RezeptID, tblRezepteMVF.Bezeichnung, tblRezepteMVF. Zutaten.Value FROM tblRezepteMVF;
Abbildung 8.21: Eine Abfrage mit mehrwertigem Feld in der Entwurfsansicht
Ganz ähnlich sieht das für Anlage-Felder aus, nur dass ein Anlage-Feld noch ein paar »Unterfelder« mehr hat.
8.4 Daten manipulieren Mit SQL lassen sich natürlich nicht nur Daten abfragen, sondern auch anlegen, ändern und löschen. Dazu dienen im Wesentlichen vier verschiedene Befehle, deren Ausprägungen und Eigenschaften Sie in den folgenden Abschnitten kennen lernen.
8.4.1 Daten aktualisieren Das Aktualisieren von Daten mit SQL erfolgt über die UPDATE-Anweisung:
486
Access-SQL UPDATE SET = <Wert1>[, = <Wert2>][, ...] [WHERE ]
Dabei können Sie als beliebige Tabellen oder aktualisierbare Abfragen angeben. Sie können auch verknüpfte Tabellen eingeben, was kein SQL-Standard ist. Dafür fällt die vom Standard vorgesehene Möglichkeit der Verwendung von Unterabfragen weg. , , … müssen in enthalten sein. <Wert1>, <Wert2>, … sind die Werte, die den Feldern , , … zugewiesen werden sollen. Mit legen Sie einen Ausdruck fest, der die von der Aktualisierung betroffenen Datensätze einschränkt. Die folgende Aktualisierungsabfrage erhöht die Preise aller Produkte, die teurer als EUR 50,– sind, um zehn Prozent: UPDATE tblProdukte SET Preis = [Preis]*1.1 WHERE Preis>50;
8.4.2 Daten löschen Löschabfragen lassen sich in SQL mit dem DELETE-Schlüsselwort ausführen. Sie haben die folgende Syntax: DELETE [.*] FROM [WHERE ]
und sind identisch, wenn unter nur eine Tabelle angegeben wird. In diesem Fall kann .* weggelassen werden. .* müssen Sie nur angeben, wenn einen aus mehreren verknüpften Tabellen bestehenden Ausdruck enthält. Diese Löschabfrage bezieht sich auf eine einzige Tabelle: DELETE FROM tblKunden WHERE KundeID = 1;
8.4.3 Daten an bestehende Tabelle anfügen Die Anweisung zum Anfügen von Daten an eine bestehende Tabelle heißt INSERT INTO. Sie hat die folgende Syntax: INSERT INTO
Als geben Sie den Namen der Tabelle an, an die die Daten angefügt werden sollen. Für den Ausdruck gibt es zwei Möglichkeiten:
487
Kapitel 8
Angabe einer SELECT-Anweisung Direkte Angabe der Felder und der einzufügenden Werte
Anzufügende Daten per SELECT-Anweisung angeben Wenn Sie eine SELECT-Anweisung als Datenherkunft für das Anfügen von Daten an eine Tabelle verwenden, müssen Sie darauf achten, dass die Datentypen der anzufügenden Felder mit den Zielfeldern übereinstimmen und dass alle Felder, die eine Eingabe erfordern, auch gefüllt werden. Die folgende Abfrage kopiert alle Datensätze der Produkte-Tabelle in die Tabelle tblProdukteArchiv, die auslaufen und deren Lagerbestand erschöpft ist: INSERT INTO tblProdukteArchiv(ProduktID, Produkt, Preis, KategorieID, HerstellerID, LaeuftAus, Lagerbestand) SELECT ProduktID, Produkt, Preis, KategorieID, HerstellerID, LaeuftAus, Lagerbestand FROM tblProdukte WHERE LaeuftAus = True AND Lagerbestand = 0;
Eine kürzere Fassung dieses SQL-Ausdrucks wäre folgende: INSERT INTO tblProdukteArchiv SELECT ProduktID, Produkt, Preis, KategorieID, HerstellerID, LaeuftAus, Lagerbestand FROM tblProdukte WHERE LaeuftAus = True AND Lagerbestand = 0;
Und nun die kürzeste Version: INSERT INTO tblProdukteArchiv SELECT * FROM tblProdukte WHERE LaeuftAus = True AND Lagerbestand = 0;
Anzufügende Daten direkt angeben Die erste und längste Variante des oben genannten Beispiels ist auch Grundlage für das Anfügen eines Datensatzes, dessen Daten nicht aus einer Tabelle ermittelt werden: INSERT INTO tblProdukteArchiv(ProduktID, Produkt, Preis, KategorieID, HerstellerID, LaeuftAus, Lagerbestand) VALUES(100, 'Testprodukt', 50, 2, 3, TRUE, 0)
Die einzufügenden Werte geben Sie in diesem Fall direkt in der VALUES-Liste an. Beachten Sie, dass Sie genau die Reihenfolge einhalten, die durch die Feldliste im INSERT INTO-Abschnitt angegeben ist, und dass die Syntax für die Datentypen korrekt
488
Access-SQL
ist: Texte in Hochkommata, Datumswerte gemäß Abschnitt 8.3.4 unter »Vergleiche mit Datumsangaben« formatiert. Diese Variante der INSERT INTO-Anweisung funktioniert nur, wenn das Feld ProduktID nicht als Autowert-Feld deklariert ist. In einer Tabelle zum Archivieren von Datensätzen kann es sinnvoll sein, keinen Autowert zu verwenden, sondern die ProduktID zu überneh men – so können später Informationen über Bestellungen dieses Produkts wieder her gestellt werden. Wenn das Feld ProduktID auch in der Zieltabelle tblProdukteArchiv den Datentyp Autowert hat, können Sie diesem Feld keinen Wert mit der Anfügeabfrage übergeben. Die obige Variante müssten Sie entsprechend kürzen, damit Access die ProduktID selbst anlegen kann: INSERT INTO tblProdukteArchiv(Produkt, Preis, KategorieID, HerstellerID, LaeuftAus, Lagerbestand) VALUES('Testprodukt', 50, 2, 3, TRUE, 0)
Sie können auch feste Werte mit dem SELECT-Schlüsselwort angeben: INSERT INTO tblProdukteArchiv(Produkt, Preis, KategorieID, HerstellerID, LaeuftAus, Lagerbestand) SELECT 'Testprodukt', 50, 2, 3, TRUE, 0
8.4.4 Neue Tabelle mit Daten erstellen Mit der SELECT INTO-Anweisung können Sie das Erstellen einer neuen Tabelle und das Anfügen von Daten in einem Schritt erledigen. Die Syntax dieses Abfragetyps ist fast identisch mit der für normale SELECT-Anweisungen – mit der Ausnahme, dass Sie mit dem INTO-Schlüsselwort noch den Namen der Tabelle angeben, die Access erstellen und mit den Daten des Abfrageergebnisses füllen soll. Den INTO-Abschnitt platzieren Sie einfach zwischen den SELECT- und den FROMAbschnitt einer Abfrage. Das folgende Beispiel zeigt, wie Sie die Abfrage zur Ermittlung der Anzahl Produkte je Kategorie und Hersteller mit einem Preis über EUR 50,– in eine neue Tabelle schreiben: SELECT KategorieID, HerstellerID, Count(ProduktID) AS AnzahlProdukte INTO tblAnzahlProdukteJeKategorieTeurerAls50EUR FROM tblProdukte WHERE Preis>50 GROUP BY KategorieID, HerstellerID;
Das Ergebnis bietet natürlich nicht den Komfort des eigentlichen Abfrageergebnisses – dort wurden dank der Festlegung von Nachschlagefeldern für die Felder KategorieID und HerstellerID noch die entsprechenden Informationen aus den verknüpften Tabellen angezeigt. Hier finden Sie nur nackte Zahlen, wie Abbildung 8.22 zeigt.
489
Kapitel 8
Abbildung 8.22: Ergebnis einer SELECT INTO-Abfrage
8.5 Datenmodell erstellen und manipulieren Neben dem Abfragen und Manipulieren der Inhalte von Tabellen können Sie mit SQL auch die Tabellen sowie die damit zusammenhängenden Elemente wie Felder und Indizes erstellen und wieder löschen. Die folgenden Abschnitte beschreiben die dazu notwendigen Anweisungen.
8.5.1 Tabellen erstellen Das Erstellen einer Tabelle erfolgt über die CREATE TABLE-Anweisung. Die Anweisung hat folgende Syntax: CREATE [TEMPORARY] TABLE ( [()] [NOT NULL] [] [, [()] [NOT NULL] [] [, ...]] [, <Mehrfelderindex> [, ...]])
gibt den Namen der zu erstellenden Tabelle an. Anschließend folgen in Klammern die einzelnen Felder der Tabelle. Felder können Sie auch nachträglich hinzufügen, Sie brauchen lediglich ein Feld beim Erstellen der Tabelle anzugeben. Für jedes Feld geben Sie den Datentyp () sowie gegebenenfalls die Größe () an. Dafür stehen die Datentypen aus Tabelle 8.1 zur Verfügung. Die unter der ACE neu eingeführten Datentypen ComplexType (mehrwertige Felder) und Attachment (Anlage) lassen sich per SQL offensichtlich nicht erzeugen. Den Parameter verwenden Sie nur bei Feldern des Datentyps Text. Sie geben damit die maximale Anzahl Zeichen an, die das Feld enthalten darf. Wenn das Feld keine Null-Werte enthalten darf, geben Sie das Schlüsselwort NOT NULL ein. Hinweis: Ab Microsoft Jet 4.0 werden die Inhalte von Text- und Memofeldern im Unicode-Format gespeichert. Da dieses mit zwei Byte pro Zeichen doppelt so viel Speicherplatz wie üblich benötigt und die Größe vorhandener Access-Anwendungen
490
Access-SQL
somit stark wachsen könnte, gibt es den Parameter WITH COMPRESSION für Felder, die als CHARACTER und MEMO angelegt wurden. Die Daten werden dann bei Bedarf in einem komprimierten Format gespeichert und beim Lesen wieder dekomprimiert. Der Parameter WITH COMPRESSION lässt sich allerdings nicht unter DAO einsetzen und führt zu einer Syntaxfehlermeldung – genauso wie in einer Datendefinitionsabfrage im SQL-Fenster. Für dieses Attribut benötigen sie ein Kommando auf eine ADOConnection. Soll das Feld auf irgendeine Weise indiziert werden – etwa als Primärschlüssel, eindeutiger Index oder einfacher Index –, tragen Sie unter Index1, Index2, … einen entsprechenden CONSTRAINT-Abschnitt ein. Wenn die Tabelle einen Mehrfelderindex erhalten soll, verwenden Sie ebenfalls einen CONSTRAINT-Abschnitt, den Sie hinter dem letzten Feld, aber noch innerhalb der Klammer eingeben. Mehr zur CONSTRAINT-Klausel erfahren Sie in Abschnitt 8.5.2, »Primärschlüssel, Indizes und Einschränkungen mit CONSTRAINT«. SQL-Datentyp
Datentyp im Tabellenentwurf
Größe
Werte
TEXT oder CHAR
Text
2 Byte pro Zeichen (siehe Anmerkungen)
Von 0 bis 255 Zeichen
LONGTEXT
Memo
2 Byte pro Zeichen (siehe Anmerkungen)
Von 0 bis maximal 1 GB
BYTE
Zahl (Byte)
1 Byte
Eine Ganzzahl von 0 bis 255
SMALLINT
Zahl (Integer)
2 Byte
Eine Ganzzahl von –32.768 bis 32.767
INTEGER
Zahl (Long Integer)
4 Byte
Eine lange Ganzzahl von –2.147.483.648 bis 2.147.483.647
REAL
Zahl (Single)
4 Byte
Eine Gleitkommazahl einfacher Genauigkeit, die die folgenden Werte annehmen kann: –3,402823E38 bis –1,401298E-45 für negative Werte, 1,401298E-45 bis 3,402823E38 für positive Werte und 0
FLOAT
Zahl (Double)
8 Byte
Eine Gleitkommazahl doppelter Genauigkeit, die die folgenden Werte annehmen kann: –1.79769313486232E308 bis –4.94065645841247E-324 für negative Werte, 4,94065645841247E-324 bis 1,79769313486232E308 für positive Werte und 0
Tabelle 8.1: Datentypen unter Access-SQL
491
Kapitel 8 SQL-Datentyp
Datentyp im Tabellenentwurf
Größe
Werte
GUID
Zahl (Replikations-ID)
16 Byte
Globally unique identifier (GUID)
DATETIME
Datum
8 Byte
Eine Datums- oder Zeitangabe ab dem Jahr 100 bis zum Jahr 9999
MONEY
Währung
8 Byte
Eine skalierte Ganzzahl von – 922.337.203.685.477,5808 bis 922.337.203.685.477,5807
COUNTER
Autowert (Long Integer)
4 Byte
Eine lange Ganzzahl von –2.147.483.648 bis 2.147.483.647
BIT
Ja/Nein
1 Byte
Ja/Nein-Werte (boolsche Werte) sowie Felder, die einen von zwei möglichen Werten enthalten
IMAGE
OLE-Objekt
Nach Bedarf
Von 0 bis maximal 1 GB. Wird für OLEObjekte verwendet
BINARY
Binär
Maximal 510 Byte
Byte-Array mit maximal 510 Werten
NUMERIC
Zahl (Double)
8 Byte
Wie FLOAT
Tabelle 8.2: Datentypen unter Access-SQL (Fortsetzung)
Die folgende SQL-Anweisung erstellt eine Tabelle mit allen in der Tabellen-Entwurfs ansicht auswählbaren Datentypen in der Reihenfolge des dortigen Vorkommens. Nicht enthalten ist der Datentyp Zahl (Dezimal): CREATE TABLE tblDatentypen( xText TEXT, xLongtext LONGTEXT, xByte BYTE, xSmallInt SMALLINT, xInteger INTEGER, xReal REAL, xFloat FLOAT, xUniqueIdentifier GUID, xDatetime DATETIME, xMoney MONEY, xCounter COUNTER, xBit BIT, xImage IMAGE xBinary BINARY, xNumeric NUMERIC); Listing 8.1: Erstellen einer Tabelle mit allen Datentypen
Die erstellte Tabelle sieht wie in Abbildung 8.23 aus.
492
Access-SQL
Abbildung 8.23: Das Produkt einer Tabellenerstellungsabfrage
Standardwerte vorgeben Unter ACE/Jet 4 (SQL-92) können Sie wie in der Abfrage-Entwurfsansicht einen Stan dardwert für ein Feld angeben. Dazu fügen Sie die Klausel DEFAULT gefolgt von dem gewünschten Standardwert hinter dem Felddatentyp des jeweiligen Feldes ein: CREATE TABLE tblBeispiel(FeldMitStandardwert TEXT(50) DEFAULT Standardwert)
Gültigkeitsregel festlegen Ebenfalls ab Jet 4 können Sie mit dem Schlüsselwort CHECK eine Gültigkeitsregel für ein Feld festlegen. CHECK ist ein zusätzliches CONSTRAINT und wird mit diesem Schlüsselwort eingeleitet. Beispiel: CREATE TABLE tblBeispiel(Beispielfeld TEXT(50), CONSTRAINT CHKBeispielfeld CHECK(Beispielfeld= 'A%'))
8.5.2 Primärschlüssel, Indizes und Einschränkungen mit CONSTRAINT Ohne einen Primärschlüssel läuft nichts in einem relationalen Datenbanksystem und auch die Verknüpfungen muss man irgendwo festlegen. Dazu dient die CONSTRAINTKlausel. Sie lässt sich an zwei Stellen einsetzen: für ein einzelnes Feld und als Mehrfeld einschränkung für mehrere Felder. Die Syntax ist in beiden Fällen unterschiedlich. Die Einzelfeldvariante sieht folgendermaßen aus:
493
Kapitel 8 CONSTRAINT Name {PRIMARY KEY | UNIQUE | NOT NULL | REFERENCES FremdTabelle [(FremdFeld1, FremdFeld2)] [ON UPDATE CASCADE | SET NULL] [ON DELETE CASCADE | SET NULL]
Die Mehrfeldeinschränkung mit dem CONSTRAINT-Schlüsselwort hat diese Syntax: CONSTRAINT Name {PRIMARY KEY (Primär1[,Primär2[, ...]]) | UNIQUE (Eindeutig1[,Eindeutig2[, ...]]) | NOT NULL (Nichtnull1[, Nichtnull2 [, ...]]) | FOREIGN KEY [NO INDEX] (Ref1[, Ref2 [, ...]]) REFERENCES FremdTabelle [(FremdFeld1 [, FremdFeld2 [, ...]])]} [ON UPDATE CASCADE | SET NULL] [ON DELETE CASCADE | SET NULL]
In den folgenden Abschnitten lernen Sie die Möglichkeiten dieses Elements genauer kennen.
Primärschlüssel anlegen Um eine Tabelle mit einem Primärschlüssel zu erstellen, können Sie beispielsweise folgende Anweisung verwenden: CREATE TABLE tblMitarbeiter(MitarbeiterID INTEGER CONSTRAINT KMitarbeiterID PRIMARY KEY, Vorname TEXT(50), Nachname TEXT(50))
Abbildung 8.24 zeigt das Ergebnis dieser Abfrage in der Entwurfsansicht.
Abbildung 8.24: Ergebnis einer Tabellenerstellungsabfrage
Eindeutigen Index anlegen Zum Anlegen einer Tabelle mit einem eindeutigen Index verwenden Sie das UNIQUESchlüsselwort. Der erweiterte SQL-Ausdruck aus dem vorherigen Beispiel fügt der Tabelle nun ein zusätzliches Feld für eine Mitarbeiternummer hinzu:
Wenn Sie eine CREATE TABLE-Anweisung verwenden und die zu erstellende Tabelle schon existiert, zeigt Access eine entsprechende Meldung an. Sie müssen die bestehende Tabelle also zuerst löschen oder umbenennen. Die Verwendung von UNIQUE entspricht dem Einstellen der Eigenschaft Indiziert des angegebenen Feldes auf den Wert Ja (Ohne Duplikate).
Feld darf nicht Null sein Um zu verhindern, dass eines der beiden Felder Vorname und Nachname der Mitarbeiter tabelle nicht gefüllt wird, können Sie die Tabelle so einstellen, dass die Eingabe erforder lich ist: CREATE TABLE tblMitarbeiter( MitarbeiterID INTEGER CONSTRAINT PKMitarbeiterID PRIMARY KEY,Mitarbeiternummer TEXT(10) CONSTRAINT UKMitarbeiternummer UNIQUE, Vorname TEXT(50) NOT NULL, Nachname TEXT (50) NOT NULL)
Die Verwendung von NOT NULL entspricht dem Einstellen der Eigenschaft Eingabe erforderlich auf den Wert Ja. Das gleiche Schlüsselwort kann auch in Mehrfeldeinschränkungen verwendet werden. Beachten Sie, dass jedes Feld nur einmal mit der Einschränkung NOT NULL ausgestattet werden darf.
Fremdschlüsselfelder festlegen Ein ganz wichtiges Element in relationalen Datenbanksystemen sind Fremdschlüssel felder. Dementsprechend bietet Access-SQL die Möglichkeit, eine Tabelle mit einem solchen Fremdschlüsselfeld anzulegen. Die Mitarbeitertabelle aus den vorherigen Beispielen soll auch Informationen über die Abteilung von Mitarbeitern enthalten, die in einer weiteren Tabelle namens tblAbteilungen enthalten sind. Zur Übung können Sie diese Tabelle kurz mit folgender Anweisung erstellen: CREATE TABLE tblAbteilungen( AbteilungID INTEGER CONSTRAINT PKAbteilungID PRIMARY KEY, Abteilung TEXT(50) CONSTRAINT UKAbteilung UNIQUE)
Nun passen Sie den SQL-Ausdruck zum Erstellen der Mitarbeitertabelle so an, dass sie direkt ein Fremdschlüsselfeld für die Verknüpfung mit der Tabelle tblAbteilungen anlegt:
In der Entwurfsansicht macht sich die besondere Eigenschaft des Feldes AbteilungID als Fremdschlüssel nicht direkt bemerkbar. Dafür wird die so erstellte Beziehung zwischen den beiden Tabellen tblMitarbeiter- und tblAbteilungen aber im Beziehungsfenster sichtbar (siehe Abbildung 8.25).
Abbildung 8.25: Eine per SQL-Anweisung erstellte Beziehung zwischen zwei Tabellen
Lösch- und Aktualisierungsweitergabe Im Eigenschaftenfenster einer Beziehung können Sie angeben, ob die Beziehung mit referentieller Integrität festgelegt und dabei Aktualisierungsweitergabe und Löschwei tergabe realisiert werden sollen (siehe Abbildung 8.26).
Abbildung 8.26: Optionen für Beziehungen mit referentieller Integrität
496
Access-SQL
Das Einstellen dieser Eigenschaften funktioniert erst mit SQL-92. Wenn Sie nicht mit dem Datenbankformat Access 2002 oder höher arbeiten und die Datenbank auf SQL Server-kompatible Syntax eingestellt haben, können Sie diese nur per VBA/ADO verwenden. Die folgende Beispielprozedur, die eine Tabelle mit Aktualisierungsweitergabe und Löschweitergabe anlegt, verwendet ADO für die Ausführung der SQL-Anweisung: Public Function CREATETABLEMitANSI92() Dim cnn As ADODB.Connection Dim strSQL As String strSQL = "CREATE TABLE tblMitarbeiter(" _ & "MitarbeiterID INTEGER CONSTRAINT PKMitarbeiterID PRIMARY KEY, " & "Mitarbeiternummer TEXT(10) " _ & "CONSTRAINT UniqueMitarbeiternummer UNIQUE, " _ & "Vorname TEXT(50) NOT NULL, " _ & "Nachname TEXT (50) NOT NULL, " _ & "AbteilungID INTEGER, " _ & "CONSTRAINT FKAbteilungID FOREIGN KEY (AbteilungID) " _ & "REFERENCES tblAbteilungen ON UPDATE CASCADE ON DELETE CASCADE)" Set cnn = CurrentProject.Connection cnn.Execute strSQL Set cnn = Nothing End Function Listing 8.2: Verwenden von SQL-92 per VBA und ADO
Einfache Indizes anlegen Ein Blick in den Entwurf der Tabelle tblMitarbeiter und dort auf den Dialog Indizes offenbart, dass dieses Feld noch nicht indiziert ist, obwohl es als Fremdschlüsselfeld dient. Aus Gründen der Performance sollte dies allerdings noch nachgeholt werden – indizierte Felder auf beiden Seiten einer Beziehung machen sich hier sehr gut. Einen solchen Index erstellen Sie nicht direkt mit der Tabelle, sondern mit einer separaten Anweisung: CREATE INDEX IndexAbteilung ON tblMitarbeiter(Vorname)
Abbildung 8.27 zeigt den Dialog Indizes mit dem neu hinzugefügten Index IAbtei lungID. Diesen Dialog aktivieren Sie, indem Sie die betroffene Tabelle in der Ent
497
Kapitel 8
wurfsansicht öffnen und dann im Entwurf-Tab des Ribbons auf die Schaltfläche Indi zes klicken.
Abbildung 8.27: Auflistung der Indizes der Tabelle tblMitarbeiter
Einsatz zusammengesetzter CONSTRAINTS Die oben genannten CONSTRAINTS können Sie auch für mehrere Felder gleichzeitig verwenden. Das bedeutet etwa beim Beispiel des eindeutigen Index, dass die Kombina tion von zwei oder mehr Feldern nicht in mehr als einem Datensatz gleich sein darf. Es können also durchaus die Inhalte eines oder mehrerer Felder zweier Datensätze überein stimmen, aber eben nicht alle.
Zusammengesetzter Primärschlüssel Einen zusammengesetzten Primärschlüssel benötigen Sie beispielsweise in Fällen, in denen Sie eine Verknüpfungstabelle für die Herstellung einer m:n-Beziehung erstellen. Meist sollen die Kombinationen der Datensätze aus den beiden betroffenen Tabellen ein deutig sein – zum Beispiel soll einem Fahrzeug jedes Ausstattungsmerkmal nur einmal zugewiesen werden. Die folgende SQL-Anweisung illustriert die Vorgehensweise. Das Ergebnis sieht wie in Abbildung 8.28 aus: CREATE TABLE tblFahrzeugeAusstattungsmerkmale( FahrzeugID INTEGER, AusstattungsmerkmalID INTEGER, CONSTRAINT PKFahrzeugIDAusstattungsmerkmalID PRIMARY KEY(FahrzeugID, AusstattungsmerkmalID))
Zusammengesetzter eindeutiger Schlüssel Die SQL-Anweisung zum Erstellen eines zusammengesetzten eindeutigen Schlüssels erfolgt genau wie beim zusammengesetzten Primärschlüssel – Sie müssen nur PRIMARY KEY durch UNIQUE KEY ersetzen.
498
Access-SQL
Abbildung 8.28: Tabelle mit zusammengesetztem Primärschlüssel
8.5.3 Tabelle ändern Bei Bedarf können Sie eine Tabelle und ihre Felder auch nachträglich ändern. Die Syntax für die ALTER TABLE-Anweisung lautet folgendermaßen: ALTER TABLE Tabelle {ADD {COLUMN Feld Typ[(Größe)] [NOT NULL] [CONSTRAINT Index] | ALTER COLUMN Feld Typ[(Größe)] | CONSTRAINT Mehrfeldindex} | DROP {COLUMN Feld I CONSTRAINT Indexname} }
Daraus lassen sich die drei Funktionen aus den folgenden Abschnitten ableiten.
Hinzufügen eines Feldes Um ein Feld einer bestehenden Tabelle hinzuzufügen, verwenden Sie beispielsweise folgende Anweisung, die das Feld Strasse an die Tabelle tblMitarbeiter anfügt: ALTER TABLE tblMitarbeiter ADD Strasse Text(50);
Auch die CONSTRAINT- und die NOT NULL-Klauseln lassen sich hiermit verwenden. Ein neues Fremdschlüsselfeld (etwa das Feld GeschlechtID in der Tabelle tblMitarbeiter) fügen Sie mit folgender Anweisung hinzu: ALTER TABLE tblMitarbeiter ADD GeschlechtID INTEGER, CONSTRAINT FKGeschlechtID FOREIGN KEY(GeschlechtID) REFERENCES tblGeschlecht;
Ändern eines Feldes Das Ändern bestehender Felder erfolgt prinzipiell auf dem gleichen Weg wie das Anlegen neuer Felder. Sie verwenden lediglich das Schlüsselwort ALTER COLUMN statt ADD COLUMN: ALTER TABLE tblMitarbeiter ALTER COLUMN Vorname TEXT(40);
Mit ALTER TABLE lassen sich nicht nur Feldnamen, sondern auch Datentypen und Feldgrößen ändern. Beachten Sie, dass nicht alle Quell- und Zielfeldtypen kompatibel sind.
499
Löschen eines Feldes Beim Löschen von Feldern müssen Sie beachten, dass Felder, die Teil eines Index sind, nur nach Entfernen des Index oder zusammen mit diesem gelöscht werden können. Die folgende Anweisung scheitert beispielsweise, wenn das Feld GeschlechtID mit der Tabelle tblGeschlecht verknüpft ist: ALTER TABLE tblMitarbeiter DROP GeschlechtID;
In diesem Fall entfernen Sie zunächst den Index: ALTER TABLE tblMitarbeiter DROP CONSTRAINT FKGeschlechtID;
Anschließend funktioniert die obige Anweisung zum Löschen des Feldes.
8.5.4 Tabelle löschen Zum Löschen einer Tabelle verwenden Sie die DROP-Anweisung zusammen mit der Objektart TABLE und dem Namen der zu löschenden Tabelle: DROP TABLE Tabelle
Die Tabelle tblMitarbeiter entfernen Sie beispielsweise mit folgender Anweisung: DROP TABLE tblMitarbeiter
8.5.5 Index löschen Auch Indizes lassen sich mit der DROP-Anweisung löschen. Hier lautet die Syntax: DROP INDEX Index ON Tabelle
Beispiel: DROP INDEX PKMitarbeiterID ON tblMitarbeiter
500
9 DAO DAO (Data Access Objects) ist neben ADO (Active Data Objects) eine der beiden Bibliotheken für den Zugriff auf die Daten in einer Access-Datenbank. In Access 97 war DAO noch Alleinunterhalter auf diesem Gebiet, ab Ac cess 2000 kam dann ADO hinzu. ADO war eigentlich als legitimer Nachfolger von DAO vorgesehen, denn es er möglicht sowohl die Verwendung von herkömmlichen Access-Datenbanken als auch von Access-Projekten (.adp), die ebenfalls mit Access 2000 eingeführt wurden. Diese Access-Projekte wurden mit Access 2007 nicht mehr wesentlich weiterentwickelt, sondern nur noch dem Einsatz mit dem SQL Server 2005 angepasst. Und es kommt noch besser: Microsoft empfiehlt in Zusammenhang mit dem SQL Server den Einsatz von ODBC. Im Gegensatz zu ADO hat Microsoft nur noch DAO für die neue Access-Version weiterentwickelt, und so ist im Rahmen der neuen Office-Version auch eine neue DAOVersion entstanden, die sich nunmehr Microsoft Office 12.0 Access database engine Objects nennt. In Access-Projekten, die die MSDE oder den Microsoft SQL Server als Datenbank-Backend verwenden, ist ADO die bessere Wahl, denn damit können Sie direkt via OLE DB auf die Daten zugreifen. Um von einer Access-Datenbank, also einer .accdb-Datei, an die Daten in einer MSDE- oder SQL Server-Datenbank heranzukommen, müssen Sie die benötigten Tabellen zunächst per ODBC verknüpfen und können dann per DAO oder ADO darauf zugreifen. Das ist in jedem Fall nur die zweitbeste Wahl, wird aber von Microsoft für Access 2007 so propagiert.
Kapitel 9
Beispiele auf CD: Sie finden alle Code-Beispiele dieses Kapitels auf der Buch-CD unter \Kap_09\DAO.accdb im Modul mdlDAO. Die Datenbankdatei enthält auch die für die Beispiele verwendeten Tabellen.
DAO hat also mit Access 2007 an Bedeutung zurückgewonnen, was auch damit zusammenhängt, dass DAO nunmehr allein Bestandteil von Office und nicht mehr von Windows ist. Daneben gibt es eine Menge weiterer Gründe, die alle genauso die Frage beantworten könnten, warum DAO sich letzten Endes – zumindest für den Einsatz mit der neuen Engine ACE – durchgesetzt hat: DAO gibt es bereits einige Zeit und es läuft mittlerweile stabil und fehlerfrei. DAO ist für den Einsatz mit der ACE-Engine optimiert und in Zusammenarbeit damit in vielen Fallen schneller als ADO. Einige Features von Access, etwa die Verwendung der RecordsetClones in Formularen oder das Komprimieren und Reparieren per VBA, erfordern den Einsatz von DAO. Aus diesen Gründen lässt sich schon erkennen, wann die Anwendung von DAO Sinn macht und wann man eher ADO den Vorzug geben sollte. Wenn Sie eine Access-Anwendung mit einem überschaubaren Lebenszyklus planen, deren Anwendungszweck keine Wünsche nach einer Erweiterung in Richtung eines »größeren« Systems, also MSDE oder SQL Server, beinhaltet, sind Sie mit DAO gut bedient. Teilweise ist ADO sicher performanter (etwa beim Schreiben von Daten), aber es unterstützt auch nicht die neuen Features von Access. Wenn Sie eine bestehende Access-Anwendung weiterentwickeln, die mit DAO arbeitet, hängt es ebenfalls von den Zukunftsplänen mit dieser Anwendung ab – bei einer geplanten Vergrößerung in Richtung SQL Server macht ein Umstieg auf ADO definitiv Sinn – je früher, desto besser. Je mehr hier noch in DAO hinzuprogrammiert wird, desto mehr ist anschließend nach ADO umzuwandeln. Sie können natürlich auch mit beiden Bibliotheken parallel arbeiten – und so bei Bedarf neue Bestandteile mit ADO entwickeln und Bestehendes Stück für Stück umwandeln. Wenn Sie DAO bereits kennen und sich noch nicht besonders mit ADO auseinander gesetzt haben, lohnt es unter Umständen vielleicht gar nicht mehr, sich noch mit ADO zu beschäftigen – ist doch ADO.NET bereits allgegenwärtig. Das ist allerdings hypothetisch, denn zum Zeitpunkt der Drucklegung dieses Buchs gab es noch keine verbindliche Aussage von Microsoft, ob und wann die .NET-Technologie (vielleicht in Form von VBA.NET?) Einzug in Access hält. Für den Fall, dass Sie aufgrund der oben genannten Gründe DAO für die richtige Datenzugriffstechnik unter VBA halten, finden Sie in den nächsten Abschnitten alles
502
DAO
Wichtige zu diesem Thema. Anderenfalls überspringen Sie dieses Kapitel einfach und wenden sich direkt Kapitel 10, »ADO« zu.
9.1 DAO und ADO im Einsatz In der Access-Welt gibt es Datenbanken, die nur DAO, nur ADO oder auch beide Objektbibliotheken verwenden. Wenn Sie niemals Probleme bekommen möchten, weil Sie die (etwas anderen) Methoden etwa eines Recordset-Objekts von DAO unter ADO einsetzen, sollten Sie direkt klarstellen, mit welchem Objektmodell Sie aktuell arbeiten (in Teilen ist dies mit der neuen DAO-Bibliothek kein Problem mehr, da diese ein neues Recordset2- und ein Field2-Objekt mitliefert – dazu später mehr). Im Verweise-Dialog (zu öffnen mit dem Menüeintrag Extras/Verweise der VBA-Entwick lungsumgebung) finden Sie gegebenenfalls beide Bibliotheken (siehe Abbildung 9.1). Hier wird deutlich, dass Microsoft tatsächlich auf DAO als Datenzugriffstechnologie für Access setzt: Standardmäßig ist lediglich noch ein Verweis auf die neue DAO-Bibliothek vorhanden, ADO wird gar nicht mehr automatisch berücksichtigt.
Abbildung 9.1: Aktivierte DAO- und ADO-Verweise
Wenn Sie beide Objektbibliotheken gleichzeitig eingebunden haben, kann es vorkommen, dass Sie eines der gleichnamigen Elemente einer der Objektbibliotheken verwenden und auf eine nicht vorhandene oder anders einzusetzende Methode oder Eigenschaft zugreifen. Falls Sie nämlich beide Objektmodelle einsetzen und ein in beiden vorkommendes Objekt benutzen, greift Access auf das im Verweise-Fenster weiter oben angeordnete Objekt zu. Dem lässt sich vorbeugen: Deklarieren Sie die Objekte
503
Kapitel 9
einfach direkt mit Bezug auf das richtige Objektmodell, indem Sie den VBA-Namen der entsprechenden Bibliothek voranstellen: 'Deklaration eines Recordset der DAO-Bibliothek Dim rst As DAO.Recordset 'Deklaration eines Recordset der ADO-Bibliothek Dim rst As ADODB.Recordset
9.2 Das DAO-Objektmodell Der erste Teil dieses Kapitels verschafft Ihnen einen kleinen Überblick über die Objekte von DAO und zeigt Ihnen, wie Sie auf diese zugreifen. Sie finden hier ausdrücklich keine Referenz mit allen Objekten, Eigenschaften und Methoden, sondern erhalten eine Vorstellung von den wichtigsten Elementen dieser Objektbibliothek. Das Verständnis gerade der obersten Objekte in der DAO-Hierarchie ist in vielen Fällen sehr wichtig. Man kommt zwar lange Zeit ohne großartiges Hintergrundwissen damit aus, einfach ein Database-Objekt und eines oder mehrere Recordsets zu verwenden, aber irgendwann tauchen die ersten Fragen auf. Im Anschluss an diesen Teil erfahren Sie anhand häufig vorkommender Anwendungen mehr über den Praxiseinsatz von DAO.
9.2.1 Zugriff auf die Elemente des Objektmodells Abbildung 9.2 zeigt das DAO-Objektmodell ohne Eigenschaften und Methoden. In der Hierarchie folgt immer eine Auflistung auf ein Objekt und umgekehrt, wobei Auflistungen durch kursive Schreibweise hervorgehoben sind. Da die neuen Klassen Recordset2 und Field2 von den älteren Objekten Recordset und Field abgeleitete Klassen sind, finden Sie diese stellvertretend für beide Klassen in der Abbildung. Recordset und Field sind jedoch nach wie vor vorhanden. Alle Zugriffe auf die Objekte erfolgen theoretisch über das oberste Element der Hierarchie namens DBEngine. Es gibt allerdings Möglichkeiten für den Quereinstieg: Der oft benötigte Zugriff auf die aktuelle Datenbank erfordert beispielsweise nur die Instanzierung eines Database-Objekts durch Zuweisung eines Verweises per CurrentDBMethode. Jedes Objekt verfügt zusätzlich über eine Properties-Auflistung, in der eingebaute wie auch benutzerdefinierte Eigenschaften des Objekts gespeichert sind. Die grau gezeichneten Objekte werden im Access 2007-Datenbankformat nicht mehr unterstützt, nur noch in Datenbanken, die im .mdb-Format von Access 2000 oder 2002/2003 geöffnet werden. Regulär kann der Zugriff auf beliebige Klassen des Objektmodells auf verschiedenen Wegen erfolgen. Der Zugriff auf ein Database-Objekt lässt sich etwa auf folgende Arten realisieren:
504
DAO
DBEngine Errors Error W orkspaces W orkspace Groups Group Users
Users User
User
Databases
Groups
Database
Group
Containers Container QueryDefs
Documents
QueryDef Recordsets
Document Fields
Recordset2 Relations
Field2 Fields
Relation TableDefs
Parameters Field2
Parameter
Fields TableDef
Field2 Indexes Index Fields
Fields Field2
Field2
Abbildung 9.2: Das DAO-Objektmodell im Überblick
Über den Namen des Objekts in der Auflistung als Zeichenkette oder Variable (zu verwenden, wenn der Name variabel ist): DBEngine.Workspaces("<Workspacename>").Databases("") DBEngine.Workspaces(strWorkspacename).Databases(strDatenbankname)
Über den Objektnamen: DBEngine.Workspaces!<Workspacename>.Databases!
Über die Ordinalzahl: DBEngine.Workspaces(0).Databases(0)
Über die Ordinalzahl (abgekürzte Variante): DBEngine(0)(0)
505
Kapitel 9
Die Abkürzung DBEngine(0)(0) profitiert gleich von zwei Abkürzungen: Erstens ist Work spaces das Default-Element des DBEngine-Objekts und Databases das Default-Element des Workspace-Elements, und zweitens liefern die jeweils ersten, mit der Zahl 0 versehenen Auflistungselemente den Verweis auf das hier benötigte aktuelle Database-Objekt.
9.2.2 Deklarieren und Instanzieren Theoretisch kann man auf Objekte des DAO-Objektmodells zugreifen, ohne überhaupt eine Variable zu verwenden. Die folgende Anweisung gibt etwa die Anzahl Datensätze einer Tabelle aus, wenn sie im Direktfenster abgesetzt wird (in einer Zeile): Debug.Print DBEngine.Workspaces(0).Databases(0).TableDefs("tblMitarbeiter"). RecordCount
In VBA-Routinen ist dies auch möglich, aber es ist kein guter Programmierstil und außerdem leidet unter Umständen die Performance darunter – nämlich dann, wenn Sie mehr als einmal auf ein Objekt zugreifen möchten. Dann macht es Sinn, eine Objektvariable anzulegen und mit dieser auf das gewünschte Objekt zu verweisen. Die Routine aus Listing 9.1 deklariert zunächst die gewünschten Objekte und weist diese anschließend den Objektvariablen zu – dabei arbeitet sie sich in der Objekthierarchie von oben nach unten durch. Während die Objekte Workspace, Database und Recordset explizit zugewiesen werden, erfolgt der Zugriff auf die Field-Elemente des TableDef-Objekts per For Each-Schleife über dessen Fields-Auflistung. Public Sub Tabelleneigenschaften() Dim Dim Dim Dim Set Set Set
wrk As DAO.Workspace db As DAO.Database tdf As DAO.TableDef fld As DAO.Field wrk = DBEngine.Workspaces(0) db = wrk.Databases(0) tdf = db.TableDefs("tblMitarbeiter")
For Each fld In tdf.Fields Debug.Print fld.Name Next fld Set tdf = Nothing Set db = Nothing Set wrk = Nothing End Sub Listing 9.1: Beispiel für das Deklarieren und Instanzieren von Objekten des DAO-Objektmodells
506
DAO
Kürzer – und in vielen Fällen sinnvoller, wie sich weiter unten zeigen wird – ist folgende Variante. Dabei wird die Referenz direkt über die Methode CurrentDB erzeugt. Diese Variante ist die am meisten verbreitete: Public Sub TabelleneigenschaftenKurz() Dim db As DAO.Database Dim tdf As DAO.TableDef Dim fld As DAO.Field Set db = CurrentDb Set tdf = db.TableDefs("tblMitarbeiter") For Each fld In tdf.Fields Debug.Print fld.Name Next fld Set tdf = Nothing Set db = Nothing End Sub Listing 9.2: Erstellen einer Objektreferenz auf ein Database-Objekt per CurrentDB
9.2.3 Auf Auflistungen zugreifen In Listing 9.2 haben Sie bereits ein Beispiel für das Durchlaufen einer Auflistung des DAO-Objektmodells kennen gelernt. Es gibt zwei Möglichkeiten, die zahlreichen Auf listungen dieses Objektmodells zu durchlaufen: die For...Next-Schleife und die For EachSchleife. Die For...Next-Schleife erscheint als eines der in fast allen Programmiersprachen vertretenen Konstrukte möglicherweise intuitiver: Public Sub TabellenfelderMitForNext() … Dim i As Integer Set db = CurrentDb Set tdf = db.TableDefs("tblMitarbeiter") For i = 0 To tdf.Fields.Count - 1 Set fld = tdf.Fields(i) Debug.Print fld.Name Next i … End Sub Listing 9.3: Auflistung per For...Next-Schleife durchlaufen
507
Kapitel 9
Alternativ dazu finden Sie hier die For Each-Variante. Diese Variante spart die Laufvaria ble i, die Ermittlung der Anzahl der Elemente der Auflistung und die explizite Zuwei sung des Field-Objekts ein. Dafür ist die folgende Prozedur aber unmerklich langsamer als die vorherige: Public Sub TabellenfelderKurz() … Set db = CurrentDb Set tdf = db.TableDefs("tblMitarbeiter") For Each fld In tdf.Fields Debug.Print fld.Name Next fld … End Sub Listing 9.4: Zugriff auf eine Auflistung per For Each-Schleife
9.2.4 Punkte und Ausrufezeichen DAO und ADO haben eines mit den übrigen Objekten von Access gemeinsam: die gemischte Verwendung von Punkten (.) und Ausrufezeichen (!) als Trennzeichen zwischen zwei miteinander in Beziehung stehenden Objekten, zum Beispiel: Debug.Print rstMitarbeiter!MitarbeiterID.Value
Bezüge zu Steuerelementen in Formularen sehen beispielsweise so aus: Debug.Print Forms!frmMitarbeiter!MitarbeiterID
Dort würde aber auch Folgendes funktionieren: Debug.Print Forms.frmMitarbeiter.MitarbeiterID
Wo setzt man nun welches Zeichen ein und warum funktionieren manchmal beide? Diese Fragen sind relativ leicht zu beantworten. Den Punkt verwendet man immer, wenn das nachgestellte Element ein Access-internes Objekt ist, das Ausrufezeichen kommt bei nachgestellten benutzerdefinierten Elementen zum Zuge. Dabei gilt als benutzerdefiniert alles, was Sie selbst zur Datenbank hinzugefügt haben – Tabellen, Tabellenfelder, Indizes, Formulare und Berichte und die enthaltenen Steuerelemente. Alles andere wird von Access gestellt: eingebaute Objekte, Eigenschaften, Methoden und Ereignisse. Warum in manchen Fällen Punkt und Ausrufezeichen funktionieren? Das geschieht nur scheinbar. Erfahrungen zeigen, dass sporadisch Probleme auftauchen, wenn man die Punkt-Notation an Stelle der Ausrufezeichen-Notation verwendet, obwohl Letztere angezeigt ist. Um allen Problemen aus dem Wege zu gehen, schreiben Sie einfach vor alle selbst erstellten Objekte ein Ausrufezeichen.
508
DAO
Hinweis: In manchen Fällen benötigen Sie eine zusätzliche Instanz des DBEngineObjekts – etwa, wenn Sie auf eine geschützte Datei zugreifen möchten. In diesem Fall erzeugen Sie die neue Instanz mit den folgenden zwei Zeilen: Dim objDBEngine As DAO.DBEngine Set objDBEngine = New DAO.DBEngine
Mehr zu diesem Thema erfahren Sie in Kapitel 18, »Sicherheit von Access-Daten banken«.
9.3 DBEngine Das DBEngine-Objekt ist der »Kopf« des DAO-Objektmodells. Beim Start von Access wird automatisch eine Instanz dieses Objekts erzeugt. Es enthält zwei Auflistungen: Die Workspaces-Auflistung und die Errors-Auflistung.
DAO-Fehler verfolgen Die Errors-Auflistung ist ein DAO-eigenes Fehlerobjekt, das Fehler separat vom ErrObjekt von VBA speichert. Die Errors-Auflistung von DAO kann allerdings mehrere Fehler speichern. Der Grund hierfür ist, dass manche DAO-Operationen mehr als einen Fehler auslösen können. Um dennoch alle Fehler auszulesen, speichert DAO diese in der Errors-Auflistung. Der oder die Fehler einer Operation werden so lange in der ErrorsAuflistung gespeichert, bis die nächste fehlerhafte Anweisung ausgeführt wird.
9.4 Workspace — Arbeitsbereich oder Sitzung? Die zweite Auflistung unterhalb des DBEngine-Objekts enthält die Workspaces einer Datenbank. Workspace ist eigentlich die Übersetzung für Arbeitsbereich, aber Benutzer sitzung trifft in diesem Fall eher den Punkt. Ein Workspace wird mit dem Anmelden eines Benutzers an eine Datenbank erzeugt und mit dessen Abmeldung wieder zerstört. Sie können in einer einzigen Access-Instanz mehrere Workspace-Objekte erzeugen – das macht vor allem Sinn, wenn Sie automatisch die Wechselwirkungen von Sperrmechanis men oder Transaktionen im Mehrbenutzerbetrieb testen möchten. Um ein neues Workspace-Objekt zu erzeugen, reicht die CreateWorkspace-Methode aus, an die Workspaces-Auflistung wird es allerdings nicht automatisch angehängt. Das Erzeugen eines Workspace-Objekts und das Anhängen an die entsprechende Auflistung zeigt folgendes Beispiel:
509
Kapitel 9 Public Sub WorkspacesSample() Dim wrk As DAO.Workspace Dim wrkExt As DAO.Workspace 'Neues Workspace-Objekt erzeugen... '(Arbeitsgruppe ist in diesem Fall die Standardsarbeitsgruppe) Set wrkExt = DBEngine.CreateWorkspace("#New Workspace#", "admin", "") '...und an die Workspaces-Auflistung anhängen Workspaces.Append wrkExt 'Namen der Workspaces ausgeben Debug.Print "Workspaces:" For Each wrk In DBEngine.Workspaces Debug.Print wrk.Name Next wrk End Sub Listing 9.5: Anlegen eines neuen Workspace-Objekts und Anhängen an die WorkspacesAuflistung
9.4.1 Auflistungen des Workspace-Objekts Das Workspace-Objekt enthält drei Auflistungen, die weiter unten ausführlicher beschrieben werden. Für den Einsatz mit Datenbanken im Access 2007-Format ist nur noch die Databases-Auflistung interessant, die anderen beiden und das damit verbundene Sicherheitssystem werden nicht mehr unterstützt (derweil diese Einträge durchaus noch verwendet werden können – eben für Datenbanken, die mit älteren Versionen von Access erstellt wurden): Databases: Enthält alle Database-Objekte, die im Kontext der Sitzung geöffnet wurden. Users: Enthält eine Auflistung aller Benutzer der aktuellen Arbeitsgruppe. Groups: Enthält eine Auflistung aller Benutzergruppen der aktuellen Arbeitsgruppe.
9.4.2 Aufgaben des Workspace-Objekts Das Workspace-Objekt stellt eine ganze Reihe Funktionen zur Verfügung. Zwei davon sind besonders wichtig: das Verwalten von Transaktionen und das Verwalten von Benutzern und Benutzergruppen. Beide werden separat behandelt: die Transaktionen in Abschnitt 9.11 dieses Kapitels und die Benutzerverwaltung in Kapitel 18, »Sicherheit von Access-Datenbanken«.
510
DAO
Darüber hinaus enthält es Funktionen zum Erzeugen neuer Datenbanken (CreateDatabase) oder zum Öffnen weiterer Datenbanken im gleichen Workspace (OpenDatabase). Informationen zu weiteren Elementen finden Sie in der Onlinehilfe.
9.4.3 Datenbanken erzeugen und öffnen Im Kontext eines Workspace-Objekts lassen sich neue Datenbanken erzeugen und bestehende Datenbanken öffnen. Eine neue Datenbank erzeugen Sie mit der CreateDatabaseMethode, eine bestehende Datenbank öffnen Sie mit OpenDatabase. Eine aus der aktuellen Datenbank heraus erzeugte neue Datenbank könnte beispielsweise dem Auslagern temporärer Tabellen dienen. Auf diese Weise würden temporäre Daten die Datenbank nicht unnötig aufblähen. Besonders interessant ist diese Vorge hensweise, wenn die Datenbankgröße ein kritisches Maß erreicht hat und die temporären Daten diese sprengen könnten. Um eine neue Datenbank im Format von Access 2007 zu erstellen, verwenden Sie die folgende Anweisung: DbEngine.CreateDatabase (, dbLangGeneral, dbVersion120
9.5 Aktuelle Datenbank referenzieren Um per DAO auf die aktuelle Datenbank zuzugreifen, gibt es zwei Möglichkeiten: Ent weder man arbeitet sich vom obersten Element der Objekthierarchie bis zum DatabaseObjekt vor und verweist darauf oder man verwendet direkt die CurrentDB-Methode, um einen Verweis zu erhalten: DBEngine.Workspaces(0).Databases(0)
oder abgekürzt DBEngine(0)(0)
verweisen dabei auf die aktuelle Datenbank, während CurrentDB
einen Verweis auf eine neue Objektinstanz erstellt und diesen zurückliefert. Welche Vor- und Nachteile gibt es nun und welche Variante setzt man wann ein? Die DBEngine-Version ist die einzige der beiden Möglichkeiten, mit der Sie von außerhalb – also etwa von Excel oder Word aus – auf eine Access-Datenbank zugreifen können. Intern sind beide Varianten möglich. CurrentDB hat dabei den Vorteil, dass es immer auf die aktuelle Datenstruktur zugreift, während dies bei DBEngine(0)(0) nicht immer der Fall ist. CurrentDB aktualisiert nämlich sämtliche im Database-Objekt enthaltenen Auflistungen, während bei der Verwendung von DBEngine ein zusätzlicher Aufruf der
511
Kapitel 9
Refresh-Methode erforderlich ist. Den von CurrentDB zurückgegebenen Verweis muss man speichern, um länger als für die Dauer der aktuellen Anweisung auf das DatabaseObjekt und untergeordnete Objekte zugreifen zu können; greift man über CurrentDB auf untergeordnete Objekte zu, sind diese nur für die Dauer der Anweisung existent. Der Nachteil von CurrentDB ist, dass es minimal langsamer als DBEngine(0)(0) ist – was sich allerdings im normalen Betrieb kaum bemerkbar macht. Ein Beispiel für dieses Verhalten finden Sie in Abbildung 9.3. Ausnahmen für dieses Verhalten gibt es auch: Wenn Sie mit folgendem Quellcode eine Datensatzgruppe erzeugen, verschwindet das Objekt nicht von alleine wieder in die ewigen Jagdgründe: Public Sub CurrentDBPersistenzMitRecordset() Dim rst As Recordset Set rst = CurrentDb.OpenRecordset("tblMitarbeiter", dbOpenDynaset) Debug.Print rst.RecordCount Set rst = Nothing End Sub Listing 9.6: Erfolgreiches Erzeugen eines Objektverweises mit CurrentDB und untergeordnetem Recordset-Objekt
Abbildung 9.3: Fehler beim Versuch, einen Verweis auf ein untergeordnetes Objekt mit CurrentDB herzustellen
Selten genutzt, aber perfekt: CurrentDBC Es gibt allerdings noch eine weitere Variante, einen Verweis auf die aktuelle Datenbank zu erstellen. Sie stammt von Michael Kaplan und wurde beispielsweise unter [1] veröffentlicht. Erstellen Sie einfach ein neues Standardmodul und fügen Sie die folgenden Zeilen dort ein:
512
DAO Private mDB As DAO.Database Public Property Get CurrentDBC() As DAO.Database If mDB Is Nothing Then Set mDB = CurrentDb End If Set CurrentDBC = mDB End Property Listing 9.7: Dynamisches Zuweisen eines Database-Objekts
Anschließend brauchen Sie nur noch CurrentDBC überall dort einzusetzen, wo Sie die aktuelle Datenbank referenzieren möchten – fertig! Und was passiert dort nun? Ganz einfach: Wann immer Sie mit CurrentDBC auf die aktuelle Datenbank zugreifen möchten, ruft Access die öffentliche Property-Prozedur CurrentDBC auf. Diese prüft, ob die private Variable nicht leer ist, was bedeutet, dass ihr zuvor ein Verweis auf die aktuelle Datenbank zugewiesen wurde. Wenn nicht, erfolgt die Zuweisung im nächsten Schritt und der Objektverweis wird an den Rückgabewert CurrentDBC weitergegeben. Ist der Verweis schon vorhanden, gibt die Property diesen ohne Umwege an die aufrufende Zeile weiter. Der Vorteil ist: Sie müssen im Normalfall nur einmal pro Anwendungssitzung einen Verweis auf die aktuelle Datenbank mit CurrentDB herstellen und können im Anschluss immer wieder auf diesen gespeicherten Verweis zugreifen. Und falls Sie noch wissen möchten, warum dieses Buch dennoch überall CurrentDB einsetzt: Es ist eine eingebaute Funktion, und außerdem sollen alle Beispiele so funktio nieren, wie Sie diese hier abgedruckt finden – ohne, dass überall ein Hinweis auf die zusätzlich zu erstellende Property notwendig ist.
9.5.1 Users und Groups Diese beiden Auflistungen werden in Datenbanken, die mit Access 2007 erstellt wurden, nicht mehr unterstützt. Da Sie aber mit Access 2007 durchaus Anwendungen öffnen können, die mit Access 2000 oder Access 2002/2003 erstellt wurden und damit das Sicherheitssystem noch unterstützen, beschreibt auch dieses Buch die damit zusammen hängenden Auflistungen, Objekte, Eigenschaften und Methoden. Die Auflistung Users und Groups sowie die enthaltenen User- und Group-Elemente dienen der Verwaltung von Benutzern und Benutzergruppen. Dies ist insbesondere in Zu sammenhang mit der Sicherheit von Access-Datenbanken interessant. Die Verwaltung von Benutzern und Benutzergruppen sowie ihre Zugriffsrechte wird aber vermutlich meist über die Verwendung der Benutzeroberfläche realisiert und weniger über eine extra programmierte Schnittstelle. Weitere Informationen zum Thema Sicherheit finden Sie in Kapitel 18, »Sicherheit von Access-Datenbanken«.
513
Kapitel 9
9.6 Das Database-Objekt Das Database-Objekt enthält einige Auflistungen, Eigenschaften und Methoden, von denen vier Kategorien in den folgenden Abschnitten besonderes Augenmerk erhalten: Manipulation des Datenmodells Zugriff auf Auflistungen wie QueryDefs, Recordsets, Relations und TableDefs Anwenden der OpenRecordset-Methode Ausführen von Aktionsabfragen
9.6.1 Manipulation des Datenmodells Tabellen, Felder und Beziehungen lassen sich nicht nur mit den Data Definion Language (DDL)-Anweisungen von SQL erzeugen, bearbeiten und löschen, sondern auch per DAO. Dazu stehen die folgenden Methoden zur Verfügung: CreateDatabase (Workspace-Objekt): Erzeugen einer Datenbankdatei CreateTableDef (Database-Objekt): Erstellen einer Tabelle CreateProperty (Database-Objekt): Anlegen einer Eigenschaft CreateQueryDef (Database-Objekt): Erstellen einer Abfrage CreateRelation (Database-Objekt): Erstellen einer Tabellenbeziehung CreateField (TableDef-Objekt, Relations-Objekt, Index-Objekt): Erstellen eines Feldes CreateIndex (TableDef-Objekt): Erstellen eines Index CreateProperty (TableDef-Objekt, Index-Objekt): Erstellen einer Eigenschaft
9.6.2 Erstellen einer Tabelle Die folgende Routine zeigt, wie man eine neue Tabelle namens tblUnternehmen und die beiden Felder UnternehmenID und Unternehmen anlegt und in der Datenbank verfügbar macht. Zu beachten ist hier, dass Sie eine Menge Tabellen und Felder mit CreateTable und CreateField erzeugen können – aber wenn Sie diese nicht an die entsprechenden Auflistungen TableDefs beziehungsweise Fields anhängen, sind alle erzeugten Objekte mit Ablauf der Routine wieder verschwunden.
514
DAO
Abbildung 9.4 zeigt die Abhängigkeiten der an der Erstellung einer Tabelle beteiligten Elemente der DAO-Objektbibliothek. Wenn Sie sich merken, dass Sie einfach zuerst die einzelnen Elemente erzeugen und diese dann an die Auflistung des übergeordneten Elements anhängen müssen, haben Sie das Objektmodell bezüglich der Erstellung von Objekten schon fast im Griff (für die Erstellung einer Tabelle können Sie statt des Field2Objekts (kursiv abgebildet) auch das ältere Field-Objekt verwenden). Databases Database TableDefs TableDef Fields Field2
Abbildung 9.4: Hierarchie der beim Erstellen einer Tabelle beteiligten Elemente der DAOObjektbibliothek
Hinweis: Die neue DAO-Bibliothek liefert neben dem bekannten Recordset-Objekt noch eine neuere Version namens Recordset2. Dieses müssen Sie immer dann verwenden, wenn die Tabelle, auf der eine Datensatzgruppe basiert, Felder mit neuen Datentypen enthält. Bei den neuen Datentypen handelt es sich um den Attachment(Anlage) und die Complex-Datentypen (mehrwertige Felder). Um diese Feldtypen zu verwenden, müssen Sie darüber hinaus die Klasse Field2 statt Field verwenden. Da beide Klassen von den älteren Klassen abgeleitet sind und somit alle darin enthaltenen Eigenschaften und Methoden aufweisen, können Sie prinzipiell immer die neuere Variante verwenden (was in diesem Buch auch geschieht). Tabelle 9.1 enthält alle Konstanten, die Sie für die Konstante Type der CreateFieldAnweisung verwenden können, wobei die kursiv gedruckten in .accdb-Dateien nicht zur Verfügung stehen, sondern nur in ODBC-Tabellen (SQL-Server-Datenbanken). Public Sub TabelleAnlegen() Dim db As DAO.Database Dim tdf As DAO.TableDef Dim fld As DAO.Field 'Verweis auf aktuelle Datenbank Set db = CurrentDb 'Verweis auf neues TableDef-Objekt Set tdf = db.CreateTableDef
515
Kapitel 9 'Name der Tabelle zuweisen tdf.Name = "tblUnternehmen" 'Feld neu erstellen und per Objektvariable referenzieren Set fld = tdf.CreateField("UnternehmenID", dbDouble) 'Feld an die Feldliste der Tabelle anhängen tdf.Fields.Append fld 'Gleiches Spiel mit einem zweiten Feld Set fld = tdf.CreateField("Unternehmen", dbText, 255) tdf.Fields.Append fld 'Die bisher nicht vorhandene Tabelle zur TableDefs-Auflistung hinzufügen db.TableDefs.Append tdf 'Auflistung aktualisieren db.TableDefs.Refresh 'Navigationsbereich aktualisieren Application.RefreshDatabaseWindow Set db = Nothing End Sub Listing 9.8: Erstellen einer Tabelle mit DAO Konstante
Zahlenwert
Datentyp
dbBoolean
1
Boolean
dbByte
2
Byte
dbInteger
3
Integer
dbLong
4
Long
dbCurrency
5
Currency
dbSingle
6
Single
dbDouble
7
Double
dbDate
8
Date/Time
dbBinary
9
Binary
dbText
10
Text
dbLongBinary
11
Long Binary (OLE Object)
dbMemo
12
Memo
dbGUID
15
GUID
dbBigInt
16
Big Integer
Tabelle 9.1: Konstanten für den Datentyp in der CreateField-Anweisung
516
DAO Konstante
Zahlenwert
Datentyp
dbVarBinary
17
VarBinary (OLE-Objekt)
dbChar
18
Char
dbNumeric
19
Numeric
dbDecimal
20
Decimal
dbFloat
21
Float
dbTime
22
Time
dbTimeStamp
23
Time Stamp
dbAttachment
101
Attachment
dbComplexByte
102
Byte (mehrwertiges Feld)
dbComplexInteger
103
Integer (mehrwertiges Feld)
dbComplexLong
104
Long (mehrwertiges Feld)
dbComplexSingle
105
Single (mehrwertiges Feld)
dbComplexDouble
106
Double (mehrwertiges Feld)
dbComplexGUID
107
GUID (mehrwertiges Feld)
dbComplexDecimal
108
Dezimal (mehrwertiges Feld)
dbComplexText
109
Text (mehrwertiges Feld)
Tabelle 9.2: Konstanten für den Datentyp in der CreateField-Anweisung (Fortsetzung)
9.6.3 Autowert anlegen Wenn Sie das Primärschlüsselfeld der Tabelle als Autowertfeld auslegen möchten, brau chen Sie lediglich ein einziges Attribut zu setzen. Dazu fügen Sie zwischen der Create Field-Anweisung und der Append-Anweisung zum Hinzufügen des Feldes die folgende Zeile ein: 'Attributes-Eigenschaft auf Autowert einstellen fld.Attributes = dbAutoIncrField
9.6.4 Attachment-Feld anlegen Die folgende Routine erzeugt eine Tabelle mit einem Feld des Typs Attachment. Die passende Konstante finden Sie in Tabelle 9.1. Public Sub TabelleMitAttachmentfeldAnlegen()' Dim db As DAO.Database Dim tdf As DAO.TableDef Dim fld As DAO.Field2 Set db = CurrentDb
517
Kapitel 9 Set tdf = db.CreateTableDef tdf.Name = "tblDateien" Set fld = tdf.CreateField("DateiID", dbLong) fld.Attributes = dbAutoIncrField tdf.Fields.Append fld Set fld = tdf.CreateField("Datei", dbAttachment) tdf.Fields.Append fld db.TableDefs.Append tdf db.TableDefs.Refresh Application.RefreshDatabaseWindow Set db = Nothing End Sub Listing 9.9: Anlegen von Feldern mit den neuen Datentypen von Access 2007
Abbildung 9.5 zeigt die Tabelle mit dem neuen Attachment-Feld.
Abbildung 9.5: Eine per DAO erstellte Tabelle mit Attachment-Feld
9.6.5 Mehrwertige Felder anlegen Das Anlegen von mehrwertigen Feldern ist nicht ganz so einfach wie bei AttachmentFeldern – zumindest nicht, wenn man diese Felder anschließend auch Gewinn bringend einsetzen möchte. Aber der Reihe nach: Schauen Sie sich zunächst einmal das folgende Listing an, das genau wie in den vorhergehenden Beispielen eine Tabelle mit einem ID-Feld und einem weiteren Feld eines bestimmten Datentyps anlegt. In diesem Fall handelt es sich dabei um ein mehrwertiges Feld zum Speichern von Text. Public Sub TabelleMitKomplexemFeldAnlegen() Dim db As DAO.Database Dim tdf As DAO.TableDef Dim fld As DAO.Field2
518
DAO Set db = CurrentDb Set tdf = db.CreateTableDef tdf.Name = "tblKomplexesFeld" Set fld = tdf.CreateField("KomplexesFeldID", dbLong) fld.Attributes = dbAutoIncrField tdf.Fields.Append fld Set fld = tdf.CreateField("KomplexesFeld", dbComplexText) tdf.Fields.Append fld db.TableDefs.Append tdf db.TableDefs.Refresh Application.RefreshDatabaseWindow Set db = Nothing End Sub Listing 9.10: Anlegen einer Tabelle mit einem komplexen Feld
Das Anlegen der Tabelle und des ComplexText-Feldes funktioniert, aber der Versuch, diesem Feld in der Datenblattansicht der Tabelle Einträge hinzuzufügen, schlägt fehl. Kein Wunder, wie ein Blick in die Entwurfsansicht und die Nachschlagen-Eigenschaften dieses Feldes zeigt: Die für das Hinzufügen verantwortliche Eigenschaft Wertlistenbearbeitung zulassen hat den Wert Nein (siehe Abbildung 9.6).
Abbildung 9.6: Eigenschaften eines per DAO und VBA erstellten mehrwertigen Feldes
519
Kapitel 9
Wie aber soll man diese Eigenschaften beim Anlegen eines solchen Feldes per DAO einstellen, wenn diese offensichtlich noch nicht vorhanden sind? Die Lösung ist der Einsatz von Properties, die Sie zunächst festlegen und dann auf die gewünschten Werte einstellen. Das folgende Beispiel legt die gleiche Tabelle wie oben an, nur dass die Eigenschaft Wertlistenbearbeitung zulassen auf den Wert Ja eingestellt wird: Public Sub TabelleMitAenderbaremKomplexemFeldAnlegen() Dim Dim Dim Dim
db As DAO.Database tdf As DAO.TableDef fld As DAO.Field2 prp As DAO.Property
Set db = CurrentDb Set tdf = db.CreateTableDef CurrentDb.TableDefs.Delete "tblKomplexesFeld" tdf.Name = "tblKomplexesFeld" Set fld = tdf.CreateField("KomplexesFeldID", dbLong) fld.Attributes = dbAutoIncrField tdf.Fields.Append fld Set fld = tdf.CreateField("KomplexesFeld", dbComplexText) tdf.Fields.Append fld db.TableDefs.Append tdf Set prp = fld.CreateProperty("AllowValueListEdits", dbBoolean) prp.Value = True fld.Properties.Append prp Set prp = fld.CreateProperty("RowSourceType", dbText) prp.Value = "Wertliste" fld.Properties.Append prp db.TableDefs.Refresh RefreshDatabaseWindow Set db = Nothing End Sub Listing 9.11: Anlegen einer Tabelle mit einem mehrwertigen Feld, das auf einer Wertliste basiert und benutzerdefinierte Einträge zulässt
520
DAO
Die Tabelle arbeitet nun in der Datenblattansicht wie erwartet: Beim Klick auf das Kom binationsfeld zum Auswählen des Inhalts für das mehrwertige Feld öffnet sich nicht nur die Liste, sondern es erscheint auch das Symbol zum Öffnen des Dialogs zum Ändern der Listeneinträge (siehe Abbildung 9.7).
Abbildung 9.7: Eine per DAO erstellte Tabelle mit erweiterbarem, mehrwertigem Feld
Weitere Properties von Nachschlage- und mehrwertigen Feldern Tabelle 9.3 zeigt die übrigen Eigenschaften von Nachschlage- und mehrwertigen Fel dern mit der englischen Bezeichnung und dem Datentyp. Eigenschaft
Englische Bezeichnung
Datentyp
Herkunftstyp
RowSourceType
dbText/String
Datensatzherkunft
RowSource
dbText/String
Gebundene Spalte
BoundColumn
dbLong/Long
Spaltenanzahl
ColumnCount
dbInteger/Integer
Spaltenüberschriften
ColumnHeads
dbBoolean/Boolean
Spaltenbreiten
ColumnWidth
dbText/String
Zeilenanzahl
ListRows
dbInteger/Integer
Listenbreite
ListWidth
dbText/String
Nur Listeneinträge
LimitToList
dbBoolean/Boolean
Mehrere Werte zulassen
AllowMultipleValues
dbBoolean/Boolean
Wertlistenbearbeitung zulassen
AllowValueListEdits
dbBoolean/Boolean
Bearbeitungsformular für Listenelemente
ListItemsEditForm
dbText/String
Nur Datensatzherkunftswerte anzeigen
ShowOnlyRowSourceValues
dbBoolean/Boolean
Tabelle 9.3: Eigenschaften für Nachschlage- und mehrwertige Felder
9.6.6 Löschen einer Tabelle Das Löschen einer Tabelle erfolgt über die Delete-Methode der TableDefs-Auflistung. Diese Methode erwartet die Angabe des Namens der zu löschenden Tabelle als Parameter:
521
Kapitel 9 Public Sub TabelleLoeschen() Dim db As DAO.Database Set db = CurrentDb db.TableDefs.Delete "tblUnternehmen" db.TableDefs.Refresh Application.RefreshDatabaseWindow Set db = Nothing End Sub Listing 9.12: Löschen der soeben erzeugten Tabelle tblUnternehmen
Die vorhergehenden beiden Beispielroutinen und auch die folgenden enthalten keine Fehlerbehandlung. Für den Praxiseinsatz ist eine Fehlerbehandlung natürlich Pflicht, aber hier wird diese aus Gründen der Übersichtlichkeit ausgespart. Mehr zum Thema Fehlerbehandlung erfahren Sie in Kapitel 13, »Debugging, Fehlerbehandlung und Fehlerdokumentation«.
9.6.7 Erstellen eines Index Der oben erstellten Tabelle fehlt noch der Primärschlüssel auf dem Feld UnternehmenID. Die wichtigste Funktion in der folgenden Routine ist die Methode CreateIndex des TableDef-Objekts. Der Weg zum Ziel ist hier in drei Ebenen verschachtelt. Abbildung 9.8 zeigt die an der Indexerstellung beteiligten Objekte des DAO-Objektmodells und ihre Anordnung (auch hier gilt: in den meisten Fällen können Sie das ältere Field-Objekt statt des Field2-Objekts verwenden, aber nicht in Zusammenhang mit einigen neuen Features). Databases Database TableDefs TableDef Indexes Index Fields Field2
Abbildung 9.8: Elemente der DAO-Bibliothek zum Anlegen eines Index
Beim Erstellen der am Index beteiligten Field2-Objekte müssen Sie nur noch den Namen des jeweiligen Feldes angeben. Voraussetzung ist, dass dieses Feld bereits existiert.
522
DAO Public Sub IndexErstellen() Dim db As DAO.Database Dim idx As DAO.Index Dim fld As DAO.Field Set db = CurrentDb 'Index erstellen Set idx = db.TableDefs("tblUnternehmen").CreateIndex("PrimaryKey") 'Feld für den Index angeben Set fld = idx.CreateField("UnternehmenID") 'Feld an Fields-Auflistung des Index anhängen idx.Fields.Append fld 'Index als Primärschlüssel kennzeichnen idx.Primary = True 'Index an Indexes-Auflistung der Tabelle anhängen db.TableDefs("tblUnternehmen").Indexes.Append idx db.TableDefs.Refresh Application.RefreshDatabaseWindow Set idx = Nothing Set db = Nothing End Sub Listing 9.13: Anlegen eines Primärschlüssels für eine Tabelle
9.6.8 Löschen eines Index Das Löschen eines Index erfolgt über die Delete-Methode der Indexes-Auflistung des betroffenen TableDef-Objekts. Die Methode erwartet den Namen des zu löschenden Index als Parameter. Public Sub IndexLoeschen() Dim db As DAO.Database Set db = CurrentDb db.TableDefs("tblMitarbeiter").Indexes.Delete "PrimaryKey" Set db = Nothing End Sub Listing 9.14: Entfernen eines Index
523
Kapitel 9
9.6.9 Erstellen einer Beziehung Das letzte fehlende Element zur Erstellung kompletter Datenmodelle per DAO ist die Methode CreateRelation zum Erstellen von Beziehungen zwischen zwei Tabellen. Das nachfolgende Beispiel erstellt eine Beziehung zwischen den beiden Tabellen tblMitarbeiter und tblUnternehmen. Die Beziehung soll über das Feld UnternehmenID hergestellt werden, das in der Tabelle tblUnternehmen als Primärschlüsselfeld mit dem Datentyp Long und in der Tabelle tblMitarbeiter als einfaches Long-Feld vorhanden ist. Neben den beteiligten Tabellen und Feldern ist die Art der Beziehung sehr wichtig. Tabelle 9.4 zeigt alle möglichen Beziehungsarten. Standardmäßig legt die CreateRelationMethode eine Beziehung mit referentieller Integrität an – dies entspricht dem Wert 0 für die Eigenschaft Attributes des Relation-Objekts. Die eigentümlichen Zahlenwerte der Konstanten für diese Eigenschaft rühren daher, dass man die Konstanten miteinander kombinieren können soll. Wenn Sie also etwa eine Beziehung mit referentieller Integrität als LEFT JOIN mit Aktualisierungs- und Löschweitergabe festlegen möchten, addieren Sie die Werte der einzelnen Konstanten in einer boolschen Operation – also 0 Or 256 Or 4096 Or 16777216. Sie können diesen Ausdruck als Wert der Eigenschaft Attributes eintragen oder auch vorher die Summe berechnen. Die folgende Routine erstellt die Beziehung aus Abbildung 9.9. Public Sub BeziehungErstellen() Dim db As DAO.Database Dim rel As DAO.Relation Dim fld As DAO.Field Set db = CurrentDb 'Erstellen der Beziehung Set rel = db.CreateRelation() 'Zuweisen der benötigten Informationen: 'Name der Beziehung rel.Name = "relMitarbeiterUnternehmen" 'Name der Tabelle mit dem Fremdschlüsselfeld rel.ForeignTable = "tblMitarbeiter" 'Name der Tabelle mit dem Primärschlüssel rel.Table = "tblUnternehmen" 'Typ der Beziehung rel.Attributes = 0
524
DAO 'Feld, über das die Beziehung hergestellt werden soll Set fld = rel.CreateField("UnternehmenID", dbLong) 'Name des Feldes in der Detailtabelle fld.ForeignName = "UnternehmenID" 'Name des Feldes in der Mastertabelle fld.Name = "UnternehmenID" 'Anhängen des Feldes an die Fields-Auflistung der Beziehung rel.Fields.Append fld 'Anhängen der Beziehung an die Relations-Auflistung der Datenbank db.Relations.Append rel Set rel = Nothing Set db = Nothing End Sub Listing 9.15: Herstellen einer Beziehung zwischen zwei Tabellen Konstante
Zahlenwert
Bedeutung
dbRelationUnique
1
1:1-Beziehung
dbRelationDontEnforce
2
Die Beziehung wird nicht erzwungen (keine referentielle Integrität).
dbRelationInherited
4
Die Beziehung besteht in Datenbanken, die zwei verknüpfte Tabellen enthalten.
dbRelationUpdateCascade
256
Aktualisierungsweitergabe
dbRelationDeleteCascade
4096
Löschweitergabe
dbRelationLeft
16777216
Nur in Microsoft Access. Erstellt ein LEFT JOIN zwischen den Tabellen.
dbRelationRight
33554432
Nur in Microsoft Access. Erstellt ein RIGHT JOIN zwischen den Tabellen.
Tabelle 9.4: Konstanten, Zahlenwerte und Bedeutung für die Eigenschaft Attributes beim Anlegen einer Beziehung
A bbildung 9.9: Diese Beziehung wurde mit der Prozedur aus Listing 10.15 angelegt
525
Kapitel 9
9.6.10 Löschen einer Beziehung Das Löschen einer Beziehung erfolgt über die Methode Delete der Relations-Auflistung. Beispiel: Public Sub BeziehungLoeschen() … db.Relations.Delete "relMitarbeiterUnternehmen" … End Sub Listing 9.16: Löschen einer Beziehung
9.6.11 Erstellen von Eigenschaften Manche Eigenschaften von Objekten der Datenbank sind unter DAO nicht verfügbar – zumindest nicht als eingebaute Eigenschaften. Diese Eigenschaften heißen benutzerdefinierte Eigenschaften und müssen vor dem Verwenden zunächst erstellt werden. Die folgende Routine zeigt, wie Sie einem Währungsfeld einer Tabelle die Eigenschaft Format hinzufügen und diese direkt mit einem Wert füllen. Public Sub PropertyEinsetzen() … Set db = CurrentDb Set tdf = db.TableDefs("tblArtikel") Set fld = tdf.Fields("Preis") Set prp = fld.CreateProperty("Format", dbText, "#,##0.00 USD") fld.Properties.Append prp … End Sub Listing 9.17: Einstellen der Format-Eigenschaft eines Tabellenfeldes auf ein bestimmtes Währungsformat
9.6.12 Zugriff auf Auflistungen und Elemente In den vorhergehenden Abschnitten haben Sie Tabellen, Felder, Indizes und Relationen erstellt. Sie werden in vielen Fällen auf diese Objekte zugreifen müssen – normalerweise schon beim Erstellen solcher Objekte, etwa um zu überprüfen, ob ein Objekt bereits vorhanden ist.
Alle Tabellen ausgeben Die Prozedur TabellenAusgeben durchläuft die Auflistung TableDefs und gibt zu jedem enthaltenen Element den Wert der Name-Eigenschaft aus.
526
DAO Public Sub TabellenAusgeben() Dim db As DAO.Database Dim tdf As TableDef Set db = CurrentDb For Each tdf In db.TableDefs Debug.Print tdf.Name Next tdf Set db = Nothing End Sub Listing 9.18: Ausgeben aller Tabellen einer Datenbank
Prüfen, ob eine Tabelle schon vorhanden ist Es gibt (mindestens) zwei Möglichkeiten, um das Vorhandensein einer Tabelle zu prüfen. Die erste arbeitet mit der gleichen Technik wie die Prozedur aus Listing 9.18: Sie durchläuft alle Tabellen und vergleicht deren Namen mit dem der gesuchten Tabelle. Die Funktion gibt den Wert True zurück, wenn die im Parameter strTabellenname angegebene Tabelle bereits vorhanden ist: Public Function IstTabelleVorhanden(strTabellenname As String) As Boolean Dim db As DAO.Database Dim tdf As TableDef Set db = CurrentDb For Each tdf In db.TableDefs If tdf.Name = strTabellenname Then IstTabelleVorhanden = True End If Next tdf Set db = Nothing End Function Listing 9.19: Prüfung auf Vorhandensein einer Tabelle per Durchlaufen der TableDefs-Auflistung
Die zweite Variante ist ein wenig brachialer: Sie versucht einfach, auf das gesuchte TableDef-Objekt zuzugreifen und löst einen Fehler aus, wenn die Tabelle nicht vorhan-
527
Kapitel 9
den ist. Diesen Fehler behandelt die Prozedur entsprechend und gibt ebenfalls den Wert True zurück, falls die gesuchte Tabelle existiert: Public Function IstTabelleVorhanden_Error(strTabellenname As String) _ As Boolean Dim db As DAO.Database Dim tdf As DAO.TableDef Set db = CurrentDb On Error Resume Next Set tdf = db.TableDefs(strTabellenname) If Err.Number = 3265 Then IstTabelleVorhanden_Error = False Else IstTabelleVorhanden_Error = True End If Set db = Nothing End Function Listing 9.20: Prüfung auf Vorhandensein einer Tabelle durch direkten Zugriff und ausgelösten Fehler
9.6.13 Datensatzgruppen erstellen mit OpenRecordset Die Methode OpenRecordset ist eine der Möglichkeiten für den Zugriff auf die Daten einer Tabelle oder einer Abfrage. Damit ist sie eine der wichtigsten Methoden des Database-Objekts. Die Methode kommt in zwei Ausführungen. Die erste öffnet eine Datensatzgruppe auf Basis einer Tabelle, einer Auswahlabfrage oder einer SQL-Select-Anweisung: Set = .OpenRecordset (, , , <Sperren>)
Die zweite Variante öffnet eine Datensatzgruppe auf Basis eines bestehenden Recordset2Objekts. Als geben Sie hierbei das bestehende Recordset2-Objekt an: Set = .OpenRecordset (, , <Sperren>)
Das Ergebnis der OpenRecordset-Methode wird immer einem Recordset2-Objekt zugewiesen. Bei handelt es sich um ein Database-Objekt, das zuvor etwa mit CurrentDB erzeugt wird. Die ist entweder der Name einer Tabelle oder Auswahlabfrage
528
DAO
oder eine SQL-Select-Anweisung. Der beschreibt die Art der zu öffnenden Datensatzgruppe. Tabelle 9.5 enthält die verschiedenen Möglichkeiten.
Performance Die Auswahl des richtigen Typs beim Öffnen eines Recordset2-Objekts spielt eine entscheidende Rolle für die Performance bei der Arbeit mit diesem Objekt. Weitere Informationen hierzu finden Sie in Kapitel 14, »Performance«. Konstante
Beschreibung
dbOpenTable
Öffnet eine Tabelle in der lokalen Datenbank; Standard für TabellenObjekte. Die Daten in einem solchen Recordset können bearbeitet werden; außerdem kann die Seek-Methode für die Suche in indizierten Feldern verwendet werden.
dbOpenDynaset
Öffnet ein Recordset vom Typ Dynaset. Standard für verknüpfte Tabellen, Abfragen und SQL-Select-Anweisungen. Die Daten solcher Recordsets können in den meisten Fällen bearbeitet werden. Ein Dynaset-Recordset enthält nur die Verweise auf die Datensätze der beteiligten Tabellen, der Inhalt eines Datensatzes wird bei Bedarf eingelesen.
dbOpenSnapshot
Öffnet ein Recordset vom Typ Snapshot. Ein Snapshot ist ein Abbild des betroffenen Datenbestandes zu einem bestimmten Zeitpunkt. Die enthaltenen Daten können nicht geändert werden.
dbOpenForwardOnly
Öffnet ein Recordset vom Typ Forward-Only. Entspricht weitgehend dem Snapshot, kann aber nur vorwärts (und damit nur einmal) durchlaufen werden.
Tabelle 9.5: Konstanten für den Typ einer per OpenRecordset geöffneten Datensatzgruppe
Der Parameter ist unter anderem für den Einsatz von Access in Mehrbenutzer umgebungen interessant; außerdem lassen sich weitere Optionen festlegen. Einen Über blick über die gängigsten Optionen bietet Tabelle 9.6. Die Parameter lassen sich auch kombinieren, indem Sie mehrere Parameter mit einer boolschen Operation addieren – etwa dbConsistent Or dbSeeChanges. Konstante
Beschreibung
dbAppendOnly
Lässt nur das Anfügen von Daten zu (Typ: Dynaset).
dbConsistent
Lässt nur konsistente Aktualisierungen zu (Typ: Dynaset und Snapshot).
dbDenyWrite
Verhindert, dass andere Benutzer Datensätze ändern oder hinzufügen können.
dbDenyRead
Verhindert, dass andere Benutzer Daten in Tabellen lesen können (Typ: Table).
dbForwardOnly
Recordset kann nur vorwärts durchlaufen werden (Typ: Snapshot). Alternative: Direkt dbOpenForwardOnly als Typ wählen.
Tabelle 9.6: Zusätzliche Optionen zum Öffnen von Datensatzgruppen
529
Kapitel 9 Konstante
Beschreibung
dbInconsistent
Ermöglicht inkonsistente Aktualisierungen (Typ: Dynaset und Snapshot).
dbReadOnly
Verhindert Änderungen am Recordset. Alternative: dbReadOnly für den Parameter Sperren angeben.
dbSeeChanges
Löst einen Laufzeitfehler aus, wenn ein Benutzer Daten ändert, die ein anderer Benutzer bearbeitet (Typ: Dynaset). Dies ist in Anwendungen hilfreich, in denen mehrere Benutzer gleichzeitig per Lese-/Schreib-Zugriff über die gleichen Daten verfügen.
dbSQLPassThrough
Gibt eine SQL-Anweisung an eine mit Microsoft ACE verbundene ODBCDatenquelle zur Bearbeitung weiter (Typ: Snapshot).
Tabelle 9.7: Zusätzliche Optionen zum Öffnen von Datensatzgruppen (Fortsetzung)
Sperren von Daten während der Bearbeitung Wenn mehrere Benutzer gleichzeitig auf die Daten einer Datenbank zugreifen können, müssen Sie festlegen, wie die ACE darauf reagieren soll. Verwenden Sie den Parameter <Sperren>, um einen der in Tabelle 9.8 aufgeführten Werte zu verwenden. Grundsätzlich gilt bei Sperrungen von Datensätzen, dass die ACE Bereiche der Daten bankdatei mit der Windows-Funktion LockFileEx sperrt. Da diese Funktion nicht einzelne Byte, sondern nur komplette RAM-Seiten sperren kann, die jeweils 4.096 Byte umfassen, sind von einer Sperrung unter Umständen mehrere Datensätze betroffen. Konstante
Beschreibung
dbReadOnly
Öffnet ein schreibgeschütztes Recordset. Kann nur für einen der Parameter <Sperren> oder eingesetzt werden. Es tritt keine Sperrung in Kraft.
dbPessimistic
Sperrt die komplette Speicherseite, in der sich der von einer Änderung betroffene Datensatz befindet, sobald die Bearbeitung beginnt (Edit-Methode). Von dieser Sperrung können je nach Umfang der Felder mehrere Datensätze betroffen sein.
dbOptimistic
Sperrt die komplette Speicherseite, in der sich der von einer Änderung betroffene Datensatz befindet, sobald der Datensatz aktualisiert wird (UpdateMethode).
Tabelle 9.8: Konstanten für den Parameter Sperren der OpenRecordset-Methode
Beispiel: Öffnen eines Recordsets auf Basis einer Tabelle Ein typisches Beispiel sieht folgendermaßen aus: Public Sub DatensatzgruppeOeffnen() Dim db As DAO.Database
530
DAO Dim rst As DAO.Recordset2 Set db = CurrentDb Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) With rst 'etwas mit dem Recordset machen End With rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 9.21: Verwendung von OpenRecordset
Hier ist die aktuelle Datenbank das Objekt, die Tabelle tblMitarbeiter die Quelle und dbOpenDynaset der Typ. Weitere Optionen sind nicht festgelegt.
Beispiel: Öffnen eines Recordsets auf Basis eines anderen Recordsets Sie können die OpenRecordset-Methode auch auf bestehenden Recordsets ausführen. Im folgenden Beispiel werden die Datensätze des ersten Recordsets gefiltert und das Ergebnis wird einem zweiten Recordset zugewiesen: Public Sub DatensatzgruppeOeffnen_Recordset() Dim db As DAO.Database Dim rstUngefiltert As DAO.Recordset2 Dim rstGefiltert As DAO.Recordset2 Set db = CurrentDb Set rstUngefiltert = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) rstUngefiltert.Filter = "Nachname LIKE 'A*'" Set rstGefiltert = rstUngefiltert.OpenRecordset With rstGefiltert 'etwas mit dem Recordset machen End With rstGefiltert.Close rstUngefiltert.Close
531
Kapitel 9 Set rstGefiltert = Nothing Set rstUngefiltert = Nothing Set db = Nothing End Sub Listing 9.22: OpenRecordset auf Basis eines bestehenden Recordsets
Was Sie alles mit Recordsets anstellen können, erfahren Sie in Abschnitt 9.7, »Daten bearbeiten mit dem Recordset- und dem Recordset2-Objekt«. Dort finden Sie auch weitere Praxisbeispiele für den Aufruf der OpenRecordset-Methode.
Beispiel: Öffnen eines Recordsets auf Basis eines QueryDef-Objekts Das Öffnen einer Datensatzgruppe auf Basis eines QueryDef (Objektbezeichnung für die in der Datenbank gespeicherten Abfragen) erfolgt prinzipiell wie bei Verwendung eines Recordsets als Basis: Public Sub DatensatzgruppeOeffnen_QueryDef() … Dim qdf As QueryDef … Set qdf = db.QueryDefs("qryMitarbeiter") Set rst = qdf.OpenRecordset(dbOpenDynaset) With rst 'etwas mit dem Recordset machen End With … Set qdf = Nothing End Sub Listing 9.23: Öffnen eines Recordsets auf Basis eines QueryDef-Objekts
Theoretisch hätten Sie hier auch direkt auf die Abfrage zugreifen können: Set rst = db.OpenRecordset("qryMitarbeiter", dbOpenDynaset)
Interessant wird es, wenn die Abfrage einen Parameter wie in Abbildung 9.10 enthält. Das Erstellen eines Recordsets auf Basis dieser Abfrage löst dann eine Fehlermeldung aus, die etwa folgendermaßen lautet: »Es wurde 1 Parameter erwartet, aber nur 0 übergeben.« Abhilfe schafft die Parameters-Auflistung des QueryDef-Objekts. Dieses enthält die in der Abfrage festgelegten Parameter. Weist man allen Parametern einen Wert zu, läuft die Routine ohne Fehler durch.
532
DAO
Abbildung 9.10: Abfrage mit Parameter
Public Sub DatensatzgruppeOeffnen_QueryDef_Parameter() … Dim prm As DAO.Parameter … Set qdf = db.QueryDefs("qryMitarbeiter") Set prm = qdf.Parameters("Nachname eingeben") prm.Value = "Wurst" Set rst = qdf.OpenRecordset(dbOpenDynaset) With rst 'etwas mit dem Recordset machen End With … End Sub Listing 9.24: Erzeugen eines Recordsets aus einem QueryDef-Objekt mit Parametern
9.6.14 Ausführen von Aktionsabfragen Die Methode Execute und die damit in Verbindung stehende Eigenschaft RecordsAffected dienen dem Ausführen von Aktionsabfragen und der Rückgabe der von einer Aktionsab frage betroffenen Zeilenzahl. Die folgende Routine verwendet die Execute-Anweisung, um eine Aktionsabfrage auszuführen, und gibt anschließend die Anzahl der betroffenen Datensätze aus:
Alles Wissenswerte zum Thema Aktionsabfragen erfahren Sie in Kapitel 8, »AccessSQL«.
533
Kapitel 9 Public Sub Aktionsabfrage() Dim db As DAO.Database Set db = CurrentDb db.Execute "INSERT INTO tblMitarbeiter(Vorname, Nachname) " _ & "VALUES('André', 'Minhorst')" Debug.Print db.RecordsAffected Set db = Nothing End Sub Listing 9.25: Ausführen einer Aktionsabfrage und Ausgabe der Anzahl der betroffenen Datensätze
9.7 Daten bearbeiten mit dem Recordset- und dem Recordset2-Objekt Das Bearbeiten von Daten ist vermutlich das bedeutendste Feature der DAO-Biblio thek. Dreh- und Angelpunkt sind das Recordset-Objekt, das Sie bereits weiter oben in 9.6.11 kurz kennen gelernt haben, und das Recordset2-Objekt, eines der neuen Ob jekte der neuen ACE-Bibliothek. Letzteres bietet einige Möglichkeiten mehr als das schon lange bekannte »klassische« Recordset-Objekt und ist unabkömmlich, wenn Sie Felder mit den neuen Attachment- oder Complex-Datentypen einsetzen möchten. Es ist vom bekannten Recordset-Objekt abgeleitet und damit abwärtskompatibel. Die folgenden Beispiele verwenden daher ausschließlich das Recordset2-Objekt. Wenn Sie keine der neuen Eigenschaften und Methoden verwenden, können Sie allerdings auch das Recordset-Objekt einsetzen. Wie bereits weiter oben erwähnt, müssen Sie in Zusammenhang mit Tabellen, die einen der mit Access 2007 eingeführten Datentypen (Attachment-Feld, mehrwertige Felder) enthalten, in jedem Fall das Recordset2-Objekt verwenden.
9.7.1 Methoden und Eigenschaften des Recordset2-Objekts In den nachstehenden Abschnitten lernen Sie die folgenden Methoden und Eigenschaften des Recordset2-Objekts kennen: EOF: Eigenschaft, die den Wert True annimmt, wenn sich der Datensatzzeiger hinter dem letzten Datensatz befindet.
534
DAO
MoveNext: Methode, die den Datensatzzeiger um eine Position zum Ende der Daten satzgruppe hin verschiebt. MovePrevious: Verschiebt den Datensatzzeiger um eine Position zum Anfang der Datensatzgruppe.. MoveFirst: Verschiebt den Datensatzzeiger auf den ersten Datensatz der Datensatz gruppe. MoveLast: Verschiebt den Datensatzzeiger auf den letzten Datensatz der Datensatz gruppe. Close: Schließt die Datensatzgruppe. AbsolutePosition: Gibt die absolute Position des Datensatzzeigers aus. PercentPosition: Gibt die prozentuale Position des Datensatzzeigers aus. RecordCount: Liefert die Anzahl der Datensätze, Besonderheiten siehe weiter unten. Requery: Aktualisiert den Inhalt der Datensatzgruppe. Fields: Auflistung der enthaltenen Felder. Index: Legt den Namen eines Indexes für die Seek-Methode fest. Seek: Durchsucht das mit der Index-Eigenschaft angegebene Feld nach dem angegebe nen Vergleichsausdruck. Funktioniert nur mit indizierten Feldern. NoMatch: Gibt True zurück, falls Seek oder eine der Find-Methoden kein Ergebnis liefern. FindFirst: Sucht nach dem ersten Datensatz mit dem angegebenen Kriterium. FindNext: Sucht den von der aktuellen Datensatzzeigerposition aus nächsten Daten satz mit dem angegebenen Kriterium. Bookmark: Liefert ein Lesezeichen auf den aktuellen Datensatz oder springt zu dem Datensatz mit dem angegebenen Bookmark. Sort: Sortiert die Datensatzgruppe nach den angegebenen Sortierkriterien. Filter: Filtert die Datensatzgruppe nach den angegebenen Filterkriterien. AddNew: Legt einen neuen Datensatz an. Edit: Bereitet einen existierenden Datensatz für Änderungen vor. Update: Speichert die geänderte Version eines Datensatzes in die Tabelle. Delete: Löscht den aktuellen Datensatz.
535
Kapitel 9
BOF: Ist True, wenn der Datensatzzeiger sich vor dem ersten Datensatz befindet. ParentRecordset (nur Recordset2): Gibt einen Verweis auf das übergeordnete Recordset zurück, wenn der Wert eines Feldes selbst ein Recordset darstellt (in mehrwertigen Feldern).
9.7.2 Datensätze durchlaufen Daten macht man aus mehreren Gründen als Recordset verfügbar, um per VBA darauf zuzugreifen: Datensätze sollen angelegt oder geändert werden und diese Änderung lässt sich nicht mit einer Aktionsabfrage erledigen. Sie möchten einen Datensatz anlegen und direkt danach auf seinen Primärschlüssel zugreifen. Sie müssen für einen oder mehrere Datensätze des Recordsets eine Aktion durchführen.
9.7.3 Alle Datensätze durchlaufen Das Durchlaufen aller Datensätze eines Recordset2-Objekts erfolgt in einer Schleife. Im folgenden Listing wird dazu eine Do While-Schleife verwendet. Die Abbruchbedingung der Schleife heißt rst.EOF. Die Eigenschaft EOF gibt an, dass sich der Datensatzzeiger hinter dem letzten Datensatz des Recordsets befindet. Das ist der Fall, wenn die MoveNext-Methode den Datensatzzeiger über den letzten Da tensatz hinausgeschoben hat. Die MoveNext-Methode schiebt den Datensatzzeiger jeweils um einen Datensatz weiter. Public Sub DatensatzgruppeDurchlaufen() Dim db As DAO.Database Dim rst As DAO.Recordset2 Set db = CurrentDb Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenTable) Do While Not rst.EOF Debug.Print rst!Vorname & " " & rst!Nachname rst.MoveNext Loop rst.Close Set rst = Nothing
536
DAO Set db = Nothing End Sub Listing 9.26: Durchlaufen aller Datensätze eines Recordsets
Tipp: Wann immer Sie eine Do While-Schleife zum Durchlaufen einer Datensatzgruppe verwenden, legen Sie immer erst den Körper an und vergessen Sie nicht die MoveNextAnweisung. Ansonsten werden Sie sich unter Umständen wundern, wie lange so ein Durchlauf durch eine Datensatzgruppe dauern kann …
9.7.4 Zu bestimmten Datensätzen springen Nicht immer möchte man die ganze Datensatzgruppe durchlaufen, sondern den Datensatzzeiger mal vor- und mal zurückbewegen oder gar auf ganz bestimmte Datensätze springen. Das Bewegen in kleinen Schritten lässt sich leicht mit den beiden Methoden MoveNext und MovePrevious bewerkstelligen, von denen Sie Erstere schon kennen gelernt haben. Zum Springen auf bestimmte Datensätze gibt es mehrere Möglichkeiten. Oft benötigt werden Sprünge auf den ersten oder den letzten Eintrag einer Datensatzgruppe. Dazu verwenden Sie die beiden Methoden MoveFirst und MoveLast. Beispiele dazu finden Sie weiter unten. Den Sprung auf einen beliebigen Datensatz ermöglicht die Methode Move. Sie erwar tet zwei Parameter: die Anzahl der zu überspringenden Datensätze und den Ausgangs punkt. Letzterer wird in Form eines Bookmarks übergeben – das ist eine Markierung eines Datensatzes. Wenn der zweite Parameter fehlt, bewegt die Move-Methode den Datensatzzeiger von der aktuellen Position aus.
9.7.5 Aktuelle Position des Datensatzzeigers ermitteln Die aktuelle Position des Datensatzzeigers lässt sich mit den beiden Eigenschaften AbsolutePosition und PercentPosition ermitteln. Die folgende Routine springt zunächst auf den ersten Datensatz und gibt die absolute und die prozentuale Position aus; dann folgt dieselbe Ausgabe für den letzten Datensatz: Public Sub Datensatzposition() Dim db As DAO.Database Dim rst As DAO.Recordset2 Set db = CurrentDb Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) rst.MoveFirst
537
Kapitel 9 Debug.Print "Absolut: " & rst.AbsolutePosition _ & " Prozentual: " & rst.PercentPosition rst.MoveLast Debug.Print "Absolut: " & rst.AbsolutePosition _ & " Prozentual: " & rst.PercentPosition rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 9.27: Ausgabe der absoluten und der prozentualen Position des Datensatzzeigers für den ersten und den letzten Datensatz
Andersherum lassen sich die beiden Eigenschaften auch dazu verwenden, die Position des Datensatzzeigers festzulegen – dazu weisen Sie einer der Eigenschaften einfach den gewünschten Wert zu. Im Falle des prozentualen Wertes landet der Datensatzzeiger auf dem nächstliegenden Datensatz.
Datensatzzeiger im Niemandsland Manchmal macht der Datensatzzeiger seinem Namen nicht gerade Ehre: Dann landet er vor dem ersten oder hinter dem letzten Datensatz. Eine der Eigenschaften, die diesen Zustand abfragt, haben Sie bereits weiter oben kennen gelernt: Dort prüfte die EOF-Eigenschaft, ob der Datensatzzeiger sich hinter dem letzten Zeiger befindet. Die Eigenschaft BOF prüft das Gegenteil, nämlich ob der Datensatzzeiger sich vor dem ersten Datensatz befindet. Zu beachten ist hier, dass man einen Fehler auslöst, wenn man den Datensatzzeiger um mehr als eine Position über den Rand des Recordsets hinausschiebt. Public Sub FehlerNachEOF() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) rst.MoveLast Debug.Print rst.EOF rst.MoveNext Debug.Print rst.EOF rst.MoveNext 'hier wird ein Fehler ausgelöst … End Sub Listing 9.28: Wenn EOF oder BOF den Wert True haben, darf der Datensatzzeiger nicht weiter vom Recordset wegbewegt werden
538
DAO
9.7.6 Anzahl der Datensätze ermitteln Das Ermitteln der in einem Recordset enthaltenen Anzahl an Datensätzen ist nicht so trivial, wie man es sich vielleicht vorstellt. Zwar gibt es eine Eigenschaft namens Record Count, aber diese kommt nicht immer zum richtigen Ergebnis. Die folgende Prozedur liefert beispielsweise den Wert 1 zurück, obwohl die zugrunde liegende Tabelle wesentlich mehr Datensätze enthält: Public Sub AnzahlDatensaetze_Falsch() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) Debug.Print rst.RecordCount … End Sub Listing 9.29: Diese Routine liefert die falsche Anzahl Datensätze zurück
Der Grund für das falsche Ergebnis liegt darin, dass die RecordCount-Eigenschaft offenbar nur die Datensätze zählt, die bereits einmal mit dem Datensatzzeiger »überfahren« wurden. Das ist zumindest nachweisbar, wenn man per MoveLast-Methode auf den letzten Datensatz springt und dann die Datensätze zählt. Folgende Routine gibt die korrekte Anzahl aus: Public Sub AnzahlDatensaetze_Richtig() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) rst.MoveLast Debug.Print rst.RecordCount … End Sub Listing 9.30: Erst ein Sprung auf den letzten Datensatz liefert die richtige Anzahl
Zwischen zwei Zählungen sollten Sie in jedem Fall die Requery-Methode des Recordset/Recordset2-Objekts verwenden, um den Datenbestand zu aktualisieren. Anderenfalls laufen Sie Gefahr, beim zwischenzeitlichen Löschen oder Hinzufügen von Datensätzen die alte Anzahl auszugeben. Die Requery-Methode sorgt für ein erneutes Einlesen der zugrunde liegenden Daten. In der folgenden Routine wird die Anzahl in beiden Fällen korrekt ausgegeben. Beachten Sie, dass die Requery-Methode vor dem Sprung auf den letzten Datensatz ausgeführt werden muss! Public Sub AnzahlDatensaetze_MitAktualisierung() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) rst.MoveLast
539
Kapitel 9 Debug.Print rst.RecordCount db.Execute "INSERT INTO tblMitarbeiter(Vorname, Nachname) " _ & "VALUES('Hans','Wurst')" rst.Requery rst.MoveLast Debug.Print rst.RecordCount … End Sub Listing 9.31: Zählen der Datensatzanzahl vor und nach dem Ändern des Datenbestands
Die ersten beiden Beispiele beschäftigen sich mit Dynaset-Recordsets. Mit Table-Record sets müssen Sie nicht erst auf den letzten Datensatz springen, um die korrekte Anzahl der enthaltenen Datensätze zu bestimmen. Hier reicht ein einfaches RecordCount: Public Sub AnzahlDatensaetze_Table() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenTable) Debug.Print rst.RecordCount … End Sub Listing 9.32: Beim Table-Recordset funktioniert RecordCount richtig
Zusätzlich wird hier deutlich, dass ein Table-Recordset direkt an die Tabelle gebunden ist: Wenn Sie die obige Prozedur zweimal die Anzahl der Datensätze ausgeben lassen und zwischendurch einen Datensatz entfernen, zeigt die zweite Ausgabe direkt die korrigierte Anzahl an. Bleibt noch das Snapshot-Recordset: Auch hier ist wieder der Sprung auf den letzten Datensatz nötig, um die korrekte Anzahl der Datensätze zu ermitteln. Da sich das Snapshot-Recordset nicht aktualisieren lässt, liefert RecordCount immer die Anzahl der Datensätze beim Anlegen der Datensatzgruppe zurück. Am schnellsten ermitteln Sie die Anzahl der Datensätze mit dem folgenden Listing: Public Sub AnzahlDatensaetze_Table_Schnell() Dim db As DAO.Database Set db = CurrentDb Debug.Print db.TableDefs("tblMitarbeiter").RecordCount Set db = Nothing End Sub Listing 9.33: Schnelle Ermittlung der Datensatzanzahl
540
DAO
9.7.7 Daten aus Datensätzen ausgeben Im Beispiel aus Listing 9.26 haben Sie bereits eine Methode zur Ausgabe des Inhalts von Feldern eines Datensatzes kennen gelernt. Dort wurde der Ausdruck rst!Vorname verwendet, um den Inhalt des Feldes Vorname der Datensatzgruppe auszugeben. Es gibt mehrere Möglichkeiten, um auf den Inhalt eines Feldes eines Datensatzes zuzugreifen. Die folgenden Ausdrücke geben alle den Inhalt des Feldes Vorname der Datensatzgruppe rst aus: Debug.Print Debug.Print Debug.Print Debug.Print Debug.Print Debug.Print Debug.Print Debug.Print Debug.Print Debug.Print
Die Eigenschaft Value kann man in jedem dieser Fälle weglassen, da es sich dabei um die Standardeigenschaft des Field-Objekts handelt. Alle Varianten haben ihre Berechtigung: rst!Vorname ist beispielsweise die kürzeste und kennzeichnet Vorname durch das Ausrufezeichen als Feldnamen und nicht als Eigenschaft oder Methode. Die Varianten, bei denen der Feldname in Klammern und Anführungszeichen eingebettet ist, können auch Variablen beinhalten: Dim strFeldname as String strFeldname = "Vorname" Debug.Print rst(strFeldname)
Auch Feldnamen mit Leerzeichen können dargestellt werden: rst.Fields("Feld mit Leerzeichen") rst![Feld mit Leerzeichen]
Das sollte Sie allerdings nicht dazu verleiten, Leerzeichen in Feldnamen unterzubringen – das ist sehr schlechter Stil und führt früher oder später zu Problemen. Und auch die Angabe der Ordinalposition eines Feldes innerhalb des Recordsets hat ihren Sinn: Auf diese Weise können Sie beispielsweise die Namen aller Felder ausgeben lassen. Außerdem ist diese Variante gegenüber denen mit Angabe einer Zeichenkette schneller.
541
Kapitel 9
9.7.8 Datensätze suchen Das Suchen von Datensätzen in Recordsets erfolgt in Abhängigkeit vom gewählten Recordset-Typ auf unterschiedliche Arten. In Table-Recordsets können Sie die in der Tabelle festgelegten Indizes direkt für die Suche einsetzen, was zu einer hohen Suchge schwindigkeit führt. Allerdings lässt sich ein Table-Recordset nicht für verknüpfte Ta bellen erstellen. In Dynaset- und Snapshot-Recordsets kann die Suche nicht zwangsläu fig auf Basis eines Index stattfinden.
9.7.9 Die Seek-Methode zum Suchen in Table-Recordsets Die Anwendung der Seek-Methode erfordert das Vorhandensein eines Index auf dem betroffenen Feld, dessen Name bekannt ist. Den Namen eines Index finden Sie im Dialog Indizes der jeweiligen Tabelle (siehe Abbildung 9.11). Sie können den Dialog über die Schaltfläche Indizes des Ribbons öffnen, während die betroffene Tabelle in der Entwurfsansicht angezeigt wird.
Abbildung 9.11: Ermitteln des Indexnamens
Die Suche per Seek sieht wie im folgenden Listing aus. Vor der Anwendung der SeekMethode legen Sie mit der Index-Eigenschaft den Namen des Index fest. Die SeekMethode selbst erwartet zwei Parameter: einen Vergleichsoperator (<, <=, =, =>, >) und den Vergleichswert. Nach dem Suchen prüft man mit der NoMatch-Eigenschaft, ob die Seek-Methode einen Datensatz gefunden hat. Die Seek-Methode verschiebt den
542
DAO
Datensatzzeiger auf den gefundenen Datensatz, sodass dieser bequem ausgegeben werden kann. Beachten Sie, dass der Datensatzzeiger auf dem aktuellen Datensatz stehen bleibt, wenn die Seek-Methode keinen Datensatz gefunden hat – die Prüfung der NoMatch-Eigenschaft ist also unbedingt erforderlich, um auch tatsächlich den gesuchten Datensatz auszugeben. Public Sub SucheMitSeek() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenTable) rst.Index = "idxNachname" rst.Seek "=", "Minhorst" If Not rst.NoMatch Then Debug.Print rst!Vorname & " " & rst!Nachname End If … End Sub Listing 9.34: Suche nach einem Datensatz mit der Seek-Methode
9.7.10 Die Find-Methoden zum Suchen in Dynaset- und Snapshot-Recordsets In Dynaset- und Snapshot-Recordsets lässt sich die Seek-Methode nicht einsetzen. Dafür gibt es dort mehrere Suchmethoden, die zwar nicht so performant sind, aber durch flexiblere Einsetzbarkeit glänzen. Es gibt nämlich insgesamt vier Methoden zum Suchen in Recordsets: FindFirst, FindNext, FindPrevious und FindLast. Da diese Methoden sich nicht auf einen Index beziehen, geben Sie hier direkt den Namen des Feldes an, in dem gesucht werden soll. Der Feldname wird zusammen mit dem Vergleichsoperator und dem Vergleichswert in einem Ausdruck zusammengefasst und der entsprechenden Find…-Methode als Parameter mitgegeben. Die folgende Routine sucht nach dem ersten Datensatz, dessen Feld Nachname den Wert Minhorst enthält: Public Sub SucheMitFindNext() Dim db As DAO.Database Dim rst As DAO.Recordset2 Dim strKriterium As String Set db = CurrentDb Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) strKriterium = "Nachname = 'Minhorst'" rst.FindFirst strKriterium If Not rst.NoMatch Then Debug.Print rst!Vorname & " " & rst!Nachname
543
Kapitel 9 Else Debug.Print "Kein passender Datensatz gefunden." End If rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 9.35: Suche nach einem Datensatz per FindNext-Methode
Auch hier müssen Sie unbedingt die NoMatch-Eigenschaft auswerten. Nur der Wert False weist auf eine erfolgreiche Suche hin, anderenfalls bleibt der Datensatzzeiger auf der aktuellen Position stehen.
9.7.11 Alle Datensätze mit einem bestimmten Kriterium finden Natürlich haben neben der FindFirst-Methode auch die anderen Methoden ihre Daseins berechtigung. Die FindNext-Methode lässt sich gut mit der FindFirst-Methode kombinieren, wenn es um das Durchforsten der ganzen Datensatzgruppe nach dem gewünschten Kriterium geht. Dazu sucht man zunächst mit der FindFirst-Methode den ersten Treffer und steigt im Erfolgsfall in eine Do While-Schleife ein, in der die Informationen zum gesuchten Datensatz ausgegeben werden und mit der FindNext-Methode der nächste Treffer gesucht wird: Public Sub SucheAlleMitFindNext() … strKriterium = "Nachname = 'Minhorst'" rst.FindFirst strKriterium Do While Not rst.NoMatch Debug.Print rst!Vorname & " " & rst!Nachname rst.FindNext strKriterium Loop … End Sub Listing 9.36: Suche aller Treffer in einem Recordset
Alternative: Vorheriges Filtern der Datensatzgruppe Die obigen Beispiele liefern keinen Grund, warum man nicht direkt die dem Recordset zugrunde liegende Tabelle oder Abfrage mit einem Kriterium so einschränken sollte, dass das Recordset nur noch die gesuchten Datensätze enthält.
544
DAO
Auch im wirklichen Leben sollten Sie immer prüfen, ob datenrelevante Funktionalität nicht in eine Abfrage mit Parameter verlagert werden könnte. Diese ist wesentlich schneller als die entsprechende Suchmethode in einem Recordset-/Recordset2-Objekt. Die Ausgabe nach allen Datensätzen mit einem bestimmten Nachnamen gestaltet sich dann wie folgt: Public Sub SucheMitSQLAbfrage() Dim Dim Dim Dim Dim
db As DAO.Database qdf As DAO.QueryDef rst As DAO.Recordset2 strName As String prm As DAO.Parameter
Set db = CurrentDb Set qdf = db.QueryDefs("qryMitarbeiter") Set prm = qdf.Parameters("Nachname eingeben") prm.Value = "Minhorst" Set rst = qdf.OpenRecordset(dbOpenDynaset) Do While Not rst.EOF Debug.Print rst!Vorname & " " & rst!Nachname rst.MoveNext Loop rst.Close Set Set Set Set
End Sub Listing 9.37: Suche nach einem Datensatz per Parameterabfrage
9.7.12 Lesezeichen DAO bietet eine Eigenschaft namens Bookmark an, um einen Verweis auf den aktuellen Datensatz zu erzeugen oder mit diesem Verweis auf den betroffenen Datensatz zu springen. Diese Eigenschaft wird meistens im Zusammenhang mit Recordsets verwendet, die an Formulare gebunden sind. Einige Beispiele dazu finden Sie in Kapitel 4, »Formulare«. An dieser Stelle sei nur die grundlegende Handhabung erläutert: Die Bookmark-Eigenschaft liefert einen Wert zurück, den Sie in einer Variant-Variablen speichern können:
545
Kapitel 9 Public Sub Lesezeichen() … Dim varLesezeichen As Variant … 'Lesezeichen speichern varLesezeichen = rst.Bookmark 'zum letzten Datensatz springen rst.MoveLast 'wieder zum markierten Datensatz zurück rst.Bookmark = varLesezeichen … End Sub Listing 9.38: Lesezeichen ermitteln und zum markierten Datensatz zurückspringen
9.8 Sortieren und Filtern von Datensätzen Zum Sortieren und Filtern von Datensätzen stellt die DAO-Bibliothek die Methoden Sort und Filter. Wie bereits beim Suchen von Datensätzen gibt es auch beim Filtern und Sortieren Unterschiede bezüglich der Handhabung von Table-Recordsets und Dynasetbeziehungsweise Snapshot-Recordsets.
9.8.1 Sortieren mit der Sort-Eigenschaft Das Sortieren von Dynasets und Snapshots erfolgt in drei Schritten: Erzeugen des Ausgangsrecordsets Festlegen des Sortierkriteriums mit der Sort-Eigenschaft Erzeugen des sortierten Recordsets auf Basis des Ausgangsrecordsets und des Wertes der Sort-Eigenschaft Für die Sort-Eigenschaft gibt man einen dem Parameter des ORDER BY-Kriteriums einer SQL-Abfrage entsprechenden Ausdruck an. In einer Prozedur sieht das folgendermaßen aus: Public Sub Sortieren_Dynaset() … 'Ausgangsrecordset erzeugen Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) 'Sortierung festlegen rst.Sort = "Nachname ASC" 'Sortierten Recordset erzeugen Set rstSortiert = rst.OpenRecordset(dbOpenDynaset)
546
DAO 'etwas mit dem sortierten Recordset machen … End Sub Listing 9.39: Sortiertes Recordset erzeugen
Alternative: Sortieren per ORDER BY-Klausel Auch hier gilt: Was sich per Abfrage erledigen lässt, sollte auch dort stattfinden. Sprich: Man verlegt den Sortiervorgang einfach in die Datenherkunft des Recordsets, indem man etwa folgenden Ausdruck verwendet: Set rst = db.OpenRecordset("SELECT * FROM tblMitarbeiter ORDER BY Nachname", dbOpenDynaset)
Theoretisch könnte man die SELECT-Anweisung auch direkt in einer Abfrage speichern, würde dadurch aber ein wenig an Flexibilität einbüßen. Das Sortierkriterium müsste man dann definitiv festlegen, während sich der SQLAusdruck in einer OpenRecordset-Anweisung in der Prozedur dynamisch zusammensetzen lässt.
9.8.2 Sortieren mit der Index-Eigenschaft Recordsets vom Typ Table lassen sich nur über die festgelegten Indizes sortieren. Wie diese Sortierung erfolgt, legen Sie im Dialog Indizes fest (siehe Abbildung 9.12). Wenn Sie sich die Möglichkeit offen halten möchten, in beiden Richtungen zu sortieren, legen Sie einfach zwei Indizes auf das gleiche Feld mit unterschiedlicher Sortier reihenfolge an. Beachten Sie, dass die Performance bei der Suche mit indizierten Feldern zwar meist verbessert wird, aber Anfüge- und Aktualisierungsabfragen mehr Rechen zeit benötigen, da auch die Indizes jeweils angepasst werden müssen.
Abbildung 9.12: Festlegen der Reihenfolge für einen Index
547
Kapitel 9 Public Sub Sortieren_Table() … 'Ausgangsrecordset erzeugen Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenTable) 'Index für die Sortierung festlegen rst.Index = "idxNachname" 'Daten in sortierter Reihenfolge ausgeben Do While Not rst.EOF Debug.Print rst!Vorname & " " & rst!Nachname rst.MoveNext Loop … End Sub Listing 9.40: Sortieren per Index
9.8.3 Filtern mit der Filter-Eigenschaft Das Filtern von Recordsets und Snapshots erfolgt nach dem gleichen Schema wie das Sortieren: Recordset anlegen, Filter festlegen, gefiltertes Recordset auf Basis des bestehen den Recordsets erzeugen. Die folgende Prozedur gibt zusätzlich zum Filtervorgang noch die gefilterten Datensätze aus: Public Sub Filtern_Dynaset() … 'Ausgangsrecordset erzeugen Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) 'Filter angeben rst.Filter = "Nachname = 'Minhorst'" 'Gefiltertes Recordset erzeugen Set rstGefiltert = rst.OpenRecordset(dbOpenDynaset) 'Gefilterte Datensätze ausgeben Do While Not rstGefiltert.EOF Debug.Print rstGefiltert!Nachname rstGefiltert.MoveNext Loop …. End Sub Listing 9.41: Erzeugen einer gefilterten Datensatzgruppe
548
DAO
9.9 Daten bearbeiten Bisher haben Sie erfahren, wie Sie mit DAO Recordsets erzeugen, darin navigieren oder Datensätze suchen. Die meisten dieser Aktionen haben weniger die Anzeige der Daten als vielmehr das Auswählen eines Datensatzes zur Bearbeitung zum Ziel. Die folgenden Abschnitte zeigen daher, wie Sie mit DAO Datensätze anlegen, bearbeiten und löschen können. Wie üblich gelten auch hier unterschiedliche Regeln für die Handhabung von Table-, Dynaset- und Snapshot-Recordsets. Das Ändern von Daten in Snapshots ist generell nicht möglich, da diese schreibgeschützt sind. Bei den anderen beiden RecordsetTypen kommt es zunächst darauf an, ob gerade ein anderer Benutzer darauf zugreift und/oder die Datenbank exklusiv geöffnet hat und/oder in den Optionsparametern beim Öffnen des Recordsets Beschränkungen auferlegt wurden (beispielsweise dbRead Only; siehe Tabelle 9.6). Außerdem kann es möglich sein, dass Sie gar keine Rechte über den schreibenden Zugriff besitzen. Schließlich gibt es noch den feinen Unterschied zwischen Table- und Dynaset-Recordsets: Greift keiner der anderen Gründe für eine Sperrung des Zugriffs, lässt sich das Table-Recordset auf jeden Fall bearbeiten und das Dynaset-Recordset in den meisten Fällen. Einzige Ausnahme könnte eine ungünstige Zusammenstellung der zugrunde liegenden Tabellen und ihrer Beziehungen sein. Ob sich der schreibende Zugriff auf ein Dynaset-Recordset generell verbietet, lässt sich ganz einfach feststellen, indem Sie den SQL-Ausdruck in eine Abfrage packen und dort versuchen, einen neuen Datensatz anzufügen. Auch programmatisch ist der Nachweis schnell erbracht: Die Eigenschaft Updatable liefert den Wert True zurück, wenn die Daten bearbeitet werden können.
9.9.1 Anlegen eines Datensatzes Das Anlegen eines Datensatzes erfolgt mit der AddNew-Methode des Recordset-/ Recordset2-Objekts und durch anschließendes Speichern des neuen Datensatzes mit der Update-Methode. Dazwischen liegen die Anweisungen zum Eintragen der Feldwerte: Public Sub DatensatzHinzufuegen() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) With rst .AddNew !Vorname = "Hans" !Nachname = "Wurst" .Update End With … End Sub Listing 9.42: Hinzufügen eines Datensatzes
549
Kapitel 9
Sofern das Primärschlüsselfeld als Autowert deklariert ist, müssen Sie sich im Übrigen genauso wenig Sorgen um dessen Anlage machen, als wenn Sie Daten direkt in die Tabelle eintragen – der Wert des Primärschlüsselfeldes wird automatisch hinzugefügt.
9.9.2 Bearbeiten eines Datensatzes Das Bearbeiten eines Datensatzes erfolgt ganz ähnlich. Hier müssen Sie allerdings zunächst zu einer der zuvor genannten Techniken greifen, um den zu bearbeitenden Datensatz zu finden. Anschließend starten Sie den Bearbeitungsvorgang mit der Edit-Methode und weisen den Feldern die gewünschten Werte zu. Das Speichern des geänderten Datensatzes erfolgt wie oben mit der Update-Methode. Public Sub DatensatzAendern() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) With rst .FindFirst "Vorname = 'Hans' AND Nachname = 'Wurst'" If Not .NoMatch Then .Edit !Vorname = "Hans-Peter" .Update End If End With … End Sub Listing 9.43: Ändern eines Datensatzes
9.9.3 Löschen eines Datensatzes Auch beim Löschen eines Datensatzes muss der zu löschende Datensatz zuvor ausgewählt werden. Ist der Datensatzzeiger auf dem gewünschten Datensatz positioniert, sorgt die Delete-Methode für die Entfernung dieses Datensatzes. Public Sub DatensatzLoeschen() … Set rst = db.OpenRecordset("tblMitarbeiter", dbOpenDynaset) With rst .FindFirst "Vorname = 'Hans' AND Nachname = 'Wurst'" If Not .NoMatch Then .Delete End If
550
DAO End With … End Sub Listing 9.44: Löschen eines Datensatzes
Alternative zum Löschen eines Datensatzes Das Löschen von Datensätzen lässt sich mit einer SQL-Aktionsabfrage in den meisten Fällen schneller und in jedem Fall durch weniger Codezeilen bewerkstelligen. Die SQLVariante des obigen Codes sieht beispielsweise folgendermaßen aus: Public Sub DatensatzLoeschenPerSQL() Dim db As DAO.Database Set db = CurrentDb db.Execute "DELETE FROM tblMitarbeiter " _ & "WHERE Vorname = 'Hans' AND Nachname = 'Wurst'" Set db = Nothing End Sub Listing 9.45: Löschen eines Datensatzes per SQL-Aktionsabfrage
9.9.4 Umgang mit Attachments Das Attachment-Feld ist einer von zahlreichen neuen Datentypen. Zahlreiche neue Datentypen? Nun, nur wenn man es sehr genau nimmt, denn für mehrwertige Felder gibt es mehrere Datentypen, die sich durch den Basisdatentyp unterscheiden. Betrachten Sie zunächst das Attachment-Feld. Es bietet auf der Benutzeroberfläche die Möglichkeit, Dateien hinzuzufügen, zu entfernen und aus der Tabelle heraus zu speichern. Dies funktioniert auch mit VBA und DAO. Attachments werden im Prinzip in einer weiteren, per 1:n-Beziehung mit der Ausgangstabelle verknüpften Tabelle gespeichert. Diese Tabelle ist zwar nicht sichtbar, hat aber Feldnamen und kann auch per DAO ausgelesen werden.
9.9.5 Attachment-Felder auslesen Die folgende Routine zeigt zunächst, wie Sie an die hinter einem Attachment-Feld ste ckende Tabelle herankommen und gibt zum Beweis die Feldnamen der verborgenen Tabelle aus. Die Routine bezieht sich auf die Tabelle tblDateien, die Sie mit der Prozedur aus Listing 9.9 angelegt haben. Nach dem Öffnen eines Recordsets auf Basis dieser
551
Kapitel 9
Tabelle weist die Routine einem weiteren Recordset die Value-Eigenschaft des AttachmentFeldes zu – spätestens dies belegt, dass sich hinter Attachments eine weitere, interne Tabelle verbirgt (natürlich können Sie auch die Kurzform mit Ausrufezeichen und ohne Value-Eigenschaft verwenden: rst!Datei). Dieses Recordset kann dann wie jedes andere behandelt werden, was hier durch das Ausgeben der Feldnamen- und Felddatentypen erfolgt: Public Sub FelderDerAttachmenttabelleAusgeben() Dim db As DAO.Database Dim rst As Recordset2 Dim rstAttachments As Recordset2 Dim fld As Field2 Set db = CurrentDb Set rst = db.OpenRecordset("tblDateien", dbOpenDynaset) If Not rst.EOF Then Set rstAttachments = rst.Fields("Datei").Value For Each fld In rstAttachments.Fields Debug.Print fld.Name, fld.Type Next fld Set rstAttachments = Nothing Else MsgBox "Die Funktion funktioniert nicht mit einer leeren Tabelle." End If rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 9.46: Auslesen der Felder der Anlagen-Tabelle
Die Ausgabe im Direktfenster sieht dann wie folgt aus: FileData FileFlags FileName FileTimeStamp FileType FileURL
11 4 10 8 10 12
Die Zahlen für die Datentypen entsprechen den Werten aus Tabelle 9.1. Daraus ergeben sich folgende Datentypen:
552
DAO
FileData: Byte-Array FileFlags: Long FileName: Text (enthält den Namen der angefügten Datei) FileTimeStamp: Date/Time FileType: Text FileUrl: Memo
Daten aus Attachment-Feldern auslesen, Variante I Gehen Sie einen Schritt weiter und erzeugen Sie einige Datensätze mit Attachments im Feld Datei. Die folgende Routine zeigt, wie Sie auf die im Attachment-Feld referenzierte, verborgene Tabelle zugreifen und deren Daten auslesen können. Im Prinzip ist diese Routine wie die aus dem vorherigen Listing aufgebaut – mit dem Unterschied, dass sie nicht die Felder, sondern die Datensätze des Attachment-Recordsets durchläuft und die darin gespeicherten Inhalte ausgibt. Public Sub AttachmentsAuslesenI() Dim db As DAO.Database Dim rst As Recordset2 Dim rstAttachments As Recordset2 Dim fld As Field2 Set db = CurrentDb Set rst = db.OpenRecordset("tblDateien", dbOpenDynaset) Do While Not rst.EOF Set rstAttachments = rst.Fields("Datei").Value Do While Not rstAttachments.EOF Debug.Print rstAttachments!FileFlags, _ rstAttachments!FileName, rstAttachments!Filetimestamp, _ rstAttachments!FileType, rstAttachments!FileURL rstAttachments.MoveNext Loop Set rstAttachments = Nothing rst.MoveNext Loop rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 9.47: Ausgabe der Informationen aller Anlagen der Datensätze einer Tabelle
553
Kapitel 9
Daten aus Attachment-Feldern auslesen, Variante II Wie obiges Beispiel zeigt, verhalten sich das Recordset rst und das Recordset rstAttachments etwa wie per 1:n-Beziehung verknüpfte Tabellen. Sie durchlaufen in einer übergeordneten Schleife alle Datensätze des Recordsets rst und in einer untergeordneten Schleife alle Datensätze von rstAttachments, die zu einem Feld des übergeordneten Recordsets gehören. Das können Sie auch einfacher haben, wenn auch scheinbar weniger strukturiert. Dazu greifen Sie bereits beim Deklarieren des Hauptrecordsets auf die Felder des im Attachment-Feld enthaltenen Recordsets zu: SELECT DateiID, Datei.FileData, Datei.Filename FROM tblDateien
Damit erstellen Sie quasi ein Recordset auf Basis der Tabelle tblDateien und der damit verknüpften und versteckten Tabelle mit den Attachments. Wenn Sie etwa alle Dateinamen der in den Datensätzen gespeicherten Dateien ausgeben möchten, können Sie das mit folgender Routine erledigen – achten Sie nur darauf, dass Sie die »Unterfelder« nicht mit der !-Syntax (rst!Datei.Filename), sondern beispielsweise mit rst("Datei.Filename") referenzieren (alternativ können Sie dem Feld bereits in der SQL-Anweisung einen passenden ALIAS-Namen zuweisen): Public Sub AttachmentsAuslesenII() Dim db As DAO.Database Dim rst As Recordset2 Set db = CurrentDb Set rst = db.OpenRecordset("SELECT DateiID, Datei.FileData, " _ & "Datei.Filename FROM tblDateien", dbOpenDynaset) Do While Not rst.EOF Debug.Print rst!DateiID, rst("Datei.FileName") rst.MoveNext Loop rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 9.48: Durchlaufen von Attachment-Recordsets ohne Einsatz eines weiteren Recordsets
Diese Variante können Sie auch für die nachfolgenden Beispiele zu Attachments verwenden. An die notwendige SQL-Anweisung kommen Sie im Übrigen am schnellsten, wenn Sie die gewünschten Felder wie in Abbildung 9.13 in der Abfrage-Entwurfsansicht einer neuen Abfrage zusammenstellen und dann in die SQL-Ansicht wechseln.
554
DAO
Abbildung 9.13: Abfrage über die Felder einer Tabelle mit Attachment-Feld
9.9.6 Dateien aus einem Attachment-Feld auf der Festplatte speichern DAO liefert natürlich auch eine Methode, mit der Sie den Inhalt eines AttachmentFeldes leicht auf der Festplatte speichern können: SaveToFile. Es handelt sich dabei um eine Methode des Field2-Objekts und die nur angewendet werden kann, wenn Sie ein Recordset2-Objekt auf Basis des Attachment-Feldes der betroffenen Tabelle erzeugt haben. Die folgende Routine zeigt anschaulich, wie das funktioniert: Sie löscht zunächst eventuell vorhandene Dateien gleichen Namens und schreibt dann die Attachments aller Datensätze in dieses Verzeichnis. Public Sub AttachmentsExportieren() Dim db As DAO.Database Dim rst As Recordset2 Dim rstAttachments As Recordset2 Dim fld As Field2 Dim strAttachment As String Set db = CurrentDb Set rst = db.OpenRecordset("tblDateien", dbOpenDynaset) Do While Not rst.EOF Set rstAttachments = rst.Fields("Datei").Value Do While Not rstAttachments.EOF Set fld = rstAttachments.Fields("Filedata") strAttachment = CurrentProject.Path & "\" _ & rstAttachments.Fields("FileName") On Error Resume Next Kill strAttachment On Error GoTo 0
555
Kapitel 9 fld.SaveToFile strAttachment rstAttachments.MoveNext Loop rst.MoveNext Loop rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 9.49: Speichern aller Anlagen einer Tabelle
Für den Praxiseinsatz können Sie diese Routine etwa dahingehend aufbohren, dass Attachments jeweils in Verzeichnissen gespeichert werden, die in Abhängigkeit von den übergeordneten Datensätzen erstellt und benannt werden – und natürlich können Sie auch nur einzelne Attachments speichern.
Limitierung durch Dateiendungen Wichtig ist außerdem, dass manche Dateitypen (etwa EXE-Dateien) vom Speichern in Attachments ausgeschlossen sind. Wenn Sie dies über die Benutzeroberfläche versuchen, erscheint noch eine aussagekräftige Meldung, beim Importieren einer Datei mit nicht zugelassender Endung per VBA liefert Access allerdings einen Laufzeitfehler mit unbekannter Fehlermeldung (»Unknown Error-message«). Gleiches gilt für das Speichern solcher Dateien: Immerhin könnte man auf die Idee kommen, Dateien mit nicht erlaubter Dateiendung vor dem Speichern in einem AttachmentFeld umzubenennen und dies anschließend durch Anpassen der Filename-Eigenschaft wieder rückgängig zu machen. Sollten Sie also auf diese Weise eine nicht erlaubte Datei in einem Attachment-Feld speichern und dieses mit SaveToFile wieder exportieren wollen, erhalten Sie ebenfalls eine entsprechende Fehlermeldung. Die genannte Limitierung lässt sich allerdings aushebeln: Die im Anlagefeld befindlichen Dateien verbergen sich im Binärfeld FileData der verborgenen Tabelle – dort sind sie Byte für Byte abgespeichert. Sie können das Binärfeld also etwa in eine ByteArray- oder eine Variant-Variable einlesen und deren Inhalt dann in einer Datei abspeichern. Das wäre ein programmierter Ersatz für die SaveToFile-Methode, der auch das Wiederherstellen blockierter Daten erlaubt. Allerdings ist der Datei im Binärfeld noch ein Header von variabler Länge vorangestellt, der zusätzliche Informationen zur Datei enthält. Wie groß dieser ist, können Sie aus dem ersten Element des Byte-Arrays ermitteln. Es enthält die Länge des Headers. Folglich beginnt die eigentliche Datei bei einem Offset im Array, der dieser Länge entspricht.
556
DAO
Weitere Informationen hierzu erhalten Sie in Kapitel 11, »Bilder und binäre Dateien«. Dort finden Sie auch Details über die Komprimierung von in Attachments gespeicherten Dateien.
9.9.7 Datei in Attachment-Feldern speichern Andersherum funktioniert dies auch: Die Methode LoadFromFile kennt ebenfalls nur den Parameter FileName. Die Routine AnlageImportieren erwartet zusätzlich die Nummer (ID) des Datensatzes, zu dessen Attachment-Feld die Datei hinzugefügt werden soll. Beim Speichern einer Datei in einem Attachment-Feld ist grundsätzlich zu beachten, dass Sie sowohl den Datensatz im Recordset des Hauptdatensatzes als auch den des im Attachment-Feld verborgenen Recordsets zum Bearbeiten vorbereiten – also entweder mit AddNew oder Edit – und die Änderung anschließend mit Update übernehmen. Im vorliegenden Fall soll ein neues Attachment hinzugefügt werden, also wird das übergeordnete Recordset mit Edit und das Attachment-Recordset mit AddNew für das Importieren der Datei vorbereitet. Public Sub AttachmentImportieren(strDatei As String, lngDateiID As Long) Dim Dim Dim Dim
db As DAO.Database rst As Recordset2 rstAttachments As Recordset2 fld As Field2
Set db = CurrentDb Set rst = db.OpenRecordset("SELECT * FROM tblDateien " _ & "WHERE DateiID = " & lngDateiID, dbOpenDynaset) rst.Edit Set rstAttachments = rst.Fields("Datei").Value rstAttachments.AddNew Set fld = rstAttachments.Fields("FileData") rstAttachments!FileData.LoadFromFile strDatei rstAttachments.Update rst.Update rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 9.50: Importieren einer Datei in das Attachment-Feld eines Recordsets
557
Kapitel 9
9.9.8 Löschen von Dateien in Attachment-Feldern Zum Löschen eines Attachments aus einer Tabelle bedarf es keiner besonderen Mittel, hier reicht die bekannte Delete-Methode, angewendet auf den passenden Datensatz der Attachment-Tabelle, völlig aus. Die folgende Routine referenziert wiederum das Attachment-Feld als eigenes Recordset2-Objekt und löscht einfach das Attachment, dessen Dateiname mit dem Parameter strDatei übergeben wurde. Das Löschen eines Attachments funktioniert im Übrigen auch ohne das vorherige Vorbereiten des übergeordneten Recordsets mit AddNew/Edit. Public Sub AttachmentLoeschen(strDatei As String, lngDateiID As Long) Dim db As DAO.Database Dim rst As Recordset2 Dim rstAttachments As Recordset2 Set db = CurrentDb Set rst = db.OpenRecordset("SELECT * FROM tblDateien " _ & "WHERE DateiID = " & lngDateiID, dbOpenDynaset) Set rstAttachments = rst.Fields("Datei").Value rstAttachments.FindFirst "Filename = '" & strDatei & "'" rstAttachments.Delete Set rstAttachments = Nothing Set rst = Nothing Set db = Nothing End Sub Listing 9.51: Das Löschen eines Attachments erfolgt auf herkömmliche Weise mit der DeleteMethode
9.9.9 Ersetzen eines Attachments Fehlt nur noch die Möglichkeit, ein bestehendes Attachment durch ein anderes zu überschreiben: Dazu setzen Sie eine Routine wie die folgende ein, die den Namen der zu überschreibenden und der neuen Datei sowie die ID des betroffenen Datensatzes als Parameter erwartet. Die Routine sucht zunächst den betroffenen Datensatz der Tabelle tblDateien heraus, referenziert dann das Attachment-Feld Datei als Recordset2-Objekt, sucht das Attachment mit dem angegebenen Namen und ersetzt dieses dann, indem es das alte Attachment löscht und ein neues hinzufügt. Public Sub AttachmentErsetzen(strNeueDatei As String, _ strAlteDatei As String, lngDateiID As Long) Dim db As DAO.Database Dim rst As DAO.Recordset2
558
DAO Dim rstAttachments As DAO.Recordset2 Dim fld As DAO.Field2 Set db = CurrentDb Set rst = db.OpenRecordset("SELECT * FROM tblDateien " _ & "WHERE DateiID = " & lngDateiID, dbOpenDynaset) rst.Edit Set rstAttachments = rst.Fields("Datei").Value rstAttachments.FindFirst "Filename = '" & strAlteDatei & "'" If Not rstAttachments.NoMatch Then rstAttachments.Delete rstAttachments.AddNew Set fld = rstAttachments.Fields("FileData") fld.LoadFromFile strNeueDatei rstAttachments.Update End If rst.Update Set rstAttachments = Nothing Set rst = Nothing Set db = Nothing End Sub Listing 9.52: Ersetzen eines Attachments per VBA
9.9.10 Umgang mit mehrwertigen Feldern Genau wie Attachment-Felder lassen sich auch die Inhalte von mehrwertigen Feldern auslesen, ändern, anlegen und löschen. Wie das funktioniert, erläutern die folgenden Abschnitte. Um mit dem Inhalt mehrwertiger Felder arbeiten zu können, müssen Sie zunächst einmal wissen, wie diese aufgebaut sind. Der Verdacht liegt nahe, dass sich ein ähnliches Konstrukt wie bei den Attachments dahinter verbirgt – was durch einen Versuch auch belegt wird. Die folgende Routine weist die Value-Eigenschaft eines mehrwertigen Feldes einem Recordset2-Objekt zu und durchläuft dessen Felder, um deren Namen und Datentypen auszulesen: Public Sub AufbauMehrwertigesFeld() Dim db As DAO.Database Dim rst As DAO.Recordset2 Dim rstMehrwertigesFeld As DAO.Recordset2 Dim fld As DAO.Field
559
Kapitel 9 Set db = CurrentDb Set rst = db.OpenRecordset("tblMehrwertigesFeld", dbOpenDynaset) Set rstMehrwertigesFeld = rst.Fields("MehrwertigesFeld").Value For Each fld In rstMehrwertigesFeld.Fields Debug.Print fld.Name, fld.Type Next fld Set rstMehrwertigesFeld = Nothing Set rst = Nothing Set db = Nothing End Sub Listing 9.53: Auslesen der Felder des Recordset2-Objekts, das sich hinter einem mehrwertigen Feld verbirgt
Das Ergebnis können Sie selbst nach dem Starten dieser Routine im Direktfenster ablesen: Die hinter einem mehrwertigen Feld steckende Tabelle besitzt ein Feld namens Value mit dem Datentyp Text.
9.9.11 Lesen des Inhalts von mehrwertigen Feldern, Variante I Mit dieser Erkenntnis lässt sich der Inhalt des Feldes leicht auslesen – nämlich genau wie bei Daten eines Attachment-Feldes. Sie müssen einfach nur einem weiteren Record set den Inhalt des mehrwertigen Feldes zuweisen und dieses Recordset durchlaufen. Achten Sie nur darauf, dass Sie ein Recordset2-Objekt und nicht das ältere RecordsetObjekt verwenden. Da das Anlegen, Bearbeiten und Löschen von Daten in mehrwertigen Feldern bei Verwendung eines Recordsets nichts anderes ist, als wenn Sie die genannten Operationen mit einem herkömmlichen Recordset-Objekt durchführen, sollen diese Techniken hier nicht erneut erläutert werden. Public Sub MehrwertigesFeldLesen() Dim db As DAO.Database Dim rst As DAO.Recordset2 Dim rstMehrwertfeld As DAO.Recordset2 Set db = CurrentDb Set rst = db.OpenRecordset("tblMehrwertigesFeld", dbOpenDynaset) Do While Not rst.EOF Set rstMehrwertfeld = rst.Fields("MehrwertigesFeld").Value Do While Not rstMehrwertfeld.EOF Debug.Print rstMehrwertfeld!Value rstMehrwertfeld.MoveNext Loop rst.MoveNext
560
DAO Loop Set db = Nothing End Sub Listing 9.54: Lesen des Inhalts mehrwertiger Felder, lange Version
9.9.12 Lesen des Inhalts mehrwertiger Felder, Variante II Genau wie bei Attachment-Feldern können Sie auch einen direkteren Weg zur Ausgabe der im mehrwertigen Feld enthaltenen Daten wählen. Public Sub MehrwertigesFeldLesenII() Dim db As DAO.Database Dim rst As DAO.Recordset2 Set db = CurrentDb Set rst = db.OpenRecordset("SELECT MehrwertigesFeld.Value " _ & "FROM tblMehrwertigesFeld", dbOpenDynaset) Do While Not rst.EOF Debug.Print rst("MehrwertigesFeld.Value") rst.MoveNext Loop Set db = Nothing End Sub Listing 9.55: Lesen des Inhalts mehrwertiger Felder, kurze Version
9.10 QueryDefs — Auswahl oder Aktion nach Wahl Weiter oben haben Sie bereits erfahren, wie Sie QueryDefs verwenden, um Parameter abfragen mit Werten zu versehen und das Ergebnis per OpenRecordset verfügbar zu machen. Ein QueryDef-Objekt kann allerdings nicht nur Auswahlabfragen als Basis für die OpenRecordset-Methode liefern, sondern auch Aktionsabfragen beinhalten und eine geeignete Methode zum Ausführen der Abfrage bereitstellen. Die folgende Routine füllt ein QueryDef-Objekt mit der Abfrage qryMitarbeiterLoeschen und führt diese mit der Execute-Methode aus. Über die RecordsAffected-Methode schreibt die Routine anschließend die Anzahl der betroffenen Datensätze in das Direktfenster. Public Sub QueryDefMitAktion() Dim db As DAO.Database Dim qdf As DAO.QueryDef Set db = CurrentDb
561
Kapitel 9 Set qdf = db.QueryDefs("qryMitarbeiterLoeschen") qdf.Execute dbFailOnError Debug.Print qdf.RecordsAffected Set qdf = Nothing Set db = Nothing End Sub Listing 9.56: Anwenden einer Aktionsabfrage per QueryDef
Die Execute-Methode enthält einen Parameter, der hier mit der Konstanten dbFailOnError gefüllt ist. Diese Konstante sorgt dafür, dass beim Auftreten eines Fehlers beim Bearbeiten einer der betroffenen Datensätze alle bereits getätigten Änderungen zurückgesetzt werden. Eine weitere interessante Konstante heißt dbSeeChanges: Diese löst einen Laufzeitfehler aus, wenn ein anderer Benutzer Daten ändert, die von Ihnen bearbeitet wurden.
9.11 Transaktionen Mit DAO lassen sich mehrere Datenoperationen in einer Transaktion zusammenfassen. Das bedeutet, dass die Änderungen an den Daten erst durchgeführt werden, wenn die Transaktion mit der entsprechenden DAO-Methode ausdrücklich abgeschlossen wird. In der Praxis ist das interessant, wenn etwa ein Geldtransfer von einem Konto zum anderen Konto abgebildet werden soll. Dazu wird der Kontostand des ersten Kontos vermindert und der des zweiten Kontos erhöht. Es müssen unbedingt beide Aktionen durchgeführt werden, da sonst eine Inkonsistenz entsteht, wenn einer der beiden Vorgänge ohne den anderen durchgeführt wird. Transaktionen beziehen sich immer auf den angegebenen Workspace. Damit ist sichergestellt, dass immer nur ein Benutzer Änderungen während einer Transaktion durchführt. Sie selbst müssen allerdings dafür sorgen, dass wirklich auch nur die geplanten Datenänderungen innerhalb einer Transaktion erfolgen – anderenfalls verwirft Access gegebenenfalls Änderungen, die vielleicht gar nicht Bestandteil der Transaktion sein sollen. Das Handhaben von Transaktionen ist eigentlich ganz einfach. Abbildung 9.14 zeigt den prinzipiellen Ablauf einer Transaktion. Der Start erfolgt mit der BeginTrans-Anwei sung. Nach dem Ändern der Daten prüft man, ob alle Vorgänge erfolgreich durchge führt werden konnten, und ruft dann eine der beiden Methoden CommitTrans oder Rollback auf.
562
DAO
B eginT rans
D atens ätze bearbeiten
B earbeitung übernehm en? N ein
Ja
C om m itT rans
R ollbac k
Abbildung 9.14: Ablauf einer Transaktion
Im Code sieht das etwa folgendermaßen aus: Public Sub Transaktion(lngKonto1ID As Long, lngKonto2ID As Long, _ curBetrag As Currency) Dim Dim Dim Dim Dim Dim
wrk As DAO.Workspace db As DAO.Database curKonto1Alt As Currency curKonto2Alt As Currency curKonto1Neu As Currency curKonto2Neu As Currency
Set wrk = DBEngine.Workspaces(0) Set db = wrk.Databases(0) 'Transaktion starten wrk.BeginTrans 'Alte Kontostände ermitteln und zwischenspeichern curKonto1Alt = FLookup("Kontostand", "tblKonten", _ "KontoID = " & lngKonto1ID) curKonto2Alt = FLookup("Kontostand", "tblKonten", _ "KontoID = " & lngKonto2ID) 'Umbuchung vornehmen db.Execute "UPDATE tblKonten SET Kontostand = Kontostand - " _ & curBetrag & " WHERE KontoID = " & lngKonto1ID db.Execute "UPDATE tblKonten SET Kontostand = Kontostand + " _ & curBetrag & " WHERE KontoID = " & lngKonto2ID
563
Kapitel 9 'Neue Kontostände ermitteln und zwischenspeichern curKonto1Neu = FLookup("Kontostand", "tblKonten", "KontoID = " _ & lngKonto1ID) curKonto2Neu = FLookup("Kontostand", "tblKonten", "KontoID = " _ & lngKonto2ID) 'Prüfen, ob die gewünschten Änderungen durchgeführt wurden 'und Änderungen entweder durchführen oder verwerfen If curKonto1Neu = curKonto1Alt - curBetrag _ And curKonto2Neu = curKonto2Alt + curBetrag Then wrk.CommitTrans Debug.Print "Transaktion erfolgreich." Else wrk.Rollback Debug.Print "Transaktion nicht erfolgreich." End If Set db = Nothing Set wrk = Nothing End Sub Listing 9.57: Beispiel einer Transaktion
Die DLookup-Anweisung läuft nicht im gleichen Kontext der Transaktion und ist daher an dieser Stelle nutzlos. Sie kann zwar die Daten vor der Änderung ermitteln, hat aber keinen Einblick in die innerhalb der Transaktion temporär geänderten Daten. Daher verwendet die Prozedur eine alternative Funktion namens FLookup, die neben der Transaktionsfähigkeit außerdem noch schneller als die klassische DLookup-Funktion ist. Das folgende Listing zeigt den Aufbau der Funktion. Sie liefert genau die gleichen Ergebnisse wie die DLookup-Funktion, enthält allerdings keine Fehlerbehandlung. Public Function FLookup(strField As String, strTable As String, _ strCriteria As String) As Variant Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb Set rst = db.OpenRecordset(strTable, dbOpenDynaset) rst.FindFirst strCriteria If Not rst.NoMatch Then FLookup = rst(strField) Else FLookup = Null
564
DAO End If rst.Close Set rst = Nothing Set db = Nothing End Function Listing 9.58: Eine schnelle DLookup-Variante auf DAO-Basis
Der Vollständigkeit halber finden Sie noch eine schnellere Variante, die direkt mit einem SQL-Ausdruck arbeitet: Public Function FLookup(strField As String, strTable As String, _ strCriteria As String) As Variant Dim strSQL As String On Error Resume Next strSQL = "SELECT [" & strField & "] FROM [" & strTable & "]" If Len(strCriteria) > 0 Then strSQL = strSQL & " WHERE " & strCriteria FLookup = DBEngine(0)(0).OpenRecordset(strSQL, dbOpenForwardOnly)(0) End Function
Ein Praxisbeispiel zum Thema »Transaktionen« finden Sie in Kapitel 4, »Formulare«, Abschnitt 4.7.2, »Undo in Haupt- und Unterformularen«.
Quellen zu diesem Kapitel [1] CurrentDBC: http://groups.google.com/group/comp.databases.ms-access/msg/9fe9 8bb5d7cba5ea
565
10 ADO Im vorherigen Kapitel haben Sie bereits erfahren, dass DAO die bevorzugte Datenzugriffs-Bibliothek ist, wenn es um die Entwicklung reiner Access-Anwendungen geht. ADO (ActiveX Data Objects) war von Microsoft als Nach folger von DAO geplant. Es eröffnet wesentlich mehr Möglichkeiten, die sich aber vor allem dann bemerkbar machen, wenn Sie ein alternatives Backend wie die Microsoft SQL Server 2005 Express Edition oder den Microsoft SQL Server 2005 (diese beiden werden nachfolgend synonym als SQL Server bezeichnet) verwenden. Der Fokus dieses Buchs richtet sich auf die Entwicklung reiner Access-Datenbankanwendungen. Daher sollen die Möglichkeiten von Access in Zusammenarbeit mit dem SQL Server hier nicht betrachtet werden. Entwickler, die jetzt noch reine Access-Anwendungen pro grammieren, aber diese im Hinblick auf einen späteren Wechsel auf den SQL Server direkt auf dieses Backend vorbereiten möchten, sollen natürlich nicht außen vor bleiben – zumal ADO auch für reine Access-Anwendungen einige Features bereithält, die DAO nicht bietet. Wegen des gegenüber DAO wesentlich größeren Funk tionsumfangs könnte über ADO ein eigenes Buch geschrie ben werden. Aus Platzgründen wird das Thema hier je doch ein wenig eingeschränkt – und zwar so, dass Sie die Techniken, die Sie im vorherigen Kapitel über DAO kennen gelernt haben, auch mit ADO einsetzen können. Natürlich soll auch die eine oder andere Spezialität von ADO nicht
Kapitel 10
unerwähnt bleiben. Viele bereits in Kapitel 9, »DAO«, enthaltene Informationen gelten auch für den Umgang mit ADO. Dies bezieht sich vor allem auf formale Techniken wie den Umgang mit Auflistungen, die Verwendung von Punkt oder Ausrufezeichen für den Bezug auf Elemente und Eigenschaften oder das Deklarieren und Instanzieren von Objekten. Wenn Sie Informationen zu diesen Themen benötigen, schlagen Sie am besten im oben genannten Kapitel nach. Die Beispiele zu diesem Kapitel finden Sie auf der Buch-CD unter Kap_10\ADO. accdb.
ADO-Neuigkeiten Mit Access 2007 kommt keine neue ADO-Version, sondern lediglich ein neuer JET-OLE DB-Provider namens Microsoft Office 12.0 Access Database Engine OLE DB Provider (Micro soft.ACE.OLEDB.12.0). Im Gegensatz zu DAO mit den neuen Objekten Recordset2 und Field2 gibt es unter ADO keine Möglichkeit, die in Attachment- oder mehrwertigen Feldern gespeicherten Daten über ein zusätzliches Recordset auszulesen. Daher kann man nur über die »Unterfelder« auf die in den verknüpften, verborgenen Tabellen enthaltenen Daten zugreifen. Wie dies funktioniert, erfahren Sie unter 10.3.7, »Daten eines Recordsets mit mehrwertigen Feldern ausgeben«, und 10.3.8, »Daten eines Recordsets mit Attachment-Feldern ausgeben«.
10.1 Zugriff auf eine Datenquelle herstellen Obgleich Sie im Folgenden feststellen werden, dass viele Vorgehensweisen unter DAO und ADO gleich ablaufen, unterscheidet sich das Objektmodell von ADO in einigen Punkten vom DAO-Objektmodell. Das macht sich beim Zugriff auf die gewünschte Da tenbank sofort bemerkbar, wie der folgende Abschnitt zeigen wird.
Connection und ConnectionString Das beginnt damit, dass nicht das Database-Objekt, sondern das Connection-Objekt Ur sprung aller lesenden, schreibenden und sonstigen Zugriffe auf die Tabellen der Daten bank ist. Das Connection-Objekt enthält immer einen ConnectionString, der Informationen über die Verbindung zur gewünschten Datenbank enthält. Diesen ConnectionString stellt man entweder selbst zusammen oder man lässt sich dabei unterstützen. Die einfachste Methode dazu ergibt sich beim Zugriff auf die aktuelle Datenbank. In diesem Fall brauchen Sie einfach nur auf das Connection-Objekt des bestehenden CurrentProject-Objekts zuzugreifen.
568
ADO Public Sub Verbindung() Dim cnn As ADODB.Connection Set cnn = CurrentProject.Connection With cnn Debug.Print cnn.ConnectionString End With End Sub Listing 10.1: Erzeugen einer Verbindung zur aktuellen Datenbank und Ausgabe der Connection String-Eigenschaft
Die Verbindungszeichenfolge für die aktuelle Datenbank sieht etwa so aus – gut, dass Sie diesen Ausdruck nicht selbst zusammenstellen müssen: Provider=Microsoft.Jet.OLEDB.4.0Microsoft.ACE.OLEDB.12.0Microsoft.ACE. OLEDB.12.0;User ID=Admin;Data Source=E:\ADO.accdbmdb;Mode=Share Deny None;Extended Properties="";Jet OLEDB:System database=C:\Dokumente und Einstellungen\Administrator\Anwendungsdaten\Microsoft\Access\System. mdw;Jet OLEDB:Registry Path= Software\Microsoft\Office\12.0\Access\Access Connectivity EngineSoftware\Microsoft\Office\11.0\Access\Jet\4.0;Jet OLEDB:Database Password="";Jet OLEDB:Engine Type=65;Jet OLEDB:Database Locking Mode=1;Jet OLEDB:Global Partial Bulk Ops=2;Jet OLEDB:Global Bulk Transactions=1;Jet OLEDB:New Database Password="";Jet OLEDB:Create System Database=False;Jet OLEDB:Encrypt Database=False;Jet OLEDB:Don't Copy Locale on Compact=False;Jet OLEDB:Compact Without Replica Repair=False;Jet OLEDB: SFP=False;Jet OLEDB:Support Complex Data=True
Was die Parameter im Einzelnen bedeuten, soll hier gar nicht aufgeschlüsselt werden – für die Verwendung der Datenbank, zu der auch das VBA-Projekt gehört, reicht die Kenntnis, dass CurrentProject.Connection die richtige Verbindung liefert. Für den Fall, dass Sie einmal eine Verbindung zu einer externen Datenquelle herstellen möchten, brauchen Sie den ConnectionString auch nicht unbedingt manuell zusammenzustellen. Zur Ermittlung der passenden Parameter können Sie auch einen Dialog verwenden, der bei der Festlegung der Parameter behilflich ist. Die folgende Routine erzeugt eine Instanz des DataLinks-Objekts und zeigt mit der Methode PromptNew den Dialog zum Anlegen einer neuen Verbindungszeichenfolge an (siehe Abbildung 10.1). Public Function ConnectionStringErmitteln() ConnectionStringErmitteln = CreateObject("DataLinks").PromptNew End Function Listing 10.2: Aufruf des Dialogs zum Ermitteln einer Verbindungszeichenfolge
Mit dem gleichen Dialog können Sie nicht nur die Eigenschaften neuer Verbindungen auswählen und zurückgeben lassen, sondern auch die Eigenschaften von CurrentProject.
569
Kapitel 10
Connection etwas übersichtlicher ausgeben (siehe Abbildung 10.2). Dazu müssen Sie die aufrufende Routine geringfügig abändern: Public Function ConnectionStringBearbeiten() ConnectionStringBearbeiten = _ CreateObject("DataLinks").PromptEdit(CurrentProject.Connection) End Function Listing 10.3: Aufrufen des Dialogs Datenverknüpfungseigenschaften für eine bestehende Verbindungszeichenfolge
Abbildung 10.1: Anlegen einer neuen Verbindung
Abbildung 10.2: Bearbeiten von CurrentProject.Connection
570
ADO
10.2 Manipulation des Datenmodells Die Manipulation von Tabellen, Feldern und anderen Datenbankobjekten erfolgt nicht über ADO selbst, sondern über die Objekte, Methoden und Eigenschaften der Bibliothek ADOX. Die entsprechende Bibliothek im Verweise-Dialog heißt Microsoft ADO Ext. 2.7 for DDL and Security.
10.2.1 Anlegen einer Tabelle Zum Anlegen einer Tabelle benötigen Sie das Catalog-Objekt der ADOX-Bibliothek. Dieses wird mit der Eigenschaft ActiveConnection auf die aktuelle Datenbank eingestellt. Zum Löschen einer eventuell schon vorhandenen gleichnamigen Tabelle stellt das Catalog-Objekt die Delete-Methode der Tables-Auflistung zur Verfügung. Anschließend wird ein neues Table-Objekt erstellt und mit dem Namen tblUnternehmen versehen. Bevor die Tabelle an die Tables-Auflistung angehängt und damit verfügbar gemacht wird, fügen Sie zwei Felder an: UnternehmenID und Unternehmen. Dabei werden zwei unterschiedliche Vorgehensweisen verwendet: Die erste instanziert zunächst ein neues Column-Objekt, füllt dessen Eigenschaften mit den entsprechenden Daten und fügt es dann an die Columns-Auflistung des Table-Objekts an. Die zweite übergibt die benötigten Informationen direkt beim Anfügen eines neuen Feldes an das Table-Objekt. Nach dem Anlegen werden die Tables-Auflistung und der Navigationsbereich aktualisiert. Public Sub TabelleAnlegen_Unternehmen() Dim cat As ADOX.Catalog Dim tbl As ADOX.Table Dim col As ADOX.Column Set cat = New ADOX.Catalog cat.ActiveConnection = CurrentProject.Connection 'Bestehende Tabelle löschen cat.Tables.Delete "tblUnternehmen" 'Verweis auf neues Table-Objekt Set tbl = New ADOX.Table 'Name der Tabelle zuweisen tbl.Name = "tblUnternehmen" 'Feld neu erstellen und per Objektvariable referenzieren Set col = New ADOX.Column
571
Kapitel 10 col.Name = "UnternehmenID" col.Type = adInteger tbl.Columns.Append col 'Noch ein Feld erstellen, kurze Fassung tbl.Columns.Append "Unternehmen", adVarWChar, 30 'Tabelle anhängen und Katalog aktualisieren With cat.Tables .Append tbl .Refresh End With 'Navigationsbereich aktualisieren Application.RefreshDatabaseWindow Set tbl = Nothing Set cat = Nothing End Sub Listing 10.4: Anlegen einer Tabelle mit ADOX
Konstanten für Datentypen unter ADO und ADOX Tabelle 10.1 zeigt die häufigsten ADOX-Konstanten für die unterschiedlichen Datentypen. Konstante
Datentyp
adBigInt
Big Integer
adBinary
Binary
adBoolean
Boolean
adUnsignedTinyInt
Byte
adChar
Char
adCurrency
Currency
adDate
Date/Time
adNumeric
Decimal
adDouble
Double
adGUID
GUID
adSmallInt
Integer
adInteger
Long
adLongVarBinary
Long Binary (OLE Object), Attachment.FileData
Tabelle 10.1: Konstanten für den Datentyp
572
ADO Konstante
Datentyp
adLongVarWChar
Memo
adNumeric
Numeric
adSingle
Single
adWChar, adVarWChar
Text (Unicode)
adDBTime
Time
adIDispatch
Attachment, ComplexTypes
adDBTimeStamp
Time Stamp
Tabelle 10.2: Konstanten für den Datentyp (Fortsetzung)
10.2.2 Autowert anlegen Wenn Sie das Feld UnternehmenID als Autowert festlegen möchten, müssen Sie in obiger Routine noch einige Zeilen hinter dem Anlegen des Feldes hinzufügen: 'Anlegen eines Autowertes With tbl.Columns("UnternehmenID") .ParentCatalog = cat .Properties("AutoIncrement") = True End With
AutoIncrement ist eine Eigenschaft, die nur für Access-Datenbanken gilt.
10.2.3 Löschen einer Tabelle Das Löschen einer Tabelle erfolgt über die Delete-Methode der Tables-Auflistung. Die folgende Routine löscht die soeben erstellte Tabelle und aktualisiert die Tables-Auflistung sowie den Navigationsbereich. Public Sub TabelleLoeschen_Unternehmen() Dim cat As ADOX.Catalog Set cat = New ADOX.Catalog cat.ActiveConnection = CurrentProject.Connection 'Bestehende Tabelle löschen On Error Resume Next cat.Tables.Delete "tblUnternehmen" On Error GoTo 0 'Katalog aktualisieren cat.Tables.Refresh
573
Kapitel 10 'Navigationsbereich aktualisieren Application.RefreshDatabaseWindow Set cat = Nothing End Sub Listing 10.5: Löschen einer Tabelle mit ADOX
10.2.4 Erstellen eines Index Mit der folgenden Routine fügen Sie der Tabelle tblUnternehmen einen Primärindex auf dem Feld UnternehmenID hinzu. Public Sub IndexErstellen() Dim Dim Dim Dim Dim
cat As ADOX.Catalog idx As ADOX.Index tbl As ADOX.Table col As ADOX.Column idxs As ADOX.Indexes
'Catalog instanzieren und auf aktuelle Datenbank einstellen Set cat = New ADOX.Catalog cat.ActiveConnection = CurrentProject.Connection 'Tabelle festlegen Set tbl = cat.Tables("tblUnternehmen") 'Verweis auf Indexes-Auflistung erstellen Set idxs = tbl.Indexes 'Neues Index-Objekt erstellen Set idx = New ADOX.Index With idx 'Index-Objekt mit Eigenschaften ausstatten .Name = "PrimaryKey" .PrimaryKey = True .Unique = True 'Column-Objekt mit zu indizierendem Feld erzeugen 'und zur Auflistung der indizierten Columns hinzufügen Set col = New ADOX.Column col.Name = "UnternehmenID" .Columns.Append col End With 'Index an die Auslistung Indexes anfügen idxs.Append idx
End Sub Listing 10.6: Anlegen eines Index mit ADOX
10.2.5 Löschen eines Index Zum Entfernen eines Index verwenden Sie die folgende Routine. Sie setzt die DeleteMethode der Indexes-Auflistung zum Entfernen des Index ein. Public Sub IndexLoeschen() Dim cat As ADOX.Catalog Dim tbl As ADOX.Table Dim idxs As ADOX.Indexes Set cat = New ADOX.Catalog cat.ActiveConnection = CurrentProject.Connection Set tbl = cat.Tables("tblUnternehmen") Set idxs = tbl.Indexes 'Index aus der Auflistung löschen idxs.Delete "PrimaryKey" Set tbl = Nothing Set idxs = Nothing Set cat = Nothing End Sub Listing 10.7: Löschen eines Index aus der Auflistung der Indizes einer Tabelle
10.2.6 Erstellen einer Beziehung Um eine Beziehung zwischen zwei Tabellen herzustellen, verwenden Sie die folgende Routine. Voraussetzung ist, dass das Fremdschlüsselfeld der Detailtabelle den gleichen Datentyp wie das Primärschlüsselfeld der Mastertabelle hat. Außerdem muss der Primärschlüssel der Mastertabelle eindeutig sein. Sind die Voraussetzungen erfüllt (und die angegebenen Tabellen beziehungsweise Felder vorhanden), legt die Routine die Beziehung aus Abbildung 10.3 an. Wenn Sie zusätzlich Löschweitergabe oder Aktualisierungsweitergaben definieren möchten, müssen Sie die Eigenschaften DeleteRule und UpdateRule des key-Objekts mit den entsprechenden Werten bestücken. Mit der Konstanten adRICascade sorgen Sie für die Weitergabe der jeweiligen Aktion.
575
Kapitel 10 Public Sub BeziehungErstellen() Dim cat As ADOX.Catalog Dim tbl As ADOX.Table Dim key As ADOX.key Set cat = New ADOX.Catalog cat.ActiveConnection = CurrentProject.Connection 'Tabelle mit dem Fremdschlüsselfeld festlegen Set tbl = cat.Tables("tblMitarbeiter") 'Neuen Key instanzieren und Eigenschaften zuweisen Set key = New ADOX.key key.Name = "ForeignKey" key.Type = adKeyForeign 'Tabelle mit Primärschlüssel festlegen key.RelatedTable = "tblUnternehmen" 'Verknüpfungsfeld der Detailtabelle angeben key.Columns.Append "UnternehmenID" 'Optional: Lösch- oder Aktualisierungsweitergabe key.DeleteRule = adRICascade key.UpdateRule = adRICascade 'Verknüpfungsfeld der Mastertabelle angeben key.Columns("UnternehmenID").RelatedColumn = "UnternehmenID" 'Key an die Keys-Auflistung anhängen tbl.keys.Append key Set key = Nothing Set tbl = Nothing Set cat = Nothing End Sub Listing 10.8: Anlegen einer Beziehung zwischen zwei Tabellen
Abbildung 10.3: Mit ADOX erstellte Beziehung
576
ADO
10.2.7 Löschen einer Beziehung Die Beziehung beziehungsweise den Fremdschlüssel löschen Sie mit der folgenden Routine: Public Sub BeziehungLoeschen() Dim cat As ADOX.Catalog Set cat = New ADOX.Catalog cat.ActiveConnection = CurrentProject.Connection 'Beziehung in Form des Fremdschlüssels löschen cat.Tables("tblMitarbeiter").keys.Delete "ForeignKey" Set cat = Nothing End Sub Listing 10.9: Löschen einer Beziehung
10.3 Zugriff auf Tabellen, Abfragen und die darin enthaltenen Daten In den folgenden Abschnitten erfahren Sie, wie Sie auf die Daten der Datenbank zugreifen können.
10.3.1 Ausgeben aller Tabellen Im vorigen Kapitel zum Thema DAO haben Sie erfahren, wie Sie mit DAO alle Tabellen ausgeben oder prüfen, ob eine Tabelle vorhanden ist. Unter ADO (nicht ADOX!) gibt es eine derartige Funktion nicht – dafür bietet Access aber ab Version 2000 die AllTables-Auf listung. Die AllTables-Auflistung ist ein Element des CurrentData-Objekts, das wiederum zum Application-Objekt gehört. Statt einer ADO-Routine zum Ausgeben aller Tabellen finden Sie also nun zumindest eine Routine, die kein DAO verwendet: Public Sub TabellenAusgeben() Dim obj As AccessObject For Each obj In CurrentData.AllTables Debug.Print obj.Name Next obj End Sub Listing 10.10: Alle Tabellen der Datenbank ausgeben
577
Kapitel 10
10.3.2 Prüfen, ob eine Tabelle vorhanden ist Auch hier brauchen Sie nicht mit ADO zu arbeiten, sondern können die AllTablesAuflistung verwenden: Public Function IstTabelleVorhanden_ADO(strTabellenname As String) _ As Boolean Dim objTable As AccessObject On Error Resume Next Set objTable = CurrentData.AllTables (strTabellenname) IstTabelleVorhanden_ADO = Not objTable Is Nothing End Function Listing 10.11: Prüfen des Vorhandenseins einer Tabelle
10.3.3 Datensatzgruppe auf Basis einer Tabelle öffnen Das Öffnen einer Datensatzgruppe erfolgt anders als unter DAO. Die Methode zum Öffnen ist keine Methode des übergeordneten Objekts (bei DAO das Database-Objekt), sondern eine Methode des ADO-Recordset-Objekts selbst. Die Open-Methode erwartet den Namen der zu öffnenden Tabelle oder Abfrage beziehungsweise einen SQLAusdruck, den Connection-String sowie zwei Parameter zur Angabe des Cursortyps und des Sperrmechanismus. Die Beschreibung der verschiedenen Möglichkeiten finden Sie im Anschluss an die Routine zum Öffnen einer Datensatzgruppe. Hier tritt offen zu Tage, weshalb es sich lohnt, bei der Variablendeklaration explizit die Bibliothek mit anzugeben, aus der die Objekte stammen: Wenn Sie das versäumt haben und die DAOBibliothek ist in der Liste der Verweise oberhalb der ADO-Bibliothek angeordnet, sucht die folgende Routine vergeblich die Open-Methode des Recordset-Objekts. Public Sub DatensatzgruppeOeffnen() Dim cnn As ADODB.Connection Dim rst As ADODB.Recordset Set cnn = CurrentProject.Connection Set rst = New ADODB.Recordset rst.Open "tblUnternehmen", cnn, adOpenDynamic, adLockOptimistic With rst 'etwas mit der Datenatzgruppe machen End With rst.Close
578
ADO Set rst = Nothing Set cnn = Nothing End Sub Listing 10.12: Öffnen einer Datensatzgruppe mit ADO
Das eigentliche Öffnen der Datensatzgruppe kann auch so erfolgen, wobei zunächst die Eigenschaften festgelegt werden und erst dann die Datensatzgruppe geöffnet wird: rst.ActiveConnection = cnn rst.LockType = adLockOptimistic rst.CursorType = adOpenKeyset rst.Open "tblUnternehmen"
10.3.4 Cursor-Typen Beim Öffnen einer Datensatzgruppe unter ADO können Sie folgende Cursor-Typen für die Eigenschaft CursorType verwenden: adOpenDynamic: Liefert eine Gruppe von Datensätzen und zeigt Datensatzänderun gen anderer Benutzer an (entspricht dbOpenDynaset unter DAO). adOpenForwardOnly: Liefert einen Snapshot der gewünschten Datensätze zum Zeit punkt des Öffnens des Recordset-Objekts, kann nur vorwärts durchlaufen werden (entspricht dbOpenForwardOnly unter DAO). adOpenKeyset: Liefert Verweise auf die Datensätze der zugrunde liegenden Tabellen (entspricht keiner DAO-Konstante genau, ist aber fast äquivalent zu dbOpenDynaset und etwas schneller als adOpenDynamic). adOpenStatic: Liefert einen Snapshot der gewünschten Datensätze zum Zeitpunkt des Öffnens des Recordset-Objekts (entspricht dbOpenSnapshot unter DAO).
10.3.5 Sperrung von Daten Mit dem Parameter LockType legen Sie fest, wie die Daten beim Schreiben gesperrt werden sollen: adLockReadonly: Öffnet ein schreibgeschütztes Recordset. adLockPessimistic: Sperrt die komplette Speicherseite, in der sich der von einer Ände rung betroffene Datensatz befindet, sobald die Bearbeitung beginnt. adLockOptimistic: Sperrt die komplette Speicherseite, in der sich der von einer Ände rung betroffene Datensatz befindet, erst, wenn der Datensatz aktualisiert wird. adLockBatchOptimistic: Wie adLockOptimistic, aber für die UpdateBatch-Methode.
579
Kapitel 10
10.3.6 Datensätze eines Recordsets durchlaufen Zum Durchlaufen der Datensätze eines Recordsets verwenden Sie beispielsweise die Do While-Schleife, in der Sie nach dem Durchführen der gewünschten Aktion jeweils mit der MoveNext-Methode einen Datensatz weiter springen. Als Abbruchbedingung dient die EOF-Eigenschaft der Datensatzgruppe, die den Wert True erhält, wenn der Datensatzzeiger über den letzten Datensatz hinaus verschoben wurde. Public Sub DatensaetzeDurchlaufen() Dim cnn As ADODB.Connection Dim rst As ADODB.Recordset Set cnn = CurrentProject.Connection Set rst = New ADODB.Recordset rst.Open "tblUnternehmen", cnn, adOpenKeyset, adLockOptimistic Do While Not rst.EOF With rst 'etwas mit dem aktuellen Datensatz tun End With rst.MoveNext Loop rst.Close Set rst = Nothing Set cnn = Nothing End Sub Listing 10.13: Datensatzgruppe durchlaufen
Das Pendant zur EOF-Eigenschaft ist die BOF-Eigenschaft. Sie erhält den Wert True, wenn der Datensatzzeiger sich vor dem ersten Datensatz der Datensatzgruppe befindet. Neben der MoveNext-Methode gibt es noch die Methoden MoveFirst, MoveLast und MovePrevious zum Bewegen innerhalb der Datensatzgruppe.
10.3.7 Daten eines Recordsets mit mehrwertigen Feldern ausgeben ADO bietet keine besondere Möglichkeit, um auf die Inhalte mehrwertiger Felder zuzugreifen – es gibt also kein Recordset2- oder Field2-Objekt wie unter DAO. Daher können Sie nur auf die in solchen Feldern enthaltenen Daten zugreifen, wenn Sie diese in der Datenherkunft über die »Unterfelder« referenzieren, wie das folgende Beispiel zeigt:
580
ADO Public Sub DatensaetzeDurchlaufen_MehrwertigesFeld() Dim cnn As ADODB.Connection Dim rst As ADODB.Recordset Set cnn = CurrentProject.Connection Set rst = New ADODB.Recordset rst.Open "SELECT MehrwertigesFeld.Value " _ & "FROM tblMehrwertigesFeld", _ cnn, adOpenKeyset, adLockOptimistic Do While Not rst.EOF With rst Debug.Print rst.Fields("MehrwertigesFeld.Value") End With rst.MoveNext Loop rst.Close Set rst = Nothing Set cnn = Nothing End Sub Listing 10.14: Zugriff auf mehrwertige Felder mit einem ADO-Recordset
10.3.8 Daten eines Recordsets mit Attachment-Feldern ausgeben Der Umgang mit den in Attachment-Feldern gespeicherten Daten sieht prinzipiell wie bei den mehrwertigen Feldern aus: Public Sub DatensaetzeDurchlaufen_Attachment() Dim cnn As ADODB.Connection Dim rst As ADODB.Recordset Set cnn = CurrentProject.Connection Set rst = New ADODB.Recordset rst.Open "SELECT DateiID, Datei.FileType, " _ & "Datei.Filename FROM tblDateien", _ cnn, adOpenKeyset, adLockOptimistic Do While Not rst.EOF With rst Debug.Print rst!DateiID, rst.Fields("Datei.FileType"), _ rst.Fields("Datei.Filename")
581
Kapitel 10 End With rst.MoveNext Loop rst.Close Set rst = Nothing Set cnn = Nothing End Sub Listing 10.15: Zugriff auf Attachment-Felder
10.3.9 Anzahl der Datensätze in einer Datensatzgruppe ermitteln Um die Anzahl der Datensätze zu ermitteln, dürfen Sie nicht die Konstante adOpenFor wardOnly für die Eigenschaft CursorType einsetzen. Außerdem müssen Sie für die Eigen schaft CursorLocation den Parameter adUseClient verwenden, da serverseitige DatensatzCursor unter ACE das Zählen von Datensätzen nicht unterstützen. Public Sub Datensatzanzahl() Dim cnn As ADODB.Connection Dim rst As ADODB.Recordset Set cnn = CurrentProject.Connection Set rst = New ADODB.Recordset rst.CursorLocation = adUseClient rst.CursorType = adOpenForwardOnly rst.LockType = adLockOptimistic rst.ActiveConnection = cnn rst.Open "tblUnternehmen" Debug.Print rst.RecordCount End Sub Listing 10.16: Ermitteln der Datensatzanzahl eines Recordsets
10.3.10 Prüfen, ob eine Datensatzgruppe leer ist Eine einfache Möglichkeit, um herauszufinden, ob eine Datensatzgruppe leer ist, besteht im Prüfen der EOF- und der BOF-Eigenschaften der Datensatzgruppe: Public Sub LeereDatensatzgruppe() Dim cnn As ADODB.Connection Dim rst As ADODB.Recordset
582
ADO Set cnn = CurrentProject.Connection Set rst = New ADODB.Recordset rst.Open "tblMitarbeiter", cnn, adOpenForwardOnly, adLockOptimistic If rst.BOF And rst.EOF Then MsgBox "Die Datensatzgruppe ist leer." End If rst.Close Set rst = Nothing Set cnn = Nothing End Sub Listing 10.17: Prüfen, ob eine Datensatzgruppe leer ist
10.3.11 Ausgabe des Inhalts eines Recordsets Manchmal möchte man den Inhalt eines Recordsets auf die Schnelle betrachten oder etwa in einer Textdatei speichern. Dabei leistet die GetString-Methode gute Dienste. Diese Methode ist sehr flexibel und kann, mit zusätzlichen Parametern ausgestattet, die Spalten auch als formatierten Text zurückgeben. Zu den möglichen (optionalen) Parametern markieren Sie GetString im Code und betätigen die F1-Taste. Public Sub DatensatzgruppeAusgeben() … rst.Open "tblUnternehmen", cnn, adOpenForwardOnly, adLockOptimistic Debug.Print rst.GetString … End Sub Listing 10.18: Ausgeben der Daten einer Datensatzgruppe
10.3.12 Speichern der Daten in einem Array Wenn Sie die Daten einer Datensatzgruppe in einem Array weiter verarbeiten möchten, können Sie mit der GetRows-Methode ein zweidimensionales Array mit den in der Datensatzgruppe enthaltenen Daten füllen. Die GetRows-Methode hat drei Parameter: Rows gibt an, wie viele Datensätze eingelesen werden sollen, Start enthält ein Bookmark auf den ersten einzulesenden Datensatz und Fields die Position oder den Namen des einzulesenden Feldes beziehungsweise ein Array mit den Namen der einzulesenden Felder. Ohne die Angabe von Parametern liest GetRows alle Datensätze mit allen Feldern ein.
583
Kapitel 10 Public Sub DatensatzgruppeInArray() Dim Dim Dim Dim Dim
cnn As ADODB.Connection rst As ADODB.Recordset varRecordset() As Variant i As Integer j As Integer
Set cnn = CurrentProject.Connection Set rst = New ADODB.Recordset rst.Open "tblUnternehmen", cnn, adOpenForwardOnly, adLockOptimistic varRecordset = rst.GetRows() For i = 0 To UBound(varRecordset, 2) For j = 0 To UBound(varRecordset, 1) Debug.Print varRecordset(j, i) Next j Next i rst.Close Set rst = Nothing Set cnn = Nothing End Sub Listing 10.19: Einlesen der Daten einer Datensatzgruppe in ein Array
10.3.13 Abfragen mit Parametern verwenden Während Sie unter DAO das QueryDef-Objekt und die Parameters-Auflistung verwendet haben, um Abfragen mit Parametern zu handhaben, stellt ADO für diesen Zweck das Command-Objekt zur Verfügung. Das folgende Beispiel zeigt, wie Sie ein Recordset auf Basis einer mit Parametern versehenen Abfrage öffnen. Sie verwenden hier nicht wie bei DAO eine Parameters-Auflistung, sondern geben den Wert eines einzelnen Parameters als String und mehrere Werte in der richtigen Reihenfolge als String-Array an – bei anderen Datentypen für die Parameter verwenden Sie dann eher ein Variant-Array. Public Sub Parameterabfrage() Dim Dim Dim Dim
cnn As ADODB.Connection cmd As ADODB.Command rst As ADODB.Recordset lngRecordsAffected As Long
Set cnn = CurrentProject.Connection
584
ADO 'Command-Objekt instanzieren Set cmd = New ADODB.Command 'Aktuelle Verbindung zuweisen cmd.ActiveConnection = cnn 'auszuführende Abfrage angeben cmd.CommandText = "qryMitarbeiter" 'Typ des Commands festlegen cmd.CommandType = adCmdTable 'Command-Objekt ausführen. Der erste Parameter wird nur für 'Aktionsabfragen benötigt, der zweite enthält die Werte für die 'Abfrageparameter. Ein Parameter wird als String, mehrere als 'Array übergeben. Set rst = cmd.Execute(, "Minhorst") Do While Not rst.EOF Debug.Print rst!Vorname, rst!Nachname rst.MoveNext Loop Set rst = Nothing Set cnn = Nothing End Sub Listing 10.20: Erzeugen eines Recordsets auf Basis einer Parameterabfrage
10.4 Datensätze suchen Zur Suche von Datensätzen gibt es mehrere Möglichkeiten. Wie unter DAO können Sie indizierte Felder mit der Seek-Methode durchsuchen, anderenfalls hilft die Find-Me thode weiter. Diese ist allerdings nicht so flexibel wie die Find…-Methoden von DAO, wie Sie nachfolgend lesen können. Am einfachsten ist es jedoch, die gesuchten Datensätze direkt in der Datenherkunft des Recordset-Objekts einzugrenzen.
10.4.1 Gesuchte Datensätze per Source-Eigenschaft des Recordsets ermitteln Die Source-Eigenschaft eines Recordset-Objekts enthält die dem Recordset-Objekt zugrunde liegende Tabelle oder Abfrage. Sie können hier auf drei Arten bereits mit dem Öffnen des Recordset-Objekts die gewünschten Daten ausfindig machen:
585
Kapitel 10
Direkte Angabe einer SELECT-Anweisung (jeweils in einer Zeile): rst.Open "SELECT * FROM Artikel WHERE Artikelname LIKE 'A%'", cnn, adOpenKeyset, adLockOptimistic
Angabe einer gespeicherten Abfrage: rst.Open "qryArtikelMitPreisGroesser50", cnn, adOpenKeyset, adLockOptimistic
Kombination aus SELECT-Anweisung und gespeicherter Abfrage: rst.Open "SELECT * FROM qryArtikelMitPreisGroesser50 WHERE Artikelname LIKE 'T%'", cnn, adOpenKeyset, adLockOptimisticJoker in Zeichenketten unter ADO und SQL
Wenn Sie wie in den obigen Beispielen Vergleichsausdrücke mit Platzhaltern verwenden möchten, müssen Sie die SQL Server-Syntax verwenden. Dabei entspricht der Platzhalter für beliebig viele Zeichen dem Prozentzeichen (%) und nicht wie in VBA oder Abfragen, die Sie über die Abfrageentwurfsansicht erstellen (siehe Abbildung 10.4), dem Sternchen (*). Der Platzhalter für ein einzelnes Zeichen entspricht dem Unterstrich (_) und nicht wie in VBA oder Abfragen dem Fragezeichen (?).
Abbildung 10.4: Joker für beliebig viele Zeichen in Abfragen
10.4.2 Seek Wenn Sie mit der Seek-Methode nach Daten suchen möchten, müssen zwei Bedingungen erfüllt sein: Das zu durchsuchende Feld muss indiziert sein und Sie müssen die Konstante adCmdTableDirect als Option beim Öffnen der Datensatzgruppe festlegen. Ersteres prüfen Sie ganz einfach, indem Sie in der Entwurfsansicht einer Tabelle den Indizes-Dialog einblenden (Ribbon-Eintrag Entwurf|Einblenden/Ausblenden|Indizes, siehe Abbildung
586
ADO
10.5). Letzteres impliziert, dass Sie nur auf einzelne Tabellen, aber nicht auf Abfragen oder verknüpfte Tabellen zugreifen können.
Abbildung 10.5: Anzeigen der Indizes einer Tabelle
Die Option adcmdTableDirect verwenden Sie mit der Open-Methode des Recordset-Objekts. Die folgende Routine zeigt, wie Sie mit der Seek-Methode einen bestimmten Datensatz einer Tabelle finden und den Wert seines Primärschlüsselfeldes ausgeben. Im Gegensatz zur weiter unten vorgestellten Find-Methode enthält das Recordset-Objekt nicht alle Datensätze, die dem Suchkriterium entsprechen, sondern es wird lediglich der Datensatzzeiger auf einem Datensatz platziert, der den mit der Seek-Methode übergebenen Parametern entspricht. Dabei gibt es verschiedene Varianten, die Sie mit dem Parameter SeekOption übergeben. Die wichtigsten sind folgende: adSeekFirstEQ: Setzt den Datensatzzeiger auf den ersten Datensatz mit dem angegebenen Wert. adSeekLastEQ: Setzt den Datensatzzeiger auf den letzten Datensatz mit dem angegebenen Wert. Seek ist unter den gegebenen Bedingungen die schnellste Möglichkeit, um auf einen bestimmten Datensatz zuzugreifen.
587
Kapitel 10 Public Sub SuchenMitSeek() Dim cnn As ADODB.Connection Dim rst As ADODB.Recordset Set cnn = CurrentProject.Connection Set rst = New ADODB.Recordset 'Recordset mit direktem Zugriff auf die Tabelle öffnen rst.Open "Artikel", cnn, adOpenKeyset, adLockOptimistic, _ adCmdTableDirect 'Index festlegen: Achtung, Indexname und nicht den Feldnamen verwenden! rst.Index = "Artikelname" 'Suche starten rst.Seek "Chocolade", adSeekFirstEQ 'Aktuellen Datensatz anzeigen If Not rst.EOF Then Debug.Print rst![Artikel-Nr] End If rst.Close Set rst = Nothing Set cnn = Nothing End Sub Listing 10.21: Suche mit der Seek-Methode
10.4.3 Find Die Find-Methode ist weniger flexibel als die Find…-Methoden von DAO. Sie fasst zwar die Funktion der Find…-Methoden von DAO zusammen, beschränkt allerdings beispielsweise die möglichen Kriterien auf ein einziges. Die Find-Methode bietet vier Parameter, von denen nur die Angabe des Suchkriteriums Pflicht ist. Als Suchkriterium dient ein aus Feldname, Vergleichsoperator und Vergleichs wert zusammengesetzter Ausdruck, wobei die gleichen Regeln wie für die WHEREKlausel von SQL-Abfragen gelten (siehe auch Kapitel 8, »Access-SQL«). Die übrigen Parameter: SkipRecords legt fest, wie viele Datensätze von der aktuellen Position aus übersprungen werden sollen, und ist beispielsweise wichtig, wenn Sie bereits einen Datensatz gefunden haben und bei der Suche des nächsten Datensatzes nicht auf dem aktuellen Datensatz stehen bleiben möchten.
588
ADO
SearchDirection erwartet eine der Konstanten adSearchForward oder adSearchBackward und sucht in der entsprechenden Richtung. Start erwartet die Position des Datensatzzeigers für einen vom ersten Datensatz abweichenden Startpunkt. Wenn die Find-Methode einen Datensatz findet, positioniert sie den Datensatzzeiger auf dem gefundenen Datensatz. Sollen weitere Datensätze gefunden werden, wie etwa im folgenden Beispiel, verwenden Sie innerhalb einer Do While-Schleife erneut die FindMethode – mit dem gleichen Kriterium, aber dem Wert 1 für den Parameter SkipRecords. Dies hat den Grund, dass die Find-Methode mit der Suche immer in dem Datensatz be ginnt, auf dem sich aktuell der Datensatzzeiger befindet. Eine solche Suche bedingt immer die Prüfung auf die Eigenschaft EOF oder BOF der Datensatzgruppe (je nach Suchrichtung), da dies die Position des Datensatzzeigers ist, wenn kein Datensatz gefunden wurde. Das entspricht dann der Eigenschaft Recordset. NoMatch unter DAO. Public Sub SuchenMitFind() Dim cnn As ADODB.Connection Dim rst As ADODB.Recordset Dim strKriterium As String Set cnn = CurrentProject.Connection Set rst = New ADODB.Recordset rst.Open "Artikel", cnn, adOpenKeyset, adLockOptimistic strKriterium = "[Artikelname] LIKE 'A*'" rst.Find strKriterium Do While Not rst.EOF Debug.Print rst!Artikelname rst.Find strKriterium, 1 Loop … End Sub Listing 10.22: Suche nach allen Datensätzen, deren Artikelname mit A beginnt
10.4.4 Filtern Genau wie DAO enthält das Recordset-Objekt auch unter ADO eine Eigenschaft namens Filter. Unter DAO weist man einem Recordset-Objekt den Filter zu und erhält das gefilterte Ergebnis, wenn man ein weiteres Recordset-Objekt auf Basis des ersten öffnet. Dies ist unter ADO einfacher: Hier weisen Sie einfach das gewünschte Filterkriterium zu und können direkt im gleichen Recordset auf das gefilterte Ergebnis zugreifen.
589
Kapitel 10 Public Sub FilternEinesRecordset() … rst.Open "Artikel", cnn, adOpenKeyset, adLockOptimistic rst.Filter = "Artikelname LIKE 'A*'" Do While Not rst.EOF Debug.Print rst!Artikelname rst.MoveNext Loop … End Sub Listing 10.23: Filtern aller Datensätze, deren Artikelname mit A beginnt
Um den Filter zu entfernen, verwenden Sie die Konstante adFilterNone an Stelle eines Parameters: rst.Filter = adFilterNone
10.4.5 Sortieren Die Datensätze eines Recordsets können Sie entweder bereits über die Datenherkunft oder durch nachträgliches Hinzufügen eines Sortierkriteriums sortieren. In der Daten herkunft gibt es zwei Möglichkeiten: Sie öffnen die Datensatzgruppe mit der Option adTableDirect. In diesem Fall kommt die im Dialog Indizes angegebene Sortierung zur Anwendung. Sie öffnen eine gespeicherte Abfrage mit voreingestellter Sortierreihenfolge oder geben einen SQL-Ausdruck mit einem entsprechenden Sortierkriterium an.
Sortieren nach dem Öffnen des Recordsets Wie in DAO bietet auch ADO eine Eigenschaft zum Angeben eines Sortierkriteriums. Voraussetzung für seinen Einsatz ist, dass das Recordset-Objekt einen clientseitigen Cur sor verwendet. Wie auch die Filter-Eigenschaft wirkt sich die Sort-Eigenschaft unmittelbar auf die aktuelle Datensatzgruppe aus. Nach dem Öffnen des Recordsets können Sie eine Sortierung mit der Sort-Eigenschaft vornehmen: Public Sub SortierenEinesRecordset() … rst.CursorLocation = adUseClient rst.Open "Artikel", cnn, adOpenDynamic, adLockOptimistic rst.Sort = "Artikelname ASC" Do While Not rst.EOF Debug.Print rst!Artikelname
590
ADO rst.MoveNext Loop … End Sub Listing 10.24: Nachträgliches Sortieren einer Datensatzgruppe
10.4.6 Lesezeichen Auch ADO-Recordsets bieten eine Bookmark-Eigenschaft. Bookmarks dienen dazu, sich die Position eines Datensatzes zu merken und auf einen gemerkten Datensatz zurückzuspringen. Daher ist diese Eigenschaft les- und schreibbar. Unter Verwendung eines Bookmarks können Sie beispielsweise von einem bestimmten Datensatz an das Ende der Datensatzgruppe und anschließend wieder zurück zum ursprünglichen Datensatz springen. Public Sub SpringenMitBookmark() … Dim varLesezeichen As Variant … rst.Open "Artikel", cnn, adOpenKeyset, adLockOptimistic rst.Find "Artikelname = 'Chai'" Debug.Print rst!Artikelname varLesezeichen = rst.Bookmark rst.MoveLast Debug.Print rst!Artikelname rst.Bookmark = varLesezeichen Debug.Print rst!Artikelname … End Sub Listing 10.25: Hin- und herspringen mit Bookmarks
10.5 Datensätze bearbeiten Daten lassen sich mit ADO auf verschiedene Art manipulieren. Nachfolgend erfahren Sie, wie Sie Daten mit Aktionsabfragen und mit den Methoden des Recordset-Objekts bearbeiten.
10.5.1 Datensatz anlegen Zum Anlegen eines neuen Datensatzes verwenden Sie AddNew- und die UpdateMethode. Dazwischen stellen Sie die Felder der Datensatzgruppe auf die gewünschten Werte ein.
591
Kapitel 10 Public Sub DatensatzAnlegen() … rst.Open "tblUnternehmen", cnn, adOpenKeyset, adLockOptimistic rst.AddNew rst!Unternehmen = "Pearson Education Deutschland GmbH" rst.Update … End Sub Listing 10.26: Neuen Datensatz anlegen
Sie brauchen im Unterschied zu DAO die Update-Methode nicht auszuführen, wenn Sie den Datensatz wechseln, bevor Sie das Recordset-Objekt schließen. Wenn Sie nach dem Anlegen beispielsweise sofort noch einen weiteren Datensatz anlegen möchten, brauchen Sie die Update-Anweisung nur nach dem Anlegen des zweiten Datensatzes und vor dem Schließen der Datensatzgruppe aufzurufen: rst.AddNew rst!Unternehmen = "Pearson Education Deutschland GmbH" rst.AddNew rst!Unternehmen = "amisoft" rst.Update
Wenn Sie mit AddNew das Anlegen eines neuen Datensatzes starten und das RecordsetObjekt schließen, bevor Sie die Update-Methode ausgeführt haben, lösen Sie einen Lauf zeitfehler aus: rst.AddNew rst!Unternehmen = "Pearson Education Deutschland GmbH" 'Schließen ohne Update löst Laufzeitfehler aus rst.Close
10.5.2 Datensatz bearbeiten Während Sie unter DAO vor dem Bearbeiten eines Datensatzes die Edit-Methode aufrufen mussten, können Sie unter ADO Änderungen am aktuellen Datensatz direkt vornehmen. Die Änderungen übernehmen Sie schließlich mit Update. Public Sub DatensatzAendern() … rst.Open "tblUnternehmen", cnn, adOpenKeyset, adLockOptimistic rst.Find "Unternehmen = 'Pearson Education Deutschland GmbH'" rst!Unternehmen = "Addison Wesley" rst.Update … End Sub Listing 10.27: Datensatz ändern
592
ADO
10.5.3 Datensatz löschen Zum Löschen eines Datensatzes verschieben Sie den Datensatzzeiger auf den zu löschenden Datensatz und entfernen diesen mit der Delete-Methode. Public Sub DatensatzLoeschen() … rst.Open "tblUnternehmen", cnn, adOpenKeyset, adLockOptimistic rst.Find "Unternehmen = 'Pearson Education Deutschland GmbH'" rst.Delete … End Sub Listing 10.28: Löschen eines Datensatzes
10.5.4 Aktionsabfragen ausführen Aktionsabfragen führen Sie unter ADO mit der Execute-Methode des Connection-Objekts aus. Diese Methode erwartet als ersten Parameter den auszuführenden SQL-Ausdruck. Als zweiten Parameter können Sie eine Variable angeben, in der die Anzahl der durch die Aktionsabfrage betroffenen Datensätze gespeichert wird. Diese können Sie nachher weiter verwenden. Public Sub AktionsabfrageAusfuehren() Dim cnn As ADODB.Connection Dim cmd As ADODB.Command Dim lngRecordsAffected Set cnn = CurrentProject.Connection cnn.Execute "INSERT INTO tblUnternehmen(Unternehmen) " _ & "VALUES('Addison-Wesley')", lngRecordsAffected Debug.Print lngRecordsAffected Set cnn = Nothing End Sub Listing 10.29: Ausführen einer Aktionsabfrage
10.6 Transaktionen Transaktionen funktionieren unter ADO prinzipiell wie unter DAO. Der wichtigste Unterschied ist, dass die Bezeichnungen der drei Methoden zum Durchführen von Transaktionen vereinheitlicht wurden. Diese heißen jetzt:
593
Kapitel 10
BeginTrans CommitTrans RollbackTrans Außerdem gehören die Methoden zum Connection-Objekt – unter DAO war es das Workspace-Objekt. Weitere Informationen zu Transaktionen finden Sie in Kapitel 9, Ab schnitt 9.11, »Transaktionen«.
10.7 Besonderheiten von ADO gegenüber DAO ADO bietet einige Besonderheiten gegenüber DAO. So ist es möglich, eine Datensatz gruppe zu speichern, Recordset-Objekte ohne Datenherkunft in Form einer Tabelle oder Abfrage zu verwenden oder Recordsets von der Datenherkunft zu trennen und anschließend wieder zu verbinden.
10.7.1 Datensatzgruppe speichern Sie können eine Datensatzgruppe in einem Microsoft-eigenen Format oder im XMLFormat speichern. Die folgende Routine speichert den Inhalt der Tabelle Personal in der Datei Personal.xml im Verzeichnis der Datenbank. Public Sub DatensatzgruppeSpeichern() … rst.Open "Personal", cnn, adOpenStatic, adLockOptimistic rst.Save CurrentProject.Path & "\Personal.xml", adPersistXML … End Sub Listing 10.30: Speichern einer Datensatzgruppe
10.7.2 Datensatzgruppe laden Um die so gespeicherte Datensatzgruppe wieder verfügbar zu machen, verwenden Sie die Open-Methode des Recordset-Objekts. Allerdings geben Sie statt eines Tabellen- oder Abfragenamens den Namen der Datei an. Public Sub DatensatzgruppeEinlesen() Dim rst As New ADODB.Recordset rst.Open CurrentProject.Path & "\Personal.xml", , adOpenStatic, _ adLockOptimistic, adCmdFile Debug.Print rst.RecordCount
594
ADO Set rst = Nothing End Sub Listing 10.31: Einlesen einer Datensatzgruppe aus einer XML-Datei
10.7.3 Ungebundene Recordsets verwenden Unter ADO lassen sich Recordsets ohne Angabe einer Datenherkunft anlegen und zum Speichern von Daten verwenden – also ohne ein Connection-Objekt. Da ein ungebundenes Recordset keine Datenherkunft hat, besitzt es natürlich auch noch keine Felder. Diese können Sie ganz einfach mit der Append-Methode der Fields-Auflistung des Recordset-Objekts hinzufügen. Anschließend können Sie das Recordset ganz normal verwenden. Ein ungebundenes Recordset ist beispielsweise sehr nützlich, wenn Sie größere Datenmengen in Kombinationsfeldern, Listenfeldern oder sogar Datenblättern anzeigen möchten, diese aber nicht in einer Tabelle gespeichert werden sollen. Die Routine aus dem folgenden Beispiel legt eine Datensatzgruppe mit den beiden Feldern ModulID und Modulname an und fügt alle Module der aktuellen Datenbank hinzu. Anschließend gibt sie alle Einträge der Tabelle im Direktfenster aus. Public Sub UngebundeneDatensatzgruppe() Dim rst As ADODB.Recordset Dim objModul As AccessObject Dim i As Integer Set rst = New ADODB.Recordset rst.Fields.Append "ModulID", adInteger rst.Fields.Append "Modulname", adVarWChar, 255 rst.Open For Each objModul In CurrentProject.AllModules rst.AddNew rst!ModulID = i rst!Modulname = objModul.Name rst.Update Next objModul rst.Update rst.MoveFirst Do While Not rst.EOF Debug.Print rst!ModulID, rst!Modulname rst.MoveNext
595
Kapitel 10 Loop rst.Close Set rst = Nothing End Sub Listing 10.32: Anlegen einer ungebundenen Datensatzgruppe
10.7.4 Disconnected Recordsets Ein Vorteil von ADO gegenüber DAO ist, dass man ADO-Recordsets auf Basis einer Tabelle erstellen und dieses dann »disconnecten« kann – was nichts anderes heißt, als dass man seine Eigenschaft ActiveConnection auf den Wert Nothing setzt. Die Verbindung zur Tabelle ist damit unterbrochen, Änderungen am Recordset werden nicht direkt in die Tabelle übertragen. Wichtig ist bei dieser Technik, dass Sie das Recordset-Objekt so deklarieren, dass es auch nach dem Einlesen der Daten noch verfügbar ist – also nicht innerhalb der Routine, die das Recordset füllt. Am einfachsten erledigen Sie dies mit einer globalen Variablen, die Sie in einem Standardmodul deklarieren: Public rstDisconnected As ADODB.Recordset
Es gibt im Wesentlichen zwei Einsatzgebiete für Disconnected Recordsets: nur lesende sowie schreibende und lesende. Bei den nur lesenden Einsätzen gilt es, oft benötigte und nicht zu ändernde Daten nur einmal einzulesen und immer wieder auf diese kopierten Werte zuzugreifen. Interessant ist dies vor allem, wenn die Daten über das Netzwerk oder gar das Internet beschafft werden müssen. Beispiele für solche Einsatzmöglichkeiten sind Konfigurationsdaten, Texte für Mel dungsfenster oder Fehlermeldungen oder VBA- oder SQL-Ausdrücke wie Funktionen und Abfragen. Der zweite Grund für den Einsatz von Disconnected Recordsets sind Daten, die man vor einer (zwangsweisen) Trennung von der Datenherkunft einliest, bearbeitet und anschließend wieder speichert. Da Verbindungen teuer sind, was die Performance angeht, kann es durchaus Sinn machen, diese bei Bedarf zu trennen, um Ressourcen freizugeben.
Disconnected Recordset zum Lesen öffnen Das erste Beispiel zeigt das Einlesen von Daten, auf die nur lesender Zugriff erfolgt: Public rstDisconnectedRead As ADODB.Recordset
596
ADO Public Sub DisconnectedRecordsetsRead() Set rstDisconnectedRead = New ADODB.Recordset With rstDisconnectedRead .ActiveConnection = CurrentProject.Connection .CursorType = adOpenStatic .CursorLocation = adUseClient .LockType = adLockReadOnly .Source = "SELECT * FROM tblMitarbeiter" .Open , , , , adCmdText If Not rstDisconnectedRead Is Nothing Then Set .ActiveConnection = Nothing End If End With End Sub Listing 10.33: Einlesen von Daten in ein Disconnected Recordset
Aus performancetechnischen Gründen können Sie hier den CursorType auf adOpenStatic und den LockType auf adLockReadOnly einstellen. Der Zugriff auf die im Recordset rstDisconnectedRead enthaltenen Daten erfolgt genau wie zu Beginn dieses Kapitels beschrieben.
Disconnected Recordset einlesen, ändern und zurückschreiben Das zweite Beispiel zeigt, wie Sie ein Recordset öffnen, es von der Datenherkunft trennen, den Inhalt ändern und das Recordset wieder in die ursprüngliche Tabelle zurückschreiben. Mit der folgenden einfachen Routine füllen Sie das Recordset und trennen es von der Datenherkunft: Public Sub DisconnectedRecordsets() Set rstDisconnected = New ADODB.Recordset With rstDisconnected .ActiveConnection = CurrentProject.Connection .CursorType = adOpenDynamic .CursorLocation = adUseClient .LockType = adLockBatchOptimistic .Source = "SELECT * FROM tblMitarbeiter" .Open , , , , adCmdText
597
Kapitel 10 If Not rstDisconnected Is Nothing Then Set .ActiveConnection = Nothing End If End With End Sub Listing 10.34: Füllen und »disconnecten« eines Recordset-Objekts
Das Recordset ist nun »disconnected«; das heißt, dass Sie unabhängig von der Datenher kunft Änderungen darin vornehmen können und diese nicht direkt dort übernommen werden. Eine einfache Änderung nehmen Sie mit folgender Routine vor: Public Sub DisconnectedRecordsetUpdaten() If Not rstDisconnected Is Nothing Then With rstDisconnected .Find "Nachname = 'Minhorst'" If Not .EOF = True Then !Vorname = "Andree" .Update End If End With End If End Sub Listing 10.35: Ändern des Inhalts eines Disconnected Recordsets
Schauen Sie in der zugrunde liegenden Tabelle nach – der im Recordset geänderte Daten satz enthält immer noch denselben Wert wie zuvor. Das ändern Sie nun, indem Sie das Recordset wieder »connecten«. Wichtig ist dabei der Aufruf der Methode UpdateBatch zum Schreiben der Änderungen in die Tabelle – damit speichern Sie alle Änderungen an den enthaltenen Datensätzen: Public Sub DisconnectedRecordsetWiederVerbinden() If Not rstDisconnected Is Nothing Then With rstDisconnected .ActiveConnection = CurrentProject.Connection .UpdateBatch End With End If rstDisconnected.Close Set rstDisconnected = Nothing End Sub Listing 10.36: Verbinden des Disconnected Recordsets mit der zugrunde liegenden Datenbank und Durchführen der Änderungen
598
ADO
10.7.5 Ereignisse von Datensatzgruppen Ein sehr interessantes Feature von ADO sind die Ereignisse der einzelnen Objekte wie Connection- oder Recordset-Objekt (siehe Abbildung 10.6). Diese sind gerade in Zusam menhang mit der Programmierung mehrschichtiger Anwendungen interessant.
Abbildung 10.6: Ereigniseigenschaften von Recordset-Objekten
Sie können die Ereignisse des Connection- und des Recordset-Objekts in einer eigenen Klasse kapseln und entsprechende Prozeduren hinzufügen, die durch die jeweiligen Ereignisse ausgelöst werden. Die komplette Kapselklasse soll hier nicht abgedruckt werden, Sie finden diese aber in der Beispieldatenbank \Kap_10\ADO.accdb im Klassenmodul clsADORS. Die folgende Routine instanziert diese Klasse und löst durch einige Datensatzoperationen verschiedene Ereignisse des Connection- und des Recordset-Objekts aus. Public Sub TestADOEvents() Dim cADO As New clsADORS cADO.SetSQL "SELECT * FROM Personal" DoEvents cADO.MoveRS 2 DoEvents cADO.SetSQL "SELECT * FROM Lieferanten" DoEvents cADO.MoveRS 7 DoEvents Set cADO = Nothing End Sub Listing 10.37: Instanzieren und Verwenden einer Klasse, die Connection- und RecordsetEreignisse auslöst
599
Kapitel 10
Dies jedoch nur als Hinweis darauf, dass ADO durchaus Eigenschaften besitzt, die es von DAO positiv abheben. Wenn es Sie interessiert, welche Möglichkeiten die Ereigniseigenschaften der ADO-Ob jekte bieten, schauen Sie sich die Beispieldatenbank \Kap_10\ADOEvents.accdb auf der Buch-CD an. Dort finden Sie ein an ein ADO-Recordset gebundenes Formular, das die bei der Arbeit mit dem Recordset im Formular zusätzlich verfügbaren Ereignisse des Connection- und des Recordset-Objekts erkennen lässt.
600
11 Bilder und binäre Dateien Probleme mit dem Speichern von Dateien und insbesondere von Bildern in Dateien gehören in den Newsgroups und Foren zum täglichen Leben. Access bietet mit dem Datentyp OLE-Objekt die Möglichkeit, auch Dateien in einer Tabelle zu speichern. Leider gab es bisher in Access keine eingebaute einfache Funktion, um Dateien ohne Weiteres in einem solchen Feld abzulegen – zumindest keine, die nicht früher oder später die Größe der Datenbankdatei explodieren ließ. Doch mit Access 2007 kommt die Wende: Microsoft hat die Access-Gemeinde erhört und nicht nur einen neuen Felddatentyp namens Anlage, sondern auch noch ein neues Steuerelement zum Anzeigen von Bildern in Formularen und Berichten spendiert. Und dabei können Sie im Anlage-Feld sogar Dateien speichern, und sogar mehrere pro Datensatz. In diesem Kapitel lernen Sie aber nicht nur, wie das neue Anlage-Feld und das passende Steuerelement funktionieren, sondern auch alternative Techniken kennen. Außer dem finden Sie einige VBA-Module, die wichtige Tech niken für den Umgang mit Bildern und binären Daten liefern. Die Module stammen aus der Feder von Sascha Trowitzsch, der auch bei der Erstellung der Beispiele maß geblich beteiligt war. Die Beispiele in diesem Kapitel drehen sich ausschließlich um Bilder, weil dies wohl der primäre Einsatzbereich für das Speichern von Dateien und binären Daten in Tabellen ist. Tatsächlich können Sie die Techniken, die sich nicht explizit auf Bilder beziehen, auch für das Speichern und Wiederherstellen von normalen Dateien verwen-
Kapitel 11
den. Ein Einsatzzweck abseits der bunten Welt der Bilder ist etwa das Speichern von Dateien, die Sie für die Ausführung einer Datenbankanwendung benötigen, und das Wiederherstellen solcher Dateien zur Laufzeit. So können Sie zum Beispiel für den Ablauf notwendige DLL-Dateien mitliefern, ohne dass der Benutzer sich mit zusätzlichen Dateien herumschlagen muss. Beispieldatenbank: Die Beispiele dieses Kapitels finden Sie auf der Buch-CD unter \Kap_11\BilderUndBinaereDaten.accdb.
11.1 Bilder und Dateien als Anlage speichern Die einfachste und naheliegendste Möglichkeit zum Speichern von Bildern bietet der neue Datentyp Anlage. Abbildung 11.1 zeigt eine Tabelle mit einem Anlage-Feld in der Entwurfsansicht.
Abbildung 11.1: Eine Tabelle mit einem Anlage-Feld in der Entwurfsansicht
In Aktion sieht dieser neue Felddatentyp wie in Abbildung 11.2 aus. Statt eines Feldna mens zeigt die Tabelle ein Büroklammer-Symbol an, das Sie aber durch die Angabe eines Wertes für die Eigenschaft Bezeichnung überschreiben können. Im Feld selbst sehen Sie das gleiche Symbol, gefolgt von einer in Klammern eingefassten Zahl. Diese gibt die Anzahl der enthaltenen Anhänge an. Klicken Sie doppelt auf das Feld, erscheint der Dialog Anlagen, mit dem Sie Folgendes erledigen können: Anlagen hinzufügen
602
Bilder und binäre Dateien
Anlagen entfernen Eine Anlage öffnen Eine Anlage aus dem Anlage-Feld auf der Festplatte speichern Alle Anlagen auf der Festplatte speichern
Abbildung 11.2: Das Anlage-Feld in Aktion
Diese Aktionen sind sämtlich selbsterklärend, weshalb das Buch an dieser Stelle nicht weiter darauf eingeht. Als Hinweis nur dies: Als Anlage können Sie beliebige Dateien aufnehmen, bis auf jene, die aus Sicherheitsgründen verboten sind und von Access blo ckiert werden. Dabei ist ausschließlich die Dateiendung von Belang. Welche Endungen das sind, erfahren Sie in der Onlinehilfe von Access unter Anlagen|Anfügen von Da teien|Referenzinformationen zu Anlage|Blockierte Dateiformate. Wie Sie später erfahren werden, lässt sich diese Limitierung mit VBA-Tricks umgehen (siehe Abschnitt 11.5.1, »Importieren von Dateien in Anlage-Felder«)
Eigenschaften des Anlage-Steuerelements Das neue Anlage-Steuerelement finden Sie in der Entwurfsansicht von Formularen und Berichten im Ribbon Entwurf|Steuerelemente|Anlage (Büroklammer). Neben den üblichen Eigenschaften, die auch andere Steuerelemente besitzen, sind die folgenden erwähnenswert: Standardbild (VBA: DefaultPicture): Laden Sie über diese Eigenschaft eine Bilddatei, die das Steuerelement standardmäßig anzeigen soll. Die Eigenschaft wirkt sich immer dann aus, wenn das Anlage-Feld des angezeigten Datensatzes leer ist – also auch beim Anlegen eines neuen Datensatzes.
603
Kapitel 11
Hintergrundart (BackStyle): Kann die Werte Normal (1) oder Transparent (0) annehmen. Wenn Transparenz eingestellt ist, scheint der Formularhintergund durch transparente Bereiche von Bildern (32bit) durch. Ereignis On Attachment Current (AttachmentCurrent): Das Ereignis wird ausgelöst, wenn per Kontextmenü oder mit dem beim Klicken des Bildes erscheinenden Navi gationsbar auf eine andere Anlage des Datensatzes geschaltet wird. Nützlich, weil damit etwa der Dateiname des aktuell angezeigten Bildes wie im folgenden Listing angezeigt werden kann: Private Sub Bildanlage_AttachmentCurrent() Me!LBLFilename.Caption = Me!Bildanlage.FileName End Sub
Die folgenden Eigenschaften stehen nur unter VBA zur Verfügung: FileName: Dateiname der aktuell angezeigten Anlage FileData: Binärdaten der aktuell angezeigten Anlage (Byte-Array) AttachmentCount: Anzahl der im Datensatz gespeicherten Anlagen CurrentAttachment: Index der aktuell angezeigten Anlage PictureDisp (versteckte schreibgeschützte Eigenschaft): Gibt bei Bildanhängen das Picture-Objekt des aktuell angezeigten Bildes zurück. Dieses können Sie dann zur weiteren Verarbeitung in VBA verwenden. Back, Forward: Mit diesen Methoden schalten Sie per VBA zwischen den einzelnen Anhängen eines Datensatzes um. Unter Zuhilfenahme der Eigenschaften Attachment Count und CurrentAttachment können Sie diese Methoden dazu verwenden, um über selbst gebaute Navigationsschaltflächen zwischen den Bildern eines Datensatzes zu navigieren. SizeToFit: Die Methode weist das Anlage-Feld an, sich den Ausmaßen des angezeigten Bildes anzupassen. Stellen Sie die Eigenschaft AutoHeight des Detailbereichs außerdem auf True, dann passt Access auch die Formulargröße dem Bild an.
11.2 Bilder aus Anlage-Feldern in Formularen anzeigen In Formularen zeigt sich das neue Anlage-Steuerelement genauso benutzerfreundlich wie in der Datenblattansicht von Tabellen. Sie brauchen für ein jungfräuliches Formular nur eine Tabelle mit einem Anlage-Feld als Datensatzquelle auszuwählen und das Feld Bildanlage dieser Tabelle in den Formularentwurf ziehen (siehe Abbildung 11.3).
604
Bilder und binäre Dateien
Mit diesem Entwurf erhalten Sie beispielsweise die Formularansicht aus Abbildung 11.4.
Abbildung 11.3: Hinzufügen des Primärschlüssels und des Anlage-Feldes zu einem Formular
Abbildung 11.4: Über das Kontextmenü des Anlage-Feldes können Sie den Anlagen-Dialog öffnen oder, wenn mehrere Anlagen vorhanden sind, darin blättern
Es geht aber auch anders als im ersten Beispiel: Vielleicht möchten Sie einmal direkt durch alle im Anlage-Feld verschiedener Datensätze enthaltenen Bilder blättern. Dann ziehen Sie nicht das komplette Anlage-Feld, sondern die in der dahinter verborgenen Tabelle enthaltenen Felder aus der Feldliste in den Formularentwurf (siehe Abbildung 11.5). Das Ergebnis sieht ähnlich aus (siehe Abbildung 11.6), unterscheidet sich aber in zwei wesentlichen Punkten:
605
Kapitel 11
Sie können direkt im Formular durch alle Bilder blättern, egal, ob mehrere Bilder im Anlage-Feld eines einzigen Datensatzes verborgen sind. Sie können so nicht auf den Anlage-Dialog zugreifen.
Abbildung 11.5: Entwurf eines Formulars mit Anlage-Feld
Abbildung 11.6: Ein Bild im Anlage-Steuerelement in einem Formular
606
Bilder und binäre Dateien
Sie müssen beachten, dass Access einen Datensatz mit mehreren Bildern hier wie zwei verknüpfte Tabellen behandelt: Eine Tabelle mit den Basisdaten (also den Daten aus den Feldern, die nicht die Anlage enthalten) und eine Tabelle mit den im Anlage-Feld enthaltenen Dateien. Diese sind über eine 1:n-Beziehung miteinander verknüpft. Wenn die oben verwendete Tabelle tblBildanlagen also im ersten Datensatz zwei Bildanlagen enthält, zeigt das Formular zwei Datensätze mit den gleichen Basisdaten, aber unterschiedlichen Anlagen beziehungsweise Anlage-Informationen an.
11.3 Bilder aus Anlage-Feldern in Berichten anzeigen In Berichten funktioniert dies genauso einfach: Sie stellen die passende Tabelle als Datensatzquelle des Berichts ein und fügen das Anlage-Feld selbst zum Bericht hinzu. Das Ergebnis überzeugt nicht – zumindest nicht in der Seitenansicht: Der Bericht zeigt nur das jeweils erste Bild zu jedem Datensatz an (siehe Abbildung 11.7).
Abbildung 11.7: Berichte zeigen nur je ein Bild eines Anlage-Feldes pro Datensatz an, auch wenn dieses mehrere Bilder enthält
Wenn Sie alle Bilder der enthaltenen Datensätze in der Seitenansicht des Berichts ausgeben möchten, fügen Sie wiederum das dem eigentlichen Anlage-Feld untergeordnete .FileData-Feld zum Berichtsentwurf hinzu. Auch hier zeigt
607
Kapitel 11
Access die Grunddaten von Datensätzen, die mehr als ein Bild enthalten, jeweils doppelt an (siehe Abbildung 11.8). Dem können Sie entgegenwirken, indem Sie die Eigenschaft Duplikate ausblenden für solche Steuerelemente, die je Datensatz nur einfach angezeigt werden sollen, auf Ja einstellen und – falls diese Steuerelemente sich allein in einer Zeile befinden – mit dem Wert Ja für die Eigenschaft Verkleinerbar dafür sorgen, dass keine unschönen Lücken entstehen.
Abbildung 11.8: Alle Bilder eines Anlage-Felds in der Berichtsansicht eines Berichts
Komfortabler geht dies mit einer Gruppierung nach dem Primärschlüssel der Tabelle mit dem Anlage-Feld. Das kann im Entwurf etwa so wie in Abbildung 11.9 aussehen. Dass die Berichtsansicht so wie in Abbildung 11.10 aussieht, ist ein weiteres Indiz dafür, dass Access die Anlagen intern in einer verborgenen Tabelle speichert.
11.4 Bilder und Dateien aus Anlage-Feldern auf der Festplatte speichern Das Speichern von Anlagen aus dem Anlage-Feld ist äußerst trivial: Sie öffnen einfach den Anlagen-Dialog per Doppelklick auf das jeweilige Feld im Formular und führen dort die erforderlichen Schritte aus. Das funktioniert aber leider nur, wenn Sie das AnlageFeld wie in der ersten in Abschnitt 11.2, »Bilder aus Anlage-Feldern in Formularen anzeigen«, beschriebenen Methode anlegen.
608
Bilder und binäre Dateien
Abbildung 11.9: Entwurf eines nach dem Primärschlüssel der Datensatzquelle gruppierten Berichts mit Bildanlagen, ...
Abbildung 11.10: ... der in der Entwurfsansicht so aussieht
11.5 Dateien per VBA in Anlage-Felder importieren und exportieren Natürlich können Sie Bilder und Dateien auch per VBA in einem Anlage-Feld speichern und von dort aus wieder ins Dateisystem exportieren.
609
Kapitel 11
11.5.1 Importieren von Dateien in Anlage-Felder Die nachfolgend vorgestellte Funktion können Sie universell für das Importieren beliebiger Dateien in Anlage-Felder verwenden. Sie erwartet die folgenden Parameter: strFileName: Verzeichnis und Name der zu importierenden Datei strTable: Name der Tabelle, in der sich das Anlage-Feld befindet strFieldAttach: Name des Anlage-Feldes boolEdit: Gibt an, ob die Anlage in einem bestehenden Datensatz gespeichert werden soll strIDField: Name des Primärschlüsselfeldes der Tabelle varID: Wert des Primärschlüsselfeldes für den Datensatz, zu dem eine Anlage hinzugefügt werden soll strAttachment: Name des Attachments, das gegebenenfalls überschrieben werden soll Wenn Sie eine Datei in einem neuen Datensatz speichern möchten, verwenden Sie etwa diesen Aufruf: StoreBLOB2007 CurrentProject.Path & "\Bilder_01.tif", "tblAnlagen", "Anlagen"
Zum Anfügen einer Datei in einem vorhandenen Datensatz setzen Sie diese Anweisung ab – wobei 2 der Primärschlüsselwert des Zieldatensatzes ist: StoreBLOB2007 CurrentProject.Path & "\Bilder_01.tif", "tblAnlagen", "Anlagen", True, "AnlageID", 2
Schließlich können Sie auch ein Attachment mit einem bestimmten Dateinamen in der Tabelle überschreiben. Auch dies erfordert die Angabe des Datensatzes und zusätzlich den Namen des bereits vorhandenen Attachments: StoreBLOB2007 CurrentProject.Path & "\Bilder_02.tif", "tblAnlagen", "Anlagen", True, "AnlageID", 2, "Bilder_01.tif"
Die Routine hat übrigens noch ein weiteres Feature, das eine völlig unverständliche Eigenschaft von Anlage-Feldern ausschaltet: Und zwar dürfen nur Dateien mit bestimmten Dateiendungen importiert werden. Und das ist wörtlich zu nehmen: Der Inhalt der Datei spielt keine Rolle, Sie brauchen die Datei nur vor dem Speichern im Anlage-Feld umzubenennen und dies anschließend wieder rückgängig zu machen. Und genau das macht diese Funktion auch, indem sie den beim Speichern einer Datei mit einer nicht erlaubten Dateiendung auftretenden Fehler entsprechend behandelt.
610
Bilder und binäre Dateien
Der Zugriff auf die Anlagen erfolgt mit DAO. Der grundlegende Ablauf ist, dass Sie zunächst ein Recordset-Objekt auf Basis der Tabelle mit dem Anlage-Feld erstellen und ein weiteres auf Basis der Value-Eigenschaft des Anlage-Feldes. Letzteres muss unbedingt ein Recordset2-Objekt sein, ebenso wie das Field-Objekt mit dem Anlage-Feld die neue Version Field2 braucht. Die Prozedur sieht wie folgt aus: Function StoreBLOB2007(strFileName As String, strTable As String, _ strFieldAttach As String, Optional boolEdit As Boolean, _ Optional strIDField As String, Optional varID As Variant, _ Optional strAttachment As String) As Boolean Dim fld2 As DAO.Field2 Dim rstDAO As DAO.Recordset2 Dim rstACCDB As DAO.Recordset2 On Error GoTo ErrHandler Set rstDAO = CurrentDb.OpenRecordset("SELECT * FROM [" & strTable _ & "]", dbOpenDynaset) If boolEdit Then If IsNull(varID) Then Err.Raise vbObjectError + 1, , _ "Keine Datensatz-ID angegeben!" rstDAO.FindFirst "CStr([" & strIDField & "])='" & CStr(varID) & "'" If rstDAO.NoMatch Then Err.Raise vbObjectError + 2, , _ "Datensatz mit ID " & varID & " nicht gefunden!" rstDAO.Edit Else rstDAO.AddNew End If Set rstACCDB = rstDAO(strFieldAttach).value If boolEdit Then If rstACCDB.EOF Then 'Fall 1: Es gibt noch keine Anlagen; > neue Anlage rstACCDB.AddNew Else rstACCDB.FindFirst "[FileName]=' & strAttachment" & "'" 'Fall2: Es gibt keine Anlage mit dem Namen in sAttachment: > 'neue Anlage If rstACCDB.NoMatch Then rstACCDB.AddNew 'Fall3: Anlage gefunden; dann editieren Else rstACCDB.Edit End If End If
611
Kapitel 11 Else rstACCDB.AddNew End If Set fld2 = rstACCDB.Fields!FileData On Error Resume Next fld2.LoadFromFile (strFileName) If Err.Number = -2146697202 Then 'Unerlaubte Dateiendung! Spezialbehandlung... On Error GoTo ErrHandler 'Datei zuerst mit erlaubte Endung ".dat" anfügen Name strFileName As strFileName & ".dat" 'Datei laden fld2.LoadFromFile (strFileName & ".dat") 'Umbenennung rückgängig machen Name strFileName & ".dat" As strFileName rstACCDB.Fields!FileName = Mid(strFileName, _ InStrRev(strFileName, "\") + 1) 'Anlagename setzen rstACCDB.Update Else On Error GoTo ErrHandler rstACCDB.Update End If rstDAO.Update StoreBLOB2007 = True 'Rückgabe True = Alles ok. Finally: On Error Resume Next rstACCDB.Close rstDAO.Close Set rstACCDB = Nothing Set rstDAO = Nothing Set fld2 = Nothing Exit Function ErrHandler: MsgBox Err.Description, vbCritical Resume Finally End Function Listing 11.1: Flexibles Speichern von Dateien im Anlage-Feld
11.5.2 Exportieren von Dateien aus dem Anlage-Feld Für den umgekehrten Weg gibt es ebenfalls eine passende und universell einsetzbare Funktion. Der Grund, warum die Datei wesentlich kompakter ist als die zum Importieren von Dateien, ist folgender: Die Routine greift direkt mit einer geeigneten SQL-Abfrage auf die in der Untertabelle des Anlage-Feldes enthaltenen Felder zu. Die Parameter sind weit
612
Bilder und binäre Dateien
gehend mit denen der vorherigen Funktion identisch. Auch der Export mit der Methode SaveToFile prüft die Dateiendung, weshalb auch hier ein Workaround eingebaut ist. Function RestoreBLOB2007(strTable As String, strFieldAttach As String, _ strIDField As String, varID As Variant, strFileName As String, _ Optional strAttachment As String = "*") As Boolean Dim rstACCDB As DAO.Recordset2 On Error GoTo ErrHandler Set rstACCDB = CurrentDb.OpenRecordset("SELECT [" & strFieldAttach _ & "].FileData FROM " & strTable & " WHERE [" & strIDField & "]=" _ & varID & " AND [" & strFieldAttach & "].FileName LIKE '" _ & strAttachment & "'", dbOpenSnapshot) If rstACCDB.EOF Then Err.Raise vbObjectError + 3, "RestoreBLOB2007", _ "Das Anlage-Feld ist leer" End If If Dir(strFileName) <> "" Then Kill strFileName DoEvents End If 'Fehlerbehandlung ausschalten, da nachfolgende Zeile 'gegebenenfalls Fehler bei blockierten Dateiendungen erzeugt On Error Resume Next rstACCDB(0).SaveToFile strFileName If Err.Number = (-2146697202) Then 'Spezialbehandlung: Datei wird mit Endung .dat versehen, 'was erlaubte Endung ist. Anschließend wird wiederhergestellte _ 'Datei wieder korrekt umbenannt rstACCDB(0).SaveToFile strFileName & ".dat" DoEvents Name strFileName & ".dat" As strFileName End If RestoreBLOB2007 = True Finally: On Error Resume Next Set rstACCDB = Nothing rstDAO.Close Set rstDAO = Nothing Exit Function ErrHandler: MsgBox Err.Number & "/" & Err.Description, vbCritical Resume Finally End Function Listing 11.2: Speichern von Anlagen im Dateisystem
613
Kapitel 11
11.6 Bilder und Dateien im OLE-Feld einbetten oder verknüpfen Dieses Thema ist andernorts so oft besprochen und beschrieben worden, dass hier nur kurz auf die Vor- und Nachteile eingegangen werden soll. Damit keine Verwechslungen mit der nachfolgend beschriebenen Vorgehensweise zum Speichern von Bildern und Dateien im binären Format in der Datenbank auftreten: Mit »Einbetten« und »Ver knüpfen« sind die Methoden der Benutzeroberfläche – etwa von gebundenen Objekt feldern – gemeint, um externe Dateien in einem OLE-Feld zu speichern oder damit zu verknüpfen. Der Einsatz des OLE-Feldes zum Einbetten oder Verknüpfen etwa von Office-Dateien macht beispielsweise Sinn, wenn Sie diese Dateien nur in der Datenbank speichern und gegebenenfalls mit der passenden Anwendung bearbeiten möchten.
Nachteile beim Speichern von Bildern in OLE-Feldern Einmal in ein OLE-Feld eingebettete Dateien lassen sich dort nicht ohne Weiteres wieder herausholen. Es gibt zwar verschiedene Verfahren, aber der Aufwand ist relativ hoch. Sie könnten das Objekt etwa per Doppelklick in der dafür vorgesehenen Anwendung öffnen und dann speichern, aber wenn Sie diesen Vorgang mehrere hundert oder gar tausend Mal durchführen müssen, um beispielsweise Ihre Urlaubsbildersammlung auf CD zu brennen, werden Sie wünschen, die Dateien niemals in der Datenbank untergebracht zu haben. Außerdem kann es hier weitere Probleme geben: Dateien, die über die Objekt einfügenFunktion von Access in ein Tabellenfeld eingefügt wurden, benötigen einen OLE-Server, um bearbeitet oder wieder in eine Datei umgewandelt werden zu können. Das ist genau die Anwendung, die auf dem aktuellen Windows-System für die jeweilige Dateiendung zuständig ist. Wenn keine für diese Dateiendung passende Anwendung eingetragen ist, wird die Datei als »Paket« in dem Feld gespeichert. In diesem Fall ist der »ObjektManager« von Windows der für das Paket zuständige OLE-Server und damit auch für die Wiederherstellung der Datei verantwortlich. Das ist der günstigere Fall, denn falls die Datei auf dem Ausgangssystem mit einem konkreten OLE-Server verknüpft ist, muss die entsprechende Anwendung auch auf anderen Rechnern vorhanden sein, um die Datei öffnen oder speichern zu können.
Bilder lassen die Datenbank wachsen Wenn es sich bei den zu speichernden Dateien um Bilder handelt, gibt es noch einen weiteren Nachteil: Bilddateien werden datenbankintern in einem speziellen bitmapähnlichen Format gespeichert, das wesentlich mehr Speicherplatz frisst als etwa das gängige für Fotos verwendete Format .jpg. Die Datenbank und die darin enthaltenen
614
Bilder und binäre Dateien
Bilder nehmen dann einen um ein Vielfaches größeren Platz auf der Festplatte ein als die Bilder im Ursprungsformat.
11.7 Bilder und Dateien als Binärstrom im OLE-Feld speichern In den folgenden Abschnitten lernen Sie einige Funktionen kennen, mit denen Sie Dateien in ein Feld einer Access-Datenbank importieren und die Datei wieder im Dateisystem speichern können.
Importieren einer Datei in ein OLE-Feld einer Tabelle Die Funktion SaveFileToOLEField importiert eine Datei in ein OLE-Feld einer Tabelle. Die Funktion erwartet folgende Parameter: strFilename: Dateiname der zu importierenden Datei strTable: Name der Zieltabelle strOLEField: Name des Zielfelds boolEdit: Angabe, ob die Datei in das OLE-Feld eines bestehenden Datensatzes eingefügt werden soll strIDField: Primärschlüsselfeld der Zieltabelle lngID: Primärschlüsselwert des Zieldatensatzes Die Routine erzeugt zunächst einen SQL-Ausdruck, der entweder den kompletten oder einen eingeschränkten Recordset auf die Zieltabelle zurückgibt – das hängt davon ab, ob boolEdit wahr ist und die Datei somit in das OLE-Feld eines bestimmten Datensatzes eingefügt werden soll. Nach einer Prüfung, ob die angegebene Datei vorhanden ist, aktiviert die Funktion entweder den aktuellen Datensatz zum Bearbeiten oder legt einen neuen Datensatz an. Schließlich erfolgt das Schreiben der Datei in Form eines Binärstroms in das Tabellenfeld – siehe auch die Kommentare im Listing. Public Function SaveFileToOLEField(strFilename As String, strTable As String, strOLEField As String, Optional boolEdit As Boolean, Optional strIDField As String, Optional lngID As Long) As Long Dim Dim Dim Dim Dim
db As DAO.Database rst As DAO.Recordset lngFileID As Long Buffer() As Byte lngFileLen As Long
615
Kapitel 11 Dim strSQL As String On Error GoTo SaveFileToOLEField_Err Set db = CurrentDb strSQL = "SELECT " & strOLEField & " FROM " & strTable If boolEdit = True Then strSQL = strSQL & " WHERE " & strIDField & " = " & lngID End If 'Datensatzgruppe mit dem Zielfeld für die Datei öffnen Set rst = db.OpenRecordset(strSQL, dbOpenDynaset) 'Meldung, falls Datei nicht vorhanden ist If Dir(strFilename) = "" Then MsgBox "Die Datei '" & strFilename & "' existiert nicht." Exit Function End If 'Bearbeitung des Datensatzes beginnen If boolEdit = True Then rst.Edit Else rst.AddNew End If 'Funktion zum Füllen des Feldes aufrufen On Error GoTo SaveFileToOLEField_Err 'Dateinummer für die Dateioperationen festlegen lngFileID = FreeFile 'Zu importierende Datei für den binären Zugriff öffnen Open strFilename For Binary Access Read Lock Read Write As lngFileID 'Dateigröße ermitteln lngFileLen = LOF(lngFileID) 'Größe der Variablen für den Dateiinhalt anpassen ReDim Buffer(lngFileLen) 'Zielfeld leeren rst(strOLEField) = Null 'Inhalt der Datei in Variable "Buffer" schreiben Get lngFileID, , Buffer 'Inhalt der Variablen in das Zielfeld schreiben rst(strOLEField).AppendChunk Buffer 'Recordset aktualisieren rst.Update SaveFileToOLEField = True SaveFileToOLEField_Exit: On Error Resume Next Close lngFileID rst.Close
616
Bilder und binäre Dateien Set rst = Nothing Set db = Nothing Exit Function SaveFileToOLEField_Err: SaveFileToOLEField = Err.Number Resume SaveFileToOLEField_Exit End Function Listing 11.3: Funktion zum Importieren einer Datei in ein OLE-Feld einer Tabelle
Das Speichern einer Datei in einen neuen Datensatz erfolgt beispielsweise mit dem folgenden Aufruf: SaveFileToOLEField CurrentProject.Path & "\bilder_01.tif", "tblOLE", "OLEFeld"
Wenn Sie die Datei in einem bestimmten Datensatz speichern wollten, müssen Sie auch noch die letzten drei Parameter angeben: SaveFileToOLEField CurrentProject.Path & "\bilder_02.tif", "tblOLE", "OLEFeld", True, "OLEFeldID", 1
Ob das Speichern funktioniert, können Sie zuverlässig nur mit der nachfolgend vorgestellten Funktion feststellen. Die soll nämlich die Datei aus dem OLE-Feld wiederherstellen.
11.8 Bilder und Dateien im binären Format aus einem OLE-Feld wiederherstellen Die Funktion SaveOLEFieldToFile stellt die in der Datenbank gespeicherten Dateien wieder her. Sie ist sehr ähnlich wie die oben beschriebene Funktion aufgebaut und tauscht im Wesentlichen Quelle und Ziel der Datei gegeneinander aus. Für weitere Informationen sei daher auf die Kommentare im Quelltext des folgenden Listings verwiesen. Public Function SaveOLEFieldToFile(strTable As String, _ strIDField As String, lngID As Long, strOLEField As String, _ strFilename As String) As Boolean Dim db As DAO.Database Dim rst As DAO.Recordset Dim lngFileID As Long Dim Buffer() As Byte Dim lngFileLen As Long On Error GoTo SaveOLEFieldToFile_Err Set db = CurrentDb
617
Kapitel 11 'Datensatz mit der angegebenen ID öffnen Set rst = db.OpenRecordset("SELECT " & strOLEField & " FROM " _ & strTable & " WHERE " & strIDField & " = " & lngID, dbOpenDynaset) On Error GoTo SaveOLEFieldToFile_Err 'Dateinummer für den Zugriff auf die zu erzeugende Datei ermitteln lngFileID = FreeFile 'Dateigröße ermitteln lngFileLen = Nz(LenB(rst(strOLEField)), 0) 'Wenn die Dateigröße größer 0 ist: If lngFileLen > 0 Then 'Größe der Variable "Buffer" anpassen ReDim Buffer(lngFileLen) 'Datei zum Schreiben öffnen Open strFilename For Binary Access Write As lngFileID 'Feldinhalt in die Variable "Buffer" schreiben Buffer = rst(strOLEField).GetChunk(0, lngFileLen) 'Inhalt der Variablen "Buffer" in die Datei schreiben Put lngFileID, , Buffer End If SaveOLEFieldToFile = True SaveOLEFieldToFile_Exit: On Error Resume Next Close lngFileID rst.Close Set rst = Nothing Set db = Nothing Exit Function SaveOLEFieldToFile_Err: SaveOLEFieldToFile = False Resume SaveOLEFieldToFile_Exit End Function Listing 11.4: Die Funktion SaveOLEFieldToFile speichert den Inhalt des angegebenen OLE-Felds in einer Datei
Beispiele für das Speichern einer als OLE-Objekt vorliegenden Datei Der nachfolgende Aufruf setzt voraus, dass die Tabelle einen Datensatz enthält, dessen OLE-Feld eine Datei im binären Format beinhaltet. Er speichert den Inhalt des Feldes OLEFeld des Datensatzes mit dem Primärschlüsselwert 1 der Tabelle tblOLE in der Datei ExportOLE.tif im aktuellen Datenbankverzeichnis. SaveOLEFieldToFile "tblOlE", "OLEFeldID", 1, "OLEFeld", currentproject.Path & "\ExportOLE.tif"
Beachten Sie, dass die Prozedur vorhandene Dateien ohne Rückfrage überschreibt.
618
Bilder und binäre Dateien
11.9 Bilder von der Festplatte in Formularen und Berichten anzeigen Je nachdem, ob Sie die gewünschten Dateien einfach nur in Formularen oder Berichten der Datenbank anzeigen, aber nicht in der Datenbank speichern möchten, können Sie sich mit sehr einfachen Mitteln behelfen. Speichern Sie die Dateien einfach gar nicht erst in der Datenbank, sondern belassen Sie diese im Dateisystem. Statt der Datei speichern Sie lediglich den Pfad zu der gewünschten Datei und verwenden die Pfadangabe, um eine Datei in einem entsprechenden Steuerelement zu öffnen. Praktisch ist es dabei, wenn man die Dateien im Datenbankverzeichnis oder in einem darunter liegenden Verzeichnis speichert. Auf diese Weise können Sie die Daten zusammen weitergeben und gleichzeitig sicherstellen, dass die Datenbank die Dateien am gleichen Ort (relativ zum Datenbankverzeichnis) wie beim Anlegen der Datei findet. Ab Access 2000 liefert die Funktion CurrentProject.Path den gewünschten Pfad zurück.
Wohin mit dem Dateipfad? Wenn Sie nur den Dateipfad in der Datenbank speichern, stehen Sie vor der Frage, wie Sie die enthaltenen Informationen aufteilen. Es gibt verschiedene Varianten: Sie speichern den kompletten Pfad inklusive Dateiname in einem Feld. Nachteil: Änderungen am Verzeichnis erfordern immer den Einsatz von ZeichenkettenFunktionen. Sie speichern Verzeichnis und Dateiname in zwei Feldern. Sie legen zwei eigene Tabellen für Verzeichnisse und Dateinamen an und verknüpfen diese per 1:n-Beziehung. Vorteil: Änderungen an Verzeichnissen erfolgen auf einen Schlag. Nachteil: Änderungen an einzelnen Verzeichnissen bereiten mehr Aufwand.
11.9.1 Anzeigen externer Bilddateien im Formular Da sich Bilddateien nicht nur für das Speichern in einer Datenbank, sondern besonders zur Anzeige in Formularen und Berichten eignen, beschäftigt sich das folgende Beispiel mit der Anzeige von extern gespeicherten Bilddateien. Die Tabelle aus Abbildung 11.11 enthält neben dem Primärschlüsselfeld BildID nur ein Feld für eine Bezeichnung des Bildes und eines für den Dateinamen. In das Feld Dateiname tragen Sie am besten den kompletten Pfad inklusive Dateiname ein oder teilen diese beiden Informationen auf zwei getrennte Felder auf. Zum Anzeigen der Bilder verwenden Sie ein Formular mit der Tabelle tblVerknuepfteBilder als Datensatzquelle. Das Formular soll alle Felder der Tabelle enthalten und – falls vorhanden – das angegebene Bild anzeigen. Dazu fügen Sie zusätzlich ein Bildsteuerelement hinzu.
619
Kapitel 11
Abbildung 11.11: Tabelle zum Speichern von Bildverknüpfungen
Dabei müssen Sie direkt die anzuzeigende Datei angeben – wählen Sie dazu irgendein Dummybild aus. Anschließend lassen Sie dieses wieder verschwinden, indem Sie in der Entwurfsansicht des Formulars die Eigenschaft Bild des Bildsteuerelements leeren. Stellen Sie außerdem den Namen des Bildsteuerelements auf imgBild ein (siehe Abbil dung 11.12).
Abbildung 11.12: Formular mit ungebundenem Bildsteuerelement
620
Bilder und binäre Dateien
Nun müssen Sie noch dafür sorgen, dass das Bildsteuerelement die im Feld Dateiname angegebene Bilddatei anzeigt. Und jetzt kommt mal wieder ein sehr nettes neues Fea ture von Access 2007 zum Zuge: Das Bildsteuerelement besitzt nämlich nunmehr die Eigenschaft Steuerelementinhalt, für die Sie direkt das Feld mit dem kompletten Pfad angeben können. Das Bildsteuerelement zeigt dann automatisch das angegebene Bild an. Sie brauchen nur noch ein wenig Code, um einen Dialog zum Auswählen der Bilddatei anzuzeigen: Private Sub cmdDateiOeffnen_Click() Me!Dateiname = WZHOpenFileName(CurrentProject.Path) End Sub Listing 11.5: Aufruf der Prozedur BildAktualisieren durch verschiedene Ereignisprozeduren
Das Bildsteuerelement aktualisiert die angegebene Datei direkt nach dem Ändern des im Feld Dateiname angegebenen Wertes. Im Unterschied zu früheren Versionen von Access sind nun übrigens nicht mehr die Office-Grafikfilter für das Laden der Bilder verantwortlich, sondern das in Access integrierte GDIPlus. Damit entfällt die lästige bisherige Fortschrittsanzeige beim Laden, die Access manchmal zum Absturz brachte. Sie müssen sich nun auch keine Sorgen mehr machen, dass auf einem anderen Rechner die erforderlichen Grafikfilter nicht installiert sein könnten. Wie aber gehen Sie vor, wenn Pfad und Dateiname auf mehrere Felder aufgeteilt sind oder der Benutzer sogar davon ausgeht, dass sich alle Bilddateien im gleichen Verzeichnis wie die Datenbank befinden und den Pfad bei Bedarf mit CurrentProject. Path hinzufügt? Nun – Sie brauchen einfach nur eine Abfrage, die alle Felder der dem Formular zugrunde liegenden Tabelle enthält und die Informationen zusammensetzt beziehungsweise ergänzt. Das Feld zum Zusammenstellen des vollständigen Pfads aus zwei einzelnen Feldern wäre dann etwa so aufgebaut (vorausgesetzt, der Pfad enthält kein abschließendes Backslash-Zeichen): KompletterPfad: Pfad & "\" & Dateiname
Und wenn nur der Dateiname vorhanden ist und dieser um den aktuellen Pfad der Datenbank ergänzt werden soll, sieht das Feld so aus: KompletterPfad: CurrentProject.Path & "\" & Dateiname
11.9.2 Anzeige externer Bilddateien in Berichten In Berichten ist die Vorgehensweise durch die Steuerelementinhalt-Eigenschaft des Bildsteuerelements ebenfalls viel einfacher geworden (siehe Abbildung 11.13).
621
Kapitel 11
Abbildung 11.13: Entwurfsansicht eines Berichts mit verknüpften Bilddateien
Hier brauchen Sie gar keinen Code mehr, sondern geben nur noch das Feld mit dem Dateinamen für die Eigenschaft Steuerelementinhalt an.
Keine verknüpften Bilder in der Berichtsansicht Die neue Berichtsansicht unterstützt im Vergleich zur Seitenansicht das Beim DruckenEreignis nicht. Sie können diese Ansicht also nicht zum Anzeigen verknüpfter Bilder verwenden.
Wenn Bilder nicht angezeigt werden wollen … Manche .jpg-Datei und Bilddatei anderer Formate kann Access nicht in seinem Bild steuerelement anzeigen. Das Problem liegt entweder tatsächlich in einem fehlenden Grafikformat oder aber das Format der Datei entspricht nicht exakt den Spezifikationen. Dann hilft nur das Konvertieren der Bilder in ein anderes Format.
11.9.3 Alternative zum Bildsteuerelement von Access Eine Alternative zum Bildsteuerelement ist das Image-Steuerelement der MSForms-Bib liothek, die bei jeder Access-Installation – auch bei Runtimes – mitinstalliert wird. Dieses Steuerelement fügen Sie über den Dialog ActiveX-Steuerelement einfügen ein (Ribbon-Ein trag Entwurf|Steuerelemente|ActiveX-Steuerelement einfügen). Damit das Steuerelement die Bilder passend zur Steuerelementfläche zoomt (siehe Abbildung 11.14), fügen Sie noch zwei Zeilen Code hinzu, sodass die Prozedur aus dem vorherigen Beispiel nun so aussieht:
622
Bilder und binäre Dateien Private Sub Detailbereich_Print(Cancel As Integer, PrintCount As Integer) Dim strDateiname As String Dim strPicture As String Dim objImage As MSForms.Image 'Prüfen, ob Dateiname vorhanden ist If Not IsNull(Me!Dateiname) Then strDateiname = Me!Dateiname 'Prüfen, ob das angegebene Bild existiert... If Not Dir(strDateiname) = "" Then '... und Bild festlegen strPicture = strDateiname End If End If 'Bildname dem Bildsteuerelement zuweisen Set objImage = Me!imgBild.Object objImage.PictureSizeMode = fmPictureSizeModeZoom Set objImage.Picture = LoadPicture(strDateiname) End Sub Listing 11.6: Anzeigen Bibliothek
verknüpfter
Bilddateien
im
Image-Steuerelement
der
MSForms-
Abbildung 11.14: Bilder im Image-Steuerelement der MSForms-Bibliothek
623
Kapitel 11
11.10 Die Office Graphics Library Die OGL (ogl.dll) ist eine spezielle Version der unter Windows XP/2003 für Grafikopera tionen verantwortlichen Bibliothek GDI+ 1.0, nämlich deren Version 1.1, die auch in Windows Vista Verwendung findet. Sie kommt mit Office 2007 und liefert noch mehr Funktionen als die GDI+ 1.0-Bibliothek. Das Problem ist: Die Bibliothek ist eine DLL und Sie müssen sich die darin enthaltenen API-Funktionen erst erschließen. Oder doch nicht? Nein! Sascha Trowitzsch hat mit seinem »Office Graphics Library Module« ein Modul geschaffen, das die für den täglichen Gebrauch notwendigen Funktionen komfortabel in leicht zugängliche Funktionen einpackt (Sie finden dieses Modul in der Beispieldatenbank zu diesem Kapitel auf der Buch-CD unter \Kap_11\ BilderUndBinaereDaten.accdb). Nachfolgend finden Sie einige Beispiele für die Anwendung dieser genialen Funktions sammlung. Vorneweg der Hinweis, dass der Datentyp StdPicture der Bibliothek OLE Automation in den meisten Fällen als Container für ein Bild dient. So lesen Sie etwa zunächst eine Bilddatei in ein Objekt des Typs StdPicture der Bibliothek OLE Automation ein, bevor Sie dieses im OLE-Feld einer Tabelle speichern – Sie benötigen also in Ihrer Datenbank einen entsprechenden Verweis. Dazwischen können Sie mit dem StdPicture-Objekt eine Menge anfangen: Sie können beispielsweise seine Abmessungen auslesen, die Größe verändern, verschiedene Eigenschaften wie Kontrast, Helligkeit oder Sättigung anpassen oder das Bild weichzeichnen. Sprich: Sie packen dieses Modul in Ihre Datenbank und können eine ganze Reihe Bildbearbeitungen automatisieren.
11.10.1 Bilder aus dem OLE-Feld in einem Formular anzeigen Mit dem StdPicture-Objekt und dem Image-Steuerelement der MSForms 2.0-Bibliothek ist es möglich, in OLE-Feldern gespeicherte Bilder direkt – und zwar ohne Umweg über das Dateisystem – in einem Formular anzuzeigen. Der Entwurf des Formulars sieht wie in Abbildung 11.15 aus, die Anzeige der Bilder erfolgt mit besagtem ImageSteuerelement. Als Datensatzquelle dient die Tabelle tblOLEBilder. Um den Inhalt des OLE-Feldes der Tabelle anzuzeigen, lesen Sie zunächst den Binärstrom aus dem passenden Tabellenfeld – hier das Feld OLEBild der Tabelle tblOLEBilder – aus und wandeln es in ein StdPicture-Objekt um. Dazu verwenden Sie die Funktion PicFromField aus dem Modul mdlOLE, die ihrerseits die Funktion ArrayToPicture des Moduls mdlOGL2007 einsetzt.
624
Bilder und binäre Dateien
Abbildung 11.15: Das Formular zum Anzeigen von Bildern direkt aus der Tabelle in der Entwurfsansicht
Public Function PicFromField(ByVal picField As DAO.Field) As Picture Dim arrBin() As Byte Dim LSize As Long On Error GoTo Fehler LSize = picField.FieldSize 'Größe des Biärstreams des Feldes bestimmen If LSize > 0 Then ReDim arrBin(LSize) 'Feldinhalt in Byte-Array überführen arrBin() = picField.GetChunk(0, LSize) 'Zu ArrayToPicture() siehe mdlOGL2007: InitGDIP Set PicFromField = ArrayToPicture(arrBin) ShutDownGDIP End If Ende: On Error Resume Next Erase arrBin Exit Function Fehler: MsgBox Err.Description Resume Ende End Function Listing 11.7: Inhalt eines OLE-Felds in ein StdPicture-Objekt einlesen
625
Kapitel 11
Die Routine BildAnzeigen macht sich die Funktion PicFromField zu Nutze, indem sie das Field-Objekt mit dem Bild ermittelt und dieses an die Funktion PicFromField übergibt, um ein passendes StdPicture-Objekt zurückzuerhalten. Dieses wiederum lässt sich einfach im Image-Steuerelement des Formulars anzeigen. Private Sub BildAnzeigen() Dim objPicture As stdole.StdPicture Dim db As DAO.Database Dim rst As DAO.Recordset Dim fld As DAO.Field Set db = CurrentDb Set rst = db.OpenRecordset("SELECT OLEBild FROM tblOLEBilder WHERE OLEBildID = " & Me.OLEBildID, dbOpenDynaset) Set fld = rst.Fields(0) InitGDIP Set objPicture = PicFromField(fld) Set Me.imgBild.Picture = objPicture ShutDownGDIP End Sub Listing 11.8: Diese Routine ermittelt das Field-Objekt mit dem Bild zum aktuell im Formular angezeigten Datensatz und wandelt dessen Inhalt in ein Format um, das vom Image-Steuerelement angezeigt werden kann
Wann nun wird das in der Tabelle enthaltene Bild in das Image-Steuerelement geladen? Natürlich in der Ereignisprozedur, die beim Anzeigen eines jeden Datensatzes ausgelöst wird. Diese prüft, ob das Feld OLEBild überhaupt gefüllt ist, und ruft dann die Routine BildAnzeigen auf, um das Image-Steuerelement damit zu füllen: Private Sub Form_Current() If Not IsNull(Me.OLEBild) Then BildAnzeigen Else Set Me.imgBild.Picture = Nothing End If End Sub Listing 11.9: Bild im Formular anzeigen
Wenn bereits ein Bild im ersten Datensatz der Datensatzquelle des Formulars enthalten ist, sieht dieses nun beim Öffnen wie in Abbildung 11.16 aus. In der Abbildung sehen Sie noch eine Schaltfläche zum Hinzufügen eines Bildes zu einem Datensatz. Diese füllen Sie natürlich auch noch mit Leben – Sie wollen ja schließlich auch komfortabel Daten zur Tabelle hinzufügen. Dazu legen Sie die folgende Ereignisprozedur an, die zunächst die Funktion WZHOpenFileName zum Auswählen der gewünschten Bilddatei aufruft (Sie finden diese Funktion im Modul mdlOpenSaveFi-
626
Bilder und binäre Dateien
le der Beispieldatenbank). Anschließend speichert die Routine den aktuellen Datensatz, weil gegebenenfalls noch keiner in der zugrunde liegenden Tabelle enthalten ist. Das ist notwendig, weil das Speichern des Bildes direkt in die Tabelle erfolgt. Schließlich speichert die Routine das Bild mit der Funktion SaveFileToOLEField in der passenden Tabelle und aktualisiert die Anzeige durch einen Aufruf der Funktion BildAnzeigen.
Abbildung 11.16: Ein direkt aus der Tabelle geladenes Bild im Binärformat
Private Sub cmdBildHinzufuegen_Click() Dim strFilename As String strFilename = WZHOpenFileName(CurrentProject.Path, "Bildddatei auswählen", , , False, ofnThumbs) Me!Dateiname = Mid(strFilename, InStrRev(strFilename, "\") + 1) DoCmd.RunCommand acCmdSaveRecord SaveFileToOLEField strFilename, "tblOLEBilder", "OLEBild", True, "OLEBildID", Me.OLEBildID BildAnzeigen End Sub Listing 11.10: Hinzufügen eines Bildes aus einer Datei in ein OLE-Feld
11.10.2 Bild aus einem OLE-Feld wiederherstellen Auf diese Art gespeicherte Bilder können Sie natürlich auch wieder in eine Datei umwandeln. Auch dazu können Sie eine passende Schaltfläche im Formular anlegen. Für diese hinterlegen Sie dann den folgenden Code. Die Routine ermittelt zunächst aus dem im Feld Dateiname gespeicherten Wert die Dateiendung und daraus weiter unten das
627
Kapitel 11
Dateiformat, unter dem die Bilddatei gespeichert werden soll. Zwischendurch erzeugt sie ein Recordset, das genau den soeben im Formular angezeigten Datensatz enthält und schreibt aus dem darin enthaltenen OLE-Feld die passende Datei auf die Festplatte (in das Verzeichnis der aktuellen Anwendung). Private Sub cmdBildSpeichern_Click() Dim objPicture As stdole.StdPicture Dim db As DAO.Database Dim rst As DAO.Recordset Dim fld As DAO.Field Dim strFileType As String Dim intPicType As PicFileType strFileType = Mid(Me.Dateiname, InStrRev(Me.Dateiname, ".") + 1) Set db = CurrentDb Set rst = db.OpenRecordset("SELECT OLEBild FROM tblOLEBilder " _ & "WHERE OLEBildID = " & Me.OLEBildID, dbOpenDynaset) Set fld = rst.Fields(0) Set objPicture = PicFromField(fld) InitGDIP Select Case strFileType Case "bmp" intPicType = pictypeBMP Case "gif" intPicType = pictypeGIF Case "png" intPicType = pictypePNG Case "jpg" intPicType = pictypeJPG Case "tif" intPicType = pictypeTIF End Select SaveImage objPicture, CurrentProject.Path & "\" & Me!Dateiname, _ intPicType ShutDownGDIP Set fld = Nothing rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 11.11: Speichern eines Bildes aus einem OLE-Feld in eine Datei
11.10.3 Speichern in verschiedenen Formaten Sie sind beim Speichern eines Bildes, das in einem OLE-Feld als Binärstrom vorliegt, keinesfalls daran gebunden, es im ursprünglichen Format zu speichern. Ändern Sie einmal die Dateiendung im passenden Feld im Formular und klicken Sie dann auf die Bild
628
Bilder und binäre Dateien
speichern-Schaltfläche – Sie werden sehen, dass die Routine SaveImage aus dem Modul mdlOGL2007 Bilder auch in andere Formate umwandeln kann.
11.10.4 Bilder bearbeiten Sie können die Funktionen des Moduls mdlOGL2007 auch dazu benutzen, ein einmal in ein StdPicture-Element geladenes Bild zu bearbeiten oder dessen Größe zu bestimmen. Leider kann das Buch an dieser Stelle keine komplette Beispielanwendung liefern, aber Sie sollen zumindest die Möglichkeiten kennen. Daher finden Sie nachfolgend eine Auflistung der vorhandenen Funktionen: InitGDIP: Initialisiert die Bibliothek ShutDownGDIP: Deinitialisiert die Bibliothek LoadPicturePlus: Lädt ein Bild aus einer Datei in ein StdPicture-Objekt ResampleImage: Skaliert ein Bild auf die angegebene Größe MakeThumb: Erstellt ein Vorschaubild in der angegebenen Größe; der Unterschied zu ResampleImage ist, dass diese Funktion die Originalproportionen beibehält und fehlende Flächen als Rahmen ausfüllt MergeImages: Mischt zwei Bilder, wobei Sie einen Überblendfaktor angeben können ImageMakeAlpha: Erzeugt ein 32-bit-Bild mit einem Transparenzkanal auf Basis der dafür angegebenen Farbe OverlayImage: Blendet ein Bild mit Transparenz in ein zweites Bild ein GetImageSize: Ermittelt die Höhe und Breite eines Bildes und gibt diese in Form des Typs TSize zurück, der die Höhe und Breite enthält SaveImage: Speichert das im StdPicture enthaltene Bild; besonders wichtig: Sie können das Bildformat festlegen, in dem das Bild gespeichert werden soll. ImageDrawFrame: Fügt einem Bild einen Rahmen hinzu (gegebenenfalls mit Farbver lauf) BlurImage: Weichzeichnen eines Bildes BrightnessContrast: Stellt die Helligkeit und den Kontrast eines Bildes ein Saturation: Stellt die Sättigung eines Bildes ein ArrayFromPicture: Erzeugt ein Byte-Array aus einem StdPicture-Objekt, das Sie in einem OLE-Feld speichern können
629
Kapitel 11
ArrayToPicture: Erzeugt ein StdPicture-Objekt aus einem als Byte-Array gespeicherten Bild GetIconPic: Liefert das System-Icon zum Datentyp der angegebenen Datei GetScreenRes: Liest die aktuelle Bildschirmauflösung aus Das Modul enthält noch weitere Funktionen, die Sie sich in der Beispieldatenbank ansehen können.
11.10.5 Ersatz für Anlagen? Das Anlage-Feld ist zweifellos eine Erleichterung für Access-Anwender. Allerdings behält das Speichern der Dateien in einer verborgenen Datei einen faden Beigeschmack. Warum also nicht Bilder und binäre Dateien als Binärstrom in einem OLE-Feld speichern? Mit den genannten Funktionen des Moduls mdlOGL2007 erhalten Sie Möglichkeiten, die das Anlage-Feld nicht liefern kann. Oder doch? Aber natürlich. Sascha Trowitzsch liefert auch noch eine Routine, mit der Sie den Inhalt eines Anlage-Feldes so in ein Byte-Array einlesen, dass Sie es leicht mit der Funktion ArrayToPicture in ein StdPictureObjekt umwandeln können. Und somit können Sie auch alle anderen in mdlOGL2007 enthaltenen Funktionen einsetzen. Aber: Eine Funktion zum Zurückverwandeln in eine Anlage liefert die Beispieldatenbank nicht.
Performance pro und kontra Bei der Entscheidung, ob Sie Anlage- oder OLE-Felder zum Speichern Ihrer Bilder oder Dateien verwenden, sollten Sie auch die Performance berücksichtigen. Access komprimiert den Inhalt von Anlage-Feldern, wenn es sinnvoll ist – etwa beim .bmp-Format. Damit benötigen Sie weniger Speicherplatz und vor allem lassen sich Daten schneller über das Netzwerk transportieren. Andererseits kostet das Packen und Entpacken ebenfalls Ressourcen. Das binäre Speichern im OLE-Feld erfolgt unkomprimiert, kostet also beim Anlegen und Laden weniger Zeit. Dafür haben die Dateien Originalgröße – schlecht also etwa beim .bmp-Format – und führen gegebenenfalls zu mehr Netzwerktraffic. Für Bilder im .jpg-, .gif- oder .png-Format, die per Definition bereits komprimiert sind, spielt das hingegen keine Rolle.
630
12 Ribbon Die neue Menüleiste der Microsoft Office-Anwendungen heißt Ribbon. Die deutsche Bezeichnung für Ribbon lautet Multifunktionsleiste (wörtlich übersetzt: »Band«), in diesem Buch wird jedoch ausschließlich von Ribbon die Rede sein – das ist erstens kürzer und zweitens wesentlich cooler. Das Ribbon ist Teil der runderneuerten Benutzer oberfläche und übernimmt die Rolle von Menü- und Sym bolleisten gleichzeitig. Für Sie als Entwickler ist natürlich besonders interessant, wie Sie das Ribbon für Ihre Zwecke anpassen können, um etwa die eingebauten Befehle auszublenden und eigene Tabs, Groups und Steuerelemente hinzuzufügen. Deshalb hält sich dieses Kapitel nicht mit einer Beschrei bung der Bedienung des Ribbons auf (wenn Sie es bis zu diesem Kapitel geschafft haben, sollten Sie das schon drauf haben), sondern konzentriert sich auf die Anpassung und Programmierung des Ribbons. Vorab zu Ihrer Beruhigung: Das Anpassen funktioniert ohne Weiteres, aber erstens völlig anders und zweitens nicht so einfach wie bei den CommandBars älterer AccessVersionen. Der Hauptgrund dafür, dass es nicht so leicht geht, ist die Tatsache, dass Microsoft noch keine grafische Benutzeroberfläche zum Anpassen des Ribbons mitliefert. Es gibt zwar schon einige freie und kostenpflichtige Tools am Markt, aber Microsoft ist zur Zeit der Drucklegung dieses Buchs noch nicht mit einer eigenen Lösung heraus gekommen. Es ist allerdings zu erwarten, dass dies früher oder später geschehen wird.
Kapitel 12
Beispieldatenbank: Eine Datenbank mit vorbereitetem Datenmodell finden Sie auf der Buch-CD unter \Kap_12\Ribbon.accdb.
12.1 Definition des Ribbons Wie schwer Ihnen das Anpassen des Ribbons fällt, hängt in erster Linie davon ab, ob Sie bereits XML-Grundkenntnisse besitzen oder sich diese aneignen wollen. Sie müssen allerdings kein XML-Guru sein, um das Ribbon anzupassen. Microsoft unternimmt in Office 2007 den Versuch, nicht nur Dokumente, sondern auch sehr viele Elemente der Oberfläche über XML-Dateien zu steuern. Diese XMLSteuerdateien befinden sich meist in den Dokumenten selbst. Im Falle von Access handelt es sich dabei üblicherweise um Tabellen. Die Anpassungen legen Sie grundsätzlich in einem XML-Dokument fest, das Sie Access auf verschiedenen Wegen zugänglich machen können. Damit das alles funktioniert, hat Microsoft eine XML-Schema-Datei (XSD) spendiert, die Vorgaben für den Aufbau einer Ribbon-XML-Definition liefert. Mit einem vernünftigen XML-Editor ausgestattet können Sie die notwendige XML-Datei leicht erstellen. »Vernünftig« bedeutet dabei, dass der Editor IntelliSense- oder eine ähnliche Funktion mitbringt, die Ihnen die möglichen Elemente und Werte für das XML-Dokument vorschlägt – dazu jedoch später mehr. Wenn die von Ihnen erstellte XML-Datei nämlich nicht exakt zu den Vorgaben der Schemadefinitionen passt, dann meldet Access einen Fehler des XML-Parsers. Sie sollten also peinlich genau darauf achten, dass die XML-Datei »wellformed« und schema konform ist. Zunächst bekommen Sie noch eine Definition dessen mit auf den Weg, was das Ribbon nun eigentlich ist: Und dabei hilft die oben erwähnte XSD-Datei. Sie legt nämlich das Ribbon-Element als Root-Element fest und sieht darunter vier verschiedene Elemente vor: qat (Schnellzugriffsleiste): Kleine Leiste rechts neben dem Office-Button und standardmäßig über der Ribbon-Leiste, enthält Befehle, die schnell zugänglich sein sollen und lässt sich per Benutzeroberfläche anpassen (Kontextmenüeintrag Symbolleiste für den Schnellzugriff anpassen) tabs: machen das »eigentliche« Ribbon aus, können über die »Registerreiter« ausgewählt werden und enthalten in Gruppen aufgeteilte Steuerelemente contexualTabs: Tabs, die in Zusammenhang mit bestimmten Objekten eingeblendet werden – etwa das Entwurf-Tab beim Anzeigen eines Formulars in der Entwurfsan sicht
632
Ribbon
officeMenu: Menü, das sich beim Klicken auf den Office-Button öffnet Auf der gleichen Ebene wie der des ribbon-Elements können Sie auch noch ein commandsElement einfügen, mit dem Sie die durch Steuerelemente eingebauter Tabs ausgeführten Funktionen durch den Aufruf benutzerdefinierter Funktionen ersetzen können.
Ribbon-Definition mit XML Notepad 2007 Offensichtlich ist das »Ribbon« also mehr als nur die neue Menüleiste von Access- und anderen Office-Anwendungen. Das können Sie sich komfortabel etwa im kostenlosen XML Notepad 2007 von Microsoft ansehen [1]. Abbildung 12.1 zeigt den schematischen Aufbau eines XML-Dokuments mit allen vier oben aufgelisteten Bereichen. Die Bedienung des XML Notepads ist gewöhnungsbedürftig, weil Sie nicht direkt mit dem Editor im Text des XML-Dokuments arbeiten, aber dafür prüft das Tool Ihr Dokument direkt anhand der passenden XSD-Datei. Diese können Sie dem XMLNotepad über das Menü View|Schemas und den dortigen Eintrag File|Add schemas bekannt machen. Sie finden die Schemadefinition customui.xsd unter [2] oder auf der BuchCD unter \Kap12\customUI.xsd. Die Abbildung veranschaulicht vor allem den Aufbau der eigentlichen Ribbon-Leiste: Sie enthält eine tabs-Auflistung mit mehreren tab-Elementen, die wiederum eine oder mehrere Gruppen mit einem oder mehreren Steuerelementen enthält (in der Abbildung gibt es aus Platzgründen nur je ein Tab und eine Gruppe, dafür aber mit allen möglichen Steuerelementen). Zu den Elementen qat, contextualTabs und officeMenu finden Sie im Laufe dieses Kapitels weitere Informationen. Wenn Sie das Ribbon anpassen möchten, schreiben Sie genau ein XML-Dokument, das alle Änderungen enthält. Am wichtigsten ist dabei zunächst, ob Sie die vorhandenen Tabs beibehalten oder ausblenden möchten. Und danach können Sie richtig loslegen und Tabs, Gruppen und Steuerelemente anlegen oder einen der anderen Bereiche beackern.
12.2 Symbolleiste für den Schnellzugriff Bevor Sie zum XML-Editor greifen, noch schnell die Beschreibung eines Elements des Ribbon, das Sie über die Benutzeroberfläche anpassen können: die »Symbolleiste für den Schnellzugriff« (im Folgenden kurz »Schnellzugriffsleiste« genannt). Sie ergänzt die eigentliche Ribbon-Leiste und enthält im Auslieferungszustand drei Einträge – einen zum Speichern, einen zum Wiederholen und einen zum Rückgängigmachen von Aktionen. Weitere Einträge fügen Sie auf verschiedene Weise hinzu:
633
Kapitel 12
Abbildung 12.1: Die Struktur und einige mögliche Elemente einer Ribbon-XML-Dokumentation
Über das mit der nebenan liegenden Schaltfläche zu öffnende Menü: Damit können Sie die vorhandenen Einträge abwählen und andere hinzufügen (siehe Abbil dung 12.2). Beliebige Befehle aus den vorhandenen Ribbons fügen Sie der Schnellzugriffsleiste über den Eintrag Zu Symbolleiste für den Schnellzugriff hinzufügen des Kontextmenüs des jeweiligen Befehls hinzu (siehe Abbildung 12.3). Im Bereich Anpassen des Dialogs Access-Optionen können Sie alle in den Ribbons vorhandenen Befehle in der Übersicht anzeigen und der Schnellstartleiste hinzufügen. Dies macht Sinn, wenn man sich etwa eine immer sichtbare Drucken-Schaltfläche auf
634
Ribbon
den Schirm zaubern möchte. Den passenden Dialog (siehe Abbildung 12.4) öffnen Sie entweder über den herkömmlichen Weg (Office-Button|Access-Optionen), über den Eintrag Weitere Befehle... des Schnellzugriffsleisten-Menüs oder den Eintrag Sym bolleiste für den Schnellzugriff anpassen... des Kontextmenüs eines Ribbon-Steuerele ments.
Abbildung 12.2: Die Symbolleiste für den Schnellzugriff und das Menü für ihre Anpassung
Abbildung 12.3: Per Kontextmenü können Sie beliebige Befehle zur Schnellzugriffsleiste hinzufügen
Schnellzugriffsleiste positionieren Die standardmäßig neben dem Office-Button befindliche Schnellzugriffsleiste können Sie auch unterhalb des Ribbons platzieren. Dazu wählen Sie etwa den Eintrag Unter der Multifunktionsleiste anzeigen des Menüs der Schnellzugriffsleiste. Mit dieser Einstellung vergrößert sich der Ribbon-Bereich allerdings um einen zusätzlichen Balken.
635
Kapitel 12
Abbildung 12.4: Dialog zum Anpassen der Schnellzugriffsleiste
Anwendungsspezifische Schnellzugriffsleiste Sie können der Schnellzugriffsleiste für jede Datenbankanwendung ein eigenes Gesicht verpassen. Dazu klappen Sie im Dialog zum Anpassen der Schnellzugriffsleiste das Kom binationsfeld auf der rechten Seite auf und wählen dort die aktuell geöffnete Datenbank aus. Access zeigt nun eine leere Liste an, die Sie mit den gewünschten Befehlen füllen können. Alle Befehle, die Sie hier hinzufügen, zeigt Access zusätzlich zu den für alle Datenbanken ausgewählten Befehlen an. Wenn Sie also nur datenbankspezifische Einträge anzeigen wollten, müssten Sie alle allgemein sichtbaren Einträge der Schnellzugriffsleiste entfernen. Damit wäre allerdings nicht sichergestellt, dass auch der Anwender nur diese Einträge sieht – das hängt von der auf seinem Rechner vorliegenden Konfiguration der Schnellzugriffsleiste ab. Um dies zu gewährleisten, kommen Sie um die Definition eines eigenen Ribbons nicht he rum.
12.3 Eigene Ribbon-Tabs erstellen Bevor Sie loslegen, sollten Sie im Dialog Verweise des VBA-Editors (Extras|Verweise) einen Verweis auf die Bibliothek Microsoft Office 12.0 Object Library anlegen. Anderenfalls kann es bei den folgenden Beispielen zu Fehlermeldungen kommen.
636
Ribbon
12.3.1 Ein einfaches Ribbon Bei der Anpassung des Ribbons ist aller Anfang schwer, vor allem für VBA-verwöhnte Entwickler: Immerhin haben Sie es hier mit XML zu tun, und dies ist bezüglich der korrekten Groß- und Kleinschreibung mindestens so pingelig wie die Korrektorin dieses Buches. Fangen Sie also mit einem ganz einfachen Beispiel an, das im Ribbon nur ein Tab mit einer einzigen Schaltfläche anlegen soll. Der einfachste Weg ist, das notwendige XML-Dokument in einer speziell für diesen Zweck vorgesehenen Tabelle zu speichern, die einen festen Aufbau und den Namen USysRibbons haben muss. Access liest beim Starten einer Anwendung alle in dieser Tabelle enthaltenen Datensätze ein und stellt diese zur Auswahl zur Verfügung (wo, erfahren Sie weiter unten). Wenn Sie diese Tabelle unter diesem Namen anlegen und speichern, scheint sie plötzlich verschwunden zu sein. Der Grund ist, dass Access alle Tabellen mit bestimmten Anfangs buchstaben (etwa MSys... oder USys...) standardmäßig für Systemtabellen hält und ausblendet. Zu sehen bekommen Sie diese Tabellen, wenn Sie in den Navigationsoptionen (Kontextmenü des Navigationsfensters, Eintrag Navigationsoptionen...) die Option Systemobjekte anzeigen aktivieren. Zum Speichern von Ribbon-Definitionen in der Tabelle USysRibbons gehen Sie wie folgt vor: Legen Sie eine neue Tabelle an und fügen Sie die in Abbildung 12.5 dargestellten Felder hinzu. Das Feld RibbonName (Text, 255 Zeichen) soll die später in einer Auswahlliste angezeigte Bezeichnung enthalten, das Feld RibbonXML (Memofeld) den für die Erstellung notwendigen XML-Code.
Abbildung 12.5: Die Tabelle USysRibbons speichert die Informationen zu benutzerdefinierten Anpassungen des Ribbons
637
Kapitel 12
Wechseln Sie dann in die Datenblattansicht der Tabelle und legen Sie einen neuen Datensatz an. Für das Feld RibbonName tragen Sie die Bezeichnung RibbonMitEinerSchalt flaeche ein, für das Feld RibbonXML den folgenden XML-Code: <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui"> Listing 12.1: XML-Code für das Anlegen eines Ribbons mit einer einzigen Schaltfläche
Das Attribut startFromScratch des Elements ribbon legt fest, ob die eingebauten Ribbons angezeigt (false) oder ausgeblendet werden sollen (true). Jedes Ribbon-XML-Dokument enthält ein tabs-Element, das wiederum eines oder mehrere tab-Elemente enthält. Ein tabElement entspricht einer eigenen Registerseite des Ribbons. Im vorliegenden Fall hat das tab-Element die Attribute id, label und visible. Das Attribut id muss für jedes Element vorhanden sein. Das group-Element leitet eine Gruppe eines Ribbons ein – das ist ein eigener Bereich, der ein oder mehrere Steuerelemente enthalten kann. Es ist das einzig mögliche Unterelement des tab-Elements, jedes Tab muss also eine Gruppe enthalten. Das button-Element schließlich beschreibt die eigentliche Schaltfläche, die in diesem Fall einfach nur angezeigt wird und keinerlei Funktion hat. Das Ribbon soll schließlich wie in Abbildung 12.6 aussehen.
Abbildung 12.6: Dieses Ribbon entspricht der Definition aus Listing 12.1
Vor der Anzeige des Ribbons müssen Sie jedoch noch einige weitere Schritte durchführen, denn irgendwie muss Access ja noch erfahren, dass es dieses Ribbon anzeigen soll. Nach dem Einfügen des Ribbon-Datensatzes in die Tabelle USysRibbons schließen Sie die Datenbank und öffnen diese erneut, damit Access die in der Tabelle enthaltenen
638
Ribbon
Ribbons einliest – und zwar in die Liste der verfügbaren benutzerdefinierten Ribbons. Am schnellsten schließen Sie die Datenbank und starten diese neu, wenn Sie den Eintrag Verwalten|Datenbank komprimieren und reparieren aus dem Office-Menü auswählen. Die vorhandenen Ribbons finden Sie in den Access-Optionen im Bereich Aktuelle Daten bank|Multifunktionsleisten- und Symbolleistenoptionen unter Name der Multifunktionsleiste (siehe Abbildung 12.7). Wenn Sie hier das soeben angelegte Ribbon-XML-Dokument auswählen, fordert Access Sie zum Schließen und erneuten Öffnen der Anwendung auf und zeigt dann endlich das modifizierte Ribbon an. Die Tabelle USysRibbons der Beispieldatenbank enthält übrigens einige Dutzend RibbonDefinitionen, deren Code in diesem Kapitel ausschnittweise besprochen wird.
12.3.2 Schaltfläche mit Funktion versehen Bisher löst ein Klick auf die Beispielschaltfläche noch keine Funktion aus, aber das holen Sie nun nach. Erweitern Sie die Definition des button-Elements aus dem obigen RibbonXML-Dokument wie folgt:
Das onAction-Attribut enthält den Aufruf einer öffentlichen VBA-Routine – eine so genannte Callback-Funktion –, die Sie in einem Standardmodul anlegen. Erstellen Sie also ein neues Standardmodul namens mdlRibbon und fügen Sie die folgende Routine hinzu: Public Function Beispielfunktion(ctl As IRibbonControl) MsgBox "Sie haben eine Ribbon-Schaltfläche angeklickt." End Function Listing 12.2: Diese Routine wird beim Klick auf die passende Schaltfläche ausgelöst
Die Funktion soll schlicht und einfach ein Meldungsfenster anzeigen, um die einwandfreie Funktion des Aufrufs zu bestätigen. Um dies zu testen, müssen Sie die Datenbank anwendung erneut schließen und wieder öffnen und dann auf die neue Schaltfläche kli cken. Möglicherweise erscheint nun eine Meldung wie »Der von Ihnen eingegebene Ausdruck enthält den Namen einer Funktion, die von Microsoft Office Access nicht gefunden werden kann«. Das bedeutet nicht, dass Sie etwas falsch gemacht haben, allerdings liefert diese Meldung auch nicht unbedingt hilfreiche Informationen. Vermutlich verbieten die aktuellen Sicherheitseinstellungen für diese Datenbankanwen dung das Ausführen von VBA-Code, was Sie ganz einfach durch einen Aufruf der soeben angelegten Funktion im Direktfenster testen können. Die Meldung »Die Makros in diesem Projekt sind deaktiviert. Informationen zum Aktivieren der Makros finden Sie in
639
Kapitel 12
der Onlinehilfe oder der Dokumentation der Host-Anwendung.« würde diese Annahme bestätigen; Sie sollten daher zunächst die Sicherheitseinstellungen anpassen. Weitere In formationen hierzu finden Sie in Kapitel 18, Abschnitt 18.4, »Vertrauensstellungscenter«.
Abbildung 12.7: Auswahl eines benutzerdefinierten Ribbons im Dialog Access-Optionen
Wenn die Sicherheitseinstellungen für die aktuelle Datenbank entsprechend einstellt sind, sollte nun das in der Funktion angegebene Meldungsfenster erscheinen und die Funktionstüchtigkeit Ihrer ersten selbst angelegten Ribbon-Schaltfläche bestätigen.
Angeklickte Schaltfläche ermitteln Vielleicht möchten Sie ja alle Funktionen für Schaltflächen in einer einzigen Routine unterbringen. Eine Ribbon-Schaltfläche liefert alles Notwendige dazu – nämlich die Möglichkeit, einen Verweis auf die betätigte Schaltfläche an die aufgerufene Routine zu übergeben. Dazu müssen Sie im Ribbon-XML-Dokument gar nichts ändern, sondern nur die Zielfunktion. Diese sieht im einfachsten Fall so aus: Public Function Beispielfunktion(ctl As IRibbonControl) MsgBox "Sie haben die Schaltfläche '" & ctl.Id & "' angeklickt." End Function Listing 12.3: Ausgeben des Namens der angeklickten Schaltfläche
Etwas strukturierter darf es dann doch sein, also fügen Sie ein Select Case-Konstrukt in die Routine ein: Public Function Beispielfunktion(ctl As IRibbonControl) Select Case ctl.Id
640
Ribbon Case "btnBeispielschaltflaeche1" 'tu was Case "btnBeispielschaltflaeche2" 'tu was anderes Case Else MsgBox "Unbekannte Schaltfläche!" End Select MsgBox "Sie haben die Schaltfläche '" & ctl.Id & "' angeklickt." End Function Listing 12.4: Auswerten des aufrufenden Steuerelements in einer Select Case-Anweisung
12.4 Fehler in Ribbon-XML-Dokumenten erkennen Beim Experimentieren mit XML-Dokumenten zur Definition von Ribbons passiert es zwangsläufig, dass Access das gewünschte Ribbon einfach nicht anzeigt. Meist ist dies auf einen Fehler im XML-Dokument zurückzuführen, den Access stillschweigend übergeht. Das lässt sich durch Einstellen einer Option ändern: Öffnen Sie per OfficeButton|Access-Optionen den Optionen-Dialog, wechseln Sie dort zur Registerseite Erwei tert und aktivieren Sie die Option Fehler in Benutzeroberflächen in Add-Ins anzeigen im Bereich Allgemein (siehe Abbildung 12.8).
Abbildung 12.8: Aktivieren der Anzeige von Fehlern in Ribbon-XML-Dokumenten
Wenn Access dann einen Fehler im Ribbon-XML-Dokument findet, zeigt es diesen mit einer Meldung wie in Abbildung 12.9 an. In diesem Fall enthält das Element ribbon das nicht dafür definierte Attribut loadImage. Anhand der Zeilen- und Spaltennummer können Sie den Fehler im XML-Dokument dann schnell lokalisieren.
641
Kapitel 12
Abbildung 12.9: Fehler in Ribbon-XML-Dokumenten werden bei aktivierter Option »Fehler in Benutzeroberflächen in Add-Ins anzeigen« gemeldet
12.5 Callback-Funktionen Callback-Funktionen werden, soweit angegeben, vom Ribbon beim Anlegen oder bei bestimmten Aktionen aufgerufen. Es gibt zwei Arten von Callback-Funktionen: solche, die Werte für Attribute zurückliefern, und jene, die als Reaktion auf Benutzeraktionen aufgerufen werden.
12.5.1 Die get...-Attribute Jedes Element hat mehrere Attribute, die mit get... beginnen. Beim button-Element sind dies beispielsweise getDescription, getEnabled, getImage, getKeytip, getLabel, getScreenTip, getShowImage, getShowLabel, getSize, getSupertip und getVisible. All diesen Attributen können Sie Namen von Callback-Funktionen zuweisen, die beim Erzeugen des Ribbons die passenden Attributwerte ermitteln und zurückgeben. Ihr Einsatz ist dann sinnvoll, wenn Sie beim Erstellen des Ribbon-XML-Dokuments noch nicht wissen, wie der Inhalt eines der zu füllenden Attribute aussieht – sonst könnten Sie ja den passenden Wert auch einfach als Wert des Attributs eintragen. Wenn Sie etwa dem Attribut label erst beim Anlegen des Ribbons (beim Öffnen der Anwendung oder, wenn das Ribbon einem Formular oder Bericht zugeordnet ist, beim Öffnen dieser Objekte) einen bestimmten Wert zuordnen möchten, legen Sie mit getLabel eine für diesen Zweck angelegte VBA-Routine namens GetLabel fest. Das Beispiel ist nicht so abwegig und könnte beispielsweise beim Erstellen mehrsprachiger Anwendungen interessant sein. Der Kopf dieser Routine muss einer bestimmten Syntax folgen; eine Zusammenstellung der Syntax aller möglichen Funktionen finden Sie weiter hinten in diesem Kapitel in Tabelle 12.3. Die Definition einer Schaltfläche im XML-Dokument für das Ribbon sieht etwa wie folgt aus (get...-Attribut hervorgehoben):
642
Ribbon
Damit sich das Attribut beim Laden des Ribbons überhaupt bemerkbar macht, müssen Sie eine passende Routine in einem Standardmodul anlegen. So stellen Sie zumindst schon einmal sicher, dass Access entweder einen Fehler meldet oder, wenn Sie alles richtig gemacht haben, das Attribut mit dem entsprechenden Wert füllt. Eine Routine für das Zurückgeben eines Wertes für das im XML-Element angegebene Attribut sieht so aus: Public Sub GetLabel(ctl As IRibbonControl, ByRef label) label = "Beispielbeschriftung" End Sub Listing 12.5: Diese Callback-Routine liefert den String »Beispielbeschriftung« an das aufrufende Ribbon zurück
Vielleicht wundert es Sie, dass hier von einer Funktion die Rede, die aufgerufene Routine aber als Sub-Prozedur ausgeführt ist. Tatsächlich handelt es sich bei den meisten Callback-Routinen um solche Sub-Prozeduren, die eine von der Prozedur zu füllende ByRef-Platzhaltervariable bereitstellen.
Sonderfall LoadImage Etwas anders funktioniert die Routine LoadImage. Sie wird wie die in den get...-Attributen angegebenen Funktionen beim Anlegen eines Ribbons aufgerufen, ersetzt aber unter Umständen eine ganze Reihe notwendiger getImage und getItemImage-Attribute: Sie liest alle in Steuerelementen und deren Unterelementen wie etwa item-Elementen enthaltenen image-Attribute aus und ruft die unter loadImages (customUI-Tag) angegebene Callback-Funktion für jedes Image einmal auf. Diese Routine gibt dann passende Objekte mit Verweisen auf die entsprechenden Bilder zurück.
12.5.2 Ereigniseigenschaften Ribbons bieten Ereigniseigenschaften, wie sie von der VBA-Programmierung von For mularen und Berichten bekannt sind – zumindest, was den Auslöser angeht. Dabei handelt es sich nämlich entweder um einen Klick auf ein Steuerelement, das Drücken einer mit dem Attribut keytip angegebenen Tastenkombination oder die Änderung seines Inhalts. Außerdem gibt es noch eine Ereigniseigenschaft, die beim Laden des Ribbons ausgelöst wird: onAction: Verfügbar für button-, checkBox-, dropDown-, gallery- und toggleButtonElemente, wird bei Aktionen wie etwa einem Mausklick oder der Auswahl eines der Einträge ausgelöst. onChange: Verfügbar für comboBox- und editBox, wird beim Ändern des Inhalts beziehungsweise der Auswahl eines neuen Eintrags ausgewählt.
643
Kapitel 12
onLoad: Wird beim Laden des Ribbons ausgelöst. Auch für die Ereigniseigenschaften gibt es jeweils eine vorgegebene Syntax, die immer einen Verweis auf das aktuelle Steuerelement und auf den aktuellen Wert übergibt. Auch diese Syntax-Beschreibungen finden Sie weiter hinten in Tabelle 12.3.
12.5.3 Umgang mit Callback-Funktionen Wenn Sie einmal ein Ribbon mit mehr als nur einem tab-Element mit wenigen Unterele menten und passenden Callback-Funktionen bestücken, bekommen Sie möglicherweise Probleme bei der Vergabe der Namen für die Callback-Funktionen. Die Beispiele verwenden ja meist Routinennamen, die dem Namen der Ereigniseigenschaft bis auf den großgeschriebenen Anfangsbuchstaben gleichen. Wenn Sie auch so vorgehen möchten, müssen Sie berücksichtigen, dass es auch einmal mehr als ein Steuerelement geben kann, für das Sie etwa eine Routine namens OnAction anlegen möchten. In diesem Fall haben Sie zwei Möglichkeiten: Sie verwenden für jede Callback-Routine jedes Elements einen eindeutigen Namen. Sie verwenden für jede Ereignisart eine Routine, die jeweils das auslösende Steuer element auswertet – etwa mit einem Select Case über den per Parameter übergebenen Steuerelementnamen.
Callback-Routinen mit eindeutigem Namen Im ersten Fall weisen Sie den zu erstellenden Routinen Namen zu, die eindeutig sind und dennoch dem Element zugeordnet werden können (sonst entsteht schnell ein sehr unübersichtlicher Berg von Callback-Funktionen). So können Sie etwa das label- oder id-Attribut des Steuerelements integrieren und die Bezeichnung btnBeispiel_onChange verwenden. Und wenn verschiedene group- oder tabElemente Steuerelemente gleichen Namens enthalten, bauen Sie eben auch noch das label- oder id-Attribut der übergeordneten Elemente mit ein. Im Extremfall würde eine Callback-Routine dann beispielsweise tabMain_grpDateien_btnOeffnen_onAction heißen.
Eine Callback-Routine für alle Elemente Bei der zweiten empfehlenswerten Variante legen Sie tatsächlich nur eine CallbackRoutine für jedes Ereignis wie etwa onAction oder getDescription an, deren Name beispielsweise der Attributbezeichnung mit großem Anfangsbuchstaben entspricht (also
644
Ribbon
OnAction oder GetDescription). Beim Aufruf einer solchen Funktion wertet diese dann den Parameter control aus, der übrigens in allen Callback-Funktionen enthalten ist. Das sieht dann beispielsweise wie im folgenden Listing aus: Public Sub OnAction(ctl As IRibbonControl) Select Case ctl.id Case "btnBeispielschaltflaeche1" 'tu was Case "btnBeispielschaltflaeche2" 'tu was anderes Case Else MsgBox "Unbekannte Schaltfläche!" End Select End Sub Listing 12.6: Diese Routine wertet aus, von welchem Steuerelement sie aufgerufen wurde, und reagiert mit der für dieses Element vorgesehenen Aktion
Leider kommen Sie damit nicht allzu weit: Wie Sie Tabelle 12.3 entnehmen können, besitzen Callback-Funktionen für gleichnamige Ereigniseigenschaften durchaus nicht immer die gleiche Syntax. Die obige Routine entspricht etwa der Syntax für ein button-Element, die Syntax für die onAction-Callback-Routine eines dropDown-Elements hingegen sieht so aus: Sub OnAction(control As IRibbonControl, selectedId As String, selectedIndex As Integer)
Und da Access einen Fehler meldet, wenn eine Callback-Funktion die falschen Parameter enthält (vorausgesetzt, Sie haben wie oben beschrieben die Fehlerbehandlung für Rib bons aktiviert), müssen Sie zumindest für jede Steuerelementart eine Callback-Funk tion mit einem eigenen Namen anlegen. Beim onAction-Attribut könnte diese etwa OnButtonAction, OnCheckBoxAction, OnDropDownAction, OnGalleryAction oder OnToggle ButtonAction heißen. Die Klasse (genauer: das Interface) IRibbonControl ist übrigens in der Microsoft Office 12.0 Library definiert, die Sie deshalb den Verweisen des VBAProjekts hinzufügen müssen. Sie hat die folgenden Eigenschaften: ID: Gibt die in der XML-Definition für das Steuerelement festgelegte und obligatorische ID zurück. Context: Gibt einen Objektverweis auf die Anwendung des Ribbons zurück. Bei Access handelt es sich hierbei um das Application-Objekt. Tag: Gibt eine Zeichenfolge zurück, die Sie optional in der XML-Definition für das Steuerelement angegeben haben. Beispiel:
645
Kapitel 12
Fehler in Callback-Routinen Falls die Deklaration der Callback-Funktion nicht genau den Vorgaben entspricht, weil Sie fälschlicherweise etwa eine Deklaration wie in Listing 12.6 für ein dropDown-Element angegeben haben, dann meldet Access nicht etwa einen Syntaxfehler, sondern bemerkt lapidar: »Access kann die Makro- oder Rückrufaktion 'OnAction' nicht ausführen«. Die gleiche Meldung erscheint aber auch, wenn die Prozedur gar nicht existiert. Sollten Sie also feststellen, dass die betroffene Prozedur nicht fehlt, können Sie von einer fehlerhaften Deklaration der Parameter ausgehen. Access setzt das automatische Debugging von VBA bei allen Callback-Funktionen, die Eigenschaften zurückliefern, außer Kraft. Das bedeutet, dass bei Fehlern im VBA-Code der Callback-Routine nicht die übliche VBAFehlermeldung erscheint, sondern die Routine stillschweigend verlassen wird – so, als enthielte die Prozedur zu Beginn ein On Error Resume Next. Das macht das Debuggen dieser Routinen schwieriger. Wenn Sie vermuten, dass mit Ihrer Callback-Routine etwas nicht stimmt oder sie nicht das erwartete Ergebnis liefert, fügen Sie dem Code ganz vorne die Anweisung Stop hinzu. VBA unterbricht dann an dieser Stelle die Ausführung des Codes und lässt Sie im Einzelschritt (F8) die nächsten Code-Zeilen durchlaufen, den Ablauf untersuchen und Variableninhalte einsehen.
12.6 Weitere Ribbon-Steuerelemente Natürlich gibt es noch mehr Steuerelemente als nur Schaltflächen – genau genommen sogar noch mehr, als es sie für herkömmliche Menü- und Symbolleisten gab – und vor allem noch erheblich mehr Eigenschaften, als das erste Beispiel gezeigt hat. Die folgenden Abschnitte stellen die Steuerelemente und ihre wichtigsten Eigenschaften vor, außerdem lernen Sie hier Einsatzmöglichkeiten für die unterschiedlichen CallbackRoutinen kennen. Zu Beginn wird jedoch noch einmal die Schaltfläche aufgegriffen, da dieses wohl das meistbenutzte Steuerelement im Ribbon sein dürfte.
12.6.1 Schaltflächen Grundlegendes zum Hinzufügen einer Schaltfläche haben Sie ja bereits weiter oben in Zusammenhang mit dem ersten benutzerdefinierten Beispiel-Ribbon gelesen. Nun möchten Sie der Schaltfläche vielleicht ein Symbol hinzufügen, da diese sonst ein wenig einsam aussieht (keine Sorge, wir fügen später auch noch weitere Steuerelemente hinzu). Dazu gibt es verschiedene Möglichkeiten: Verwenden eines Symbols der Microsoft-Office-Bibliothek Hinzufügen einer benutzerdefinierten Bilddatei mit der Callback-Funktion LoadImage für das customIU-Objekt
646
Ribbon
Hinzufügen einer benutzerdefinierten Bilddatei mit der Callback-Funktion getImage des button-Elements oder sonstiger Steuerelemente In den folgenden Abschnitten erfahren Sie mehr über die ersten beiden Methoden und darüber, was es mit der Callback-Funktion getImage auf sich hat.
Hinzufügen eines eingebauten Symbols Die größte Hürde beim Hinzufügen eines bereits in einem der Ribbon-Steuerelemente vorhandenen Bildes ist, den Namen des jeweiligen Steuerelements herauszufinden. Dabei hilft die Access-Tabelle aus [3]. Aus dieser Tabelle suchen Sie die Bezeichnung des gesuchten Steuerelements heraus und entnehmen dann der Spalte ControlName die englische Bezeichnung. Wenn Sie beispielsweise eine Schaltfläche mit dem SpeichernSymbol versehen möchten, passen Sie die Definition des button-Elements an, indem Sie das Attribut imageMso wie folgt hinzufügen:
Die Schaltfläche sieht dann wie in Abbildung 12.10 aus.
Abbildung 12.10: Eine Schaltfläche mit einem eingebauten Symbol
Wenn Sie wissen möchten, welches Image wie heißt, finden Sie auf der Buch-CD eine passende Beispieldatenbank (\Kap_12\imagemsos.accdb). Die Datenbank zeigt alle über 4000 Symbole in mehreren Ribbon-Tabs an, wodurch das Laden der Datenbank einige Momente dauern kann. Nach dem Klick auf eines der Symbole zeigt ein Formular direkt das passende Attribut wie etwa imageMso="CreateFormBlankForm" zum Kopieren und Einfügen in die XML-Datei an.
Hinzufügen einer benutzerdefinierten Bilddatei per loadImage Zum Einfügen einer benutzerdefinierten Bilddatei sind zwei Anpassungen am XMLDokument und zusätzlicher VBA-Code notwendig. Fügen Sie zunächst der Definition der Schaltfläche, die mit einem Symbol ausgestattet werden soll, das Attribut Image hinzu und versehen Sie dieses mit dem Namen der zu verwendenden Bilddatei:
647
Kapitel 12
Zusätzlich müssen Sie Access mitteilen, dass die im Ribbon enthaltenen benutzerdefi nierten Symbole geladen werden sollen. Dazu fügen Sie dem Root-Element customUI das Attribut loadImage mit dem Namen einer Callback-Funktion hinzu, die einen Objekt verweis auf das jeweils zu verwendende Image zurückliefern soll: <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" loadImage="LoadImage">
Das loadImage-Attribut sorgt dafür, dass beim Initialisieren des Ribbons alle in Steuer elementen angegebenen image-Attribute ausgewertet und die passenden Symbole hinzugefügt werden. Für die angegebene Callback-Funktion müssen Sie natürlich eine passende VBA-Routine anlegen – die hier interessanterweise gar nicht als Funktion, sondern als Sub-Prozedur ausgeführt werden muss und statt eines Funktionswertes den Rückgabewert per Parameter liefert. Die Funktion sieht wie folgt aus: Public Sub LoadImage(control, ByRef image) Set image = LoadPictureGDIP(CurrentProject.Path & "\" & control) End Sub Listing 12.7: Die LoadImage-Routine liefert das per Parameter angegebene Bild zurück
Der erste Parameter control enthält die mit dem Attribut image angegebene Zeichenkette und damit in der Regel den Dateinamen der zu verwendenden Datei. Sie können diese auch mit komplettem Pfad angeben, aber wenn Sie die passenden Bilddateien in einem Verzeichnis unterhalb des Datenbankverzeichnisses oder im Datenbankverzeichnis selbst (wie in diesem Fall) speichern, reicht der pure Dateiname des Bildes. Die Routine weist dem Parameter image einen Objektverweis auf ein mit der Funktion LoadPictureGDIP geladenes Bild zu. Die Funktion LoadPictureGDIP stammt aus dem in Kapitel 11, »Bilder und binäre Dateien« vorgestellten Standardmodul mdlOGL2007. In dem Kapitel haben Sie auch erfahren, wie Sie Bilder in einem OLE-Feld speichern und daraus ein StdPicture-Objekt erzeugen, das Sie direkt ohne Umweg über das Dateisystem für die Anzeige in einem Steuerelement eines Ribbons verwenden können. Das Ergebnis sieht schließlich wie in Abbildung 12.11 aus – eine Schaltfläche mit einem benutzerdefinierten Symbol.
Abbildung 12.11: Eine Schaltfläche mit einem benutzerdefinierten und auf der Festplatte befindlichen Symbol
648
Ribbon
Ribbon-Schaltfläche anpassen Anhand der soeben erzeugten Schaltfläche lassen sich leicht die weiteren Attribute von Schaltflächen und sonstigen Steuerelementen erklären.
Größe der Schaltfläche Die Größe der Schaltfläche passen Sie über das Attribut size des button-Elements an. Es stehen die Werte normal (Standard) und large zur Verfügung. Die obige Schaltfläche sieht größenmäßig wie in Abbildung 12.12 aus.
Abbildung 12.12: Eine Schaltfläche in der Ausführung »large«
Hilfetexte Die Attribute screentip und supertip sind das (erweiterte) Pendant zum Attribut Control TipText. Erweitert deshalb, weil Sie mit den beiden Attributen eine Überschrift und einen zusätzlichen Text angeben können. Das mit folgender Zeile definierte SchaltflächenElement sieht wie in Abbildung 12.13 aus:
Abbildung 12.13: Eine Schaltfläche mit screentip- (Überschrift) und supertip-Attribut (Text)
Zeilenumbrüche innerhalb des Textes für das Attribut supertip markiert die Zeichenfolge:
649
Kapitel 12
12.6.2 Kontrollkästchen (checkBox) Kontrollkästchen bestehen aus einer Beschriftung und dem eigentlichen Kontroll kästchen. Zusätzliche Symbole sind eher selten. Optisch wirkt es am besten, wenn man Kontrollkästchen zwischen Schaltflächen mit Symbolen in der Größe »normal« einreiht (siehe Abbildung 12.14).
Abbildung 12.14: Beispiel für ein Kontrollkästchen
Wert des Kontrollkästchens abfragen Was hilft ein Kontrollkästchen in einer Menüleiste, wenn man dessen Wert nicht programmatisch abfragen kann? Also bauen Sie eine passende Funktion ein. Die Definition des Kontrollkästchens erweitern Sie um das onAction-Attribut:
Die aufgerufene Funktion heißt fctKontrollkaestchen. Wenn Sie nun eine Funktion wie weiter oben für eine Schaltfläche verwenden, erleiden Sie Schiffbruch: Die für ein Kontrollkästchen notwendige Funktion hat eine etwas andere Syntax, die einen zusätzlichen Parameter für den Wert des Kontrollkästchens enthält. Ein Beispiel sieht wie folgt aus: Public Function fctKontrollkaestchen(ctl As IRibbonControl, _ pressed As Boolean) MsgBox "Das Kontrollkästchen '" & ctl.Id & "' hat den Wert '" _ & pressed & "'." End Function Listing 12.8: Auswertung des Wertes eines Kontrollkästchens
12.6.3 Textfelder Textfelder heißen im Ribbon-Slang editBox-Steuerelement. Eine Besonderheit einer solchen Steuerelements ist wie bei den Kontrollkästchen wiederum die Syntax der VBAFunktion, die Sie durch Eingeben des Textes und Bestätigung per Eingabetaste oder
650
Ribbon
Verschieben des Fokus auf ein anderes Element der Benutzeroberfläche auslösen. Die Definition einer einfachen EditBox im XML-Dokument erfolgt wie beim Kontrollkäst chen: <editBox id="txtBeispielEditBox" label="Beispieltextfeld" onChange="fctTextfeld" />'
Lediglich die Routine zum Auswerten der Eingabe verwendet einen anderen, zweiten Parameter: Public Sub fctTextfeld(control As IRibbonControl, text As String) MsgBox "Das Textfeld '" & control.Id & "' hat den Wert '" & text & "'" End Sub Listing 12.9: Diese Routine wird durch das Ereignis onChange einer EditBox ausgelöst und zeigt den Namen des Steuerelements sowie den enthaltenen Text an
Das Aussehen des oben definierten Textfeldes sowie die Meldung, die Listing 12.9 verursacht, zeigt Abbildung 12.15.
Abbildung 12.15: Ein Ribbon mit Textfeld und einer Meldung, die nach dem Aktualisieren des Textfelds angezeigt wird
Weitere wichtige Attribute: maxLength: Anzahl Zeichen, die der Benutzer in das Textfeld eingeben kann;, der maximale Wert für dieses Attribut beträgt 1024. sizeString: Eine Zeichenkette zum Festlegen der Breite des Steuerelements. Die folgende Beispieldefinition setzt die beiden genannten Attribute ein und sorgt somit dafür, dass unter den gegebenen Bedingungen die Zeichenfolge »André Minhorst« genau in das Textfeld passt. <editBox maxLength="50" sizeString="André Minhorst" id="txtBeispielEditBox" label="Beispieltextfeld" onChange="fctTextfeld" />
Das Resultat zeigt Abbildung 12.16.
651
Kapitel 12
Abbildung 12.16: Die EditBox meldet das Überschreiten der zulässigen Zeichenanzahl
12.6.4 Kombinationsfelder I: Das comboBox-Element Es gibt in Ribbons zwei Typen von Kombinationsfeldern: comboBox und dropDown. Beide haben Eigenschaften, die Sie vermutlich gerne in einem vereint sehen würden – warum, erfahren Sie in den folgenden Abschnitten. Zunächst lernen Sie dabei das comboBox-Element kennen. comboBox-Kombinationsfelder in Ribbons bieten wie ihre Formular-Pendants einen oder mehrere Werte zur Auswahl an. Das schreit natürlich nach dem dynamischen Füllen per VBA, zunächst aber soll ein einfaches Beispiel den grundlegenden Aufbau veranschaulichen. <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" loadImage="LoadImage" onLoad="OnLoad"> Listing 12.10: Diese Ribbon-Definition erzeugt ein tab-Element mit einem Kombinationsfeld
Der Aufbau ähnelt dem eines Kombinationsfeldes in HTML. Das comboBox-Element schließt die enthaltenen item-Elemente ein, die Informationen über die zur Auswahl stehenden Einträge enthalten. Das obige XML-Dokument führt zum Ribbon in Abbil dung 12.17.
652
Ribbon
Abbildung 12.17: Ein Ribbon mit einer zweiten Gruppe und einem einfachen Kombinationsfeld
Kombinationsfeld dynamisch füllen Das dynamische Füllen eines einfachen comboBox-Elements erfolgt durch drei CallbackFunktionen. Die erste ermittelt die Anzahl der einzufügenden Einträge, die zweite die IDs und die dritte die Beschriftungen der Einträge. Der XML-Code für das Ribbon sieht wie im folgenden Listing aus, die für den Aufruf der Callback-Funktionen verantwortlichen Attribute sind fett markiert: <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui"> Listing 12.11: Ribbon-XML-Dokument für das dynamische Füllen eines Kombinationsfeldes
Die drei VBA-Callback-Funktionen sehen wie im folgenden Listing aus. Die Routine GetItemCount liefert die Anzahl der Einträge zurück, in diesem kleinen Beispiel sind dies drei. GetItemID ermittelt die IDs der anzuzeigenden Einträge, die hier aus dem Text »Testindex« und dem Wert des vom Ribbon gelieferten Index zusammengestellt werden. Fehlt noch die Beschriftung, die in GetItemLabel auf ähnliche Weise wie in der Routine GetItemID entsteht. Das resultierende Kombinationsfeld sieht im ausgeklappten Zustand schließlich wie in Abbildung 12.18 aus.
653
Kapitel 12 Public Sub GetItemCount(control As IRibbonControl, ByRef count) count = 3 End Sub Public Sub GetItemID(control As IRibbonControl, index As Integer, ByRef id) id = "Testindex" & index End Sub Public Sub GetItemLabel(control As IRibbonControl, index As Integer, _ ByRef label) label = "Testlabel" & index End Sub Listing 12.12: Callback-Funktionen zum Füllen eines einfachen Kombinationsfeldes
Abbildung 12.18: Kombinationsfeld mit dynamisch zusammengestelltem Inhalt
Im wirklichen Leben füllen Sie Steuerelemente wie Kombinationsfelder natürlich meist mit Daten aus Tabellen. Dies ist am einfachsten, wenn Sie den Inhalt der Tabelle direkt beim Ermitteln der Anzahl der einzufügenden Datensätze in einem Array zwischenspeichern. Auf dieses können Sie dann einfach über dessen Index zugreifen. Die VBARoutinen für eine solche Konstellation sehen so aus: Public Sub GetItemCount(control As IRibbonControl, ByRef count) Dim db As DAO.Database Dim rst As DAO.Recordset Dim i As Integer Set db = CurrentDb Set rst = db.OpenRecordset("SELECT KontaktID, Nachname & ', ' & " _ & "Vorname AS Kontakt FROM tblKontakte", dbOpenDynaset) Do While Not rst.EOF ReDim Preserve strKontakte(2, i + 1) As String strKontakte(0, i) = rst!KontaktID strKontakte(1, i) = rst!Kontakt rst.MoveNext i = i + 1 Loop count = i rst.Close Set rst = Nothing
654
Ribbon Set db = Nothing End Sub Public Sub GetItemID(control As IRibbonControl, index As Integer, ByRef id) id = strKontakte(0, index) End Sub Public Sub GetItemLabel(control As IRibbonControl, index As Integer, _ ByRef label) label = strKontakte(1, index) End Sub Listing 12.13: Diese Callback-Routinen füllen ein Kombinationsfeld mit den Daten einer Tabelle
Weitere Features von Ribbon-Kombinationsfeldern Gegenüber herkömmlichen Access-Kombinationsfeldern hat das comboBox-Element in Ribbons einen sehr interessanten Vorteil: Sie können damit jedem Eintrag ein Symbol zuweisen (siehe Abbildung 12.19).
Abbildung 12.19: Ein comboBox-Steuerelement mit Symbolen
Und dies funktioniert ganz einfach – in diesem Fall für vier Bilddateien, die unter den Namen 1.png, 2.png, 3.png und 4.png im gleichen Verzeichnis wie die Datenbank gespeichert sind. Um obige Ansicht zu erhalten, müssen Sie dem comboBox-Element lediglich das Attribut getItemImage mit dem Namen der Callback-Funktion GetItemImage zuweisen. Im gleichen Zug stellen Sie dann auch noch das Attribut onChange auf den Wert OnChange ein – gleich mehr dazu:
655
Die folgende Routine gibt schließlich für jedes Element des Kombinationsfeldes eine Ob jektvariable mit einem Verweis auf eine Bilddatei zurück. Für das Einlesen der Bilddatei ist wiederum die Routine LoadPicturePlus verantwortlich – siehe auch Kapitel 11, »Bilder und binäre Dateien«. Public Sub GetItemImage(control As IRibbonControl, index As Integer, _ ByRef image) Set image = LoadPicturePlus(CurrentProject.Path & "\" & index + 1 _ & ".png") End Sub Listing 12.14: Einlesen von Bilddateien für die Einträge eines comboBox-Elements per CallbackRoutine
Das Attribut OnChange sorgt für den Aufruf der folgenden Routine. Diese wird nach jeder Neuauswahl eines Eintrags des comboBox-Elements aufgerufen und gibt lediglich den Inhalt des ausgewählten Eintrags aus, den das Ribbon mit dem Parameter text übergibt. Public Sub OnChange(control As IRibbonControl, text As String) MsgBox "Sie haben den Eintrag '" & text & "' ausgewählt." End Sub Listing 12.15: Ausgabe des aktuellen Eintrags des comboBox-Steuerelements
Genau wie bei den Menüs von Access 2003 und älter kann man hier den Nachteil ausmachen, dass man nicht die ID des ausgewählten Eintrags auswerten kann; diese muss man in Abhängigkeit des angezeigten Wertes selbst ermitteln, denn die Eigenschaft control.id der OnChange-Prozedur liefert immer die ID des comboBox-Steuerelements.
12.6.5 Kombinationsfelder II: Das dropDown-Element Warum gibt es zwei Typen von Kombinationsfeldern und welche Unterschiede weisen diese auf? Einen genauen Einblick gibt Tabelle 12.4 – dort finden Sie einen Vergleich der Attribute der beiden Kandidaten. Die wesentlichen Unterschiede sind, dass das comboBox-Element ein onChange-Attribut und das dropDown-Element dafür die Attribute onAction, getSelectedItemID und getSelectedItemIndex enthält. Welche Vor- und Nachteile bringt das nun? Zwischen den beiden Ereigniseigenschaften onChange und onAction an sich fällt jedenfalls kein Unterschied auf; beide werden beim Auswählen eines anderen Eintrags ausgelöst. Den Unterschied entdeckt man
656
Ribbon
erst, wenn man sich die Syntax der passenden Callback-Routinen anschaut (siehe Tabelle 12.3): Während die onChange-Variante nur das Attribut label des ausgewählten Eintrags zurückliefert, wartet onAction mit den Attributen index und id auf. Das spart natürlich Arbeit, wenn Sie ein Kombinationsfeld dynamisch mit Daten aus einer Tabelle bestücken und auf Basis der getroffenen Auswahl etwas mit den dahinter stehenden Datensätzen anfangen möchten. Die weiteren Unterschiede beschränken sich auf zwei weitere zusätzliche Attribute des dropDown-Elements namens getSelectedItemID und getSelectedItemIndex. Beide dienen dazu, beim Anlegen des Steuerelements einen Eintrag vorauszuwählen – entweder auf Basis der ID oder des Index des Eintrags. Das folgende Beispielelement enthält die Attribute onAction und getSelectedItemIndex:
Die beiden Routinen, die bei onAction beziehungsweise getSelectedItemIndex ausgelöst werden, sehen wie folgt aus: Public Sub OnAction(control As IRibbonControl, selectedId As String, _ selectedIndex As Integer) MsgBox "Der ausgewählte Eintrag hat die ID '" & selectedId _ & "' und den Index '" & selectedIndex & "'." End Sub Listing 12.16: Anzeigen der ID und des Index eines neu ausgewählten Eintrags eines dropDownSteuerelements
Public Sub GetSelectedItemIndex(control As IRibbonControl, ByRef index) index = 3 End Sub Listing 12.17: Festlegen eines Eintrags eines dropDown-Elements per Index-Wert (der Index ist in diesem Fall immer nullbasiert)
Sie können die Neuabfrage der Vorauswahl mit GetSelectedItemIndex übrigens auch zur Laufzeit mit der Methode invalidateControl erzwingen (siehe auch Abschnitt 12.9.1).
comboBox oder dropDown? Diese Entscheidung ist leicht zu fällen: Bezieht das Kombinationsfeld seine Daten aus einer Tabelle und sollen Aktionen auf Basis dieser Einträge in Zusammenhang mit den
657
Kapitel 12
zugrunde liegenden Daten ausgeführt werden oder ist eine automatische Voreinstellung notwendig, ist das dropDown-Element erste Wahl. Sollen einfach nur Texte zur Auswahl stehen und diese nach der Auswahl weiterverarbeitet werden, setzen Sie das comboBoxSteuerelement ein.
12.6.6 Umschaltflächen Mit Umschaltflächen lassen sich Stati festlegen – etwa, um Funktionen zu aktivieren oder zu deaktivieren (aktivierte Umschaltflächen sind standardmäßig orange). In einem Ribbon sieht die Definition eines einfachen toggleButton-Elements so aus:
Die passende Routine für das Attribut onAction stellt sich wie folgt dar: Public Sub OnToggleAction(control As IRibbonControl, pressed As Boolean) If pressed = True Then MsgBox "Funktion aktiviert" Else MsgBox "Funktion deaktiviert" End If cancelDefault = False End Sub Listing 12.18: Beim Klick auf ein toggleButton-Element wertet diese Routine den Parameter pressed aus
Möglicherweise fällt Ihnen auf, dass hier nicht einfach die großgeschriebene Variante des Attributs als Prozedurname herhält. Das ist zwingend notwendig, wenn verschiedenartige Steuerelemente die gleiche Callback-Funktion aufrufen, die Syntax sich aber unterscheidet. In diesem Fall verwendet man einfach unterschiedliche Funktionsnamen, die etwa den Namen oder die Bezeichnung des Steuerelements enthalten. Den Status des toggleButtonSteuerelements legt die folgende Routine fest. Sie weist dem Parameter returnvalue entweder den Wert True oder False zu, wobei True dem gedrückten Zustand entspricht. Public Sub GetPressed(control As IRibbonControl, ByRef returnvalue) returnvalue = True End Sub Listing 12.19: Ob ein toggleButton-Element beim Anlegen gedrückt oder nicht gedrückt ist, legt die durch getPressed ausgelöste Routine fest
658
Ribbon
12.6.7 Galerien Noch mehr Komfort als die beiden Kombinationsfeldtypen liefert das gallery-Element. Es kann verschiedene Einträge mit Symbolen anzeigen und das auch noch zweidimensional. Ein ganz einfaches Beispiel sieht wie in Abbildung 12.20 aus, den passenden XML-Code für dieses Element finden Sie hier:
An diesem Beispiel erkennen Sie schnell, dass Access die Elemente zuerst von links nach rechts und dann von oben nach unten anordnet. Natürlich hat das gallery-Element noch einiges mehr zu bieten – etwa das Einfügen von Bilddateien wie in Abbildung 12.21.
Abbildung 12.20: Eine Galerie mit vier einfachen Einträgen
Abbildung 12.21: Eine Galerie mit Symbolen
Dazu ist der XML-Code aus folgendem Listing notwendig, wobei Sie für das Element customUI noch das Attribut loadImage so einstellen müssen, dass dieses eine passende Routine zum Laden der in den einzelnen Items angegebenen Bilder aufruft.
659
Kapitel 12 <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" loadImage="LoadImage"> ... Listing 12.20: Dieses gallery-Element zeigt acht Items mit passenden Symbolen an
In diesem Fall heißt die Routine LoadImageGallery und sieht wie folgt aus: Public Sub LoadImageGallery(control, ByRef image) Set image = LoadPicturePlus(CurrentProject.Path & "\Getraenke\" _ & control) End Sub Listing 12.21: Die Callback-Routine LoadImageGallery ist für das Laden von Bildern aus einem Unterverzeichnis des Anwendungsverzeichnisses verantwortlichs
Besonderheiten des gallery-Elements Das gallery-Element ist insbesondere mit den Kombinationsfeld-Steuerelementen comboBox und dropDown zu vergleichen. Der erste Vorteil ist die Möglichkeit, Elemente in mehreren Spalten und Zeilen anzuzeigen, der zweite wäre, wenn es denn funktionieren würde, das Attribut invalidateContentOnDrop. Dieses verspricht laut customUI.xsd das Ausführen von Callback-Funktionen beim Auf klappen der Galerie, was aber zum Zeitpunkt der Erstellung dieses Kapitels nicht möglich war. Weitere interessante Attribute sind folgende: itemHeight: Gibt die Höhe des item-Elements an. itemWidth: Gibt die Breite des item-Elements an. Die Ereigniseigenschaft zum Festlegen einer Callback-Funktion, die beim Klicken auf eines der Elemente ausgelöst wird, heißt beim gallery-Element onAction und hat folgende Syntax:
660
Ribbon Sub OnAction(control As IRibbonControl, selectedId As String, selectedIndex As Integer)
12.6.8 Menüs (menu) Menüs ähneln den von älteren Office-Versionen bekannten Untermenüs der Menüleiste. Dabei kann man einige unterschiedliche Steuerelemente anlegen, angezeigt werden allerdings stets nur normale Menüeinträge. Das folgende Ribbon-XML-Dokument erzeugt beispielsweise das Menü aus Abbildung 12.22. <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" loadImage="LoadImage" onLoad="OnLoad"> <menu id="mnuBeispielmenue" label="Beispielmenü"> Listing 12.22: Beispiel für ein Menü, dessen verschiedenartige Steuerelemente jedoch alle in einem Standardmenüeintrag enden
Abbildung 12.22: Das menu-Steuerelement mit einigen Untersteuerelementen
Funktionen beschreiben mit dem description-Attribut Das menu-Element ist eines der wenigen, in denen sich das description-Attribut für Steuer elemente bemerkbar macht. In üblichen button-Elementen zeigt Access diese einfach
661
Kapitel 12
nicht an – in button- und weiteren Elementen innerhalb eines menu-Elements hingegen schon. Wie das aussehen kann, zeigt Abbildung 12.23. Der passende XML-Code sieht wie folgt aus: <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" loadI mage="mnuSportartAuswaehlen_LoadImage" onLoad="OnLoad"> <menu id="mnuSportartAuswaehlen" label="Sportart auswählen" itemSize="large"> ... Listing 12.23: Diese XML-Definition Beschreibungen versieht
zeigt
beispielhaft,
wie
man
Menüelemente
mit
Wichtig ist hier, dass Sie itemSize des menu-Elements auf large einstellen. Wenn Sie eine optische Trennung zwischen zwei Einträgen vornehmen möchten, setzen Sie das menuSeparator-Element ein. Sie können es mit oder ohne Text verwenden, wobei Sie einen Text für das Attribut title eintragen. Folgender Codeschnipsel verursacht beispielsweise die beiden Trenner aus Abbildung 12.24: <menu id="mnu" label="Menü"> <menuSeparator id="mns" title="Ich bin ein menuSeparator"/> <menuSeparator id="mns1" title="Ich auch."/>
662
Ribbon
Abbildung 12.23: Im menu-Steuerelement lassen sich neben Symbolen auch Beschreibungstexte zu einem Steuerelement anzeigen
Abbildung 12.24: Ein Menü mit zwei textbehafteten Trennlinien
12.6.9 Splitbuttons (splitButton) Ein splitButton-Steuerelement ist ein kombiniertes Schaltflächen/Menü-Steuerelement, wie es auch etwa im Start-Ribbon für die Auswahl der Ansicht verwendet wird. Dabei kann man auf das Steuerelement selbst oder auf einen der Menüeinträge klicken, um eine bestimmte Aktion auszuführen. Dementsprechend sieht das XML-Konstrukt für die Realisierung eines solchen SplitButtons aus: Es besteht aus einem splitButton-Element, das ein button- oder toggleButtonElement und ein menu-Element umschließt. Der XML-Code aus dem folgenden Listing veranschaulicht die für das SplitButton-Steuerelement aus Abbildung 12.25 notwendigen Elemente: <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" loadImage="LoadImageSplitMenu" onLoad="OnLoad">
663
Kapitel 12 <splitButton id="spbSplitButton" size="large"> <menu id="mnuMenu" label="Beispielmenü" itemSize="large"> Listing 12.24: Dieses XML-Dokument erzeugt das splitButton-Element aus Abbildung 5.25
Abbildung 12.25: Das splitButton-Steuerelement zeigt ständig eine Schaltfläche und bietet andere zur Auswahl an
12.6.10 Gruppendialog anzeigen Einige eingebaute Tabs enthalten Gruppen, deren Beschriftungsfeld im rechten Bereich einen kleinen Pfeil anzeigt, mit dem man weiterführende Dialoge öffnen oder beliebigen
664
Ribbon
anderen Code aufrufen kann. Dies realisieren Sie mit dem dialogBoxLauncher-Element. Der folgende Codeschnipsel zeigt, wie es funktioniert:
Heraus kommt dabei die kleine Schaltfläche unten rechts in Abbildung 12.26. Den Code zum Aufrufen des entsprechenden Dialogs – etwa eines Formulars – bringen Sie in einer Routine namens DialogOeffnen unter, die die gleiche Prozedurdeklaration wie die OnAction-Callbackfunktion von Schaltflächen hat.
Abbildung 12.26: Eine Gruppe mit einem dialogBoxLauncher-Element
12.6.11 Trennstrich (separator) Das separator-Element zwischen zwei weiteren Elementen sorgt für die Anzeige eines Trennstriches. Der folgende Codeschnipsel sorgt für die Anzeige der beiden Beschriftungen und der Trennlinie aus Abbildung 12.27 <separator id="Separator"/>
Abbildung 12.27: Beispiel für einen Trennstrich (separator)
12.7 Weitere Anpassungen des Ribbons Die folgenden Abschnitte zeigen, wie Sie das Ribbon feintunen – etwa durch Hinzufügen passender Tastenkombinationen.
665
Kapitel 12
12.7.1 Tastenkombinationen Genau wie bei älteren Access-Versionen können Sie auch in Access 2007 Tastenkombina tionen verwenden, um Menübefehle aufzurufen. Früher fügte man dazu ein KaufmannsUnd (&) vor dem Buchstaben der Beschriftung ein, der zum Aktivieren des jeweiligen Elements dienen sollte. Heute ist alles anders: Ribbon-Elemente blenden Buchstaben ein, sobald Sie auf die Alt-Taste klicken. Von da an können Sie durch Betätigen der zu den angezeigten Buchstaben passenden Tasten Aktionen wie das Einblenden eines Ribbon, das Auslösen eines Steuerelements und mehr bewirken (mehr dazu in Kapitel 1, »Warum Access 2007?«). Natürlich können Sie auch für benutzerdefinierte Elemente diese so genannten Keytips festlegen: Dazu verwenden Sie das Attribut keytip, das einen Wert bestehend aus ein bis drei Buchstaben oder Zahlen erwartet. Wenn Sie keinen Wert für das Attribut keytip angeben, verwendet Access vorgegebene Werte wie Y für ein benutzerdefiniertes Ribbon und Y1, Y2, ... für die darin enthaltenen Steuerelemente. Fügen Sie einmal wie im folgenden Code eine Tastenkombination für das Ribbon selbst und für die oben erstellte Schaltfläche hinzu (fett markiert): <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" loadImage="LoadImage" onLoad="OnLoad"> Listing 12.25: Die fett gedruckten Stellen zeigen Zuweisungen von Tastenkombinationen zu verschiedenen Elementen
Access zeigt dann nach Betätigen der Alt-Taste die für das tab-Element definierte Tastenkombination an (siehe Abbildung 12.28). Nach Eingabe der Buchstaben KB aktiviert Access das gewünschte Ribbon und blendet auch hier die Tastenkombination ein. Gibt man diese ein, führt Access die Funktion der so »gedrückten« Schaltfläche aus (siehe Abbildung 12.29).
666
Ribbon
Abbildung 12.28: Nach dem Betätigen der Alt-Taste erscheinen zunächst die Tastenkombinationen für die Auswahl eines Ribbons ...
Abbildung 12.29: ... und Access aktiviert das per Tastenkombination ausgewählte Ribbon und zeigt dessen Tastenkombinationen an. Die Eingabe von »BS« hätte nun die gleiche Wirkung wie ein Mausklick auf die Schaltfläche.
12.7.2 Alle Ribbons ausblenden Wenn Sie vor dem Anlegen eines benutzerdefinierten Ribbons für eine eigene Anwendung alle anderen Ribbons ausblenden möchten, setzen Sie das Attribut startFromScratch des ribbon-Elements auf den Wert true:
Access zeigt nun nur noch das in den Access-Optionen unter Aktuelle Datenbank|Multi funktionsleisten- und Symbolleistenoptionen|Name der Multifunktionsleiste angegebene Ribbon an. Zusätzlich hat Access auch noch das Office-Menü ausgedünnt: Dort finden Sie nur noch die Schaltflächen zum Anlegen einer neuen Datenbank, zum Öffnen einer bestehenden Datenbank, zum Speichern und zum Schließen. Allerdings lassen sich noch die Datenbankoptionen anzeigen (siehe Abbildung 12.30).
12.7.3 Ribbon-Leiste minimieren Die Leiste mit den Tabs können Sie auch minimieren. Dazu wählen Sie aus dem Kontext menü des Ribbons oder über den Anpassungspfeil der Schnellstartleiste den Eintrag Multifunktionsleiste minimieren aus. Die Tabs blenden sich dann erst nach einem Klick auf
667
Kapitel 12
die Registerreiter ein. Sie sparen damit eine Menge Raum, den Sie besser für Formulare und Berichte verwenden können.
Abbildung 12.30: Das Setzen von startFromScratch auf true führt zum Eindampfen des OfficeMenüs
12.7.4 Ein tab-Element ein- und ausblenden Möglicherweise möchten Sie ein einzelnes tab-Element ein- oder ausblenden. In diesem Fall fügen Sie dem XML-Dokument ein einzelnes tab-Element hinzu, das den Namen des betroffenen Elements und den Sichtbarkeitsstatus enthält. Das folgende Element blendet das tab-Element Anpassen aus, wenn Sie es in die tabsAuflistung einfügen:
Hier wie auch beim Anpassen von Gruppen stellen Sie sich vielleicht die Frage, wie Sie an all die Konstanten für das Attribut idMso kommen sollen. Eine Übersicht darüber finden Sie in einer Beispieldatenbank, die eine Tabelle namens tblAccessRibbonControls mit allen Ribbon-Elementen von Access enthält [3].
12.7.5 Eine Gruppe ein- und ausblenden Das funktioniert natürlich auch mit einer Gruppe. Mit folgendem Ribbon-XMLDokument sorgen Sie etwa dafür, dass die Gruppe zum Wechseln der Ansicht aus dem Start-Ribbon ausgeblendet wird: <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" loadImage="LoadImageSplitMenu" onLoad="OnLoad">
668
Ribbon Listing 12.26: Wenn man den Namen einer Gruppe und des übergeordneten Tabs kennt, kann man diese mit dem visible-Attribut ausblenden
Die Tabs wie auch andere Steuerelemente oder Gruppen können Sie dynamisch per Code ein- und ausblenden, wenn Sie eine Callback-Funktion und das Attribut getVisible einsetzen:
Die Sichtbarkeit wird dann in der Callback-Funktion gesteuert, deren Abfrage Sie mit der Methode InvalidateControl (siehe Abschnitt 12.9.1) jederzeit erzwingen können.
12.7.6 Ein Steuerelement ein- und ausblenden Schließlich lässt sich das Ein- und Ausblenden bis hin zum einzelnen Steuerelement fortsetzen – aber nur für benutzerdefinierte Elemente. Bei den eingebauten Ribbons ist mit dem Anpassen von group-Elementen Schluss; Steuerelemente lassen sich nur in komplett selbst erstellten Ribbons ein- und ausblenden. Dazu fügen Sie einfach das Attribut visible hinzu und geben ihm den Wert »false«.
12.7.7 Eingebaute Steuerelemente aktivieren und deaktivieren Das Aktivieren und Deaktivieren eingebauter Steuerelemente erfolgt ebenfalls im Rib bon-XML-Dokument, allerdings nicht unterhalb des ribbon-Elements. Hierfür gibt es unterhalb des customUI-Elements eine eigene Auflistung namens commands. Die Elemen te heißen passend command und werden anhand des Attributs idMso eindeutig vorhan denen Steuerelementen zugeordnet. Der folgende Beispielcode zeigt, wie Sie etwa die Filterschaltfläche deaktivieren: <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui">
Die msoID entnehmen Sie der in [3] enthaltenen Tabelle der Steuerelemente. Offensichtlich arbeitet diese Eigenschaft aber nicht konsistent, denn etwa die Suchen-Schaltfläche ließ sich nicht aktivieren; Access lieferte aber für folgende Zeile auch keinen Fehler:
669
Kapitel 12
12.7.8 Eingebaute Steuerelemente mit neuen Funktionen belegen Sie können die eingebauten Steuerelemente (allerdings nur button-, toggleButton- und checkBox-Elemente) mit eigenen Funktionen versehen. Die folgende Definition sorgt etwa dafür, dass ein Klick auf die Filtern-Schaltfläche die VBA-Routine OnFilternAction auslöst. <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui">
12.7.9 Sonderzeichen in Ribbon-Texten Wenn Sie in Attribute wie label, description, screenTip oder superTip Sonderzeichen einfügen möchten, müssen Sie die in XML-Dokumenten übliche Schreibweise verwenden. Dazu suchen Sie sich den ASCII-Wert des Sonderzeichens heraus und verpacken diesen – etwa für einen Zeilenumbruch (ASCII-Wert 13) – wie folgt:
. Falls Sie einmal einen Teil des auszugebenden Textes in Anführungszeichen setzen möchten, verwenden Sie keine doppelten Anführungszeichen wie in VBA-Zeichenketten, sondern die Zeichenfolge ".
12.7.10 Einen Eintrag zum Office-Menü hinzufügen Wenn Sie den Benutzern Ihrer Anwendung auch das Office-Menü als Ausgangspunkt für den Aufruf von Funktionen anbieten möchten, können Sie auch dort Einträge hinzufügen. Der passende Bereich ist direkt unterhalb des ribbon-Elements, also auf der gleichen Ebene wie die tabs-Auflistung zu finden und heißt officeMenu. Unterhalb dieses Elements können Sie verschiedene Steuerelemente wie im folgenden Beispiel anlegen. Wenn das Steuerelement an einer bestimmten Position eingefügt werden soll, können Sie mit dem Attribut insertAfterMso oder insertBeforeMso festlegen, vor oder nach welchem eingebauten Steuerelement das neue Element Platz finden soll. Natürlich können Sie auch hier Symbole hinzufügen (siehe Abbildung 12.31). <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" loadImage="LoadImage">
670
Ribbon
Abbildung 12.31: Ein neuer Eintrag im Office-Menü
12.7.11 Einträge des Office-Menüs ausblenden Die Einträge des Office-Menüs wie Neu, Öffnen, Speichern und so weiter möchten Sie den Benutzern Ihrer Anwendung unter Umständen nicht zur Verfügung stellen, und ganz sicher nicht den Dialog mit den Access-Optionen. Die einzelnen Einträge des Office-Menüs lassen sich mit XML-Definitionen wie folgt ausblenden: <splitButton idMso="FileSaveAsMenuAccess" visible="false"/>
Wenn Sie alle Elemente inklusive der Schaltfläche Access-Optionen entfernen möchten, stellen Sie dies in eben jenem Optionen-Dialog ein. Der richtige Bereich heißt logischerweise Aktuelle Datenbank und die passende Option lautet Vollständige Menüs zulassen (siehe Abbildung 12.32). Wenn Sie diese Option deaktivieren, sieht das Office-Menü wie in Abbildung 12.33 aus. Der Bereich Zuletzt verwendete Dokumente lässt sich übrigens nicht ausblenden. Wenn Sie die Optionen einer auf diese Weise angepassten Datenbank wieder einblenden möchten, müssen Sie die Datenbank bei gedrückter Umschalt-Taste öffnen – Access setzt die Einstellungen unter Aktuelle Datenbank dann außer Kraft.
671
Kapitel 12
Abbildung 12.32: Der Optionen-Dialog bietet unter Benutzeroberfläche einer Access-Anwendung anzupassen
anderem
die
Möglichkeit,
die
Abbildung 12.33: Das Deaktivieren der Option »Vollständige Menüs zulassen« sorgt für ein auf das Notwendigste reduziertes Office-Menü
12.7.12 Einen Eintrag zur Schnellzugriffsleiste hinzufügen Wenn Sie der Schnellzugriffsleiste (Quick Access Toolbar, QAT) eine benutzerdefinierte Schaltfläche hinzufügen möchten, müssen Sie zunächst das Attribut startFromScratch auf true einstellen und damit alle eingebauten Ribbons ausblenden – anderenfalls funktioniert dies nicht. Anschließend verwenden Sie den unter dem Element ribbon befindlichen Bereich qat und fügen dort zunächst die Liste documentControls und dann die gewünschten Steuerelemente hinzu. Das folgende Beispiel sorgt etwa für die Anzeige der Schaltflächen aus Abbildung 12.34:
Abbildung 12.34: Die Schnellzugriffsleiste mit benutzerdefinierten Schaltflächen
12.8 Ribbons für Formulare und Berichte Genau wie in Access 2003 und älter können Sie in Access 2007 Formularen und Berichten Ribbon-Tabs zuweisen. Dazu weisen Sie einfach der Eigenschaft Name der Multifunktionsleiste eines der verfügbaren benutzerdefinierten Ribbons zu (siehe Abbildung 12.35). Zu beachten ist hier, dass das Zuweisen eines Ribbon zu Formularen, die als modaler Dialog geöffnet sind, keine Wirkung hat – das Ribbon wird einfach nicht angezeigt. Als modale Dialoge gelten dabei Formulare, deren Eigenschaft Popup den Wert Ja hat und/oder die mit DoCmd.OpenForm mit dem Parameter WindowMode:=acDialog geöffnet wurden. Für Berichte gilt das Gleiche, auch hier weisen Sie der Eigenschaft Name der Multifunktionsleiste den passenden Eintrag zu und modal geöffnete Berichte versagen die Anzeige der angegebenen Multifunktionsleiste. Auch in der Layoutansicht von Berichten zeigt sich eine angegebene Multifunktionsleiste nicht, wohl aber in der neuen Berichtsansicht.
12.9 XML-Dokument mit Application.LoadCustomUI laden Neben dem Speichern des Ribbon-XML-Dokuments in der Tabelle USysRibbons und dem Festlegen der beim Start einzulesenden Ribbon-Definition gibt es noch eine weitere Möglichkeit, Ribbons anzulegen.
673
Kapitel 12
Abbildung 12.35: Auswählen eines benutzerdefinierten Ribbons für ein Formular
Diese erfordert das Vorhandensein des XML-Dokuments in einem beliebigen Format – wie oben beschrieben im Memofeld der Tabelle USysRibbons oder einer anderen Tabelle gespeichert oder auch in Form einer externen Textdatei. Sie müssen nur sicherstellen, dass das XML-Dokument in einer String-Variablen gespeichert vorliegt. Anschließend können Sie dann die Methode LoadCustomUI des Application-Objekts verwenden, um ein Ribbon mit der angegebenen Definition zur Liste der verfügbaren Ribbons hinzuzufügen. Die folgende Routine liest zunächst mit der Funktion TextdateiLesen (siehe Listing 12.29) die in einer Datei gespeicherte Ribbon-Definition ein und weist diese dann per LoadCustomUI-Methode der Liste der benutzerdefinierten Ribbons der aktuellen Datenbank zu. Diese können Sie dann in den Access-Optionen unter Aktuelle Daten bank|Multifunktionsleisten- und Symbolleistenoptionen|Name der Multifunktionsleiste auswählen. Public Sub DynamischeZuweisungDatei() Dim strRibbon As String strRibbon = TextdateiLesen(CurrentProject.Path & "\Ribbon.xml") Application.LoadCustomUI "RibbonAusDatei", strRibbon End Sub Listing 12.28: Zuweisen einer Ribbon-Definition per LoadCustomUI
Public Function TextdateiLesen(strDateiname As String) Dim lngDatei As Long
674
Ribbon Dim strText As String lngDatei = FreeFile Open strDateiname For Input As lngDatei strText = Input$(LOF(1), lngDatei) Close lngDatei TextdateiLesen = strText End Function Listing 12.29: Funktion zum Einlesen von Textdateien
Anschließend können Sie die Ribbon-Definition jederzeit durch eine neu erstellte Text datei ersetzen – solange der Parameter CustomUiName der Methode LoadCustomUI gleich bleibt (hier RibbonAusDatei), aktualisiert sich das Ribbon dann automatisch.
12.9.1 Dynamisches Aktualisieren des Ribbons In bestimmten Situationen möchten Sie vielleicht das Aussehen des Ribbons beziehungsweise der enthaltenen Steuerelemente dynamisch verändern. Normalerweise erhält man keinen Zugriff auf die Ribbons wie es in früheren Versionen von Access bei den Symbolleisten per VBA möglich war. Statt dessen müssen Sie bereits beim Anlegen des Ribbons eine Objektvariable mit einem Verweis auf das Ribbon erstellen, über die Sie dann später auf das Ribbon zugreifen können. Dazu verwenden Sie wiederum eine Callback-Funktion, die Sie diesmal dem Attribut onLoad des customUI-Elements der Ribbon-Definition zuweisen. Das customUI-Element sieht dann so aus, wobei die beim Laden aufzurufende Funktion OnLoad heißt: <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" loadImage="LoadImage" onLoad="OnLoad">
In einen Standardmodul legen Sie dann zunächst eine globale Objektvariable an, die den Verweis auf das Ribbon speichern soll: Dim objRibbon As IRibbonUI
Die folgende Funktion ist schließlich für das Setzen des Objektverweises verantwortlich: Public Function OnLoad(ribbon As IRibbonUI) Set objRibbon = ribbon End Function Listing 12.30: Speichern eines per Parameter übergebenen Objektverweises
Ist der Verweis gesetzt, können Sie fortan über die Variable objRibbon auf das RibbonObjekt zugreifen und eine seiner beiden Methoden aufrufen (siehe Abbildung 12.36). Zu beachten ist, dass keine der Methoden die Ereignisse onLoad oder loadImages auslöst:
675
Kapitel 12
Invalidate: Initialisiert alle enthaltenen Steuerelemente neu. InvalidateControl: Initialisiert das als String-Parameter (ControlID) übergebene Steuerelement neu.
Abbildung 12.36: Der Objektverweis auf ein Ribbon liefert zwei Methoden
12.9.2 Beispiel: Abhängige Kontrollkästchen Die beiden Kontrollkästchen des folgenden Beispiels für die InvalidateControl-Methode sollen eine Abhängigkeit aufweisen, bei der das zweite Kontrollkästchen bei markiertem ersten Kontrollkästchen aktiv ist und umgekehrt (siehe Abbildung 12.37).
Abbildung 12.37: Das untere Kontrollkästchen ist aktiviert, wenn das obere Kontrollkästchen markiert ist und umgekehrt
Dazu ist zunächst eine passende XML-Definition des Ribbons erforderlich: <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" onLoad="OnLoad">
676
Ribbon onAction="chk1_OnAction" getPressed="chk1_GetPressed"/> Listing 12.31: Ribbon-XML-Dokument für abhängige Kombinationsfelder
Zunächst muss das customUI-Element die Ereigniseigenschaft onLoad aufweisen und damit die Routine aus folgendem Listing aufrufen. Sub OnLoad(ribbon As IRibbonUI) Set objRibbon = ribbon boolChk1 = True End Sub Listing 12.32: Setzen eines Objektverweises auf das Ribbon
Die beiden dort gefüllten Variablen objRibbon zum Speichern eines Verweises auf das Ribbon sowie boolChk1 zum Speichern des Anfangswerts des ersten Kontrollkästchens legen Sie der Einfachheit halber als öffentliche Variablen an: Public objRibbon As IRibbonUI Public boolChk1 As Boolean
Beim Anlegen des Ribbons ruft Access auch sämtliche in get...-Attributen angegebenen Routinen auf. Hier sorgt ein getPressed-Attribut dafür, dem ersten Kontrollkästchen den in der Variab len boolChk1 gespeicherten Wert zuzuweisen und ein getEnabled-Attribut aktiviert oder deaktiviert das zweite Kontrollkästchen je nach dem Inhalt von boolChk1: Sub chk1_GetPressed(control As IRibbonControl, ByRef pressed) pressed = boolChk1 End Sub Listing 12.33: Diese Routine setzt in Abhängigkeit von boolChk1 einen Haken in das erste Kontrollkästchen
Sub chk2_GetEnabled(control As IRibbonControl, ByRef enabled) enabled = boolChk1 End Sub Listing 12.34: Aktivieren oder deaktivieren des zweiten Kontrollkästchens in Abhängigkeit von boolChk1
677
Kapitel 12
Schließlich fehlt noch ein wenig Aktion – und dafür sorgt die Routine, die beim Ankli cken des ersten Kontrollkästchens ausgelöst wird. Sie sorgt dafür, dass boolChk1 den Wert des ersten Kontrollkästchens erhält und das zweite Kontrollkästchen aktualisiert wird – indem es mit der Methode InvalidateControl für den erneuten Aufruf der Methode GetEnabled sorgt. Sub chk1_OnAction(control As IRibbonControl, pressed As Boolean) boolChk1 = pressed objRibbon.InvalidateControl "chk2" End Sub Listing 12.35: Beim Klick auf das erste Kontrollkästchen werden der Inhalt von boolChk1 und das Steuerelement chk2 aktualisiert
Wenn Sie diese Technik verwenden, müssen Sie sicherstellen, dass die Objektvariable objRibbon ihren Inhalt nie verliert. Das kann aber der Fall sein, sobald es an beliebiger Stelle des VBA-Projekts zu einem unbehandelten Fehler kommt. Folglich müssen Sie den VBA-Code Ihrer Anwendung für einen reibungslosen Betrieb mit objRibbon komplett mit Fehlerbehandlungen ausstatten.
12.10 Menü- und Symbolleisten aus bestehenden Access 2003-Anwendungen Viele Access-Entwickler sorgen sich um die Kompatibilität der Menü- und Symbolleisten von Anwendungen, die sie mit Access 2003 oder älter erstellt haben. Die Sorge ist natürlich berechtigt, denn immerhin hat Microsoft die Menüstruktur komplett umgekrempelt. Für die Verwendung älterer benutzerdefinierter Menüs wurde allerdings ein Hintertürchen offen gelassen: Diese werden standardmäßig einfach in einem eigenen Ribbon namens Add-Ins in einem tab-Element mit der Beschriftung Benutzerdefinierte Symbolleisten angezeigt (siehe Abbildung 12.38). Wenn Sie eine Menüleiste und eine oder mehrere Symbolleisten gleichzeitig anzeigen, erfolgt dies auch im Ribbon AddIns – mit dem feinen Unterschied, dass Sie die Position der Symbolleisten nicht mehr verändern können. Das Verschieben an den linken, rechten oder unteren Rand oder das Platzieren im freien Raum ist also nicht mehr möglich. Davon abgesehen übernimmt Access 2007 alle menürelevanten Einstellungen, die Sie im Start-Dialog (Optionen|Start) vornehmen können – zum Beispiel: Datenbankfenster: blendet bei Deaktivierung den Navigationsbereich aus Statusleiste: blendet bei Deaktivierung die Statusleiste aus Unbeschränkte Menüs anzeigen: blendet bei Deaktivierung alle Einträge im OfficeMenü außer Datenbank schließen und Access beenden aus
678
Ribbon
Abbildung 12.38: Menüs von Anwendungen, die mit Access 2003 und älter erstellt wurden, erhalten ein eigenes Ribbon
Es gibt aber auch die Möglichkeit, in Access 2007 eine Menüleiste im alten Stil anzuzeigen. Dazu sind folgende Voraussetzungen erforderlich: Es handelt sich um eine Datenbank im Format von Access 2000 bis Access 2003. Die Datenbank enthält eine benutzerdefinierte Menüleiste und diese ist in den Startoptionen von Access 2003 und älter als Menüleiste eingetragen. Sie öffnen die Datenbank in Access 2007, deaktivieren dort in den Access-Optionen die Eigenschaft Aktuelle Datenbank|Navigation|Navigationsbereich anzeigen und stellen im Bereich Aktuelle Datenbank|Multifunktionsleisten- und Symbolleistenoptionen die Eigenschaft Menüleiste auf den Namen Ihrer Menüleiste und die Option Integrierte Symbolleisten zulassen auf Nein ein. Das Ergebnis sollte nun etwa wie in Abbildung 12.39 aussehen:
Abbildung 12.39: Eine .mdb-Datei in Access 2007 mit benutzerdefinierter Symbolleiste
Bedenken Sie, dass Sie mit einer solchen .mdb-Datei keine der vielen Neuerungen von Access 2007 nutzen können.
679
Kapitel 12
12.11 Übersicht über Ribbon-XML-Elemente und Attribute Die vorhergehenden Abschnitte haben Ihnen einen Überblick über die Techniken zum Anpassen des Ribbons gegeben. Damit Sie beim Bauen benutzerdefinierter Tabs alle möglichen Elemente im Überblick haben, finden Sie nachfolgend vier Tabellen mit folgendem Inhalt: Tabelle 12.1: Auflistung aller Elemente mit Beschreibung und möglichen KindElementen Tabelle 12.2: Auflistung aller Attribute mit Datentyp, Werten und Beschreibung Tabelle 12.3: Auflistung aller Ereigniseigenschaften und deren VBA-Syntax Tabelle 12.4: Zuordnung der Attribute und Ereigniseigenschaften zu den einzelnen Elementen
12.11.1 Auflistung der Ribbon-Elemente Die folgende Tabelle enthält alle möglichen Elemente mit Beschreibung möglicher KindElemente. Element
Element eines comboBox-, dropDown- oder gallerySteuerelements, wird beim dynamischen Zuweisen überschrieben
labelControl
Steuerelement mit einer Beschriftung
menu
Menü-Steuerelement
menuSeparator
Trennstrich zwischen Menüpunkten
button, toggleButton, checkBox, menu, gallery, splitButton, menuSeparator, dynamicMenu, control
Tabelle 10.1: Mögliche Elemente von Ribbon-XML-Dokumenten (Fortsetzung)
681
Kapitel 12 Element
Beschreibung
Mögliche Kind-Steuerelemente
officeMenu
Office-Menü-Element, kann um benutzerdefinierte Steuerelemente ergänzt werden
button, toggleButton, checkBox, menu, gallery, splitButton, menuSeparator, dynamicMenu, control
qat
Schnellzugriffsleiste (Quick Access Toolbar), kann nur bei startFromScratch="true" angepasst werden
sharedControls, documentControls
ribbon
Enthält alle Ribbon-Bestandteile
officeMenu, qat, contextualTabs, tabs
separator
Trennstrich zwischen Steuerelementen
sharedControls
Elemente, die für alle OfficeAnwendungen verfügbar sein sollen
button, separator, control
splitButton
Kombination aus Schaltfläche und Menü wie etwa das Steuerelement zum Einstellen der Objektansicht
button, toggleButton
tab
Eine Registerseite des Ribbon
group
tabs
Enthält die Registerseiten des Ribbon
tab
tabSet
Fasst tab-Elemente zusammen, die in bestimmten Kontexten sichtbar gemacht werden sollen
tab
toggleButton
Umschaltfläche
Tabelle 10.1: Mögliche Elemente von Ribbon-XML-Dokumenten (Fortsetzung)
12.11.2 Attribute der Ribbon-Elemente Die folgende Tabelle enthält eine Auflistung aller Attribute der Ribbon-Elemente. Eigenschaft
Datentyp
Werte
Beschreibung
boxStyle
String
horizontal|vertical
Legt fest, wie die Elemente innerhalb eines box-Elements angeordnet werden
columns
Long
Anzahl der Spalten in einem gallery-Objekt
description
String
Beschreibungstext, der in den Einträgen eines menu-Elements angezeigt wird, wenn die Eigenschaft itemSize auf large eingestellt ist
Tabelle 10.2: Eigenschaften von Ribbon-Elementen
682
Ribbon Eigenschaft
Datentyp
Werte
Beschreibung
boxStyle
String
horizontal|vertical
Legt fest, wie die Elemente innerhalb eines box-Elements angeordnet werden
columns
Long
Anzahl der Spalten in einem gallery-Objekt
description
String
Beschreibungstext, der in den Einträgen eines menu-Elements angezeigt wird, wenn die Eigenschaft itemSize auf large eingestellt ist
enabled
Boolean
id
String
Eindeutige ID eines benutzerdefinierten Steuerelements; nicht zu benutzen in Kombination mit idMso oder idQ
idMso
Long
Eindeutige ID eines eingebauten Steuerelements; nicht zu benutzen in Kombination mit idMso oder idQ
idQ
Long
Steuerelement-ID, enthält NamespaceBezeichnung; nicht in Kombination mit id oder idMso
image
String
String zum Angeben einer Image-Datei
imageMso
String
Verweist auf ein eingebautes Symbol
insertAfterMso
String
Gibt an, hinter welches eingebaute Steuerelement mit idMso ein neues Element angelegt werden soll
insertAfterQ
String
Gibt an, hinter welches eingebaute Steuerelement mit idQ ein neues Element angelegt werden soll
insertBeforeMso
String
Gibt an, vor welches eingebaute Steuerelement mit idMso ein neues Element angelegt werden soll
insertBeforeQ
String
Gibt an, vor welches eingebaute Steuerelement mit idQ ein neues Element angelegt werden soll
itemHeight
Long
Höhe eines Elements des gallerySteuerelements in Pixeln
itemSize
String
itemWidth
Long
Breite eines Elements des gallerySteuerelements in Pixeln
keytip
String
Gibt das Tastenkürzel an, dass beim Betätigen der Alt-Taste erscheint
false|true|0|1
normal|large
Legt fest, ob ein Steuerelement aktiviert oder deaktiviert ist
Größe eines Menü-Elements
Tabelle 10.2: Eigenschaften von Ribbon-Elementen (Fortsetzung)
683
Kapitel 12 Eigenschaft
Datentyp
Werte
Beschreibung
label
String
Bezeichnungsfeld eines Steuerelements
maxLength
Long
Maximale Länge einer einzugebenden Zeichenkette
rows
Long
Anzahl der Zeilen in einem gallerySteuerelement
screentip
String
Überschrift des beim Überfahren des Elements angezeigten Textes
showImage
String
false|true|0|1
Soll ein Symbol angezeigt werden?
showItemImage
String
false|true|0|1
Soll ein Symbol für ein Listenelement des comboBox-, dropDown- oder gallerySteuerelements angezeigt werden?
showItemLabel
String
false|true|0|1
Soll ein Text für ein Listenelement des comboBox-, dropDown- oder gallerySteuerelements angezeigt werden?
showLabel
String
false|true|0|1
Soll die Bezeichnung eines Steuerelements angezeigt werden?
size
String
normal|large
Legt die Größe für die Anzeige eines Steuerelements fest.
sizeString
String
startFromScratch
String
supertip
String
Unter dem screenTip beim Überfahren eines Elements angezeigter Text
tag
String
?
title
String
Fügt einem Trennstrich eines menu-Elements eine Beschriftung hinzu.
visible
String
Das Steuerelement wird so breit gestaltet, dass der für diese Eigenschaft angegebene Text komplett angezeigt wird. false|true|0|1
false|true|0|1
Legt fest, ob eingebaute Ribbons und einige Einträge des Office-Menüs ausgeblendet werden.
Gibt an, ob ein Element sichtbar ist.
Tabelle 10.2: Eigenschaften von Ribbon-Elementen (Fortsetzung)
12.11.3 Ereigniseigenschaften der Ribbon-Elemente Die folgende Tabelle enthält alle Ereigniseigenschaften der Ribbon-Elemente und die VBA-Syntax der passenden Callback-Funktionen.
684
Ribbon Eigenschaft
Steuerelement
VBA-Syntax
getDescription
(several controls)
Sub GetDescription(control As IRibbonControl, ByRef description)
getEnabled
(several controls)
Sub GetEnabled(control As IRibbonControl, ByRef enabled)
getImage
(several controls)
Sub GetImage(control As IRibbonControl, ByRef image)
getImageMso
(several controls)
Sub GetImageMso(control As IRibbonControl, ByRef imageMso)
getItemCount
comboBox
Sub GetItemCount(control As IRibbonControl, ByRef count)
dropDown
Sub GetItemCount(control As IRibbonControl, ByRef count)
gallery
Sub GetItemCount(control As IRibbonControl, ByRef count)
getItemHeight
gallery
Sub getItemHeight(control As IRibbonControl, ByRef height)
getItemID
comboBox
Sub GetItemID(control As IRibbonControl, index As Integer, ByRef id)
dropDown
Sub GetItemID(control As IRibbonControl, index As Integer, ByRef id)
gallery
Sub GetItemID(control As IRibbonControl, index As Integer, ByRef id)
comboBox
Sub GetItemImage(control As IRibbonControl, index As Integer, ByRef image)
dropDown
Sub GetItemImage(control As IRibbonControl, index As Integer, ByRef image)
gallery
Sub GetItemImage(control As IRibbonControl, index As Integer, ByRef image)
comboBox
Sub GetItemLabel(control As IRibbonControl, index As Integer, ByRef label)
dropDown
Sub GetItemLabel(control As IRibbonControl, index As Integer, ByRef label)
getItemImage
getItemLabel
Tabelle 10.3: Methoden von Steuerelementen und ihre Syntax
685
Kapitel 12 Eigenschaft
Steuerelement
VBA-Syntax
gallery
Sub GetItemLabel(control As IRibbonControl, index As Integer, ByRef label)
comboBox
Sub GetItemScreenTip(control As IRibbonControl, index As Integer, ByRef screentip)
dropDown
Sub GetItemScreenTip(control As IRibbonControl, index As Integer, ByRef screenTip)
gallery
Sub GetItemScreenTip(control As IRibbonControl, index as Integer, ByRef screen)
comboBox
Sub GetItemSuperTip (control As IRibbonControl, index As Integer, ByRef supertip)
dropDown
Sub GetItemSuperTip (control As IRibbonControl, index As Integer, ByRef superTip)
gallery
Sub GetItemSuperTip (control As IRibbonControl, index as Integer, ByRef screen)
getItemWidth
gallery
Sub getItemWidth(control As IRibbonControl, ByRef width)
getKeytip
(several controls)
Sub GetKeytip (control As IRibbonControl, ByRef label)
getLabel
(several controls)
Sub GetLabel(control As IRibbonControl, ByRef label)
getPressed
checkBox
Sub GetPressed(control As IRibbonControl, ByRef return)
toggleButton
Sub GetPressed(control As IRibbonControl, ByRef return)
getScreentip
(several controls)
Sub GetScreentip(control As IRibbonControl, ByRef screentip)
getSelectedItemID
dropDown
Sub GetSelectedItemID(control As IRibbonControl, ByRef index)
gallery
Sub GetSelectedItemID(control As IRibbonControl, ByRef index)
dropDown
Sub GetSelectedItemIndex(control As IRibbonControl, ByRef index)
gallery
Sub GetSelectedItemIndex(control As IRibbonControl, ByRef index)
getItemScreenTip
getItemSuperTip
getSelectedItemIndex
Tabelle 10.3: Methoden von Steuerelementen und ihre Syntax (Fortsetzung)
686
Ribbon Eigenschaft
Steuerelement
VBA-Syntax
getShowImage
button
Sub GetShowImage (control As IRibbonControl, ByRef showImage)
getShowLabel
button
Sub GetShowLabel (control As IRibbonControl, ByRef showLabel)
getSize
(several controls)
Sub GetSize(control As IRibbonControl, ByRef size)
getSupertip
(several controls)
Sub GetSupertip(control As IRibbonControl, ByRef screentip)
getText
comboBox
Sub GetText(control As IRibbonControl, ByRef text)
editBox
Sub GetText(control As IRibbonControl, ByRef text)
getTitle
menuSeparator
Sub GetTitle (control As IRibbonControl, ByRef title)
getVisible
(several controls)
Sub GetVisible(control As IRibbonControl, ByRef visible)
loadImage
customUI
Sub LoadImage(imageId As string, ByRef image)
onAction
button
Sub OnAction(control As IRibbonControl)
checkBox
Sub OnAction(control As IRibbonControl, pressed As Boolean)
dropDown
Sub OnAction(control As IRibbonControl, selectedId As String, selectedIndex As Integer)
gallery
Sub OnAction(control As IRibbonControl, selectedId As String, selectedIndex As Integer)
toggleButton
Sub OnAction(control As IRibbonControl, pressed As Boolean)
comboBox
Sub OnChange(control As IRibbonControl, text As String)
editBox
Sub OnChange(control As IRibbonControl, text As String)
customUI
Sub OnLoad(ribbon As IRibbonUI)
onChange
onLoad
Tabelle 10.3: Methoden von Steuerelementen und ihre Syntax (Fortsetzung)
12.11.4 Steuerelemente und ihre Eigenschaften Die folgende Tabelle zeigt, welche Steuerelemente aus Tabelle 12.1 mit welchen Eigen schaften aus Tabelle 12.2 und Ereigniseigenschaften aus Tabelle 12.3 ausgestattet sind.
687
toggleButton
textBox
tabSet
tab
splitButton
separator
ribbon
menuSeparator
menu
labelControl
item
gallery
editBox
dynamicMenu
dropDown
customUI
command
comboBox
buttonGroup
•
checkBox
boxStyle
button
Eigenschaften/ Steuerelemente
box
Kapitel 12
• •
columns description
•
•
enabled
•
•
getDescription
•
•
getEnabled
•
•
getImage
•
getImageMso
•
• •
•
•
•
•
•
•
•
•
•
•
• •
•
•
getContent
getItemCount
• •
•
•
• •
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
• •
getItemHeight getItemID
•
•
•
getItemImage
•
•
•
getItemLabel
•
•
•
getItemScreentip
•
•
•
getItemSupertip
•
•
• •
getItemWidth getKeytip
•
•
•
•
•
•
•
getLabel
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
getPressed getScreentip
•
•
•
•
getSelectedItemID
•
•
getSelectedItem-
•
•
Index getShowImage
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
getShowItemImage getShowLabel
•
getSize
•
getSupertip
•
getText
•
•
• •
•
•
•
•
•
•
•
•
•
•
getTitle getVisible
•
•
•
•
•
•
Tabelle 10.4: Eigenschaften von Steuerelementen
688
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
button
insertAfterMso
•
•
insertAfterQ
•
insertBeforeMso
•
insertBeforeQ
•
customUI
•
•
imageMso
•
command
•
•
comboBox
•
buttonGroup
•
checkBox
•
•
image
•
•
•
•
•
•
•
•
•
•
•
itemSize
•
ItemWidth keytip
•
•
•
•
•
•
label
•
•
•
•
•
•
•
•
•
•
•
•
loadImage
•
maxLength
•
•
• •
•
•
•
onChange
•
• •
onLoad
•
rows screentip
•
showImage
•
•
•
•
• •
•
•
•
•
showItemImage
•
•
•
showItemLabel
•
•
•
•
•
•
showLabel size
•
•
•
ItemHeight
onAction
•
•
toggleButton
•
•
•
idQ
•
textBox
•
•
•
•
tabSet
•
•
•
•
tab
•
•
•
idMso
splitButton
menuSeparator
•
•
separator
menu
•
•
•
ribbon
labelControl
•
gallery
•
editBox
•
dynamicMenu
•
dropDown
•
•
•
•
box
•
•
•
id
Eigenschaften/ Steuerelemente
item
Ribbon
•
•
•
•
•
•
•
•
•
•
•
sizeString
•
•
•
•
•
•
•
•
•
•
startFromScratch supertip
•
•
•
•
•
•
•
tag
•
•
•
•
•
•
visible
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
• •
•
•
Tabelle 10.4: Eigenschaften von Steuerelementen (Fortsetzung)
689
Kapitel 12
Quellen zu diesem Kapitel [1] XML-Notepad Download: http://www.microsoft.com/downloads/details.aspx?familyid =72d6aa49-787d-4118-ba5f-4f30fe913628&displaylang=en [2] Schemadefinitionsdatei für Ribbons (customui.xsd): http://officeblogs.net/UI/customUI. zip [3] Tabelle mit Informationen zu allen Ribbon-Elementen von Access: Buch-CD \Kap_ 12\AccessIDMSO.accdb oder http://www.access-entwicklerbuch.de/2007/grundlagen/ AccessIDMSO.accdb
690
13 Debugging, Fehlerbehandlung und Fehlerdokumentation Die drei in der Kapitelüberschrift aufgeführten Themen sind eng miteinander verknüpft und können über Erfolg oder Scheitern einer Anwendung entscheiden. Grundsätz lich greifen alle drei Mechanismen erst, wenn etwas im Argen liegt – allerdings an unterschiedlichen Stellen. Die klassischen Debugging-Mechanismen von Access helfen vorrangig bei der Entwicklung einer Anwendung und liefern beispielsweise an Stellen Anhaltspunkte, an denen der Code unerwartete Ergebnisse liefert. Fehlerbehandlung und Fehlerdokumentation arbeiten sowohl bei der Entwicklung als auch beim Einsatz der An wendung Hand in Hand. Die Fehlerbehandlung sorgt dafür, dass eine Anwendung auch beim Auftreten eines Fehlers stabil weiterläuft, während die Fehlerdokumentation für das Notieren von Feh lern sorgt, damit der Entwickler diesen schnell und unkom pliziert auf den Grund gehen kann. Letzteres macht sich im Übrigen sowohl während der Entwicklung gut als auch während des Einsatzes beim Benutzer.
13.1 Fehlerarten Die bei der Programmierung und beim Einsatz einer An wendung auftretenden Fehler lassen sich in mehrere Kate gorien einteilen.
Kapitel 13
Der Schwierigkeitsgrad beim Auffinden der Fehler und die dazu benötigten Werkzeuge sind je nach Kategorie unterschiedlich. Beispiele auf CD: Sie finden alle Code-Beispiele dieses Kapitels auf der Buch-CD unter \Kap_13\Debugging.accdb.
13.1.1 Syntaxfehler Syntaxfehler sind vermutlich am leichtesten zu finden, weil die Entwicklungsumgebung sich selbst darum kümmert. Dabei handelt es sich um unvollständige Code-Konstrukte, fehlende beziehungsweise überzählige Klammern oder Anführungszeichen oder die Verwendung von Schlüsselwörtern an Stellen, an denen sie nicht erlaubt sind. Wenn Sie beispielsweise eine If Then-Verzweigung einfügen und vor dem Sprung in die nächste Zeile zwar das Schlüsselwort If und die Bedingung, aber nicht das Schlüssel wort Then eingegeben haben, teilt Access Ihnen dies unmissverständlich mit (siehe Ab bildung 13.1).
Abbildung 13.1: Syntaxfehler werden an Ort und Stelle geahndet
Andere Syntaxfehler deckt die Entwicklungsumgebung nicht umgehend auf. Meist handelt es sich dabei um fehlende, falsche oder überflüssige Zeilen. Bleiben Sie bei obigem Beispiel: Wenn Sie die erste Zeile der If Then-Anweisung vervollständigen, ist Access zunächst zufrieden, auch, wenn Sie dann an einer anderen Stelle weiterprogrammieren, ohne die abschließende End If-Anweisung hinzuzufügen und diese dann vergessen. Darum kümmert sich Access aber später, und zwar spätestens beim Aufrufen der Prozedur. Die Meldung aus Abbildung 13.2 zeigt exakt an, vor welcher Zeile sie spätestens die fehlende End If-Anweisung vermisst.
692
Debugging, Fehlerbehandlung und Fehlerdokumentation
Abbildung 13.2: Unvollständige Code-Konstrukte findet Access erst beim Kompilieren oder Ausführen
Das gleiche Ergebnis erhalten Sie, wenn Sie das VBA-Projekt mit dem Menübefehl De buggen/Kompilieren von kompilieren. Das regelmäßige Kompilieren des VBA-Projekts während der Entwicklung sollten Sie sich zur Angewohnheit machen – etwa jeweils nach Erstellung einer Prozedur. Sie kommen so Fehlern schneller auf die Spur, als wenn der Kompiler sich erst beim Testdurchlauf der Anwendung meldet. Das Ausführen einer Routine erreichen Sie übrigens am schnellsten durch die Betätigung der Taste F5, während sich die Einfügemarke innerhalb der zu startenden Routine befindet. Das klappt allerdings auch nur in Routinen ohne Übergabeparameter, und das auch nur in Standardmodulen.
Syntaxprüfung und weitere Optionen Die Syntaxprüfung während der Eingabe ist eine nützliche Funktion, die Sie auf Fehler aufmerksam macht, während Sie gedanklich noch in der betroffenen Routine stecken. Würden solche Fehler erst nach dem Schreiben einiger Zeilen Code oder gar ganzer Routinen beim nächsten Debuggen oder Test bemerkt, müssten Sie sich zunächst in die jeweiligen Codezeilen hineindenken. Manch einer wird durch diese Funktion aber in seinem Tatendrang gestört. Die Hinweise auf Syntaxfehler können natürlich auch einmal nervig sein – etwa wenn Sie in einer If Then-Anweisung eine Variable verwenden möchten, deren Namen Sie gerade nicht im Kopf haben. Wenn Sie dann schnell nach oben blättern, um den Variablennamen zu ermitteln, macht Ihnen die Syntaxprüfung einen Strich durch die Rechnung. Deshalb wird es manchen freuen, dass man diese »Live«-Syntaxprüfung abschalten kann, und zwar im Optionen-Dialog der VBA-Entwicklungsumgebung (Menüeintrag Extras/Optionen, Option Automatische Syntaxüberprüfung). Dadurch bleiben allerdings
693
Kapitel 13
nur die Fehlermeldungen aus, fehlerhafte Zeilen sind aber weiterhin in roter Schrift markiert – Sie verpassen also nichts.
Variablendeklaration erzwingen Ein weiterer Garant für nicht bemerkte Fehler ist die fehlende Deklaration von Variablen. Im einfachsten Fall weist man einer nicht explizit deklarierten Variablen, die eigentlich Zahlen enthalten sollte, einen Text zu und eckt damit irgendwo an. Damit das nicht passiert, sollten Sie tunlichst alle verwendeten Variablen deklarieren. Da selbst erfahrene Programmierer dies einmal vergessen, können Sie mit der folgenden Zeile im Modulkopf nachhelfen: Option Explicit
Access meldet sich anschließend bei jeder Verwendung einer nicht deklarierten Variablen. Damit Sie auch das Setzen dieser Zeile nicht mal vergessen, können Sie im Dialog aus Abbildung 13.3 die Option Variablendeklaration erforderlich aktivieren. Access fügt die Option Explicit-Anweisung dann automatisch in jedes neue Modul ein.
Abbildung 13.3: Optionen für die VBA-Entwicklungsumgebung
13.1.2 Laufzeitfehler Laufzeitfehler werden nicht durch den Compiler von Access entdeckt. Sie fallen erst bei der Durchführung der entsprechenden Routine auf. Typische Beispiele für Laufzeitfehler sind Divisionen durch 0, Zuweisung von Werten falschen Datentyps (besonders oft in Zusammenhang mit dem Wert Null) oder durch Verweise auf Objekte, die nicht instanziert wurden oder aus anderen Gründen nicht zur Verfügung stehen.
694
Debugging, Fehlerbehandlung und Fehlerdokumentation
Im Falle eines Laufzeitfehlers zeigt die VBA-Entwicklungsumgebung eine Fehlermeldung an, die eine Fehlernummer und eine Fehlerbeschreibung enthält. Laufzeitfehler sind natürlich meist unerwünscht. Manchmal lässt sich ein Laufzeitfehler allerdings auch für die Ablaufsteuerung innerhalb einer Routine zweckentfremden oder liefert gar eine performantere oder einfacher zu implementierende Variante. Ein Beispiel ist das Instanzieren von OLE-Servern: Hier versucht man zunächst, ein bestehenes Objekt zu referenzieren. Ist keine Instanz vorhanden, löst dies einen Fehler aus. Dieser Fehler wird allerdings abgefangen und in der Fehlerbehandlungsroutine wird eine neue Instanz des benötigten Objekts erstellt. Mehr dazu erfahren Sie in diesem Kapitel unter 13.3.8, »Funktionale Fehlerbehandlung«.
13.1.3 Logische Fehler Logische Fehler werden gar nicht von Access gemeldet. Sie schlagen sich vielmehr in falschen Ergebnissen bei der Ausführung von Routinen nieder – entweder geben diese unerwartete Werte zurück, speichern falsche Daten in den Tabellen der Datenbank oder machen auf ähnliche Weise auf sich aufmerksam – früher oder später.
Logische Fehler aufdecken Beim Aufdecken logischer Fehler sind gute Debugging-Möglichkeiten das A und O. Wenn Sie die Möglichkeit haben, Ein- und Ausgangswerte von Funktionen zu prüfen oder gar die Inhalte von Variablen innerhalb der Routinen zu kontrollieren, lassen sich die meisten Fehler mit mehr oder weniger Schritten eingrenzen und schlussendlich auch finden. Die VBA-Entwicklungsumgebung liefert einige sinnvolle Werkzeuge für das Debugging, wie der folgende Abschnitt zeigt.
13.2 Debugging in der VBA-Entwicklungsumgebung Die VBA-Entwicklungsumgebung bietet einige Werkzeuge zum Debuggen von Code. »Debugging« fasst eigentlich alle Tätigkeiten zur Fehlerbehebung zusammen, wozu das Auffinden, Diagnostizieren und Beheben von Fehlern – oder eben »Bugs« – gehört. Interessant ist in diesem Zusammenhang die Herkunft der Bezeichnung »Bug»: Laut der Internetseite http://www.waterholes.com/~dennette/1996/hopper/bug.htm stammt dieser Begriff aus der Zeit des zweiten Weltkriegs und bezieht sich auf eine Motte, die ein Relais des Computers Mark I bei der Arbeit behinderte. Diese Motte wurde damals als (toter) Beweis in ein Logbuch eingeklebt, das sich zurzeit in der Smithsonian Institution in Washington befindet.
695
Kapitel 13
Die Debugging-Werkzeuge der VBA-Entwicklungsumgebung konzentrieren sich auf das Melden und Anzeigen von Laufzeitfehlern und das Nachverfolgen von VariablenWerten, wobei dazu unterschiedliche Möglichkeiten zur Verfügung stehen. Nachfolgend lernen Sie die einzelnen Werkzeuge kennen.
13.2.1 Die Debuggen-Symbolleiste Die Debuggen-Symbolleiste liefert die Möglichkeit, alle nachfolgend beschriebenen Debugging-Tools per Mausklick aufzurufen und den Ablauf von Routinen durch das Starten und Beenden, das Setzen von Haltepunkten oder das schrittweise Durchlaufen zu steuern (siehe Abbildung 13.4).
Abbildung 13.4: Symbolleiste zum Aufrufen der Debugging-Funktionen
13.2.2 Das Direktfenster Das wichtigste, weil flexibelste Element der VBA-Entwicklungsumgebung ist das Direktfenster. Sie können damit folgende Aktionen durchführen: Aufrufen von Function- und Sub-Prozeduren (mit oder ohne Parameter) Aufrufen von Standardfunktionen und Ausgabe der Ergebnisse mit der Debug.PrintAnweisung Untersuchen von Formularen und Berichten und deren Eigenschaften während der Anzeige in der Formular- beziehungsweise Berichtsansicht durch Ausgeben von Werten mit der Debug.Print-Anweisung Ausgabe von Werten der Variablen einer gerade angehaltenen Routine sowie Anpassen von Variablen Ausgabe von Debug.Print-Anweisungen, die Sie an beliebigen Stellen im Quellcode platzieren können Sollte das Direktfenster einmal nicht sichtbar sein, können Sie es mit der Tastenkom bination Strg + G sichtbar machen. Das gilt übrigens auch für die Access-Entwicklungs umgebung: Dort führt diese Tastenkombination zum Öffnen der VBA-Entwicklungsum gebung und zum Aktivieren des Direktfensters. Das Direktfenster sieht wie in Abbildung 13.5 aus und kann wie eine flexiblere Variante der Eingabeaufforderung verwendet werden. Geben Sie dort einfach die gewünschten Ausdrücke ein und lassen Sie das Ergebnis ausgeben.
696
Debugging, Fehlerbehandlung und Fehlerdokumentation
Abbildung 13.5: Testen von Funktionen im Direktfenster
Debug.Print und die Kurzformen Die vollständige Anweisung mit Angabe des Debug-Objekts müssen Sie nur außerhalb des Direktfensters – also innerhalb von Modulen – verwenden. Das Debug-Objekt ist das Direktfenster, daher reicht dort die Anweisung Print oder die abgekürzte Form ?.
13.2.3 Haltepunkte Zum punktuellen Untersuchen des VBA-Codes und aktueller Zustände von Variablen können Sie im VBA-Code Haltepunkte setzen. Das ist zum Beispiel sinnvoll, wenn Sie ahnen, dass an einer bestimmten Stelle im Quellcode etwas nicht so läuft, wie es laufen sollte, und Sie sich die in der Umgebung vorhandenen Variablen einmal näher ansehen möchten. In solch einem Fall setzen Sie einen Haltepunkt, indem Sie einfach in die graue Leiste links neben dem Quellcode klicken. Auf die gleiche Weise entfernen Sie den Haltepunkt auch wieder. Die zweite Methode zum Setzen eines Haltepunktes ist das Positionieren der Einfüge marke auf der gewünschten Zeile und die Betätigung der Taste F9 – mit dieser Taste entfernen Sie vorhandene Haltepunkte auch wieder. Haben Sie wie in Abbildung 13.6 einen Haltepunkt gesetzt, müssen Sie die Routine nur noch starten. Sobald die Abarbeitung an diesem Punkt angelangt ist, wird die Aus führung unterbrochen und die aktuelle Zeile gelb hinterlegt. Sie können dann durch Überfahren mit dem Mauszeiger die Werte der im Code enthaltenen Ausdrücke anzeigen lassen (siehe Abbildung 13.7). Alternativ können Sie sich den Inhalt von Ausdrücken auch im Direktfenster ausgeben lassen – etwa, wenn Sie den Wert für einen anderen Zweck weiterverwenden möchten. Um den Wert aus Abbildung 13.7 im Direktfenster auszugeben, benutzen Sie den folgenden Ausdruck: Debug.Print tdf.Fields.Count
697
Kapitel 13
Abbildung 13.6: Ein Haltepunkt in einer VBA-Prozedur
Abbildung 13.7: Anzeige der Werte von Ausdrücken im Haltemodus
Von Anweisung zu Anweisung Wenn Sie nach dem Erreichen des Haltepunktes Schritt für Schritt durch den Code laufen möchten, verwenden Sie dazu die Schaltfläche Einzelschritt (F8).
Von Haltepunkt zu Haltepunkt Wenn Sie mehrere Haltepunkte in der Anwendung gesetzt haben, können Sie die Abarbeitung des Codes nach Erreichen eines Haltepunktes mit der Taste F5 fortsetzen. Der Code läuft dann bis zum nächsten Haltepunkt weiter, sofern zwischendurch nichts Unvorhergesehenes passiert.
Den Ablauf einer Routine im Haltemodus beeinflussen Der Haltemodus ist relativ mächtig: Sie können etwa den Zeiger, der die aktuell auszuführende Zeile markiert, verschieben und damit in den Programmablauf eingreifen. Damit können Sie beispielsweise einen Laufzeitfehler überbrücken, wenn Sie diesen erst später beheben und zunächst die Routine weiter untersuchen möchten, oder auch im Ablauf zurückspringen. Dies ist allerdings mit Vorsicht zu genießen, da somit beispielsweise auch Zählervariablen wiederholt erhöht werden können.
698
Debugging, Fehlerbehandlung und Fehlerdokumentation
Haltepunkte fixieren Die am linken Rand des Codemoduls markierten Haltepunkte halten nicht lange – wenn Sie die Access-Anwendung einmal geschlossen haben und wieder öffnen, sind die Haltepunkte verschwunden. Wenn Sie einen Haltepunkt benötigen, der etwas haltbarer ist, fügen Sie vor der Zeile, die Sie sonst mit einem Haltepunkt markiert hätten, die Anweisung Stop ein.
13.2.4 Die Aufrufliste Manchmal kann es wichtig sein, wenn Sie wissen, über welche Prozeduren Sie zu der aktuellen Routine gelangt sind. Das ist vor allem interessant, wenn Sie einen Haltepunkt in einer Routine platziert haben, die von mehreren anderen Prozeduren aus aufgerufen wird. Die Aufrufliste zeigt nur Informationen an, wenn der Ablauf durch einen Haltepunkt unterbrochen wurde (siehe Abbildung 13.8).
Abbildung 13.8: Die Aufrufliste zeigt die aufgerufenen Routinen in der Hierarchie an
13.2.5 Ausdrücke überwachen Das Fenster Überwachungsausdrücke ermöglicht die Überwachung bestimmter Ausdrücke über den gesamten Ablauf einer Anwendung. Sie zeigen dieses Fenster an, indem Sie den Menüeintrag Ansicht/Überwachungsfenster auswählen. Um einen Ausdruck zur Überwachung anzulegen, können Sie diesen beispielsweise im aktuellen Codefenster markieren und dann im Kontextmenü den Eintrag Überwachung hinzufügen auswählen.
699
Kapitel 13
Der Dialog Überwachung hinzufügen (siehe Abbildung 13.9) enthält dann direkt den gewünschten Ausdruck. Hier gibt es nun drei Möglichkeiten: Der Ausdruck soll einfach nur überwacht werden. Der Ablauf soll unterbrochen werden, wenn der Ausdruck einen bestimmten Wert annimmt. In diesem Fall müssen Sie den Ausdruck noch um das entsprechende Kriterium erweitern, etwa fld.Name ="MitarbeiterID". Der Ablauf soll unterbrochen werden, sobald der Wert geändert wurde.
Abbildung 13.9: Anlegen eines neuen Überwachungsausdrucks
Ausdrücke per VBA-Code überwachen Dass Sie mit der Debug.Print-Anweisung beliebige Ausdrücke während des Ablaufs von Routinen ausgeben können, haben Sie bereits weiter oben erfahren. Damit lässt sich die Überwachung von Ausdrücken gut nachbilden, was der ersten Option der Überwachungs-Eigenschaften entsprechen würde. Auch die zweite Option lässt sich simulieren: Um den Ablauf einer Prozedur zu unter brechen, wenn ein Ausdruck einen bestimmten Wert enthält, verwenden Sie die Debug. Assert-Anweisung. Als Parameter legen Sie einen boolschen Ausdruck mit dem ge wünschten Ergebnis fest.
700
Debugging, Fehlerbehandlung und Fehlerdokumentation
13.2.6 Das Lokal-Fenster Das Lokal-Fenster zeigt direkt alle Variablen und ihre Werte einschließlich der Eigen schaften von Objekten der laufenden Prozedur an (siehe Abbildung 13.10). Um die Ei genschaften von Objekten anzuzeigen, klicken Sie auf das Plus-Symbol (+) vor dem jeweiligen Objekt. Auch dieses Debugging-Tool funktioniert nur in Zusammenhang mit angehaltenem Code.
Abbildung 13.10: Das Lokal-Fenster zeigt alle Variablen der laufenden Routine an
13.3 Fehlerbehandlung in VBA Wenn man von Fehlerbehandlung spricht, bezieht sich dies auf Laufzeitfehler. Andere Fehler wie Syntax- oder Kompilierzeitfehler werden bereits vor dem Start einer Routine gemeldet – hier gibt es auch keine Möglichkeit einer externen Behandlung. Laufzeitfehler lassen sich unter VBA wie in anderen Programmiersprachen »behandeln«; das heißt, dass Sie mit zusätzlichem VBA-Code auf Fehler reagieren können.
Fehlerbehandlung für benutzerdefinierte Fehlermeldungen Meist verwendet man eine Fehlerbehandlung dazu, eine benutzerdefinierte Fehler meldung anzuzeigen, anstatt dem Benutzer der Anwendung die tatsächliche Fehlermel
701
Kapitel 13
dung mit Blick auf die fehlerhafte Stelle im Code zu präsentieren. Auf diese Weise lassen sich in vielen Fällen aussagekräftigere Meldungen ausgeben. Wenn Sie die AccessAnwendung als Runtime-Version weitergeben (mehr dazu in Kapitel 19, Abschnitt 19.2, »Weitergabe von Access-Datenbanken«), gehört eine solche Fehlerbehandlung quasi zum Pflichtprogramm – erweitert um eine Möglichkeit, Informationen zu den Fehlern in irgendeiner Weise zur Weiterleitung an den Entwickler verfügbar zu machen. Wie das funktioniert, erfahren Sie weiter unten in Abschnitt 13.4, »Fehlerdokumentation und -übermittlung«.
Fehlerbehandlung als Vereinfachung Oft ist es gar nicht geplant, den Benutzer vom Auftreten eines Fehlers in Kenntnis zu setzen – wenn Sie beispielsweise eine Routine verwenden, um eine temporäre Tabelle anzulegen, löschen Sie zuvor eine eventuell vorhandene Tabelle gleichen Namens. Wenn Sie nicht sicher wissen, ob die Tabelle überhaupt vorhanden ist, und keinen Code schreiben wollen, der dies überprüft, sorgen Sie einfach dafür, dass die Tabelle gelöscht wird, wenn diese vorhanden ist, und dass der zu erwartende Fehler, der beim Versuch, eine nicht vorhandene Tabelle zu löschen, auftritt, nicht angezeigt wird – ein Beispiel folgt weiter unten.
Fehlerbehandlung als Lieferant für Bedingungs-Kriterien Manchmal verwendet man Fehler sogar als eine Art »Verzweigung« – wenn Sie etwa eine Referenz auf ein Word-Objekt benötigen, versuchen Sie zunächst, eine bestehende Instanz anzusprechen. Ist diese nicht vorhanden, löst dieser Versuch einen Fehler aus. Direkt danach werten Sie aus, ob ein Fehler auftrat und erzeugen in Abhängigkeit davon eine neue Instanz von Word oder verwenden die existierende Instanz.
13.3.1 Elemente der Fehlerbehandlung Damit VBA überhaupt irgendetwas anderes macht, als eine Standardfehlermeldung auszugeben, teilen Sie der Routine mit einer On Error-Anweisung mit, dass eine Alternative zur Standardfehlermeldung ansteht.
13.3.2 Fehlerbehandlung einleiten Dabei gibt es folgende Varianten: On Error Resume Next: Ignoriert sämtliche Fehler und bearbeitet einfach die folgende Zeile.
702
Debugging, Fehlerbehandlung und Fehlerdokumentation
On Error GoTo <Markierung>: Lässt die Routine im Fehlerfall mit dem hinter <Markierung>: befindlichen Code fortfahren. On Error GoTo 0: Sorgt dafür, dass nachfolgend auftretende Fehler wieder von VBA selbst behandelt werden.
13.3.3 Klassischer Aufbau einer Fehlerbehandlung Typischerweise ist eine Fehlerbehandlung wie im folgenden Listing aufgebaut. Die Elemente der Fehlerbehandlung sind fett gedruckt. Den Start macht die On Error-Anweisung, die angibt, dass die Routine im Falle eines Fehlers an der Markierung Beispielfehler_Err: fortgeführt werden soll. Diese Markierungen (auch »Label« genannt) sind an der Stelle der Markierung mit abschließendem Doppelpunkt (:) anzugeben, beim Verweis aber ohne Doppelpunkt. Zwischen der Markierung Beispielfehler_Err: und dem Ende der Routine ist Platz für die eigentliche Fehlerbehandlung. Hier können Sie Informationen zum Fehler auswerten und entsprechende Schritte einleiten – mehr dazu weiter unten. Nach dem Behandeln des Fehlers erfolgt ein erneuter Sprung zur Marke Beispielfehler_Exit:. Dort bringen Sie diejenigen Anweisungen unter, die trotz Fehler unbedingt noch ausgeführt werden müssen. Dabei kann es sich um das Schließen von Objekten, die Freigabe von Objektvariablen oder auch das Löschen temporärer Daten handeln. Wenn die Routine ohne Fehler durchläuft, bleiben die Markierungen bedeutungslos. Das heißt, dass die hinter der Markierung Beispielfehler_Exit befindlichen Anweisungen ausgeführt werden, als ob diese Markierung dort gar nicht vorhanden sei. Die Exit-Anweisung schließlich sorgt dafür, dass die Routine beim Verlauf ohne Fehler nicht dennoch mit der Fehlerbehandlung endet. Public Function Beispielfehler() On Error GoTo Beispielfehler_Err … 'Fehlerbehandlung Beispielfehler_Exit: 'Restarbeiten … Exit Function Beispielfehler_Err: 'Fehler auswerten und reagieren GoTo Beispielfehler_Exit End Function Listing 13.1: Grundgerüst einer Fehlerbehandlung
703
Kapitel 13
13.3.4 Fehler auswerten Eine oft gesehene Art der Fehlerauswertung ist die Ausgabe der Fehlerinformationen, etwa durch eine Meldung wie im folgenden Beispiel. Der Fehler durch die Division durch Null führt zur Meldung aus Abbildung 13.11: Public Function DivisionDurchNull() On Error GoTo DivisionDurchNull_Err Dim i As Integer i = 1 / 0 'Fehlerbehandlung DivisionDurchNull_Exit: 'Restarbeiten Exit Function DivisionDurchNull_Err: MsgBox "Fehlernummer: " & Err.Number _ & vbCrLf & "Beschreibung: " & Err.Description GoTo DivisionDurchNull_Exit End Function Listing 13.2: Fehlermeldung per Meldungsfenster
Abbildung 13.11: Diese Fehlermeldung resultiert aus der Fehlerbehandlung in Listing 13.2.
Obwohl diese Art der Fehlermeldung in vielen Beispieldatenbanken zu finden ist, bringt sie leider nicht viel. Als Entwickler wird man hoffentlich nicht mit einer solchen Fehlermeldung arbeiten, da sie nicht den geringsten Aufschluss über den Ort gibt, an dem der Fehler auftritt. Auch in Datenbankanwendungen, die zur Weitergabe bestimmt sind, ist diese Fehlerbehandlung aus oben genanntem Grund nicht besonders hilfreich. Die Fehlermeldung soll ja nicht nur den Benutzer über das Auftreten eines Fehlers informieren, sondern auch Informationen liefern, die der Benutzer an den Entwickler weitergeben kann.
13.3.5 Das Err-Objekt Bevor es untergeht, sollen Sie noch das Err-Objekt kennen lernen, das für das Bereitstel len etwa der Fehlernummer oder der Fehlerbeschreibung zuständig ist. Mit diesem Ob jekt können Sie gut prüfen, ob zu einem bestimmten Zeitpunkt ein Fehler aufgetreten ist. Wenn die Eigenschaft Err.Number den Wert 0 zurückliefert, liegt derzeit kein Fehler vor.
704
Debugging, Fehlerbehandlung und Fehlerdokumentation
Das Err-Objekt enthält unter anderem die folgenden Eigenschaften und Methoden: Clear: Löscht die Eigenschaften des Err-Objekts. Das bewirkt unter anderem auch jede Anweisung zur Änderung der Fehlerbehandlung wie On Error GoTo 0. Number: Gibt die Fehlernummer zurück. Description: Gibt die Beschreibung des Fehlers zurück. Raise: Löst einen benutzerdefinierten Fehler aus (weitere Informationen in Abschnitt 13.3.4, »Funktionale Fehlerbehandlung«).
13.3.6 Nach der Fehlerbehandlung Wenn der Fehler behandelt wurde, gibt es verschiedene Möglichkeiten, die Ausführung fortzusetzen: Mit der fehlerhaften Zeile fortfahren: Resume Mit der ersten Zeile hinter der Fehlerzeile fortfahren: Resume Next Mit dem Code hinter irgendeiner Markierung fortfahren: Resume <Markierung> oder GoTo <Markierung>
13.3.7 Fehlernummern und -beschreibungen Mit der Number- und der Description-Eigenschaft erhalten Sie Informationen zu einem Fehler. Wenn sie einmal nur eine Nummer haben – etwa, weil ein Anwender nicht mehr Informationen liefern kann – können Sie die dazugehörende Fehlernummer meist mit der Error-Funktion ermitteln. Dazu setzen Sie im Direktfenster beispielsweise folgende Anweisung ab: Debug.Print Error(429)
Das Direktfenster gibt dann folgerichtig »Objekterstellung durch ActiveX-Komponente nicht möglich« aus. Informationen über die einzelnen Fehlernummern und -beschreibungen finden Sie in der VBA-Onlinehilfe unter dem Stichwort »Auffangbare Fehler«.
13.3.8 Benutzerdefinierte Fehlerbehandlung temporär ausschalten Manchmal kann es sinnvoll sein, die Fehlerbehandlung zu deaktivieren. Dazu kommentieren Sie dann einfach die On Error…-Anweisung aus. Ist davon nur eine Routine betroffen, hält sich der Aufwand in Grenzen. Anders ist es, wenn Sie die Fehlerbehandlung in mehreren Routinen ausschalten möchten.
705
Kapitel 13
Glücklicherweise stellt die VBA-Entwicklungsumgebung eine Möglichkeit zum anwendungsweiten Abschalten der benutzerdefinierten Fehlerbehandlung zur Verfügung. Damit alle Fehler von Access angezeigt werden, als ob keine benutzerdefinierte Fehler behandlung vorhanden sei, stellen Sie die Option Unterbrechen bei Fehlern des OptionenDialogs auf den Wert Bei jedem Fehler ein (siehe Abbildung 13.12).
Abbildung 13.12: Optionen für die Handhabung von Fehlern einstellen
Die Routine aus Listing 13.2 erzeugt dann die Standardfehlermeldung von Access (siehe Abbildung 13.13). Normalerweise sollten Sie die Option aber auf den Wert Bei nicht verarbeiteten Fehlern einstellen.
Abbildung 13.13: Access-eigene Fehlermeldung
13.3.9 Funktionale Fehlerbehandlung Fehlermeldungen können auch zur Vereinfachung von Funktionalität verwendet werden. Wie das funktioniert, zeigen die beiden folgenden Beispiele.
706
Debugging, Fehlerbehandlung und Fehlerdokumentation
Prüfen auf Duplikate und Einfügen von Daten auf einen Streich Im ersten Beispiel erzeugt das Anfügen eines Wertes an eine Tabelle einen Fehler, wenn der Wert schon vorhanden und das Feld mit einem eindeutigen Index versehen ist. Man könnte diesen Fehler umgehen, indem man zuvor prüft, ob der anzulegende Wert schon in der Zieltabelle enthalten ist. Das würde einen Zugriff auf die Zieltabelle erfordern, das anschließende Hinzufügen des Datensatzes einen weiteren. Den ersten Zugriff können Sie sich sparen, indem Sie den Datensatz einfach direkt hinzufügen und den im Falle eines doppelten Datensatzes auftretenden Fehler entsprechend behandeln. Das folgende Listing zeigt, wie das aussehen kann. Dort ist eine Fehlerbehandlung wie oben beschrieben eingebaut, die im Auswertungsteil prüft, ob der ausgelöste Fehler die Nummer 3022 hat. Dies ist die Nummer des Fehlers, der beim Hinzufügen von bereits vorhandenen Werten in ein eindeutiges Feld auftritt. Statt der von Access ausgegebenen Standardmeldung (siehe Abbildung 13.14) erhält der Benutzer der Anwendung eine deutlich aussagekräftigere Meldung (siehe Abbildung 13.15). Public Function FirmaAnfuegen(strFirma As String) On Error GoTo FirmaAnfuegen_Err Dim db As DAO.Database Set db = CurrentDb db.Execute "INSERT INTO tblFirmen(Firma) VALUES('" _ & strFirma & "')", dbFailOnError 'Fehlerbehandlung FirmaAnfuegen_Exit: 'Restarbeiten Set db = Nothing Exit Function FirmaAnfuegen_Err: If Err.Number = 3022 Then MsgBox "Die Firma ist bereits vorhanden. " _ & "Bitte geben Sie einen anderen Firmennamen ein." End If GoTo FirmaAnfuegen_Exit End Function Listing 13.3: Routine mit einer zielgerichteten Fehlerbehandlung
Vorhandensein von Objektinstanzen per Fehlerbehandlung prüfen Eine andere Möglichkeit, die Fehlerbehandlung als Ersatz komplexerer Funktionalität zu nutzen, ist folgende. Hier soll eine Instanz eines Objekts erzeugt werden, sofern noch keine Instanz dieses Objekts vorhanden ist. Dazu greift man einfach auf die benötigte Instanz zu und prüft, ob daraus ein Fehler resultiert. Das ist der Fall, wenn das Objekt noch nicht existiert; anderenfalls erzeugt der Zugriff keinen Fehler.
Die folgende Routine versucht, mit der GetObject-Anweisung auf eine bestehende Instanz von Word zuzugreifen. Durch die vorherige On Error Resume Next-Anweisung läuft die Prozedur auf jeden Fall weiter, egal ob ein Fehler auftritt oder auch nicht. Ob es einen Fehler gab, wird in der nächsten Zeile geprüft: Die Fehlernummer 429 deutet darauf hin, dass die Zuweisung des Objekts fehlgeschlagen ist, worauf eine neue Instanz erzeugt wird. Wenn Word auf dem Rechner gar nicht installiert ist, erzeugt auch dies einen Fehler mit der Nummer 429, der mit einer entsprechenden Fehlermeldung quittiert wird. Zwischendurch müssen Sie das Err-Objekt übrigens immer wieder initialisieren, da sonst auch die Fehlernummer beibehalten wird. Dazu können Sie die Clear-Methode des Err-Objekts verwenden, aber auch eine folgende On Error-Anweisung initialisiert das Err-Objekt. Public Sub ZugriffAufWord() Dim objWord As Object On Error Resume Next Set objWord = GetObject(, "Word.Application") If Err.Number = 429 Then 'Err-Objekt initialisieren On Error Resume Next
708
Debugging, Fehlerbehandlung und Fehlerdokumentation Set objWord = CreateObject("Word.Application") If Err.Number = 429 Then Msgbox "Word ist auf diesem Rechner nicht installiert", _ vbCritical End If End If On Error GoTo 0 objWord.Visible = True End Sub Listing 13.4: Word-Instanz holen und bei Fehler neu erstellen
13.3.10 Benutzerdefinierte Fehler Mit der bereits weiter oben kurz vorgestellten Raise-Methode des Err-Objekts können Sie benutzerdefinierte Fehler erzeugen. Benutzerdefinierte Fehler erzeugen? Reichen die von Access erzeugten Fehler nicht? Vermutlich schon. Die Raise-Methode bietet vielmehr die Möglichkeit, Eingabefehler oder dergleichen in einem Zuge mit den sonstigen Fehlern zu behandeln und diesbezüglich eine einheitliche Form zu finden. Die Funktion aus Listing 13.3 bietet ein gutes Beispiel. Mit den Erweiterungen aus folgendem Listing prüft die Funktion, ob eine leere Zeichen kette als Firmenname übergeben wurde. Ist das der Fall, löst sie einen Fehler mit der Nummer vbObjectError + 1 aus, wobei vbObjectError eine in VBA eingebaute Konstante ist, die eine Kollision mit den eingebauten Fehlernummern verhindern soll. Beim Auftreten dieses Fehlers springt die Routine wie bei einem herkömmlichen Fehler in die Fehlerbehandlungsroutine und gibt eine entsprechende Meldung aus. Public Function FirmaAnfuegen(strFirma As String) On Error GoTo FirmaAnfuegen_Err Dim db As DAO.Database If Len(strFirma) = 0 Then Err.Raise vbObjectError + 1 End If Set db = CurrentDb db.Execute "INSERT INTO tblFirmen(Firma) VALUES('" & strFirma _ & "')", dbFailOnError Set db = Nothing 'Fehlerbehandlung FirmaAnfuegen_Exit: 'Restarbeiten Exit Function FirmaAnfuegen_Err: Select Case Err.Number Case 3022
709
Kapitel 13 MsgBox "Die Firma ist bereits vorhanden. " _ & "Bitte geben Sie einen anderen Firmennamen ein." Case vbObjectError + 1 MsgBox "Bitte geben Sie einen Firmennamen an." End Select GoTo FirmaAnfuegen_Exit End Function Listing 13.5: Auslösen eines benutzerdefinierten Fehlers
Sie können der Raise-Methode noch weitere Parameter wie Source (entspricht dem aktuellen VBA-Projekt) oder vor allem Description übergeben, in der Sie den anzuzeigenden Fehlertext unterbringen.
13.3.11 Fehler bei API-Aufrufen Wenn Sie mit API-Funktionen arbeiten, kann es ebenfalls zu Fehlern kommen, die aber keine Fehlermeldung anzeigen oder den Debugger aufrufen. Die Fehlerursache kann man etwa auf falsche Parameterwerte oder auch auf Systemfehler von Windows zurückführen. API-Funktionen geben so gut wie immer einen Long-Wert zurück, der anzeigt, ob sie erfolgreich durchgeführt werden konnten. Falls nicht, dann ist der Rückgabewert häufig entweder 1 oder 0. Das ist natürlich wenig aussagekräftig und aus diesem Grund sieht das Windows-API vor, dass der genaue Fehler-Code mit der Funktion GetLastError unmittelbar im Anschluss an die fehlgeschlagene Funktion ermittelt werden kann. VBA kennt dazu die synonyme Eigenschaft LastDllError des Err-Objekts. Die Beispielprozedur TestAPIFehler versucht, per API das Access-Hauptfenster zu maximieren. Dazu kommt die Funktion ShowWindow zum Einsatz, die als Parameter für das Maximieren eine Konstante SW_MAXIMIZED mit dem Long-Wert 3 erwartet. Fälschlicherweise wird sie im Code aber als 300 deklariert. Private Declare Function ShowWindow Lib "user32.dll" ( _ ByVal hwnd As Long, ByVal nCmdShow As Long) As Long Private Declare Function FormatMessage Lib "kernel32.dll" _ Alias "FormatMessageA" (ByVal dwFlags As Long, _ ByRef lpSource As Any, ByVal dwMessageId As Long, _ ByVal dwLanguageId As Long, ByVal lpBuffer As String, _ ByVal nSize As Long, ByRef Arguments As Long) As Long Sub TestAPIFehler() Dim ret As Long Dim sError As String Dim nErr As Long Dim lLen As Long
710
Debugging, Fehlerbehandlung und Fehlerdokumentation Const SW_SHOWMAXIMIZED As Long = 300
'korrekt: 3
ret = ShowWindow(Application.hWndAccessApp, SW_SHOWMAXIMIZED) If ret = 0 Then nErr = Err.LastDllError sError = String(255, 0) lLen = FormatMessage(&H1000, 0&, nErr, 0&, sError, 255, 0&) Err.Raise vbObjectError, "TestAPIError", _ "API-Fehler Nr. " & nErr & ":" & vbCrLf & Chr(34) _ & Left(sError,lLen - 2) & Chr(34) End If End Sub Listing 13.6: Fehlerbehandlung für Fehler beim Aufruf von API-Funktionen
Dass die Funktion mit diesem Parameter fehlschlägt, erkennt man laut Windows-SDK am Rückgabewert 0. Im Folgenden wird deshalb Err.LastDllError bemüht, um die Windows-Fehlernummer zu erhalten – in diesem Fall 87. Nun wissen Sie allerdings noch nicht, was diese Zahl bedeutet. Mit der API-Funktion FormatMessage lässt sich jedoch der Fehlertext ermitteln, in der Stringvariablen sError speichern und mit Err.Raise an die benutzerdefinierte Fehlerauslösung leiten – Ergebnis: »Falscher Parameter«. Wenn eine API-Funktion zum Absturz von Access führt, dann nutzt diese Technik allerdings wenig. Solche Fehler kann nur der Windows-Debugger selbst abfangen und mit einer Beendigung des auslösenden Prozesses quittieren.
13.4 Fehlerdokumentation und -übermittlung Ziel der Fehlerbehandlung in Anwendungen, die man an andere Benutzer weitergibt, muss sein, diesen eine möglichst robuste Anwendung zu bieten, die sich nach Auftreten eines Fehlers nicht wortlos verabschiedet, sondern dem Benutzer die Möglichkeit bietet, dem Entwickler Informationen über aufgetretene Fehler zukommen zu lassen. Für die Robustheit ist freilich nicht nur die Fehlerbehandlung, sondern die komplette Anwendung verantwortlich. Indirekt führt aber auch eine sinnvolle Fehlerbehandlung wieder zu mehr Robustheit, wenn diese eine Übermittlung der Informationen zu aufgetretenen Fehlern und damit deren Behebung ermöglicht. Die Übermittlung von Fehlermeldungen per Telefon und E-Mail führt normalerweise zu folgendem Satz aus dem Munde des Entwicklers: »Also nun versuchen Sie noch einmal, den Fehler zu reproduzieren. Und wenn dann die Fehlermeldung erscheint, machen Sie davon einen Screenshot. Und dann klicken Sie auf Debuggen und machen noch einen Screenshot und schicken mir den bitte zu.« Worauf der Benutzer nicht selten
711
Kapitel 13
eine der folgenden Antworten liefert: »Also, ich weiß wirklich nicht mehr, wodurch ich diesen Fehler ausgelöst habe …« oder »Was ist denn ein Screenshot?« Irgendwie erhält man dann zwar die gewünschten Informationen, aber dennoch muss man hier zu der Erkenntnis kommen: So funktioniert das nicht. Die Übermittlung von Fehlern sollte so weit automatisiert sein, dass der Benutzer höchstens noch eine Textdatei mit den notwendigen Informationen per E-Mail an den Entwickler schickt. Und dies zu realisieren, ist gar nicht so schwer.
13.4.1 Wichtige Fehlerinformationen Damit Sie, der Entwickler, etwas mit einer Fehlermeldung des Kunden anfangen können, benötigen Sie folgende Informationen: Datum/Zeit Datenbankpfad und -name Modul, in dem der Fehler aufgetreten ist Routine, in der der Fehler aufgetreten ist Nummer der fehlerhaften Zeile Aktueller Benutzer Arbeitsrechner/Workgroup oder Domäne Fehlernummer Fehlerbeschreibung Bemerkungen Mit diesen Informationen können Sie den Fehler vermutlich nachvollziehen, und wenn das nicht genügend Anhaltspunkte liefert, können Sie immer noch den Anwender kontaktieren und nach näheren Umständen fragen – das sollte aber eigentlich nicht mehr notwendig sein. Natürlich können Sie noch weitere Informationen hinzufügen, um die Fehlersuche weiter zu vereinfachen.
13.4.2 Zeilen nummerieren Wenn Sie sich die Auflistung durchsehen, werden Sie vermutlich an einer Stelle hängen bleiben: bei der Nummer der fehlerhaften Zeile. Wie bekommt man die denn nun heraus? Die Frage ist verständlich: Immerhin sind die Zeiten, in denen man BasicProgramme mit Zeilennummern versah, lange vorbei – und als Sprungmarke kommen die bereits oben erwähnten Zeichenketten zum Zuge. Und nun soll man sich für eine Fehlerdokumentation in die Frühzeit der Programmierung zurückversetzen?
712
Debugging, Fehlerbehandlung und Fehlerdokumentation
In der Tat liefert VBA keine versteckte Zeilennummer oder eine andere interne Möglich keit, die Nummer einer fehlerhaften Zeile zu ermitteln. Sie müssen tatsächlich alle Zei chen einer Routine durchnummerieren, wenn Sie wissen wollen, in welcher Zeile der Fehler auftritt – einige Zeilen lassen sich zwar nicht nummerieren, doch dazu später mehr. Die nächste Frage lautet dann: Wie teile ich der Fehlerbehandlung denn nun mit, in welcher Zeile der Fehler auftrat? Dazu gibt es wiederum eine nicht dokumentierte Funktion. Sie heißt Erl und liefert – sofern vorhanden – die Nummer der Zeile, in der der letzte Fehler aufgetreten ist. Zum Nachweis ein kleines Experiment: Fügen Sie der Routine aus Listing 13.2 Zeilennummern hinzu und ergänzen Sie die Fehlermeldung wie folgt: Public Function DivisionDurchNull() 10 On Error GoTo DivisionDurchNull_Err 20 Dim i As Integer 30 i = 1 / 0 40 'Fehlerbehandlung 50 DivisionDurchNull_Exit: 60 'Restarbeiten 70 Exit Function 80 DivisionDurchNull_Err: 90 MsgBox "Fehlernummer: " & Err.Number _ & vbCrLf & "Beschreibung: " & Err.Description _ & vbCrLf & "Zeile: " & Erl 100 GoTo DivisionDurchNull_Exit End Function Listing 13.7: Routine mit Zeilennummern und Ausgabe der Zeilennummer im Falle eines Fehlers
Das Ausführen der Routine führt zu der Meldung aus Abbildung 13.16 – die fehlerhafte Zeile wird also wie geplant ausgegeben.
Abbildung 13.16: Fehlermeldung mit Zeilenangabe
Komplettes VBA-Projekt mit Zeilennummern und Fehlerbehandlungen ausstatten Da prinzipiell überall einmal der Fehlerteufel zuschlagen kann, sollten Sie den kompletten Code mit Zeilennummern und entsprechend erweiterten Fehlerbehandlungen aus-
713
Kapitel 13
statten. Was? Den ganzen Code? Jede kleine Routine in jedem einzelnen Modul? Und soll ich etwa die Nummerierung immer wieder anpassen, wenn ich mal ein paar Zeilen einfüge? Keine Angst: Dieses Buch verlangt nichts von Ihnen, was nicht den Aufwand lohnte. Und in diesem Fall liefert es auch noch ein Tool mit, das stundenlange Tipparbeit in Sekundenbruchteilen erledigt. Das besagte Tool heißt accessVBATools und liefert eine neue Symbolleiste mit einigen Be fehlen (siehe Abbildung 13.17) – zum Beispiel: Modul nummerieren: Nummeriert alle Zeilen des aktuellen Moduls durch. Modul entnummerieren: Entfernt die Nummerierung des aktuellen Moduls. Fehlerbehandlung hinzufügen: Fügt der Routine, in der sich aktuell die Einfügemarke befindet, eine Fehlerbehandlung hinzu. Fehlerdokumentations-Funktion hinzufügen: Fügt dem VBA-Projekt ein Standardmodul mit einer Funktion zur Dokumentation von Fehlern hinzu. Die Funktionen lassen sich nicht nur über die Symbolleiste, sondern auch über das Kontextmenü des aktuellen Moduls aufrufen (siehe Abbildung 13.18). Natürlich gibt es noch weitere Tools, die Funktionen wie das Nummerieren von Zeilen oder andere mitbringen – mehr dazu auf http://www.access-entwicklerbuch.de. Das hier beschriebene Tool liefert allerdings die nachfolgend dargestellte Fehlerbehandlung und die darauf abgestimmte Fehlerdokumentationsfunktion mit.
13.4.3 Einsatz der accessVBATools Die vier Funktionen der accessVBATools helfen Ihnen dabei, Ihre Anwendungen mit einer professionellen Fehlerbehandlung und -dokumentation zu versehen. Das Tool liegt auf der Buch-CD in Form einer .dll-Datei unter dem Dateinamen Tools\accessVBATools. dll vor. Diese .dll-Datei kopieren Sie in ein Verzeichnis Ihrer Wahl (vorzugsweise c:\ Windows\System32) und registrieren diese über den Ausführen …-Dialog von Windows mit der Anweisung regsvr32.exe c:\Windows\ System32\accessVBATools.dll. Anschließend öffnen Sie die VBA-Entwicklungsumgebung erneut und finden die neue Symbolleiste sowie die Einträge im Kontextmenü vor.
Nummerierungen hinzufügen und entfernen Mit den beiden Befehlen Modul nummerieren und Modul entnummerieren können Sie die Nummerierung nach Belieben hinzufügen und wieder entfernen – zumindest in der Entwicklungsphase. Das macht das Nummerieren besonders einfach: Wenn Sie die Anwendung testen, fügen Sie die Nummerierung hinzu, wenn Sie Änderungen am Code vornehmen, entfernen Sie die Nummerierung, ändern den Code und fügen die Nummerierung wieder hinzu.
714
Debugging, Fehlerbehandlung und Fehlerdokumentation
Abbildung 13.17: accessVBATools im Einsatz
Abbildung 13.18: Aufruf der accessVBATools per Kontextmenü
715
Kapitel 13
Sobald Sie eine Anwendung einmal weitergegeben haben, müssen Sie eine unveränderte Kopie dieser Version behalten, um auf Fehlermeldungen reagieren zu können, die der Benutzer Ihnen mitteilt – sonst würden die Zeilennummern unter Umständen doch wieder nicht weiterhelfen.
Fehlerbehandlung hinzufügen Die dritte Funktion der accessVBATools fügt einer Prozedur eine Fehlerbehandlung wie im folgenden Listing hinzu – in diesem Fall ausgehend von einem leeren Prozedurrumpf: Public Sub Beispielfehlerbehandlung() On Error GoTo Beispielfehlerbehandlung_Err 'Fehlerbehandlung Beispielfehlerbehandlung_Exit: 'Restarbeiten Exit Sub Beispielfehlerbehandlung_Err: Call Fehlerbehandlung("Fehlerbehandlung - Modul1 ", _ "Beispielfehlerbehandlung", Erl, "Bemerkungen: ./.") GoTo Beispielfehlerbehandlung_Exit End Sub Listing 13.8: Eine ansonsten nackte Routine nach der Bestückung mit einer Fehlerbehandlung
Die Fehlerbehandlung entspricht weitgehend dem weiter oben vorgeschlagenen Muster. Einzige Ausnahme ist, dass keine Fehlermeldung per MsgBox-Anweisung ausgegeben, sondern eine Funktion namens Fehlerbehandlung aufgerufen wird – was eine passende Überleitung zur vierten Funktion ist.
Funktion zur Fehlerdokumentation hinzufügen Die noch fehlende Funktion Fehlerbehandlung wird durch den letzten Eintrag der accessVBATools hinzugefügt – und zwar samt eigenem Standardmodul. Dieses hat den folgenden Inhalt: Option Compare Database Option Explicit Private Declare Function GetComputerName Lib "kernel32.dll" _ Alias "GetComputerNameA" (ByVal lpBuffer As String, _ ByRef nSize As Long) As Long Public Function Fehlerbehandlung(strModul As String, _ strRoutine As String, lngZeile As Long, _ Optional strBemerkungen As String)
716
Debugging, Fehlerbehandlung und Fehlerdokumentation On Error Resume Next Open CurrentProject.Path & "\Fehler.log" For Append As #1 Print Print Print Print Print Print Print Print Print Print Print Close Reset
MsgBox "Es ist ein Fehler aufgetreten. " & vbCrLf _ & "Weitere Informationen finden Sie in der Datei Fehler.log im " _ & " Verzeichnis dieser Datenbank." End Function Function ComputerName() As String Dim strComputer As String Dim n As Long n = 255 strComputer = String$(n, 0) Call GetComputerName(strComputer, n) ComputerName = Left(strComputer, n) End Function Listing 13.9: Diese Routinen legen einen Eintrag für eine Fehlermeldung in einer Textdatei an
Wenn Sie nach Anlegen dieses Moduls eine fehlerhafte und nummerierte Routine starten, die die Prozedur Fehlerbehandlung aufruft, zeigt Access die folgende Meldung an (siehe Abbildung 13.19).
Abbildung 13.19: Die Fehlerbehandlung sorgt beim Auftreten eines Fehlers für die Anzeige dieser Meldung
717
Kapitel 13
Die dort angegebene Datei sieht wie in Abbildung 13.20 aus. Die Datei enthält einige Informationen, die zum Auffinden der meisten Fehler führen sollten. Sie können die Fehlerbehandlung natürlich nach Belieben erweitern, um etwa Informationen über das verwendete Betriebssystem, die Office-Version und mehr zu extrahieren. Sie müssen dem Benutzer nur noch mitteilen, dass er Ihnen diese kleine Textdatei zusendet. Dazu können Sie leicht die in der Prozedur aus Listing 13.9 ausgegebene Meldung anpassen – etwa mit einer Erweiterung wie »Zum Beheben des Fehlers schicken Sie diese Textdatei bitte an <E-Mail-Adresse>«. Sie sollten sich auch gut überlegen, ob Sie dem Anwender wirklich jeden sich ereignenden Fehler mit einer Meldung zu Gesicht bringen wollen. Erhält dieser häufig solche Meldungen, dann schwindet möglicherweise sein Vertrauen in die Anwendung und damit in Ihre Fähigkeiten. Eine Möglichkeit, dies zu umgehen, wäre etwa, am Ende der Fehlerbehandlungsroutine die Dateilänge des Fehlerlogs zu ermitteln (VBA.FileLen) und nur dann eine Meldung zu erzeugen, wenn diese Größe ein bestimmtes Limit wie etwa fünf Kilobyte erreicht hat.
Abbildung 13.20: Dokumentation eines Fehlers per Textdatei
Bemerkungen in der Fehlerbehandlung Der Aufruf der oben beschriebenen Fehlerdokumentationsfunktion bietet einen Parameter namens Beschreibung an. Hier können Sie zusätzliche Informationen unterbringen – etwa um die Werte bestimmter Variablen innerhalb der fehlerhaften Routine auszugeben.
Mögliche Erweiterungen der Fehlerbehandlung Wer regelmäßig mit Microsoft-Produkten arbeitet, ist vermutlich bereits einmal bei einem Programmabsturz mit der damit verbundenen Anfrage der Anwendung konfrontiert worden, nähere Informationen an Microsoft zu senden. Solch eine Funktion ist natürlich auch für Access-Anwendungen denkbar. Die notwendigen Informationen könnten dabei via E-Mail (entweder über den vorhandenen E-Mail-
718
Debugging, Fehlerbehandlung und Fehlerdokumentation
Client oder direkt über die WinSock-Schnittstelle) oder auch durch automatischen Auf ruf einer Internetseite mit entsprechenden Parametern übertragen werden. Leider würde die Ausarbeitung einer solchen Lösung den Rahmen dieses Kapitels sprengen.
13.5 Fehlerbehandlung in Formularen Die bisher vorgestellten Möglichkeiten der Fehlerbehandlung beziehen sich komplett auf Fehler, die beim Ablauf von VBA-Routinen auftreten – wenn auch manchmal andere Ursachen dahinter stehen, wie etwa Fehler in eingebetteten SQL-Anweisungen. Formulare können Fehler liefern, ohne auch nur eine einzige Zeile VBA-Code zu enthalten. Die dabei ausgegebenen Fehlermeldungen sind für normale Benutzer manchmal ebenso wenig aussagekräftig wie solche, die durch Fehler im VBA-Code ausgelöst werden. Deshalb gilt auch hier: Behandeln Sie alle denkbaren Fehler und sorgen Sie dafür, dass der Benutzer Fehlermeldungen erhält, mit denen er etwas anfangen kann. Das folgende Beispiel greift das Problem mit den doppelten Datensätzen in eindeutigen Feldern aus Listing 13.3 erneut auf – diesmal allerdings ohne VBA-Code, aber mit einem Formular zur Eingabe von Firmen. Ein doppelter Eintrag führt zu der gleichen Meldung wie unter VBA, nur die Fehlernummer fehlt (siehe Abbildung 13.21).
Abbildung 13.21: Doppelte Werte in eindeutigen Feldern werden auch in Formularen mit einer unverständlichen Meldung kommentiert
Auch hier besteht zum Glück die Möglichkeit einzugreifen. Ansatzpunkt ist die Ereignis eigenschaft Bei Fehler des Formulars. Die dadurch ausgelöste Prozedur sieht als nacktes Gerüst wie im folgenden Listing aus: Private Sub Form_Error(DataErr As Integer, Response As Integer) End Sub Listing 13.10: Die Ereignisprozedur Form_Error im Rohbau
719
Kapitel 13
Die Prozedur hat zwei Parameter, von denen der erste von Access mit der Fehlernummer gefüllt wird und der zweite einen Wert erwartet, der Access über die weitere Vorgehens weise bezüglich des Fehlers informiert. Für den Parameter Response gibt es folgende Werte: acDataErrContinue: Die Standardfehlermeldung von Access wird nicht angezeigt. acDataErrDisplay: Die Standardfehlermeldung von Access wird angezeigt.
13.5.1 Behandlung von Formularfehlern Wenn Sie einen Formularfehler wie den obigen behandeln möchten, müssen Sie zunächst einmal seine Fehlernummer ermitteln. Dazu legen Sie die folgende Prozedur für die Ereigniseigenschaft Bei Fehler an und lassen sich darüber die Fehlernummer ausgeben, die mit dem Parameter DataErr übergeben wird (siehe Abbildung 13.22): Private Sub Form_Error(DataErr As Integer, Response As Integer) MsgBox "Fehler-Nummer: " & DataErr End Sub Listing 13.11: Routine zum Ermitteln der Fehlernummer
Abbildung 13.22: Fehlernummer des Fehlers im Formular
Wenn Sie die Fehlernummer kennen, können Sie diesen Fehler gezielt behandeln. Dazu erstellen Sie in der Ereignisprozedur On_Error eine geeignete Fehlerbehandlung: Private Sub Form_Error(DataErr As Integer, Response As Integer) Select Case DataErr Case 3022 Response = acDataErrContinue MsgBox "Die Firma ist bereits vorhanden. " _ & "Bitte geben Sie einen anderen Firmennamen ein." Case Else Response = acDataErrDisplay End Select End Sub Listing 13.12: Behandlung von Formularfehlern
Die Routine sorgt dafür, dass beim Auftreten eines Fehlers mit der Nummer 3022 eine aussagekräftige Meldung ausgegeben und die Standardfehlermeldung durch Setzen
720
Debugging, Fehlerbehandlung und Fehlerdokumentation
des Parameters Response auf den Wert acDataErrContinue unterdrückt wird. Bei allen anderen Fehlernummern wird hingegen die dafür vorgesehene Standardmeldung ausgegeben.
13.5.2 Formularfehler dokumentieren Auch die in Formularen auftretenden Fehler sollten Sie dokumentieren, sofern diese nicht anderweitig verarbeitet werden. Um den Fehler aus Listing 13.12 brauchen Sie sich keine Gedanken mehr zu machen. Die Benutzer Ihrer Datenbankanwendung finden aber sicher noch den einen oder anderen Fehler, mit dem Sie nicht gerechnet haben. Dafür bauen Sie eine Fehlerdokumentation wie weiter oben beschrieben ein. In der Ereignisprozedur On_Error sieht das wie folgt aus: Private Sub Form_Error(DataErr As Integer, Response As Integer) Select Case DataErr Case 3022 Response = acDataErrContinue MsgBox "Die Firma ist bereits vorhanden. " _ & "Bitte geben Sie einen anderen Firmennamen ein." Case Else Call Fehlerbehandlung("Form_frmFirmen ", "", 0, "Bemerkungen: Fehlernummer " & DataErr) Response = acDataErrContinue End Select End Sub Listing 13.13: Behandlung unvorhergesehener Fehler in Formularen
Da Formularfehler keine Fehlernummer über das Err-Objekt bereitstellen, wird hier alternativ die Fehlernummer in die Bemerkungen zum Fehler eingetragen.
721
14 Performance Einer der wichtigsten Aspekte für die Akzeptanz einer Anwendung beim Benutzer ist ihre Performance. Eine An wendung, die Aufgaben nicht zügig in der gewünschten Zeit erledigt, ist von vornherein zum Scheitern verurteilt – die Benutzer verzichten in diesem Fall oft lieber darauf. Deshalb widmet dieses Buch dem Thema Performance ein eigenes Kapitel. Dass dies gerechtfertigt ist, werden Sie im Folgenden sehen: Access birgt in allen Bereichen eine Men ge Möglichkeiten für »Performance-Schlucker« und ebenso viel Potenzial für Optimierungen: Dies zieht sich durch alle Bereiche wie Tabellen, Abfragen, Formulare, Berichte und Module.
14.1 Tabellen Tabellen bilden die Basis einer Datenbankanwendung. Der Aufbau der Tabellen und der Verknüpfungen zwischen den Tabellen entscheidet wesentlich über die Performance der gesamten Anwendung, da alle weiteren Objekte wie Abfragen, gebundene Formulare und Berichte wie auch viele VBA-Prozeduren auf Tabellen zugreifen.
14.1.1 Normalisieren des Datenmodells Das Normalisieren des Datenmodells ist der erste Schritt, um einen schnellen Zugriff auf die in den Tabellen enthaltenen Daten zu ermöglichen. Welche Schritte zur Normali sierung eines Datenmodells notwendig sind, erfahren Sie in Kapitel 2, »Tabellen und Datenmodellierung«.
Kapitel 14
Die Beispieldatenbank zu diesem Kapitel finden Sie auf der Buch-CD unter \Kap_14\Performance.accdb. Für die Performance ist die Normalisierung des Datenmodells wichtig, weil Sie damit beispielsweise die Menge der gespeicherten Daten reduzieren – etwa durch das Extra hieren redundanter Daten in verknüpften Tabellen. Wenn Sie dabei neue Tabellen erzeugen, versäumen Sie nicht, eine Beziehung zwischen den neu entstandenen Tabellen zu erstellen. Das bringt einen Performance-Gewinn bei der Verknüpfung dieser Tabellen mittels JOIN in Abfragen. Falls Sie Beziehungen verwenden, bei denen beim Löschen eines Datensatzes in der Detailtabelle auch alle verknüpften Datensätze der Mastertabelle gelöscht werden sollen, können Sie keine schnellere Methode wählen, als die Löschweitergabe für diese Beziehung zu aktivieren. Diese Einstellung nehmen Sie in den Beziehungseigenschaften im Beziehungen-Fenster vor (siehe Abbildung 14.1). Gleiches gilt für die Aktualisierungs weitergabe, die Sie im gleichen Dialog aktivieren können.
Abbildung 14.1: Aktivieren der Löschweitergabe für eine Beziehung
Eine Suche über mehrere verknüpfte Tabellen, deren Daten zuvor in einer einzigen Tabelle gespeichert waren, kann unter Umständen auch langsamer sein als mit der vorherigen allein stehenden Tabelle. Immerhin kostet das Auflösen von Verknüpfungen innerhalb einer Abfrage ebenso Zeit wie die Suche nach den gewünschten Datensätzen. Wenn Sie tatsächlich einmal eine solche Konstellation vermuten, können Sie je nach Anwendungsfall mit einer INSERT INTO-Aktionsabfrage eine temporäre Tabelle aus den verknüpften Tabellen erzeugen, die keine Verknüpfungen mehr aufweist.
724
Performance
In der Regel sind durch das Normalisieren des Datenmodells keine Geschwindigkeits einbußen zu erwarten, die ein nicht normalisiertes Design rechtfertigen.
14.1.2 Indizes Die richtigen Indizes ermöglichen einen schnellen Zugriff auf die Daten einer Tabelle. Dieser Zugriff erfolgt meist durch Abfragen und bezieht sich oft auf mehr als eine Tabel le. Damit die ACE-Engine die Verknüpfungen schnell verarbeiten kann, sollten alle an einer Verknüpfung beteiligten Felder indiziert sein. Beim Primärschlüsselfeld ist das ohnehin der Fall: Es wird nicht nur indiziert, sondern es ist auch noch eindeutig und lässt keine Null-Werte zu. Auch für Fremdschlüsselfelder ist ein Index zu empfehlen. Wenn Sie eine Beziehung zwischen zwei Tabellen etwa im Beziehungen-Fenster herstellen (siehe Abbildung 14.2), legt Access automatisch einen Index für das Fremdschlüsselfeld der Detailtabelle an (siehe Abbildung 14.3).
Abbildung 14.2: Das Anlegen einer Beziehung im Beziehungen-Fenster …
Abbildung 14.3: … führt zum automatischen Anlegen eines Index für das Fremdschlüsselfeld
725
Kapitel 14
Auch für Felder, die nicht fester Bestandteil einer Beziehung sind, aber gegebenenfalls in einer Abfrage als Schlüsselfeld dienen, legt man einen Index an. Indizieren Sie auch Felder, die als Sortier- oder Filterkriterium in Abfragen dienen. Sparen Sie dabei nur jene Felder aus, die nur wenige unterschiedliche Werte enthalten wie beispielsweise Ja/Nein-Felder.
Kriterium für das Indizieren von Feldern Ob ein Index abhängig von den Datensätzen sinnvoll ist oder nicht, lässt sich mit der Eigenschaft DistinctCount eines Index über folgende Funktion berechnen. Die Funktion ermittelt das Verhältnis der enthaltenen verschiedenen Werte zu der Gesamtanzahl der Werte. Dazu übergeben Sie der Routine den Namen der zu prüfenden Tabelle und des Index. Ein Aufruf könnte beispielsweise wie folgt aussehen und das angegebene Ergebnis liefern (Beispiel aus der Nordwind-Datenbank): ? GetIndexDistinct ("Bestellungen", "Versanddatum") 0,467469879518072
Wenn das Ergebnis zwischen 0,1 und 0,5 liegt, macht der angelegte Index Sinn. Die Funktion sieht folgendermaßen aus: Function GetIndexDistinct(strTabelle As String, strIndex As String) _ As Double Dim Dim Dim Dim
db As Database tdf As TableDef lngDistinct As Long lngRecordCount As Long
Set db = CurrentDb Set tdf = dbs.TableDefs(strTabelle) lngDistinct = tdf.Indexes(strIndex).DistinctCount lngRecordCount = tdf.RecordCount GetIndexDistinct = lngDistinct / lngRCnt Set tdf = Nothing Set dbs = Nothing End Function Listing 14.1: Diese Routine prüft, ob ein Feld indexiert werden sollte
Die Routine funktioniert nicht mit verknüpften Tabellen, da die Recordcount-Eigenschaft des TableDef-Objekts hier den Wert –1 ausgibt. Und hinterfragen Sie nicht den Sinn dieser Funktion, wenn Sie diese mit einem Primär- oder eindeutigen Schlüssel testen – hier liefert die Funktion logischerweise den Wert 1.
726
Performance
Manchmal ist weniger mehr Das Indizieren von Feldern ist allerdings kein Wundermittel, wenn es um PerformanceSteigerungen geht. Wenn Sie mehrspaltige Indizes verwenden, binden Sie so wenig Fel der wie möglich in den Index ein. Verwenden Sie außerdem eindeutige Indizes, wenn dies möglich ist. Und zu guter Letzt: Indizieren Sie nicht zu viele Felder! Bedenken Sie, dass eine Da tenbankanwendung nicht nur zur Auswahl von Daten gedacht ist, sondern dass damit auch Daten angelegt, bearbeitet und gelöscht werden sollen. Bei all diesen Operationen müssen die Indizes aktualisiert werden, was wiederum zu Lasten der Performance geht. Als Leitsatz kann gelten: Je größer die beteiligten Tabellen sind und je mehr Datensätze sie enthalten, desto wichtiger sind Indizes beim Mehrbenutzerbetrieb, weil diese die zu übertragende Datenmenge in der Regel reduzieren.
14.1.3 Datentypen Die Auswahl passender Datentypen beeinflusst nicht nur die Performance, sondern auch den benötigten Speicherplatz der Datenbankanwendung.
Kleinstmögliche Datentypen wählen Wenn Sie Felder in Tabellen anlegen, wählen Sie den kleinstmöglichen Datentyp aus. Verwenden Sie kein Memo-Feld, wenn auch ein Textfeld reicht. Auch Zahlenformate bieten eine Menge Einsparpotenzial. Suchen Sie einfach aus folgender Tabelle den passenden Wertebereich heraus und verwenden Sie den entsprechenden Datentyp. Die Spalte Speicherbedarf gibt Ihnen Auskunft darüber, wie sehr die einzelnen Datentypen Speicherplatz und Performance beeinflussen (siehe Tabelle 14.1). Die landläufige Meinung, dass man Textfelder möglichst klein halten sollte, um nicht unnötig Speicherplatz zu verschwenden, ist nicht richtig. In der Tat können Sie jedes Textfeld mit 255 Zeichen als Feldgröße einrichten; wenn Sie dort aber nur Postleitzahlen mit je sieben Stellen ablegen, werden auch nur sieben Byte belegt.
OLE-Feld versus Anlage-Feld Der neue Anlage-Felddatentyp ist zwar bezüglich der Handhabung besser als das OLEFeld aufgestellt, allerdings sollte man sich dennoch überlegen, ob dies performancetechnisch auch die bessere Alternative ist. Der Grund ist, dass Daten im Anlage-Feld automatisch komprimiert werden, die in OLE-Feldern hingegen nicht. Wenn Sie also oft in den in einem dieser Felder zu speichernden Daten lesen oder diese oft schreiben, könnte der Einsatz eines OLE-Feldes Vorteile bringen.
727
Kapitel 14 Felddatentyp
Speicherbedarf
Wertebereich
Byte
1 Byte
0 bis 255
Boolean
2 Byte
True oder False
Integer
2 Byte
–32.768 bis 32.767
Long(lange Ganzzahl)
4 Byte
–2.147.483.648 bis 2.147.483.647
Single (Gleitkommazahl mit einfacher Genauigkeit)
4 Byte
–3,402823E38 bis –1,401298E-45 für negative Werte; 1,401298E-45 bis 3,402823E38 für positive Werte
Double (Gleitkommazahl mit doppelter Genauigkeit)
8 Byte
–1,79769313486231E308 bis –4,94065645841247E-324 für negative Werte; 4,94065645841247E-324 bis 1,79769313486232E308 für positive Werte
Currency (skalierte Ganzzahl)
8 Byte
–922.337.203.685.477,5808 bis 922.337.203.685.477,5807
Decimal
14 Byte
+/–79.228.162.514.264.337.593.543.950.335 ohne Dezimalzeichen; +/–7,9228162514264337593543950335 mit 28 Nachkommastellen; die kleinste Zahl ungleich Null ist +/–0,0000000000000000000000000001
Tabelle 14.1: Datentypen und ihr Speicherbedarf
Gleiche oder ähnliche Datentypen in Beziehungen Primärschlüsselfeld und Fremdschlüsselfeld einer Beziehung sollten den gleichen Da tentyp besitzen. In einem Fall ist das (zumindest was den Namen betrifft) nicht möglich: Wenn das Primärschlüsselfeld den Datentyp Autowert hat, können Sie das Fremdschlüs selfeld natürlich nicht auf den gleichen Datentyp einstellen. Der Autowert entspricht dem Datentyp Long und seltener (zum Beispiel in replizierbaren Datenbanken) dem Typ Replikations-ID (GUID).
14.2 Abfragen Wenn eine Tabelle nicht gerade genau die Felder enthält, die für die Anzeige in einem Formular oder Bericht oder für die Verwendung in einer VBA-Prozedur benötigt werden, verwenden Sie eine Abfrage, um nur die notwendigen Informationen abzufragen.
14.2.1 Abfragen und die ACE-Engine Für die Optimierung von Abfragen kann die Kenntnis der bei der Verarbeitung von Ab fragen durch die ACE-Engine verwendeten Technik hilfreich sein.
728
Performance
Abfragen entstehen auf unterschiedliche Weise: durch Verwendung der Abfrage-Entwurfsansicht durch Eingabe eines SQL-Ausdrucks als Datensatzquelle von Formularen und Berichten oder als Datensatzherkunft von Kombinations- und Listenfeldern durch Zusammenstellung eines SQL-Ausdrucks in Form eines Strings und anschließende Verwendung in einer VBA-Prozedur Wie auch immer die Abfrage entstanden ist, verwendet die ACE-Engine letzten Endes den dahinter stehenden SQL-Ausdruck.
Kompilieren einer Abfrage Die Abfrage wird vor ihrer Ausführung zunächst kompiliert. Das heißt, dass die ACEEngine die Basisinformationen aus der Abfrage ermittelt: Basistabellen: Tabellen, die an der Abfrage beteiligt sind Tabellenfelder aus den Ausgabefeldern der Abfrage Vorhandene Indizes in den beteiligten Tabellen Kriterien Verknüpfungsfelder zwischen zwei Tabellen Sortierfelder
Optimierung der Abfrage Anschließend optimiert die ACE-Engine die Abfrage auf Basis dieser Informationen. Dabei erstellt sie unterschiedliche Ausführungspläne und ermittelt daraus die schnellste Variante. Den größten Einfluss auf die Wahl des Ausführungsplans hat dabei die Analyse der Basistabellen und der Verknüpfungen zwischen diesen Tabellen. Um den Zugriff auf die Basistabellen zu optimieren, verwendet die ACE-Engine drei Tabellen-Zugriffsstrategien: Table Scan, Index Range oder Rushmore Restriction. Wel che dieser Techniken die ACE-Engine benutzt, hängt von der Größe der jeweiligen Tabelle, der Anzahl der enthaltenen Indizes und der Art und Menge der Kriteriums felder ab. Beim Table Scan durchsucht die ACE-Engine jeden einzelnen Datensatz der Tabelle. Diese Methode wird angewendet, wenn Kriterienfelder nicht indiziert sind oder wenn die Kriterien die Ergebnismenge nur geringfügig einschränken.
729
Kapitel 14
Die Index Range-Methode wird verwendet, wenn nur ein indiziertes Feld mit einem Kriterium versehen ist. Die Suche nach den gewünschten Datensätzen erfolgt über den Index dieses Kriterienfeldes. Die Rushmore Restriction kommt zum Zuge, wenn mehr als ein Kriterienfeld indiziert ist. Weitere Informationen finden Sie weiter unten in diesem Kapitel, Abschnitt »Abfragen mit Rushmore optimieren«. Auch die Beziehungen zwischen den Tabellen spielen eine Rolle bei der Optimierung von Abfragen. Für die Auswertung der Beziehungen werden fünf verschiedene Strategien verwendet, die sich Nested Iteration Join, Index Join, Lookup Join, Merge Join und IndexMerge Join nennen. Welche Strategie zum Zuge kommt, hängt von der Beschaffenheit der beteiligten Tabellen, Felder und Indizes, vom Vorkommen von Null-Werten und von anderen Faktoren ab. Weitere Informationen hierzu finden Sie in: Access 2002 Desktop Developer’s Handbook, Litwin, Getz, Gunderloy, Sybex
Abfragen mit Rushmore optimieren Rushmore ist eine Technik zur Optimierung von Abfragen, die auf das Vorhandensein von mehreren Restriktionen auf indizierten Feldern abzielt. Die Rushmore-Optimierung funktioniert mit den folgenden Operationen: Index Intersection: Diese Operation wird auf Abfragekriterien folgenden Aussehens angewendet, wobei alle Kriterienfelder indiziert sein müssen: Spalte1 = "Ausdruck1" AND Spalte2 = "Ausdruck2"
Der Clou ist, dass die ACE-Engine bei der Rushmore-Optimierung nicht mit den eigentlichen Vergleichsoperatoren, sondern mit ihrem Index arbeitet. Dabei sucht die ACE-Engine zunächst nach allen Datensätzen, die die erste Bedingung erfüllen, und anschließend nach allen Datensätzen, die die zweite Bedingung erfüllen, und ermittelt schließlich die Schnittmenge daraus. Index Union: Die zweite Operation zielt auf durch OR verknüpfte Kriterien nach folgendem Schema ab: Spalte1 = "Ausdruck1" OR Spalte2 = "Ausdruck2"
Die Abfrage ermittelt hier ebenfalls alle Datensätze, die dem ersten, und alle Datensätze, die dem zweiten Kriterium entsprechen. Allerdings bildet sie hier nicht die Schnittmenge, sondern die Vereinigungsmenge der beiden Ergebnisse. Rushmore optimiert obige Abfragen deshalb so gut, weil es »Bitmaps« aus den IndexWerten erstellt und diese indiziert.
730
Performance
Kombiniere, kombiniere … Um die Abfrage wirklich optimal ausführen zu können, ermittelt die ACE-Engine den ungefähren Aufwand für die in Frage kommenden Tabellen-Zugriffsstrategien in Kom bination mit den möglichen Verknüpfungsstrategien und wendet schließlich die am günstigsten erscheinende Variante an.
Ausführung der Abfrage Wird die Abfrage ausgeführt, tritt noch ein sehr wichtiger Faktor auf: der Recordset-Typ. Diese Eigenschaft, die Sie beispielsweise in gespeicherten Abfragen im Eigenschaftsfens ter einstellen können (siehe Abbildung 14.4), entscheidet maßgeblich über die Geschwin digkeit der Abfrage. Eine Abfrage mit dem Recordset-Typ Dynaset gibt beispielsweise ein Ergebnis zurück, das lediglich die eindeutigen Schlüsselfelder – soweit vorhanden – der in der Abfrage enthaltenen Tabellen enthält. Es werden nur zusätzliche Daten für die Datensätze in den Speicher geladen, die beispielsweise für die Anzeige im Formular benötigt werden. Abfragen des Recordset-Typs Snapshot laden die kompletten Daten direkt in den Speicher – also alle Datensätze mit allen angegebenen Feldern. Das dauert natürlich erheblich län ger – erst recht, wenn nicht alle Daten in den Speicher passen und ein Teil des Abfrage ergebnisses auf der Festplatte zwischengespeichert werden muss.
Abbildung 14.4: Einstellen des Recordset-Typs einer Abfrage
731
Kapitel 14
Abfragestrategien unter der Lupe Access bietet die Möglichkeit, die bei einer Abfrage verwendeten Informationen (zumindest teilweise) auszuwerten. Mit der Anpassung eines Registry-Eintrags können Sie dafür sorgen, dass Access Informationen über die Durchführung von Abfragen in einer Textdatei ausgibt. Den entsprechenden Schlüssel müssen Sie zunächst in der Registry unter HKEY_LOCAL_ MACHINE\SOFTWARE\Microsoft\Office\12.0\Access Connectivity Engine\Engines hinzufügen. Legen Sie zunächst mit dem Kontextmenüeintrag Neu|Schlüssel einen neuen Schlüssel für den Schlüssel Engines an und nennen Sie diesen Debug. Wählen Sie dann auch dem Kontextmenü des neuen Schlüssels den Eintrag Neu|Zeichenfolge aus und nennen Sie den neuen Eintrag Jetshowplan. Stellen Sie dort den Wert auf On ein. Anschießend schreibt die ACE-Engine Informationen über die Ausführungspläne der Abfragen in eine Datei namens showplan.out. Diese Funktion ist nicht offiziell dokumentiert. Der Ausgabeort der Datei showplan.out liegt meist im aktuellen Datenbankverzeichnis, das Sie in den Access-Optionen unter Häufig verwendet|Standarddatenbankordner finden. Die Funktion lässt sich leider nicht auf eine einzelne Datenbank beschränken (es sei denn, Sie aktivieren und deaktivieren diesen Schlüssel per VBA von der entsprechenden Datenbank aus). Da diese Option demnach für alle Datenbanken gilt, wird die Datei Showplan.out schnell sehr groß und macht Access langsam; behalten Sie diese daher im Auge oder schalten Sie die Funktion nur bei Bedarf ein. Um die Funktion zu deaktivieren, entfernen Sie den Schlüssel entweder komplett oder stellen den Wert auf Off ein (das wirkt sich übrigens erst beim nächsten Start von Access aus). Das folgende Listing zeigt den Inhalt der Datei showplan.out für die Abfrage Bestellungs zusammenfassung der Nordwind-Datenbank von Access 2007. Der erste Teil enthält die für die Auswahl der Strategie für den Zugriff auf die Tabellen ermittelten Informationen, der zweite Teil die Reihenfolge bei der Abarbeitung der Verknüpfungen. --- Bestellungszusammenfassung --- Inputs to Query Table 'Status der Bestellung' Using index 'PrimaryKey' Having Indexes: PrimaryKey 4 entries, 1 page, 4 values which has 1 column, fixed, unique, primary-key, no-nulls Table 'Bestellungen' Table 'Bestelldetails' Table 'Status für Bestelldetails' Using index 'PrimaryKey' Having Indexes: PrimaryKey 6 entries, 1 page, 6 values which has 1 column, fixed, unique, primary-key, no-nulls - End inputs to Query -
732
Performance 01) Outer Join table 'Bestelldetails' to table 'Status für Bestelldetails' using index 'Status für Bestelldetails!PrimaryKey' join expression "[Bestelldetails].[Status-Nr]=[Status für Bestelldetails].[Status-Nr]" 02) Group result of '01)' 03) Outer Join table 'Bestellungen' to result of '02)' using temporary index join expression "Bestellungen.[Bestell-Nr]=[Gesamtpreis der Bestellung].BestellNr" 04) Outer Join result of '03)' to table 'Status der Bestellung' using index 'Status der Bestellung!PrimaryKey' join expression "Bestellungen.[Status-Nr]=[Status der Bestellung].[Status-Nr]" 05) Sort result of '04)' --- temp query --- Inputs to Query Recordset - End inputs to Query 01) Scan recordset 02) Restrict rows of table 01) using index 'UNKNOWN' for expression "[_VersionHistory_F5F8918F-0A3F-4DA9-AE71184EE5012880]=FK" 03) Sort result of '02)' --- Bestellungszusammenfassung --- Inputs to Query Table 'Status der Bestellung' Using index 'PrimaryKey' Having Indexes: PrimaryKey 4 entries, 1 page, 4 values which has 1 column, fixed, unique, primary-key, no-nulls Table 'Bestellungen' Table 'Bestelldetails' Table 'Status für Bestelldetails' Using index 'PrimaryKey' Having Indexes: PrimaryKey 6 entries, 1 page, 6 values which has 1 column, fixed, unique, primary-key, no-nulls - End inputs to Query 01) Outer Join table 'Bestelldetails' to table 'Status für Bestelldetails' using index 'Status für Bestelldetails!PrimaryKey' join expression "[Bestelldetails].[Status-Nr]=[Status für Bestelldetails].[Status-Nr]" 02) Group result of '01)' 03) Outer Join table 'Bestellungen' to result of '02)'
733
Kapitel 14 using temporary index join expression "Bestellungen.[Bestell-Nr]=[Gesamtpreis der Bestellung].BestellNr" 04) Outer Join result of '03)' to table 'Status der Bestellung' using index 'Status der Bestellung!PrimaryKey' join expression "Bestellungen.[Status-Nr]=[Status der Bestellung].[Status-Nr]" 05) Sort result of '04)' Listing 14.2: Inhalt der Datei showplan.out nach dem Durchführen einer komplexen Abfrage
Wenn Sie beim Tuning Ihrer Abfragen ins Detail gehen möchten, kann die Ausgabe der ACE-Engine durchaus weiterhelfen. Wenn Sie der Abfrage Bestellungszusammenfas sung beispielsweise ein Kriterium für das Feld Versanddatum hinzufügen und dieses Feld nicht indiziert ist, erhalten Sie im Ausführungsplan folgende Zeile als ersten Schritt: 01) Restrict rows of table Bestellungen by scanning testing expression "Bestellungen.Versanddatum>#1/1/2006#"
Das Schlüsselwort scanning bedeutet in diesem Zusammenhang, dass ein Table Scan durchgeführt wird – ein Index für dieses Feld ist nicht vorhanden. Besser wäre es, das Feld Versanddatum der zugrunde liegenden Tabelle Bestellungen mit einem Index zu versehen – auf diese Weise könnte die Rushmore-Technik verwendet werden. Die entsprechende Zeile in der Datei showplan.out sieht dann folgendermaßen aus: 01) Restrict rows of table Bestellungen using rushmore for expression "Bestellungen.Versanddatum>#1/1/2006#"
14.2.2 Datenbank mit kompilierten Abfragen ausliefern Die ACE-Engine kompiliert eine Abfrage beim ersten Ausführen dieser Abfrage nach dem Speichern. Das Kompilieren beinhaltet auch das Optimieren, weshalb es sinnvoll sein kann, die Abfrage zu bestimmten Anlässen neu zu kompilieren.
Bei Änderungen neu kompilieren Wie Sie weiter oben erfahren haben, hängt der Ausführungsplan und damit die Reihenfolge und Geschwindigkeit der einzelnen Schritte bei der Durchführung der Abfrage wesentlich von den Tabellen-Eigenschaften wie Indizes und Beziehungen und von Daten-Eigenschaften wie der Anzahl der Datensätze und der dadurch belegten Speicherseiten ab.
734
Performance
Wenn Sie Änderungen am Entwurf der Tabellen vornehmen, auf denen die zu optimierende Abfrage basiert, sollten Sie die Abfrage neu kompilieren, damit diese den neuen Gegebenheiten entsprechend optimiert wird. Auch die Anzahl der in den Tabellen enthaltenen Datensätze spielt eine Rolle. Versuchen Sie, die Tabellen möglichst mit realen Daten zu füllen, um die Abfragen optimiert ausliefern zu können.
Auslieferung im kompilierten Zustand Die ACE-Engine kompiliert eine Abfrage erst neu, wenn Sie eine Änderung am Entwurf vorgenommen haben, diese dann speichern und ausführen. Das Kompilieren ist in vielen Fällen aufwändiger als die eigentliche Durchführung der Abfrage. Liefern Sie daher die Datenbank immer mit kompilierten Abfragen aus. So erhält der Benutzer direkt bei der ersten Verwendung der Abfrage optimale Performance.
14.2.3 Gespeicherte Abfragen versus Ad-hoc-Abfragen Gespeicherte Abfragen haben den Vorteil, dass man sie durch Speichern und Kompilieren optimiert und damit in folgenden Einsätzen auf die dabei gewonnenen Erkenntnisse bezüglich der Performance zugreifen kann. So genannte Ad-hoc-Abfragen, die in VBA-Routinen zusammengesetzt und erst bei Bedarf das erste Mal kompiliert und optimiert werden, ziehen in der Regel den Kürzeren, was die Performance angeht – in den meisten Fällen sind daher gespeicherte Abfragen vorzuziehen. Auch hier gibt es allerdings eine Ausnahme: Weiter oben wurde bereits beschrieben, dass man Abfragen vor der Auslieferung möglichst mit der zu erwartenden Anzahl Datensätze in den betroffenen Tabellen kompiliert und speichert. Das ist aber nicht immer möglich. Wenn die Anzahl der Datensätze völlig anders ausfällt als geplant, ist eine gespeicherte Abfrage möglicherweise weniger performant als eine Abfrage, die vor jeder Ausführung neu erstellt wird.
14.2.4 Abfragen auf Performance trimmen Das Design von Abfragen bietet jede Menge Fallstricke, wenn es um gute Performance geht. Die folgenden Abschnitte enthalten deshalb den nötigen »Feinschliff« für Ihre Abfragen.
Nur notwendige Felder anzeigen Je mehr Felder eine Abfrage anzeigt, desto mehr leidet die Performance – das gilt vor allem für Abfragen mit dem Recordset-Typ Snapshot. Zeigen Sie daher nur die unbe-
735
Kapitel 14
dingt notwendigen Felder an. Prüfen Sie vor allem genau, ob die Abfrage gegebenenfalls Kriterienfelder enthält, die nicht unbedingt angezeigt werden müssen. Diese lassen sich über das entsprechende Kontrollkästchen ausblenden. Auch die Anzahl der Tabellen, aus denen Felder angezeigt werden müssen, spielt eine Rolle. Je mehr Tabellen eines oder mehrere Felder zum Abfrageergebnis beitragen, des to langsamer die Abfrage.
Kurze Feldbezeichnungen und Alias-Namen verwenden Weitere Vorteile bei der Performance liefern kurze Namen von Tabellenfeldern sowie Alias-Namen für Tabellen, wie folgendes Beispiel zeigt: SELECT t2.[Bestell-Nr], t2.[Artikel-Nr], t1.Artikelname, t2.Einzelpreis, t2.Anzahl, t2.Rabatt, CCur(t2.Einzelpreis*[Anzahl]*(1-[Rabatt])/100)*100 AS Endpreis FROM Artikel AS t1 INNER JOIN Bestelldetails AS t2 ON t1.[Artikel-Nr] = t2.[Artikel-Nr] ORDER BY t2.[Bestell-Nr];
statt SELECT Bestelldetails.[Bestell-Nr], Bestelldetails.[Artikel-Nr], Artikel.Artikelname, Bestelldetails.Einzelpreis, Bestelldetails.Anzahl, Bestelldetails.Rabatt, CCur(Bestelldetails.Einzelpreis*[Anzahl]*(1-[Rabatt])/100)*100 AS Endpreis FROM Artikel INNER JOIN Bestelldetails ON Artikel.[Artikel-Nr] = Bestelldetails.[Artikel-Nr] ORDER BY Bestelldetails.[Bestell-Nr];
Sternchen zählen Soll eine Abfrage die Anzahl der gefundenen Datensätze zurückgeben, wenden Sie die Count-Funktion auf den Ausdruck * (Sternchen) an und nicht auf eines der Felder wie beispielsweise das Primärschlüsselfeld.
Keine Funktionen und berechneten Ausdrücke Verwenden Sie in Abfragen nach Möglichkeit so wenig eingebaute oder benutzerdefinierte Funktionen oder berechnete Ausdrücke wie möglich. Dazu gehören Funktionen wie IIf oder DLookup und weitere Domänen-Funktionen. Die ACE kann solche Abfragen nicht optimieren, weil sich das Ergebnis einer Berechnung statistisch nicht vorhersagen lässt.
736
Performance
Kriterien für Schlüsselfelder variieren Manchmal verwendet man Kriterien für ein Feld, über das eine Beziehung zu einer anderen Tabelle hergestellt wird. Das Primärschlüsselfeld der beteiligten Mastertabelle hat zwar den gleichen Wert wie das entsprechende Fremdschlüsselfeld der Detailtabelle. Es kann aber einen Unterschied in der Verarbeitungsgeschwindigkeit machen, auf welches der beiden Felder Sie ein Kriterium anwenden. Bei dieser Konstellation müssen Sie ausprobieren, wo das Kriterium für eine schnellere Abarbeitung sorgt.
14.3 Formulare Die Möglichkeiten zur Verbesserung der Performance von Formularen lassen sich in zwei Kategorien einteilen: Die einen beeinflussen die Zeit, bis das Formular geöffnet und einsatzbereit ist, und die anderen die Arbeit mit dem Formular selbst.
14.3.1 Formulare offen halten oder schließen? Je nachdem, wie oft Sie ein Formular verwenden, sollten Sie in Erwägung ziehen, es nicht jedes Mal zu schließen und neu zu öffnen, sondern das Formular einfach unsichtbar zu machen. Dazu verwenden Sie die Eigenschaft Sichtbar, die Sie folgendermaßen in VBA einstellen können: 'Ausblenden Me.Visible = False 'Einblenden Me.Visible = False
Andererseits fressen offene Formulare Speicherplatz, sodass Sie diese Vorgehensweise tunlichst nicht mit allen Formularen durchexerzieren sollten. Außerdem besteht die Gefahr, dass Sie einmal mit Screen.ActiveForm das aktuelle Formular ermitteln möchten, es sich bei diesem jedoch um ein unsichtbares Formular handelt.
14.3.2 Daten des Formulars Die im Formular angezeigten Daten stammen aus einer oder mehreren Tabellen, die entweder direkt oder in Form einer Abfrage angesprochen werden. Verwenden Sie eine Abfrage, können Sie entweder auf eine gespeicherte Abfrage zurückgreifen oder geben den entsprechenden SQL-Ausdruck direkt für die Eigenschaft Datensatzquelle ein. Gleiches gilt im Übrigen auch für die Datensatzherkunft von Kombinations- und Listenfeldern. Hier gibt es einige Regeln:
737
Kapitel 14
Die Datensatzquelle von Formularen beziehungsweise die Datensatzherkunft von Kombinations- und Listenfeldern sollte möglichst eine Abfrage sein, die nur die benötigten Felder und – noch wichtiger – nur die benötigten Datensätze enthält. Daher ist in den meisten Fällen eine Abfrage die beste Wahl. Wenn Sie eine Abfrage verwenden, können Sie diese entweder speichern und den Namen der Abfrage als Datensatzquelle oder Datensatzherkunft angeben oder direkt den SQL-Ausdruck der Abfrage in die Eigenschaft Datensatzquelle oder Daten satzherkunft eintragen. Es stimmt nicht, dass als Abfrage gespeicherte Datenherkünfte Vorteile bringen, weil diese vorkompiliert und damit optimiert werden können. SQL-Ausdrücke, die Sie für die Eigenschaften Datensatzquelle oder Datensatzherkunft eintragen, werden beim Ausführen genauso kompiliert wie gespeicherte Abfragen. Interessant ist auch die Variante, die ins Spiel kommt, wenn Sie ein Formular nur zur Eingabe von Daten verwenden möchten. Öffnen Sie ein solches Formular mit folgender Anweisung, wenn Sie dazu VBA verwenden: DoCmd.OpenForm "", DataMode:=acFormAdd
Anderenfalls können Sie die Eigenschaft Daten eingeben im Eigenschaftsfenster des Formulars auf den Wert Ja einstellen.
14.3.3 Steuerelemente Je mehr Steuerelemente Sie in einem Formular anlegen, desto schlechter wird die Perfor mance. Aber auch die Anordnung der Steuerelemente spielt eine Rolle: Übereinander angeordnete Steuerelemente sorgen beispielsweise für eine weitere Verschlechterung. Das könnte etwa der Fall sein, wenn Sie ein Textfeld und ein Kombinationsfeld direkt übereinander legen, um je nach Bedarf das eine oder andere anzuzeigen. Und auch die Gestaltung eines assistenten-ähnlichen Formulars, das mehrere Schritte eines Assistenten in einem Formular enthält und die verschiedenen Stufen nacheinander ein- beziehungsweise ausblendet, ist performancetechnisch betrachtet keine optimale Lösung – dann doch lieber verschiedene Formulare, die nacheinander geöffnet werden.
Einfache Steuerelemente verwenden Es gibt unterschiedlich gewichtete Steuerelemente, was die Performance angeht. Das gilt gerade für die verschiedenen Möglichkeiten zur Auswahl von Informationen. Wenn Sie eine feste, nicht allzu große Anzahl Optionen zur Auswahl bereitstellen möchten, verwenden Sie dazu eine Optionsgruppe.
738
Performance
Diese ist bereits performanter als ein Kombinationsfeld oder ein Listenfeld. Sollte einmal die Entscheidung zwischen einem Kombinations- oder Listenfeld und einem Unterformular anstehen und sollten keine wesentlichen Punkte für eine der beiden Möglichkeiten sprechen, verwenden Sie ein Kombinations- oder Listenfeld.
Bild-Steuerelement vor! Wenn Sie Bilder in einem Formular anzeigen möchten, verwenden Sie dazu das BildSteuerelement statt des ungebundenen Objekt-Steuerelements. Generell sollten Sie jedoch nur so verfahren, wenn es unbedingt notwendig ist. In vielen Fällen sollte eine Schaltfläche ausreichen, die ein Popup-Formular mit dem gewünschten Bild öffnet.
Schaltflächen durch Hyperlinks ersetzen Wenn Sie eine Schaltfläche lediglich verwenden, um damit ein weiteres Formular zu öffnen, können Sie dazu auch ein Bezeichnungsfeld mit einen Hyperlink verwenden. Damit sparen Sie die Schaltfläche ein. Die Zuordnung des entsprechenden Links zu einem Bezeichnungsfeld erfolgt ganz einfach über den Dialog aus Abbildung 14.5.
Abbildung 14.5: Setzen eines Hyperlinks auf ein zu öffnendes Formular
Den Hyperlink können Sie natürlich auch manuell eintragen – in diesem Fall würde er folgendermaßen lauten: Form frmKontakte
739
Kapitel 14
Wie Abbildung 14.5 zeigt, lassen sich auch andere Objekte der Datenbank über einen Hyperlink öffnen.
Kombinationsfelder und Listenfelder Für Kombinations- und Listenfelder gelten die gleichen Regeln wie für das Formular selbst: Die zugrunde liegende Datenquelle sollte in Form einer gespeicherten Abfrage angegeben werden, die nur die notwendigen Felder und Datensätze enthält. Dabei sollte das gebundene Feld indiziert sein. Wenn Sie die automatische Ergänzung bei der Eingabe von Einträgen in ein Kombina tionsfeld nicht benötigen, lassen Sie diese Funktion weg, indem Sie die Eigenschaft Auto matisch ergänzen auf den Wert Nein einstellen. Das angezeigte Feld im Kombinationsfeld sollte den Datentyp String haben, da andere Datentypen sonst erst konvertiert werden müssten. Sollte der Inhalt eines Kombinations- oder Listenfeldes aus einer Tabelle kommen, auf die über das Netzwerk zugegriffen wird, prüfen Sie, ob sich die in dieser Tabelle gespeicherten Daten oft oder überhaupt ändern. Falls nicht, kopieren Sie die Daten in eine lokale Tabelle und greifen Sie so auf diese Daten zu.
Unterformulare Unterformular-Steuerelemente enthalten externe Formulare und zählen damit zu den ressourcen-hungrigsten Steuerelementen. Sie zeigen Daten aus einer eigenen Datensatz quelle an, enthalten gegebenenfalls weitere Kombinations- oder Listenfelder und müssen in den meisten Fällen auch noch mit dem Hauptformular synchronisiert werden. Diese Synchronisation erfolgt über die Eigenschaften Verknüpfen von und Verknüpfen nach des Unterformular-Steuerelements. Zur Optimierung der Performance sollten Sie die in diesen Eigenschaften angegebenen Felder in den zugrunde liegenden Tabellen indizieren. Schlechter für die Performance eines Formulars als ein Unterformular sind zwei oder mehr Unterformulare. Wenn diese alle gleichzeitig sichtbar sein sollen, müssen Sie in den sauren Apfel beißen; in manchen Fällen werden die Daten aber auch auf verschiedenen Seiten eines Register-Steuerelements angezeigt. In diesem Fall gibt es Optimierungspotenzial: Unterformular-Steuerelemente, die kein Unterformular enthalten, brauchen Sie auch nicht mit Daten zu füllen. Auf diese Weise müssen beim Blättern durch die Datensätze des Hauptformulars nicht angezeigte Unterformulare auch nicht mit Daten gefüllt werden. Um nur im aktuellen Register enthaltene Unterformular-Steuerelemente mit den entsprechenden Formularen zu füllen, gehen Sie folgendermaßen vor:
740
Performance
Legen Sie die Unterformulare wie gewohnt an, um Größe und Position einzustellen. Stellen Sie die Eigenschaft Herkunftsobjekt der Unterformular-Steuerelemente auf eine leere Zeichenkette ein. Weisen Sie dem Unterformular-Steuerelement, das beim Öffnen des Formulars sichtbar ist, das anzuzeigende Formular zu: Private Sub Form_Open(Cancel As Integer) Me!.SourceObject = _ "" End Sub
Sorgen Sie beim Wechsel der Registerseite dafür, dass nicht angezeigte Unterformu lare ausgeblendet und sichtbare eingeblendet werden: Private Sub RegisterStr0_Change() Select Case Me!RegisterStr0 Case 0 Me!frmProjekte1.SourceObject Me!frmProjekte2.SourceObject Case 1 Me!frmProjekte1.SourceObject Me!frmProjekte2.SourceObject End Select End Sub
= "" = "" = "" = ""
14.3.4 VBA in Formularen Die Programmierung von Formularen erlaubt weitere Möglichkeiten zur Code-Optimie rung – oder auch nicht, wie der folgende Abschnitt zeigt.
Formular von Code befreien Formulare ohne Code werden deutlich schneller geladen. Stellt sich die Frage, was man mit einem Formular ohne VBA-Code alles anstellen kann. Die Antwort lautet: Alles, was auch mit einem Formular mit VBA-Code geht. Sie können ein Formular sogar komplett von seinem Formularmodul befreien und dennoch auf den gewohnten Komfort von VBA zugreifen. Um das Formular von seinem Modul zu befreien, stellen Sie einfach die Eigenschaft Ent hält Modul auf den Wert Nein ein. Wenn das Formularmodul zu diesem Zeitpunkt bereits Code enthält, meckert Access natürlich – prüfen Sie also, ob Sie den enthaltenen Code noch benötigen (siehe Abbil dung 14.6).
741
Kapitel 14
Abbildung 14.6: Diese Meldung erscheint vor dem Entfernen des Formularmoduls
Wohin aber nun mit dem VBA-Code? Legen Sie die Funktionalität, die durch ein bestimmtes Ereignis ausgelöst werden soll, in einer öffentlichen Funktion an. Sie müssen dann lediglich den Namen der aufzurufenden Funktion für die entsprechende Ereigniseigenschaft angeben (siehe Abbildung 14.7). Wichtig ist die Angabe der Klam mern hinter dem Funktionsnamen.
Abbildung 14.7: Verwenden einer Funktion an Stelle einer Ereignisprozedur
Die in einem Standardmodul angelegte Funktion könnte etwa so aussehen: Public Function frmKontakte_BeimAnzeigen() MsgBox "Dies ist eine externe Ereignisprozedur" End Function Listing 14.3: Diese Funktion wird durch ein Ereignis aufgerufen
14.4 Berichte Viele für Formulare vorgestellte Maßnahmen zur Verbesserung der Performance lassen sich auf Berichte übertragen; es gibt aber auch einige Berichts-Spezialitäten.
742
Performance
14.4.1 Datensatzquelle unsortiert übergeben Sortierungen und Gruppierungen übernimmt ein Bericht selbst, wie in dem dafür vorge sehenen Bereich zu sehen ist (siehe Abbildung 14.8). Daher sollten Sie keine sortierte oder gruppierte Datensatzquelle verwenden.
Abbildung 14.8: Sortierungen und Gruppierungen nehmen Sie im Bericht selbst vor
14.4.2 Keine Funktionen und Ausdrücke in Sortierungen und Gruppierungen Abbildung 14.8 liefert ein Negativbeispiel: Ausdrücke und Funktionen in Sortierungen und Gruppierungen sind nicht sinnvoll, da sie zunächst ausgewertet werden müssen. Besser wäre es, die beiden Felder Nachname und Vorname zu indizieren und einzeln nach den beiden Feldern zu sortieren.
14.4.3 Bericht nur öffnen, wenn er Daten enthält Berichte ohne Daten lassen sich zwar in der Berichts- oder Vorschauansicht öffnen, aber Sinn macht das natürlich nicht. Um dem Benutzer diesen unnötigen Zeitaufwand zu ersparen, treffen Sie geeignete Maßnahmen. Dazu brauchen Sie noch nicht einmal selbst eine Anweisung zur Prüfung der Anzahl der Datensätze in der Datensatzherkunft zu schreiben, sondern können die Ereignis eigenschaft Bei Ohne Daten nutzen, die genau dann ausgelöst wird, wenn der Bericht keine Daten enthält. Hier können Sie Code anlegen, um eine entsprechende Meldung auszugeben und das Öffnen des Berichts durch Setzen des Parameters Cancel auf den Wert True abzubrechen: Private Sub Report_NoData(Cancel As Integer) MsgBox "Der Bericht enthält keine Daten." Cancel = True End Sub Listing 14.4: Ereignisprozedur, die das Öffnen eines leeren Berichts unterbindet
743
Kapitel 14
Wenn Sie es ganz gut mit dem Benutzer meinen, lassen Sie es allerdings gar nicht erst so weit kommen: Deaktivieren Sie einfach die Schaltfläche zum Öffnen des Berichts, wenn abzusehen ist, dass dieser keine Daten enthält.
14.5 VBA In Kapitel 7, »VBA« erhalten Sie einige Informationen, wie Sie VBA-Routinen so optimieren, dass diese eine hohe Wiederverwendbarkeit und eine gute Lesbarkeit erhalten. Performancetechnisch gesehen gehen Sie damit den richtigen Weg: Einerseits strukturieren Sie den Code besser und machen ihn damit wesentlich leichter lesbar, was eine gute Vorarbeit für folgende Performance-Optimierungen ist. Andererseits ergibt sich nach umfangreichen Code-Optimierungsmaßnahmen vermutlich eine Aufteilung der Funktionalität auf wesentlich mehr Module als zuvor: Und das liefert eine direkt messbare Performancesteigerung, denn Access lädt immer das komplette Modul in den Speicher, auch wenn es nur eine öffentliche Variable oder eine einzige Routine daraus verwendet. Standardfunktionen, die Sie oft verwenden, können Sie auch in einem großen Modul sammeln – wenn diese nach dem ersten Aufruf einmal im Speicher liegen, braucht für die folgenden Aufrufe kein Modul mehr nachgeladen zu werden. Nachfolgend finden Sie einige grundlegende Tipps zur Optimierung von VBA-Code und einige Verbesserungsmöglichkeiten für die Programmierung von Routinen für den Datenzugriff mit DAO und ADO.
14.5.1 Performance von VBA-Code optimieren Das Kapitel 7, »VBA«, enthält unter anderem Informationen über das Schreiben optimalen VBA-Codes. Die dort dargestellten Möglichkeiten beziehen sich nicht primär auf das Erreichen einer besseren Performance, sondern einer besseren Struktur, Lesbarkeit und Wiederverwendbarkeit. Tipps für eine bessere Performance finden Sie in den folgenden Abschnitten.
Variablen und Datentypen Was für die Festlegung der Datentypen von Tabellenfeldern gilt, trifft auch für die De klaration von Variablen in VBA-Routinen zu. Sie sollten immer den kleinstmöglichen Datentyp wählen. Dazu gehört, dass Sie überhaupt einen Datentyp festlegen, denn sonst verwendet Access automatisch den Datentyp Variant. Dieser kann beliebige Daten aufnehmen, belegt entsprechenden Speicherplatz und nötigt VBA intern, permanent zwischen verschie-
744
Performance
denen Datentypen zu konvertieren. Um sicherzugehen, dass Sie jeder Variablen einen Datentyp zuweisen, schreiben Sie die folgende Zeile in den Kopf ihrer Module: Option Explicit
Ist eine Variable nicht deklariert, kompiliert Access das Modul nicht (siehe Abbil dung 14.9).
Abbildung 14.9: Kein Kompilieren ohne Variablendeklaration
Bei der Wahl des richtigen Datentyps für eine Variable hilft Tabelle 14.1 weiter – verwenden Sie einfach den Datentyp, mit dessen Wertebereich Sie vermutlich auskommen. Dabei gibt es wiederum eine Ausnahme: Wenn Sie mit ganzzahligen Variablen rechnen möchten, verwenden Sie möglichst den Datentyp Long. Windows als 32bit-System kann mit diesem Zahlentyp schneller rechnen.
Early Binding statt Late Binding Deklarieren Sie Objektvariablen direkt mit dem konkreten Objekttyp und nicht mit dem Datentyp Object. Dadurch kann Access Early Binding verwenden – die Bibliothek mit den benötigten Objekten, Methoden und Eigenschaften ist Access bekannt und ermöglicht so einen schnelleren Zugriff auf die enthaltenen Objekte. Dazu müssen Sie zuvor einen Verweis auf die entsprechende Objektbibliothek erstellen (siehe Abbildung 14.10). Die folgende Routine zeigt ein Beispiel für Early Binding: Public Function EarlyBinding() Dim cbr As CommandBar For Each cbr In Application.CommandBars Debug.Print cbr.Name
745
Kapitel 14 Next cbr Set cbr = Nothing End Function Listing 14.5: Beispiel für das Early Binding von Objekten …
Wenn Sie die Routine ohne Early Binding realisieren wollten, würde das wie im folgenden Listing aussehen: Public Function LateBinding() Dim cbr As Object For Each cbr In Application.CommandBars Debug.Print cbr.Name Next cbr Set cbr = Nothing End Function Listing 14.6: … und die gleiche Funktionalität mit Late Binding
Early Binding liefert einen weiteren Vorteil: Da die Objektbibliothek und die enthaltenen Elemente bekannt sind, können Sie bei der Eingabe IntelliSense nutzen. Das ist nicht nur für die Auswahl der Methoden und Eigenschaften interessant, sondern gerade für die Verwendung von Parametern. Sie brauchen dort nicht die nackten Zahlenwerte zu verwenden, sondern können auf die entsprechenden Bezeichnungen zurückgreifen.
Objektvariablen verwenden, wenn ein Objekt mehr als einmal referenziert wird In vielen Fällen ist es gute Gewohnheit, ein Objekt zu deklarieren und zu instanzieren, bevor Sie auf dessen Elemente zugreifen. Das beste Beispiel dafür ist sicher die Verwendung von db und rst für Database- und Recordset-Objekte: Public Sub ObjekteMehrfachNutzen() Dim db As DAO.Database Dim rst As DAO.Database Set db = CurrentDb Set rst = db.OpenRecordset("tblKontakte", dbOpenDynaset) Debug.Print rst!Vorname Debug.Print rst!Nachname rst.Close Set rst = Nothing Set db = Nothing End Sub Listing 14.7: Verwendung einer Objektvariablen
746
Performance
Abbildung 14.10: Ein Verweis auf die passende Objektbibliothek ist Voraussetzung für die Verwendung von Early Binding
Wann immer Sie mehr als einmal auf ein Objekt zugreifen, sollten Sie es mit einer geeigneten Objektvariablen referenzieren. Wenn Sie beispielsweise mehrere Steuerelemente eines Formulars auslesen, legen Sie einfach eine Referenz auf das Formular an. Die erste Variante einer Routine zum Auslesen der Textfelder eines Formulars stellt für jeden einzelnen Zugriff die Verbindung zum Formular her: Public Function FormularAuslesenOhneReferenz() Dim strVorname As String Dim strNachname As String strVorname = Forms!frmKontakte!Vorname strNachname = Forms!frmKontakte!Nachname End Function Listing 14.8: Direkter Zugriff auf die Eigenschaften eines Formulars
Die zweite Variante erfordert zwar ein wenig mehr Code, ist jedoch schneller: Public Function FormularAuslesenMitReferenz() Dim frm As Form Dim strVorname As String Dim strNachname As String Set frm = Forms!frmKontakte strVorname = frm!Vorname strNachname = frm!Nachname Set frm = Nothing End Function Listing 14.9: Zugriff auf Steuerelemente eines Formulars via Objektvariable
747
Kapitel 14
Me statt Verweis auf Forms- oder Reports-Auflistung Verwenden Sie den Ausdruck Me statt der Variante über die Formular- oder BerichtsAuflistung. Die folgende Routine ist die langsamere Möglichkeit: Public Function FormularbezugMitFormsFormularname() Dim strVorname As String strVorname = Forms!frmKontakte!Vorname End Function Listing 14.10: Die langsame Variante für den Zugriff auf objektinterne Elemente …
Die nächste Variante verwendet das Schlüsselwort Me und ist um rund 40% schneller: Public Function FormularbezugMitMe() Dim strVorname As String strVorname = Me!Vorname End Function Listing 14.11: … und die schnellere Variante
Variablen verwenden, wenn konstante Werte mehr als einmal ermittelt werden Wenn Sie mehrmals im Code die gleiche Funktion wie etwa eine Domänen-Funktion (Dlookup, Dmax, Dcount) verwenden, deren Ergebnis vermutlich in der Zwischenzeit nicht verändert wird, speichern Sie das Ergebnis in einer Variablen.
Variablen auf jeden Fall verwenden, wenn der Inhalt Start- oder End-Wert einer Schleife markiert Obiger Tipp ist in einem Fall auch dann anzuwenden, wenn der Inhalt der Variablen scheinbar nur einmal verwendet wird – und zwar innerhalb einer Schleife. Anderenfalls wird der Wert bei jedem Schleifendurchlauf neu berechnet. Das folgende Beispiel sieht sehr harmlos aus, aber der Inhalt des Feldes txtAnzahl wird mit jedem Durchlauf neu ausgelesen: For i = 1 to Me!txtAnzahl ...
Verwenden Sie also stattdessen folgende Codezeilen, um den benötigten Wert zuvor in einer Variablen zu speichern: Dim lngAnzahl As Long lngAnzahl = Me!txtAnzahl For i = 1 to lngAnzahl ...
748
Performance
Zeichenketten-Funktionen sparsam verwenden Zeichenketten-Funktionen sind sehr aufwändig. Versuchen Sie, diese möglichst sparsam einzusetzen.
String-Variante von Zeichenketten-Funktionen verwenden Es gibt jede Zeichenketten-Funktion in zwei Ausführungen: mit und ohne angehängtes Dollar-Zeichen ($). Der Unterschied der Funktionen liegt im Datentyp des Rückgabe wertes: Die Version der Textfunktionen mit dem Dollar-Zeichen liefert einen String zurück, die ohne Dollar-Zeichen einen Wert vom Datentyp Variant. Da man das Ergebnis von Zeichenketten-Funktionen meistens in einer String-Variablen speichert, ist bei der Verwendung der Variant-Version der Zeichenketten-Funktion noch eine zusätzliche interne Umwandlung des Datentyps erforderlich.
Zeichenketten vergleichen mit StrComp Das gängige Mittel zum Vergleichen zweier Zeichenketten ist die Verwendung des Gleichheitszeichens als Operator. VBA bietet eine dazu besser geeignete Funktion namens StrComp. Die Funktion erwartet die zu vergleichenden Zeichenketten und gegebe nenfalls eine Einstellung für die Vergleichsmethode.
Leere Zeichenkette testen über die Länge der Zeichenkette Ob eine Zeichenkette leer ist, prüfen Sie üblicherweise durch einen Vergleich mit der Zeichenkette "". Das ist nicht der schnellste Weg. Performanter ist es, die Länge der Zeichenkette zu ermitteln und diesen Wert mit 0 zu vergleichen.
Leere Zeichenkette per vbNullString zuweisen Eine Zeichenkette leeren Sie normalerweise durch Zuweisen einer leeren Zeichenkette: str = ""
Etwas schneller geht es mit der Konstanten vbNullString: str = vbNullString
Zeichenverkettung mit dem kaufmännischen Und vermeiden Wenn zwei Zeichenketten miteinander verknüpft werden müssen, dann ist das &-Zei chen unvermeidlich.
749
Kapitel 14
Es ist aber sehr beliebt, längere Zeichenketten der Übersichtlichkeit halber auf mehrere Co dezeilen zu verteilen – vor allem bei dynamisch zusammengesetzten SQL-Ausdrücken: Public Function UndOperator1() Dim strSQL As String strSQL = "SELECT tblKontakte.Vorname, tblKontakte.Nachname " strSQL = strSQL & "FROM tblKontakte " strSQL = strSQL & "WHERE tblKontakte.Vorname = 'André' " strSQL = strSQL & "AND tblKontakte.Nachname = 'Minhorst'" End Function Listing 14.12: Zusammengesetzter SQL-Ausdruck
Die folgende Variante ist schneller, weil die Zeichenkette nicht mehr verknüpft werden muss. Wenn Sie also alles aus Ihrer Datenbankanwendung herausholen möchten, wan deln Sie alle Ausdrücke, die ohne Not mit dem Kaufmanns-Und verbunden sind, wieder in einen einzigen Ausdruck um: Public Function UndOperator2() Dim strSQL As String strSQL = "SELECT tblKontakte.Vorname,..." _ End Function Listing 14.13: SQL-Ausdruck in einer Zeile
Statische oder dynamische Arrays? Die Größe eines Arrays können Sie bereits bei der Deklaration festlegen (statisches Array) oder erst später (dynamisches Array). Bei der statischen Variante geben Sie die Anzahl der vorgesehenen Einträge bereits bei der Deklaration an: Public Function ArrayStatisch() Dim i As Long Dim lng(10000) As Long For i = 1 To 10000 lng(i) = i Next i End Function Listing 14.14: Verwendung eines statischen Arrays
Die dynamische Variante sieht etwas anders aus. Hier geben Sie bei der Deklaration noch keinen Wert an und redimensionieren die Größe des Arrays bei Bedarf. Public Function ArrayDynamisch() Dim i As Long Dim lng() As Long For i = 1 To 10000
750
Performance ReDim Preserve lng(i) lng(i) = i Next i End Function Listing 14.15: Verwendung eines dynamischen Arrays
Die beiden Varianten haben Vor- und Nachteile. Die statische Variante reserviert von vornherein ein Array von einer Größe, die Sie vielleicht gar nicht benötigen. Und falls doch, wird im Durchschnitt die Hälfte des reservierten Speicherplatzes nicht gebraucht. Andererseits kostet die ständige Redimensionierung wesentlich mehr Zeit – hier ist also zwischen benötigtem Speicherplatz und Geschwindigkeit zu entscheiden.
Logische Ausdrücke vereinfachen Logische Ausdrücke landen oft in den übersichtlichen If…Then-Konstrukten. Das sieht dann beispielsweise so aus: Public Function IfThen1() Dim i As Integer Dim bol As Boolean If i = 1 Then bol = True Else bol = False End If End Function Listing 14.16: Ermitteln eines Boolean-Wertes per If...Then-Konstrukt …
Etwas schneller geht es mit der folgenden Variante, die außerdem noch einige Zeilen Code einspart: Public Function IfThen2() Dim i As Integer Dim bol As Boolean bol = (i = 1) End Function Listing 14.17: … und über die direkte Auswertung des Ausdrucks
Sollte Ihnen die Übersicht bei der zweiten Variante etwas zu kurz kommen, können Sie allerdings auch auf den hier relativ geringen Geschwindigkeitsvorteil verzichten.
751
Kapitel 14
Boolean-Werte switchen Wenn Sie einer Boolean-Variablen, die den Wert True enthält, den Wert False zuweisen möchten oder umgekehrt, verwenden Sie vermutlich die folgende Variante: Public Function Invertieren1() Dim x As Boolean If x = True Then x = False Else x = True End If End Function Listing 14.18: Lange Variante der Invertierung einer Boolean-Variablen …
Etwas schneller und Platz sparender ist die folgende Variante: Public Function Invertieren2() Dim x As Boolean x = Not x End Function Listing 14.19: … und hier die kürzere, etwas schnellere Lösung
Performance von Schleifen mit fester Durchlaufzahl Wenn Sie die Anzahl Durchläufe einer Schleife kennen, ist eine For…Next-Schleife schneller als eine Do…While- oder Do…Loop-Schleife. Die beiden folgenden Funktionen durchlaufen eine Schleife jeweils tausend Mal. Die zweite Variante enthält bereits im nackten Zustand eine Anweisung mehr, die den Zähler erhöht. Dadurch ist sie wesentlich langsamer als die erste Variante. Der Unterschied wird allerdings immer kleiner, je mehr Anweisungen sich innerhalb der Schleifen befinden. Public Function Schleife1() Dim i As Integer For i = 1 To 1000 'Etwas tun... Next i End Function Listing 14.20: Diese For…Next-Schleife erledigt die gleiche Arbeit …
Public Function Schleife2() Dim i As Integer Do While Not i = 1000 'Etwas tun...
752
Performance i = i + 1 Loop End Function Listing 14.21: … wie diese Do…While-Schleife, nur schneller
If Then oder IIf? Die IIf-Funktion wird gerne als einzeiliger Ersatz für einfache If…Then-Konstrukte verwendet. Sie wertet den im ersten Parameter angegebenen Ausdruck aus und gibt für das Ergebnis True den Wert des zweiten Parameters und für das Ergebnis False den dritten Parameter zurück. Diese Funktion hat aber gegenüber If…Then entscheidende Nachteile: Die IIf-Funktion wertet immer beide möglichen Antworten aus. Dadurch wird erstens Zeit vergeudet und zweitens können nicht eingeplante Nebeneffekte auftreten. Das folgende Beispiel veranschaulicht dies. Die IIf-Anweisung soll das Ergebnis der Division der Variablen a und b ausgeben, aber nur, wenn b nicht 0 ist. Ist das doch der Fall, würde die Division einen Division-durch-Null-Fehler erzeugen, was durch die IIfFunktion abgefangen werden soll. Das funktioniert aber nicht: Obwohl die Bedingung erfüllt ist und eigentlich der Text Division durch 0 ausgegeben werden müsste, erscheint die Fehlermeldung, die eigentlich verhindert werden sollte. Public Function Division() Dim a As Integer Dim b As Integer a = 1 b = 0 Debug.Print IIf(b = 0, "Division durch 0", a / b) End Function Listing 14.22: Unerwünschte Nebeneffekte mit der IIf-Funktion
Die gleiche Variante mit einem If…Then-Konstrukt sieht folgendermaßen aus. Performancetests ergaben keine wesentlichen Vorteile für eine der beiden Varianten, sodass Sie die If…Then-Variante wegen der besseren Kalkulierbarkeit einsetzen sollten. Public Function Division2() Dim a As Integer Dim b As Integer a = 1 b = 0 If b = 0 Then Debug.Print "Division durch Null" Else Debug.Print a / b
753
Kapitel 14 End If End Function Listing 14.23: Diese Variante bringt keine bösen Überraschungen
14.5.2 Punkt oder Ausrufezeichen In vielen Fällen lässt Access die synonyme Verwendung von Ausrufezeichen und Punkt zu – etwa beim Bezug auf die Steuerelemente eines Formulars: Debug.Print Forms!frmKontakte!txtVorname
oder Debug.Print Forms.frmKontakte.txtVorname
Setzen Sie das Ausrufezeichen, wann immer es möglich ist. Diese Variante ist immer schneller als die mit Punkt.
14.5.3 Datenzugriff optimieren Access bietet verschiedene Techniken für den Zugriff auf die in der Datenbank gespeicherten Daten – DAO und ADO. Im Folgenden finden Sie einige Tipps rund um den Einsatz dieser beiden Bibliotheken.
Datenzugriff möglichst per gespeicherter Abfrage statt mit ADO oder DAO Wenn Sie per Code auf Daten zugreifen, sollten Sie nach Möglichkeit mit gespeicherten Abfragen arbeiten. Sie können damit nicht nur Daten abfragen, sondern diese durch die Verwendung von Aktionsabfragen auch ändern. Letzteres lässt sich leicht durch ein Beispiel belegen. Die nachfolgenden Prozeduren ändern beide den Inhalt des Feldes Vorname für alle Datensätze der Tabelle tblKontakte. Die erste Variante verwendet dafür DAO und durchläuft per Code alle Datensätze einer zuvor geöffneten Datensatzgruppe auf Basis der Tabelle tblKontakte. Public Function Aktionsabfrage1() Dim db As DAO.Database Dim rst As Recordset Set db = CurrentDb Set rst = db.OpenRecordset("SELECT Vorname FROM tblKontakte", _ dbOpenDynaset) Do While Not rst.EOF rst.Edit
754
Performance rst!Vorname = "André" rst.Update rst.MoveNext Loop Set db = Nothing End Function Listing 14.24: Aktualisieren von Daten mittels DAO …
Die zweite Variante setzt auf eine gespeicherte Abfrage namens qryUpdateKontakte, deren SQL-Code wie folgt aussieht: UPDATE tblKontakte SET tblKontakte.Vorname = "André";
Die gespeicherte Aktionsabfrage führt die Aufgabe wesentlich schneller aus als die DAO-Variante. Das ist auch nicht verwunderlich, da hier die ACE-Engine direkt mit der Änderung der Daten beauftragt wird – und auf solche Fälle ist sie ja nun spezialisiert. Public Function Aktionsabfrage2() Dim db As DAO.Database Dim qdf As DAO.QueryDef Set db = CurrentDb Set qdf = db.QueryDefs("qryUpdateKontakte") qdf.Execute Set qdf = Nothing Set db = Nothing End Function Listing 14.25: … und per gespeicherter Abfrage
Wenn Sie nichts anderes mehr mit den Objektvariablen anfangen und nur die Abfrage ausführen möchten, können Sie natürlich auch auf die Variablen verzichten und mit einer einzigen Abfrage auskommen: CurrentDb.QueryDefs("qryUpdateKontakte").Execute
Tipp: Noch schneller ist die Variante mit DBEngine(0)(0) statt CurrentDB. Diese hat allerdings auch Nachteile, wie in Kapitel 9, Abschnitt 9.5, »Aktuelle Datenbank referenzieren«, beschrieben ist.
14.6 Sonstige Performance-Tipps Die folgenden Tipps lassen sich nicht in eine der vorherigen Kategorien einordnen, tragen aber durchaus ihren Teil zur Performance-Steigerung bei.
755
Kapitel 14
14.6.1 Verwendung als .accde-Datei Wenn der Benutzer die Datenbankanwendung nicht selbst bearbeiten können soll, liefern Sie die Datenbank als .accde-Datei aus. Dadurch überführen Sie den kompletten Quellcode in den kompilierten Zustand, den er auch nicht mehr verlässt. Außerdem ist der durch eine .accde-Datei benötigte Speicherplatz geringer, da diese keinen Quellcode mehr enthält (zur Sicherheit: Löschen Sie niemals das Original der .accde-Datei – bisher ist es noch niemandem gelungen, eine .accde-Datei wieder in die Variante mit Quellcode zurückzuverwandeln).
14.6.2 Exklusiver Zugriff bei Einzelplatzanwendungen Wenn Sie allein auf eine Datenbank zugreifen, öffnen Sie diese im Exklusiv-Modus. Diese Einstellung nehmen Sie in den Access-Optionen unter Erweitert|Erweitert|Standardöff nungsmodus vor. Auf keinen Fall sollten Sie ein Datenbank-Backend, auf das Sie allein zugreifen, auf einem anderen Rechner als dem lokalen ablegen (weitere Informationen zum Aufteilen von Datenbanken finden Sie in Kapitel 19, »Installation, Betrieb und Wartung«). Unter Umständen würde man so verfahren, um das Backend mit dem Inhalt eines Servers mit zu sichern – zu Gunsten der Performance sollten Sie sich allerdings die Arbeit machen, auf anderem Wege für eine geeignete Sicherung zu sorgen.
14.6.3 Komprimieren der Datenbank Durch das Komprimieren einer Datenbank werden nicht nur gelöschte Datensätze endgültig aus der Datenbank entfernt, was die Größe der Datenbank-Datei verringert, sondern auch die Tabellenstatistiken aktualisiert. Das ist für die Erstellung der Ausfüh rungspläne von Abfragen wichtig, da diese sonst über längere Zeit mit falschen Anga ben arbeiten und gegebenenfalls die Abfragen schlechter optimieren. Wenn Sie sicherstellen möchten, dass die Datenbank regelmäßig komprimiert wird, aktivieren Sie die Access-Option Aktuelle Datenbank|Anwendungsoptionen|Beim Schließen komprimieren. Mit dieser Option sorgen Sie bei aufgeteilten Datenbanken ausschließlich dafür, dass das Frontend einer Datenbank komprimiert wird, nicht aber das Backend mit den Daten – zumindest, wenn Sie die Option im Frontend einstellen!
14.6.4 Objektnamen-Autokorrektur abschalten Die Objektnamen-Autokorrektur sorgt dafür, dass Änderungen – etwa an Feldnamen einer Tabelle – automatisch auf Steuerelemente übertragen werden, die auf ein solches Feld zugreifen oder Feldnamen in Abfragen aktualisieren. Offensichtlich wirkt sich die Aktivierung dieser Option auch im laufenden Betrieb stark auf die Performance aus, weshalb Sie diese Option nach Fertigstellung einer Anwendung abschalten sollten. Wenn
756
Performance
Sie eine Datenbank namens leer.accdb erstellen, dort die Objektnamenautokorrektur ausschalten und diese im Verzeichnis C:\Programme\Microsoft Office\Templates\1031\ Access speichern, werden neue Datenbankdateien automatisch mit der deaktivierten Ob jektnamenautokorrektur geöffnet.
14.6.5 Unterdatenblätter abschalten Unterdatenblätter gibt es seit Access 2000. Sie ermöglichen die Anzeige von Daten aus verknüpften Tabellen innerhalb der Datenblattansicht einer anderen Tabelle (siehe Ab bildung 14.11). Dieses Feature ist standardmäßig aktiviert und kostet unter Umständen Rechenzeit beim direkten Öffnen von Tabellen in Datenblattansicht – vornehmlich mit wachsender Anzahl verknüpfter Tabellen.
Abbildung 14.11: Einfaches Beispiel für ein Unterdatenblatt
Um diese Funktion zu deaktivieren, stellen Sie den Wert der Eigenschaft Unterdatenblatt name auf den Wert [Keines] ein (siehe Abbildung 14.12).
14.6.6 Rechtschreibprüfung ausschalten Wenn Sie die Rechtschreibprüfung nicht benötigen, können Sie diese ebenfalls ausschalten. Die passende Option finden Sie in den Access-Optionen unter Dokumentprüfung. Die Auswirkungen dürften allerdings eher sparsam ausfallen.
14.7 Performance-Unterschiede messen Sie haben in diesem Kapitel einige Tipps und Hinweise darauf gefunden, wie Sie die Performance einer Anwendung verbessern können. Dazu müssen Sie an vielen Stellen herumschrauben; manche Tipps bringen einen garantierten Performance-Gewinn, andere sind mit Vorsicht zu genießen.
757
Kapitel 14
Abbildung 14.12: Deaktivieren der Anzeige von Unterdatenblättern
Wie findet man nun heraus, ob eine Maßnahme den gewünschten Erfolg gebracht hat? Locker formuliert lautet die Antwort: Nehmen Sie sich eine Stoppuhr zur Hand und las sen Sie die jeweiligen Varianten gegeneinander antreten. Im Entwicklerjargon heißt das: Programmieren Sie die Funktionalität, die zur Zeitmessung notwendig ist, und begin nen Sie mit der Optimierung.
14.7.1 Werkzeug für Performance-Tests selbst gebaut Wenn Sie erst einmal wissen, wie Sie die Systemzeit mit der gewünschten Genauigkeit ermitteln, ist der Rest ein Kinderspiel. Wie so oft hilft die API aus: Die beiden verwendeten Funktionen tragen die Namen QueryPerformanceCounter und QueryPerformanceFre quency. Wie Sie die beiden Funktionen deklarieren und einsetzen, erfahren Sie im Modul clsZeitmessung der Beispieldatenbank unter \Kap_14\Performance.accdb. Das Messen der Performance ist gleichbedeutend mit dem Messen der Zeit, die Access für eine bestimmte Aufgabe benötigt. Das Ganze macht natürlich wenig Sinn, wenn es keine Vergleichsmöglichkeiten gibt – Sie werden also zum Zweck der Performance-Steigerung immer mit mindestens zwei Varianten zur Erledigung der Aufgabe arbeiten und vergleichen, welche der beiden die Aufgabe schneller erledigt. Voraussetzung für die Zeitmessung bestimmter Vorgänge ist, dass sich diese Vorgänge per VBA starten lassen und das Ende auch per VBA erkannt wird. Da sich in Access fast jeder Vorgang per VBA steuern lässt, bedeutet diese Voraussetzung keine Einschränkung, sondern in manchen Fällen lediglich eine Erschwerung der Aufgabe. Ist diese Voraussetzung erfüllt, messen Sie die Zeit eines Vorgangs einfach, indem Sie
758
Performance
vor dem Start die Systemzeit erfassen, den Vorgang durchführen und anschließend die Differenz aus der aktuellen und der beim Start erfassten Systemzeit bilden. Nun dauern die zu testenden Vorgänge vermutlich meist nur wenige Millisekunden und benötigen auch nicht immer exakt die gleiche Zeit, sodass Sie die zu testende Funktionalität mit dem Ziel einer hohen Genauigkeit doch lieber mehrmals durchführen und die benötigte Zeit messen sollten. Je mehr Durchläufe, desto genauer das Ergebnis.
Testframework Und um dabei den Code nicht mit Zeitmessungen zu überladen, erstellen Sie ein kleines Framework zum einfachen Testen der Performance der unterschiedlichen Varianten einer Funktionalität. Sie können mit dem nachfolgend vorgestellten Framework sowohl Prozeduren aus Standardmodulen als auch aus Formularen und Klassenmodulen tes ten. Der Test der Prozeduren aus Standardmodulen erfordert allerdings einen etwas anderen Ansatz als der von Prozeduren in Formular- und Klassenmodulen. Damit das Framework flexibel ist und Sie die zu testenden Prozeduren und die übrigen Informationen als Parameter eines einfachen Aufrufs übergeben können, benötigen Sie eine Funktion, der Sie nur den Namen der auszuführenden Routine übergeben müssen.
Aufrufen von Funktionen in Standardmodulen Funktionen lassen sich ganz einfach mit der Eval-Anweisung aufrufen. Die EvalAnweisung erwartet lediglich die Angabe des Namens der auszuführenden Funktion inklusive öffnender und schließender Klammer als Parameter: Eval "Funktionsname()"
Diese Form des Aufrufs reicht für den Performancetest. Üblicherweise verwendet man die Eval-Anweisung allerdings, um das Resultat einer Funktion zurückzugeben – dann weist man das Ergebnis einer Variablen zu oder gibt es im Direktfenster aus: Debug.Print Eval("LateBinding()")
Die Eval-Anweisung arbeitet nur mit Funktionen und nicht mit Sub-Prozeduren. Daher müssen Sie die zu testenden Anweisungen entweder direkt in eine Funktion schreiben oder die entsprechende Sub-Prozedur durch eine Funktion aufrufen lassen.
Aufrufen von Funktionen und Prozeduren in Formular- und Klassenmodulen Für den Aufruf von Funktionen in Formular- und Klassenmodulen können Sie die EvalAnweisung nicht verwenden. Selbst das Aufrufen öffentlich deklarierter Funktionen
759
Kapitel 14
erfordert hier die Angabe des Objekts, in dem die Funktionen enthalten sind. Die EvalAnweisung kann Funktionen nur über den reinen Funktionsnamen aufrufen. Hier kommt die CallByName-Methode ins Spiel: Sie ermöglicht die Ausführung von Funktionen und Sub-Prozeduren (genau genommen sogar auch von Property-Prozeduren, aber das ist in diesem Zusammenhang irrelevant). Die CallByName-Methode hat folgende Syntax: CallByName(object, procname, calltype,[args()])
Die Parameter erwarten die folgenden Informationen: object: Objektvariable mit Verweis auf das Objekt, das die auszuführende Prozedur enthält procname: Name der Prozedur (ohne Klammern!) calltype: Erwartet eine der Konstanten vbLet, vbGet, vbSet oder vbMethod. Im vorliegenden Fall ist vbMethod der richtige Wert. args(): Parameter-Array für die Werte, die an eventuell vorhandene Parameter der in proctype benannten Prozedur übergeben werden müssen
Eigenschaften und Methoden des Frameworks Das Framework besteht aus einer einzigen Klasse mit zwei Methoden und einigen Eigenschaften. Die Klasse stellt die folgenden Eigenschaften zur Eingabe der für den Test benötigten Parameter bereit: ErsteRoutine: Name der ersten Routine ZweiteRoutine: Name der zweiten Routine AnzahlDurchgaenge: Anzahl der Durchgänge jeder Funktion Objekt: Objektvariable mit Verweis auf ein Objekt, in dem sich die Prozeduren befinden Die ersten drei Eigenschaften müssen vor dem Aufruf des Performancetests übergeben werden. Die Letzte brauchen Sie nur mit einer Objektvariablen zu belegen, wenn die zu testende Routine sich in einem Formular- oder Klassenmodul befindet.
Test von Prozeduren aus Standardmodulen Für den Test einer öffentlichen Funktion eines Standardmoduls rufen Sie die Methode Performancetest_Standard auf. Diese startet die Zeitmessung, indem sie die Startzeit in der Variablen lngStartzeit speichert. Anschließend wird die erste Funktion in einer Schlei
760
Performance
fe mit der in der Eigenschaft AnzahlDurchgaenge angegebenen Anzahl aufgerufen. Die Differenz der jetzigen Systemzeit und der Startzeit wird in der Variablen mErsteZeit gespeichert. Auf die gleiche Weise ermittelt die Methode die Zeit für die Durchläufe der zweiten Funktion und speichert sie in der Variablen mZweiteZeit. Nach der Zeitmessung berechnet die Methode noch zwei Werte: die absolute Differenz zwischen den gemessenen Zeiten und die relative Differenz. Die ermittelten Werte können anschließend über die Eigenschaften ErsteZeit, ZweiteZeit, AbsoluteDifferenz und RelativeDifferenz ausgelesen werden.
Test von Prozeduren aus Formular- und Klassenmodulen Der Test öffentlicher Prozeduren aus Formular- und Klassenmodulen läuft ähnlich wie der von Prozeduren in Standardmodulen ab. Sie müssen lediglich noch eine Objektvariable mit einem Verweis auf eine Instanz des Formulars oder der Klasse übergeben, in der sich die Prozedur befindet. Beachten Sie, dass die zu testenden Prozeduren als öffentlich deklariert sein müssen. Anschließend rufen Sie die Methode Performancetest_Objekt auf. Die Ergebnisse werden in den gleichen Eigenschaften zur Verfügung gestellt wie bei der zuvor beschriebenen Methode.
Die Klasse clsZeitmessung Nachfolgend finden Sie das Listing mit der kompletten Klasse clsZeitmessung. Es enthält die Eigenschaften zur Eingabe der Parameter sowie zur Ausgabe der Ergebnisse und die beiden Prozeduren Performancetest_Standard und Performancetest_Objekt. Option Compare Database Option Explicit Private Declare Function QueryPerformanceCounter Lib "kernel32.dll" _ (ByRef lpPerformanceCount As Currency) As Long Private Declare Function QueryPerformanceFrequency Lib "kernel32.dll" _ (ByRef lpFrequency As Currency) As Long Dim Dim Dim Dim Dim Dim
curStartzeit As Currency mFreq As Currency mZeit As Currency mErsteProzedur As String mZweiteProzedur As String mErsteZeit As Currency
761
Kapitel 14 Dim Dim Dim Dim Dim Dim
mZweiteZeit As Currency mNull As Currency mAnzahlDurchgaenge As Long mAbsoluteDifferenz As Currency mRelativeDifferenz As Currency mObjekt As Object
Private Sub Class_Initialize() 'Ermitteln der rechnerspezifischen Counter-Frequenz QueryPerformanceFrequency mFreq End Sub Public Property Set Objekt(obj As Object) Set mObjekt = obj End Property Public Property Let ErsteProzedur(strErsteProzedur As String) mErsteProzedur = strErsteProzedur End Property Public Property Let ZweiteProzedur(strZweiteProzedur As String) mZweiteProzedur = strZweiteProzedur End Property Public Property Let AnzahlDurchgaenge(lngAnzahlDurchgaenge As Long) mAnzahlDurchgaenge = lngAnzahlDurchgaenge End Property Public Property Get AbsoluteDifferenz() As Currency AbsoluteDifferenz = mAbsoluteDifferenz End Property Public Property Get RelativeDifferenz() As Currency RelativeDifferenz = mRelativeDifferenz End Property Public Property Get Zeit() Zeit = mZeit End Property Public Property Get ErsteZeit() As Currency ErsteZeit = mErsteZeit End Property Public Property Get ZweiteZeit() As Currency ZweiteZeit = mZweiteZeit End Property
762
Performance Private Sub Starten() QueryPerformanceCounter curStartzeit End Sub Private Sub Stoppen() Dim curStop As Currency QueryPerformanceCounter curStop 'Umrechnung Counter in Millisekunden über Counterfrequenz mZeit = (curStop - curStartzeit) / mFreq * CCur(1000) End Sub Public Sub Performancetest_Standard() Dim l As Long Dim mNull As Double Starten For l = 1 To mAnzahlDurchgaenge Eval mErsteProzedur Next l Stoppen mErsteZeit = mZeit - mNull Starten For l = 1 To mAnzahlDurchgaenge Eval mZweiteProzedur Next l Stoppen mZweiteZeit = mZeit - mNull If mErsteZeit = 0 Or mZweiteZeit = 0 Then Debug.Print "###Zeit nicht messbar, AnzahlDurchgaenge erhöhen." Else mAbsoluteDifferenz = (mZweiteZeit - mErsteZeit) mRelativeDifferenz = AbsoluteDifferenz / mZweiteZeit * 100 End If End Sub Public Sub Performancetest_Objekt() Dim l As Long Starten For l = 1 To mAnzahlDurchgaenge CallByName mObjekt, mErsteProzedur, VbMethod Next l Stoppen mErsteZeit = mZeit Starten
763
Kapitel 14 For l = 1 To mAnzahlDurchgaenge CallByName mObjekt, mZweiteProzedur, VbMethod Next l Stoppen mZweiteZeit = mZeit If mErsteZeit = 0 Or mZweiteZeit = 0 Then Debug.Print "###Zeit nicht messbar, AnzahlDurchgaenge erhöhen." Else mAbsoluteDifferenz = (mZweiteZeit - mErsteZeit) mRelativeDifferenz = AbsoluteDifferenz / mZweiteZeit * 100 End If End Sub Listing 14.26: Die Klasse clsZeitmessung
Einsatz der Klasse clsZeitmessung Die Methoden und Eigenschaften lassen sich am einfachsten in einer Routine wie im folgenden Listing verwenden. Die Prozedur ZeitMessenStandard deklariert und instanziert ein Objekt des Typs clsZeitmessung. Es weist die Anzahl der Durchgänge zu und übergibt die Namen der zu vergleichenden Funktionen. Nach dem Ausführen des Tests mit der Methode Performancetest_ Standard fragt die Routine die ermittelten Werte ab und gibt sie im Testfenster aus. Public Sub ZeitMessenStandard(strErsteProzedur As String, _ strZweiteProzedur As String, lngAnzahlDurchgaenge As Long) Dim objZeitmessung As clsZeitmessung Set objZeitmessung = New clsZeitmessung With objZeitmessung .AnzahlDurchgaenge = lngAnzahlDurchgaenge .ErsteProzedur = strErsteProzedur & "()" .ZweiteProzedur = strZweiteProzedur & "()" .Performancetest_Standard Debug.Print "Zeit '" & strErsteProzedur & "': " _ & .ErsteZeit & "ms" Debug.Print "Zeit '" & strZweiteProzedur & "': " _ & .ZweiteZeit & "ms" Debug.Print "Absolute Differenz: " & .AbsoluteDifferenz & "ms" Debug.Print "Relative Differenz: " _ & Format(.RelativeDifferenz, "0.00") & "%" End With Set objZeitmessung = Nothing End Sub Listing 14.27: Diese Routine verwendet die Methoden und Eigenschaften der Klasse clsZeitmessung
764
Performance
Bei beiden Funktionen, die Sie weiter oben in Abschnitt »Objektvariablen verwenden, wenn ein Objekt mehr als einmal referenziert wird« kennen gelernt haben, sehen der Aufruf der Prozedur ZeitMessen im Direktfenster und das Ergebnis wie folgt aus: ZeitmessenStandard "FormularAuslesenOhneReferenz", _ "FormularAuslesenMitReferenz", 10000 Zeit 'FormularAuslesenOhneReferenz': 1360ms Zeit 'FormularAuslesenMitReferenz': 1228ms Absolute Differenz: -132ms Relative Differenz: -9,30%
Wenn Sie zwei Prozeduren miteinander vergleichen möchten, die sich in einem Formu lar oder einer Klasse befinden, verwenden Sie die Prozedur ZeitMessenObjekt. Diese Funktion erwartet zusätzlich eine Objektvariable mit der Instanz des Formulars oder der Klasse, in dem sich bzw. in der sich die zu testende Routine befindet. Außerdem müssen die zu testenden Routinen mit dem Schlüsselwort Public deklariert sein. Die Prozedur ZeitMessenObjekt liefert genau die gleichen Informationen zurück wie die Prozedur ZeitMessenStandard. Public Sub ZeitMessenObjekt(strErsteProzedur As String, _ strZweiteProzedur As String, lngAnzahlDurchgaenge As Long, obj As Object) Dim objZeitmessung As clsZeitmessung Set objZeitmessung = New clsZeitmessung With objZeitmessung Set .Objekt = obj .AnzahlDurchgaenge = lngAnzahlDurchgaenge .ErsteProzedur = strErsteProzedur .ZweiteProzedur = strZweiteProzedur .Performancetest_Objekt Debug.Print "Zeit '" & strErsteProzedur & "': " & .ErsteZeit & "ms" Debug.Print "Zeit '" & strZweiteProzedur & "': " _ & .ZweiteZeit & "ms" Debug.Print "Absolute Differenz: " & .AbsoluteDifferenz & "ms" Debug.Print "Relative Differenz: " _ & Format(.RelativeDifferenz, "0.00") & "%" End With Set objZeitmessung = Nothing End Sub Listing 14.28: Prozedur Klassenmodulen
zur
Performancemessung
von
Prozeduren
in
Formular-
und
Ein Beispiel für den Einsatz dieser Prozedur ist ein Performancetest, der typische For mular-Eigenheiten unter die Lupe nimmt. Weiter oben in Abschnitt 14.5.1 unter »Me statt Verweis auf Forms- oder Reports-Auflistung« finden Sie zwei Prozeduren, die ein
765
Kapitel 14
Textfeld eines Formulars einmal über das Schlüsselwort Me! und einmal über Forms!! referenzieren. Der Performancevergleich liefert folgendes Ergebnis: Zeitmessenobjekt "FormularbezugMitFormsFormularname", "FormularbezugMitMe", 10000, Forms("frmKontakte") Zeit 'FormularbezugMitFormsFormularname': 571ms Zeit 'FormularbezugMitMe': 407ms Absolute Differenz: -164ms Relative Differenz: -40,29%
Die Variante mit dem Schlüsselwort Me ist also um rund 40 Prozent schneller.
Komfortable Zeitmessung Am einfachsten erledigen Sie die Zeitmessung mit dem Formular aus Abbildung 14.13.
Abbildung 14.13: Performance vergleichen mit komfortabler Benutzeroberfläche
Es verwendet die Methoden und Eigenschaften der Klasse clsZeitmessung. Um alles gemeinsam in einer eigenen Datenbank einzusetzen, müssen Sie folgende Objekte importieren: Klasse clsZeitmessung Formular frmPerformance Außerdem müssen Sie einen Verweis auf die Bibliothek Microsoft Visual Basic for Appli cations Extensibility 5.3 anlegen. Anschließend können Sie loslegen: Wählen Sie das Mo
766
Performance
dul oder das Formular aus, das die zu vergleichenden Routinen enthält, geben Sie die Anzahl der Durchläufe ein, wählen Sie die Testkandidaten aus und klicken Sie auf Test starten. Fertig! Das Formular liefert Ihnen die passenden Ergebnisse. Falls Sie damit liebäugeln, dieses Formular in einen Assistenten einzubauen, sollten Sie Folgendes beachten: Sie können Routinen aus fremden Datenbanken (Sie würden ja von der .accda-Datenbank auf die Routinen der aufrufenden .accdb-Datenbank zugreifen) nur mit der Methode Application.Run aufrufen. Und die verfälscht teilweise die Ergebnisse. Das Integrieren der beiden Objekte in die zu testende Datenbank dürfte wesentlich bessere Ergebnisse liefern.
767
15 Objektorientierte Programmierung Wenn Sie im Internet nach Informationen über den Einsatz objektorientierter Programmierung in Zusammenhang mit Microsoft Access suchen, müssen Sie eine Menge Geduld mitbringen. Das macht sich erst recht bemerkbar, wenn Sie erstmal diejenigen Seiten oder NewsgroupBeiträge herausfiltern müssen, in denen Entwickler über die Vor- und Nachteile der objektorientierten Entwicklung unter Access beziehungsweise VB/VBA diskutieren oder beleuchten, ob sich VB/VBA überhaupt »objektorientiert« nennen darf. Warum geht dieses Buch also so ausführlich auf objekt orientierte Techniken ein, wenn dieses Thema selbst in Entwicklerkreisen anscheinend umstritten ist und sich nur wenige Entwickler diesem Thema widmen? Die Antwort ist einfach: Erstens arbeiten Sie, wenn Sie mit VBA entwickeln, ohnehin schon mehr oder weniger bewusst mit Objekten. Beispiele dafür sind Formulare, Berichte, Steuerelemente, Recordsets oder andere Anwendungen wie Word oder Excel. Dabei greifen Sie beim Programmieren wie selbstverständlich auf die per IntelliSense komfortabel einsetzbaren Methoden und Eigenschaften der Objekte zu. Den Umgang mit Objekten sind Sie also gewohnt – warum sollten Sie nicht von benutzerdefinierten Klassen profitieren? Und zweitens werden Sie, selbst wenn Sie nur kleine Häppchen Objektorientierung in Form des einen oder anderen Klassenmoduls zum Kapseln einer bestimmten
Kapitel 15
Funktionalität in Ihre Anwendungen einbauen, davon profitieren und möglicherweise schnell mehr davon verwenden wollen. Die Beispiele zu diesem Kapitel finden Sie auf der Buch-CD in der Datenbankdatei \Kap_15\Objektorientierung.accdb.
Beispiel für den Einsatz einer benutzerdefinierten Klasse Ein gutes Beispiel ist die Klasse CTimer, die zusammen mit einem weiteren Modul Funk tionen bereitstellt, um einen oder mehrere Timer zu betreiben. Die Aufgabe eines Timers ist dabei, in zuvor festgelegten Intervallen oder zu festgelegten Zeitpunkten Ereignisse auszulösen und damit dafür vorgesehene Routinen zu starten. Damit lässt sich etwa die Anzeige von Daten in einem Formular in festen Intervallen aktualisieren oder das Blinken von Steuerelementen steuern (nur ein Scherz!). Das Wichtige hierbei ist, dass Sie, wenn Sie auf herkömmlichem Wege einen Timer einsetzen wollten, dazu ein Formular und dessen Ereigniseigenschaft Bei Zeitgeber dazu missbrauchen würden – und das selbst dann, wenn der Zweck des Zeitgebers gar nichts mit dem Formular selbst zu tun hat. Außerdem können Sie mit einem Formular nur ein einziges Intervall definieren, und wenn man mal mehrere braucht ... Also programmiert man sich vielleicht selbst eine passende rudimentäre Funktionali tät (es soll ja schnell gehen), die möglicherweise die Intervalle fest im Code verdrahtet. Dabei kann man einen solchen Zeitgeber immer wieder brauchen, warum also nichts Wiederverwendbares schaffen? Etwas, das noch dazu einfach zu handhaben ist? Ohne, dass man sich erst die Namen der Funktionsaufrufe heraussuchen muss? Werfen Sie also einen Blick auf die fertige Klasse CTimer. Welche Vorteile bietet diese und wie arbeitet man überhaupt damit? Um diese Klasse einzusetzen, müssen Sie einfach nur das Klassenmodul CTimer und das dazugehörende Standardmodul mdlTimer in die Zieldatenbank kopieren. Wenn Sie es dann in einem Formularmodul oder einem weiteren Klassenmodul mit dem Schlüsselwort WithEvents (dazu später mehr) deklarieren, können Sie für diese Klasse nicht nur Eigenschaften festlegen, sondern auch noch auf Ereignisse reagieren – was ja bei einem Timer ganz sinnvoll ist: Dim WithEvents objTimer1 As CTimer
Nehmen wir an, Sie möchten ein Steuerelement eines Formulars alle paar Sekunden aktualisieren – etwa, um darin die Uhrzeit anzuzeigen – und dafür nicht die eingebaute Eigen schaft Bei Zeitgeber verwenden (weil Sie diesen möglicherweise für einen anderen Zweck benötigen). In dem Fall gehen Sie folgendermaßen vor: Fügen Sie einem neuen Formular ein Bezeichnungsfeld namens lblUhrzeit hinzu und speichern Sie das Formular unter dem
770
Objektorientierte Programmierung
Namen frmUhrzeit. Legen Sie für die Ereigniseigenschaft Beim Laden des Formulars die folgende Routine an: Private Sub Form_Load() Me!lblUhrzeit.Caption = Time Set objTimer1 = New CTimer With objTimer1 .TimerID = 1 .Interval = 1000 .Repeated = True End With End Sub Listing 15.1: Instanzieren und einstellen der Timer-Klasse
Beachten Sie, dass Sie die Eigenschaften von objTimer1 mit IntelliSense-Unterstützung eingeben können. TimerID ist die Nummer des Timers (falls Sie mal mehrere benötigen, verwenden Sie etwa eine fortlaufende Nummerierung), Intervall gibt die Zeit bis zum nächsten Auslösen in Millisekunden an, und Repeated legt fest, ob ein Ereignis wiederholt ausgelöst werden soll. Damit der Timer beim Schließen des Formulars seinen Dienst beendet, legen Sie auch eine Prozedur an, die das Formular beim Entladen aufruft: Private Sub Form_Unload(Cancel As Integer) Set objTimer1 = Nothing End Sub Listing 15.2: Freigeben der Objektvariablen
Und jetzt kommt der Clou: Irgendwie müssen Sie ja noch festlegen, was eigentlich in Intervallen von x Millisekunden passieren soll. Dazu bietet das Objekt ein Ereignis an, für das Sie eine Ereignisprozedur anlegen können! Wählen Sie einfach aus dem rechten Kombinationsfeld im oberen Teil des Codefensters den Eintrag objTimer1 aus und Access legt automatisch den Rumpf der Prozedur objTimer_Plop an. Hier müssen Sie nur noch eintragen, dass Access dem vorhin erstellten Bezeichnungsfeld als Beschriftung die aktuelle Uhrzeit zuweist: Private Sub objTimer_Plop(dtDateTime As Date, ATag As Variant) Me!lblUhrzeit.Caption = Time End Sub Listing 15.3: Ereignis zur richtigen Zeit
Vorteile von Klassen Dieses Beispiel zeigt bereits einige Vorteile der Verwendung von Klassen:
771
Kapitel 15
Komplizierte Funktionen lassen sich hinter einfachen Methodenaufrufen verste cken. Getestete und funktionstüchtige Klassen lassen sich beliebig wieder verwenden. Parameter müssen nicht mehr mit dem Funktionsaufruf übergeben werden, sondern können übersichtlich als Eigenschaften des Objekts festgelegt werden. Objekte bieten eine komfortable Programmierschnittstelle: IntelliSense verrät Ihnen genau, welche Eigenschaften und Methoden die Instanz eines Klassenmoduls bereitstellt. Dadurch sind Objekte quasi selbst dokumentierend und erlauben einen einfachen Zugriff. Klassen können Ereignisse bereitstellen, die durch beliebige Vorgänge ausgelöst werden. Darüber hinaus gibt es noch einige weitere Vorteile: Zusammenhängende Daten, die sonst als Variablen mehr oder weniger öffentlich irgendwo im Quellcode zu finden sind, lassen sich als Eigenschaften eines Klassenmoduls zusammenfassen. Methoden, die sich genau auf diese zu einem Klassenmodul zusammengefassten Daten beziehen, lassen sich nur in diesem Zusammenhang aufrufen. Die in einem Klassenmodul zusammengefassten Eigenschaften und Methoden können Objekte aus der Realität abbilden. Änderungen an einem Klassenmodul, das möglicherweise an vielen Stellen instanziert wird, müssen nur an einer Stelle durchgeführt werden. Sie können gleichzeitig mit mehreren Instanzen eines Klassenmoduls arbeiten. Mit Objekten lassen sich mehrschichtige Anwendungen realisieren. Objekte können den Datenzugriff kapseln. Damit können Sie etwa Anwendungen erstellen, ohne sich auf eine bestimmte Datenquelle wie Tabellen einer Datenbank, Daten im XML-Format oder eine einfache Textdatei festzulegen. In den folgenden Abschnitten erfahren Sie, wie Sie eigene Klassen erstellen und diese einsetzen. Das anschließende Kapitel greift diese Kenntnisse auf und vermittelt Ihnen die Grundlagen für den professionellen Einsatz von Objekten in Access.
15.1 Abstrakte Datentypen, Klassen und Objekte »One way of thinking of a class is as an abstract data type plus inheritance and polymorphism.« (Steve McConnell in »Code Complete 2«, Microsoft Press)
772
Objektorientierte Programmierung
Steve McConnell beschreibt abstrakte Datentypen im oben genannten Buch als eine Samm lung von Daten und Operationen, die mit diesen Daten arbeiten, wobei die Operationen der Anwendung den Zugriff auf die enthaltenen Daten und deren Änderung erlauben. Nimmt man das einleitende Zitat hinzu und zieht die fehlenden Möglichkeiten der Vererbung und Polymorphie unter VBA ab, ergibt sich Folgendes: Unter VBA entspricht ein abstrakter Datentyp einer Klasse. Außen vor bleibt dabei die Möglichkeit der Schnittstellenvererbung unter VBA (mehr dazu in Abschnitt 16.9, »Schnittstellen und Vererbung«). Ein abstrakter Datentyp ist im Gegensatz zu Basisdatentypen wie String, Integer oder Long oder zusammengesetzten Datentypen (etwa aus Basisdatentypen zusammengesetzten Strukturen) eine Beschreibung einer Schnittstelle zu Daten oder Datenstrukturen und deren Operationen. Die Betonung liegt dabei auf Beschreibung, denn ein abstrakter Datentyp ist unabhängig von der Implementierung in einer konkreten Programmiersprache. Ein abstrakter Datentyp zeichnet sich außerdem durch folgende Eigenschaften aus: Die Kapselung sorgt für das Verbergen der Realisierung der enthaltenen Opera tionen. Die Kapselung verhindert unkontrollierte Zugriffe auf die enthaltenen Daten und sorgt damit für deren Integrität; die Daten können nur über die definierte Schnittstelle geändert werden. Der abstrakte Datentyp ist universell einsetzbar und unabhängig von der Implementation. »Abstrakt« sind abstrakte Datentypen, weil sie reale Objekte modellieren und dabei nur ihre wichtigsten Eigenschaften und Funktionen berücksichtigen. Durch Abstrahieren werden komplexe Strukturen und Zusammenhänge vereinfacht; erst dadurch lassen sich komplizierte Objekte datentechnisch abbilden. Eine Klasse ist eine Implementierung eines abstrakten Datentyps in einer bestimmten Programmiersprache wie beispielsweise VBA. Die Implementierungsdetails werden dabei in einem Klassenmodul festgelegt. Die Gemeinsamkeiten zwischen dem abstrakten Datentyp und der Klasse beschränken sich dabei auf die fest definierte Schnittstelle, die aus den Methoden und Eigenschaften besteht. Die Implementierung kann und wird vermutlich in jeder Programmiersprache anders aussehen. Die Klasse kann neben den für die Realisierung der Schnittstelle notwendigen Methoden und Eigenschaften natürlich auch private Variablen, Funktionen und Prozeduren enthalten. Mehr dazu erfahren Sie in Abschnitt 16.3, »Klassenmodule«.
15.2 Objekte Wenn Sie einige Erfahrung mit VBA haben (was wahrscheinlich ist, wenn Sie sich bis hierhin durchgeschlagen haben), sind Sie vermutlich mit der Verwendung von
773
Kapitel 15
Objekten innerhalb von VBA-Routinen vertraut. Dennoch finden Sie noch einmal eine Zusammenfassung der dabei verwendeten Techniken und einige Hinweise, wie Sie häufige Fehlerquellen umgehen.
15.2.1 Eingebaute Objekte Im Urzustand enthält eine Access 2007-Datenbank Verweise auf mindestens drei Biblio theken, die einige für die Programmierung benötigte Objekte zur Verfügung stellen: Visual Basic for Applications Microsoft Access 12.0 Object Library OLE Automation Microsoft Office 12.0 Access database engine Object Library Die Methoden, Eigenschaften und Ereignisse dieser Objekte stehen jederzeit zur Verfü gung, ihre Verwendung erfolgt ohne vorheriges Instanzieren. Zum größten Teil greift man vermutlich auf diese Objekte zu, ohne dass man sich im Klaren darüber ist, dass es sich auch bei den herkömmlichen Methoden und Eigenschaften um Teile eines Objekts handelt. Ein Beispiel verdeutlicht dies. Mit der folgenden Anweisung können Sie beispielsweise den aktuellen Datenbankbenutzer herausfinden: Debug.Print CurrentUser
Der folgende Aufruf belegt, dass diese Eigenschaft zur Microsoft Access x.y Object Library gehört: Debug.Print Access.CurrentUser
Letztere Version ist eigentlich korrekter: Access.CurrentUser ist nur möglich, weil Application als GlobalMultiUse-Objekt von Access quasi die Standardeigenschaft von Access ist und daher nicht ausgeschrieben werden muss.
MultiUse-Klassen und GlobalMultiUse-Klassen Vielleicht haben Sie schon einmal festgestellt, dass man manche Objekte unter VBA ohne Weiteres verwenden kann, während man andere erst instanzieren muss. Der Grund ist, dass es tatsächlich unterschiedliche Arten von zugrunde liegenden Klassen gibt: Die so genannten GlobalMultiUse-Klassen brauchen Sie nicht zu instanzieren – dies geschieht im Hintergrund automatisch, sobald Sie eine Methode oder Eigenschaft der Klasse aufrufen. Die Eigenschaften und Methoden dieser Klassen stehen ohne Ihr Zutun global zur
774
Objektorientierte Programmierung
Verfügung. Beispiele sind DBEngine, DoCmd oder Application. Es gibt auch eine Menge GlobalMultiUse-Klassen, die Sie niemals bemerken – so sind beispielsweise alle Datumsund Zeitfunktionen Methoden der Klasse Datetime. Wenn Sie neugierig geworden sind, zu welcher Klasse die eine oder andere Eigenschaft oder Methode gehört, zeigen Sie einfach den Objektkatalog an und geben als Suchbegriff den gewünschten Ausdruck ein. Beispiele für Klassen, die Sie zunächst instanzieren müssen, sind Ihnen vermutlich ausreichend geläufig – Database und Recordset etwa dürften Ihnen schon das eine oder andere Mal über den Weg gelaufen sein.
Beispiele für Objekte Alle Elemente, die in VBA Eigenschaften, Methoden oder Ereignisse bereitstellen, sind Objekte. Manche davon stehen nicht nur für den Zugriff per VBA, sondern direkt in der Benutzeroberfläche zur Verfügung. Dazu gehören beispielsweise: Fenster Steuerelemente Formulare Berichte Textfelder Schaltflächen Alle genannten Elemente (und natürlich noch mehr) lassen sich auch per VBA ansprechen.
Funktionen zum Instanzieren neuer Objekte Neben den Methoden und Eigenschaften stellt die Access-Bibliothek wiederum Objekte beziehungsweise Funktionen zur Ermittlung von Verweisen auf die entsprechenden Objekte zur Verfügung. Wenn Sie beispielsweise CurrentDb verwenden, könnte der Eindruck entstehen, dass es sich dabei bereits um ein Objekt handelt. Der Eindruck verstärkt sich, wenn Sie folgendes Beispiel betrachten: Debug.Print Access.CurrentDb.Name
Die Anweisung bewirkt die Ausgabe des Verzeichnisses und des Dateinamens der aktuellen Datenbankdatei. Der Zugriff auf die Eigenschaft Name erfolgt dabei aber nicht über das »Objekt« CurrentDb, sondern über das Objekt, das durch die CurrentDb-Methode zurückgeliefert wird. Und dabei handelt es sich wiederum um eine neue Instanz der aktuellen Datenbank.
775
Kapitel 15
Man könnte diese »implizite« Instanzierung über Funktionen noch weitertreiben. Die folgende Anweisung gibt die Anzahl Datensätze der angegebenen Tabelle zurück: Debug.Print CurrentDb.OpenRecordset("tblKontakte").RecordCount
Diese Anweisung erstellt nicht nur eine neue Instanz der aktuellen Datenbank, sondern innerhalb dieser Instanz auch noch eine neue Datensatzgruppe, um deren Daten satzanzahl auszugeben. Für die Ausgabe zu Testzwecken ist die Verwendung einer solchen Anweisung sicher legitim, innerhalb von Prozeduren sollten Sie diese An weisungen aber nur für den einfachen Gebrauch einsetzen – hier sind sie dann aller dings auch schneller. Wenn Sie aber öfter auf ein solches Objekt zugreifen werden, sollten Sie Objektvariablen verwenden und darüber auf die benötigten Eigenschaften und Methoden zugreifen. Die Verwendung der Objektvariablen sieht wie in folgender Rou tine aus: Public Function DatensaetzeZaehlen() 'Objektvariablen deklarieren Dim db As DAO.Database Dim rst As DAO.Recordset 'Objektvariable auf eine bestehende Instanz setzen Set db = Access.CurrentDb 'Neue Instanz eines Objekts erzeugen und 'per Objektvariable darauf verweisen Set rst = db.OpenRecordset("tblKontakte", dbOpenDynaset) Debug.Print rst.RecordCount '… weitere Aktionen mit dem Recordsetobjekt … rst.Close 'Objektvariablen freigeben Set rst = Nothing Set db = Nothing End Function Listing 15.4: Beispiel für das Setzen von Objektvariablen
Wenn Sie die mit CurrentDB erzeugte Instanz der aktuellen Datenbank mit einer Objektvariablen des Typs Database referenzieren, haben Sie einen entscheidenden Vorteil: Sie können die Objektvariable anschließend wieder auf den Wert Nothing setzen und gehen nicht das Risiko ein, dass die Instanz über den gewünschten Zeitraum hinaus existiert. Gleiches gilt für das im Beispiel erzeugte Recordset-Objekt: Dieses können Sie durch die Verwendung einer Objektvariablen erstens schließen (entfernt die Datensätze daraus) und zweitens die Objektvariable leeren (gibt belegten Speicherplatz frei).
776
Objektorientierte Programmierung
Auflistungen Viele Objekte lassen sich über Auflistungen ansprechen. Das Database-Objekt enthält beispielsweise eine TableDefs-Auflistung, über die man auf alle TableDef-Objekte zugreifen kann. Auflistungen stellen je nach Typ unterschiedliche Elemente zur Verfügung – in diesem Fall eine Count-Eigenschaft zur Ausgabe der Anzahl der enthaltenen Objekte und die drei Methoden Append, Delete und Refresh. Auf die Elemente von Auflistungen können Sie meist auf unterschiedliche Art zugreifen. Wenn Sie den Namen des Elements kennen, können Sie ihn in Klammern angeben, um auf das Objekt zuzugreifen: Debug.Print .TableDefs("tblKontakte").RecordCount
Eine andere Möglichkeit bietet der Index der jeweiligen Auflistung: Debug.Print CurrentDb.TableDefs(0).RecordCount
Die folgenden beiden Prozeduren enthalten Beispiele für die Verwendung von Auflis tungen. Public Sub TableDefsAuflisten_I() Dim db As DAO.Database Dim tdf As TableDef Set db = CurrentDb For Each tdf In db.TableDefs Debug.Print tdf.Name Next tdf Set db = Nothing End Sub Public Sub TableDefsAuflisten_II() Dim db As DAO.Database Dim intAnzahl As Integer Set db = CurrentDb intAnzahl = db.TableDefs.count For i = 0 To intAnzahl Debug.Print db.TableDefs(i).Name - 1 Next i
777
Kapitel 15 Set db = Nothing End Sub Listing 15.5: Beispiele für die Verwendung von Auflistungen
15.2.2 Erzeugen eines Objekts Wie Sie bereits oben erfahren haben, lassen sich manche Objekte über implizite Methoden wie die CurrentDB-Methode oder die OpenRecordset-Methode erzeugen. In manchen Fäl len ist allerdings eine explizite Instanzierung erforderlich. Die ADODB-Bibliothek erfordert im Vergleich zur DAO-Bibliothek grundsätzlich die explizite Instanzierung: Public Sub ExpliziteInstanzierung() Dim cnn As ADODB.Connection Dim rst As ADODB.Recordset 'Verweis auf existierendes Connection-Objekt Set cnn = CurrentProject.Connection 'explizite Instanzierung Set rst = New ADODB.Recordset rst.Open "tblKontakte", cnn, adOpenDynamic, adLockOptimistic Debug.Print rst.RecordCount rst.Close Set rst = Nothing Set cnn = Nothing End Sub Listing 15.6: Beispiele für implizite und explizite Instanzierung von Objektvariablen
15.2.3 Zugriff auf die Methoden, Eigenschaften und Ereignisse eines Objekts Die Methoden und Eigenschaften von Objekten können Sie über den Punkt-Operator ansprechen.
Eigenschaften Der Zugriff auf die Eigenschaften eines Objekts kann prinzipiell lesend und schreibend erfolgen, aber auch auf eine der beiden Möglichkeiten beschränkt sein. Das Schreiben eines Wertes in eine Eigenschaft erfolgt mit folgender Syntax:
778
Objektorientierte Programmierung .<Eigenschaft> =
Beispiel für das Füllen eines Textfeldes im aktuellen Formular (Schlüsselwort Me) mit einer neuen Zeichenkette: Me!txtBeispiel.Value = "Text"
Da es sich bei der Eigenschaft Value um die Default-Eigenschaft von Textfeldern handelt, können Sie diese Anweisung auch abkürzen: Me!txtBeispiel = "Text"
Das Lesen einer Eigenschaft eines Objekts sieht ähnlich aus: .<Eigenschaft>
Folgende Beispielanweisung liest den Inhalt eines Textfeldes im aktuellen Formular und gibt ihn im Direktfenster aus: Debug.Print Me.txtBeispiel
Etwas anders sieht es aus, wenn die Eigenschaft einen Verweis auf ein Objekt enthält. Das Setzen einer solchen Eigenschaft auf ein Objekt folgt dieser Syntax: Set .<Eigenschaft> =
Methoden Auch Methoden werden über die Punkt-Syntax referenziert: .<Methode>
Wie bei herkömmlichen VBA-Routinen verbergen sich auch hinter den Methoden eines Objekts Function- und Sub-Prozeduren mit oder ohne Parameter. Grundsätzlich ruft man Sub-Methoden mit Parameter ohne Klammern auf: .<Methode> <Parameterliste>
Function-Methoden sollen Werte zurückliefern, also muss man die Syntax mit Klammern verwenden: = .<Methode>(<Parameterliste>)
15.2.4 Lebensdauer eines Objekts Objekte beginnen ihr Dasein mit der Instanzierung. Wie lange ein Objekt »lebt«, hängt von zwei Faktoren ab – dem Gültigkeitsbereich und einer eventuellen manuellen Zer störung.
779
Kapitel 15
Es gibt folgende Gültigkeitsbereiche: Global: Die globale Gültigkeit wird in einem Standardmodul über die Deklaration einer öffentlichen Objektvariablen erreicht. Der Gültigkeitsbereich endet entweder mit der manuellen Zerstörung der Variablen oder mit dem Beenden der Anwendung. Daneben führen unbehandelte Fehler im VBA-Projekt ebenfalls dazu, dass Objekte ihren Inhalt verlieren. Modulweit: Die Objektvariable wird in einem Klassenmodul oder Formular-/ Berichtsmodul deklariert. Der Gültigkeitsbereich beginnt mit dem Instanzieren eines Objekts auf Basis des Klassenmoduls beziehungsweise mit dem Öffnen eines Formulars oder Berichts oder dem Instanzieren des jeweiligen Moduls.
15.3 Klassenmodule Wenn Sie in Ihrer Anwendung mit benutzerdefinierten Objekten arbeiten möchten, müssen Sie zunächst entsprechende Klassenmodule anlegen und darin die Eigenschaften und Methoden festlegen, die das Objekt zur Verfügung stellen soll.
15.3.1 Anlegen eines Klassenmoduls Das Anlegen eines Klassenmoduls mit seinen Eigenschaften und Methoden lernen Sie am Beispiel eines Bankkontos kennen. Ein Klassenmodul legen Sie so an: Im Access-Fenster: Betätigen Sie den Ribbon-Eintrag Erstellen|Andere|Makro/Klassen modul. Im VBA-Editor verwenden Sie entweder denselben Menüeintrag Einfügen|Klassenmo dul oder den gleichnamigen Kontextmenüeintrag des Projektexplorers (siehe Abbil dung 15.1). Den Projektexplorer aktivieren Sie am schnellsten mit Strg + R.
15.3.2 Benennen des Klassenmoduls Das neue Klassenmodul erhält automatisch den Namen Klasse1 (außer, dieser Name ist bereits vergeben – dann wird statt der 1 die nächst höhere noch nicht verwendete Zahl angehängt). Diesen Namen ersetzen Sie natürlich durch eine sinnvollere Bezeichnung, die aus dem Präfix cls (im Gegensatz zum Präfix mdl für Standardmodule) und dem Singular des Namens des beschriebenen Objekts besteht. Im vorliegenden Fall soll ein Objekt mit den Eigenschaften und Methoden eines Kontos erzeugt werden, also vergeben Sie für das entsprechende Klassenmodul den Namen clsKonto. Um den Modulnamen zu ändern, müssen Sie das Modul zunächst speichern (Strg + S). Im nun erscheinenden Speichern unter-Dialog tragen Sie dann den gewünschten Namen
780
Objektorientierte Programmierung
ein. Zum späteren Ändern des Namens eines Klassenmoduls gibt es zwei Möglichkei ten: Markieren Sie im Navigationsbereich den zu ändernden Eintrag und drücken Sie F2 oder ändern Sie den Namen direkt im VBA-Editor, indem Sie das Eigenschaftsfenster aktivieren (F4) und dort die Änderung vornehmen. Beide Varianten setzen das vorherige Speichern des Klassenmoduls voraus.
Abbildung 15.1: Einfügen eines neuen Klassenmoduls
15.4 Eigenschaften einer Klasse Bevor Sie Eigenschaften und Methoden im Klassenmodul anlegen, empfiehlt sich ihre Skizzierung in Form eines Klassendiagramms wie in Abbildung 15.2. Das scheint im vorliegenden Fall vielleicht ein wenig übertrieben, aber es ist sicher kein Fehler, sich vor dem Programmieren noch einmal mit dem Plan auseinander zu setzen. Die Kontoklasse hat die vier Eigenschaften Besitzer, Kontonummer, Kontostand und Dispositionsrahmen sowie die beiden Methoden Einzahlen() und Auszahlen(). Den Besitzer könnte man auch in einer eigenen Klasse modellieren, aber für dieses einfache Beispiel soll sein Name in der Kontoklasse gespeichert werden.
781
Kapitel 15
cls Ko n to Bes itz er Kontonummer Kontos tand Dis pos itions rahmen Einz ahlen() A us z ahlen()
Abbildung 15.2: Die Kontoklasse im Überblick
15.4.1 Öffentliche und nicht öffentliche Eigenschaften Das Klassenmodul enthält für jede Eigenschaft der Klasse eine Variable. Eine Variable, die von außen änderbar sein soll, deklariert man in Standardmodulen als öffentliche Variable. Der Inhalt des Klassenmoduls clsKonto würde dann folgendermaßen aussehen: Option Compare Database Option Explicit Public Public Public Public
Besitzer As String Kontonummer As Long Kontostand As Currency Dispositionsrahmen As Currency
Listing 15.7: Kontoklasse mit öffentlichen Eigenschaften
Die Eigenschaften des Kontos wären dann von allen Stellen aus les- und schreibbar, von denen man auch auf das Objekt zugreifen könnte. Sie könnten beispielsweise die folgenden Anweisungen im Testfenster (Strg + G) absetzen und damit den Kontostand lesen und auf einen beliebigen Wert ändern (Details über das Instanzieren eines Objekts und den Zugriff auf dessen Eigenschaften finden Sie in Abschnitt 16.2, »Objekte«): Set objKonto = New clsKonto objKonto.Kontostand = 100 Debug.Print objKonto.Kontostand
Die Debug.Print-Anweisung gibt im Testfenster den Wert 100 als aktuellen Kontostand aus, der zuvor manuell auf diesen Wert eingestellt wurde. Die Kapselung wird mit der öffentlichen Deklaration der Variablen wie mit einem Vorschlaghammer zerstört. Das wird gerade im Beispiel des Bankkontos deutlich: Auch sorgfältig festgelegte Geschäftsregeln können nicht greifen, wenn man sie von außen umgehen kann. In diesem Beispiel könnte man etwa den Kontostand so verändern, dass er nicht mehr mit dem Dispositionsrahmen vereinbar ist.
782
Objektorientierte Programmierung
15.4.2 Zugriff auf die Eigenschaften einer Klasse kontrollieren Sie müssen die Eigenschaften also auf irgendeine Weise vor dem direkten Zugriff von außen schützen und damit die Kapselung realisieren. Dazu sind zwei Schritte erforderlich. Deklarieren Sie die Variablen der Klasse mit dem Schlüsselwort Private und verhindern Sie so den direkten Zugriff von außen. Erstellen Sie öffentliche Methoden für den kontrollierten Zugriff auf die privaten Eigenschaften. Schritt 1 lässt sich leicht in die Tat umsetzen. Das folgende Listing enthält die als Private deklarierten Eigenschaften. Außerdem hat jede Eigenschaft ein m als Präfix erhalten. Damit kennzeichnet man die Member-Variablen eines Objekts – das sind Variablen, die zwar privat sind, aber über entsprechende Prozeduren gelesen oder geschrieben werden können. Private Private Private Private
mBesitzer As clsKunde mKontonummer As Long mKontostand As Currency mDispositionsrahmen As Currency
Listing 15.8: Deklaration privater Variablen in einem Klassenmodul
Nachdem die Variablen nun vor dem unkontrollierten Zugriff von außen geschützt sind, können Sie die Möglichkeit zum Lesen und Schreiben der Variable nach Bedarf freigeben. Dazu verwenden Sie so genannte Eigenschaftsprozeduren. Es gibt drei unterschiedliche Arten von Eigenschaftsprozeduren: Eine Property Get-Prozedur gibt je nach dem Variablentyp einen Verweis auf ein Objekt oder den Wert der Variablen zurück. Eine Property Let-Prozedur schreibt den als Parameter übergebenen Wert in die angegebene Variable. Diese Prozedurart können Sie nur verwenden, wenn die Variable einen konkreten Wert erhält. Eine Property Set-Prozedur weist der angegebenen Variablen einen Verweis auf das als Parameter übergebene Objekt zu. Diese Prozedurart ist das Pendant für die Property Let-Prozedur für Objektvariablen.
Skalare Variable versus Objektvariable Bei der Verwendung von Eigenschafts-Prozeduren für den Zugriff auf skalare Variablen und Objektvariablen gibt es einige Unterschiede. Der Grund sind Unterschiede zwi-
783
Kapitel 15
schen den Datentypen selbst. Damit Sie jeweils den richtigen Fall auswählen, finden Sie nachfolgend eine kurze Erläuterung und Einordnung dieser beiden Variablenarten. Skalare Variablen sind Variablen mit eingebauten Datentypen wie String, Integer oder Long, Aufzählungstypen und benutzerdefinierte Datentypen. Diese Datentypen haben gemein, dass sie konkrete Werte enthalten und je nach Datentyp den entsprechenden Speicherplatz reservieren. Die Zuweisung von Werten an solche Datentypen erfolgt durch Verwendung des Gleichheitszeichens und des entsprechenden Wertes: intZahl = 1
Im Gegensatz dazu enthalten Objektvariablen kein Objekt, sondern lediglich einen Verweis darauf. Das bringt einige Besonderheiten mit sich. Speicherplatz wird nach der Deklaration nur für den Verweis selbst reserviert (Zeiger des Datentyps Long). Der Verweis auf das Objekt erfordert die Verwendung des Schlüsselwortes Set: Set objKunde = New clsKunde
Es können auch mehrere Objektvariablen auf dasselbe Objekt verweisen: Set objKunde = New clsKunde Set objPartner = objKunde
Vergleiche zweier Objektvariablen erfolgen über den Operator Is: If objKunde Is objPartner Then MsgBox "Kunde und Partner sind gleich." End If
Die Prüfung, ob eine Objektvariable auf ein Objekt verweist, erfolgt über den Vergleich mit dem Operator Is und dem Vergleichswert Nothing: If objKunde Is Nothing Then MsgBox "Kunde ist nicht gesetzt." End If
Das Aufheben des Verweises erfolgt beim Verlassen des Gültigkeitsbereichs (also etwa beim Beenden der Prozedur, in der der Verweis gesetzt wurde) oder durch explizites Setzen des Verweises auf den Wert Nothing. Tipp: Setzen Sie jede erzeugte Objektvariable per Code wieder auf den Wert Nothing. Manchmal benötigt man einen Verweis auf eine Objektvariable längst nicht mehr, obwohl ihr Gültigkeitsbereich noch nicht verlassen wurde. Zu Gunsten einer ressourcen schonenden Programmierweise sollten Sie eine Objektvariable daher auf den Wert Nothing setzen, sobald diese nicht mehr benötigt wird. Am besten legen Sie beim Schreiben der Zeile zum Zuweisen des Objektverweises auch direkt eine Zeile an, die
784
Objektorientierte Programmierung
die passende Objektvariable wieder gleich Nothing setzt – so vergessen Sie das anschließend nicht.
15.4.3 Property Let: Setzen von skalaren Variablen Wenn Sie den schreibenden Zugriff auf eine Variable eines Objekts erlauben möchten, müssen Sie dazu eine Property Let-Prozedur verwenden. Eine solche Prozedur erwartet als Parameter den Wert, auf den die Variable gesetzt werden soll. Das folgende Listing enthält ein Beispiel für die Variable Kontonummer: Public Property Let Kontonummer(lngKontonummer As String) mKontonummer = lngKontonummer End Property Listing 15.9: Property-Prozedur für den schreibenden Zugriff auf eine private Variable
Die Property Let-Prozedur erwartet den Eingabeparameter lngKontonummer nicht in der für Prozeduren üblichen Art, auch wenn es die Notation des Prozedurkopfs erwarten ließe. Statt dessen weisen Sie den Wert einfach der Eigenschaft mit dem Namen der Prozedur zu: Public Sub KontonummerZuweisen() Dim objKonto As clsKonto Set objKonto = New clsKonto 'Kontonummer zuweisen objKonto.Kontonummer = 123456789 '... Set objKonto = Nothing End Sub Listing 15.10: Zuweisen einer Objekt-Eigenschaft
15.4.4 Property Set: Setzen von Objektvariablen Das Zuweisen von Objektvariablen funktioniert prinzipiell genauso wie das Zuweisen einer skalaren Variablen. Der Unterschied ist, dass die erste Zeile der Eigenschaftsprozedur das Schlüsselwort Set statt Let enthält und dass die eigentliche Zuweisung über das Schlüsselwort Set erfolgt: Public Property Set Besitzer(objBesitzer As clsKunde) Set mBesitzer = objBesitzer End Property Listing 15.11: Diese Objektvariable
Property-Prozedur
erlaubt
den
schreibenden
Zugriff
auf
eine
785
Kapitel 15
Der Zugriff auf private Objektvariablen über eine Property Set-Prozedur erfolgt wie im folgenden Beispiel: Public Sub KontoErzeugen() Dim objKonto As clsKonto Set objKonto = New clsKonto With objKonto Set .Besitzer = New clsKunde End With End Sub Listing 15.12: Die Objektvariable Besitzer erhält einen Verweis auf ein neues Kundenobjekt
15.4.5 Property Get: Lesen von skalaren Variablen und Objektvariablen Property Get-Prozeduren dienen dem lesenden Zugriff sowohl auf skalare als auch auf Objektvariablen. Diese Prozeduren arbeiten prinzipiell wie Funktionen: Sie geben den Inhalt der gewünschten Membervariablen des Objekts als Funktionswert zurück.
Skalare Variablen lesen Die Eigenschafts-Prozedur im folgenden Listing erlaubt den lesenden Zugriff auf die private Variable mKontonummer. Die Routine weist dem Rückgabewert Kontonummer den Inhalt der Variablen mKontonummer zu. Public Property Get Kontonummer() As String Kontonummer = mKontonummer End Property Listing 15.13: Property-Prozedur für den lesenden Zugriff auf eine private Variable
Mit der folgenden Prozedur weisen Sie der Eigenschaft Kontonummer zunächst einen Wert zu und verwenden dann die Property Get-Prozedur, um den Wert in einem Meldungsfenster auszugeben: Public Sub KontonummerAusgeben() Dim objKonto As clsKonto Set objKonto = New clsKonto 'Wert zuweisen per Property Let objKonto.Kontonummer = 123456789
786
Objektorientierte Programmierung 'Wert ausgeben per Property Get MsgBox "Die Kontonummer lautet: " & objKonto.Kontonummer Set objKonto = Nothing End Sub Listing 15.14: Aufeinander folgendes Aufrufen der Property Let- und der Property Get-Prozedur des Konto-Objekts
Objektvariablen lesen Das Lesen von Objektvariablen erfolgt ebenfalls mit einer Property Get-Prozedur. Aller dings verwendet man zum Zuweisen des Verweises innerhalb der Prozedur das SetSchlüsselwort: Public Property Get Besitzer() As clsKunde Set Besitzer = mBesitzer End Property Listing 15.15: Property Get-Prozedur für eine Objektvariable
Das Zuweisen der Objektvariablen eines Objekts an eine andere Objektvariable erfolgt mit der Set-Anweisung und der entsprechenden Eigenschaft des Objekts: Set objKunde = objKonto.Besitzer
Get = Let/Set Damit Sie eine Eigenschaft sowohl lesen als auch setzen können, müssen die passenden Get- und Let/Set-Property-Routinen natürlich den gleichen Namen aufweisen.
15.4.6 Vertrauen ist gut, Kontrolle ist besser Weiter oben war vom »kontrollierten Zugriff« auf Eigenschaften die Rede. Nachdem Sie nun wissen, wie Sie den schreibenden und lesenden Zugriff auf die Eigenschaften einer Klasse realisieren, müssen Sie nur noch entsprechende Kontrollfunktionen einbauen. Die Property Let-/Set-/Get-Prozeduren sind nämlich nicht auf die Anweisung zum Weiterleiten des Übergabewertes beschränkt, sondern können noch weitere Anwei sungen aufnehmen. So könnten Sie beispielsweise das Setzen des Kontostandes in Ab hängigkeit vom Dispositionsrahmen kontrollieren: Public Property Let Kontostand(curKontostand As Currency) If curKontostand < mDispositionsrahmen Then MsgBox "Der gewünschte Kontostand konnte nicht eingestellt werden." Else
787
Kapitel 15 mKontostand = curKontostand End If End Property Listing 15.16: Prüfen einer Eingabe in einer Property Let-Prozedur
Wenn Sie wie mit der folgenden Prozedur den Kontostand auf einen Wert einstellen möchten, der unter dem Dispositionsrahmen liegt, erscheint eine entsprechende Mel dung. Public Sub KontostandEinstellen() Dim objKonto As clsKonto Set objKonto = New clsKonto objKonto.Dispositionsrahmen = 0 objKonto.Kontostand = -100 Set objKonto = Nothing End Sub Listing 15.17: Einstellen eines ungültigen Kontostands
Auf die gleiche Weise lässt sich beispielsweise protokollieren, wann bestimmte Daten abgerufen oder geändert wurden.
15.5 Methoden einer Klasse Es gibt zwei unterschiedliche Arten von Methoden, die Sie schon von der prozeduralen Programmierung her kennen: Function-Prozeduren und Sub-Prozeduren. Eine Klasse kann private und öffentliche Methoden enthalten, aber nur die öffentlichen Methoden sind über die Schnittstelle ansprechbar. Für Function- und Sub-Prozeduren gelten dabei genau die gleichen Regeln wie bei der prozeduralen Programmierung: Sie können beiden Parameter übergeben, aber nur eine Funktion liefert auch einen Wert zurück. Die folgende Sub-Prozedur Einzahlen erwartet als Parameter den einzuzahlenden Betrag und ändert den Wert der Membervariablen mKontostand: Public Sub Einzahlen(curBetrag As Currency) mKontostand = mKontostand + curBetrag End Sub Listing 15.18: Beispiel einer Objekt-Methode
788
Objektorientierte Programmierung
Die Prozedur lässt sich mit der Routine aus folgendem Listing prüfen: Public Sub Einzahlung() Dim objKonto As clsKonto Set objKonto = New clsKonto objKonto.Kontostand = 0 objKonto.Einzahlen 100 MsgBox "Der neue Kontostand beträgt EUR " & objKonto.Kontostand Set objKonto = Nothing End Sub Listing 15.19: Anwendung der Einzahlen-Methode
Als Beispiel für eine Function-Prozedur dient der Auszahlungsvorgang: Da eine Auszah lung wegen fehlender Deckung fehlschlagen könnte, gibt die folgende Funktion den Wert False zurück, falls die Auszahlung nicht möglich ist: Public Function Auszahlen(curBetrag As Currency) As Boolean If mKontostand - curBetrag < mDispositionsrahmen Then Auszahlen = False Else mKontostand = mKontostand - curBetrag Auszahlen = True End If End Function Listing 15.20: Beispiel einer Objekt-Methode mit Rückgabewert
Der folgende Beispielaufruf der Methode Auszahlen versucht, einen größeren Betrag als erlaubt abzuheben: Public Sub Auszahlung() Dim objKonto As clsKonto Set objKonto = New clsKonto objKonto.Kontostand = 0 objKonto.Dispositionsrahmen = -100 If objKonto.Auszahlen(200) = False Then MsgBox "Der Betrag konnte nicht ausgezahlt werden." End If
789
Kapitel 15 Set objKonto = Nothing End Sub Listing 15.21: Test der Auszahlen-Methode
15.6 Standardereignisse in Klassen VBA-Klassenmodule liefern zwei Standardereignisse mit: Initialize und Terminate. Sie können die entsprechenden Ereignisprozeduren einfügen, indem Sie im linken Kombi nationsfeld des Codefensters den Eintrag Class und im rechten das gewünschte Ereignis auswählen (siehe Abbildung 15.3). Diese Ereignisse werden beim Initialisieren und beim Zerstören jeder Instanz dieser Klasse ausgelöst. Sie können dort beispielsweise Anweisungen zum Instanzieren von in der Klasse benötigten Objekten unterbringen. Ein Beispiel für den Einsatz dieser Ereignisse finden Sie etwa in Abschnitt 16.8.2, »Benutzerdefinierte Auflistungsklassen«.
Abbildung 15.3: Einfügen der Standardereignisse einer Klasse
15.7 Benutzerdefinierte Ereignisse Benutzerdefinierte Ereignisse sind eine sehr mächtige Funktion von VBA. Bevor Sie selbst solche Ereignisse anlegen und lernen, wie Sie diese aufrufen und abfangen können, sehen Sie sich ein Objekt an, das bereits Ereignisse besitzt, und schauen Sie, wie Sie dessen Ereignisse abfangen und für Ihre Zwecke einsetzen können.
15.7.1 Ereignisse abfangen Ein Beispiel für ein Objekt, auf dessen Ereignisse Sie in jedem Fall angewiesen sind, ist das Webbrowser-Steuerelement. Angenommen Sie möchten eine Internetseite aufrufen
790
Objektorientierte Programmierung
und eine bestimmte Information daraus abfragen – und das auch noch automatisiert. Dazu benötigen Sie nicht den Internet Explorer, sondern das oben erwähnte Steuerele ment, das sich bequem in ein Formular einbauen lässt: Legen Sie ein neues Formular an und öffnen Sie es in der Entwurfsansicht. Wählen Sie den Ribbon-Eintrag Entwurf|Steuerelemente|ActiveX-Steuerelement einfügen aus. Markieren Sie im nun erscheinenden Dialog den Eintrag Microsoft Webbrowser und klicken Sie auf OK (siehe Abbildung 15.4).
Abbildung 15.4: Auswahl eines ActiveX-Steuerelements
Access fügt das Webbrowser-Steuerelement in das Formular ein. Ändern Sie den Namen des Steuerelements auf ctlWebbrowser ab (siehe Abbildung 15.5). Nun legen Sie eine Schaltfläche namens cmdLoad an, mit der Sie das Laden einer bestimmten Internetseite (beispielsweise http://www.access-im-unternehmen.de) starten. Und dann folgt endlich das Beispiel für ein Ereignis: Wenn die Seite fertig geladen ist, soll das Formular ein Meldungsfenster anzeigen. Damit ein Klick auf die Schaltfläche das Laden der Seite auslöst, hinterlegen Sie eine entsprechende Prozedur für die Ereigniseigenschaft Beim Klicken der Schaltfläche. Im Codefenster sollten Sie nun zunächst eine Objektvariable mit einem Verweis auf das Webbrowser-Steuerelement deklarieren und diesem das Steuerelement zuweisen. Normalerweise würden Sie dabei wie in Abbildung 16.6 vorgehen. Da Sie allerdings auf die Ereignisse des Webbrowser-Steuerelements zugreifen möchten, müssen Sie das entsprechende Objekt mit dem Schlüsselwort WithEvents deklarieren.
791
Kapitel 15
Abbildung 15.5: Das Webbrowser-Steuerelement
Abbildung 15.6: Eigenschaften und Methoden des Webbrowser-Steuerelements
Außerdem werden Sie früher oder später von mehreren Prozeduren auf das WebbrowserObjekt zugreifen wollen und dieses auch nicht erst beim Klicken auf die Schaltfläche zum Laden der Internetseite instanzieren. Der folgende Code erfüllt alle diese Voraus setzungen:
792
Objektorientierte Programmierung Option Compare Database Option Explicit Dim WithEvents objWebbrowser As WebBrowser Private Sub cmdLoad_Click() 'Eine Internetseite laden objWebbrowser.Navigate "http://www.access-im-unternehmen.de" End Sub Private Sub objWebbrowser_NavigateComplete2(ByVal pDisp As Object, _ URL As Variant) 'Wenn fertiggeladen, Meldung ausgeben MsgBox objWebbrowser.LocationURL End Sub Private Sub Form_Close() 'Zerstören des Objektverweises beim Schließen Set objWebbrowser = Nothing End Sub Private Sub Form_Open(Cancel As Integer) 'Zuweisen des Objekts beim Öffnen des Formulars Set objWebbrowser = Me!ctlWebbrowser.Object End Sub Listing 15.22: Klassenmodul des Webbrowser-Formulars
Durch die Verwendung des Schlüsselworts WithEvents bei der Deklaration des WebbrowserObjekts stehen alle Ereignisse dieses Objekts im Formularmodul zur Verfügung. Im Beispielcode kommt davon das Ereignis NavigateComplete2 zur Geltung: Es wird ausgelöst, wenn das Webbrowser-Steuerelement die angeforderte Seite fertig geladen hat. Die Prozedur objWebbrowser_NavigateComplete2 fängt also ein Ereignis eines eingebundenen Objekts ab und gibt eine Meldung mit dem URL der geladenen Seite aus. Die Deklaration mit WithEvents kann nur in Klassenmodulen erfolgen – in allein stehen den und auch in denen von Formularen und Berichten. Sie können nicht das Schlüssel wort New in Zusammenhang mit WithEvents verwenden.
15.7.2 Eigene Ereignisse anlegen Genau wie das Webbrowser-Objekt im vorherigen Beispiel können auch benutzerdefinierte Klassen Ereignisse bereitstellen. Benutzerdefinierte Ereignisse bieten die Möglichkeit, von außen auf bestimmte Ereignisse in einer betroffenen Klasse zu »lauschen«. Und das funktioniert folgendermaßen:
793
Kapitel 15
Sie legen in einer Klasse ein Ereignis fest. Dazu verwenden Sie das EventSchlüsselwort. Bei Bedarf löst irgendeine Prozedur innerhalb der Klasse dieses Ereignis aus. Dazu ist die RaiseEvent-Anweisung erforderlich. Die Klasse mit dem Ereignis deklarieren Sie in der Klasse, die auf das Eintreten des Ereignisses reagieren soll, mit dem Schlüsselwort WithEvents. Schließlich legen Sie eine entsprechende Ereignisprozedur an. Das folgende Beispiel füllt die Vorgehensweise mit Leben. Dabei spielen zwei Formulare die Hauptrolle: Das erste Formular namens frmKontakte enthält ein Listenfeld zur Auswahl von Kontakten. Per Doppelklick auf das Listenfeld oder mit einem einfachen Klick auf die Schaltfläche soll das Formular frmKontaktDetails geöffnet werden und den ausgewählten Datensatz anzeigen (siehe Abbildung 15.7).
Abbildung 15.7: Formulare des Beispiels für benutzerdefinierte Ereignisse
Ziel der Übung ist, dem Detailformular ein Ereignis zu verpassen, das beim Ändern des angezeigten Datensatzes ausgelöst wird. Das aufrufende Formular soll mit einer Ereignisprozedur auf dieses Ereignis reagieren und den Inhalt des Listenfeldes aktualisieren. Nun öffnet man Detailformulare in der Regel als modalen Dialog, indem man die OpenForm-Methode des DoCmd-Objekts verwendet und dabei den Parameter WindowMode auf True setzt. Die Ausführung der aufrufenden Prozedur wird dann so lan-
794
Objektorientierte Programmierung
ge unterbrochen, bis das aufgerufene Formular entweder unsichtbar gemacht oder geschlossen wird – dann sorgt eine entsprechende Requery-Methode für die Aktualisierung des Inhalts des Listenfeldes. Diese Methode wird auch aufgerufen, wenn der Benutzer den Kontakt-Datensatz nur ansehen möchte und ihn gar nicht ändert – in diesem Fall also völlig unnötig.
Hinzufügen des Ereignisses Der erste Schritt auf dem Weg zur benutzerdefinierten Ereignisbehandlung ist das Anlegen des Ereignisses. Das Ereignis soll Change heißen und es sind keine Parameter notwendig. Dementsprechend sieht die Deklaration des Ereignisses wie folgt aus: Public Event Change()
Auslösen des Ereignisses Nachdem Sie das Ereignis angelegt haben, müssen Sie einen Zeitpunkt auswählen, an dem das Ereignis ausgelöst werden soll. Die Ereignisprozedur Nach Aktualisierung des Formulars scheint die richtige Wahl zu sein: Sie wird nur ausgelöst, wenn der Datensatz geändert wurde und deshalb gespeichert werden soll. Legen Sie die Ereignisprozedur an, indem Sie die beiden Kombinationsfelder im Codefenster auf die Einträge Form und AfterUpdate einstellen (siehe Abbildung 15.8) und ergänzen Sie den automatisch angelegten Prozedurrumpf wie folgt: Private Sub Form_AfterUpdate() 'Ereignis auslösen RaiseEvent Change End Sub Listing 15.23: Auslösen eines Ereignisses
Abbildung 15.8: Anlegen der Ereignisprozedur, die wiederum ein Ereignis auslösen soll
795
Kapitel 15
Auf ein Ereignis reagieren Um mit einer Ereignisprozedur auf ein Ereignis zu reagieren, müssen einige Vorausset zungen erfüllt sein: Nur Klassenmodule (einschließlich Formular- und Berichtsmodule) können Ereignis prozeduren implementieren. Die Klasse, die das Ereignis enthält, muss in der Klasse, in der mit einer Ereignispro zedur auf das Ereignis reagiert werden soll, mit dem Schlüsselwort WithEvents in Form einer Objektvariablen deklariert werden: Private WithEvents objKontaktDetail As Form_frmKontaktDetail
Die Objektvariable muss auf eine Instanz der Klasse mit dem Ereignis verweisen. Die erste Bedingung ist erfüllt, da die Ereignisprozedur im aufrufenden Formular ausgewertet werden soll. Die für die zweite Bedingung notwendige Codezeile können Sie einfach von dort in das Klassenmodul des Übersichtsformulars übernehmen. Ein kleines Problem ist die dritte Bedingung, denn, wie bereits weiter oben erwähnt, öffnet man ein Formular in der Regel mit der DoCmd.OpenForm-Anweisung – gerade, weil dies offensichtlich der einzige Weg ist, ein Formular modal zu öffnen. Außerdem lässt sich so bequem eine Where-Bedingung mitgeben: DoCmd.OpenForm "frmSchnellsuchePerKombifeld", _ WhereCondition:="KontaktID = " & Me!lstKontakte, _ WindowMode:=acDialog
Das ist aber auch nur die halbe Wahrheit: Formulare stellen eine Eigenschaft namens Modal zur Verfügung, die Sie im Eigenschaftsfenster auf der Registerseite Andere unter dem Namen Gebunden finden. Nach der Änderung auf den Wert True lässt sich das Formular ausschließlich im modalen Modus öffnen – aber für dieses Beispiel ist das durchaus in Ordnung. Anschließend speichern und schließen Sie das Formular. Nun können Sie auf die DoCmd.OpenForm-Anweisung verzichten, eine neue Instanz des Formulars mit der New-Anweisung erstellen und direkt mit der Objektvariable objKontaktDetail darauf verweisen. Die notwendige Where-Bedingung ersetzen Sie durch die Verwendung der beiden Eigenschaften Filter und FilterOn. Erst durch Setzen der Visible-Eigenschaft auf den Wert True wird das Formular sichtbar gemacht – als modaler Dialog. Die Voraussetzungen zum Abfangen von Ereignissen des so geöffneten Formulars per Ereignisprozedur wären damit erfüllt.
796
Objektorientierte Programmierung Private Sub Anzeigen() 'Wenn Kontakt ausgewählt If Not IsNull(Me!lstKontakte) Then 'Neues Formular instanzieren Set objKontaktDetail = New Form_frmKontaktDetail With objKontaktDetail 'Filter setzen .Filter = "KontaktID = " & Me!lstKontakte .FilterOn = True 'Sichtbar machen .Visible = True End With End If End Sub Listing 15.24: Setzen der Objektvariable auf eine neue Instanz des Formulars frmKontaktDetail
Nun müssen Sie im aufrufenden Formular nur noch die gewünschte Ereignisprozedur anlegen. Dazu wählen Sie im linken Kombinationsfeld des Codefensters den Eintrag objKontaktDetail aus – der Prozedurrumpf für das einzige zur Verfügung stehende Ereignis wird automatisch angelegt (siehe Abbildung 15.9).
Abbildung 15.9: Anlegen einer Ereignisprozedur für die benutzerdefinierte Eigenschaft
Den Prozedurrumpf müssen Sie nun nur noch mit der für die Aktualisierung notwendigen Anweisung füllen: Private Sub objKontaktDetail_Change() 'Listenfeld nach Änderungen im Detailformular aktualisieren Me.lstKontakte.Requery End Sub Listing 15.25: Ereignisprozedur zum Aktualisieren des Listenfeldes
797
Kapitel 15
15.8 Benutzerdefinierte Auflistungen mit dem Collection-Objekt Viele VBA-Objekte sind Bestandteil von Auflistungen und enthalten selbst wiederum Auflistungen mit anderen Objekten. Ein Beispiel aus der DAO-Bibliothek ist das DatabaseObjekt, das eine TableDefs-Auflistung mit TableDef-Objekten enthält, die wiederum eine Fields-Auflistung mit Field-Objekten bereitstellt. Diese Möglichkeit ist sehr interessant, da man damit leicht auf die komplette Sammlung enthaltener Objekte zugreifen kann. Das sieht dann beispielsweise so wie im folgenden Listing aus. Die Routine TabellenUndFelder gibt alle Tabellen der aktuellen Datenbank inklusive Feldnamen aus. Dabei durchläuft sie zwei For Each-Schleifen: die erste bearbeitet die Auflistung TableDefs des Database-Objekts, die zweite die Auflistung Fields des aktuellen TableDef-Objekts. Die Ausgabe sieht wie in Abbildung 15.10 aus. Public Function TabellenUndFelder() Dim db As DAO.Database Dim tdf As DAO.TableDef Dim fld As DAO.Field Set db = CurrentDb For Each tdf In db.TableDefs Debug.Print tdf.Name For Each fld In tdf.Fields Debug.Print " " & fld.Name Next fld Debug.Print "=================" Next tdf Set db = Nothing End Function Listing 15.26: Ausgabe von Tabellen und Feldern per Auflistung
Auflistungen mit dem Collection-Objekt Das Collection-Objekt ermöglicht die benutzerdefinierte Verwendung von Auflistungen, die nicht nur Zahlen oder Zeichenketten, sondern komplexe Objekte enthalten können. Diese können auch wieder Auflistungen enthalten, sodass sich die oben dargestellte Technik zum Durchlaufen von einfachen oder verschachtelten Auflistungen realisieren lässt (siehe Abschnitt 15.8.2, »Benutzerdefinierte Auflistungsklassen«).
798
Objektorientierte Programmierung
Abbildung 15.10: Ausgabe verschachtelter For Each-Schleifen über zwei Collections
Bevor Sie sich an ein solches – zugegebenermaßen relativ komplexes – Beispiel begeben, lernen Sie an einem einfacheren Beispiel den grundlegenden Umgang mit dem Collection-Objekt kennen.
15.8.1 Auflistungen selbst gemacht Ein Beispiel für die Verwendung des Collection-Objekts ist eine Auflistung zum Speichern globaler Variablen. Globale Variablen sind generell anfällig, da sie leicht aus Versehen geändert werden können. Ein wenig sicherer ist die Verwendung einer öffentlichen Auf listung. Das folgende Listing zeigt den Inhalt des Moduls mdlGlobal, das eine öffentliche Auflistung namens colVariablen deklariert und zwei Routinen zum Setzen und zum Lesen von Variablen bietet: Option Compare Database Option Explicit Public colVariablen As VBA.Collection Public Sub SetVariable(strVariable As String, strWert As Variant) If colVariablen Is Nothing Then Set colVariablen = New VBA.Collection End If On Error Resume Next 'Variable hinzufügen colVariablen.Add strWert, strVariable
799
Kapitel 15 'Variable ist bereits vorhanden If Err.Number = 457 Then 'Element löschen ... colVariablen.Remove (strVariable) '... und neu hinzufügen colVariablen.Add strWert, strVariable End If End Sub Public Function GetVariable(strVariable As String) As Variant On Error Resume Next 'Variable ermitteln GetVariable = colVariablen(strVariable) 'Falls nicht vorhanden, Fehlermeldung ausgeben If Err.Number <> 0 Then MsgBox "Variable mit diesem Namen nicht vorhanden!", vbCritical End If End Function Public Sub DeleteVariable(strVariable As String) On Error Resume Next colVariablen.Remove strVariable End Sub Public Function CountVariables() On Error Resume Next CountVariables = colVariablen.count If Err = 91 Then CountVariables = 0 End If End Function Listing 15.27: Beispiel für die Anwendung des Collection-Objekts
Hinzufügen von öffentlichen Variablen Die Routine SetVariable erwartet zwei Parameter: den Namen der Variablen und den zu speichernden Wert. Folgender Aufruf würde etwa eine Variable namens Datenbankpfad und als Wert den Pfad der aktuellen Datenbank als Listeneintrag speichern:
Die Prozedur verwendet die Add-Methode des Collection-Objekts, um das Wertepaar hinzuzufügen.
Abfragen von öffentlichen Variablen Mit der Funktion GetVariable fragen Sie einen bestehenden Wert ab. Dabei verwendet die Funktion den Variablennamen als Schlüssel des Collection-Objekts und ermittelt so den gewünschten Wert: Debug.Print GetVariable("Datenbankpfad")
Löschen von öffentlichen Variablen Für die Entfernung von Einträgen aus einem Collection-Objekt ist die Remove-Methode zuständig. Sie erwartet den Index des zu entfernenden Eintrags. Ein Beispielaufruf lautet wie folgt: DeleteVariable "Datenbankpfad"
Anzahl gespeicherter Variablen Auch die Anzahl der Elemente einer Auflistung lässt sich ermitteln. Dazu dient die Count-Eigenschaft des Collection-Objekts.
15.8.2 Benutzerdefinierte Auflistungsklassen Eine benutzerdefinierte Auflistungsklasse kapselt das Collection-Objekt und erweitert dieses um ein beziehungsweise zwei Features (eines davon funktioniert unter Access 2003 und 2007, aber nicht mit älteren Versionen von Access). Nachfolgend finden Sie das Listing der kompletten Auflistungsklasse, die – relativ sinnfrei – Namen in der Auflistung speichert, ausgibt, löscht und die Anzahl der enthaltenen Einträge ermittelt. Eingebaute Auflistungen lassen sich per For Each-Schleife durchlaufen; außerdem kann man auf eine Standard-Eigenschaft zugreifen und damit Bezüge abkürzen – etwa DBEn gine(0) statt DBEngine.Workspaces(0). Ersteres klappt mit dem Collection-Objekt unter Access ab Version 2003 (VBA 6.4), Letzteres selbst unter Access 2007 nicht. Für beide liefert die folgende Variante mit ein paar Tricks nach: Private mNamen As Collection
801
Kapitel 15 Private Sub Class_Initialize() Set mNamen = New Collection End Sub Private Sub Class_Terminate() Set mNamen = Nothing End Sub 'Default-Property: Property Get Item(Index As Long) As String 'Attribute Item.VB_UserMemId = 0 Item = mNamen(Index) End Property Property Get Count() As Long Count = mNamen.Count End Property Function AddItem(AName As String) As Long mNamen.Add AName AddItem = Count End Function Sub Remove(Index As Long) mNamen.Remove Index End Sub 'Enumeration der Collection nach außen ermöglichen: Property Get NewEnum() As IUnknown 'Attribute Enumerate.VB_UserMemId = -4 Set NewEnum = mNamen.[_NewEnum] End Property Listing 15.28: Klassenmodul einer Auflistungsklasse
Damit die genannten Features unter Access ab Version 2000 funktionieren, sind folgende Schritte notwendig: Exportieren Sie die Klasse in eine .cls-Datei. Löschen Sie die Klasse aus dem VBA-Projekt. Entfernen Sie in der exportierten Datei die Kommentarzeichen vor den beiden fett gedruckten Zeilen. Importieren Sie die Klasse wieder. Die beiden ehemals auskommentierten Zeilen sind nun nicht mehr sichtbar. Es handelt sich dabei um verborgene Eigenschaften, die aber durchaus ihren Zweck erfüllen. Das können Sie mit der folgenden Prozedur nachweisen:
802
Objektorientierte Programmierung Public Sub BeispielCollection() Dim objNamen As New clsNamenTest Dim varName As Variant With objNamen .AddItem "Trowitzsch" .AddItem "André" .AddItem "Minhorst" .AddItem "Addison-Wesley" Debug.Print "Gespeichert sind " & .Count & " Einträge:" For Each varName In objNamen Debug.Print varName Next varName End With Debug.Print "Der erste Eintrag war: " & objNamen(1) Set objNamen = Nothing End Sub Listing 15.29: Test der Standardeigenschaft
benutzerdefinierten
Auflistungsklasse
mit
Enumeration
und
15.8.3 Nachbildung relationaler Beziehungen per Auflistungsklasse Solange Anwendungen einen überschaubaren Rahmen haben und nicht allzu viel VBACode notwendig ist, um alle Aufgaben zu erledigen, benötigen Sie Techniken wie die nachfolgend vorgestellte sicher nicht. Unter Umständen müssen Sie aber innerhalb einer Datenbankanwendung beispielsweise Berechnungen durchführen und Daten ermitteln, die über viele Tabellen verteilt sind. In diesem Fall wäre es sicher angenehm, wenn Sie etwa auf verknüpfte Tabellen und ihre Felder über hierarchisch angeordnete Objekte zugreifen könnten – also genau wie im oben genannten Beispiel mit dem Database-Objekt und den enthaltenen Tabellen, die wiederum aus mehreren Feldern bestehen. Als einfaches Beispiel dienen zwei Tabellen, die Mitarbeiter und ihre Abwesenheitszeiten durch Urlaub, Krankheit oder Fortbildung enthalten. Die beiden Tabellen stehen in einer 1:n-Beziehung zueinander. Zusätzlich sollen zum Mitarbeiterobjekt die Abteilung und zum Abwesenheitsobjekt die Abwesenheitsart aus den jeweiligen Lookup-Tabellen hinzugefügt werden (siehe Abbildung 15.11). Ziel ist es nun, etwa wie in der folgenden Routine auf die in den Tabellen enthaltenen Daten zugreifen zu können. Dort wird ein Objekt namens Personal mit dem Typ cls-
803
Kapitel 15
Personal instanziert, das eine Auflistung namens AlleMitarbeiter enthält. Die Auflistung erlaubt genau wie die oben vorgestellten eingebauten Auflistungen das Durchlaufen der einzelnen Elemente und sogar das Ausgeben der Eigenschaften dieser Elemente wie MitarbeiterID, Vorname oder Nachname. Die Ausgabe dieser Routine soll wie in Abbildung 15.12 aussehen:
Public Function AlleMitarbeiterAusgeben() Dim Personal As clsPersonal Dim Mitarbeiter As clsMitarbeiter Dim i As Integer Set Personal = New clsPersonal For Each Mitarbeiter In Personal.AlleMitarbeiter With Mitarbeiter Debug.Print "MitarbeiterID: " & .MitarbeiterID Debug.Print "Name: " & .Nachname & ", " & .Vorname Debug.Print "================" End With Next Mitarbeiter Set Personal = Nothing End Function Listing 15.30: Zugriff auf eine Mitarbeiterauflistung
Abbildung 15.11: Diese Tabellen sollen durch Objekte abgebildet werden
Für die Realisierung eines Zugriffs per Objekt auf in der Datenbank gespeicherte Daten benötigen Sie zwei Klassen namens clsPersonal und clsMitarbeiter.
804
Objektorientierte Programmierung
Abbildung 15.12: Ausgabe der Routine aus Listing 15.30
Die erste Klasse enthält lediglich die Deklaration des Collection-Objekts zur Aufnahme der Mitarbeiter-Objekte und eine Property Get-Prozedur zum Zusammenstellen und Zurückgeben des Collection-Objekts. Dim mAlleMitarbeiter As Collection Public Property Get AlleMitarbeiter() As Collection 'Wenn die Auflistung noch nicht existiert ... If mAlleMitarbeiter Is Nothing Then 'Auslistung instanzieren Set mAlleMitarbeiter = New Collection Dim db As DAO.Database Dim rst As DAO.Recordset 'Mitarbeiterobjekt instanzieren Dim objMitarbeiter As clsMitarbeiter 'Datenbankobjekte instanzieren, 'um Mitarbeiter in die Objekte zu laden Set db = CurrentDb Set rst = db.OpenRecordset("SELECT MitarbeiterID " _ & "FROM tblMitarbeiter", dbOpenDynaset) 'Für jeden Mitarbeiter ein eigenes Objekt anlegen 'und an die Auflistung anfügen Do While Not rst.EOF 'Neues Mitarbeiterobjekt erstellen Set objMitarbeiter = New clsMitarbeiter
805
Kapitel 15 'Füllen des Objekts mit den Mitarbeitereigenschaften objMitarbeiter.Laden rst!MitarbeiterID rst.MoveNext 'Mitarbeiterobjekt zur Auflistung hinzufügen mAlleMitarbeiter.Add objMitarbeiter Set objMitarbeiter = Nothing Loop Set rst = Nothing Set db = Nothing End If Set AlleMitarbeiter = mAlleMitarbeiter End Property Listing 15.31: Die Klasse clsPersonal liefert eine Collection mit Mitarbeiter-Objekten zurück
Die zweite Klasse, clsMitarbeiter, dient dem Erstellen der einzelnen Mitarbeiter-Objekte. Sie enthält die Property Let- und Propety Set-Methoden für die Eigenschaften MitarbeiterID, Vorname, Nachname und Abteilung sowie die Function-Methode Laden, die den Mitarbeiter mit der angegebenen MitarbeiterID in das Objekt lädt. Dim Dim Dim Dim
mMitarbeiterID As Long mVorname As String mNachname As String mAbteilung As String
Public Property Get MitarbeiterID() As Long MitarbeiterID = mMitarbeiterID End Property Public Property Let MitarbeiterID(lngMitarbeiterID As Long) mMitarbeiterID = lngMitarbeiterID End Property Public Property Get Vorname() As String Vorname = mVorname End Property Public Property Let Vorname(strVorname As String) mVorname = strVorname End Property Public Property Get Nachname() As String Nachname = mNachname End Property
806
Objektorientierte Programmierung Public Property Let Nachname(strNachname As String) mNachname = strNachname End Property Public Property Get Abteilung() As String Abteilung = mAbteilung End Property Public Property Let Abteilung(strAbteilung As String) mAbteilung = strAbteilung End Property Public Function Laden(lngMitarbeiterID As Long) As Boolean Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb 'ID des Mitarbeiter zuweisen mMitarbeiterID = lngMitarbeiterID 'Datensatz mit der angegebenen MitarbeiterID öffnen '(enthält Mitarbeiterdaten und Abteilung) Set rst = db.OpenRecordset("SELECT tblMitarbeiter.*, " _ & "tblAbteilungen.Abteilung FROM tblMitarbeiter " _ & "INNER JOIN tblAbteilungen ON tblMitarbeiter.AbteilungID " _ & "= tblAbteilungen.AbteilungID WHERE MitarbeiterID = " _ & mMitarbeiterID, dbOpenDynaset) 'Falls Datensatz vorhanden, Eigenschaften zuweisen If Not rst.EOF Then mVorname = rst!Vorname mNachname = rst!Nachname mAbteilung = rst!Abteilung Laden = True End If Set rst = Nothing Set db = Nothing End Function Listing 15.32: Code der Klasse clsMitarbeiter
Mit diesen beiden Klassen können Sie wie in Listing 15.30 auf die Auflistung aller Mitarbeiter und die Eigenschaften der einzelnen Listeneinträge zugreifen. Der Aufwand hierfür ist gar nicht so hoch, wenn man vom relativ umfangreichen Code absieht. Dieser
807
Kapitel 15
enthält aber ohnehin fast nur Property-Prozeduren, deren Herstellung keine große Denkleistung erfordert – Sie können ähnliche Klassen mit ein wenig Fleißarbeit leicht selbst nachbauen.
15.8.4 »Echtes« Objekt mit Auflistung Die Auflistung des vorherigen Beispiels wurde durch ein abstraktes Objekt realisiert – es gibt in der Datenbank keine Objekte namens Personal. Es diente lediglich als Basis für die Auflistung AlleMitarbeiter. Im folgenden Beispiel erhält ein echtes Objekt eine Auflistung mit untergeordneten Objekten. Dazu greifen Sie das Mitarbeiter-Objekt aus dem vorherigen Beispiel auf und erweitern es um eine Auflistung der Abwesenheiten, die in der Tabelle tblAbwesenheiten gespeichert sind. Die Klasse clsMitarbeiter ist nur geringfügig zu erweitern: Hier fügen Sie im Modulkopf lediglich die Deklaration des Auflistungsobjekts für die Abwesenheiten und zusätzlich die Property Get-Prozedur Abwesenheiten hinzu. Diese Property lädt beim ersten Aufruf der Collection Abwesenheiten aus einer aufrufenden Routine die Abwesenheitsdaten des aktuellen Mitarbeiters in die einzelnen Elemente der Auflistung. Diese stehen dann bei diesem und den folgenden Aufrufen weiter zur Verfügung, ohne dass Sie sie erneut laden müssen. Dim mAbwesenheiten As Collection Public Property Get Abwesenheiten() As Collection 'Falls die Abwesenheiten-Auflistung noch nicht existiert ... If mAbwesenheiten Is Nothing Then 'Neue Abwesenheiten-Auflistung erstellen Set mAbwesenheiten = New Collection Dim db As DAO.Database Dim rst As DAO.Recordset Dim objAbwesenheit As clsAbwesenheit Set db = CurrentDb 'Datensatzgruppe aller Abwesenheiten zum aktuellen 'Mitarbeiter öffnen Set rst = db.OpenRecordset("SELECT * FROM tblAbwesenheiten " _ & "WHERE MitarbeiterID = " & mMitarbeiterID, dbOpenDynaset) 'Alle Abwesenheiten durchlaufen Do While Not rst.EOF Set objAbwesenheit = New clsAbwesenheit 'Abwesenheit in das Objekt laden objAbwesenheit.Laden rst!AbwesenheitID rst.MoveNext
808
Objektorientierte Programmierung 'Abwesenheit zur Auflistung hinzufügen mAbwesenheiten.Add objAbwesenheit Set objAbwesenheit = Nothing Loop Set rst = Nothing Set db = Nothing End If Set Abwesenheiten = mAbwesenheiten End Property Listing 15.33: Erweiterung der Klasse clsMitarbeiter aus Listing 16.30
Die Klasse clsAbwesenheiten Fehlt nur noch die Klasse, die als Basis für die Abwesenheits-Objekte dient. Diese ist ähnlich wie die Klasse clsMitarbeiter aufgebaut – sie hat ein paar Eigenschaften mit den entsprechenden Property Let- und Property Get-Prozeduren und eine Methode zum Laden der Daten einer Abwesenheit in das Objekt. Private Private Private Private
mAbwesenheitID As Long mAbwesenheitsart As String mStartdatum As Date mEnddatum As Date
Public Property Get AbwesenheitID() As Long AbwesenheitID = mAbwesenheitID End Property Public Property Let AbwesenheitID(lngAbwesenheitID As Long) mAbwesenheitID = lngAbwesenheitID End Property Public Property Get Abwesenheitsart() As String Abwesenheitsart = mAbwesenheitsart End Property Public Property Let Abwesenheitsart(strAbwesenheitsart As String) mAbwesenheitsart = strAbwesenheitsart End Property Public Property Get Startdatum() As Date Startdatum = mStartdatum End Property
809
Kapitel 15 Public Property Let Startdatum(datStartdatum As Date) mStartdatum = datStartdatum End Property Public Property Get Enddatum() As Date Enddatum = mEnddatum End Property Public Property Let Enddatum(datEnddatum As Date) mEnddatum = datEnddatum End Property Public Function Laden(lngAbwesenheitID As Long) As Boolean Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb 'ID der Abwesenheit zuweisen mAbwesenheitID = lngAbwesenheitID 'Datensatz mit der angegebenen AbwesenheitID öffnen '(enthält Mitarbeiterdaten und Abteilung) Set rst = db.OpenRecordset("SELECT tblAbwesenheiten.*, " _ & "tblAbwesenheitsarten.Abwesenheitsart FROM tblAbwesenheiten " _ & "INNER JOIN tblAbwesenheitsarten ON " _ & "tblAbwesenheiten.AbwesenheitsartID = " _ & "tblAbwesenheitsarten.AbwesenheitsartID " _ & "WHERE AbwesenheitID = " & mAbwesenheitID, dbOpenDynaset) 'Falls Datensatz vorhanden, Eigenschaften zuweisen If Not rst.EOF Then mAbwesenheitID = rst!AbwesenheitID mAbwesenheitsart = rst!Abwesenheitsart mStartdatum = rst!Startdatum mEnddatum = rst!Enddatum Laden = True End If Set rst = Nothing Set db = Nothing End Function Listing 15.34: Inhalt des Klassenmoduls clsAbwesenheiten
810
Objektorientierte Programmierung
Was tun mit Mitarbeiter- und Abwesenheits-Objekten? Einige Abschnitte weiter oben haben Sie zunächst eine Routine kennen gelernt, die komfortabel über Objekte und ihre Auflistungen zugreift und dann die notwendigen Klassen clsPersonal und clsMitarbeiter erstellt. Mit der hinzugekommenen Klasse clsAbwesenheiten können Sie nun die Klasse clsMitarbeiter als Objekt mit einer AbwesenheitenCollection verwenden. Das folgende Listing zeigt eine Beispielanwendung, in der zu einem Mitarbeiter alle vorhandenen Abwesenheiten ausgegeben werden. Durch die sorgfältigen Vorbereitungen – dem Erstellen der zugrunde liegenden Klassen – sind hier nur noch wenige Zeilen Code notwendig, um die Informationen von mehreren Tabellen auszugeben. Public Function MitarbeiterUndAbwesenheitenAusgeben(lngMitarbeiterID _ As Long) Dim Mitarbeiter As clsMitarbeiter Dim Abwesenheit As clsAbwesenheit Dim i As Integer Set Mitarbeiter = New clsMitarbeiter If Mitarbeiter.Laden(lngMitarbeiterID) = True Then Debug.Print "Vorname: " & Mitarbeiter.Vorname Debug.Print "Nachname: " & Mitarbeiter.Nachname Debug.Print "Abteilung: " & Mitarbeiter.Abteilung For Each Abwesenheit In Mitarbeiter.Abwesenheiten With Abwesenheit Debug.Print "================" Debug.Print .Abwesenheitsart Debug.Print .Startdatum & " - " & .Enddatum End With Next Abwesenheit End If Set Mitarbeiter = Nothing End Function Listing 15.35: Ausgabe der Mitarbeiter und ihrer Abwesenheiten
15.9 Schnittstellen und Vererbung Weiter oben haben Sie erfahren, dass die öffentlichen Eigenschaften und Methoden die Schnittstelle einer Klasse ausmachen. Die Schnittstelle und ihre Implementierung sind
811
Kapitel 15
in einer einzigen Klasse enthalten. Das ist in den meisten Fällen durchaus ausreichend. Es gibt aber Fälle, in denen das relativ unschöne Effekte haben kann.
15.9.1 Beispiel für den Einsatz der Schnittstellenvererbung Stellen Sie sich einmal vor, Sie müssten regelmäßig Daten aus verschiedenen Quellen in Ihre Datenbank importieren. Zum Steuern des Importvorgangs – also etwa zur Auswahl der Quelldatei und des für den Import zu verwendenden Algorithmus – benutzen Sie ein Formular wie in Abbildung 15.13.
Abbildung 15.13: Steuerung des Imports aus unterschiedlichen Quellen
Hinter der Schaltfläche Importieren verbirgt sich etwa folgender Code: Private Sub cmdImportieren_Click() Select Case Me.ogrImportart Case 1 'Text (.txt) 'Import der Daten aus der Textdatei '[... viele weitere Zeilen Code] MsgBox "Import aus der Textdatei ist erfolgt." Case 2 'XML (.xml) 'Import der Daten aus der XML-Datei '[... viele weitere Zeilen Code] MsgBox "Import aus der XML-Datei ist erfolgt." Case 3 'Access-Tabelle (.accdb) 'Import aus der Access-Tabelle '[... viele weitere Zeilen Code] MsgBox "Import aus der Access-Tabelle ist erfolgt." Case 4 'Excel-Tabelle (.xls) 'Import aus der Excel-Tabelle '[... viele weitere Zeilen Code] MsgBox "Import aus der Excel-Tabelle ist erfolgt." End Select End Sub Listing 15.36: Aufbau des Importvorgangs auf prozedurale Art
812
Objektorientierte Programmierung
Diese Vorgehensweise dürfte durchaus gängig sein. Der erste Schritt zu einer objekt orientierten Variante ist die Erstellung je einer eigenen Klasse für die einzelnen ImportFunktionen. Damit nimmt man schon einmal einige Funktionalität aus der GUI-Schicht der Anwendung – also dem Formular und dessen Klassenmodul – heraus. Die durch die Schaltfläche Importieren ausgelöste Prozedur wertet jetzt nur noch die Benutzereingaben aus und lässt das entsprechende Objekt den Rest erledigen: Private Sub cmdImportieren_Click() Dim strDateiname As String strDateiname = Me!txtDateiname Select Case Me.ogrImportart Case 1 'Text (.txt) Dim objImport_txt As clsImport_txt Set objImport_txt = New clsImport_txt If objImport_txt.Import(strDateiname) = True Then MsgBox "Import aus der Textdatei ist erfolgt." End If Case 2 'XML (.xml) Dim objImport_xml As clsImport_xml Set objImport_xml = New clsImport_xml If objImport_xml.Import(strDateiname) = True Then MsgBox "Import aus der XML-Datei ist erfolgt." End If Case 3 'Access-Tabelle (.accdb) Dim objImport_mdb As clsImport_mdb Set objImport_mdb = New clsImport_mdb If objImport_mdb.Import(strDateiname) = True Then MsgBox "Import aus der Access-Tabelle ist erfolgt." End If Case 4 'Excel-Tabelle (.xls) Dim objImport_xls As clsImport_xls Set objImport_xls = New clsImport_xls If objImport_xls.Import(strDateiname) = True Then MsgBox "Import aus der Excel-Tabelle ist erfolgt." End If End Select End Sub Listing 15.37: Diese Variante des Imports basiert auf der Verwendung unterschiedlicher Klassen
Die Klassenmodule der verwendeten Objekte haben alle eine Methode namens Import. Da je nach Import-Art jeweils ein Objekt verwendet wird, könnte die Import-Funktionalität
813
Kapitel 15
auch in Methoden mit anders lautenden Namen untergebracht werden. Sie werden allerdings gleich sehen, dass die Auswahl gleicher Methodennamen durchaus sinnvoll ist. Nachfolgendes Listing zeigt den Inhalt eines Klassenmoduls am Beispiel clsImport_ mdb: Option Compare Database Option Explicit Public Function Import(strDateiname As String) 'Import aus der Access-Tabelle '[... viele weitere Zeilen Code] Import = True End Function Listing 15.38: Aussehen eines der Import-Klassenmodule
15.9.2 Vereinheitlichen per Schnittstellenvererbung Wenn nun schon alle Klassen die gleiche Methode zur Verfügung stellen, warum sollte man diese nicht auch wie eine Klasse ansteuern, anstatt jede Klasse bei Bedarf einzeln zu deklarieren und zu instanzieren? Genau hier setzt die Schnittstellenvererbung an: Sie deklarieren in einem Klassenmodul alle Elemente der Schnittstelle, die alle betroffenen Klassen gemeinsam haben. In den Klassenmodulen für die unterschiedlichen Importe implementieren Sie die Elemente der Schnittstelle und legen mit einer entsprechenden Anweisung fest, welche Schnittstelle implementiert wird. Im Klassendiagramm sieht das wie in Abbildung 15.14 aus. Die Schnittstelle IImport stellt die öffentliche Methode Import() zur Verfügung, die von den Implementierungsklassen auf verschiedene Art umgesetzt wird. Um die Objekte des Beispiels zu komplettieren, wird auch das Formular zur Auswahl des Import-Algorithmus dargestellt. «interface» IIm port Im port()
clsIm port_txt Im port()
clsIm port_m db Im port()
«benutzt»
clsIm port_xm l Im port()
clsIm port_xls Im port()
Abbildung 15.14: Klassendiagramm zur Veranschaulichung der Schnittstellenvererbung
814
Objektorientierte Programmierung
15.9.3 Realisierung der Schnittstellenvererbung Im Code sind zur Anwendung der Schnittstellenvererbung drei Schritte erforderlich: Erstellen der Schnittstelle in Form eines eigenen Klassenmoduls mit den öffentlichen Eigenschaften und Prozeduren Anpassen oder Erstellen der Klassen, die die Schnittstelle implementieren Anpassen der Klassen, die auf die Schnittstelle zugreifen sollen
Erstellen der Schnittstelle Die Schnittstelle enthält die Deklaration aller Eigenschaften und Methoden, die in allen Implementierungen realisiert werden müssen. Deklaration bedeutet in diesem Fall, dass nur die Rümpfe der jeweiligen Methoden, aber keinesfalls Implementierungsdetails angelegt werden. Die Schnittstelle für obiges Beispiel enthält lediglich die Methode Import(): Option Compare Database Option Explicit Public Function Import(strDateiname As String) End Function Listing 15.39: Klassenmodul mit einer Schnittstelle
Das Klassenmodul enthält keine Merkmale, die es von anderen Klassenmodulen unterscheiden – mit Ausnahme der Tatsache, dass die Methode keine Implementierung enthält, und mit einem zweiten Unterschied, der hier nicht offensichtlich wird: SchnittstellenKlassenmodule erhalten ein I (für »Interface«) statt des für Klassenmodule üblichen cls als Präfix. Das obige Klassenmodul heißt folglich IImport.
Implementieren der Schnittstelle Beim Implementieren bestehender Klassenmodule sind erhebliche Umbauarbeiten angesagt. Eine Anweisung legt die zu implementierende Schnittstelle fest (beispielsweise Implements IImport). Die Eigenschaften und Methoden, die in der Schnittstelle enthalten sind und implementiert werden sollen, müssen als private Elemente deklariert werden. Die Namen der betroffenen Eigenschaften und Methoden erhalten ein Präfix, das aus der Bezeichnung der Schnittstelle und einem Unterstrich besteht (zum Beispiel
815
Kapitel 15
IImport_Import). Dementsprechend sind auch die Variablen zum Speichern der Rückgabewerte von Funktionen anzupassen. Die Implementierung der Schnittstelle IImport für den Import aus Textdateien sieht beispielsweise wie in folgendem Listing aus: Option Compare Database Option Explicit Implements IImport Private Function IImport_Import(strDateiname As String) 'Import der Daten aus der Textdatei '[... viele weitere Zeilen Code] MsgBox "Import aus Textdatei läuft..." IImport_Import = True End Function Listing 15.40: Inhalt des Klassenmoduls clsImport_txt
Anpassen der Klassen, die auf die Schnittstelle zugreifen Auch die auf die unterschiedlichen Implementierungen einer Schnittstelle zugreifenden Routinen muss man ordentlich umbauen. Das nachfolgende Listing zeigt das Aussehen der neuen Version der Routine aus Listing 15.37. Besonderes Augenmerk sollten Sie der Vorgehensweise bei der Deklaration und Instanzierung der Schnittstelle und der ImportObjekte widmen. Die Objektvariable objImport wird zunächst als Interface IImport deklariert. Erst im Select Case-Teil der Routine erfolgt die Instanzierung des Objekts – dort wird je nach gewähltem Import-Algorithmus eine Instanz der entsprechenden Implementierung erzeugt. Ebenfalls zu beachten ist, dass der Zugriff auf das einzige Element der Schnittstelle – das heißt auf die Import-Funktion – nur noch von einer Stelle aus erfolgt. Private Sub cmdImportieren_Click() Dim objImport As IImport Dim strDateiname As String strDateiname = Me!txtDateiname Select Case Me.ogrImportart Case 1 'Text (.txt) Set objImport = New clsImport_txt Case 2 'XML (.xml) Set objImport = New clsImport_xml Case 3 'Access-Tabelle (.accdb) Set objImport = New clsImport_mdb
816
Objektorientierte Programmierung Case 4 'Excel-Tabelle (.xls) Set objImport = New clsImport_xls End Select If objImport.Import(strDateiname) = True Then MsgBox "Import ist erfolgt." End If End Sub Listing 15.41: Zugriff auf unterschiedliche Implementierungen einer Schnittstelle
15.9.4 Was vom Beispiel übrig bleibt … Leider lassen sich mit einem einzigen Beispiel nicht immer alle theoretischen Grundlagen abdecken. Deshalb finden Sie nachfolgend die wichtigsten Regeln bei der Verwendung der Schnittstellenvererbung im Überblick. Die Schnittstelle enthält nur die öffentlich verfügbaren Elemente der zu implementierenden Klassen. Da öffentliche Variablen dem Prinzip der Kapselung widersprechen, handelt es sich dabei also nur um folgende Elemente: Property Let-/Get-/Set-Prozeduren Function-Prozeduren Sub-Prozeduren Die Variablen, auf die die Property Let-/Get-/Set-Prozeduren zugreifen, legt man ausschließlich in den Implementierungen der Schnittstellen an.
Vereinfachung im VBA-Editor Nach dem Anlegen der Implements-Anweisung in der Implementierung stellt der VBAEditor die Schnittstelle und ihre Elemente in den beiden Kombinationsfeldern oben im Code-Fenster zur Verfügung (siehe Abbildung 16.15). Bei Auswahl von Import im rechten Kombinationsfeld wird automatisch die entsprechende Rumpfprozedur IImport_Import im Code-Fenster angelegt – sie braucht also nicht manuell in den Code geschrieben zu werden.
Schnittstelle als Pflichtprogramm Besonders wichtig ist die Umsetzung aller Elemente der Schnittstelle in sämtlichen Implementierungen. Die Anwendung lässt sich sonst gar nicht erst kompilieren – Abbildung 15.16 zeigt die nicht ganz unberechtigte Antwort des Compilers.
817
Kapitel 15
Abbildung 15.15: Auswahl der Elemente der Schnittstelle
Abbildung 15.16: Kein Kompilieren ohne vollständige Schnittstellenimplementierung
Weitere Anwendungsmöglichkeiten der Schnittstellenvererbung Genau wie der Import von Daten ist natürlich auch der Zugriff auf unterschiedliche Backends einer Datenbankanwendung ein sinnvoller Einsatzort für die Schnittstellen vererbung. In diesem Fall würde man allerdings nicht wie im obigen Beispiel ad hoc, sondern beispielsweise einmalig beim Anwendungsstart die Art des Backends und damit die Implementierung der Datenzugriffsschicht festlegen.
818
16 Objektorientierung im Praxiseinsatz Im vorherigen Kapitel haben Sie bereits einige Ansätze für den Einsatz der Objektorientierung mit Access und VBA kennen gelernt. In diesem Kapitel folgen nun weitere Mög lichkeiten. Sie erfahren, wie Sie mehrschichtige Anwen dungen mit Access entwickeln, Standardfunktionen von Formularen wie Öffnen, Schließen, Suche per Kombina tionsfeld und andere in eine eigene Klasse auslagern und wie Sie komfortabel mehrere Instanzen eines Formulars öffnen können, um etwa mehrere Datenblätter zu Artikeln anzuzeigen.
16.1 Standardfunktionen von Formularen auslagern In manchen Fällen macht der Mehraufwand für die Erstel lung mehrschichtiger Anwendungen beziehungsweise des Einsatzes von Objektklassen für Tabellen oder Daten zugriffsklassen definitiv keinen Sinn, weil die geplante An wendung einfach nur ein kleines Tool zur Verwaltung weni ger Daten ist – ohne dass größere Erweiterungswünsche abzusehen sind. Diese Bedingung sollten Sie allerdings genau überprüfen – nach den Erfahrungen des Autors trifft sie nur zu, wenn Sie selbst alleiniger Benutzer der Anwendung sind (alle anderen erkennen in der Regel erst nach Fertigstellung einer Anwendung, welches »Poten zial« noch dahintersteckt …). Es lohnt sich aber allemal, sich mit den praktischen Seiten der objektorientierten Ent
Kapitel 16
wicklung auseinanderzusetzen, denn früher oder später werden auch Sie vielleicht mit einer objektorientierten Programmiersprache arbeiten. Die Beispiele zu diesem Abschnitt finden Sie auf der Buch-CD unter \Kap_16\ObjektorientierteTechniken.accdb. Es handelt sich dabei um die Tabelle tblKontakte, das Formular frmKontakte sowie das Modul clsFormCode.
16.1.1 Formulare zur Datenbearbeitung Die meisten Access-Formulare dienen der Bearbeitung von Daten, die direkt an eine Tabelle gebunden sind. Diese Formulare haben immer den gleichen Aufbau und die gleichen Funktionen – nur die angezeigten Daten unterscheiden sich. Ein Formular zum Bearbeiten von Daten hat in der Regel einige der folgenden Eigenschaften: Direkte Bindung zur Tabelle mit den angezeigten Daten Je ein Steuerelement für jedes Feld der Tabelle Möglichkeit zur schnellen Auswahl von Datensätzen, etwa ein Kombinationsfeld zur Eingabe oder Auswahl des Nachnamens eines Kontaktes Schaltfläche zum Schließen des Formulars Schaltfläche zum Abbrechen und damit zum Verwerfen der am aktuellen Datensatz vorgenommenen Änderungen Schaltfläche zum Hinzufügen eines neuen Datensatzes beziehungsweise zum Springen zu einem neuen Datensatz Schaltfläche zum Löschen des aktuellen Datensatzes Validierung der gebundenen Steuerelemente Mit dem Anlegen der hier aufgelisteten Elemente und dem notwendigen Code sind Sie je Formular schon einige Minuten beschäftigt – besonders das Anlegen der Validierungs funktionen und ihr Test kosten Zeit. Das Formular aus Abbildung 16.1 besitzt die meisten der soeben aufgelisteten Eigen schaften eines Formulars zur Datenerfassung und –bearbeitung. Der einzige Unterschied ist, dass dieses Formular keine Funktionalität beinhaltet – sowohl das Kombinationsfeld als auch die Schaltflächen lösen keine Ereignisse aus. Das soll auch weitgehend so bleiben, denn Sie lernen nun eine Möglichkeit kennen, die Anwendungslogik hinter Formularen mit gleichem oder ähnlichem Aufbau wie diesem nur einmal zu erstellen und beliebig oft wieder zu verwenden.
820
Objektorientierung im Praxiseinsatz
Abbildung 16.1: Dieses Formular hat keine eigenen Funktionen
16.1.2 Codeauslagerung am Beispiel der OK-Schaltfläche Die OK-Schaltfläche ist ein einfaches Beispiel für die nachfolgend vorgestellte Technik. Sie enthält in der Regel nur eine einzige Anweisung. Das folgende Listing zeigt, wie die Ereignisprozedur aussieht, die beim Klicken auf diese Schaltfläche ausgelöst wird: Private Sub cmdOK_Click() DoCmd.Close acForm, Me.Name End Sub Listing 16.1: Prozedur zum Schließen eines Formulars
Es kostet zwar nicht viel Mühe, diese Ereignisprozedur zu erstellen und die einzige Zei le hinzuzufügen. Aber wenn Sie in einer Anwendung einige dieser Prozeduren anlegen, kostet das schon Zeit – und diese Prozedur ist nicht die einzige, die Sie benötigen. Versuchen Sie also, einen einfacheren Weg zu finden. Ein erster Ansatz wäre, eine öffentliche Prozedur in einem Standardmodul unterzubringen und diese von der Ereignis eigenschaft cmdOK_Click aus aufzurufen. Sie könnte dann auch von den entsprechenden Prozeduren anderer Formulare aus aufgerufen werden. Gewonnen haben Sie damit allerdings nichts – Sie ersetzen einfach nur die eigentliche Anweisung durch eine, die eine weitere Prozedur aufruft. Das mag ein bisschen Zeit sparen, wenn die eigentliche Prozedur mehrere Zeilen enthält, bringt aber keine wirklichen Vorteile. Gegebenenfalls müssen Sie den Prozeduraufruf auch noch parametrisieren – etwa wenn es um den Code für das Kombinationsfeld geht, der je nach Datensatzquelle des Formulars anders aussieht. Allein die Idee, die Funktion der Prozedur zentral verfügbar zu machen, fließt in die folgenden Schritte ein.
821
Kapitel 16
In Kapitel 15, »Objektorientierte Programmierung«, haben Sie erfahren, wie Sie Er eignisse abfangen und eigenen Code dafür ausführen lassen können. Diese Technik machen Sie sich nun zu Nutze. Ziel ist also – am konkreten Beispiel der OK-Schalt fläche betrachtet – das Ereignis Beim Klicken der Schaltfläche abzufangen und bei dessen Aufruf eigenen Code auszuführen, der an völlig anderer Stelle steht. Um die Ereignisse der Schaltfläche abzufangen, benötigen Sie eine Objektvariable, die Sie mit folgender Zeile deklarieren: Private WithEvents mOkButton As CommandButton
Diese Anweisung fügen Sie in ein neues Klassenmodul namens clsFormCode ein. Damit das hier deklarierte Objekt die Ereignisse der Schaltfläche abfangen kann, muss es zunächst einmal existieren – und dazu ist eine Instanz der Klasse erforderlich, in der es deklariert wird. Wo erwecken Sie das Klassenmodul nun zum Leben? Natürlich in dem Formular, dessen Ereignisse es abfangen soll. Und dabei übergeben Sie möglichst auch noch je eine Objektvariable, die auf das Formular und die betroffene Schaltfläche zeigt. Der güns tigste Zeitpunkt für diese Aktion ist das Öffnen des Formulars. Im folgenden Listing finden Sie das komplette Klassenmodul des Formulars frmKontakte. Die Objektvariable objFormCode wird modulweit deklariert, da sie für die komplette Lebensdauer dieser Formularinstanz benötigt wird. Die Ereignisprozedur Form_Open wird beim Öffnen des Formulars ausgelöst. Die bisher einzige Prozedur weist der Objektvariablen objFormCode eine neue Instanz der Klasse clsFormCode zu. Option Compare Database Option Explicit 'Deklarieren der Objektvariablen für die Codeklasse Dim objFormCode As clsFormCode Private Sub Form_Open(Cancel As Integer) 'Instanzieren des Codeklasse-Objekts Set objFormCode = New clsFormCode End Sub Listing 16.2: Instanzieren und vorbereiten der Codeklasse
Das Formular erstellt nun beim Öffnen ein Objekt, das auf Ereignisse eines Command Button-Objekts lauschen soll. Damit das auch funktioniert, müssen Sie dem Objekt einen Verweis auf diese Schaltfläche übergeben – den die Klasse wiederum entgegennehmen muss. Daher fügen Sie dem Klassenmodul clsFormCode eine Property Set-Prozedur hinzu, um den schreibenden Zugriff auf die bereits deklarierte Objektvariable mOkButton zu erlauben:
822
Objektorientierung im Praxiseinsatz Public Property Set OkButton(cmb As CommandButton) Set mOkButton = cmb End Property Listing 16.3: Property Set-Prozedur für die Membervariable mOKButton
Im gleichen Zuge erweitern Sie die Ereignisprozedur Form_Open des Formulars. Die neue Anweisung erzeugt einen Verweis auf die Schaltfläche cmdOK. Private Sub Form_Open(Cancel As Integer) 'Instanzieren des Codeklasse-Objekts Set objFormCode = New clsFormCode 'Zuweisen der OK-Schaltfläche Set objFormCode.OkButton = Me!cmdOK End Sub Listing 16.4: Zuweisen der OK-Schaltfläche an die entsprechende Eigenschaft der Klasse clsFormCode
In der Klasse clsFormCode fehlt nun noch die Prozedur, die beim Abfangen des Beim KlickenEreignisses der Schaltfläche ausgeführt werden soll. Um schnell den Prozedurrumpf anzulegen, wählen Sie aus dem linken Kombinationsfeld des Codefensters den Eintrag OKButton aus, woraufhin der Prozedurrumpf automatisch erscheint. Anschließend brauchen Sie nur noch eine Zeile zum Testen hinzuzufügen – etwa eine MsgBox-Anweisung: Private Sub mOKButton_Click() MsgBox "Ereignis abgefangen" End Sub Listing 16.5: Prozedur zum Abfangen des Click-Ereignisses
Wenn Sie das Formular nun öffnen und auf die OK-Schaltfläche klicken, sollte eigentlich das Meldungsfenster erscheinen – es lässt sich aber nicht blicken! Bei genauer Betrach tung des Eigenschaftsfensters der Schaltfläche fällt auf, dass das auch gar nicht funktionieren kann, denn die Schaltfläche löst gar kein Ereignis aus (siehe Abbildung 16.2). Wählen Sie also für die Eigenschaft Beim Klicken den Eintrag [Ereignisprozedur] aus, öffnen Sie das Formular erneut und klicken Sie noch einmal auf die OK-Schaltfläche – und das Meldungsfenster erscheint. Zum Glück lässt sich auch dieser Vorgang automatisieren. Dazu passen Sie erneut das Klassenmodul clsFormCode an, indem Sie der Property Set-Prozedur OkButton eine Zeile wie folgt hinzufügen:
823
Kapitel 16
Abbildung 16.2: Die OK-Schaltfläche löst offensichtlich kein Ereignis aus
Public Property Set OkButton(cmb As CommandButton) 'Objektvariable auf Ok-Schaltfläche einstellen Set mOkButton = cmb 'Beim Klicken-Ereigniseigenschaft hinzufügen mOkButton.OnClick = "[Event Procedure]" End Property Listing 16.6: Anpassen der Property Set-Prozedur OKButton
Die Beim Klicken-Eigenschaft der Schaltfläche im Formular können Sie wieder leeren, da sie später ohnehin automatisch eingestellt wird. Fügen Sie nun die eigentliche Funktion hinzu – schließlich soll die Schaltfläche das Formular schließen und kein Meldungsfens ter anzeigen. Dazu sind insgesamt vier Schritte erforderlich: Erstellen einer Objektvariablen namens mForm für das betroffene Formular im Klassenmodul clsFormCode Anlegen einer Property Set-Prozedur zum Setzen der Objektvariablen mForm (ebenfalls im Klassenmodul clsFormCode) Setzen der Objektvariablen auf das aktuelle Formular (vom Beim Öffnen-Ereignis des Formulars aus) Anpassen der Ereignisprozedur im Klassenmodul clsFormCode
824
Objektorientierung im Praxiseinsatz
Das Klassenmodul clsFormCode sieht anschließend wie folgt aus (geänderte Zeilen fett gedruckt): Option Compare Database Option Explicit Private WithEvents mOkButton As CommandButton Private WithEvents mForm As Form Public Property Set OkButton(cmb As CommandButton) 'Objektvariable auf Ok-Schaltfläche einstellen Set mOkButton = cmb 'Beim Klicken-Ereigniseigenschaft hinzufügen mOkButton.OnClick = "[Event Procedure]" End Property Private Sub mOKButton_Click() 'Schließen des Formulars DoCmd.Close acForm, mForm.Name End Sub Public Property Set ThisForm(frm As Form) 'mForm auf das mit frm referenzierte Formular setzen Set mForm = frm End Property Listing 16.7: Diese Version des Klassenmoduls fängt das Beim Schließen-Ereignis des instanzierenden Formulars ab und führt den entsprechenden Code aus
Das Formular-Klassenmodul hat nun diesen Stand: Option Compare Database Option Explicit 'Deklarieren der Objektvariablen für die Codeklasse Dim objFormCode As clsFormCode Private Sub Form_Open(Cancel As Integer) 'Instanzieren des Codeklasse-Objekts Set objFormCode = New clsFormCode 'Zuweisen des aktuellen Formulars Set objFormCode.ThisForm = Me 'Zuweisen der OK-Schaltfläche
825
Kapitel 16 Set objFormCode.OkButton = cmdOK End Sub Listing 16.8: Das Beim Öffnen-Ereignis instanziert die Klasse clsFormCode und stellt deren Eigenschaften so ein, dass es die Ausführung des Beim Klicken-Ereignisses der OK-Schaltfläche übernimmt
Lohnt sich der Aufwand? Nachdem nun schon für die Erläuterung des Auslagerns einer einzigen Prozedur einige Seiten notwendig waren und die Menge des Codes sich vervielfacht hat, fragen Sie sich vermutlich, ob sich der Aufwand lohnt. Die Antwort lautet ganz klar: Ja! Denn jetzt haben Sie die Vorgehensweise einmal erfasst. Das Erstellen der übrigen Ereignisprozeduren ist reine Fleißarbeit und das Vorbereiten weiterer Formulare für die Verwendung der Klasse clsFormCode nur eine Sache weniger Zeilen.
16.1.3 Abbrechen der Bearbeitung auslagern Natürlich soll nicht nur die Funktion der OK-Schaltfläche in eine eigene Klasse ausgelagert werden, sondern auch die der übrigen Ereignisprozeduren. Die AbbrechenSchaltfläche führt in der Regel zum Durchführen der Undo-Methode des Formulars und zum anschließenden Schließen. Um diese Anweisungen in der Klasse clsFormCode zu implementieren, fügen Sie im Deklarationsbereich zunächst folgende Zeile hinzu: Private WithEvents mCancelButton As CommandButton
Außerdem erstellen Sie noch eine Property Set-Prozedur, damit das instanzierende Formular eine Referenz auf die Abbrechen-Schaltfläche übergeben kann, und legen die Prozedur mit der eigentlichen Funktionalität an: Public Property Set CancelButton(cmb As CommandButton) Set mCancelButton = cmb mCancelButton.OnClick = "[Event Procedure]" End Property Private Sub mCancelButton_Click() mForm.Undo DoCmd.Close acForm, mForm.Name End Sub Listing 16.9: Prozeduren für die ausgelagerte Funktion der Abbrechen-Schaltfläche
826
Objektorientierung im Praxiseinsatz
16.1.4 Löschen von Datensätzen auslagern Auch für die Löschen-Schaltfläche legen Sie zunächst eine private Objektvariable an: Private WithEvents mDeleteButton As CommandButton
Der Löschvorgang soll außerdem eine benutzerdefinierte Rückfrage verwenden können. Diese wird in folgender Variablen gespeichert: Private mDeleteMessage As String
Die weitere Vorgehensweise ist ein wenig aufwändiger als bei den vorherigen Schalt flächen. Bereits die Property Set-Prozedur legt im Formular neben der Ereigniseigenschaft für die Schaltfläche eine weitere Ereigniseigenschaft für das Formular selbst an: Public Property Set DeleteButton(cmd As CommandButton) 'Referenz auf die Löschen-Schaltfläche setzen Set mDeleteButton = cmd 'Ereigniseigenschaft für die Schaltfläche anlegen mDeleteButton.OnClick = "[Event Procedure]" 'Ereigniseigenschaft "Beim Löschen" für das Formular anlegen mForm.OnDelete = "[Event Procedure]" End Property Listing 16.10: Das Löschen eines Datensatzes erfordert zwei Ereigniseigenschaften
Natürlich ist auch für die Variable mDeleteMessage für die je nach Formular individuelle Rückfrage eine Property Let-Prozedur zum Einstellen der Eigenschaft vorzusehen: Public Property Let DeleteMessage(strDeleteMessage As String) mDeleteMessage = strDeleteMessage End Property Listing 16.11: Property Let-Eigenschaft für die Lösch-Rückfrage
Die erste Ereigniseigenschaft wird beim Klicken auf die Löschen-Schaltfläche ausgelöst. Die entsprechende Ereignisprozedur sieht folgendermaßen aus: Private Sub mDeleteButton_Click() On Error GoTo mDeleteButton_Click_Err With DoCmd 'Systemmeldungen ausschalten .SetWarnings False
827
Kapitel 16 'Aktuellen Datensatz löschen .RunCommand acCmdDeleteRecord 'Systemmeldungen wieder einschalten .SetWarnings True End With mDeleteButton_Click_Exit: Exit Sub mDeleteButton_Click_Err: If Err.Number = 2501 Then 'Dieser Fehler tritt beim Abbrechen eines durch die 'Docmd.RunCommand accmdDeleteRecord ausgelösten Löschvorgangs 'auf. Er soll nicht behandelt werden. Else MsgBox "Fehler: " & Err.Description & vbCrLf & "Fehlernummer: " _ & Err.Number, vbOKOnly + vbCritical, "Fehler" End If Resume mDeleteButton_Click_Exit End Sub Listing 16.12: Ereignisprozedur beim Löschen eines Datensatzes per Schaltfläche
Diese zweite Ereigniseigenschaft wird immer beim Löschen eines Datensatzes ausgelöst, etwa durch Betätigen der Entfernen-Taste. Sie kann beispielsweise Code für die Anzeige einer Rückfrage vor dem Löschen in Form eines Meldungsfensters enthalten. Private Sub mForm_Delete(Cancel As Integer) Dim strDeleteMessage As String 'Prüfen, ob individuelle Rückfrage vorhanden ist If Len(mDeleteMessage)> 0 Then 'Falls ja, diese verwenden... strDeleteMessage = mDeleteMessage Else '... oder die Standardmeldung anzeigen. strDeleteMessage = "Soll der aktuelle Datensatz gelöscht werden?" End If 'Rückfrage, ob der Datensatz wirklich gelöscht werden soll If MsgBox(strDeleteMessage, vbYesNo + vbExclamation + _ vbDefaultButton1, _ "Datensatz löschen") = vbCancel Then 'Falls nein, Abbruch einleiten
828
Objektorientierung im Praxiseinsatz Cancel = True End If End Sub Listing 16.13: Diese Prozedur wird bei allen Löschvorgängen aufgerufen
In der Prozedur aus Listing 16.13 kommt nun auch die Variable mDeleteMessage zum Zuge. Sie kann genau wie die anderen Elemente über die entsprechende Property Set-Prozedur von der Beim Öffnen-Prozedur des Formulars eingestellt werden. Zum Einstellen der beiden neuen Member der Klasse clsFormCode fügen Sie in der Beim Öffnen-Prozedur die folgenden beiden Codezeilen ein: Set objFormCode.DeleteButton = Me.cmdLoeschen objFormCode.DeleteMessage = "Möchten Sie diesen Kontakt löschen?"
16.1.5 Hinzufügen von Datensätzen auslagern Mit der Neu-Schaltfläche springen Sie auf einen neuen, leeren Datensatz. Damit die Klasse clsFormCode diese Operation übernimmt, legen Sie dort die folgenden Zeilen an: Private WithEvents mAddButton As CommandButton Public Property Set AddButton(cmd As CommandButton) Set mAddButton = cmd mAddButton.OnClick = "[Event Procedure]" End Property Private Sub mAddButton_Click() mForm.Recordset.AddNew End Sub Listing 16.14: Code zum Abfangen der Beim Klicken-Prozedur der Neu-Schaltfläche
Schließlich benötigen Sie noch eine Zeile in der Beim Öffnen-Prozedur des Formulars, damit die Schaltfläche cmdNeu auch im objFormCode-Objekt bekannt gemacht wird: Set objFormCode.AddButton = Me.cmdNeu
16.1.6 Einstellen des Kombinationsfeldes für die Schnellauswahl Das Kombinationsfeld im oberen Bereich des Formulars dient der Schnellauswahl von Kontakten nach dem Namen. Das Formular sollte beim Öffnen des Formulars initialisiert und mit den entsprechenden Daten gefüllt werden. Außerdem muss man den Inhalt bei jeder Änderung des Datenbestandes aktualisieren, also nach dem Bearbeiten,
829
Kapitel 16
Löschen oder Hinzufügen eines Datensatzes. Dazu gehört auch das Aktualisieren des im Kombinationsfeld angezeigten Eintrags beim Blättern in den Datensätzen. Sie benötigen also die folgenden Elemente: Ereignisprozeduren des Formulars, die durch Änderungen am Datenbestand ausgelöst werden: − Löschen eines Datensatzes: Nach Löschbestätigung (AfterDelConfirm) − Anlegen eines neuen Datensatzes: Nach Eingabe (AfterInsert) − Bearbeiten des aktuellen Datensatzes: Nach Aktualisierung (AfterUpdate) Ereignisprozedur des Formulars, die beim Wechseln des Datensatzes ausgelöst wird: Beim Anzeigen (Current) Ereignisprozedur des Kombinationsfeldes, das bei Auswahl eines neuen Eintrags ausgelöst wird: Nach Aktualisierung (AfterUpdate) Für den Objektverweis auf das Formular haben Sie bereits eine Variable angelegt; fehlt also noch eine für das Kombinationsfeld. Fügen Sie diese Zeile im Kopf der Klasse clsFormCode hinzu: Private WithEvents mSearchComboBox As ComboBox
Außerdem benötigen Sie noch eine Eigenschaft zum Übergeben der Bezeichnung des Primärschlüsselfeldes der Datensatzquelle des Formulars sowie eine weitere Eigenschaft, um anzugeben, ob das Primärschlüsselfeld den Datentyp String oder einen anderen Datentyp hat: Private mPrimaryKey As String Private mPrimaryKeyString As Boolean
Zum Setzen der letzten beiden Eigenschaften verwenden Sie die folgenden Property LetProzeduren: Public Property Let PrimaryKey(strPrimaryKey As String) mPrimaryKey = strPrimaryKey End Property Public Property Let PrimaryKeyString(bolPrimaryKeyString As Boolean) mPrimaryKeyString = bolPrimaryKeyString End Property Listing 16.15: Property Let-Prozeduren für die Verwendung des Kombinationsfeldes
Die Property Set-Prozedur zum Referenzieren des Kombinationsfeldes ist etwas umfangreicher:
830
Objektorientierung im Praxiseinsatz Public Property Set SearchComboBox(cbo As ComboBox) 'Referenz auf das Kombinationsfeld setzen Set mSearchComboBox = cbo 'Ereigniseigenschaft Nach Aktualisieren für das 'Kombinationsfeld anlegen mSearchComboBox.AfterUpdate = "[Event Procedure]" 'Ereigniseigenschaften für die Formular-Ereignisse '"Beim Anzeigen", "Nach Eingabe" und "Nach Löschbestätigung" anlegen mForm.AfterInsert = "[Event Procedure]" mForm.AfterDelConfirm = "[Event Procedure]" mForm.AfterUpdate = "[Event Procedure]" 'Ereigniseigenschaft für das Formular-Ereignis "Beim Anzeigen" anlegen mForm.OnCurrent = "[Event Procedure]" End Property Listing 16.16: Property Set-Prozedur zum Referenzieren des Kombinationsfeldes
16.1.7 Aktualisieren des Kombinationsfeldes Das Aktualisieren des Kombinationsfeldes besteht aus zwei Aktionen: Die erste tritt bei Änderungen am Datenbestand auf und aktualisiert die Datensatzherkunft des Kombinationsfeldes. Die zweite sorgt dafür, dass das Kombinationsfeld immer den Datensatz anzeigt, den auch das Formular darstellt. Die Datensatzherkunft des Kombinationsfeldes soll in mehreren Fällen aktualisiert werden – beim Einfügen, Ändern und Löschen von Datensätzen. Für die drei Ereignisse gibt es auch drei Ereignisprozeduren. Alle drei enthalten lediglich eine Anweisung, die immer die gleiche Prozedur aufruft (auf diese Weise brauchen Sie Änderungen nur an einer Stelle durchzuführen): Private Sub mForm_AfterDelConfirm(Status As Integer) RequeryComboBox End Sub Private Sub mForm_AfterInsert() RequeryComboBox End Sub Private Sub mForm_AfterUpdate() RequeryComboBox End Sub Listing 16.17: Ereignisprozeduren zum Löschen, Einfügen und Ändern von Daten
831
Kapitel 16
Die von den drei Routinen aufgerufene Prozedur enthält ebenfalls nur eine Anweisung. Diese führt die Requery-Methode des Kombinationsfeldes aus. Private Sub RequeryComboBox() 'Kombinationsfeld aktualisieren mSearchComboBox.Requery End Sub Listing 16.18: Aktualisieren der Datensatzherkunft des Kombinationsfeldes
Der zweite Teil der Aktualisierung stellt im Kombinationsfeld den Datensatz ein, der dem im Formular angezeigten Datensatz entspricht. Diese Aktion soll bei jedem Daten satzwechsel ausgelöst werden, was ein typischer Einsatzfall für die Ereigniseigenschaft Beim Anzeigen des Formulars ist. Die entsprechende Ereignisprozedur hat folgendes Aussehen: Private Sub mForm_Current() 'Wenn ein Kombinationsfeld referenziert wurde If Not (mSearchComboBox Is Nothing) Then 'Kombinationsfeld aktualisieren UpdateCombobox End If End Sub Listing 16.19: Beim Wechsel des im Formular angezeigten Datensatzes wird das SchnellsucheKombinationsfeld ebenfalls aktualisiert
Die Ereignisprozedur aus Listing 16.19 prüft vor dem Aufrufen der Prozedur Update ComboBox noch, ob überhaupt ein Schnellsuche-Kombinationsfeld referenziert ist, und unterlässt gegebenenfalls die Aktualisierung. Die Prozedur UpdateComboBox prüft, ob das Formular überhaupt einen Datensatz anzeigt, und ermittelt in diesem Fall den Wert des Primärschlüsselfeldes des aktuellen Datensatzes. Anderenfalls – entweder weil kein oder ein noch nicht gespeicherter Datensatz angezeigt wird – leert die Prozedur das Kombinationsfeld. Private Sub UpdateCombobox() Dim rst As DAO.Recordset 'Kopie des Formular-Recordset erzeugen Set rst = mForm.RecordsetClone 'Wenn kein Datensatz markiert oder Datensatz neu ist If rst.NoMatch Or mForm.NewRecord Then 'Kombinationsfeld leeren mSearchComboBox.Value = 0 Else
832
Objektorientierung im Praxiseinsatz 'sonst Recordset-Kopie auf den gleichen Datensatz wie im 'Formular einstellen rst.Bookmark = mForm.Bookmark 'Kombinationsfeld auf den Primärindex dieses Datensatzes setzen mSearchComboBox.Value = rst.Fields(mPrimaryKey) End If Set rst = Nothing End Sub Listing 16.20: Aktualisieren des Kombinationsfeldes nach Änderungen im Datenbestand
16.1.8 Anzeige des im Kombinationsfeld ausgewählten Datensatzes Andersherum soll es natürlich möglich sein, mit dem Kombinationsfeld einen Datensatz auszuwählen, der anschließend im Formular angezeigt wird. Das durch das Kombina tionsfeld ausgelöste Ereignis heißt Nach Aktualisierung; der auszuführende Code wird in der Ereignisprozedur mSearchComboBox_AfterUpdate untergebracht. Die Prozedur erstellt eine Kopie der Datensatzgruppe des Formulars und verweist mit einer Objektvariablen vom Typ Recordset darauf. Mit der bereits weiter oben vorgestellten Membervariablen mPrimaryKeyString überprüft die Routine, ob das Primärschlüsselfeld den Datentyp String hat, und setzt den Vergleichsausdruck des nachfolgend zusammengesetzten Kriteriumsausdrucks gegebenenfalls in Hochkommata. Der erste Teil des Kriteriums ist übrigens der in der Membervariablen mPrimaryKey gespeicherte Wert – dieser entspricht dem Primärschlüsselfeld des Formulars. Mit diesem Kriteriumsausdruck ausgerüstet sucht die Routine in der Kopie des Formular-Recordsets nach dem entsprechenden Datensatz und stellt bei Erfolg das Originalrecordset und damit den im Formular angezeigten Datensatz auf den gesuchten Datensatz ein. Private Sub mSearchComboBox_AfterUpdate() Dim strCriteria As String 'Wenn Primärschlüsselfeld den Typ "String" hat, dann ... If mPrimaryKeyString = True Then 'Vergleichswert in Hochkomma einschließen strCriteria = mPrimaryKey & " = '" & mSearchComboBox & "'" Else '... sonst ohne Hochkomma angeben strCriteria = mPrimaryKey & " = " & mSearchComboBox End If 'Datensatz mit dem im Kombinationsfeld angegebenen
833
Kapitel 16 'Primärschlüsselwert suchen mForm.Recordset.FindFirst strCriteria End Sub Listing 16.21: Anzeigen des im Kombinationsfeld ausgewählten Datensatzes
16.1.9 Weitere Möglichkeiten Damit wären alle sichtbaren Funktionen des Formulars in die Klasse clsFormCode ausgelagert. Eine Fleißaufgabe wird allerdings noch nicht abgedeckt – das Validieren von Daten. Auch dies lässt sich aber mit der hier vorgestellten Technik realisieren.
16.2 Mehrere Formularinstanzen anzeigen Access weigert sich beharrlich, durch einfaches Öffnen mehrere Instanzen eines Formulars anzuzeigen. Dabei wäre dies aber gerade zur gleichzeitigen Anzeige von Detailansichten von Artikeln, Mitarbeitern und ähnlichen Objekten sehr interessant – zwar könnte man diese auch einfach in Tabellenform untereinander anzeigen, aber bei Objekten mit vielen Eigenschaften beziehungsweise Feldern scrollt man mehr hin und her als man Daten vergleichen kann. In den folgenden Abschnitten erfahren Sie, wie Sie mehrere Instanzen eines Formulars gleichzeitig öffnen und diese auch noch vernünftig verwalten.
16.2.1 Beispielformulare Als Beispielformulare dienen die Formulare frmKontaktuebersicht und frmKontaktdetails. Das Formular frmKontaktuebersicht enthält ein Listenfeld namens lstKontakte sowie drei Schaltflächen zum Öffnen und Schließen des aktuell markierten Kontakts sowie zum Schließen aller offenen Instanzen des Formulars frmKontaktdetails (siehe Abbildung 16.3). Beispiel auf CD: Die Tabelle tblKontakte und die Formulare frmKontaktuebersicht und frmKontaktdetails finden Sie auf der Buch-CD unter Kap_16/ObjektorientierteTechniken. accdb. Das Listenfeld hat als Datensatzherkunft folgenden SQL-Ausdruck: SELECT tblKontakte.KontaktID, [Nachname] & ", " & [Vorname] AS Kontakt FROM tblKontakte;
Stellen Sie außerdem die Eigenschaften Spaltenanzahl und Spaltenbreiten auf die Werte 2 und 0cm ein, damit das erste Feld der Datensatzherkunft nicht angezeigt wird, aber als
834
Objektorientierung im Praxiseinsatz
gebundene Spalte verwendet werden kann. Für die Anzeige der Details zu jedem Kon takt ist das Formular frmKontaktdetails verantwortlich. Das Formular ist relativ einfach aufgebaut. Es verwendet die Tabelle tblKontakte als Datensatzquelle. Sämtliche Felder der Tabelle werden im Formular angezeigt (siehe Abbildung 16.4).
Abbildung 16.3: Formular zum Öffnen eines oder mehrerer Detailformulare
Abbildung 16.4: Dieses Formular soll mehrfach instanziert werden können
16.2.2 Erzeugen einer neuen Instanz Der übliche Weg zum Öffnen eines Formulars per DoCmd.OpenForm-Methode funktioniert hier nicht. Das können Sie ganz einfach im Testfenster nachvollziehen: Geben Sie dort zunächst einmal die folgende Anweisung ein: DoCmd.OpenForm "frmKontaktdetails"
835
Kapitel 16
Die Anweisung öffnet das angegebene Formular wie gewünscht. Wenn Sie die gleiche Anweisung nun ein zweites Mal ausführen, erscheint kein weiteres Formular, sondern es wird bestenfalls der Fokus auf das bestehende Formular gesetzt. DoCmd.OpenForm öffnet lediglich die Standardinstanz eines Formulars. Das Öffnen mehrerer Instanzen eines Formulars setzt Folgendes voraus: Das zu öffnende Formular hat ein Klassenmodul. Jede Instanz wird durch eine eigene Objektvariable referenziert. Schauen Sie sich folgendes Beispiel an. Es beschreibt, wie Sie mit der Öffnen-Schaltfläche des Formulars frmKontaktuebersicht eine neue Instanz des Formulars frmKontaktdetails mit dem aktuell in der Liste ausgewählten Kontakt erzeugen. Das Formular frmKontaktdetails hat bisher noch kein Modul, da Sie noch keine Ereignis prozedur angelegt oder anderweitig ein Modul erstellt haben. Das ist auch nicht notwendig; stellen Sie einfach die Eigenschaft Enthält Modul in der Entwurfsansicht des Formulars auf den Wert Ja ein. Der folgende Code zeigt die Vorgehensweise für eine einzelne Instanz des Formulars. Er enthält die Deklaration einer Objektvariablen, die später auf die neue Instanz verweisen wird, und eine Ereignisprozedur, die beim Klicken auf die Schaltfläche zur Anzeige der Details ausgelöst wird. Die Deklaration der Objektvariablen frm erfolgt modulweit und nicht innerhalb der Prozedur cmdDetails_Click. Fände die Deklaration in der Prozedur statt, würde die Vari able mit Beenden der Prozedur gelöscht. Durch die Deklaration als Modulvariable bleibt die neue Instanz des Formulars geöffnet, bis Sie das aufrufende Formular schließen. Die Prozedur selbst erstellt die Instanz des Formulars und stellt die Eigenschaften Filter und FilterOn so ein, dass der im Listenfeld des aufrufenden Formulars ausgewählte Kontakt angezeigt wird. Option Compare Database Option Explicit 'Objektvariable deklarieren Dim frm As Form_frmKontaktdetails Private Sub cmdDetails_Click() 'Instanz von frmKontaktdetails erstellen und 'der Objektvariablen zuweisen Set frm = New Form_frmKontaktdetails 'Filter auf den gewünschten Kontakt setzen frm.Filter = "KontaktID = " & Nz(Me!lstKontakte)
836
Objektorientierung im Praxiseinsatz frm.FilterOn = True 'Formular sichtbar machen frm.Visible = True End Sub Listing 16.22: Code zum Öffnen einer Formularinstanz
16.2.3 Öffnen mehrerer Instanzen eines Formulars Das eigentliche Ziel haben Sie mit der oben gezeigten Vorgehensweise noch nicht erreicht, aber auch nicht aus den Augen verloren: Der Ablauf beim Instanzieren ist auch beim Erzeugen mehrerer Instanzen des Formulars gleich; die notwendige Erweiterung besteht darin, die einzelnen Instanzen durch die entsprechende Anzahl von Objektvariablen zu referenzieren und diese zu verwalten.
16.2.4 Formularinstanz-Sammlung Das Zauberwort für die Verwaltung mehrerer Instanzen von Objekten der gleichen Klasse heißt Collection. Eine VBA-Collection kann Objekte beliebigen Typs aufnehmen – also auch verschiedene Instanzen eines Formulars. Statt der einsamen Objektvariablen frm zum Speichern einer einzigen Instanz, verwenden Sie nun eine Collection, die Sie folgendermaßen deklarieren: Dim colForms As Collection
Bevor Sie Formular-Objekte in dieser Collection speichern können, müssen Sie diese zunächst instanzieren. Das ginge theoretisch, indem Sie der obigen Deklaration einfach das Schlüsselwort New hinzufügen: Dim colForms As New Collection
Programmiertechnisch sauberer ist allerdings die Aufteilung der Deklaration und der Instanzierung. Bei global per New deklarierten Objekten haben Sie weniger Kontrolle darüber, ob sie schon instanziert sind, da dies nicht – wie viele glauben – direkt in der mit New ausgerüsteten Zeile erfolgt, sondern erst beim ersten Zugriff auf eine Methode oder Eigenschaft der Klasse. Ein besserer Ansatz wäre, Letzteres in der Ereignisprozedur durchzuführen, die beim Öffnen des Formulars ausgelöst wird: Private Sub Form_Open(Cancel As Integer) Set colForms = New Collection End Sub Listing 16.23: Instanzieren der Collection beim Öffnen des Formulars
837
Kapitel 16
16.2.5 Neue Formularinstanz erzeugen und zur Collection hinzufügen Nach der Auswahl eines Kontaktes aus dem Listenfeld soll ein Knopfdruck eine Instanz des Formulars frmKontaktdetails öffnen, ohne die bereits vorhandenen Instanzen wieder zu löschen (siehe Abbildung 16.5).
Abbildung 16.5: Öffnen mehrerer Formularinstanzen per Mausklick
Dies soll natürlich nur eine neue Formularinstanz erzeugen, wenn noch kein Formular für diesen Kontakt vorhanden ist. Wird der Kontakt bereits angezeigt, soll das jeweilige Formular einfach aktiviert werden. Außerdem sollen natürlich nicht alle Instanzen des Formulars direkt übereinander erscheinen. Die folgende Prozedur wird diesen Anforderungen gerecht: Sie ermittelt zunächst die KontaktID und den im Listenfeld angezeigten Namen des aktuellen Kontaktes und speichert diese in entsprechenden Variablen. Dann prüft die Prozedur, ob bereits eine Formularinstanz existiert, die genau diesen Kontakt enthält. Die Anzahl der in der Collection bereits enthaltenen Elemente wird dabei aus zwei Gründen in einer Variablen gespeichert: Erstens lässt sich an diesem Wert überprüfen, ob überhaupt schon Instanzen des Formulars existieren, und falls ja, dient dieser Wert als Endpunkt der For…NextSchleife, die alle enthaltenen Instanzen auf den angezeigten Inhalt hin überprüft. Innerhalb dieser Schleife kommt die Tag-Eigenschaft der Formularinstanz zum Tragen: Beim Anlegen einer neuen Instanz weist die Prozedur dieser Eigenschaft einen Wert zu, der aus der Zeichenkette »Kontakt« und dem Wert des Feldes KontaktID des Kontaktes besteht, also beispielsweise »Kontakt12« – dazu später mehr. Während die Prozedur alle Elemente der Collection durchläuft, vergleicht sie jeweils den Wert der Tag-Eigenschaft
838
Objektorientierung im Praxiseinsatz
des enthaltenen Formulars mit dem Wert, den die neue Instanz erhalten würde. Trifft die Prozedur beim Durchlaufen der Schleife auf ein Formular, das genau diesen TagWert hat, bedeutet dies, dass bereits ein Formular für den entsprechenden Kontakt erzeugt wurde. Dieses wird dann mit der SetFocus-Methode aktiviert und die Prozedur wird beendet. Wenn die Prozedur in diesem Bereich feststellt, dass die Collection entweder noch gar kein Element enthält oder das betroffene Element sich nicht unter den vorhandenen Elementen befindet, erstellt es eine neue Instanz des Formulars für den ausgewählten Kontakt. Nach der Deklaration und Instanzierung eines neuen Objekts des Typs Form_ frmKontaktdetails erhält dieses die notwendigen Eigenschaftswerte – der Filter wird auf den gewünschten Kontakt-Datensatz eingestellt, die Tag-Eigenschaft enthält einen Wert, der aus der Zeichenkette »Kontakt« und dem Wert des Feldes KontaktID des betroffenen Kontaktes besteht (den Zweck haben Sie bereits weiter oben erfahren), die Überschrift des Formulars wird auf den Namen des Kontaktes eingestellt und schließlich das Formular sichtbar gemacht. Und dann folgt der wichtigste Schritt: Die Objektvariable, die auf die Formularinstanz verweist, wird der Collection colForms hinzugefügt. Da diese modulweit gültig ist, bleiben die enthaltenen Verweise bis zum Schließen des Formulars bestehen. Damit nicht alle Instanzen übereinander angezeigt werden, ermittelt die Prozedur noch die Anzahl der vorhandenen Formulare und versetzt das aktuelle Formular um einige Pixel nach rechts unten. Private Sub cmdDetails_Click() Dim Dim Dim Dim
i As Integer intFormCount As Integer lngKontaktID As Long strKontakt As String
If IsNull(Me!lstKontakte) Then Exit Sub End If lngKontaktID = Me!lstKontakte strKontakt = Me!lstKontakte.Column(1) 'Kontrollieren, ob Kontakt schon angezeigt wird...: 'Anzahl ermitteln intFormCount = colForms.Count 'Prüfen, ob überhaupt schon ein Element vorhanden ist If intFormCount > 0 Then
839
Kapitel 16 '... und falls ja, prüfen, ob bereits 'eines den gewünschten Kontakt enthält For i = 1 To intFormCount If colForms.Item(i).Form.Tag = "Kontakt" & lngKontaktID Then colForms.Item(i).Form.SetFocus Exit Sub End If Next i End If '... und falls nicht, eine neue Formularinstanz mit dem Artikel 'erzeugen Dim frm As Form_frmKontaktdetails Set frm = New Form_frmKontaktdetails 'Eigenschaften wie anzuzeigender Kontakt, Überschrift und Tag festlegen With frm .Filter = "KontaktID = " & lngKontaktID .FilterOn = True .Tag = "Kontakt" & lngKontaktID .Caption = "Kontakt: " & strKontakt 'Formularinstanz sichtbar machen .Visible = True End With 'Formularobjekt zur Collection hinzufügen colForms.Add frm 'Aktuelle Anzahl ermitteln intFormCount = colForms.Count 'Formulare entsprechend der Anzahl nach unten rechts verschieben DoCmd.MoveSize (intFormCount * 500) + 1000, (intFormCount * 500) + 1000 End Sub Listing 16.24: Per Mausklick eine weitere Formularinstanz öffnen
Gelöst ist das Problem noch lange nicht: Sie müssen noch zwei Schaltflächen zum Schließen eines bestimmten Formulars beziehungsweise zum Schließen aller geöffneten Formulare mit Leben füllen.
16.2.6 Schließen aller Instanzen des Formulars Letztere Aufgabe ist offensichtlich schneller erledigt: Wenn Sie sich einer Instanz eines Formulars durch Leeren der Objektvariablen entledigen können, dann lassen sich durch die gleiche Vorgehensweise mit der Collection, die alle Objektvariablen enthält, vermutlich auch alle Formulare schließen:
840
Objektorientierung im Praxiseinsatz Private Sub cmdAlleSchliessen_Click() Set colForms = Nothing End Sub Listing 16.25: Schließen aller Formularinstanzen durch Leeren der kompletten Sammlung
Das funktioniert auch – solange sich keines der Formulare wehrt. Formulare zur Datenbearbeitung besitzen in der Regel Mechanismen zum Validieren der enthaltenen Daten. Das Formular frmKontaktdetail könnte etwa folgende Prozedur zum Überprüfen des Vornamens besitzen: Private Sub Form_BeforeUpdate(Cancel As Integer) If Nz(Me!Vorname, "") = "" Then MsgBox "Bitte geben Sie einen Vorname ein." Me!Vorname.SetFocus Cancel = True Exit Sub End If End Sub Listing 16.26: Validierung eines Formularfeldes
Wenn Sie nun den Vornamen in einer Instanz des Formulars frmKontaktdetails leeren und dann mit der Prozedur aus Listing 16.27 schließen möchten, zeigt Access zwar die für diesen Fall vorbereitete Meldung an, schließt aber anschließend das Formular, ohne dem Benutzer Gelegenheit zum Korrigieren der Eingabe zu geben – und ohne irgend eine Änderung in diesem Formular zu übernehmen. Die folgende Prozedur ist zwar wesentlich umfangreicher als der Vorschlag aus Lis ting 16.25, dafür lässt sie Formulare, deren Inhalt »dirty«, also seit dem letzten Speichern verändert ist, außen vor und schließt nur die übrigen Formulare, indem sie diese aus der Collection colForms entfernt. Wenn die Collection nach dem kollektiven Schließen noch Elemente enthält und dement sprechend noch ein oder mehrere Formulare geöffnet sind, gibt die Prozedur eine Mel dung aus. Private Sub cmdAlleSchliessen_Click() Dim intFormCount As Integer Dim i As Integer Dim frm As Form_frmKontaktdetails 'Anzahl der Instanzen ermitteln intFormCount = colForms.Count
841
Kapitel 16 'Alle Instanzen durchlaufen - rückwärts wegen Löschvorgang For i = intFormCount To 1 Step -1 'Aktuelles Element per Objektvariable referenzieren Set frm = colForms.Item(i) 'Prüfen, ob Datensatz gespeichert ist If frm.Dirty = False Then 'Falls gespeichert: Aus Collection entfernen. colForms.Remove i End If Next i 'Anzahl der Instanzen erneut ermitteln intFormCount = colForms.Count 'Wenn noch Instanzen vorhanden sind, Meldung ausgeben If intFormCount = 0 Then Set colForms = Nothing Else MsgBox "Es konnten nicht alle Formulare mit " _ & "Kontaktdetails geschlossen werden." End If End Sub Listing 16.27: Schließen aller Instanzen
16.2.7 Schließen einer bestimmten Instanz Die Möglichkeit, mehrere Instanzen eines Formulars mit verschiedenen Datensätzen zu erzeugen, ist in vielen Fällen vermutlich die ergonomischste Lösung für die gleichzeitige Ansicht mehrerer Datensätze. Das gilt vor allem, wenn die durch die Datensätze repräsentierten Objekte so viele Daten enthalten, dass diese nicht nebeneinander auf einer Bildschirmbreite angezeigt werden können. Außerdem kann der Benutzer mit dieser Methode die Formulare mit den interessanten Daten auch noch so anordnen, wie es für den jeweiligen Fall am sinnvollsten ist. Es fehlen noch einige kleine Schritte, um die ergonomischen Vorteile dieser Lösung zu vollkommnen: Die Formularinstanzen sollten auch per Doppelklick in das Listenfeld geöffnet werden können. Der Benutzer soll auch einzelne Formulare vom Übersichtsformular aus schließen können. Die Formulare sollen auch mit der dafür vorgesehenen Schaltfläche geschlossen werden können. Der erste Wunsch ist reine Code-Optimierung. Fügen Sie den gesamten Code der Er eignisprozedur cmdDetails_Click in eine neue Prozedur namens InstanzOeffnen ein. Die
842
Objektorientierung im Praxiseinsatz
beiden Variablen lngKontaktID und strKontakt sollen weiterhin von der aufrufenden Rou tine ermittelt und an die Prozedur InstanzOeffnen übergeben werden. Die Ereignisprozedur cmdDetails_Click sieht nunmehr wie folgt aus: Private Sub cmdDetails_Click() InstanzOeffnen Me!lstKontakte, Me!lstKontakte.Column(1) End Sub Listing 16.28: Aufruf der Prozedur InstanzOeffnen beim Klick auf die Schaltfläche cmdDetails …
Die gleiche Anweisung sorgt beim Doppelklick ins Listenfeld lstKontakte für den Aufruf der Prozedur InstanzOeffnen: Private Sub lstKontakte_DblClick(Cancel As Integer) InstanzOeffnen Me!lstKontakte, Me!lstKontakte.Column(1) End Sub Listing 16.29: … und beim Doppelklick auf den gewünschten Eintrag im Listenfeld
Fehlt noch die in die neue Prozedur »extrahierte« Funktionalität (Erläuterungen siehe Listing 16.24): Private Sub InstanzOeffnen(lngKontaktID As Long, strKontakt As String) Dim i As Integer Dim intFormCount As Integer Dim frm As Form_frmKontaktdetails intFormCount = colForms.Count If intFormCount > 0 Then For i = 1 To intFormCount If colForms.Item(i).Form.Tag = "Kontakt" & lngKontaktID Then colForms.Item(i).Form.SetFocus Exit Sub End If Next i End If Set frm = New Form_frmKontaktdetails With frm .Filter = "KontaktID = " & lngKontaktID .FilterOn = True .Tag = "Kontakt" & lngKontaktID .Caption = "Kontakt: " & strKontakt .Visible = True End With
Der Wunsch nach dem Schließen einzelner Formularinstanzen wahlweise vom Über sichtsfenster aus oder direkt per Schließen-Schaltfläche des Formulars schreit ebenso wie im obigen Fall nach Bereitstellung einer Routine, die von den entsprechenden Stellen aus aufgerufen werden kann. Warum aber kann man das Formular nicht einfach mit seiner Schließen-Schaltfläche verschwinden lassen? Ganz einfach: Weil es dann nicht aus der Collection entfernt wird. Zugriffe auf den zurückgelassenen Eintrag in der Collection würden in der Folge zu Fehlern führen. Daher wird die InstanzSchliessen-Prozedur aus folgendem Listing auch vom zu schließenden Formular aus aufgerufen und deshalb als Public deklariert. Die Prozedur überprüft genau wie die Prozedur zum Schließen aller Formulare, ob das Formular noch zu speichernde Daten enthält, und bricht den Vorgang gegebenenfalls ab. Wenn dem Schließen aber nichts mehr im Wege steht, entfernt die Prozedur das durch den Eingangsparameter strTag charakterisierte Formular aus der Collection und schließt es damit. Public Sub InstanzSchliessen(strTag As String) Dim frm As Form_frmKontaktdetails On Error GoTo InstanzSchliessen_Err 'Per Objektvariable auf die Formularinstanz verweisen Set frm = colForms.Item(strTag) 'Nur wenn der Inhalt des Formulars seit dem letzten 'Speichern nicht geändert wurde: If frm.Dirty = False Then 'Tag des Formulars leeren frm.Tag = "" 'Formular aus Collection entfernen und damit schließen colForms.Remove strTag End If InstanzSchliessen_Exit: Exit Sub
844
Objektorientierung im Praxiseinsatz InstanzSchliessen_Err: 'Formular nicht mehr vorhanden If Err.Number = 5 Or Err.Number = 9 Then GoTo InstanzSchliessen_Exit Else MsgBox Err.Number & " " & Err.Description GoTo InstanzSchliessen_Exit End If End Sub Listing 16.31: Funktion zum Schließen eines Formulars
Im Vergleich zur Prozedur aus Listing 16.27 fällt die zusätzliche Anweisung frm.Tag = "" auf: Ihre Bedeutung wird nachfolgend erläutert.
16.2.8 Schließen-Vorgang des Formulars anpassen Wie bereits erwähnt, soll die Funktion InstanzSchliessen auch beim Schließen einer For mularinstanz über deren Schließen-Schaltfläche ausgelöst werden. Den Prozeduraufruf bringen Sie in der Ereignisprozedur Beim Schließen des Formulars unter: Private Sub Form_Close() 'Prüfen, ob die Formularinstanz noch in der Collection enthalten ist If Not Nz(Me.Tag, "") = "" Then 'Prozedur zum Entfernen der Instanz aus der Collection aufrufen Forms!frmKontaktuebersicht.InstanzSchliessen Me.Tag End If End Sub Listing 16.32: Prozedur, die beim Schließen des Detailformulars ausgelöst wird
Hier klärt sich auch die zusätzliche Zeile in der Prozedur zum Entfernen der Instanz aus der Collection (Listing 16.31). Wenn man das Formular mit seinen Bordmitteln schließen und dabei einfach nur die Funktion zum Entfernen der Instanz aufrufen würde, hätte man es mit einem typischen »Die Katze beißt sich in den Schwanz«-Problem zu tun: Das Beim Schließen-Ereignis ruft kurz vor dem Exitus des Formulars noch die Prozedur InstanzSchliessen auf. Durch das dortige Entfernen aus der Collection fliegt das Formular aus seinem Gültigkeitsbereich und löst wiederum das Ereignis Beim Schließen aus – und so beginnt das Spiel von vorne. Die Tag-Eigenschaft ist die Rettung: Beim Schließen vom Formular aus enthält diese Eigenschaft noch einen Wert wie »Kontakt12«. Dann wird die InstanzSchliessen-Prozedur aufgerufen, die diese Eigenschaft leert. Damit führt der durch das Entfernen der Instanz aus der Collection verursachte Schließen-Vorgang endgültig zum Exitus des Formulars.
845
Kapitel 16
16.3 Mehrschichtige Anwendungen Im Gegensatz zu objektorientierten Programmiersprachen wie C#, VB.NET oder Java gibt es in Access-Anwendungen keine einzelnen Dateien, die unterschiedliche Klassen definieren. Ganz im Gegenteil: Sämtliche Objekte wie Tabellendefinitionen, Abfragen, Formulare, Berichte, Module und selbst die Daten sind in der .accdb-Datei verborgen. Prinzipiell handelt es sich dabei zwar auch um eigene Dateien, aber diese werden in der Storage von Access für die Augen des Benutzers unsichtbar verwaltet. Da wird sich der eine oder andere fragen: Mehrschichtige Anwendungen mit diesem aus einem Berg einzelner Objekte und wirr verteiltem VBA-Code bestehenden Klotz? Wie soll das funktionieren? Letztendlich funktioniert das genau wie in anderen objektorientierten Programmiersprachen – nur dass diese natürlich einige Features mehr liefern, wie etwa Vererbung und Polymorphie. Und die Tatsache, dass Access alle Klassen und Objekte intern speichert, spielt letzten Endes nur eine Rolle, wenn Sie den Code örtlich verteilen möchten. Welche Vorteile bringen mehrschichtige Anwendungen nun konkret? Nun, schauen Sie sich erstmal die Nachteile herkömmlicher Access-Anwendungen an. Benutzeroberfläche und Anwendungslogik sind zu einem sehr hohen Anteil in den Formularen und Berichten konzentriert. Das ist an sich kein Nachteil, wenn nicht verschiedene Funk tionen auch noch miteinander vermengt wären: Der Zugriff auf die Daten erfolgt direkt über die Bindung von Formularen und die enthaltenen Steuerelemente auf die Abfra gen beziehungsweise Tabellen. Manchmal sorgt eine Gültigkeitsregel oder ein anderer Integritätsmechanismus innerhalb der Tabellen für die Konsistenz der Daten, in vielen Fällen ist diese Funktion jedoch in den Formularen selbst enthalten und wird etwa durch die Ereignisse Vor Aktualisierung des Formulars oder Steuerelements ausgelöst. Andere Felder sind nicht direkt an die Datensatzquelle gebunden, sondern beziehen ihre Daten beispielsweise per VBA über Domänenfunktionen. Sprich: Die Anwendungslogik ver teilt sich über mehrere verschiedene Objektarten wie Tabellen, Formulare und VBA-Mo dule – vielleicht gibt es sogar noch ein paar Makros. Die Pflege solcher Anwendungen kann sehr zeitintensiv werden: Wenn Sie – als einfaches Beispiel – eine Meldung, die auf einen falschen Datentyp bei der Eingabe in ein For mularfeld hinweist, ändern oder entfernen möchten, sind Sie unter Umständen lange unterwegs, da sich der Auslöser in verschiedenen Ereignissen des Formulars oder auch im Tabellenentwurf befinden kann. Das alles muss man natürlich insoweit relativieren, als man mit ein wenig Sorgfalt und konsistenter Vorgehensweise durchaus seine eigenen Anwendungen und die Orte, an denen man die Anwendungslogik unterbringt, im Griff hat. Und Access ist natürlich zuerst einmal dafür ausgelegt, auf schnellstem Wege Anwendungen für den Zugriff auf und die Verwaltung von Daten zu entwickeln. Wenn eine Anwendung allerdings ein
846
Objektorientierung im Praxiseinsatz
gewisses Maß an Komplexität überschritten hat und Sie für kleine Änderungen beinahe genauso lange brauchen, als wenn Sie die halbe Anwendung neu programmieren, sollten Sie über alternative Vorgehensweisen nachdenken. Diese liegen beispielsweise in der Verwendung eines mehrschichtigen Datenmodells. Solche Modelle gibt es in mehreren Varianten mit unterschiedlicher Interpretation. Im Folgenden lernen Sie ein Modell kennen, das je nach Sichtweise aus drei oder vier Schichten besteht: Der Benutzeroberfläche (GUI-Schicht), der Business-Schicht, der Datenzugriffsschicht und den Daten. Manch einer betrachtet die Datenzugriffsschicht und die Daten als Einheit, andere sehen zwei Schichten darin. Im Rahmen dieses Buches werden Datenzugriffsschicht und Daten als zwei Schichten betrachtet. Nachteile hat die Verwendung einer mehrschichtigen Architektur natürlich auch: Das Schichtenmodell bringt einen immensen zusätzlichen Programmieraufwand mit sich, der sich eindeutig in geringerer Performance niederschlägt – vor allem, wenn die Daten einer oder mehrerer Tabellen komplett in Form von Objekten vorliegen. Das nachfolgende Beispiel kann auch nur einen Eindruck vom Aufbau einer mehrschichtigen Anwendung vermitteln – für den Einsatz in der Praxis müsste man in einige Stellen noch weit mehr Arbeit investieren. So wäre zum Beispiel ein Mechanismus zu schaffen, der sicherstellt, dass die Objekte regelmäßig mit den aktuellen Daten aus der Datenbank gefüttert werden, da auch andere Benutzer auf die enthaltenen Daten zugreifen können. Die Beispiele zu diesem Abschnitt finden Sie auf der Buch-CD unter \Kap_16\Objektori entierteTechniken.accdb. Es handelt sich dabei um die Tabelle tblPersonen, das Formular frmPersonen sowie die Module clsPerson, clsController und clsPersonDAO_ DAO.
16.3.1 Beispiel Als Beispiel für den mehrschichtigen Datenzugriff dient eine Tabelle namens tblPersonen (siehe Abbildung 16.6) und ein Formular namens frmPersonen (siehe Abbildung 16.7). Das Formular ist komplett ungebunden und enthält lediglich die folgenden Steuerele mente: cboSchnellsuche: dient der Auswahl von Personen txtPersonID: schreibgeschützt txtVorname, txtNachname, txtStrasse, txtPLZ, txtOrt: Textfelder der Tabelle cmdSpeichern: Schaltfläche zum Speichern des aktuellen Inhalts cmdLoeschen: Schaltfläche zum Löschen des aktuellen Datensatzes cmdNeu: Schaltfläche zum Anlegen eines neuen Datensatzes
847
Kapitel 16
Abbildung 16.6: Entwurfsansicht der Tabelle tblPersonen
Abbildung 16.7: Das Formular frmPersonen ist ungebunden
16.3.2 Die GUI-Schicht Die GUI-Schicht – also die Benutzeroberfläche – bildet für das nachfolgende Beispiel das Formular frmPersonen aus Abbildung 16.7. In diesem Fall wird die GUI-Schicht also in Form eines Access-Frontends realisiert. Die GUI-Schicht enthält nur Methoden für den Zugriff auf die Business-Schicht, auf keinen Fall kann sie direkt auf eine darunter liegende Schicht zugreifen. Das ist bei herkömmlichen Access-Anwendungen der Fall: Hier werden Datensatzquelle und Steuerelemente direkt an die Datenschicht gebunden. Andersherum kann keine der anderen Schichten auf die GUI-Schicht zugreifen – das ist eine der Hauptprämissen bei der Entwicklung mehrschichtiger Anwendungen. Sie minimieren die Abhängigkeit, indem Sie dafür sorgen, dass diese lediglich einseitig ist.
848
Objektorientierung im Praxiseinsatz
16.3.3 Die Business-Schicht Die Business-Schicht enthält zwei Typen von Objekten: Der erste repräsentiert die in den Datensätzen der Tabellen enthaltenen Daten (Daten-Objekte), der zweite enthält die Steuermechanismen für den Transfer der Daten zwischen der Datenzugriffsschicht und der GUI-Schicht (Controller-Objekte). Genau genommen ist das nicht ganz richtig: Die Controller-Objekte steuern zwar die Objekte der Datenzugriffsschicht und andere Objekte der Business-Schicht, aber die Kooperation zwischen der GUI-Schicht und den Controller-Objekten geht immer von der GUI-Schicht aus. Wie bereits erwähnt – die oberen Schichten können zwar auf die unteren zugreifen, aber niemals umgekehrt. Und da auch nie eine Schicht übersprungen werden darf, muss die GUI-Schicht immer über die Business-Schicht auf die Datenzugriffsschicht zugreifen, die dann die gewünschten Daten nach oben reicht. Wie viele und welche Objekte Sie in der Business-Schicht ansiedeln, hängt von der Art der zugrunde liegenden Daten und der Benutzeroberfläche ab. Sie werden vermutlich für jede Tabelle, die objektartige Daten enthält, ein eigenes Objekt erstellen. Außerdem müssen Sie entscheiden, ob Sie ein Controller-Objekt pro Element der Benutzeroberfläche oder vielleicht sogar ein großes Controller-Objekt verwenden. Übersichtlicher dürfte etwa ein Objekt pro Formular sein.
16.3.4 Die Datenzugriffsschicht Die Datenzugriffsschicht enthält Datenzugriffsobjekte. Zu jedem Daten-Objekt gibt es ein Datenzugriffsobjekt, das verschiedene Operationen ausführen kann: Erzeugen eines Datenobjekts auf der Basis eines Datensatzes der zugrunde liegenden Tabelle Erzeugen eines Recordsets mit Datensätzen als Suchergebnis mit vorgegebenen Kriterien Aktualisieren eines Datensatzes in der Datenbank auf Basis der in einem Datenobjekt enthaltenen Daten Anlegen eines neuen Datensatzes in der Datenbank Löschen eines Datensatzes aus der Datenbank Damit entkoppelt die Datenzugriffsschicht die Business-Schicht von den Daten. Der Vorteil liegt darin, dass Sie ohne Probleme die Datenquelle wechseln können – etwa um von einem Access-Backend auf einen SQL-Server umzusteigen oder vielleicht sogar, um eine XML-Datei als Datenquelle zu verwenden. Sie müssen lediglich die Klassen der
849
Kapitel 16
Datenzugriffsschicht anpassen – die GUI-Schicht und die Business-Schicht bleiben vom Wechsel der Datenquelle unberührt. Im nachfolgend beschriebenen Beispiel erfahren Sie, wie die fünf Operationen eines Datenzugriffsobjekts aussehen. Man benötigt je ein Datenzugriffsobjekt pro BusinessObjekt der Business-Schicht. Das Beispiel verwendet lediglich ein Datenzugriffsobjekt für den Zugriff auf die Datenbank per DAO. Sie können alternativ ein Datenzugriffsobjekt mit den gleichen Methoden, aber anderen Anweisungen für den Zugriff auf die Daten verwenden, um etwa die ADODB-Bibliothek statt der DAO-Bibliothek einzusetzen. Oder Sie erstellen ein drittes Datenzugriffsobjekt, das den Zugriff auf eine XML-Datei über das mit der Bibliothek MSXML gelieferte Document Object Model ermöglicht. Wenn Sie bezüglich des Datenzugriffs derart flexibel sein möchten, empfiehlt sich die Verwendung einer Schnittstelle – mehr dazu haben Sie bereits in Kapitel 15, Abschnitt 15.9, »Schnittstellen und Vererbung«, gelesen.
16.3.5 Die Datenschicht Die Datenschicht enthält die eigentlichen Daten. Im vorliegenden Beispiel ist das eine Tabelle in einer Access-Datenbank. Es kann sich aber auch um eine Tabelle in einer SQLServer-Datenbank oder um eine XML-Datei handeln. Diese Flexibilität erhalten Sie durch die Aufteilung der Anwendung auf verschiedene Schichten – um etwa auf eine XML-Datei statt auf eine Access-Datenbank zuzugreifen, müssten Sie nur die Objekte der Datenzugriffsschicht anpassen. Benutzeroberfläche und Business-Schicht bleiben unangetastet.
16.3.6 Zusammenhänge der Objekte und Schichten Abbildung 16.8 zeigt die Aufteilung der Objekte auf die einzelnen Schichten. Es gibt eine Menge Zusammenhänge, die Sie in den folgenden Abschnitten detailliert und mit Code versehen näher kennen lernen.
16.3.7 Initialisieren des Formulars Direkt nach dem Öffnen soll das Formular keinen Datensatz anzeigen. Lediglich das Kombinationsfeld cboSchnellsuche soll alle enthaltenen Personen zur Auswahl anbieten. Das Füllen dieses Steuerelements ist dann auch die erste Funktion, die programmiert und auf mehrere Schichten aufgeteilt werden soll. Der Beginn sieht unspektakulär aus: Die beim Öffnen des Formulars ausgelöste Routine initialisiert das im Kopf des Moduls deklarierte Controller-Objekt und ruft die Prozedur cboSchnellsucheAktualisieren auf.
850
Objektorientierung im Praxiseinsatz G U I-S ch ich t
B u sin esssch ich t clsP erso n
clsC o n tro ller
D aten zu g riffssch ich t
clsP erso n _D A O
D aten sch ich t
tb lP erso n en
Abbildung 16.8: Aufteilung der Objekte auf die einzelnen Schichten
Dim objController As clsController Private Sub Form_Open(Cancel As Integer) Set objController = New clsController cboSchnellsucheAktualisieren End Sub Listing 16.33: Initialisieren des Formulars
Die Prozedur cboSchnellsucheAktualisieren ruft die Routine GetPersons des ControllerObjekts auf. Diese Methode liefert ein Collection-Objekt zurück, das in dem Objekt objPersonen gespeichert wird. Bevor Sie die restlichen Zeilen der Prozedur betrachten, schauen Sie sich zunächst den weiteren Verlauf an. Private Sub cboSchnellsucheAktualisieren() Dim objPerson As clsPerson Dim objPersonen As Collection
851
Kapitel 16 Dim str As String Set objPersonen = objController.GetPersons If Not objPersonen Is Nothing Then For Each objPerson In objPersonen Me.cboSchnellsuche.AddItem objPerson.PersonID & ";" _ & objPerson.Nachname & ", " & objPerson.Vorname Next objPerson Else MsgBox "Personenliste konnte nicht geladen werden." End If End Sub Listing 16.34: Zuweisen einer Datensatzgruppe mit allen Personen an das Kombinationsfeld zur Schnellsuche
16.3.8 Initialisieren des Controller-Objekts In Listing 16.33 wurde eine Instanz der Klasse clsController erzeugt. Diese löst das Initia lize-Ereignis dieser Klasse aus, die folgendermaßen aussieht und das modulweit dekla rierte Objekt objPersonDAO instanziert: Dim objPersonDAO As clsPersonDAO_DAO Dim objPersonen As Collection Private Sub Class_Initialize() Set objPersonDAO = New clsPersonDAO_DAO End Sub Listing 16.35: Initialisieren der Klasse clsController der Business-Schicht
Damit hat die Business-Schicht direkt die nächste Schicht – die Datenzugriffsschicht – ins Spiel gebracht. Diese enthält die Methoden für den Zugriff auf die Datenschicht und kommt gleich zum Einsatz.
16.3.9 Aufruf der Methode GetPersons der Business-Schicht Nach dem Initialisieren des Controller-Objekts kann die Prozedur aus Listing 16.34 endlich die GetPersons-Methode aufrufen. Diese ist wiederum recht kurz und enthält lediglich den Aufruf der Find-Methode des Objekts objPersonDAO der Datenzu griffsklasse sowie die Zuweisung des erhaltenen Objekts an den Rückgabewert der Funktion. Der Zugriff auf die Daten wird also nach unten an die nächste Schicht weitergereicht:
852
Objektorientierung im Praxiseinsatz Public Function GetPersons() As Collection Set objPersonen = objPersonDAO.Find Set GetPersons = objPersonen End Function Listing 16.36: Die Methode GetPersons der Klasse clsController
16.3.10 Zugriff des Datenzugriffsobjekts auf die Datenschicht Die Find-Methode stellt nach einigem Weiterreichen den ersten Zugriff auf die Daten dar. Sie verwendet Objekte, Methoden und Eigenschaften des DAO-Objektmodells für den Zugriff auf die Tabelle tblPersonen. Die Methode hat einen optionalen Parameter namens varSearch – hier können Sie eine beliebige WHERE-Bedingung übergeben. Dieser Parameter wird im vorliegenden Beispiel nicht verwendet. Deshalb setzt die Prozedur die Zeichenkette strSQL lediglich aus dem SQL-Ausdruck SELECT * FROM tblPersonen zusammen. Neben dem Database- und dem Recordset-Objekt für den Zugriff auf die Daten deklariert die Methode noch ein Personen-Objekt und ein Collection-Objekt, das später die eingelesenen Personen-Objekte enthalten wird. Nach dem Öffnen der Datensatzgruppe rst durchläuft die Routine alle enthaltenen Datensätze und legt jeweils ein neues Objekt des Typs clsPerson an (Beschreibung siehe weiter unten) und füllt dessen Eigenschaften mit den Inhalten der entsprechenden Tabellenfelder. Nach dem Einlesen der Daten wird das fertige Objekt mit dem Wert der Eigenschaft PersonID als Schlüssel an die Auflistung objPersonen angehängt. Diesen Schlüssel verwenden Sie später, um auf einzelne Personen-Objekte der Collection zugreifen zu können. Nachdem auf diese Weise alle Datensätze der Auflistung objPersonen in Form eines Personen-Objekts zugewiesen wurden, gibt die Funktion das Collection-Objekt mit den Personen-Objekten an die aufrufende Prozedur zurück. Public Function Find(Optional varSearch As Variant) As Collection On Error GoTo Find_Err Dim Dim Dim Dim Dim
db As DAO.Database rst As DAO.Recordset strSQL As String objPerson As clsPerson objPersonen As Collection
Set db = CurrentDb
853
Kapitel 16 strSQL = "SELECT * FROM tblPersonen" If Not IsMissing(varSearch) Then strSQL = strSQL & " WHERE " & varSearch End If Set rst = db.OpenRecordset(strSQL, dbOpenDynaset) Set objPersonen = New Collection Do While Not rst.EOF Set objPerson = New clsPerson With objPerson .PersonID = rst!PersonID .Vorname = rst!Vorname .Nachname = rst!Nachname .Strasse = rst!Strasse .PLZ = rst!PLZ .Ort = rst!Ort End With objPersonen.Add objPerson, CStr(objPerson.PersonID) rst.MoveNext Loop Set Find = objPersonen Find_Exit: On Error Resume Next Set db = Nothing Exit Function Find_Err: GoTo Find_Exit End Function Listing 16.37: Die Find-Methode des Datenzugriffsobjekts clsPersonDAO_DAO
Ab nach oben Als Rückgabewert der Funktion Find wird das Collection-Objekt zunächst an die aufrufende Prozedur GetPersons der Business-Schicht weitergegeben, die es für weitere Zugriffe zwischenspeichert und es ihrerseits an die Routine cboSchnellsucheAktualisieren des Formulars zurückgibt. Diese wiederum wertet die Collection mit den Personen-Objekten derart aus, dass jeweils der Wert der Eigenschaft PersonID und Vor- und Nachname in der Form , im Kombinationsfeld cboSchnellsuche angezeigt werden können. Dazu müssen
854
Objektorientierung im Praxiseinsatz
Sie noch die Eigenschaft Herkunftsart auf Wertliste und die Eigenschaften Spaltenanzahl und Spaltenbreite auf die Werte 2 und 0cm einstellen (siehe Abbildung 16.9).
Abbildung 16.9: Kombinationsfeld mit Daten aus einer Collection von Personen-Objekten
16.3.11 Die Klasse clsPerson Die Find-Routine hat Objekte des Typs clsPerson verwendet, um die Daten aus der Tabelle tblPersonen in einer Collection aller enthaltenen Datensätze zu speichern. Solche Objekte zum Verwenden von in Tabellen gespeicherten Objekten heißen Value-Objekte. Sie speichern lediglich die Daten der in der Tabelle enthaltenen Datensätze. Dabei sind alle Felder als private Variablen vorhanden, die mit Property Get- und Property Let-Prozeduren von außen gelesen und geschrieben werden können. Die Klasse clsPerson sieht wie folgt aus: Dim Dim Dim Dim Dim Dim
mPersonID As Long mVorname As String mNachname As String mStrasse As String mPLZ As String mOrt As String
Public Property Get PersonID() As Long PersonID = mPersonID End Property Public Property Let PersonID(lngPersonID As Long) mPersonID = lngPersonID End Property Public Property Get Vorname() As String Vorname = mVorname End Property
855
Kapitel 16 Public Property Let Vorname(strVorname As String) mVorname = strVorname End Property Public Property Get Nachname() As String Nachname = mNachname End Property Public Property Let Nachname(strNachname As String) mNachname = strNachname End Property Public Property Get Strasse() As String Strasse = mStrasse End Property Public Property Let Strasse(strStrasse As String) mStrasse = strStrasse End Property Public Property Get PLZ() As String PLZ = mPLZ End Property Public Property Let PLZ(strPLZ As String) mPLZ = strPLZ End Property Public Property Get Ort() As String Ort = mOrt End Property Public Property Let Ort(strOrt As String) mOrt = strOrt End Property Listing 16.38: Code der Klasse clsPerson
16.3.12 Auswählen und Anzeigen eines Datensatzes Nach dem Füllen des Kombinationsfeldes zur Schnellauswahl von Personen kümmern Sie sich nun um die Funktionalität des Kombinationsfeldes. Dieses soll nach der Aus wahl den gewählten Datensatz im Formular anzeigen. Hier kommt wiederum die Klasse clsPerson ins Spiel. Sie dient als Transportmittel der Da ten aus der immer noch in der Klasse objController befindlichen Collection objPersonen. Ja, genau: Nach der Auswahl des anzuzeigenden Datensatzes aus dem Kombinationsfeld greift die Anwendung nicht etwa über die Zwischenschichten auf die Datenschicht zu,
856
Objektorientierung im Praxiseinsatz
sondern bezieht die Informationen aus der im Controller zwischengespeicherten Collec tion, die alle im Kombinationsfeld auswählbaren Personen in Form von Objekten des Typs clsPerson enthält. Und das sieht so aus: Nach dem Deklarieren der Objektvariablen objPerson wird diese mit Hilfe der Methode LoadPerson des Controller-Objekts gefüllt. Als Parameter wird dabei das gebundene Feld des Kombinationsfeldes übergeben, das die PersonID der anzuzeigenden Person enthält. Private Sub cboSchnellsuche_AfterUpdate() Dim objPerson As clsPerson Set objPerson = objController.LoadPerson(Me.cboSchnellsuche) With objPerson Me!txtPersonID = .PersonID Me!txtVorname = .Vorname Me!txtNachname = .Nachname Me!txtStrasse = .Strasse Me!txtPLZ = .PLZ Me!txtOrt = .Ort End With End Sub Listing 16.39: Diese Routine wird nach der Auswahl eines Eintrags des Kombinationsfelds aufgerufen
Für das Füllen der Eigenschaften mit den Inhalten der Felder des Datensatzes mit der gesuchten PersonID ist die Methode LoadPerson des Controller-Objekts zuständig. Diese Methode deklariert zunächst ein Objekt des Typs clsPersonen und weist diesem das Objekt aus der Collection objPersonen mit dem passenden Wert der Eigenschaft PersonID zu, der in der Collection als Schlüsselwert eines jeden Elements gespeichert ist. Sollte ein solches Objekt einmal nicht in der Collection zu finden sein, greift die Funktion über die Methode Read des Objekts objPersonDAO auf die in der Datenschicht beziehungsweise der Datenbank enthaltenen Daten zu. Anderenfalls dient die in der Auflistung vorgefundene Instanz des gesuchten Personen-Objekts als Rückgabewert der Methode. Public Function LoadPerson(lngPersonID As Long) As clsPerson Dim objPerson As clsPerson Set objPerson = objPersonen(CStr(lngPersonID)) If objPerson Is Nothing Then Set LoadPerson = objPersonDAO.Read(lngPersonID)
857
Kapitel 16 Else Set LoadPerson = objPerson End If End Function Listing 16.40: Weiterdelegieren des Ladens von Personendaten in das entsprechende Objekt
16.3.13 Einlesen von Personen, die nicht in der Collection enthalten sind Für den in diesem Beispiel eigentlich nicht vorgesehenen Fall, dass ein Personen-Objekt angezeigt werden soll, das nicht in der Collection objPersonen enthalten ist, greift die Methode LoadPerson des Controller-Objekts mit der Read-Methode der Datenzugriffs klasse auf den in der Datenbank gespeicherten Personendatensatz zu. An dieser Stelle wird natürlich offensichtlich, dass die hier beschriebene Vorge hensweise keinerlei Feedback von der Datenschicht beinhaltet, wenn Daten durch andere Benutzer geändert, gelöscht oder hinzugefügt werden. Eine Möglichkeit, dieses Feedback zu realisieren, wäre die Verwendung von ADO. ADO-Recordsets enthalten Ereigniseigenschaften, mit denen sich Prozeduren zum Benachrichtigen übergeordneter Instanzen erstellen lassen. Dieses zusätzliche Feature soll aber aus Gründen der Übersichtlichkeit nicht eingebunden werden. Diese öffnet zunächst eine Datensatzgruppe aller Datensätze mit der übergebenen Per sonID, wobei die Anzahl logischerweise 1 ist. Die dort enthaltenen Informationen werden nun in ein frisch instanziertes Personen-Objekt eingetragen, das anschließend als Rückgabewert der Funktion festgelegt wird. Public Function Read(PersonID As Long) As clsPerson On Error GoTo Read_Err Dim db As DAO.Database Dim rst As DAO.Recordset Dim objPerson As clsPerson Set db = CurrentDb Set rst = db.OpenRecordset("SELECT * FROM tblPersonen " _ & "WHERE [PersonID] = " & PersonID, dbOpenDynaset) Set objPerson = New clsPerson With objPerson .PersonID = rst![PersonID]
858
Objektorientierung im Praxiseinsatz .Vorname = rst![Vorname] .Nachname = rst![Nachname] .Strasse = rst![Strasse] .PLZ = rst![PLZ] .Ort = rst![Ort] End With Set Read = objPerson Read_Exit: On Error Resume Next Set objPerson = Nothing rst.Close Set rst = Nothing Set db = Nothing Exit Function Read_Err: GoTo Read_Exit End Function Listing 16.41: Einlesen eines Datensatzes in ein Objekt der Business-Schicht
Von hier aus geht es dann über die Business-Schicht direkt in die GUI-Schicht. Dort wartet die Routine aus Listing 16.39 bereits und trägt die Eigenschaften der gewünschten Person in die entsprechenden Textfelder des Formulars ein.
16.3.14 Neuer Datensatz Das Anlegen neuer Datensätze verläuft in ungebundenen Formularen erstaunlich ruhig. Damit ist natürlich vor allem die Interaktion mit der Datenbank gemeint, denn wenn man nicht die Speichern-Schaltfläche anklickt, passiert gar nichts. Falls noch ein Datensatz im Formular angezeigt wird, müssen Sie dieses allerdings erst einmal leeren. Dazu verwenden Sie die Schaltfläche mit der Beschriftung Neu. Diese ruft eine weitere Funktion auf, die alle Textfelder des Formulars leert. Private Sub cmdNeu_Click() FormularLeeren End Sub Listing 16.42: Zum Anlegen eines neuen Datensatzes …
Private Sub FormularLeeren() With Me
859
Kapitel 16 !txtPersonID = Null !txtVorname = "" !txtNachname = "" !txtStrasse = "" !txtPLZ = "" !txtOrt = "" End With End Sub Listing 16.43: … sind lediglich die Textfelder zu leeren
16.3.15 Speichern eines Datensatzes Nach der Eingabe der Daten ist es sinnvoll, diese zu speichern. Dazu klicken Sie auf die Schaltfläche cmdSpeichern. Die löst die folgende Prozedur aus: Private Sub cmdSpeichern_Click() Me!txtPersonID = objController.SavePerson(Nz(Me!txtPersonID, 0), _ Me!txtVorname, Me!txtNachname, Me!txtStrasse, Me!txtPLZ, Me!txtOrt) cboSchnellsucheAktualisieren End Sub Listing 16.44: Auslösen des Speicher-Vorgangs im Formular
Die Routine prüft, ob der Datensatz bereits einen Wert im Feld PersonID hat oder nicht. Falls nicht, handelt es sich um einen neuen Datensatz und die Funktion CreatePerson des Controller-Objekts wird aufgerufen.
16.3.16 Datensatz neu anlegen oder aktualisieren? Der beim Betätigen der Speichern-Schaltfläche im Formular befindliche Datensatz kann bereits in der Datenbank vorhanden sein oder auch nicht. Ein eindeutiges Kennzeichen dafür ist das Vorhandensein eines Wertes in der Eigenschaft PersonID. Diese ID wird nur beim Anlegen eines Objekts in der Datenbank erstellt. Die Methode SavePerson prüft dies und reicht die zu speichernden Daten entweder an die Routine CreatePerson oder UpdatePerson weiter. Public Function SavePerson(lngPersonID As Long, strVorname As String, _ strNachname As String, strStrasse As String, strPLZ As String, _ strOrt As String) As Long If lngPersonID = 0 Then lngPersonID = CreatePerson(strVorname, strNachname, strStrasse, _ strPLZ, strOrt) Else
860
Objektorientierung im Praxiseinsatz UpdatePerson lngPersonID, strVorname, strNachname, strStrasse, _ strPLZ, strOrt End If SavePerson = lngPersonID End Function Listing 16.45: Diese Methode entscheidet, ob ein Objekt in der Datenbank gespeichert oder nur aktualisiert werden soll
16.3.17 Neuen Datensatz anlegen Die Funktion CreatePerson gibt die Daten des anzulegenden Objekts an die Methode Create des Datenzugriffsobjekts weiter. Dies geschieht in der Form, dass zunächst ein Personen-Objekt mit den Eigenschaften der Person erstellt und dieses dann an das Da tenzugriffsobjekt übergeben wird. Public Function CreatePerson(strVorname As String, _ strNachname As String, strStrasse As String, strPLZ As String, _ strOrt As String) As Long Dim objPerson As clsPerson Set objPerson = New clsPerson With objPerson .Vorname = strVorname .Nachname = strNachname .Strasse = strStrasse .PLZ = strPLZ .Ort = strOrt End With If objPersonDAO.Create(objPerson) = True Then CreatePerson = objPerson.PersonID Else MsgBox "Die Person konnte nicht angelegt werden.", _ vbOKOnly + vbExclamation, "Fehler beim Anlegen von Daten" End If Set objPerson = Nothing End Function Listing 16.46: Die Funktion CreatePerson des Controller-Objekts erwartet die zu speichernden Eigenschaften des Person-Objekts als Parameter
Die Create-Methode kümmert sich nun um das Anlegen des Datensatzes in der Tabelle tblPersonen. Dabei wird neben dem Anlegen des Datensatzes auch die Eigenschaft Per
861
Kapitel 16
sonID mit dem in der Tabelle angelegten Wert gefüllt. Wenn das Anlegen erfolgreich war, liefert die Methode den Wert True zurück. Die Methode aus Listing 16.46 kann dann aus dem per Referenz übergebenen Objekt den neuen Wert der Eigenschaft PersonID auslesen. Public Function Create(objPerson As clsPerson) As Long On Error GoTo Create_Err Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb Set rst = db.OpenRecordset _ ("SELECT * FROM tblPersonen", dbOpenDynaset) With objPerson rst.AddNew rst![Vorname] = .Vorname rst![Nachname] = .Nachname rst![Strasse] = .Strasse rst![PLZ] = .PLZ rst![Ort] = .Ort .PersonID = rst![PersonID] rst.Update End With Create = True Create_Exit: On Error Resume Next Set rst = Nothing Set db = Nothing Exit Function Create_Err: Create = False GoTo Create_Exit End Function Listing 16.47: Die Create-Methode des Datenzugriffsobjekts legt einen neuen Datensatz auf Basis des übergebenen Objekts an
16.3.18 Aktualisieren eines Datensatzes Das Aktualisieren bestehender Datensätze erfolgt analog. Diesmal ruft die Methode SavePerson die Funktion UpdatePerson auf, wobei im Vergleich zum Anlegen des Datensatzes der Wert der Eigenschaft PersonID mit übergeben wird.
862
Objektorientierung im Praxiseinsatz Private Function UpdatePerson(lngPersonID As Long, strVorname As String, _ strNachname As String, strStrasse As String, strPLZ As String, _ strOrt As String) Dim objPerson As clsPerson Set objPerson = New clsPerson With objPerson .PersonID = lngPersonID .Vorname = strVorname .Nachname = strNachname .Strasse = strStrasse .PLZ = strPLZ .Ort = strOrt End With If Not objPersonDAO.Update(objPerson) = True Then MsgBox "Die Person konnte nicht aktualisiert werden.", _ vbOKOnly + vbExclamation, "Fehler beim Aktualisieren" End If End Function Listing 16.48: Vorbereitung der Aktualisierung eines Datensatzes im Controller-Objekt
Die Methode Update des Datenzugriffsobjekts öffnet eine Datensatzgruppe, die lediglich einen Datensatz enthält – den mit der übergebenen PersonID. Public Function Update(objPerson As clsPerson) As boolean On Error GoTo Update_Err Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb Set rst = db.OpenRecordset _ ("SELECT * FROM tblPersonen WHERE [PersonID] = " _ & objPerson.PersonID, dbOpenDynaset) With objPerson rst.Edit rst![Vorname] = .Vorname rst![Nachname] = .Nachname rst![Strasse] = .Strasse rst![PLZ] = .PLZ rst![Ort] = .Ort rst.Update End With
863
Kapitel 16 Update = True Update_Exit: On Error Resume Next Set rst = Nothing Set db = Nothing Exit Function Update_Err: Update = False GoTo Update_Exit End Function Listing 16.49: Aktualisieren eines Datensatzes auf Basis des passenden Objekts
16.3.19 Löschen eines Datensatzes Um den aktuell im Formular angezeigten Datensatz zu löschen, klicken Sie auf die Löschen-Schaltfläche. Diese ruft wie gehabt die Business-Schicht auf und übergibt den Wert des Feldes PersonID an die dortige Methode DeletePerson. Nach erfolgreichem Löschvorgang leert die Routine das Formular, aktualisiert das Kombinationsfeld zur Schnellsuche und leert auch dieses. Private Sub cmdLoeschen_Click() If objController.DeletePerson(Me!txtPersonID) = True Then FormularLeeren cboSchnellsucheAktualisieren Me!cboSchnellsuche = Null End If End Sub Listing 16.50: Starten des Löschvorgangs
Die Methode DeletePerson des Controller-Objekts reicht die ID des zu löschenden Daten satzes direkt an die Methode Delete der Datenzugriffsklasse weiter. Public Function DeletePerson(lngPersonID As Long) As Boolean If objPersonDAO.Delete(lngPersonID) = True Then DeletePerson = True Else DeletePerson = False MsgBox "Die Person konnte nicht gelöscht werden.", _ vbOKOnly + vbExclamation, "Fehler beim Löschen" End If End Function Listing 16.51: Von der Business-Schicht zur Datenzugriffsschicht: Löschen einer Person
864
Objektorientierung im Praxiseinsatz
Diese löscht den Datensatz mit einem DELETE-Statement und gibt bei Gelingen den Wert True zurück. Public Function Delete(PersonID As Long) As Boolean On Error GoTo Delete_Err Dim db As DAO.Database Set db = CurrentDb db.Execute "DELETE FROM tblPersonen WHERE [PersonID] = " & PersonID, _ dbFailOnError Delete = True Delete_Exit: On Error Resume Next Set db = Nothing Exit Function Delete_Err: Delete = False GoTo Delete_Exit End Function Listing 16.52: Entfernen eines Datensatzes aus der Tabelle tblPersonen
16.3.20 Businesslogik und mehr Dieses Beispiel zeigt, wie Sie zwei Pattern der objektorientierten Welt mit VBA und Access einsetzen – das Model View Controller-Pattern und das DAO-Pattern (wobei DAO hier auch Data Access Objects bedeutet, aber nichts mit der DAO-Bibliothek von Access zu tun hat) – diese Stichwörter nur für diejenigen, die sich genauer mit der Materie auseinandersetzen möchten. Was bringt das Ganze nun? Immerhin geht hier eine Menge Code für eine Aufgabe drauf, die sonst mit wenigen Zeilen zu lösen wäre. Dafür erhalten Sie aber auch mehr Flexibilität und Übersicht. Sie können die Businessregeln komplett in der BusinessSchicht versenken, was sich natürlich erst dann auszahlt, wenn Sie nicht nur mit einem, sondern mit mehreren Objekten, einer umfangreicheren Benutzeroberfläche und einem dementsprechenden Datenmodell arbeiten. Da die Datenschicht mehr einzelne Datenzugriffe auf die Backend-Datei erfordert als sonst üblich – zahlreiches Öffnen von Recordsets, häufiges Instanzieren von CurrentDB –, wird bei hohem Datenaufkommen und Mehrbenutzerumgebung auch die Performance der Anwendung gegenüber den gewohnten Verfahren leiden. Deshalb eignen sich mehrschichtige Anwendungen ins-
865
Kapitel 16
besondere für Access-Frontends, die mit einem SQL-Server-Backend zusammenarbeiten. Die Validierung der Daten können Sie je nach Anforderung in die GUI-Schicht verfrachten oder in die Business-Schicht integrieren. Wenn Werte direkt nach der Eingabe in ein Textfeld auf ihre Gültigkeit geprüft werden sollen, macht eine entsprechende Validierung in der GUI-Schicht sicher Sinn. Geschäftsregeln, die sich auf einen größeren Zusammenhang beziehen und gegebenenfalls mehrere Projekte betreffen, lassen sich bestens in der Business-Schicht unterbringen.
16.3.21 Objektklassen und Datenzugriffsobjekte automatisch erstellen Die objektorientierte Entwicklung in der Form, wie Sie in den vorhergehenden Abschnitten dargestellt wurde, erfordert natürlich eine Menge Code-Einsatz. Eines dürfen Sie aber dabei nicht vergessen: Ein großer Teil ist reine Fleißarbeit. Während die vorgestellten Routinen alle einen recht individuellen Eindruck machten, benötigen Sie für eine eventuelle weitere Klasse zum Verwalten anderer Objekte wie etwa Unternehmen nur noch ganz wenig neuen Code. Die Datenzugriffsklassen clsPersonDAO_DAO und die Objektklasse clsPerson etwa enthalten nur Code, der automatisch auf Basis der entsprechenden Tabelle tblPersonen erstellt wurde. Damit Sie beim Ausbauen des vorliegenden Beispiels oder beim Umsetzen auf eine eigene Datenbankanwendung keine wunden Finger bekommen, finden Sie auf der Buch-CD ein Tool, das einige Funktionen zum automatischen Generieren von Objekt- und Datenzugriffsklassen zur Verfügung stellt (»accessVBATools«, siehe weiter unten). Interessant sind in diesem Zusammenhang die beiden Funktionen Objekt aus Tabelle erstellen und DAO-Objekt für Tabelle erstellen (siehe Abbildung 16.10).
Abbildung 16.10: Funktionen zum Erstellen von Tabellen-Objekten und DAO-Objekten
866
Objektorientierung im Praxiseinsatz
Die erste der beiden Funktionen legt ein Objekt für eine in einem Dialog festgelegte Tabelle an. Dazu wählen Sie im Dialog aus Abbildung 16.11 die Tabelle aus, zu der eine Klasse erstellt werden soll, und fügen das Primärschlüsselfeld dieser Tabelle sowie den Objektnamen ein. Der Objektname sollte im Singular stehen und das Objekt bestmöglich umschreiben. Er wird unter anderem zusammen mit dem Präfix »cls« als Klassenname verwendet (hier beispielsweise clsPerson).
Abbildung 16.11: Festlegen der Parameter zum Erstellen einer Tabellen-Klasse
Die zweite Funktion DAO-Objekt für Tabelle erstellen erwartet die gleichen Parameter und legt eine Datenzugriffsklasse an, die per DAO auf eine Access-Tabelle zugreift. Die Klasse enthält folgende Methoden (in Klammern das entsprechende Listing aus obigem Beispiel): Create (siehe Listing 16.47) Delete (siehe Listing 16.52) Find (siehe Listing 16.37) Read (siehe Listing 16.41) Update (siehe Listing 16.49) Das Tool liegt auf der Buch-CD in Form einer .dll-Datei unter dem Dateinamen Tools\ accessVBATools.dll vor. Diese .dll-Datei kopieren Sie in ein Verzeichnis Ihrer Wahl (vorzugsweise c:\Windows\System32) und registrieren diese über den Ausführen…-Dialog mit der Anweisung regsvr32.exe c:\Windows\System32\accessVBATools.dll. Anschließend öffnen Sie die VBA-Entwicklungsumgebung neu und finden die neue Symbolleiste sowie die Einträge im Kontextmenü vor. Eine aktuelle Version der DLL erhalten Sie unter http://www.access-entwicklerbuch.de.
867
17 Anpassen der Entwicklungsumgebung Die VBA-Entwicklungsoberfläche enthält eine Reihe Ele mente, von denen das wichtigste zweifellos das Codefens ter ist, das der Anzeige und Bearbeitung des in den Mo dulen enthaltenen VBA-Codes dient. Jedes geöffnete Modul wird in einem eigenen Codefenster angezeigt. Neben dem Codefenster gibt es noch ein weiteres »reguläres« Fenster – den Objektkatalog. Die »Steuerzentrale« des VBA-Editors befindet sich wie in Windows-Anwendungen üblich in den Menü- und Symbolleisten. Die dritte Gruppe der Bedienelemente der VBA-Entwick lungsumgebung sind die so genannten Toolwindows. Das sind Fenster, die sich am rechten, linken, oberen oder un teren Rand des Hauptfensters oder an anderen bereits vorhandenen Bedienelementen »andocken« lassen oder einfach frei im Hauptfenster »schweben«. Beispiele für oft verwendete Toolwindows sind der Projektbrowser und der Direktbereich. Abbildung 17.1 zeigt die VBA-Entwick lungsumgebung mit den genannten Bedienelementen. Die VBA-Entwicklungsumgebung scheint für die meisten Anwendungsfälle ausreichend zu sein, aber wenn man sich die Möglichkeiten von Entwicklungsumgebungen wie Eclipse oder Microsoft Visual Studio .NET vor Augen führt, wird man schnell neidisch wegen der Vielfalt der verfügbaren und leicht integrierbaren Erweiterungen – das gilt vor allem für die aus dem Java-Umfeld stammende Eclipse-Plattform.
Kapitel 17
Abbildung 17.1: Die VBA-Entwicklungsumgebung mit dem Projektbrowser, dem Codefenster und dem Direktbereich
Wenn Sie eine dieser Entwicklungsumgebungen und die eine oder andere Erweiterung für die VBA-Entwicklungsumgebung herbeisehnen oder einfach eine zündende Idee für ihren Ausbau haben, gibt es gute Nachrichten: Sie können – das richtige Werkzeug und die technischen Fähigkeiten vorausgesetzt – selbst für die gewünschten Funktionen sorgen und sowohl die Menüs erweitern als auch eigene Toolwindows hinzufügen. Beispiele für bestehende Erweiterungen sind etwa der Prozedurbrowser von Sascha Trowitzsch (auf der Buch-CD im Verzeichnis \Kap_17\Prozedurbrowser.zip, aktuelle Ver sion unter http://www.access-im-unternehmen.de/374.0.html) oder eine Menüleiste mit Funk tionen zum Hinzufügen und Entfernen der Zeilennummerierung im aktuellen Modul und zum Hinzufügen einer Fehlerbehandlung zur aktuell markierten Routine. Abbildung 17.2 zeigt die VBA-Entwicklungsumgebung mit den beiden Erweiterungen. Der Prozedurbrowser ist gerade bei der Arbeit mit Modulen mit vielen Codezeilen hilfreich, denn er stellt alle enthaltenen Deklarationen, Funktionen und Sub-Prozeduren übersichtlich dar. Per Mausklick auf den gewünschten Eintrag im Prozedurbrowser zeigt das Codefenster die entsprechende Stelle an. Außerdem gibt es zu jedem Eintrag ein Kontextmenü mit Funktionen zum Ausführen, Kopieren oder Löschen einer Prozedur und einige weitere Optionen.
870
Anpassen der Entwicklungsumgebung
Abbildung 17.2: VBA-Entwicklungsumgebung mit benutzerdefinierten Erweiterungen
In Abschnitt 17.8, »Toolwindows«, und 17.9, »COM-Add-Ins per Menübefehl aufrufen«, finden Sie detaillierte Informationen zum Erstellen benutzerdefinierter Toolwindows und Menüleisten für die VBA-Entwicklungsumgebung. Außerdem erfahren Sie dort, wie Sie die Erweiterungen in die Entwicklungsumgebung integrieren.
Zusätzlich benötigte Software: Microsoft Visual Studio 6.0 Für die Entwicklung der nachfolgend beschriebenen Toolwindows und MenüleistenTools benötigen Sie Microsoft Visual Studio 6.0. Auch wenn Sie nicht über dieses Werkzeug verfügen, können Sie die nachfolgend beschriebenen Tools bei der Arbeit mit der VBAEntwicklungsumgebung verwenden – sie liegen fertig kompiliert auf der Buch-CD vor. Sie können auch ein alternatives Werkzeug wie die Visual Studio .NET 2005 Express Edition, eine der aktuellen Ausgaben der Office-Entwickler-Versionen oder jede andere Entwicklungsplattform verwenden, die mit OLE-Schnittstellen umgehen kann. Da für die Verwendung eines .NET-Toolwindows aber das .NET-Framework auf allen Zielrechnern installiert sein muss, was zum Zeitpunkt der Drucklegung dieses
871
Kapitel 17
Buchs noch nicht gewährleistet ist, beschränken sich die folgenden Abschnitte auf die Erstellung mit dem »klassischen« Visual Basic.
17.1 Gründe für die Erweiterung der Entwicklungsumgebung Möglicherweise reicht die VBA-Entwicklungsumgebung in der derzeitigen Form für Sie völlig aus. Das kann eigentlich nur zwei Gründe haben: Entweder Sie beschäftigen sich so wenig mit diesem Werkzeug, dass es für Ihre Ansprüche ausreicht, oder Sie haben vielleicht noch keine Anregungen gefunden, die sich positiv auf Ihre Arbeit auswirken könnten. Wenn Sie die vorhergehenden Kapitel zum Thema Objektorientie rung gelesen haben, ist Ihnen aufgefallen, dass die dort beschriebenen Vorteile auch Mehrarbeit erfordern (zumindest im ersten Schritt). Wenn Sie nicht mehr direkt mit gebundenen Formularen oder Berichten auf die gewünschten Daten zugreifen, bedeutet der notwendige Code für die einzufügenden Schichten natürlich zusätzliche Arbeit.
17.1.1 Automatische Codegenerierung Diese zusätzliche Arbeit ist in den meisten Fällen allerdings reine Fleißarbeit. Das Rad werden Sie dabei vermutlich nicht neu erfinden müssen. Die Objektklassen zu den in Tabellen gespeicherten Daten sowie die Klassen, die die Methoden und Eigenschaf ten für die Übertragung der Daten zwischen Objektklassen und Tabellen (die Daten zugriffsobjekte) bereitstellen, sind in der Regel nach dem gleichen System aufgebaut. Der Unterschied liegt lediglich in den Namen der betroffenen Tabelle und der enthaltenen Felder. Damit haben Sie – vorausgesetzt Sie möchten die objektorientierten Ent wicklungstechniken einsetzen – bereits einen interessanten Anwendungsfall für eine Erweiterung der Entwicklungsumgebung gefunden: eine Funktion, die für eine angegebene Tabelle eine Objektklasse und/oder eine entsprechende Datenzugriffsklasse erstellt. Dieser Funktion müssten Sie auf geeignete Weise den Namen der Tabelle angeben, für die entsprechende Klassen erstellt werden sollen. Außerdem müssten Sie einige Parameter vorsehen, mit denen man etwa einstellen kann, ob eine Objekteigenschaft lesbar und/oder schreibbar sein soll.
17.1.2 Fehlerbehandlung per Knopfdruck Es gibt aber auch Beispiele für Vereinfachungen bei der Quellcodeerstellung, die Sie verwenden können, wenn Sie die prozedurale Entwicklung bevorzugen (natürlich lässt sich folgendes Beispiel auch in Klassen einsetzen): Jede Prozedur sollte eine Fehlerbe handlung enthalten. Diese ist immer wie im folgenden Beispiel aufgebaut:
872
Anpassen der Entwicklungsumgebung Public Function () On Error GoTo Beispielfunktion_Err _Exit: 'Restarbeiten Exit Function _Err: 'Fehlerbehandlung Call Fehlerbehandlung("<Modulname>", "", Erl, _ "Bemerkungen: ./.") Resume _Exit End Function Listing 17.1: Aufbau einer Fehlerbehandlung
Mit wachsender Anzahl Prozeduren wird die manuelle Erstellung von Fehlerbehand lungsroutinen mitunter etwas nervig. Wie schön wäre es doch, wenn man die entsprechenden Zeilen einfach per Mausklick hinzufügen könnte! Natürlich lässt sich das be werkstelligen. In Abbildung 17.3 sehen Sie eine zusätzliche Symbolleiste, die unter ande rem einen Befehl namens Fehlerbehandlung hinzufügen enthält.
Abbildung 17.3: Zusatzfunktionen in der VBA-Entwicklungsumgebung
Um die Funktion zu verwenden, platzieren Sie einfach die Einfügemarke innerhalb der Zielprozedur und klicken auf den entsprechenden Menüeintrag. Die Funktion ermittelt den Namen des aktuellen Moduls und der Prozedur, in der sich die Einfügemarke befindet, und fügt die der Prozedur angepasste Fehlerbehandlung hinzu.
873
Kapitel 17
Die Fehlerbehandlung ruft eine globale Fehlerbehandlungsroutine auf und übergibt einige Parameter wie den Modulnamen, den Prozedurnamen, die Funktion Erl, die – falls vorhanden – die Nummer der fehlerhaften Zeile enthält, und eventuell notwendige Be merkungen. Die Fehlerbehandlungsroutine könnte dann beispielsweise eine aussagekräftige Fehler meldung ausgeben oder auch die Fehlermeldung in einer Datei speichern, die von den Benutzern der Anwendung zur Auswertung an den Entwickler weitergeleitet werden kann. Unter http://www.access-entwicklerbuch.de finden Sie die aktuelle Version der accessVBATools zum Download. Der Download enthält die Datei accessVBATools.dll, die alle in Abbildung 17.3 gezeigten Funktionen beinhaltet. Sie können diese .dll-Datei ganz einfach in ein beliebiges Verzeichnis kopieren (am besten in c:\Windows\System32) und mit der Anwendung regsvr32.exe unter Angabe des Dateinamens registrieren. Anschließend können Sie die Funktionen nach einem Neustart von Access in der VBA-Entwicklungs umgebung einsetzen.
17.1.3 Nummerieren von Codezeilen Sie haben soeben richtig gelesen: Die Funktion Erl ist eine nicht dokumentierte Funktion von VBA, die beim Auftreten eines Fehlers die Nummer der betroffenen Zeile zurückgibt. Möglicherweise fühlen Sie sich nun in die guten alten C64-Zeiten zurückversetzt, als Zeilennummern wesentlicher Bestandteil eines Basic-Programms waren. Sie können aber tatsächlich vor fast jede Zeile einer Prozedur Nummern setzen. Sie können dann beim Auftreten eines Fehlers über die Funktion Erl die entsprechende Zeilennummer ermitteln. Der Nutzen dieser Funktion ist phänomenal: Setzt man sie konsequent ein, sind die Zeiten vorbei, da die armen Benutzer Ihrer Anwendungen bei jeder Fehlermeldung erst einmal Screenshots vom Debug-Fenster und von der Fehlermeldung erstellen mussten, damit der Entwickler Informationen über die Herkunft des Fehlers erhielt. Die Frage, wie man denn nun schnell mal mehrere hundert, tausend oder mehr Zeilen Code durchnummeriert, beantwortet sich praktisch von selbst: Natürlich mit einer Erweiterung der Entwicklungsumgebung. Diese kann auf Knopfdruck die Prozeduren des aktuellen Moduls durchnummerieren und die Nummerierung ebenso schnell wieder entfernen.
17.2 Programmieren der Entwicklungsumgebung Sie sehen, dass sich sehr schnell sinnvolle Anwendungen zur Erweiterung der Entwick lungsumgebung finden lassen. Erwartungsgemäß befassen sich alle genannten Erwei terungsmöglichkeiten mit dem Manipulieren von Modulen und Quellcode. Das ist na-
874
Anpassen der Entwicklungsumgebung
türlich kein Zufall, denn Tools zum Erstellen oder Bearbeiten von Tabellen, Abfragen, Berichten oder Formularen gehören zweifellos zur Benutzeroberfläche von Access und sind dort in Form geeigneter Add-Ins zu integrieren. Die Erweiterung der Entwicklungsumgebung steht nicht gerade im Mittelpunkt des Interesses der Anwendungsentwickler, da kurze Entwicklungszeiten in der Regel keine Zeit lassen, Tools zu erstellen, die dem Entwickler regelmäßig anfallende Aufgaben abnehmen. Das ist zwar ein gutes Argument, aber wenn man immer wieder manuell die gleichen Schritte durchführt, ist zu überlegen, ob die Erstellung eines entsprechenden Tools nicht auf Dauer viel Zeit spart. Nun sind aber die zur Erstellung von Tools für die Entwicklungsumgebung benötigten Grundlagen im Internet nicht gerade leicht zu finden und wenn man sich nicht mit englischsprachigen Quellen auseinandersetzen möchte, wird es noch schwieriger. Deshalb soll dieses Thema im vorliegenden Entwicklerhandbuch etwas ausführlicher besprochen werden. Die Entwicklung eines Tools, wie das in Abbildung 17.2 abgebildete Toolwindow oder die ebenfalls in dieser Abbildung gezeigte Symbolleiste zum Anlegen von Fehlerbehand lungen, erfolgt in zwei Schritten: Entwickeln der eigentlichen Funktionalität zum Manipulieren von Modulen und ihres Inhalts Erstellen einer Benutzerschnittstelle zum Aufrufen der Funktionalität, etwa per Toolwindow oder in Form einer speziellen Symbolleiste Sie finden die benötigten Grundlagen in dieser Reihenfolge in den folgenden Abschnitten. Der Abschnitt 17.3, »Das Objektmodell der VBA-Entwicklungsumgebung«, behandelt Objekte, Methoden und Eigenschaften des Objektmodells der Benutzeroberfläche. Sie finden dort die Grundlagen, um Module und Quellcode zu manipulieren. Die weiteren Abschnitte stellen Möglichkeiten vor, um die Funktionalität verfügbar zu machen. In Abschnitt 17.8, »Toolwindows«, erfahren Sie, wie Sie eigene Formulare erstellen, die Sie in die VBA-Entwicklungsumgebung integrieren können. Damit können Sie Steuerelemente wie Textfelder, Kombinations- oder Listenfelder und Schaltflächen zur Verfügung stellen, um für das Durchführen der gewünschten Funktionalität benötigte Parameter anzugeben oder um bestimmte Informationen über das aktuelle Projekt und seine Objekte anzuzeigen. Für einfachere Funktionen, die der Benutzer ohne weitere Informationen einfach per Knopfdruck starten können soll, reicht normalerweise die Bereitstellung einfacher Schaltflächen in speziellen Symbolleisten aus. Mehr dazu erfahren Sie in Abschnitt 17.9, »COM-Add-Ins per Menübefehl aufrufen«. Die Beispiele zu diesem Kapitel finden Sie auf der Buch-CD in der Datenbank \Kap_17\VBAIDE.accdb.
875
Kapitel 17
17.3 Das Objektmodell der VBA-Entwicklungsumgebung Für die Programmierung der VBA-Entwicklungsumgebung steht eine eigene Bibliothek mit entsprechendem Objektmodell für den Zugriff auf die enthaltenen Objekte bereit. Die enthaltenen Objekte und ihre Methoden und Eigenschaften lassen sich grob in zwei Bereiche gliedern: Die erste Kategorie dient der Programmierung der Entwicklungsumgebung selbst. Mit den enthaltenen Objekten, Methoden und Eigenschaften lassen sich beispielsweise die Menüleisten, Fenster, Verweise oder Add-Ins steuern. Einige Elemente dieser Kategorie lernen Sie später kennen, wenn es um das Anpassen der VBA-Ent wicklungsumgebung geht. In der zweiten Kategorie finden Sie Objekte, Methoden und Eigenschaften zum Manipulieren der Module und des darin enthaltenen Quellcodes. In den folgenden Abschnitten erfahren Sie, wie Sie die enthaltenen Elemente für die Automatisierung oft wiederkehrender Arbeitsschritte einsetzen.
17.3.1 Verweis für den Zugriff auf das VBE-Objektmodell einrichten Um die enthaltenen Objekte verwenden zu können, müssen Sie zunächst einen Verweis auf die Objektbibliothek Visual Basic for Applications Extensibility 5.3 einrichten. Dazu ver wenden Sie wie üblich den Dialog Verweise (siehe Abbildung 17.4).
Abbildung 17.4: Einrichten eines Verweises auf die VBA-IDE-Objektbibliothek
876
Anpassen der Entwicklungsumgebung
Um ein wenig in den Objekten dieser Bibliothek zu stöbern, nehmen Sie den Objektkatalog aus Abbildung 17.5 zu Hilfe (anzuzeigen mit F2). Im Kontextmenü der einzelnen Einträge finden Sie die Möglichkeit, die Onlinehilfe zu einem Objekt anzuzeigen. Dieses erreichen Sie durch Markieren des gewünschten Eintrags und Betätigen von F1. Die Onlinehilfe hilft weiter, wenn Sie detaillierte Informationen zu einzelnen Elementen der Entwicklungsoberfläche benötigen. Im Folgenden finden Sie daher keine Auflistung der Objekte, Eigenschaften und Methoden der Entwicklungsoberfläche, sondern lernen anhand einiger Beispiele ihren Einsatz kennen.
Abbildung 17.5: Durchstöbern der Objektbibliothek mit dem Objektkatalog
17.3.2 Aufbau des Objektmodells Zum Nachvollziehen der folgenden Beispiele werden Kenntnisse in den Grundzügen des Objektmodells vorausgesetzt. Daher finden Sie nachfolgend eine kurze Zusammen fassung der wichtigsten Elemente. Ganz oben in der Hierarchie steht die VBE-Klasse (Visual Basic Environment). Sie enthält die folgenden für die nachfolgenden Beispiele relevanten Elemente: ActiveCodePane: Verweis auf das aktuelle oder zuletzt verwendete Code-Fenster ActiveVBProject: Verweis auf das aktuelle VBA-Projekt ActiveWindow: Verweis auf das aktive Fenster in der Entwicklungsumgebung Addins: Auflistung aller geladenen Add-Ins
877
Kapitel 17
CodePanes: Auflistung aller geöffneten Code-Fenster CommandBars: Auflistung aller verfügbaren Menüleisten (einschließlich der durch COM-Add-Ins bereitgestellten Menüleisten) SelectedVBComponent: Enthält einen Verweis auf die im Projekt-Explorer enthaltene Komponente (das sind alle Objekte, die sich in den Ordnern Microsoft Office Klassen objekte, Module, Klassenmodule, Formulare und Verweise befinden). VBProjects: Auflistung aller geöffneten Projekte. Normalerweise enthält eine AccessAnwendung nur ein einziges Projekt. Wenn Sie aber etwa eine andere Datenbank per Verweis einbinden, enthält VBProjects auch dieses Projekt. Sie können mit dieser Auflistung auf alle Projekte zugreifen, die auch der Projekt-Explorer anzeigt (siehe Abbildung 17.6). Version: Gibt die VBA-Version an. Windows: Auflistung aller Window-Objekte. Enthält die sichtbaren Code-Fenster sowie alle verfügbaren Toolwindows – das bezieht sich nicht auf die sichtbaren, sondern auf die eingebauten dockenden Fenster und die geladenen COM-Add-Ins, die als dockende Fenster ausgeführt sind.
Abbildung 17.6: Projekt-Explorer mit mehreren Projekten
17.4 Mit Modulen arbeiten Was man unter Access Standardmodul, Klassenmodul oder Formular-/Berichtsmodul nennt, fasst das VBE-Objektmodell unter VBComponents zusammen. Das VBE-Objektmo dell unterscheidet die für Access interessanten Arten im Wesentlichen durch die Eigen schaft Type. Die Liste der Module lässt sich ausgeben und – was noch interessanter ist – es lassen sich auch neue Elemente hinzufügen.
878
Anpassen der Entwicklungsumgebung
17.4.1 Auflisten aller enthaltenen Module Einen Überblick über die Module eines Projekts bietet zum Beispiel die Möglichkeit, das gewünschte Modul in der VBA-Entwicklungsumgebung anzuzeigen oder zu löschen. Die folgende Prozedur gibt eine Liste aller Module mit Modulnamen und Typ des aktuellen Projekts im Testfenster aus (siehe Abbildung 17.7). Dazu ermittelt sie zunächst die Anzahl der Module über die Count-Eigenschaft der Auflistung VBComponents für das betreffende Projekt. In einer For Next-Schleife durchläuft die Prozedur dann alle Elemente der Auflistung und ermittelt den Namen und den Typ des Moduls. Letzterer wird durch einen der jeweiligen Konstanten entsprechenden Text repräsentiert. Public Sub ListAllModules() Dim Dim Dim Dim Dim
i As Integer intVBComponentsCount As Integer objVBComponent As VBComponent strModulename As String strModuletype As String
intVBComponentsCount = VBE.ActiveVBProject.VBComponents.Count For i = 1 To intVBComponentsCount Set objVBComponent = VBE.ActiveVBProject.VBComponents.Item(i) strModulename = objVBComponent.Name Select Case objVBComponent.Type Case vbext_ct_StdModule strModuletype = "Standardmodul" Case vbext_ct_ClassModule strModuletype = "Klassenmodul" Case vbext_ct_Document strModuletype = "Formular- oder Berichtsmodul" Case vbext_ct_MSForm strModuletype = "MSForms Userform-Modul" End Select Debug.Print strModulename, strModuletype Next i Set objVBComponent = Nothing End Sub Listing 17.2: Prozedur zur Ausgabe aller Module eines Projekts
879
Kapitel 17
Abbildung 17.7: Ausgabe aller Module im Testfenster
17.4.2 Anlegen eines neuen Moduls Die automatisierte Quellcode-Erstellung umfasst natürlich auch das Anlegen der entsprechenden Module. Die Prozedur aus Abbildung 17.8 fügt der Auflistung der vorhan denen VBComponents ein weiteres Element des Typs Klassenmodul hinzu. Bei der Anga be der kryptischen Konstanten hilft IntelliSense. Die Konstante vbext_ct_Document ent spricht übrigens einem Formular- oder Berichtsmodul.
Abbildung 17.8: Anlegen der Prozedur zum Erstellen einer neuen Klasse
Die Ausgabe der Eigenschaften zeigt, dass genau die gleiche Vorgehensweise zum Er stellen des neuen Moduls angewendet wird: Als Modulname kommt »Klasse1« zum Zu ge (sofern noch nicht vergeben) und … halt: Die Eigenschaft Saved zeigt den Wert True an. Normalerweise muss man neu erstellte Module doch erst noch speichern! Ein Blick auf die Definition dieser Eigenschaft klärt die Sache auf: Der Wert True bedeutet, dass das Objekt seit dem letzten Speichern nicht mehr geändert wurde. Nehmen Sie also mit folgender Anweisung im Testfenster eine Änderung – beispielsweise des Modulnamens – vor: VBE.ActiveVBProject.VBComponents("Klasse1").Name = "clsBeispielklasse"
880
Anpassen der Entwicklungsumgebung
Im Projekt-Explorer können Sie beobachten, dass diese Anweisung das gewünschte Ergebnis bringt. Prüfen Sie nun erneut die Eigenschaft Saved: Debug.Print VBE.ActiveVBProject.VBComponents("clsBeispielklasse").Saved Falsch
Das bedeutet, dass das Modul seit dem letzten Speichern bearbeitet wurde. Eine Methode zum Speichern stellt das Objektmodell nicht zur Verfügung; hier schafft die Save-Methode des DoCmd-Objekts Abhilfe: DoCmd.Save acModule, "clsBeispielklasse"
Beachten Sie, dass das DoCmd-Objekt Bestandteil der Access-Bibliothek ist und Sie einen Verweis auf die Bibliothek Microsoft Access x.y Object Library anlegen müssen, wenn Sie das Objekt etwa in einem COM-Add-In verwenden möchten.
17.4.3 Entfernen eines Moduls Das Entfernen eines Moduls erfolgt über die Remove-Methode der VBComponents-Auflis tung. Ihr übergeben Sie eine Objektvariable mit einem Verweis auf das zu entfernende Modul. Die Prozedur des folgenden Beispiels erwartet den Namen der Prozedur als String-Variable und ermittelt damit das entsprechende Objekt. Public Sub RemoveModule(strModulename As String) Dim objVBComponent As VBComponent Set objVBComponent = _ VBE.ActiveVBProject.VBComponents.Item(strModulename) VBE.ActiveVBProject.VBComponents.Remove objVBComponent Set objVBComponent = Nothing End Sub Listing 17.3: Prozedur zum Entfernen eines Moduls
17.5 Mit Prozeduren arbeiten Mit den Methoden und Eigenschaften des CodeModule-Objekts, das den Zugriff auf den Inhalt eines Moduls erlaubt, erhalten Sie eine Vielfalt von Möglichkeiten. Ihre Beschreibung wird daher in den lesenden und den schreibenden Zugriff gegliedert (siehe Abschnitt 17.5.1, »Lesender Zugriff auf den Quellcode«, und 17.7, »Manipulieren des Quellcodes«). Zusätzlich finden Sie zu jedem Bereich ein Beispiel (siehe Abschnitt 17.6,
881
Kapitel 17
»Beispielanwendung: Codeviewer«, und 17.7.4, »Beispielanwendung: Nummerieren von Codezeilen in einem Modul«).
17.5.1 Lesender Zugriff auf den Quellcode Für den Zugriff auf den Code eines Moduls benötigen Sie einen Verweis auf das betreffende Modul. Dazu gibt es mehrere Möglichkeiten: Sie kennen den Namen des Moduls, dessen Code Sie manipulieren möchten. In diesem Fall können Sie über die bereits in Zusammenhang mit den obigen Beispielpro zeduren vorgestellte VBComponents-Auflistung das Modul referenzieren und über das CodeModule-Objekt auf den enthaltenen Code zugreifen. Das ist zum Beispiel beim automatisierten Anlegen neuer Klassen und dem anschließenden Hinzufügen von Quellcode sinnvoll. Sie möchten den Code im aktiven Fenster manipulieren. Dann verwenden Sie einfach die CodeModule-Eigenschaft des ActiveCodePane-Objekts der VBE. Damit können Sie beispielsweise Quellcode an der Stelle der Einfügemarke hinzufügen oder einen markierten Bereich des Quellcodes kopieren. Auf welche der beiden Arten Sie einen Verweis auf das zu manipulierende Modul erstellen, ist für die nachfolgend vorgestellten Beispiele unwichtig. Daher werden beide Möglichkeiten angewendet. Die folgenden Beschreibungen der Eigenschaften und Methoden des CodeModuleObjekts beziehen sich auf das Modul mdlZeilenZaehlen in Abbildung 17.9.
Abbildung 17.9: Beispielmodul für die Anwendung der Methoden und Eigenschaften des CodeModule-Objekts
882
Anpassen der Entwicklungsumgebung
17.5.2 Zählen der Codezeilen des Moduls Leider speichert die VBA-Entwicklungsumgebung die einzelnen Elemente wie Dekla rationen, Funktionen und Sub-Prozeduren nicht in einer Auflistung wie beispielsweise die Module. Es steht lediglich das komplette Modul zur Verfügung. Die gewünschten Inhalte müssen Sie selbst einkreisen. Dazu bietet das Objektmodell wiederum ausreichende Möglichkeiten. Die Eigenschaft CountOfLines gibt beispielsweise die Anzahl der Zeilen des Moduls zurück. Wenn Sie die folgende Anweisung im Direktbereich ausführen, erhalten Sie die Anzahl der Zeilen des aktuellen Moduls: Debug.Print VBE.ActiveCodePane.CodeModule.CountOfLines
Für das Beispielmodul aus Abbildung 17.9 gibt dieser Ausdruck den Wert 17 zurück. Manchmal befinden sich noch einige Leerzeilen hinter der letzten Prozedur, sodass die Funktion eine scheinbar größere Zeilenanzahl als vorhanden zurückgibt.
17.5.3 Zählen der Zeilen des Deklarationsbereichs eines Moduls Etwas differenzierter ist die Eigenschaft CountOfDeclarationLines. Sie gibt die Anzahl der Zeilen des Deklarationsbereichs des Moduls zurück. Dabei ist Folgendes zu beachten: Wenn das Modul lediglich Deklarationen und keine Prozeduren enthält, stimmt der Wert von CountOfDeclarationLines mit dem von CountOfLines überein. Wenn das Modul mindestens eine Prozedur enthält, gibt CountOfDeclarationLines die Anzahl der Zeilen bis zur letzten Deklarationszeile zurück. Die Ausgabe der Zeilenanzahl des Deklarationsbereichs für das aktuelle Modul sieht folgendermaßen aus und liefert den Wert 4 zurück: Debug.Print VBE.ActiveCodePane.CodeModule.CountOfDeclarationLines
17.5.4 Erste Zeile und Deklarationszeile einer Prozedur Die Entwickler des Objektmodells haben sich vermutlich die Frage gestellt, wie man mit den Leerzeilen zwischen zwei Prozeduren umgeht – gehören diese zu einer Prozedur? Und wenn ja – zu welcher? Gelöst haben sie das Problem auf die folgende Art: Die erste Zeile einer Prozedur ist die erste Zeile nach dem vorherigen Bereich, wobei mit Bereich der Deklarationsbereich eines Moduls oder die vorherige Prozedur gemeint ist. Wenn sich keine Elemente vor der ersten Prozedur befinden, ist die erste Zeile des Moduls gleichzeitig die erste Zeile der Prozedur.
883
Kapitel 17
Die Funktion ProcStartLine erwartet den Prozedurnamen und die dem Prozedurtyp entsprechende Konstante als Eingangsparameter und gibt die Nummer der ersten Zeile der Prozedur gemäß obiger Beschreibung zurück (in diesem Fall die 5): Debug.Print VBE.ActiveCodePane.CodeModule.ProcStartLine("SampleFunction",vb ext_pk_Proc)
Für den zweiten Parameter gibt es vier gültige Werte: vbext_pk_Proc: Sub- und Function-Prozeduren vbext_pk_Get, vbext_pk_Set, vbext_pk_Let: unterschiedliche Property-Prozeduren Nun sind die Zeilen zwischen zwei Prozeduren eigentlich nur interessant, wenn sich dort wichtige Kommentare befinden – und das auch nur, wenn Sie auf diese zugreifen möchten. In allen anderen Fällen beginnt der spannende Teil mit der Zeile der Proze durdeklaration (beispielsweise »Public Sub …«). Diese Zeile entlocken Sie dem CodeMo dule-Objekt mit der ProcBodyLine-Funktion, die genau die gleichen Parameter wie die ProcStartLine-Funktion erwartet: Debug.Print VBE.ActiveCodePane.CodeModule.ProcBodyLine("SampleFunction",vbe xt_pk_Proc)
17.5.5 Zeilenanzahl einer Prozedur Um die Anzahl Zeilen einer Prozedur zu ermitteln, verwenden Sie die Funktion Proc CountLines. Die Funktion erwartet die üblichen zwei Parameter. Bei der Anwendung der Funktion ist zu beachten, dass sie die Anzahl der Codezeilen von der ersten Zeile nach dem vorherigen Element (also der mit ProcStartLine zu ermittelnden Zeilennummer) bis zur letzten Zeile der Prozedur (also der Zeile mit dem Schlüsselwort »End«) zählt. Ein Beispielaufruf sieht folgendermaßen aus: Debug.Print VBE.ActiveCodePane.CodeModule.ProcCountLines("SampleFunction", vbext_pk_Proc)
17.5.6 Anzahl der Codezeilen einer Prozedur Wenn Sie die tatsächliche Anzahl Zeilen einer Prozedur von der Prozedurdeklaration bis zum End-Statement ermitteln möchten, gehen Sie folgendermaßen vor: Ermitteln Sie die Nummer der Zeile mit der Prozedurdeklaration: VBE.ActiveCodePane.CodeModule.ProcBodyLine("SampleFunction", _ vbext_pk_Proc)
Ermitteln Sie die Nummer der letzten Zeile der Prozedur:
Bilden Sie die Differenz der beiden Werte und addieren Sie den Wert 1 hinzu. Zusammengefasst heben sich die subtrahierte und die addierte 1 auf. Die Ermittlung der Codezeilen einer Prozedur lässt sich in einer Funktion zusammenfassen, die den Modulnamen, den Prozedurnamen sowie den Typ erwartet: Public Function GetRealProcLineCount(strModule As String, _ strProcName As String, lngProcType As Long) Dim objCodeModule As CodeModule Dim lngProcLineCount As Long Set objCodeModule = _ VBE.ActiveVBProject.VBComponents.Item(strModule).CodeModule With objCodeModule 'Berechnung der Zeilenanzahl lngProcLineCount = .ProcStartLine(strProcName, lngProcType) _ + .ProcCountLines(strProcName, lngProcType) _ - .ProcBodyLine(strProcName, lngProcType) End With GetRealProcLineCount = lngProcLineCount Set objCodeModule = Nothing End Function Listing 17.4: Ermitteln der Zeilen von der Deklarations- bis zur End-Zeile einer Prozedur
17.5.7 Zu welcher Prozedur gehört eine Zeile? Wenn Sie genau wissen möchten, welche Zeile eines Moduls zu welcher Prozedur gehört, probieren Sie einfach einmal folgende Prozedur aus. Die Prozedur erwartet als Parameter den Namen des zu untersuchenden Moduls. Sie durchläuft alle Zeilen des Moduls und gibt zu jeder Zeile die Zeilennummer und den Namen der Prozedur aus, zu der die aktuelle Zeile gehört. Dabei werden sowohl der Deklarationsbereich als auch Property Get-/Let-/Set-Prozeduren außer Acht gelassen. Für die Ausgabe der PropertyProzeduren ersetzen Sie die Konstante vbext_pk_Proc durch die der Property-Prozedur entsprechende Konstante. Public Sub LinesAndProcedures(strModule As String) Dim objCodeModule As CodeModule
885
Kapitel 17 Dim intLines As Integer Dim i As Integer Set objCodeModule = _ VBE.ActiveVBProject.VBComponents.Item(strModule).CodeModule intLines = objCodeModule.CountOfLines For i = 1 To intLines Debug.Print Format(i, "000"), _ objCodeModule.ProcOfLine(i, vbext_pk_Proc) Next i End Sub Listing 17.5: Zeilennummer und Prozedur ausgeben
17.5.8 Ausgabe des kompletten Codes eines Moduls Um den kompletten Quellcode eines Moduls auszugeben oder ihn in einer Variablen zu speichern, verwenden Sie die Lines-Eigenschaft in Verbindung mit der CountOfLinesEigenschaft. Die CountOfLines-Eigenschaft gibt die Anzahl Zeilen des Moduls zurück. Die LinesFunktion erwartet die Nummern der ersten und der letzten auszugebenden Zeile und gibt den entsprechenden Inhalt des Moduls zurück. Folgendes Beispiel zeigt, wie Sie den Inhalt des aktuellen Codefensters im Testfenster ausgeben: Debug.Print VBE.ActiveCodePane.CodeModule.Lines(1,VBE.ActiveCodePane. CodeModule.CountOfLines)
Die folgende Funktion zeigt, wie Sie mit der Lines-Funktion und der CountOfLines-Funk tion den Inhalt eines per Parameter übergebenen Moduls ermitteln: Public Function GetCompleteCode(strModule As String) Dim objCodeModule As CodeModule Dim lngLineCount As Long Dim strCompleteCode As String Set objCodeModule = _ VBE.ActiveVBProject.VBComponents.Item(strModule).CodeModule With objCodeModule 'Ermitteln der Zeilenanzahl lngLineCount = .CountOfLines 'Einlesen der Zeilen von der ersten bis zur letzten Zeile strCompleteCode = .Lines(1, lngLineCount) End With
886
Anpassen der Entwicklungsumgebung GetCompleteCode = strCompleteCode Set objCodeModule = Nothing End Function Listing 17.6: Diese Funktion ermittelt den kompletten Code eines Moduls
17.5.9 Ermitteln der Position der aktuellen Markierung Weiter oben wurde eine Funktion zum Hinzufügen einer Fehlerbehandlung zur aktuell markierten Prozedur per Mausklick erwähnt. Eine Voraussetzung für die Umsetzung dieser Funktion ist die aktuelle Position der Einfügemarke. Die für diese Ermittlung notwendige Funktion liefert direkt Informationen über den kompletten markierten Bereich – sollte kein Bereich markiert sein und sich nur die Einfügemarke an einer bestimmten Position befinden, handelt es sich um einen Sonderfall: Die Markierung erstreckt sich dann über 0 Zeichen und 0 Zeilen. Für die Kennzeichnung des markierten Bereichs sind vier Werte erforderlich, daher wird das Ergebnis nicht als Funktionswert, sondern per Parameter zurückgegeben. Diese müssen Sie vorher als Long-Variablen deklarieren. Die folgende Prozedur zeigt, wie Sie die Parameter der aktuellen Markierung ermitteln (siehe auch Abbildung 17.10). Public Function GetSelectionParameters() Dim Dim Dim Dim Dim
objCodePane As CodePane lngStartLine As Long lngEndLine As Long lngStartColumn As Long lngEndColumn As Long
Set objCodePane = VBE.ActiveCodePane 'Lesen der Parameter des markierten Bereichs objCodePane.GetSelection lngStartLine, lngStartColumn, _ lngEndLine, lngEndColumn Debug.Print Debug.Print Debug.Print Debug.Print
Set objCodePane = Nothing End Function Listing 17.7: Ausgabe der Parameter des aktuell markierten Bereichs im Codefenster
887
Kapitel 17
Abbildung 17.10: Ausgabe der Parameter der aktuellen Markierung im Direktbereich
17.5.10 Ermitteln des Inhalts der aktuellen Markierung Für die Rückgabe des Inhalts des aktuell markierten Bereichs gibt es keine eingebaute Funktion, sodass Sie selbst Hand anlegen müssen. Diesmal benötigen Sie zwei Objekte: eines, das auf das aktuelle Codefenster verweist, und eines für das entsprechende Codemodul. Mit der Funktion GetSelection des aktuellen CodePane-Objekts ermittelt die Funktion GetSelectedText (eigentlich passt die Bezeichnung GetSelection besser, aber so heißt halt schon die darin verwendete CodePane-Methode) die Parameter des markierten Bereichs und speichert diese in vier entsprechenden Long-Variablen. Mit der Lines-Methode des CodeModule-Objekts liest die Funktion dann alle Zeilen aus, die vom markierten Bereich geschnitten werden. Dann kommt der knifflige Teil: Natürlich kann sich eine Markierung beispielsweise auch von der Mitte der einen bis zur Mitte der übernächsten Codezeile erstrecken. Die nicht markierten Teile der ersten und der letzten Zeile muss die Funktion dann natürlich noch entfernen. Für die erste Zeile reicht der einfache Einsatz der Mid-Funktion aus: Diese erhält als Startposition einfach den Wert des Parameters lngStartColumn der Markierung und gibt alles zurück, was sich rechts davon befindet. Eine nicht vollständige letzte Zeile erfordert zusätzlichen Aufwand: Erst ermitteln Sie mit der Len-Funktion die Länge der letzten Zeile. Die Differenz zwischen dem Wert des
888
Anpassen der Entwicklungsumgebung
Parameters lngLastColumn und der Länge der letzten Zeile gibt an, um wie viele Zeichen Sie die komplette Zeichenkette kürzen müssen. Public Function GetSelectedText() Dim Dim Dim Dim Dim Dim Dim Dim
objCodePane As CodePane objCodeModule As CodeModule lngStartLine As Long lngStartColumn As Long lngEndLine As Long lngEndColumn As Long strSelection As String lngLenLastLine As Long
Set objCodePane = VBE.ActiveCodePane Set objCodeModule = VBE.ActiveCodePane.CodeModule 'Parameter des markierten Bereichs ermitteln objCodePane.GetSelection lngStartLine, lngStartColumn, _ lngEndLine, lngEndColumn# 'Markierte Zeilen komplett einlesen strSelection = objCodeModule.Lines(lngStartLine, _ lngEndLine - lngStartLine + 1) 'Falls erste Zeile nicht komplett, 'entsprechenden linken Teil abschneiden strSelection = Mid(strSelection, lngStartColumn) 'Länge der letzten Zeile ermitteln lngLenLastLine = Len(objCodeModule.Lines(lngEndLine, 1)) 'Falls letzte Zeile nicht komplett, 'entsprechenden rechten Teil abschneiden strSelection = Left(strSelection, _ Len(strSelection) - lngLenLastLine + lngEndColumn - 1) GetSelection = strSelection Set objCodePane = Nothing Set objCodeModule = Nothing End Function Listing 17.8: Funktion zum Ermitteln des Inhalts des markierten Bereichs
17.5.11 In Modulen suchen Natürlich bietet das CodeModule-Objekt auch eine Methode zur Suche von Ausdrücken in einem Modul. Die Find-Methode hat acht Parameter – fünf Pflichtparameter und drei
889
Kapitel 17
optionale. Die folgende Prozedur zeigt, wie Sie die Find-Methode zum Ermitteln des Ortes einer bestimmten Zeichenfolge einsetzen. Public Function FindString(strModule As String, strSearch As String) Dim Dim Dim Dim Dim
objCodeModule As CodeModule lngStartLine As Long lngStartColumn As Long lngEndLine As Long lngEndColumn As Long
'Referenz auf angegebenes CodeModul anlegen Set objCodeModule = _ VBE.ActiveVBProject.VBComponents.Item(strModule).CodeModule 'Suche durchführen FindString = objCodeModule.Find(strSearch, lngStartLine, _ lngStartColumn, lngEndLine, lngEndColumn) 'Ausgabe des Ergebnisses Debug.Print "Erste Zeile: " & lngStartLine Debug.Print "Erstes Zeichen: " & lngStartColumn Debug.Print "Letzte Zeile: " & lngEndLine Debug.Print "Letztes Zeichen: " & lngEndColumn Set objCodeModule = Nothing End Function Listing 17.9: Ausgabe der Position einer zu suchenden Zeichenkette
Zusätzlich zu den fünf in der Prozedur verwendeten Parametern besitzt die Find-Metho de noch drei Boolean-Parameter: WholeWord: Sucht bei True nach kompletten übereinstimmenden Wörtern. MatchCase: Beachtet bei True die Groß-/Kleinschreibung. PatternSearch: Wertet den Suchausdruck bei True als regulären Ausdruck aus.
17.6 Beispielanwendung: Codeviewer Der CodeViewer aus Abbildung 17.11 fasst die Funktionen zum lesenden Zugriff auf Module und die enthaltenen Prozeduren zusammen. Als Benutzeroberfläche dient ein Access-Formular mit drei Steuerelementen: zwei Kombinationsfelder zum Auswählen des Moduls und der Prozedur und ein Textfeld zur Anzeige des Codes der ausgewählten Prozedur. Das zweite Kombinationsfeld hängt vom ersten Kombinationsfeld ab und
890
Anpassen der Entwicklungsumgebung
zeigt nur die Prozeduren des jeweils ausgewählten Moduls an. Die Funktionen zum Ermitteln der durch die Kombinationsfelder anzuzeigenden Werte befinden sich im Klassenmodul clsVBE.
Abbildung 17.11: Der Codeviewer dient dem Betrachten von Prozeduren
17.6.1 Anzeige der Module Das Kombinationsfeld zur Anzeige der Module besitzt als Datensatzherkunft eine Wert liste, die aus einer durch Semikolons getrennten Auflistung des Index und des Namens der einzelnen Module einschließlich Angabe der Modulart besteht. Die Datensatzher kunft sieht also etwa wie folgt aus: 1;'Modul1 (Standardmodul)'; 2;'Modul2 (Klassenmodul)'; 3;'Modul3 (Formular-/Berichtsmodul)'
Das Kombinationsfeld soll nur die Modulnamen, nicht aber den Index anzeigen; daher stellen Sie die Werte der Eigenschaften Spaltenanzahl und Spaltenbreiten auf die Werte 2 beziehungsweise 0cm ein. Letzterer sorgt dafür, dass der jeweils erste Wert der Zeile mit der Spaltenbreite 0cm, also nicht sichtbar, und der zweite Wert über die volle Spaltenbreite angezeigt wird. Das Füllen des Kombinationsfeldes erfolgt in der Ereigniseigenschaft Beim Anzeigen des Formulars. Die entsprechende Prozedur enthält folgenden Code: Private Sub Form_Current() Dim strModules As String Dim objVBE As clsVBE
891
Kapitel 17 Set objVBE = New clsVBE 'Module einlesen strModules = objVBE.GetModulesCSV 'Herkunftsart des Kombinationsfeldes auf Wertliste einstellen Me.cboModules.RowSourceType = "Value List" 'Zuweisen der Wertliste an die Datensatzherkunft Me.cboModules.RowSource = strModules End Sub Listing 17.10: Diese Prozedur füllt das erste Kombinationsfeld mit der Modulliste
Die Prozedur instanziert ein Objekt der Klasse clsVBE und verwendet deren Funktion GetModulesCSV, um die Liste der Module des Codeprojekts einzulesen. Die Funktion ermittelt die Anzahl der enthaltenen Module und durchläuft diese anschließend nacheinander. Dabei erstellt es eine Objektvariable mit einem Verweis auf das entsprechende Element der VBComponents-Auflistung des aktuellen Projekts. Das Ermitteln des Namens erfolgt über die Name-Eigenschaft des Objekts, und eine dem Modultyp entsprechende Konstante lässt sich mit Hilfe der Type-Eigenschaft herausfinden. Da diese Konstanten nicht besonders benutzerfreundlich sind, weist die Funktion der String-Variablen strModuleType einen der Ausdrücke Standardmodul, Klassenmodul oder Formular-/Berichtsmodul zu. Schließlich fügt die Funktion den Index, den Namen und den Typ an die StringVariable strModules an. Public Function GetModulesCSV() Dim Dim Dim Dim Dim Dim
intVBComponentsCount As Integer i As Integer objVBComponent As VBComponent strModuleName As String strModuleType As String strModules As String
'Anzahl der Module ermitteln intVBComponentsCount = VBE.ActiveVBProject.VBComponents.Count 'Alle Module durchlaufen und ... For i = 1 To intVBComponentsCount '... per Objektvariable auf das aktuelle Modul verweisen ... Set objVBComponent = VBE.ActiveVBProject.VBComponents.Item(i) '... den Modulnamen lesen ... strModuleName = objVBComponent.Name
892
Anpassen der Entwicklungsumgebung '... und den Typ ermitteln und übersetzen Select Case objVBComponent.Type Case vbext_ct_StdModule strModuleType = "Standardmodul" Case vbext_ct_ClassModule strModuleType = "Klassenmodul" Case vbext_ct_Document strModuleType = "Formular-/Berichtsmodul" Case vbext_ct_MSForm strModuletype = "MSForms Userform-Modul" End Select 'Informationen des aktuellen Moduls zur Liste hinzufügen strModules = strModules & i & ";" strModules = strModules & "'" & strModuleName _ & " (" & strModuleType & ")';" Next i 'Zuweisen des Ergebnisses an den Rückgabewert GetModulesCSV = strModules End Function Listing 17.11: Erzeugen einer Liste aller Module
17.6.2 Anzeige der Prozedurliste Die Anzeige der Prozeduren ist um einiges komplizierter, da Sie nicht per Auflistung auf diese zugreifen können. Das ist aber nicht weiter schlimm, da Sie auf diese Weise einige Methoden und Eigenschaften des CodeModule-Objekts im Praxiseinsatz kennen lernen. Das Füllen des Kombinationsfeldes zur Anzeige der Prozeduren erfolgt beim Auslösen des Ereignisses Nach Aktualisieren des Kombinationsfeldes cboModule, also nach der Auswahl eines Moduls. Private Sub cboModules_AfterUpdate() Dim strProcedures As String Dim objVBE As clsVBE Set objVBE = New clsVBE 'Einlesen der Prozeduren strProcedures = objVBE.GetProceduresCSV(Me.cboModules) 'Zuweisen der Prozedurliste und Leeren des Kombinationsfelds With Me.cboProcedures .RowSourceType = "Value List" .RowSource = strProcedures .Value = Null End With
893
Kapitel 17 'Leeren des Codefensters Me.txtCode = Null Set objVBE = Nothing End Sub Listing 17.12: Aktualisieren des Kombinationsfeldes zur Anzeige der Prozeduren
Das Einlesen der Prozeduren des ausgewählten Moduls erfolgt wiederum über eine Funktion der Klasse clsVBE. Die Funktion heißt GetProceduresCSV und erwartet den Index des auszuwertenden Moduls als Parameter. Die aufrufende Prozedur hält den Indexwert in der gebundenen Spalte des Kombinationsfeldes cboModule vor und übergibt ihn mit dem Aufruf an die Funktion GetProceduresCSV. Die Funktion erstellt zunächst einen Objektverweis auf das CodeModule-Objekt des Moduls mit dem übergebenen Index. Dann ermittelt es mit der Eigenschaft CountOfLines die Gesamtanzahl der Zeilen dieses Moduls. Die folgende For Next-Schleife durchläuft alle Zeilen des Moduls – mit folgender Ausnahme: Die Funktion überprüft mit der ProcOfLine-Funktion, ob die aktuelle Zeile zu einer Prozedur gehört. Ist das der Fall, liest sie aus der Eigenschaft ProcCountLines die Zeilenanzahl dieser Prozedur und erhöht nach der Ermittlung der gewünschten Informationen die Laufvariable so, dass im nächsten Durchlauf der For Next-Schleife die erste Zeile nach der aktuellen Prozedur geprüft wird. Dazwischen liest die Funktion die Informationen zu jeder einzelnen Prozedur ein. Der Prozedurname stammt dabei aus der Funktion ProcOfLine, die als Parameter die zu untersuchende Zeilennummer und eine Variable für die Rückgabe des Prozedurtyps erwartet. Diese müssen Sie unbedingt vorher als Long-Variable deklarieren. Die Funktion ProcCountLines enthält den gleichen Parameter zur Angabe des Prozedurtyps. Hier muss dieser allerdings konkret angegeben werden und mit dem tatsächlichen Typ der Prozedur mit dem angegebenen Namen übereinstimmen. Das ist hier gegeben, denn die Variable lngProcType wird in der Funktion ProcOfLine mit der entsprechenden Typkonstante belegt und kann diesen Wert in der Funktion ProcCountLines bereitstellen. Zusätzlich zum Prozedurnamen soll die Funktion GetProceduresCSV den Prozedurtyp zurückgeben – und zwar in Form einer aussagekräftigen Zeichenkette und nicht als Konstante wie etwa vbext_pk_Proc. Dazu werden die Konstanten in einem Select CaseStatement ausgewertet und durch eine entsprechende Zeichenkette ersetzt. Damit lassen sich »normale« Prozeduren und Property Get-/Let-/Set-Prozeduren unterscheiden. Das ist etwas unbefriedigend; die Unterscheidung von Sub- und Function-Prozeduren wäre schon sinnvoll. Daher forscht die Funktion GetProceduresCSV im Falle einer »normalen« Prozedur noch etwas weiter. Ein erster Ansatz wäre, die erste Zeile der Prozedur
894
Anpassen der Entwicklungsumgebung
nach dem Schlüsselwort »Sub« oder »Function« zu durchsuchen, doch ist erstens die Position dieser Schlüsselwörter nicht immer gleich, da noch die Wörter Private oder Public vorangestellt sein könnten, und zweitens könnten diese Schlüsselwörter auch noch im Prozedurnamen vorkommen. Doch warum mit verschieden ausgeprägten Ausdrücken herumplagen, wenn es auch einfacher geht? Die letzte Zeile einer Prozedur enthält garantiert einen der beiden Ausdrücke »End Sub« oder »End Function« – hier brauchen Sie also nur die letzte Zeile ausfindig zu machen und mit einem dieser beiden Ausdrücke zu vergleichen. Den Namen, den Typ und den Zahlenwert der dem Typ entsprechenden Konstante der einzelnen Prozeduren setzt die Funktion schließlich in der String-Variablen strProcs zusammen und gibt deren Inhalt an die aufrufende Prozedur zurück. Die Liste sieht beispielsweise wie folgt aus: 'Beispiel1';'Sub-Prozedur';0; 'Beispiel2';'Property Get-Prozedur';3; 'Beispiel3';'Property Let-Prozedur';1;
Das Kombinationsfeld soll die ersten beiden Werte der Liste anzeigen und den dritten Wert verbergen. Daher stellen Sie die Eigenschaften Spaltenanzahl und Spaltenbreiten auf die Werte 3 beziehungsweise 5cm;5cm;0cm ein. Public Function GetProceduresCSV(intModuleID As Integer) Dim Dim Dim Dim Dim Dim Dim Dim Dim
objCodeModule As CodeModule intModLineCount As Integer intProcLineCount As Integer i As Integer strProcName As String strProcs As String lngProcType As Long strProcType As String strLastLine As String
Set objCodeModule = _ VBE.ActiveVBProject.VBComponents.Item(intModuleID).CodeModule With objCodeModule 'Zeilenanzahl des Moduls ermitteln intModLineCount = .CountOfLines 'Alle Zeilen untersuchen For i = 1 To intModLineCount 'Wenn die aktuelle Zeile nicht leer ist ... If Not .ProcOfLine(i, lngProcType) = "" Then
895
Kapitel 17 'Prozedur der aktuellen Zeile ermitteln strProcName = .ProcOfLine(i, lngProcType) 'Zeilen der aktuellen Prozedur ermitteln intProcLineCount = .ProcCountLines(strProcName, _ lngProcType) 'Ermitteln des Prozedurtyps Select Case lngProcType Case vbext_pk_Proc 'Prozedur: kann Sub oder Function sein, 'daher Untersuchung der letzten Zeile strLastLine = .Lines(i + intProcLineCount - 1, 1) If strLastLine = "End Sub" Then strProcType = "Sub-Prozedur" Else strProcType = "Function-Prozedur" End If Case vbext_pk_Let strProcType = "Property Let-Prozedur" Case vbext_pk_Set strProcType = "Property Set-Prozedur" Case vbext_pk_Get strProcType = "Property Get-Prozedur" End Select 'Anfügen von Name, Typbezeichnung und Typkonstante 'eine String-Variable strProcs = strProcs & "'" & strProcName & "';" strProcs = strProcs & "'" & strProcType & "';" strProcs = strProcs & lngProcType & ";" 'Laufvariable auf erste Zeile hinter der 'aktuellen Prozedur setzen i = i + intProcLineCount - 1 End If Next i End With 'Liste an Rückgabeparameter übergeben GetProceduresCSV = strProcs Set objCodeModule = Nothing End Function Listing 17.13: Einlesen von Name und Typ der Prozeduren eines Moduls
896
Anpassen der Entwicklungsumgebung
17.6.3 Anzeige des Codes einer Prozedur Nach der Auswahl des Moduls und der Prozedur bleibt nur noch ihre Anzeige im dafür vorgesehenen Textfeld. Die Ausgabe wird durch das Auswählen einer Prozedur aus dem Kombinationsfeld cboProcedures angestoßen. Die verantwortliche Ereignisprozedur aus folgendem Listing ermittelt den Quellcode der Prozedur mit der Funktion GetCode, die drei Parameter erwartet: den Modulindex, den Prozedurnamen sowie den Prozedurtyp. Alle Werte lassen sich aus den beiden Kombinationsfeldern ermitteln. Private Sub cboProcedures_AfterUpdate() Dim objVBE As clsVBE Dim strCode As String Set objVBE = New clsVBE 'Einlesen des Codes der ausgewählten Prozedur strCode = objVBE.GetCode(Me.cboModules, _ Me.cboProcedures, Me.cboProcedures.Column(2)) 'Zuweisen des Codes an das Textfeld txtCode Me.txtCode = strCode Set objVBE = Nothing End Sub Listing 17.14: Anzeigen des Codes einer Prozedur
Die Funktion GetCode verarbeitet ihre Eingangsparameter folgendermaßen: Zunächst erstellt die Funktion eine Objektvariable mit einem Verweis auf das CodeModule-Objekt des Elements der VBComponents-Auflistung mit dem im ersten Parameter angegebenen Index. Mit dem Prozedurnamen und -typ, die mit den übrigen Eingangsparametern übergeben wurden, ermittelt die Funktion dann drei Zeilennummern: die Nummer der Zeile, in der die angegebene Prozedur beginnt (das ist, wie bereits weiter oben beschrieben, die erste Zeile nach der vorherigen Prozedur beziehungsweise des Deklarationsbereichs) die Nummer der Zeile, die die Prozedurdeklaration enthält die Nummer der letzten Zeile der Prozedur (das ist die Zeile mit dem EndSchlüsselwort) Mit diesen Informationen und der Lines-Funktion lässt sich der komplette Prozedurtext leicht auslesen.
897
Kapitel 17 Public Function GetCode(intModuleID As Integer, strProcedure As String, _ lngProcedureType As Long) Dim Dim Dim Dim Dim
objCodeModule As CodeModule intFirstLine As Integer intStartLine As Integer intLastLine As Integer strCode As String
'Objektvariable mit Verweis auf das Codemodul erstellen Set objCodeModule = _ VBE.ActiveVBProject.VBComponents.Item(intModuleID).CodeModule 'Erste Zeile der Prozedur ermitteln intStartLine = objCodeModule.ProcStartLine(strProcedure, _ lngProcedureType) 'Zeile der Prozedurdeklaration ermitteln intFirstLine = objCodeModule.ProcBodyLine(strProcedure, _ lngProcedureType) 'Letzte Zeile der Prozedur ermitteln intLastLine = intStartLine + objCodeModule.ProcCountLines _ (strProcedure, lngProcedureType) - 1 'Prozedur auslesen ... strCode = objCodeModule.Lines(intFirstLine, _ intLastLine - intFirstLine + 1) '... und zurückgeben GetCode = strCode End Function Listing 17.15: Auslesen des Codes einer konkreten Prozedur
17.7 Manipulieren des Quellcodes Die in den Abschnitten 17.5.1, »Lesender Zugriff auf den Quellcode«, und 17.6, »Beispielanwendung: Codeviewer«, beschriebenen Techniken bilden die Grundlage für einige Manipulationen am Quellcode. Wenn Sie beispielsweise eine Fehlerbehandlung zur Prozedur hinzufügen möchten, in der sich die Einfügemarke gerade befindet, müssen Sie zunächst einmal deren Position herausfinden. Andere Operationen lassen sich auch ohne vorheriges Positionieren ausführen – das Einfügen von Prozeduren erfolgt standardmäßig beispielsweise immer di-
898
Anpassen der Entwicklungsumgebung
rekt hinter dem Deklarationsteil des Moduls. In den folgenden Abschnitten lernen Sie die Techniken für das Manipulieren des Inhalts von Modulen kennen – dazu gehören das Löschen, Suchen und Ersetzen, Hinzufügen von Prozeduren oder Hinzufügen einzelner Codezeilen in bestehende Prozeduren.
17.7.1 Code hinzufügen Neuer Code lässt sich auf verschiedene Art in ein Modul einfügen. Dabei kommen die folgenden Methoden des CodeModule-Objekts zum Zuge: AddFromFile: Fügt den Inhalt einer Textdatei direkt hinter dem Deklarationsbereich des Moduls ein. AddFromText: Fügt die angegebene Zeichenkette direkt hinter dem Deklarationsbereich des Moduls ein. InsertLines: Fügt die angegebene Zeichenkette in der ebenfalls angegebenen Zeile ein und verschiebt den restlichen Code nach unten. ReplaceLine: Fügt die angegebene Zeichenkette an Stelle der ebenfalls angegebenen Zeile ein. CreateEventProc: Fügt den Rumpf einer Ereignisprozedur ein (siehe weiter unten in Abschnitt 17.7.2, »Ereignisprozeduren hinzufügen«). Die AddFromFile- und die AddFromText-Methode fügen beide den gewünschten Text direkt hinter dem Deklarationsteil des Moduls ein. Sie sind einfach zu bedienen, aber auch nur dann sinnvoll, wenn komplette Prozeduren oder zumindest Prozedurrümpfe eingesetzt werden. Die folgende Anweisung fügt beispielsweise eine kleine Prozedur wie in Abbildung 17.12 in das Modul im aktuellen Codefenster ein: VBE.ActiveCodePane.CodeModule.AddFromString(vbcrlf & "Public Sub Test()" _ & vbcrlf & " MsgBox ""Hallo""" & vbcrlf & "End Sub")
Abbildung 17.12: Diese Prozedur lässt sich mit einem Einzeiler automatisch hinzufügen ...
899
Kapitel 17
Die InsertLines-Methode erwartet zwei Parameter: den einzufügenden Text und die Zielzeile. Nach der Behandlung mit der folgenden Zeile sieht die Prozedur aus Abbil dung 17.12 wie in Abbildung 17.13 aus: VBE.ActiveCodePane.CodeModule.InsertLines 4, "
On Error Resume Next"
Abbildung 17.13: … und sie lässt sich mit einem weiteren Einzeiler erweitern
Wenn Sie einzelne Zeilen ersetzen möchten, verwenden Sie dazu die ReplaceLine-Metho de. Damit können Sie allerdings nur jeweils eine Zeile austauschen: VBE.ActiveCodePane.CodeModule.ReplaceLine 4, "
On Error Goto Test_Err"
17.7.2 Ereignisprozeduren hinzufügen Während das Hinzufügen herkömmlicher Prozeduren nicht unterstützt wird, ist das bei Ereignisprozeduren sehr wohl der Fall. Die dazu verwendete Methode CreateEvent erwartet zwei Parameter: die englische Bezeichnung der Methode (etwa Open, OnCurrent, BeforeUpdate) und den Namen des Objekts (Form, Report, txtText, lstListenfeld). Die folgende Anweisung fügt beispielsweise eine Prozedur namens Form_Open in das Klassenmodul eines Formulars ein: VBE.ActiveCodePane.CodeModule.CreateEventProc "Open", "Form"
Mit dieser Methode lassen sich beispielsweise automatisch Ereignisprozeduren für größere Mengen Steuerelemente anlegen. Der Vorteil dieser Prozedur ist, dass sie automatisch überprüft, ob das angegebene Steuerelement überhaupt vorhanden ist, und das gewünschte Ereignis zur Verfügung stellt.
17.7.3 Löschen von Zeilen Für das Löschen von Zeilen ist die Methode DeleteLines des CodeModule-Objekts verantwortlich. Die Methode erwartet die Nummer der ersten und die Gesamtanzahl der zu löschenden Zeilen.
900
Anpassen der Entwicklungsumgebung
17.7.4 Beispielanwendung: Nummerieren von Codezeilen in einem Modul Die folgende Routine nummeriert die Zeilen des im Übergabeparameter angegebenen Moduls. Dabei durchläuft sie alle Zeilen des Moduls und prüft, ob sich die aktuelle Zeile innerhalb einer Routine befindet, ob es sich um die erste Zeile einer Select CaseAnweisung handelt und ob die Zeile gegebenenfalls die Fortsetzung einer bestehenden Zeile ist (gekennzeichnet durch den einleitenden Unterstrich). Die Routine können Sie zum Ausprobieren vom Direktfenster aus aufrufen. Das Pendant zum Entnummerieren eines Moduls finden Sie auf der Buch-CD im Modul mdlModulNummerieren der Datenbank \Kap_17\VBAIDE.accdb. Die Routine findet übrigens auch in dem in Abbildung 17.3 gezeigten Tool Verwendung und ist hier nur so weit angepasst, dass Sie es vom Direktfenster aus aufrufen und dort den Namen des zu nummerierenden Moduls übergeben können. Im Tool öffnen Sie einfach das gewünschte Modul und wählen den Menüeintrag Nummerieren beziehungsweise Entnummerieren aus. Public Sub Nummerieren(strModulname As String) Dim Dim Dim Dim Dim Dim Dim Dim Dim
objVBE As VBE cdPane As VBIDE.CodePane mdl As VBIDE.CodeModule bolNummerieren As Boolean bolJetztNicht As Boolean bolNaechsteNicht As Boolean strZeile As String i As Integer j As Integer
Entnummerieren strModulname Set objVBE = Application.VBE j = 1 Set mdl = objVBE.ActiveVBProject.VBComponents(strModulname).CodeModule For i = 1 To mdl.CountOfLines strZeile = Trim(mdl.Lines(i, 1)) If Left(strZeile, 10) Or Left(strZeile, Or Left(strZeile, Or Left(strZeile, Or Left(strZeile, Or Left(strZeile, Or Left(strZeile,
Kapitel 17 Or Left(strZeile, 16) = "Private Property" Then bolNummerieren = True bolJetztNicht = True Else bolJetztNicht = False End If If bolNaechsteNicht = True Then bolJetztNicht = True End If If Left(strZeile, 12) = "End Function" _ Or Left(strZeile, 7) = "End Sub" _ Or Left(strZeile, 12) = "End Property" Then bolNummerieren = False bolJetztNicht = True End If bolNaechsteNicht = Right(strZeile, 1) = "_" _ Or Left(strZeile, 11) = "Select Case" If bolNummerieren = True And bolJetztNicht = False Then mdl.ReplaceLine i, j & "0 " & mdl.Lines(i, 1) j = j + 1 End If Next i End Sub Listing 17.16: Diese Routine nummeriert die Zeilen eines Moduls
17.8 Toolwindows Die VBA-Entwicklungsumgebung liefert bereits einige andockbare Toolwindows mit. Nachfolgend finden Sie eine Auflistung der vorhandenen Toolwindows und ihre Funk tion: Projektbrowser: Navigation in den Klassenmodulen von Formularen und Berichten, Standardmodulen und eigenständigen Klassenmodulen Direktbereich: Schnelles Testen von Anweisungen und Funktionsaufrufen; Ausgabe von Debug.Print-Anweisungen im Quellcode Lokalfenster: Überwachung der Werte der Variablen der aktuellen Prozedur Eigenschaften: Anzeige der Eigenschaften des aktuellen Moduls Überwachungsausdrücke: Überwachung benutzerdefinierter Ausdrücke
902
Anpassen der Entwicklungsumgebung
Sie können die eingebauten Toolwindows über das Untermenü Ansicht aufrufen (siehe Abbildung 17.14). Diese werden dann an einer voreingestellten Stelle eingeklinkt. Um ein Toolwindow auszublenden, klicken Sie einfach auf die Schließen-Schaltfläche.
Abbildung 17.14: Anzeigen der eingebauten Toolwindows per Menüaufruf
17.8.1 Benutzerdefiniertes Toolwindow = COM-Add-In Toolwindows können Sie auch selbst erstellen. Die dazu verwendete Technik ist jedoch wenig verbreitet. Sowohl in der Fachliteratur als auch im Internet finden sich dazu kaum Informationen. Tatsächlich gibt es nur wenige Anbieter, die überhaupt als Toolwindow ausgelegte COM-Add-Ins vertreiben. Das ist aber weiter nicht schlimm, da Sie nach dem Studium der folgenden Abschnitte selbst in der Lage sein werden, Toolwindows zu erstellen. Und nicht nur das: In Ab schnitt 17.9, »COM-Add-Ins per Menübefehl aufrufen«, erfahren Sie auch noch, wie Sie COM-Add-Ins erstellen, die per benutzerdefinierter Symbolleiste oder Kontextmenü aufgerufen werden können. Sie finden auf der Buch-CD ein VB-Projekt, das Sie als Grundlage für eigene Toolwindows/COM-Add-Ins verwenden können. Die notwendigen Dateien befinden sich im Verzeichnis \Kap_17\COMAddIn_Toolwindow_Basis. Die nachfolgenden Ausführungen beziehen sich auf die Erstellung von COM-Add-Ins mit Visual Basic und dem Visual Studio 6.0. Es ist auch möglich, diese mit der Developer Edition von Access 2000 oder höher oder mit .NET im Visual Studio .NET zu erstellen. Grundlage für die Verwendung von COM-Add-Ins ist eine Schnittstelle namens IDTEx tensibility2. Ist ein COM-Add-In einmal als solches registriert, sorgt diese Schnittstelle
903
Kapitel 17
dafür, dass das Add-In beim Laden oder Entladen der Zielanwendung – in diesem Fall der VBA-Entwicklungsumgebung – gestartet wird und Informationen über die Zielan wendung erhält. Ist ein COM-Add-In ordnungsgemäß registriert, erscheint es im Add-In-Manager der Zielanwendung – hier im VBA-Editor. Abbildung 17.15 zeigt den entsprechenden Dialog der VBA-Entwicklungsumgebung mit einigen COM-Add-Ins. Den Dialog öffnen Sie über den Menüeintrag Add-Ins|Add-In-Manager … In diesem Dialog können Sie das Ladeverhalten der Add-Ins festlegen. Wenn Sie ein COM-Add-In für die Access-Entwicklungsumgebung (also nicht für die VBA-Entwicklungsumgebung!) entwickeln, benötigen Sie noch die Information, dass Sie COM-Add-Ins dort mit einem anderen Dialog als in der VBA-Entwicklungsumgebung verwalten. Den Menüeintrag, um diesen Dialog zu öffnen, müssen Sie zunächst zu einem der Menüs hinzufügen: Dazu verwenden Sie den Anpassen-Dialog für Menüs und wählen auf der Registerseite Befehle die Kategorie Extras und den Befehl COM-Add-Ins… aus.
Abbildung 17.15: Verwalten der Add-Ins der VBA-Entwicklungsumgebung
17.8.2 Anlegen eines leeren Toolwindows In den folgenden Schritten legen Sie ein erstes eigenes COM-Add-In an, das Sie anschließend als Toolwindow in die VBA-Entwicklungsumgebung integrieren können. Den Weg dahin macht das Microsoft Visual Studio 6.0 Ihnen relativ leicht – richtig inte ressant wird das Hinzufügen der eigentlichen Funktionalität. Da der Weg dorthin aber
904
Anpassen der Entwicklungsumgebung
nur über das hier im Folgenden vorgestellte Grundgerüst führt, ist damit nun erst einmal der Pflichtteil an der Reihe. Falls Sie noch keine Erfahrung im Umgang mit dem Visual Studio besitzen, werden Ihnen die folgenden Tipps an einigen Stellen weiterhelfen: Das Kompilieren eines COM-Add-Ins erfolgt wie bei allen anderen Projektarten über den Menübefehl Datei| erstellen. Erst wenn Sie ein COM-Add-In kompiliert haben, wird ein Eintrag in der Registry vorgenommen und das COM-Add-In ist im Dialog Add-In-Manager verfügbar. Das Erstellen der .dll-Datei entspricht dem Kompilieren eines VBA-Moduls. Dement sprechend werden Kompilierfehler erst beim Erstellen der .dll-Datei aufgedeckt. Sie können das Projekt natürlich auch debuggen. Dazu starten Sie es mit F5. Im Fall der hier vorliegenden COM-Add-Ins tut sich natürlich nichts, bis Sie Access und die VBA-Entwicklungsumgebung starten und das zu debuggende Add-In aktivieren. Das Debuggen läuft ansonsten wie in der VBA-Entwicklungsumgebung ab – Sie können Hal tepunkte in den Code-Fenstern des VB6-Editors setzen, Überwachungen hinzufügen und so weiter. Wichtig ist, dass Sie mit dem Debuggen die Registrierung der eventuell schon kompilierten .dll-Datei wieder aufheben. Das können Sie am besten beobachten, indem Sie alle Optionen des Ladeverhaltens der fertigen .dll-Datei im Add-In-Manager aktivieren. Wenn Sie das Projekt anschließend debuggen und erneut die Optionen des Ladeverhaltens betrachten, stellen Sie fest, dass diese zurückgesetzt sind. Sie müssen also nach dem Debuggen des Projekts die .dll-Datei wieder neu registrieren oder das Projekt komplett neu erstellen.
17.8.3 Anlegen eines neuen Projekts Nach dem Öffnen von Microsoft Visual Basic 6.0 folgen Sie dem Vorschlag, ein neues Projekt anzulegen, und wählen im Dialog aus Abbildung 17.16 den Eintrag Add-In aus. Falls dieser Dialog nicht automatisch beim Start angezeigt wird, verwenden Sie den Menübefehl Datei|Neues Projekt zum Anlegen des neuen Projekts. Durch die Auswahl dieses Projekttyps nimmt das Visual Studio Ihnen einige Aufga ben ab. So legt es etwa schon die benötigten Verweise auf die Bibliotheken Microsoft Office x.y Objekt Library und Microsoft Visual Basic 6.0 Extensibility und Microsoft AddIn Designer an (siehe Abbildung 17.17). Die Bibliothek Microsoft Visual Basic 6.0 Ex tensibility müssen Sie in diesem Fall allerdings durch die Bibliothek Microsoft Visual Basic for Applications Extensibility 5.3 ersetzen. Die Office-Bibliothek benötigen Sie für den Zugriff auf die Menü-Objekte, und die Microsoft Visual Basic for Applications 5.3 Extensibilty-Bibliothek enthält das Objektmodell für den Zugriff auf das in der
905
Kapitel 17
VBA-Entwicklungsumgebung angezeigte Projekt einschließlich seiner Module und enthaltener Codes.
Abbildung 17.16: Anlegen eines neuen COM-Add-Ins
Abbildung 17.17: Visual Studio legt für das Add-In-Projekt automatisch einige Verweise an
906
Anpassen der Entwicklungsumgebung
Wenn Sie den Projekt-Explorer aktivieren, bietet sich nun das Bild aus Abbildung 17.18. Von den beiden vorhandenen Elementen benötigen Sie nur eines – den Add-In-Desig ner. Das Formular würde – wenn Sie ein Add-In mit einem Formular erstellen wollten – als herkömmliches Fenster in der VBA-Entwicklungsumgebung angezeigt. Es weist nicht die gleichen Eigenschaften des Toolwindows auf und lässt sich dementsprechend nicht an einer bestimmten Stelle andocken. Falls Sie ein Add-In planen, das zusätzlich zum Toolwindow weitere Formulare benötigt, lassen Sie das automatisch erstellte Formular zunächst im Projekt, ansonsten sollten Sie es entfernen.
Abbildung 17.18: Der Projekt-Explorer des neuen Projekts
17.8.4 Der COM-Add-In-Designer Schauen Sie sich nun das verbliebene Objekt des Projekts an – den Add-In-Designer. Mit einem Doppelklick auf den entsprechenden Eintrag im Projekt-Explorer öffnen Sie das Eigenschaftsfenster aus Abbildung 17.19. Neben dem Namen, der dann später im Add-In-Manager aufgelistet wird, und der Beschreibung des Add-Ins ist vor allem die Einstellung der Anwendung interessant. Für die VBA-Entwicklungsumgebung als Zielanwendung wählen Sie den Eintrag Visual Basic For Applications IDE. Wenn Sie das anfängliche Ladeverhalten auf Startup einstellen, wird das COM-Add-In direkt beim Start der VBA-Entwicklungsumgebung im Menü Add-Ins angezeigt, ansonsten müssen Sie dieses erst mit dem Add-InManager starten. Der Add-In-Designer enthält außerdem noch ein Code-Modul, in dem Sie die benötigten Ereigniseigenschaften anlegen. Das Code-Modul zeigen Sie an, indem Sie aus dem Kontextmenü des Eintrags Connect (Connect.Dsr) des Projekt-Explorers den Befehl Code anzeigen auswählen. Im nun erscheinenden Code-Fenster finden Sie bereits einigen Quellcode, der beim Erstellen des Projekts automatisch angelegt wurde (siehe Abbil dung 17.20). Um das handelsübliche COM-Add-In in eines mit Toolwindow umzuwandeln, sind einige Schritte notwendig.
907
Kapitel 17
Abbildung 17.19: Eigenschaftsfenster des COM-Add-In-Designers
Abbildung 17.20: Ereignisprozeduren des Add-In-Designers
17.8.5 Das Userdocument als Toolwindow An Stelle eines normalen Formulars verwenden Sie ein Objekt namens Benutzerdokument zur Realisierung des Toolwindows. Das Benutzerdokument ist einem herkömmlichen VB-Formular sehr ähnlich. Um ein solches Objekt zum Projekt hinzuzufügen, verwenden
908
Anpassen der Entwicklungsumgebung
Sie den Eintrag Hinzufügen|Benutzerdokument des Kontextmenüs des Projekt-Explorers. Wählen Sie im Dialog Benutzerdokument hinzufügen den Eintrag Benutzerdokument aus. Der VB-Editor legt das neue Benutzerdokument an und öffnet es in der Entwurfsansicht (siehe Abbildung 17.21). Das Toolwindow enthält noch keine Steuerelemente und auch keinen Quellcode. Das soll auch vorerst so bleiben, denn zunächst soll nur das leere Toolwindow in der VBAEntwicklungsumgebung angezeigt werden.
Abbildung 17.21: Dieses Benutzerdokument dient in der VBA-Entwicklungsumgebung als Toolwindow
17.8.6 Ereignisprozeduren des COM-Add-Ins mit Leben füllen Selbstverständlich bekommen Sie in der VBA-Entwicklungsumgebung kein Toolwindow zu sehen, ohne es mit der grundlegenden Funktionalität zu bestücken. Die folgenden Prozeduren gehören alle ins Codemodul des Add-In-Designers. Den Start machen einige modulweite Konstanten und Deklarationen (siehe Listing 17.17). Die String-Konstanten werden in mehreren Ereignisprozeduren benötigt und sind daher modulweit erreichbar. Die Variable evtMenu hat den Typ CommandbarEvents und wird mit dem Schlüsselwort WithEvents deklariert. Später wird die Variable auf Ereignisse des Menüelements für das neue COM-Add-In im Menü Add-Ins reagieren. Die Deklaration der Variable für dieses Menüelement namens cbc befindet sich in der nächsten Zeile. Die letzten drei Zeilen enthalten die Deklarationen von Objektvariablen für die VBA-Ent wicklungsumgebung selbst (objApplication), für das Toolwindow (objToolwindow) und das Benutzerdokument (objUserDocument), das zur Laufzeit zum Toolwindow wird. Option Explicit Const strGUID$ = "{E3A0FF80-720A-4AB2-BAAC-0BB233E7526E}" Const strAppTitle = "COMAddin" 'Applikations-Titel Const strTitle = "Toolwindow (COM-Add-In)" 'Titelzeile des Addin-Fensters
909
Kapitel 17 Const strMenu = "Add-&Ins" 'Menü für Eintrag des Addins Const strSubMenu = "Beispiel-Toolwindow-Add-In" 'Bezeichnung Menüeintrag Const strUserDocument = "Toolwindow" 'Name des Userdocuments Public WithEvents evtMenu As VBIDE.CommandBarEvents Private cbc As Office.CommandBarControl Dim strMenubar As String Public objApplication As VBIDE.VBE Public objToolwndow As VBIDE.Window Public objUserDocument As Object Listing 17.17: Deklarationsteil des Add-In-Designer-Moduls
Als Nächstes folgt die Prozedur, die beim Aufrufen des COM-Add-Ins durch die VBAEntwicklungsumgebung gestartet wird. Voraussetzung für diesen Aufruf ist, dass Sie im Dialog Add-In-Manager die Eigenschaft Geladen|Entladen aktivieren und den Dialog schließen oder dass die VBA-Entwicklungsumgebung bei aktivierter Beim Start ladenEigenschaft des jeweiligen COM-Add-Ins geöffnet wird. Diese Ereignisprozedur heißt AddinInstance_OnConnection und enthält folgenden Code: Private Sub AddinInstance_OnConnection( _ ByVal Application As Object, _ ByVal ConnectMode As AddInDesignerObjects.ext_ConnectMode, _ ByVal AddInInst As Object, _ custom() As Variant) Dim cbcTemp As Office.CommandBarControl Dim i As Long On Error GoTo AddinInstance_OnConnection_Err Set objApplication = Application App.Title = strAppTitle On Error Resume Next strMenubar = "Menüleiste" strMenubar = objApplication.CommandBars(strMenubar).Name If Err.Number <> 0 Then strMenubar = "Menu Bar" End If On Error GoTo AddinInstance_OnConnection_Err
910
Anpassen der Entwicklungsumgebung For i = objApplication.CommandBars(strMenubar). _ Controls(strMenu).Controls.Count To 1 Step -1 Set cbcTemp = objApplication.CommandBars(strMenubar). _ Controls(strMenu).Controls(i) If cbcTemp.Caption = strSubMenu Then cbcTemp.Delete End If Next i If Not objToolWindow Is Nothing Then objToolWindow.Visible = True Else Set objToolWindow = objApplication.Windows.CreateToolWindow( _ AddInInst, strAppTitle & "." & strUserDocument, strTitle, _ strGUID$, objUserDocument) End If AddToCommandBar AddinInstance_OnConnection_Exit: Set cbcTemp = Nothing Exit Sub AddinInstance_OnConnection_Err: MsgBox "...in Zeile " & Erl & " Fehlernummer " & Err.Number _ & vbNewLine & "- " & Err.Description _ & " in Prozedur: AddinInstance_OnConnection in Connect", _ vbCritical, "Fehler in " & strTitle Resume AddinInstance_OnConnection_Exit End Sub Listing 17.18: Die Ereignisprozedur AddinInstance_OnConnection
Die Eingangsparameter der Prozedur übergeben automatisch die benötigten Informatio nen über die aufrufende VBA-Entwicklungsumgebung. Die Prozedur führt im Wesent lich die folgenden Schritte aus: Entfernen des Eintrags für das Toolwindow aus dem Menü Add-Ins, sofern noch vorhanden – was zum Beispiel durch einen früheren Absturz der Umgebung der Fall sein könnte. Überprüfen, ob das Toolwindow schon instanziert ist, und es gegebenenfalls in den Vordergrund holen oder es mit der Methode CreateToolWindow neu erzeugen. Aufruf der Prozedur AddToCommandBar zum Hinzufügen eines Eintrags für das Toolwindow in das Menü Add-Ins und Funktion zur Verarbeitung der MenüEreignisse dieses Eintrags hinzufügen.
911
Kapitel 17
Die Prozedur AddToCommandBar macht zunächst die Hauptmenüleiste der VBA-Ent wicklungsumgebung sichtbar, sofern das noch nicht der Fall war. Anschließend fügt sie dem Menü Add-Ins einen Eintrag zum Aktivieren des Toolwindows hinzu und stellt dessen Beschriftung, Symbol und das beim Klicken auszulösende Ereignis ein. Schließlich überprüft die Routine, ob das Toolwindow beim letzten Schließen sichtbar war oder nicht, und stellt diesen Zustand wieder her. Dabei greift sie mit der Funktion GetSetting auf die Registry zu. Weitere Informationen zum Erstellen von Menüs per VBA erhalten Sie einem Kapitel aus »Das Access 2003 Entwicklerbuch«, das Sie auf der Buch-CD unter \Kap17\Menue leisten.pdf finden. Sub AddToCommandBar() On Error GoTo AddToCommandBar_Err objApplication.CommandBars(strMenubar).Visible = True Set cbc = objApplication.CommandBars(strMenubar). _ Controls(strMenu).Controls.Add(1 , , , , True) cbc.Caption = strSubMenu cbc.Style = msoButtonIconAndCaption cbc.FaceId = 611 Set Me.evtMenu = objApplication.Events.CommandBarEvents(cbc) If GetSetting("VBA AddIns", App.Title, "DisplayOnConnect", "0") _ = "1" Then objToolWindow.Visible = True End IfAddToCommandBar_Exit: Exit Sub AddToCommandBar_Err: MsgBox "...in Zeile " & Erl & " Fehlernummer " & Err.Number _ & vbNewLine & "- " & Err.Description _ & " in Prozedur: AddToCommandBar in Connect", vbCritical, _ "Fehler in " & strTitle Resume AddToCommandBar_Exit End Sub Listing 17.19: Hinzufügen eines Menüleisteneintrags zum Aktivieren des Toolwindows
Damit der Menüeintrag beim Klicken auch das Toolwindow aktiviert, benötigt das Modul noch einen Event-Handler. Dieser enthält lediglich eine Anweisung, die das Toolwindow sichtbar macht:
912
Anpassen der Entwicklungsumgebung Private Sub evtMenu_Click(ByVal CommandBarControl As Object, _ handled As Boolean, CancelDefault As Boolean) On Error Resume Next objToolWindow.Visible = True End Sub Listing 17.20: Event-Handler für den Toolwindow-Menüeintrag
Zu guter Letzt bleibt noch die Ereignisprozedur, die beim Entladen des COM-Add-Ins ausgelöst wird. Die Prozedur sorgt für das Entfernen des Menüeintrags zum Anzeigen des Toolwindows, speichert den aktuellen Zustand bezüglich der Sichtbarkeit des Toolwindows in der Registry und gibt die Objektvariablen wieder frei: Private Sub AddinInstance_OnDisconnection(ByVal RemoveMode As _ AddInDesignerObjects.ext_DisconnectMode, custom() As Variant) Dim cbcTemp As Office.CommandBarControl Dim i As Long On Error Resume Next For i = objApplication.CommandBars(strMenubar).Controls(strMenu). _ Controls.Count To 1 Step -1 Set cbcTemp = objApplication.CommandBars(strMenubar). _ Controls(strMenu).Controls(i) If cbcTemp.Caption = strSubMenu Then cbcTemp.Delete End If Next i If objToolWindow.Visible Then SaveSetting "VBA AddIns", App.Title, "DisplayOnConnect", "1" objToolWindow.Visible = False Else SaveSetting "VBA AddIns", App.Title, "DisplayOnConnect", "0" End If objToolWindow.Close Set Me.evtMenu = Nothing Set cbcTemp = Nothing Set cbc = Nothing Set objToolWindow = Nothing Set objUserDocument = Nothing Set objApplication = Nothing End Sub Listing 17.21: Die Ereignisprozedur AddinInstance_OnDisconnection wird beim Entladen des COM-Add-Ins ausgelöst
913
Kapitel 17
17.8.7 Anpassen der Eigenschaften des COM-Add-Ins Sicher möchten Sie das COM-Add-In und damit das Toolwindow Ihren eigenen Bedürf nissen entsprechend zurechtschneiden. Dazu gibt es folgende Ansatzpunkte: Passen Sie die Eigenschaften Angezeigter Name des Add-Ins und Add-In-Beschreibung im Designer an (siehe auch Abbildung 17.19). Diese Informationen werden im AddIn-Manager angezeigt. Ändern Sie die Bezeichnungen der String-Konstanten im Codemodul des Designers. Hier ist vor allem der Fenstertitel interessant, der in der Konstanten strTitle gespeichert wird. Sehr wichtig ist, dass Sie – wenn Sie die oben aufgeführten Listings aus dem Buch oder von der Buch-CD übernehmen – für die Eindeutigkeit der verwendeten GUID sorgen. Um eine solche neue GUID zu ermitteln, fügen Sie den Code aus Listing 17.22 in ein neues Modul ein (entweder im Visual Studio oder in der VBA-Entwicklungs umgebung) und verwenden die Funktion CreateGUID. Sie können diese Funktion beispielsweise im Direktfenster beziehungsweise Direktbereich mit dem Aufruf De bug.Print CreateGUID starten und erhalten die gewünschte Ausgabe im gleichen Be reich. Anschließend kopieren Sie diesen GUID-String in die Konstante strGUID$ des Designer-Moduls. Public Type TYP_GUID bytes(15) As Byte End Type Public Declare Function CoCreateGuid Lib "OLE32.dll" (Guid As TYP_GUID) _ As Long Public Declare Function StringFromGUID2 Lib "OLE32.dll" _ (Guid As TYP_GUID, _ ByVal lpszString As String, _ ByVal iMax As Long) As Long Public Function CreateGUID() As String Dim uGuid As TYP_GUID, sBuffer As String, lResult As Long sBuffer = VBA.Space(78) CoCreateGuid uGuid lResult = StringFromGUID2(uGuid, sBuffer, Len(sBuffer)) CreateGUID = Left$(StrConv(sBuffer, vbFromUnicode), lResult - 1) End Function Listing 17.22: Funktion zum Ermitteln einer GUID
914
Anpassen der Entwicklungsumgebung
17.8.8 Anzeige des Toolwindows beim Starten der VBAEntwicklungsumgebung Wenn Sie die VBA-Entwicklungsumgebung starten, werden Abläufe in Gang gesetzt, die für das Einbinden eines COM-Add-Ins interessant sind: Die VBA-Entwicklungsumgebung sucht in der Registry von Windows nach den vorhandenen Add-Ins. Die entsprechenden Einträge legt Windows unter HKEY_ CURRENT_USER\Software\Microsoft\VBA\VBE6.0\Addins an (siehe Abbildung 17.22). Sind dort Einträge vorhanden, wird der Wert der Eigenschaft LoadBehaviour geprüft. Der Wert gibt an, ob das Add-In beim Anwendungsstart geladen wird oder nicht. Die Eigenschaft kann folgende Werte annehmen und gibt damit die Einstellungen aus Abbildung 17.15 wieder: − 0: Geladen|Entladen: Falsch; Beim Start laden: Falsch − 1: Geladen|Entladen: Falsch; Beim Start laden: Wahr − 2: Geladen|Entladen: Wahr; Beim Start laden: Falsch − 3: Geladen|Entladen: Wahr; Beim Start laden: Wahr
Abbildung 17.22: Registrierungsort der COM-Add-Ins
915
Kapitel 17
17.8.9 Testen des neuen Toolwindows Nun wird es Zeit, das neue COM-Add-In zu testen. Kompilieren Sie das Projekt im Vi sual Studio über den Menüeintrag Datei|.dll. Wenn keine Fehler auftreten, starten Sie Access und öffnen die VBA-Entwicklungsumgebung. Ein Blick in den AddIn-Manager (Menüpunkt Add-Ins|Add-In-Manager) schafft Gewissheit: Wenn alles funktioniert hat, befindet sich dort der neue Eintrag – noch mit jungfräulichen Einstellungen für das Ladeverhalten (siehe Abbildung 17.23).
Abbildung 17.23: Das COM-Add-In im jungfräulichen Zustand
Das soll allerdings nicht lange so bleiben. Aktivieren Sie die Optionen Geladen|Entladen und Beim Start laden und schließen Sie den Add-In-Manager. Öffnen Sie erneut das Me nü Add-Ins und machen Sie die Probe aufs Exempel (siehe Abbildung 17.24). Das fer tige Toolwindow zeigt Abbildung 17.25. Das Toolwindow präsentiert sich ein wenig schmucklos und es enthält auch noch keine Steuerelemente geschweige denn Funktio nalität. Außerdem schwebt es noch frei in der VBA-Entwicklungsumgebung. Das Ando cken müssen Sie also per Maus selbst übernehmen. Sie haben aber immerhin schon den Grundstein für die Erstellung vieler nützlicher Helfer geschaffen.
Abbildung 17.24: Das neue COM-Add-In ist einsatzbereit
916
Anpassen der Entwicklungsumgebung
Abbildung 17.25: Das Toolwindow im Einsatz
17.8.10 Das Toolwindow füllen Das Toolwindow brauchen Sie nur noch zu füllen. Dazu stehen prinzipiell die gleichen Möglichkeiten zur Verfügung wie in herkömmlichen VB-Formularen. Leider ist die Funktionalität von Toolwindows je nach Anwendung relativ umfangreich, sodass Sie an dieser Stelle auf Ihre eigene Geschicklichkeit angewiesen sind. Wer sich bis hierher durchgekämpft hat und ambitioniert ist, die VBA-Entwicklungsumgebung um nütz liche Funktionen zu erweitern, der wird auch diesen letzten Schritt in Angriff nehmen. Außerdem wissen Sie selbst am besten, welche Funktionen Sie in der VBA-Entwicklungs umgebung am meisten vermissen – die Basisausrüstung für nützliche Helfer haben Sie nun vor sich liegen. Und falls Ihnen Ideen fehlen, hier einige Vorschläge: Toolwindow zum Speichern, Auswählen und Suchen von VBA-Code: Damit sichern Sie oft verwendete Code-Fragmente oder Routinen und machen diese dank der Suchfunktion bei Bedarf verfügbar ToDo-Manager: Toolwindow mit einer Auflistung aller im aktuellen Modul oder auch im kompletten Projekt enthaltenen Kommentare, die mit »ToDo« oder »Fix« beginnen und offene Aufgaben enthalten. Per Knopfdruck auf einen der Einträge wird die entsprechende Stelle im Code angezeigt. Wenn Sie eigene Vorschläge haben oder anderen Lesern dieses Buchs ein selbst entwickeltes Tool zur Verfügung stellen möchten, schreiben Sie einfach eine Mail an info@ access-entwicklerbuch.de. Ihre Vorschläge und Tools werden dann nach Prüfung auf http:// www.access-entwicklerbuch.de veröffentlicht.
17.9 COM-Add-Ins per Menübefehl aufrufen Neben Toolwindows lassen sich auch Erweiterungen der VBA-Entwicklungsumgebung erstellen, die Sie über benutzerdefinierte Menüs aufrufen können. Beispiele sind die Nummerierung von Modulen, das automatische Einfügen von Fehlerbehandlungen oder die automatische Generierung von Datenzugriffsklassen auf Basis einer ausgewähl
917
Kapitel 17
ten Tabelle. Ein Beispiel für ein menügesteuertes COM-Add-In finden Sie zu Beginn dieses Kapitels in Abbildung 17.3. Dort sehen Sie auch einen Hinweis, wo Sie das Tool finden und wie Sie es installieren können.
17.9.1 Vorbereitungen Die Erstellung eines COM-Add-Ins, das zusätzliche, über Menüleisten erreichbare Funktionen zur Verfügung stellt, erfolgt genau wie beim Toolwindow mit dem Microsoft Visual Studio 6.0. Diesmal verwenden Sie eine ActiveX-DLL als Vorlage für das neue Projekt (siehe Abbildung 17.26). Auch menügesteuerte COM-Add-Ins können Sie mit den Developer-Editionen von Access oder mit .NET erstellen. Hier soll jedoch nur die Erstellung mit Visual Basic 6 beschrieben werden. Auf der Buch-CD finden Sie ein VB-Projekt, das Sie als Grundlage für eigene COMAdd-Ins mit Menüs verwenden können. Die notwendigen Dateien befinden sich im Verzeichnis \Kap_17\COMAddIn_Menu_Basis.
Abbildung 17.26: Anlegen einer ActiveX-DLL
17.9.2 Objekte hinzufügen Das im Projekt-Explorer angezeigte Klassenmodul können Sie entfernen. Es wird im weiteren Verlauf nicht benötigt. Damit es im Projekt-Explorer nicht allzu trist aussieht, fügen Sie die benötigten Komponenten direkt hinzu: Zum Hinzufügen der AddIn Class wählen Sie im Kontextmenü des Projekts im ProjektExplorer den Eintrag Hinzufügen|Add-In-Class aus. Ist dieser Eintrag nicht vorhanden, müssen Sie ihn zunächst aktivieren: Öffnen Sie den Dialog Komponenten über den Menübefehl Projekt|Komponenten und wechseln Sie dort auf die Registerseite Designer.
918
Anpassen der Entwicklungsumgebung
Aktivieren Sie den Eintrag Addin Class (siehe Abbildung 17.27) und schließen Sie den Dialog wieder.
Abbildung 17.27: Bereitstellen der Addin Class
Wiederholen Sie dann den Vorgang zum Hinzufügen der AddIn Class (siehe Abbil dung 17.28).
Abbildung 17.28: Hinzufügen der Addin Class zum Projekt
Neben diesem Designer benötigen Sie lediglich noch ein Standardformular für globale Variablen. Legen Sie dieses mit dem Eintrag Hinzufügen|Modul des Kontextmenüs des Projekts im Projekt-Explorer an.
919
Kapitel 17
17.9.3 Eigenschaften der AddIn Class anpassen Die AddIn Class stellt zwei Eigenschaftsfenster zur Verfügung: Das erste ist das Eigen schaftsfenster, das jedes Objekt besitzt (anzuzeigen über F4), das zweite lässt sich durch einen Doppelklick auf das Objekt im Projektexplorer anzeigen. Zusätzlich besitzt es natürlich ein Codemodul, das Sie über das Kontextmenü öffnen. Sie müssen in beiden Eigenschaftsfenstern Änderungen vornehmen. Die »speziellen« Eigenschaften passen Sie wie in Abbildung 17.29 an. Im herkömmlichen Eigenschafts fenster stellen Sie die Eigenschaft Public auf True ein und bestätigen die anschließend erscheinende Meldung.
Abbildung 17.29: Eigenschaften der AddIn Class
17.9.4 Anpassen des Standardmoduls Das Standardmodul statten Sie mit einer einzigen Anweisung aus. Diese stellt eine öffentlich zugängliche Objektvariable für den Zugriff auf das Objektmodell der VBA-Ent wicklungsumgebung zur Verfügung: Public objVBE As VBIDE.VBE
920
Anpassen der Entwicklungsumgebung
17.9.5 Weitere Einstellungen Da Sie mit den in der VBA-Entwicklungsumgebung angelegten Menüs vermutlich Funktionen zur Automatisierung dieser Entwicklungsumgebung durchführen möchten, benötigen Sie noch entsprechende Verweise auf zwei Bibliotheken, die Sie wie folgt festlegen: Öffnen Sie den Dialog Verweise über den Menüeintrag Projekt|Verweise und aktivieren Sie dort den Eintrag Microsoft Visual Basic for Applications Extensibility 5.3. Zum Anlegen von Menüleisten benötigen Sie ebenfalls einen Verweis; die entsprechende Bibliothek heißt Microsoft Office x.y Object Library.
17.9.6 Projekt speichern Damit Sie später noch wissen, welches Objekt welche Aufgabe hat, vergeben Sie nun sinnvolle Namen und speichern das komplette Projekt. Nennen Sie das Standardmodul mdlGlobal, die AddIn Class AddInDesigner und vergeben Sie für das Projekt den Namen VBEMenus (wobei VBE für Visual Basic Editor steht). Anschließend speichern Sie das Projekt in einem Verzeichnis Ihrer Wahl. Die Speicher namen der einzelnen Objekte werden von den Bezeichnungen abgeleitet, die Sie vergeben haben – diese können Sie einfach übernehmen. Die Vorarbeiten sind damit abgeschlossen. Sie können den Zwischenstand testen, indem Sie dem Modul des Add-In-Designers eine Anweisung zum Anzeigen eines Meldungsfensters hinzufügen, das Projekt und damit die .dll-Datei erstellen und die VBA-Entwicklungsumgebung aufrufen: Öffnen Sie das Codemodul des Add-In-De signers. Wählen Sie im linken Kombinationsfeld des Codefensters den Eintrag AddinInstance aus. Die Prozedur AddinInstance_OnConnection wird automatisch angelegt. Fügen Sie dieser Prozedur lediglich eine MsgBox-Anweisung mit dem gewünschten Text hinzu. Kompilieren Sie das Projekt mit dem Menübefehl Datei|VBMenus.dll erstellen… (nach Murphy’s Gesetz ist die Wahrscheinlichkeit hoch, dass das Projekt sich nicht kompilieren lässt – gehen Sie in diesem Fall einfach noch einmal die Beschreibung durch). Wenn die .dll-Datei erstellt wurde, öffnen Sie Access und rufen die VBA-Entwick lungsumgebung auf. Wenn das Meldungsfenster wie geplant erscheint, ist bis hierhin alles in Ordnung – nun geht es mit der eigentlichen Funktionalität weiter.
921
Kapitel 17
17.9.7 Hinzufügen der Funktionen und Menüs Die folgenden Beschreibungen beziehen sich alle auf das Codemodul des Add-In-Desig ners. Die prinzipielle Funktionstüchtigkeit haben Sie ja bereits mit dem Meldungsfenster in der Prozedur AddinInstance_OnConnection bewiesen – die entsprechenden Anweisungen können Sie daher nun entfernen und durch wirklich nützlichen Code ersetzen.
Grundgerüst erstellen Das Programmieren benutzerdefinierter Menüleisten und Schaltflächen mit entsprechenden Funktionen erfordert ein kleines Grundgerüst und für jede Schaltfläche ein wenig Code. Der größte Teil der Arbeit liegt im Entwickeln der eigentlichen Funktionen, die durch die einzelnen Schaltflächen ausgelöst werden. Grundlage für die Verwendung der Schaltflächen der Menüleiste und das Auslösen der entsprechenden Ereignisprozeduren sind einige modulweit deklarierte Variablen. Sie benötigen für jede Schaltfläche eine Objektvariable des Typs CommandBarButton und für das entsprechende Ereignis eine Objektvariable des Typs CommandBarEvents. Der Kopf des Moduls sieht, wenn Sie nur eine Schaltfläche verwenden, folgendermaßen aus: Option Explicit Dim cbb As CommandBarButton Dim WithEvents evt As CommandBarEvents
Da hier zunächst die Struktur der AddinInstance_OnConnection-Prozedur und der Schalt flächenereignisse beschrieben werden soll, heißen die Variablen einfach dem Typ entsprechend »cbb« für CommandBarButton und »evt« für CommandBarEvents. Wenn Sie später mehrere Schaltflächen einfügen, benötigen Sie für jede Schaltfläche je eine dieser Objektvariablen. Am besten verwenden Sie als Variablennamen die beiden Präfixe »cbb« und »evt« zuzüglich eines Namens, der die enthaltene Funktion beschreibt – etwa cbbZeilenNummerieren und evtZeilenNummerieren. Die folgende Ereignisprozedur kennen Sie bereits – sie wird von der VBA-Entwick lungsumgebung aufgerufen, wenn diese geladen wird. Da Sie in den Eigenschaften des Add-In-Designers das anfängliche Ladeverhalten auf Startup eingestellt haben, wird die Prozedur beim nächsten Start der VBA-Entwicklungsumgebung auf jeden Fall aufgerufen (weiter oben beim Erstellen des Toolwindows wurde das Startverhalten auf None eingestellt – dort musste der Anwender das COM-Add-In zunächst über den Add-InManager aktivieren). Die Prozedur erstellt zunächst einen Verweis auf das Objektmodell der VBA-Entwick lungsumgebung. Diesen Verweis benötigen Sie, um dort eine Symbolleiste und die Schaltflächen anlegen zu können; außerdem stellt das Objektmodell die Methoden und
922
Anpassen der Entwicklungsumgebung
Eigenschaften für den Zugriff auf die Module und den enthaltenen Code zur Verfügung. Wenn Sie mehr als eine Symbolleiste anlegen möchten, müssen Sie die entsprechenden Zeilen der Prozedur mehrere Male wiederholen. Zum Anlegen mehrerer Schaltflächen wiederholen Sie lediglich die letzten fünf Zeilen der Prozedur für jede Schaltfläche einmal – genau wie die beiden weiter oben beschriebenen Deklarationszeilen. Private Sub AddinInstance_OnConnection(ByVal Application As Object, _ ByVal ConnectMode As AddInDesignerObjects.ext_ConnectMode, _ ByVal AddInInst As Object, custom() As Variant) Dim cbr As CommandBar 'Verweis auf die aufrufende Anwendung setzen; 'in diesem Fall die VBA-Entwicklungsumgebung Set objVBE = Application 'Hinzufügen einer temporären Menüleiste Set cbr = objVBE.CommandBars.Add("VBETools", msoBarTop, , True) 'Sichtbarmachen der Menüleiste cbr.Visible = True 'Hinzufügen einer Schaltfläche zur Menüleiste Set cbb = cbr.Controls.Add(msoControlButton, , , , True) 'Anpassen der Eigenschaften der Schaltflächen cbb.Style = msoButtonIconAndCaption cbb.FaceId = 2950 cbb.Caption = "Beispielbutton" 'Ereignis für die Schaltfläche festlegen Set evt = objVBE.Events.CommandBarEvents(cbb) End Sub Listing 17.23: Diese Prozedur wird beim Öffnen der VBA-Entwicklungsumgebung ausgeführt (vorausgesetzt, die .dll-Datei ist ordnungsgemäß registriert)
Fehlt noch die Ereignisprozedur, die beim Anklicken der Schaltfläche ausgeführt werden soll. Das Anlegen des Prozedurrumpfs ist ein Kinderspiel: Dazu wählen Sie einfach aus dem linken Kombinationsfeld des Codefensters den Eintrag evt aus – Visual Studio legt dann automatisch die Click-Ereignisprozedur an. Eine solche Prozedur benötigen Sie später für jede anzulegende Schaltfläche. Sobald Sie eine neue Objektvariable des Typs CommandBarEvents erstellt haben, steht der Eintrag im entsprechenden Kombinationsfeld des Codefensters zur Verfügung.
923
Kapitel 17 Private Sub evt_Click(ByVal CommandBarControl As Object, _ handled As Boolean, CancelDefault As Boolean) 'Testanweisung für das Ereignis MsgBox "Die Menü-Schaltfläche funktioniert!" End Sub Listing 17.24: Ereignis einer Menüschaltfläche
Zugriff auf die Objekte der VBA-Entwicklungsumgebung Wenn Sie obiges Grundgerüst mit den in den vorherigen Abschnitten vorgestellten Prozeduren ausstatten, können Sie beliebige Manipulationen der Module und des enthaltenen Codes durchführen. Die meisten zu automatisierenden Vorgänge werden sich dabei auf den Inhalt des aktuellen Codefensters beziehen, das sich sehr leicht referenzieren lässt. Ein wenig interessanter wird es, wenn weitere Informationen für die Operationen notwendig sind, also wenn Sie beispielsweise einen Assistenten zum Anlegen der MsgBoxAnweisung benötigen oder eine Objektklasse für die Daten einer Tabelle automatisch erstellen lassen möchten – Sie müssen dann eine Benutzungsschnittstelle für die Eingabe der Daten zur Verfügung stellen. Für einfache Ansprüche reicht möglicherweise die InputBox-Anweisung aus, mit der sich jeweils ein Wert abfragen lässt. In den meisten Fällen ist aber die Verwendung eines Formulars erforderlich. Natürlich bietet VB auch die Möglichkeit, Formulare zu erstellen und diese in Com-AddIns zu verwenden. Leider würde eine Beschreibung der entsprechenden Vorgehensweise nicht nur den Umfang, sondern auch den thematischen Rahmen des Buchs sprengen. Weitere diesbezügliche Informationen erhalten Sie in Fachbüchern zum Thema Visual Basic und auf den entsprechenden Internetseiten.
924
18 Sicherheit von AccessDatenbanken Wenn bei Access-Datenbanken von »Schutz« oder »Sicher heit« die Rede ist, kann dies ganz unterschiedliche Gründe haben. Der Entwickler beispielsweise möchte seinen in langen Nächten produzierten Code vor den Blicken des Kunden verbergen, um sich Exklusivrechte an eventuellen Erweiterungen zu sichern, der Lottospieler schützt die Da tenbank zum Ermitteln des sicheren Sechsers per Kennwort vor seinen Kollegen und der Geschäftsführer einer Firma möchte selbst auf alle Daten zugreifen, aber den Mitarbei tern nur die jeweils relevanten Daten zeigen. Eines vorneweg: Access 2007 unterstützt in .accdb-Dateien nicht mehr das alte Sicherheitssystem von Access, das Si cherheit auf Gruppen- oder Benutzerebene liefern sollte. Die Betonung liegt hier auf »sollte«, denn es war seit langem bekannt, dass dieses Sicherheitssystem leicht ausgehebelt werden konnte. Aus diesem Grund hat sich Microsoft entschlossen, dieses System zum Schutz der Anwender komplett zu entsorgen. Schade ist dies für Entwickler, die das Sicherheitssystem weniger zur Gewährleistung der Sicherheit, als vielmehr zur Benutzer- und Gruppenverwaltung eingesetzt haben – etwa, um je nach Benutzer den Zugriff auf bestimmte Formulare, Funktionen oder Berichte freizugeben. In den nächsten Abschnitten finden Sie nicht nur eine Antwort, wie Sie das Sicherheitssystem für sicherheits- und kom forttechnische Zwecke ersetzen, sondern erfahren auch Näheres zu den folgenden Aufgabenstellungen:
Kapitel 18
Verbergen des Codes durch Umwandlung in eine .accde-Datenbank Schützen des Codes durch ein Kennwort Schützen der Datenbank durch Aktivieren des Kennwortschutzes Verschlüsseln einer Datenbank Kein Sicherheitssystem – was nun? Die Beispieldateien zu diesem Kapitel finden Sie auf der Buch-CD im Verzeichnis \Kap_18.
18.1 Code schützen per .accde-Datenbank Wer viel Zeit und Hirnschmalz in die Entwicklung einer Anwendung gesteckt hat, möchte diese möglicherweise nicht mit frei zugänglichem Quellcode weitergeben. Access bietet hier einen sehr zuverlässigen Schutz: Dabei wandeln Sie die Datenbank einfach in eine so genannte .accde-Datei um und verhindern so den Zugriff auf den in Formular-, Berichts-, Klassen- und Standardmodulen enthaltenen Quellcode. Die Umwandlung ist äußerst einfach: Sie rufen einfach nur den Ribbon-Eintrag Datenbanktools|Datenbank tools|ACCDE erstellen auf und geben im folgenden Dialog den Namen der zu erstellenden .accde-Datei ein. Gegebenenfalls können Sie hier auch noch einen alternativen Pfad auswählen. Einen kleinen Haken hat die Sache allerdings, wenn Sie mit Access 2007 arbeiten, aber eine Datei im Format von Access 2000, 2002 oder 2003 geöffnet haben. Access bietet dann im Ribbon zwar einen Eintrag an, um eine Datenbank in eine MDE-Datei zu konvertieren, allerdings funktioniert dieser nicht. Stattdessen erscheint die Meldung, dass Sie die Datenbank für das Konvertieren zunächst in eine .accdb-Datenbank umwandeln müssen – das ist nicht ganz konsistent, denn dann erhält man eine .accde-Datenbank und keine .mde-Datenbank. Wie auch immer: Wenn Sie eine mit einer älteren AccessVersion erstellte Datenbank in eine .mde-Datenbank umwandeln möchten, brauchen Sie die jeweilige Access-Version. Access 2007 erlaubt wie die Vorgängerversionen nur die Umwandlung von Datenbanken im gleichen Format. Wenn Sie die Datenbank im Access 2000- oder Access 2002/2003-Format weitergeben möchten, benötigen Sie die entsprechende Access-Version für die Konvertierung in eine .mde-Datenbank. Nach dem Konvertieren ist die Entwurfsansicht von Formularen, Berichten, Seiten, Mak ros und Modulen gesperrt und außer Makros lässt sich auch keines der genannten Ob jekte neu anlegen. Lediglich Tabellen und Abfragen entziehen sich der Sperrung der Entwurfsansicht – sie sind offen zugänglich und können auch neu angelegt werden.
926
Sicherheit von Access-Datenbanken
Um keine falschen Hoffnungen zu wecken, deaktiviert Access auch alle Möglichkeiten zum Kopieren oder Exportieren (siehe Abbildung 18.1).
Abbildung 18.1: In .accde-Datenbanken sind die Änderungsmöglichkeiten einiger Objekte stark eingeschränkt
Löschen Sie nach der Erstellung der .accde-Datenbank auf gar keinen Fall die Original datei. Die .accde-Datenbank bietet keine reguläre Möglichkeit zur Rückumwandlung in eine normale Datenbankdatei (möglicherweise gibt es in Zukunft Dienstleister, die ein solches Reengineering gegen Entgelt durchführen). Wenn Sie nur Teile Ihres Codes schützen möchten – etwa die Sammlung Standardfunk tionen, die Sie im Laufe der Jahre zusammengetragen haben –, gibt es eine verfeinerte Variante: Speichern Sie die relevanten Objekte in einer separaten Datenbank und binden Sie diese per Verweis in die eigentliche Datenbank ein. Natürlich müssen Sie die separate Datenbank zuvor in eine .accde-Datenbank umwandeln. Auf diese Weise kann der Benutzer zumindest die anwendungsspezifischen Objekte wie Formulare und Berichte bearbeiten beziehungsweise eigene Objekte hinzufügen.
18.2 Code schützen per Kennwort Wenn Sie das VBA-Projekt nicht direkt als .accde-Datei weitergeben wollen, sondern die Möglichkeit offen halten möchten, noch einmal Änderungen daran durchzuführen, können Sie dem VBA-Projekt und den enthaltenen Modulen auch ein Kennwort zuweisen.
927
Kapitel 18
Dazu öffnen Sie im exklusiven Modus den Dialog – Projekteigenschaften mit dem Menübefehl Extras|Eigenschaften von , wobei immer für den jeweiligen Projektnamen steht. Im Dialog wechseln Sie zur Registerseite Schutz, haken das Kontrollkästchen Projekt für die Anzeige sperren an und geben zweimal das gleiche Kennwort ein (siehe Abbildung 18.2).
Abbildung 18.2: Dialog zum Anlegen eines Kennworts für ein VBA-Projekt
Nach dem nächsten Öffnen von Access müssen Sie das Kennwort eingeben, bevor Sie auf die enthaltenen Module zugreifen können.
18.3 Einfacher Kennwortschutz mit Verschlüsselung Für den schnellen Rundumschutz bietet Access einen einfachen Kennwortschutz, der seit der Version 2007 mit einer Verschlüsselung der Datenbank einhergeht. Damit verhindern Sie das Öffnen einer Datenbank durch Unbefugte und sorgen gleichzeitig dafür, dass ihr Inhalt nicht mit einem Hex-Editor gelesen werden kann. Um ein Kennwort für eine Datenbank anzulegen, verwenden Sie den Ribbon-Eintrag Datenbanktools|Daten banktools|Mit Kennwort verschlüsseln. Im nun erscheinenden Dialog geben Sie das Kenn wort zweimal ein (siehe Abbildung 18.3). Beim nächsten Öffnen der Datenbank erscheint ein Dialog, der den Benutzer zur Einga be des Datenbankkennworts auffordert und bei Eingabe eines falschen Kennworts den Zugang verwehrt.
928
Sicherheit von Access-Datenbanken
Das Zuweisen eines Datenbankkennworts erfordert, dass die Datenbank im ExklusivModus geöffnet ist.
Abbildung 18.3: Zuweisen eines Datenbankkennworts
18.4 Vertrauensstellungscenter Access 2003 lieferte einige Mechanismen, mit denen man sich vor bösartigem Code und bösartigen SQL-Statements schützen konnte. Diese Vorrichtungen waren sehr sinnvoll, aber das ständige Erscheinen von Sicherheitsmeldungen beim Öffnen einer Datenbank brachte viele Benutzer dazu, diese Mechanismen abzuschalten – die Access-FAQ von Karl Donaubauer führt sogar einen eigenen Eintrag speziell zu diesem Thema [1]. In Access 2007 gibt es diesbezüglich einige Neuerungen, die in erster Linie die Anzahl der zu bestätigenden Sicherheitsmeldungen reduzierten. Nach dem Öffnen einer Datenbank prüft Access 2007 aufgrund einer von zwei Bedingungen, ob die Datenbank vertrauenswürdig ist und alle enthaltenen Funktionen komplett zur Verfügung stehen: Vertrauenswürdige Speicherorte: Sie können in den Access-Optionen im Bereich Ver trauensstellungscenter|Microsoft Office-Access Vertrauensstellungscenter mit der Schalt fläche Einstellungen für das Vertrauensstellungscenter den Dialog aus Abbildung 18.4 öffnen und dort Verzeichnisse festlegen, aus denen Access-Datenbanken ohne Wenn und Aber geöffnet werden. Vertrauenswürdige Herausgeber: Sie können wie in Access 2003 festlegen, dass Daten banken von bestimmten Anbietern sicher sind. Dazu versieht dieser die Anwendung mit einer digitalen Signatur (weitere Informationen siehe weiter unten). Die Einstellungen für vertrauenswürdige Herausgeber und Speicherorte greifen natürlich nur, wenn Sie entsprechende Einstellungen für die Makros vorgenommen haben. Diese finden Sie im gleichen Dialog im Bereich Einstellungen für Makros (siehe Abbil dung 18.5). Mit der Einstellung aus der Abbildung brauchen Sie sich nicht um ver trauenswürdige Herausgeber und Speicherorte zu kümmern – Access führt so alle Da tenbanken in vollem Umfang aus. Keine Frage, dass dies nicht besonders sicher ist; Sie sollten also eine höhere Sicherheitsstufe wählen.
929
Kapitel 18
Abbildung 18.4: Einstellen von vertrauenswürdigen Verzeichnissen
Abbildung 18.5: Einstellungen für das Ausführen von Makros
930
Sicherheit von Access-Datenbanken
Wenn Sie etwa die Option Alle Makros mit Benachrichtigung deaktivieren ausgewählt haben, blendet Access unterhalb des Ribbons eine Leiste mit einer Sicherheitswarnung ein, zu der Sie mit der Schaltfläche Optionen... weitere Informationen anzeigen lassen können (siehe Abbildung 18.6).
Abbildung 18.6: Wenn Sie eine nicht als vertrauenswürdig eingestufte Anwendung öffnen, erscheint eine Sicherheitswarnung und Sie können den Dialog aus dieser Abbildung anzeigen lassen
Mit der Option aus dem Bereich Statusleiste des Vertrauensstellungscenters können Sie auch die Anzeige der Statusleiste unterbinden (siehe Abbildung 18.7). Bei dieser Einstel lung bekommt der Benutzer nichts davon mit, wenn der VBA-Code einer Anwendung gesperrt wurde – außer, dass der VBA-Code eben nicht mehr funktioniert. Das ist natürlich ziemlich schlecht: Wer will schon zahllosen Benutzern klarmachen, dass die Anwendung nicht funktioniert, weil die Sicherheitseinstellungen dies nicht er lauben? Immerhin hat Microsoft sich für diesen Fall etwas einfallen lassen: Es hat näm lich die Makros erheblich aufgewertet. (Hinweis an Quereinsteiger aus der Excel-Ecke:
931
Kapitel 18
Unter Access steht der Begriff Makro für eine spezielle, vereinfachte Art der Automa tisierung, die nur eine kleine Teilmenge von VBA abbildet!)
Abbildung 18.7: Deaktivieren der Anzeige der Statusleiste, die bei Datenbanken mit gesperrtem Inhalt angezeigt wird
Jedenfalls werden einige Makrobefehle, mit denen Sie keinen Schaden anrichten können, von der Sperrung durch die Sicherheitsmechanismen von Access ausgeschlossen, damit Sie zumindest grundlegende Aufgaben auch bei höchster Sicherheit automatisieren können. Benutzen sollten Sie dies allerdings nur in einem Fall: Um beim Öffnen der Datenbank zu prüfen, ob die lokalen Einstellungen von Access das Ausführen aller Funktionen der Anwendung erlauben, und den Benutzer gegebenenfalls darauf aufmerksam zu machen, dass er mit den aktuellen Einstellungen keinen Spaß an der Datenbankanwendung haben wird. Und das geht so: Legen Sie mit dem Ribbon-Befehl Erstellen|Andere|Makro ein neues Makro an. Blenden Sie mit Entwurf|Einblenden/Ausblenden|Bedingungen die Bedingungen-Spalte ein. Fügen Sie die Makro-Aktionen aus Abbildung 18.8 in den Makroentwurf ein. Speichern Sie das Makro unter dem Namen AutoExec. Auf diese Weise wird es direkt beim Start der Datenbank aufgerufen, soweit der Anwender nicht beim Starten die Umschalttaste drückt. Mit diesem Makro stellen Sie sicher, dass der Benutzer auf jeden Fall eine Meldung er hält, dass die Anwendung nur eingeschränkt ausgeführt werden kann – selbst, wenn er im Vertrauensstellungscenter die höchste Sicherheitsstufe aktiviert und die Statusleiste zum Anzeigen von Meldungen deaktiviert hat. Auf diese Weise braucht Sie zumindest kein Benutzer Ihrer Datenbanken zu fragen, warum diese Datenbanken nicht richtig funktionieren. Am besten fügen Sie zum Mel
932
Sicherheit von Access-Datenbanken
dungstext noch einen Hinweis hinzu, wie der Anwender sich Zugang zum vollen Funk tionsumfang verschaffen kann. Aber Achtung: Wenn Sie die Datenbanken nach dem Anzeigen der Meldung wie in obigem Makro schließen, bekommt der Benutzer auch keine Gelegenheit, auf eine eventuell angezeigte Statusleiste mit der Möglichkeit zum Aktivieren des Inhalts der Anwendung zu reagieren. Lassen Sie die mittlere Anweisung des Makros also gegebenenfalls weg. Die letzte Anweisung führt übrigens eine VBA-Funktion aus – damit können Sie dann direkt feststellen, ob die Anwendung wie gewünscht läuft.
Abbildung 18.8: Dieses Makro zeigt eine Meldung an, wenn die Anwendung mangels Vertrauenswürdigkeit nur eingeschränkt ausgeführt wird
18.5 Digitale Signaturen Digitale Signaturen schützen Benutzer von Access-Anwendungen beziehungsweise der dahinter stehenden VBA-Projekte, indem sie diese als »echt« authentifizieren. Das funktioniert folgendermaßen: Eine Signatursoftware bildet einen Hashwert auf Basis des Inhalts der zu signierenden Datei. Der Hashwert wird mit einem privaten Schlüssel verschlüsselt – der resultierende Wert ist die Signatur. Datei und Signatur inklusive öffentlichem Schlüssel werden an den Benutzer weitergegeben.
933
Kapitel 18
Aus der Signatur und dem öffentlichen Schlüssel wird der weiter oben erzeugte Hashwert gewonnen und mit dem Hashwert der signierten Datei verglichen. Stimmen die Hashwerte überein, gilt die Datei als vom angegebenen Absender erzeugt und als seit dem Signieren unverändert übermittelt. Die Sicherheit Ihres Systems erhöhen Sie dadurch nicht unbedingt: Immerhin müssen Sie sich an einem bestimmten Punkt entscheiden, ob Sie die Datenbankanwendung als »gutartig« einschätzen und ausführen oder nicht. Da die digitale Signatur in Zusammen hang mit Access-Anwendungen quasi keinen öffentlichen Zuspruch findet, verweist das Buch an dieser Stelle auf die Onlinehilfe: Unter dem Stichwort »Signatur« finden Sie dort eine Liste von Artikeln zu diesem Thema.
18.6 Schutz vor bösartigen SQL-Statements Eine weitere Maßnahme ist der Sandbox-Modus des ACE Expression Service. Unter Access bietet die ACE die Möglichkeit, VBA-Funktionen in SQL-Ausdrücke einzubinden. Das kann sehr hilfreich sein, bietet aber auch jede Menge Möglichkeiten für Angreifer, ungewünschte Aktionen auf einem Rechner auszuführen. So lässt sich in einer SELECT-Anweisung die Shell-Funktion aktivieren, die bekanntermaßen alle möglichen Anwendungen aufrufen und über die Anwendung cmd.exe auch DOS-Befehle absetzen kann. Mit folgender SQL-Anweisung können Sie etwa bei deaktiviertem Sandbox-Modus leicht eine Datei löschen (hier die Datei c:\test.txt): SELECT (Shell("cmd.exe /c del c:\test.txt"));
Da ist es leicht zu erahnen, dass sich auch umfangreichere Datenbestände mit einer solchen oder ähnlichen Anweisung entfernen lassen. Welche VBA-Anweisungen genau von einer Sperrung im Sandbox-Modus betroffen sind, erfahren Sie unter [2]. Wenn der Sandbox-Modus aktiviert ist und Sie versuchen, eine nicht zulässige Funktion via ACE aufzurufen, erscheint eine Meldung wie in Abbildung 18.9. Das Gleiche passiert, wenn die Makro-Einstellungen die Ausführung von VBA-Code verhindern – auch die betroffenen Anweisungen im SQL-Code werden dann nicht ausgeführt.
Abbildung 18.9: Diese Meldung erscheint beim Einsatz nicht erlaubter VBA-Anweisungen in SQLBefehlen
934
Sicherheit von Access-Datenbanken
Ob Sie den Sandbox-Modus beim Benutzer Ihrer Datenbankanwendung aktivieren müs sen oder nicht, hängt davon ab, ob die enthaltenen Abfragen Funktionen beinhalten, die im Sandbox-Modus gesperrt sind, die aber für die Benutzung der Anwendung Vo raussetzung sind. In diesem Fall bleibt Ihnen nicht viel anderes übrig, als den SandboxModus zu deaktivieren – und auch die Makro-Sicherheit muss natürlich zumindest für diese Datenbank deaktiviert sein. Den Sandbox-Modus können Sie über die Registry von Windows einstellen. Er erfordert eine computerweite Einstellung. Dementsprechend ist der passende RegistrySchlüssel im Pfad HKEY_LOCAL_MACHINE zu finden, und zwar unter HKEY_LOCAL_ MACHINE\SOFTWARE\Microsoft\Office\12.0\Access Connectivity Engine\Engines (siehe Abbildung 18.10). Folgende Einstellungen sind möglich: 0: Der Sandbox-Modus ist für alle Anwendungen, die auf die ACE zugreifen, deaktiviert. 1: Der Sandbox-Modus ist nur für Access-Anwendungen aktiviert. 2: Der Sandbox-Modus ist nur für Nicht-Access-Anwendungen aktiviert. 3: Der Sandbox-Modus ist immer aktiviert.
Abbildung 18.10: Registry-Einstellung für den Sandbox-Modus
18.7 Kein Sicherheitssystem — was nun? Wer mehr möchte, als einfach nur den Zugriff auf Daten zu verhindern – etwa um ver schiedenen Benutzern und Benutzergruppen unterschiedliche Berechtigungen für die einzelnen Datenbankobjekte und die enthaltenen Daten zu gewähren, hat bisher das Sicherheitssystem von Access verwendet. Dieses ist zumindest insoweit nicht mehr
935
Kapitel 18
vorhanden, als Sie es mit Access 2007 nicht mehr einrichten können. Datenbankanwen dungen, die Sie mit einer älteren Version von Access geschützt haben, werden allerdings auch unter Access 2007 unter Einsatz des Sicherheitssystems geöffnet. Sie brauchen also erstens keine Sorge tragen, dass jemand einfach so mit der neuen AccessVersion auf die Daten Ihrer Anwendung zugreifen kann, und können zweitens immer noch Datenbanken mit älteren Access-Versionen schützen und diese unter Access 2007 einsetzen. Der einzige Nachteil ist, dass Sie geschützte Datenbanken nicht in das Datenbankformat von Access 2007 konvertieren können. Möglicherweise sind Sie noch in Besitz einer älteren Version von Access 2003 und möchten einfach eine .mdb-Datenbankanwendung für den Einsatz mit Access 2007 erstellen und diese mit dem Sicherheitssystem schützen. Dann können Sie natürlich wie gehabt vorgehen. Wie das funktioniert, erfahren Sie am Ende dieses Kapitels. Sie sollten sich jedoch im Klaren darüber sein, dass das Sicherheitssystem von Access nicht wirklich sicher ist und dass jemand, der unbedingt an die Daten einer geschützten Datenbank herankommen möchte, dies auch bewerkstelligen kann.
18.7.1 Benutzer- und gruppenabhängige Benutzeroberfläche Wenn Sie das Sicherheitssystem von Access nur dazu verwenden, Benutzer und Grup pen zu verwalten, damit diese nach der Anmeldung eine individuell auf die jeweilige Benutzergruppe zugeschnittene Benutzeroberfläche verwenden können, schießen Sie mit Kanonen auf Spatzen. Sie sollten stattdessen drei Tabellen anlegen, in denen Sie Benutzer, Gruppen und die Zuteilung von Benutzern zu Gruppen vornehmen und dort die benötigten Daten speichern. Fügen Sie Ihrer Datenbankanwendung einen Anmeldedialog hinzu, mit dem die Benutzer sich identifizieren können (wenn es sein muss, auch mit Kennwort). Die Daten über den aktuellen Benutzer speichern Sie im einfachsten Fall in einer globalen Variablen, besser aber in einer Klasse oder einer Konfigurationstabelle. Für die Steuerung der Benutzeroberfläche in Abhängigkeit vom aktuellen Benutzer müssen Sie ja auch unter Verwendung des Sicherheitssystems entsprechende Funktionen bereitstellen, die den aktuellen Benutzer beziehungsweise die Benutzergruppe auslesen und die entsprechenden Elemente der Benutzeroberfläche bereitstellen.
18.7.2 Daten schützen: Alternativen Wenn Sie Ihre Daten mit einem Sicherheitssystem schützen wollen, das Funktionen zur Verwaltung von Benutzern und Benutzergruppen bereitstellt, verwenden Sie am besten einen SQL-Server zum Verwalten Ihrer Daten. Eine Vielzahl der Hinweise in der
936
Sicherheit von Access-Datenbanken
Communitiy führt in Richtung von Microsoft-Produkten wie dem Microsoft SQL Server 2005 oder dem kostenlosen Microsoft SQL Server 2005 Express Edition. Microsoft empfiehlt, von Access aus per ODBC auf die Daten eines SQL-Servers zuzugreifen, es ist aber auch noch möglich, ein Access-Projekt dafür zu verwenden. Davon abgesehen muss es aber auch gar nicht unbedingt ein Microsoft-Produkt sein: Wenn Sie schon – wie von Microsoft empfohlen – per ODBC auf den SQL-Server zugreifen und damit auf die Vorzüge der Access-Projekte verzichten, können Sie auch direkt eine Alternative wie MySQL oder eine der anderen teilweise frei erhältlichen SQL-Server verwenden. Da es im Web von Dokumentationen zum Thema Access in Zusammenarbeit mit dem Microsoft SQL Server wimmelt und auch jedes Fachbuch, das sich mit dem Thema Access/SQL-Server beschäftigt, sich auf den Microsoft SQL Server bezieht, soll dieses Buch mit MySQL einmal eine alternative Lösung vorstellen. Sie finden in den folgenden Abschnitten zwar Hinweise zum Download der Express Edition des Microsoft SQL Servers und dessen Benutzeroberfläche, dem Microsoft SQL Server Management Studio, aber für weitere Informationen ziehen Sie bitte die jeweilige Dokumentation heran. Das Gleiche gilt für das von Microsoft ohnehin nicht mehr empfohlene Gespann AccessProjekt (.adp)/Microsoft SQL Server.
SQL Server 2005 Express Edition Die kostenlose Express Edition des SQL Server 2005 können Sie unter [3] herunterladen. Diese Software benötigt wie auch das SQL Server Management Studio Express Edition das .NET Framework 2.0 (zu finden unter [4]).
SQL Server Management Studio Express Edition Wenn Sie – wie von Microsoft empfohlen – nicht per Access-Projekt, sondern per ODBC auf eine Microsoft SQL Server-Datenbank zugreifen möchten, können Sie zur Adminis tration der Datenbank das kostenlose SQL Server Management Studio Express verwen den. Sie finden es unter [5].
Access-Projekte Ein Access-Projekt ist ein Datenbankformat, das für den Zugriff auf Datenbanken im Microsoft SQL Server spezialisiert ist. Sie legen eine solche Datenbank an, indem Sie im Dialog zum Auswählen des Verzeichnisses und des Namens der neuen Datenbank als Dateityp Microsoft Office Access-Projekte (*.adp) auswählen (siehe Abbildung 18.11). Voraussetzung für das Anlegen eines Access-Projekts ist das Vorhandensein eines laufenden Microsoft SQL Servers oder der jeweils kostenlosen Variante. Alle weiteren Schritte
937
Kapitel 18
wie das Anlegen von Tabellen, das Verwalten von Benutzern und Benutzergruppen so wie Informationen über SQL Server-Eigenheiten wie Views, Trigger und Stored Procedu res würden den Rahmen dieses Buchs bei Weitem sprengen.
Abbildung 18.11: Anlegen eines Access-Projekts
Datenbanken mit ODBC-Schnittstelle ODBC hat den Vorteil, dass Sie damit auf beliebige SQL-Server zugreifen können, sofern diese eine passende ODBC-Schnittstelle bereitstellen. Ein Beispiel ist MySQL: MySQL dient als Datenbank für zahlreiche Web-Anwendungen und liefert mit MyODBC eine ODBC-Schnittstelle, die Sie von Access aus leicht ansprechen können. MySQL gibt es nicht nur für Windows-Betriebssysteme, sondern auch für Unix- und Linux-basierte Betriebssysteme sowie für MacOS. Der Nachteil bei mit ODBC verknüpften Tabellen ist, dass Sie die Datenbank mit den passenden Tools des jeweiligen SQL-Servers verwalten müssen.
18.8 MySQL MySQL ist ein Open Source-SQL-Server, der wegen seiner Geschwindigkeit und seiner guten Sicherheitseigenschaften oft für Internetanwendungen im Einsatz ist. Da sehr viele
938
Sicherheit von Access-Datenbanken
Internetprovider MySQL in mittleren bis großen Hosting-Paketen als Datenbankserver anbieten, besitzt es eine Sonderstellung unter den freien SQL-Servern. MySQL ist für den Einsatz in Internetanwendungen und anderen Anwendungen in Mehrbenutzerumgebungen vorgesehen. Es kommt im Vergleich zu Access ohne eigene Benutzeroberfläche daher. Es gibt allerdings einige Tools, die Sie zum Verwalten von MySQL-Datenbanken einsetzen können. In Kombination mit Access liefert MySQL eine Datenbankanwendung mit einem sicheren und performanten Backend und einer je nach Anforderung gestalteten Benutzer oberfläche. Genau genommen können Sie bei der Gestaltung der Benutzeroberfläche genauso vorgehen, wie Sie es von reinen Access-Anwendungen gewohnt sind – der einzige Unterschied ist in der Tat, dass die Tabellen nur eingebunden und nicht physisch in der Access-Datei enthalten sind. Die Verbindung stellen Sie einfach her, indem Sie die Access-Benutzeroberfläche via ODBC mit der MySQL-Datenbank verknüpfen. Wie sich später zeigen wird, sind die beiden aber nicht in allen Aspekten wie füreinander geschaffen – der Export eines bestehenden Datenmodells nach MySQL ist unter Umständen mühselig, weil keine Informationen über Indexfelder oder Beziehungen übernommen werden oder die Da tentypen nicht kompatibel sind. Die lasche Handhabung von Tabellen- und Feldnamen unter Access kann beim Export zu Schwierigkeiten führen, weil MySQL dort wesentlich restriktiver ist und beispielsweise keine Sonderzeichen wie Bindestriche oder Leerzei chen erlaubt. Access eignet sich nicht nur als Frontend für MySQL-Datenbanken im lokalen Netz, sondern auch für die hinter Internetanwendungen stehenden Datenbanken. Die Ent wicklung von Internetanwendungen wie Online-Shops und dergleichen ist meist so aufwändig, dass eine Benutzeroberfläche für die Administratoren beziehungsweise die Betreiber des Onlineshops erst zuletzt oder gar nicht realisiert wird. Das ist auch gar nicht unbedingt sinnvoll: Die Anwendung soll zunächst den Anwendern beziehungsweise Kunden eine ergonomische Oberfläche und schnelle Lieferung der gewünschten Daten via Webbrowser garantieren. Der Administrator oder Betreiber braucht ja gar kein Webfrontend: Für ihn ist ein Rich Client wesentlich sinnvoller. Es sind meist ohnehin nur eine Handvoll Personen, die administrativ auf die Datenbank einer Internetanwendung zugreifen. Bevor man also mit einem rudimentären Webfrontend arbeitet oder gar direkt auf dem Server per Kommandozeile auf die Datenbank zugreift, kann man sich viel besser eine AccessAnwendung bauen, diese per ODBC mit der Datenbank verbinden und schnell die benötigten Abfragen, Formulare und Berichte erstellen. Gerade die Möglichkeit, Auswertungen per Bericht und nicht mit aufwändig gestalteten HTML-Ausgaben zu erstellen, macht die Sache äußerst reizvoll.
939
Kapitel 18
Die einzige Frage ist, wie Sie eine sichere Verbindung zwischen dem lokalen AccessFrontend und dem beispielsweise auf einem Internetserver befindlichen MySQL-Back end herstellen. Wie dies funktionieren kann, lesen Sie weiter unten. Zunächst sollen Sie jedoch erfahren, wie Sie MySQL und MyODBC installieren, wie Sie MySQL zum Laufen bringen, was dabei zu beachten ist und wie Sie eine lokale Ver bindung zwischen einer Access- und einer MySQL-Datenbank herstellen. Bleibt das Thema Upsizing: Aufgrund der vielen Fallstricke lässt sich das Thema nicht umfassend in diesem Kapitel abhandeln, aber Sie erhalten zumindest einige Hin weise.
18.8.1 MySQL installieren Für den Betrieb von MySQL als Backend für Access ist neben MySQL selbst ein entsprechender ODBC-Treiber erforderlich. Im Rahmen der nachfolgenden Beispiele wurde die Version 5.0 von MySQL sowie die Version 3.51.12 von MyODBC verwendet. Die Installationsdateien von MySQL finden Sie im Internet unter [6]. Der Start der Installation erfolgt nach den Entpacken über den Aufruf der Datei Setup.exe. Die in den einzelnen Installationsschritten angebotenen Optionen können Sie problemlos übernehmen – bis auf drei: Wählen Sie die Standardinstallation aus (siehe Abbildung 18.14) und sorgen Sie dafür, dass das Setup direkt das bin-Verzeichnis von MySQL in die Path-Variable aufnimmt (siehe Abbildung 18.13).
Abbildung 18.12: Auswählen der Standardinstallation von MySQL
940
Sicherheit von Access-Datenbanken
Abbildung 18.13: Das Setup soll das bin-Verzeichnis zur Path-Variablen hinzufügen
Und geben Sie in jedem Fall ein Kennwort für den Benutzer root an; das Anlegen eines anonymen Benutzerkontos sollten Sie aus Sicherheitsgründen vermeiden. Aktivieren Sie außerdem im Dialog aus Abbildung 19.14 die Option Enable root access from remote machines, wenn Sie standardmäßig von anderen Rechnern aus als root auf den MySQLServer zugreifen möchten.
Abbildung 18.14: Vergabe eines Kennworts und Aktivieren des Zugriffs von anderen Rechnern
941
Kapitel 18
Die Installationsroutine installiert alle Dateien in ein Verzeichnis namens c:\mysql (soweit Sie kein anderes Verzeichnis angegeben haben). Im Unterverzeichnis bin befinden sich alle wichtigen ausführbaren Dateien von MySQL, darunter auch ein grafisches Administrationstool – dazu später mehr. Im Rahmen der vorgesehenen Anwendung sollten Sie außerdem wissen, dass das Unterverzeichnis data alle MySQL-Datenbanken enthält. Für Informationen über MySQL, die über die in diesem Buch enthaltenen hinausgehen, bietet sich die Lektüre des im Verzeichnis Docs enthaltenen Benutzerhandbuchs von MySQL an. Nach der Installation zeigt sich das System äußerlich unverändert, MySQL legt keine Einträge im Startmenü oder auf dem Desktop an. Dafür finden Sie – wenn Sie nicht die obere Option aus Abbildung 18.13 abgewählt haben – in der Systemsteuerung unter Verwaltung|Dienste (siehe Abbildung 18.15) den Eintrag des zu MySQL gehörenden Dienstes. Sie können diesen Dienst wie alle anderen vorhandenen Dienste konfigurieren, starten und stoppen.
Abbildung 18.15: MySQL läuft als Windows-Dienst
Sie können den MySQL-Server auch über die Kommandozeile starten und stoppen: NET START mysql NET STOP mysql
18.8.2 Einfache Konfiguration Mit dem Konfigurationstool, das Sie über den Startmenü-Eintrag Alle Programme|My SQL|MySQL Server 5.0|MySQL Server Instance Config Wizard aufrufen, können Sie die
942
Sicherheit von Access-Datenbanken
Konfiguration des MySQL-Servers nachträglich ändern. Das geht auch manuell: Die passende Datei finden Sie standardmäßig im Verzeichnis c:\Programme\MySQL\ MySQL Server 5.0\my.ini.
18.8.3 MySQL-Anweisungen Sobald MySQL läuft, können Sie das Kommandozeilentool mysql über die Eingabeauf forderung starten. Um sich im Kontext des soeben angelegten Benutzers root mit dem von Ihnen gewählten Kennwort anzumelden, verwenden Sie folgende Anweisung: mysql -u root -p
MySQL fragt dann das Kennwort ab und ist aufnahmebereit (siehe Abbildung 18.16).
Abbildung 18.16: Nach der Anmeldung ist MySQL bereit für Benutzereingaben
Mit der Anweisung status erfahren Sie unter anderem, unter welchem Benutzernamen die aktuelle Session läuft. Als Benutzer root können Sie direkt einmal einen Blick auf die vorhandenen Datenbanken werfen. Die folgende Anweisung bewirkt die Ausgabe der Liste aus Abbildung 18.17: show databases;
Um die Tabellen einer der Datenbanken anzuzeigen, wechseln Sie zunächst zu dieser: use mysql
Anschließend zeigen Sie mit folgender Anweisung eine Liste der Tabellen der aktuellen Datenbank an (siehe Abbildung 19.18):
943
Kapitel 18 show tables;
Abbildung 18.17: Liste aller vorhandenen Datenbanken
Abbildung 18.18: Liste der Tabellen der MySQL-Systemdatenbank
An dieser Stelle können Sie mit dem Kommandozeilentool MySQL alles mit einer Da tenbank durchführen, was Sie unter Access mit der Benutzeroberfläche erledigen würden: Datenbanken erstellen, Tabellen erstellen und löschen, den Entwurf der Tabellen bearbeiten und mit entsprechenden Auswahl- und Aktionsabfragen Daten ausgeben und bearbeiten. Die dazu erforderlichen SQL-Anweisungen entsprechen weitgehend denen, die Sie auch unter Access verwenden, daher gehen wir an dieser Stelle nicht weiter darauf ein. Zu beachten ist eigentlich nur, dass Sie die Anweisungen in mehreren Zeilen eingeben können und MySQL diese erst beim Auftauchen eines Semikolons abarbeitet.
944
Sicherheit von Access-Datenbanken
Nur einige Anweisungen, die entweder gar keine oder nur wenige Parameter erwarten, werden direkt ausgeführt – wie beispielsweise die Anweisung exit, mit der Sie eine Session beenden.
18.8.4 Sicherheit unter MySQL Sicherheit wird unter MySQL großgeschrieben. Da MySQL im Gegensatz zu Microsoft Access auch hauptsächlich für den Mehrbenutzerbetrieb und für den Einsatz im In ternet gedacht ist, ist das nicht weiter verwunderlich. MySQL bietet ein zweistufiges Sicherheitssystem: In der ersten Stufe legt es fest, welche Benutzer sich – unabhängig von der gewünschten Datenbank – überhaupt anmelden dürfen. Die zweite Stufe ent scheidet, auf welche Datenbanken, Objekte und Daten der angemeldete Benutzer zu greifen darf. Das Sicherheitssystem ist gut verständlich, aber es gibt sehr umfangreiche Möglichkeiten. Daher kann dieses Buch leider nicht im Detail darauf eingehen. Weitere Informationen finden Sie im Referenzhandbuch von MySQL unter [7].
18.8.5 Administrationstool für MySQL Unter [8] finden Sie ein Paket mit Tools für MySQL, darunter ein grafisches Administra tionstool. Das Setup erzeugt einen Eintrag im Start-Menü von Windows, wo Sie das Administrationstool unter Alle Programme|MySQL|MySQL Administrator finden. Das Tool fragt nach dem Start zunächst einige Informationen über den zu verwendenden MySQL-Server ab (siehe Abbildung 18.19). Diese Anmeldung funktioniert nur vom gleichen Rechner aus, auf dem auch der MySQL-Server läuft. Wenn Sie von einem anderen Rechner auf den Server zugreifen möchten, müssen Sie dies bei der Installation wie oben beschrieben angeben.
Abbildung 18.19: Anmeldung an den lokalen MySQL-Server
945
Kapitel 18
Der MySQL Administrator präsentiert sich recht übersichtlich (siehe Abbildung 18.20). Sie können damit komfortabel Benutzer und Berechtigungen vergeben und die Datenbanken verwalten. Weitere Informationen entnehmen Sie bitte der Onlinehilfe des Tools.
18.8.6 Installation von MyODBC Der ODBC-Treiber für MySQL heißt MyODBC. Sie können ihn unter [9] herunterladen. Die Setup-Routine kopiert lediglich einige Dateien in das Verzeichnis c:\WINDOWS\ system32 und registriert diese so, dass Sie von Access aus den passenden ODBC-Treiber verwenden können. Den MyODBC-Treiber installieren Sie auf allen Systemen, von denen aus Sie auf den MySQL-Server zugreifen möchten.
18.9 Access und MySQL Die Verwendung von Access als Frontend einer MySQL-Datenbank setzt das Vorhanden sein einer entsprechenden MySQL-Datenbank voraus. Sollte diese Voraussetzung nicht gegeben sein, erfahren Sie nun zunächst, wie Sie eine bestehende Access-Datenbank nach MySQL upsizen. Anderenfalls fahren Sie mit dem Abschnitt »Verwenden von MySQL-Datenbanken mit Access« fort, wo Sie erfahren, wie Sie ein Access-Frontend mit Tabellen einer MySQL-Datenbank verknüpfen.
18.9.1 Upsizing von Access-Datenbanken auf MySQL Das Upsizing einer Datenbank von Access auf MySQL kann, um direkt auf eventuelle Schwierigkeiten hinzuweisen, alles andere als trivial sein. Unter Upsizing ist in diesem Zusammenhang die Erstellung einer neuen MySQL-Datenbank zu verstehen, der Sie alle Tabellen der für das Upsizing vorgesehenen Datenbank hinzufügen. Für dieses Vorhaben sind prinzipiell drei Wege denkbar: Sie exportieren alle betroffenen Tabellen per Exportieren…-Funktion in die Zieldaten bank. Sie verwenden ein Tool, das Ihnen die Arbeit mehr oder weniger abnimmt. Die beste Wahl treffen Sie derzeit mit dem MySQL Migration Toolkit [11], das sehr ergonomisch gestaltet und einfach zu bedienen ist und nahezu keine Nacharbeit mehr erforderlich macht. Die folgenden Abschnitte beschreiben die erste der drei genannten Möglichkeiten. Sie bietet Gelegenheit, sich ein wenig in den Sprachumfang von SQL unter MySQL einzuarbeiten, beinhaltet allerdings ein wenig Handarbeit. Wenn Sie beispielsweise die Tabellen der Nordwind-Datenbank inklusive Beziehungen, Indizes und Daten exportieren möchten, sind einige Nacharbeiten notwendig.
946
Sicherheit von Access-Datenbanken
Abbildung 18.20: Verwalten von Datenbanken mit dem MySQL Administrator
18.9.2 Export von Tabellen nach MySQL Für diesen Ansatz erstellen Sie zunächst eine neue Datenbank in MySQL. Dazu gehen Sie folgendermaßen vor: Starten Sie die Eingabeaufforderung und wechseln Sie in das Verzeichnis c:\mysql\ bin (soweit Sie MySQL nicht in einem anderen Verzeichnis installiert haben). Melden Sie sich mit der Anweisung mysql –u root –p beim MySQL-Server an und geben Sie das Kennwort ein, soweit erforderlich. Erstellen Sie mit der Anweisung create database Nordwind eine neue Datenbank namens Nordwind. Lassen Sie die Eingabeaufforderung geöffnet, da Sie diese später noch benötigen. Nun öffnen Sie die Nordwind-Datenbank und exportieren eine Tabelle. Dazu verwenden Sie den Eintrag Exportieren|ODBC-Datenbank des Kontextmenüs der zu exportierenden Tabelle im Navigationsbereich. Anschließend erscheint der Dialog zum Auswählen einer
947
Kapitel 18
Datenquelle. Wechseln Sie hier zur Registerseite Computerdatenquelle, klicken Sie auf die Schaltfläche Neu, wählen Sie je nach Bedarf eine der Optionen Benutzerdatenquelle oder Systemdatenquelle und im nächsten Schritt den Treiber MySQL ODBC 3.51 Driver aus. Anschließend erscheint der Dialog aus Abbildung 18.21. Hier tragen Sie Informationen entsprechend der Abbildung ein. Wenn Sie die Verbindung vom selben Rechner herstellen, auf dem sich auch der MySQL-Server befindet, können Sie als Server localhost eintragen. Falls nicht, müssen Sie in der Konfiguration des MySQL-Servers auf jeden Fall festlegen, dass Verbindungen von externen Rechnern zugelassen sind. Aktivieren Sie außerdem auf der Registerseite Advanced noch die Option Return Matching Rows.
Abbildung 18.21: Informationen für den ODBC-Connector
Fertig! Der Dialog zum Auswählen der Datenquelle sollte nun den soeben angelegten Eintrag enthalten (siehe Abbildung 18.22). Bestätigen Sie die Auswahl dieses Eintrags und schließen Sie das nun erscheinende Fenster. Wenn Sie nun in der Eingabeaufforderung mit der Anweisung use Nordwind die neue Datenbank namens Nordwind aktivieren, können Sie dort die folgende Anweisung eingeben, um die vorhandenen Tabellen anzuzeigen: show tables;
Die Tabelle ist, wie die Ausgabe zeigt, erfolgreich exportiert worden. Um sich die Struktur der Tabelle anzusehen, verwenden Sie die folgende Anweisung: describe artikel;
948
Sicherheit von Access-Datenbanken
Abbildung 18.22: Die neue Computerdatenquelle steht bereit
Das Ergebnis der beiden Anweisungen finden Sie in Abbildung 18.23. Es ist schnell zu erkennen, dass beispielsweise keine Informationen über den Primärschlüssel mitexportiert wurden. Das lässt sich auch nicht so einfach nachholen, da MySQL im Vergleich zu Access keine Sonderzeichen – und auch keine Umlaute – in Feldnamen akzeptiert.
Abbildung 18.23: Ausgabe der Tabellen der neuen Nordwind-Datenbank und des Aufbaus der Artikel-Tabelle
Hier beginnen die Probleme des Exports, denn eine Änderung des Primärindexfeldes wäre zwar realisierbar und auch die Fremdschlüsselfelder der mit dieser Tabelle ver-
949
Kapitel 18
knüpften Felder müssten ohnehin noch angepasst werden, aber letzten Endes müssen Sie auch alle in Formularen, Berichten und Modulen der Access-Anwendung vorhandenen Verweise noch anpassen. Eine Möglichkeit wäre, die Tabellen unter allgemein üblichen Namen wie tblArtikel statt Artikel zu exportieren, die kritischen Feldnamen beim Feintuning durch zulässige Namen zu ersetzen, eine Verknüpfung zu den Tabellen herzustellen und eine Abfrage mit dem eigentlichen Tabellennamen (also etwa Artikel) zu erstellen, die alle Felder der verknüpften Tabelle (also tblArtikel) enthält. Da Access Tabellen und Abfragen gleichberechtigt behandelt, funktioniert diese Vorgehensweise. Diese Probleme einmal außen vor gelassen, bietet MySQL alle sprachlichen Möglich keiten, die entsprechenden Felder in Primärindexfelder umzuwandeln – Stichwort für die Lektüre des Referenzhandbuchs ist hier ALTER TABLE. Sie können die Tabelle aber auch mit dem MySQL Administrator anpassen. Abbildung 18.24 zeigt etwa die Entwurfsansicht der Artikel-Tabelle.
Abbildung 18.24: Entwurfsansicht einer Tabelle im MySQL Administrator
Sie sehen: Es gibt eine Menge zu tun, bevor eine Access-Datenbank ins MySQL-Format überführt ist. Der Aufwand lohnt sich aber, wenn Sie die Vorteile von MySQL in Sachen Mehrbenutzer-Fähigkeiten und Sicherheit nutzen wollen.
950
Sicherheit von Access-Datenbanken
18.9.3 Verwenden von MySQL-Datenbanken mit Access Im Optimalfall wissen Sie frühzeitig, dass eine Anwendung gewisse Mehrbenutzerund Sicherheitsanforderungen aufweist und erstellen direkt ein MySQL-Backend. Aber auch hierbei sind einige Punkte zu beachten: Legen Sie für jede Tabelle ein Primärschlüsselfeld fest, da sonst das Anlegen von Datensätzen in einer verknüpften Tabelle über Access nicht möglich ist. Erstellen Sie in jeder Tabelle ein Feld des Typs Timestamp(14). Es hat sich erwiesen, dass ohne die Existenz eines solchen Feldes in den Tabellen Schwierigkeiten beim Löschen und Editieren (Updaten) von Datensätzen auftreten können. Verwenden Sie auf keinen Fall BIGINT-Felder, da diese 64Bit enthalten, die Access nicht komplett auswerten kann. Die Herstellung einer Verbindung zwischen Access und einer MySQL-Datenbank erfordert genau wie der Export die oben beschriebene ODBC-Verbindung. Die nachfolgenden Beispiele gehen davon aus, dass Sie den MySQL-Server auf dem lokalen oder einem per Netzwerk verfügbaren Rechner installiert und zum Laufen gebracht haben und dass dort die zu verknüpfende Datenbank bereits vorhanden ist. Mit den folgenden Schritten erstellen Sie eine Verknüpfung zu einer Tabelle einer MySQL-Datenbank: Betätigen Sie die Ribbon-Schaltfläche Externe Daten|Importieren|Weitere|ODBCDatenbank. Wählen Sie im nun erscheinenden Dialog die Option zum Erstellen einer verknüpften Tabelle aus. Wählen Sie dann die passende Computerdatenquelle. Ein Dialog zeigt nun alle in der angegebenen Datenbank enthaltenen Tabellen an. Wählen Sie diejenigen aus, die Sie verknüpfen möchten, und bestätigen Sie die Auswahl mit OK. Gegebenenfalls meckert Access, wenn die Tabelle kein Primärschlüsselfeld enthält. In dem Fall können Sie eines der vorhandenen Felder auswählen. Besser aber legen Sie in der MySQL-Datenbank einen richtigen Primärschlüssel an. Access zeigt dann die verknüpften Tabellen mit einem passenden Symbol im Naviga tionsbereich (siehe Abbildung 19.25).
18.9.4 Aktualisieren von Tabellen Wenn Sie auf dem MySQL-Server Änderungen an der Struktur der verknüpften Tabellen vornehmen – etwa durch Hinzufügen eines Feldes – werden diese nicht automatisch in der verknüpften Instanz dieser Tabelle angezeigt. Um die Verknüpfung zu aktualisieren, verwenden Sie den dafür vorgesehenen Assistenten, den Sie über den Ribbon-Eintrag Da
951
Kapitel 18
tenbanktools|Datenbanktools|Tabellenverknüpfungs-Manager aktivieren. Wählen Sie dort die zu aktualisierenden Verknüpfungen aus und klicken Sie auf OK (siehe Abbildung 18.26).
Abbildung 18.25: Eine per ODBC verknüpfte Tabelle im Navigationsbereich
Abbildung 18.26: Aktualisieren von Verknüpfungen mit dem Tabellenverknüpfungs-Manager
18.9.5 Internetverbindung mit MySQL Die Erstellung von Webfrontends ist immer noch wesentlich mühevoller als die Erstel lung einer entsprechenden Windows-Anwendung. Dadurch sind die Funktionen zur Be arbeitung der Daten meist auf die Benutzerschnittstelle beschränkt; für die Administra tion der Datenbank verwendet man in der Regel ein fertiges Administrationsfrontend. Dafür kommt natürlich auch Access in Frage, und da Sie nun schon wissen, wie Sie auf eine MySQL-Datenbank auf dem lokalen Rechner oder im lokalen Netz zugreifen,
952
Sicherheit von Access-Datenbanken
können Sie dieses Wissen auch für den Zugriff auf die MySQL-Datenbank auf dem Web server einsetzen. Alles, was Sie dafür benötigen, ist ein Weg zum Internetserver. Am sichersten funktioniert dies per Secure Shell (SSH). Dafür muss auf dem Internetserver ein SSH-Server wie beispielsweise OpenSSH instal liert sein und laufen. Außerdem benötigen Sie einen Benutzeraccount für den Zielrech ner. Die verwendete Technik ist recht simpel: Mit PuTTY, einer Open Source-Software, und dem verwandten Programm Plink (beide unter [11] zu finden) stellen Sie einen SSH-Tunnel zwischen dem Arbeitsplatzrechner und dem Internetserver her. Dieser Tunnel verbindet die beiden Rechner über den Port 3306 – das bedeutet, dass jede Anfrage an Port 3306 des Arbeitsplatzrechners an Port 3306 des Internetservers weiter geleitet wird. Die beiden Tools können Sie direkt starten (PuTTY) beziehungsweise über die Komman dozeile bedienen (Plink). Es ist also keine Installation notwendig. Sie sollten allerdings Plink in ein Verzeichnis legen, das in der Path-Umgebungsvariablen enthalten ist, oder einen entsprechenden Eintrag hinzufügen.
18.9.6 Erstellen eines Profils mit PuTTY PuTTY ist eigentlich ein Telnet- oder SSH-Client für den kommandozeilengesteuerten Zugriff auf andere Computer über das Netzwerk. Sie werden PuTTY hier ein wenig zweckentfremden und damit lediglich ein Profil anlegen, das Sie mit dem Kommando zeilentool Plink für die Portweiterleitung auf den Internetserver verwenden. Zum An legen des Profils gehen Sie folgendermaßen vor: Starten Sie PuTTY. Tragen Sie unter Host Name die IP-Adresse oder die Internetadresse des Internet servers und unter Saved Sessions einen geeigneten Namen für die Session – etwa ODBCTunnel – ein (siehe Abbildung 18.27). Wechseln Sie in den Bereich Connection|SSH|Tunnels und fügen Sie unter Port Forwar ding die entsprechenden Daten ein – mit der gleichen IP wie im vorherigen Schritt. Wechseln Sie zurück in den Bereich Session und speichern Sie die Session durch einen Klick auf die Schaltfläche Save. Mit einem Doppelklick auf den soeben erzeugten Eintrag können Sie testen, ob alles funktioniert. Es sollte eine Shell erscheinen und den Benutzernamen und das Kennwort abfragen. Wenn Sie sich hier erfolgreich einloggen können, sind die Voraussetzungen für den nachfolgend beschriebenen automatisierten Zugriff erfüllt.
953
Kapitel 18
Abbildung 18.27: Konfiguration der PuTTY-Verbindung
18.9.7 Testen des Tunnels Durch die Einstellungen im Bereich Tunnels werden bei bestehender Verbindung automatisch Daten an Port 3306 des lokalen Rechners an den entsprechenden Port des Inter netrechners weitergeleitet. Um dies zu testen, benötigen Sie eine bestehende MySQLDatenbank auf dem Zielrechner sowie eine bestehende Access-Datenbank. Als Test erstellen Sie wie oben beschrieben eine Verknüpfung von Access zu einer Tabelle der MySQL-Datenbank. Nun müssen Sie nur noch die Computerdatenquelle wie oben konfigurieren und tes ten. Dazu tragen Sie in den Dialog aus Abbildung 18.21 die entsprechenden Werte ein. Wichtig ist, dass Sie für Host/Server Name (or IP) auf jeden Fall localhost eintragen. Das ist schließlich der Sinn der Sache: Die Daten sollen erst an den lokalen Port 3306 und dann per SSH geschützt an den Internetserver geschickt werden. Für die Felder User und Password tragen Sie die Daten eines Benutzers der unter Database Name angegebenen Datenbank ein. Mit der Schaltfläche Test Data Source können Sie nun die Verbindung testen. Beachten Sie, dass das nur bei hergestelltem Tunnel funktionieren kann – Sie müssen sich also vorher per PuTTY am Internetserver angemeldet haben. Sie können die Verbindung auch mit dem Kommandozeilentool Plink herstellen. Dazu verwenden Sie etwa folgende Anweisung in der Kommandozeile (ohne Zeilenum bruch):
954
Sicherheit von Access-Datenbanken plink –load ODBCTunnel –l -pw
Beachten Sie, dass Sie mit PuTTY wie oben beschrieben ein Profil namens ODBCTunnel anlegen müssen. Die obige Anweisung müssen Sie nur noch in einer VBA-Routine unterbringen und dafür sorgen, dass diese zum gewünschten Zeitpunkt – etwa beim Start der Datenbank anwendung – aufgerufen wird. Um Sicherheitsrisiken zu vermeiden, sollten Sie ebenfalls einen Mechanismus unterbringen, der die Verbindung beim Schließen der Datenbank beendet. Praktisch wäre sicher ein komplett unsichtbarer An- und Abmeldevorgang, doch auch hier gebietet die Sicherheit das Verwenden einer Alternative: Bei voller Automatisierung müssen Sie (wenn Sie nicht mit öffentlichen und privaten Schlüsseln arbeiten) den Benutzernamen und das Kennwort für den Zugang zum Internetserver in der Datenbank speichern. Alternativ verwenden Sie ein Formular, das beim Öffnen der Datenbank die Zugangs daten abfragt und die Verbindung herstellt. Die Verwendung eines Formulars hat den zusätzlichen Vorteil, dass Sie es nach der Anmeldung unsichtbar machen und darin auch den Mechanismus zum Schließen der Verbindung unterbringen können. Dazu legen Sie einfach für die Beim Entladen-Ereigniseigenschaft eine entsprechende Prozedur an. Die Beschreibung eines solchen Formulars würde den Rahmen dieses Kapitels sprengen, daher finden Sie das Formular samt Quellcode in der Beispieldatenbank unter \kap19\Putty.accdb auf der Buch-CD. Dem sicheren Zugriff auf MySQL-Datenbanken auf einem Internet-Server steht damit nichts mehr im Wege. Sie können nun, wie weiter oben beschrieben, Tabellen verknüpfen und über Abfragen, Formulare und Berichte auf die enthaltenen Daten zugreifen.
Quellen zu diesem Kapitel [1] Access-FAQ, Eintrag zum Thema Sicherheitsmeldungen in Access 2003: http://www. donkarl.com, FAQ|Punkt 2.28 [2] Informationen über im Sandbox-Modus nicht zulässige VBA-Befehle: http://support. microsoft.com/kb/294698/en-us [3] Download SQL Server 2005 Express Edition: http://www.microsoft.com/downloads/ details.aspx?FamilyID=220549b5-0b07-4448-8848-dcc397514b41&DisplayLang=en [4] Download Microsoft .NET Framework Version 2.0 Redistributable Package (x86): http://www.microsoft.com/downloads/details.aspx?FamilyID=0856eacb-43624b0d-8edd-aab15c5e04f5&DisplayLang=en
955
Kapitel 18
[5] Download SQL Server Management Studio Express: http://www.microsoft.com/ downloads/details.aspx?FamilyID=c243a5ae-4bd1-4e3d-94b8-5a0f62bf7796&Displ ayLang=en [6] Download MySQL: http://dev.mysql.com/downloads/mysql/5.0.html#downloads [7] Dokumentation von MySQL: http://dev.mysql.com/doc/refman/5.1/de/index.html [8] Administrationstools für MySQL-Datenbanken: http://dev.mysql.com/downloads/ gui-tools/5.0.html [9] Download MyODBC: http://dev.mysql.com/downloads/connector/odbc/3.51.html [10] Download MySQL Migration Toolkit: http://www.mysql.com/products/tools/migration-toolkit/ [11] PuTTY und Plink: http://www.chiark.greenend.org.uk/~sgtatham/putty/
956
19 Installation, Betrieb und Wartung Mit der Fertigstellung ist die Arbeit an einer AccessAnwendung noch lange nicht beendet. Die Anwendung muss installiert, komprimiert oder gewartet werden, wobei gerade der erste Schritt manche Schikane birgt. Soll die Anwendung im Mehrbenutzerbetrieb als Einzelplatz lösung eingesetzt werden oder kommt vielleicht sogar Replikation zum Einsatz? Wie sorgen Sie während des Betriebs für das Erstellen von Backups und was hat es mit den oft erwähnten Problemen mit den Verweisen auf sich? Antworten auf diese und weitere Fragen finden Sie in diesem Kapitel. Die Beispiele zu den einzelnen Abschnitten dieses Kapitels sind auf mehrere Datenbankdateien auf der Buch-CD verteilt. Wenn keine andere Datei angegeben ist, finden Sie die Objekte und Quellcodes unter \Kap_19\InstallationBetrieb UndWartung.accdb.
19.1 Verschiedene AccessVersionen auf demselben Rechner Wenn Sie mehrere Access-Versionen auf demselben Rech ner verwenden müssen, sollten Sie einige Punkte bei der Installation beachten. Die beiden wichtigsten sind, dass Sie immer zuerst die ältere Version von Access installieren (das gilt insbesondere, wenn es sich dabei um Access 97
Kapitel 19
handelt). Außerdem geben Sie bei der Installation der jüngeren Version an, dass Sie diese erstens in ein eigenes Verzeichnis installieren und dass Sie die ältere Version nicht durch die neue Version aktualisieren möchten. Um jeglichen Problemen vorzubeugen, sollten Sie einfach auf die parallele Installation mehrerer Access-Versionen verzichten. Das können Sie nicht? Doch, sicher: Mittlerweile gibt es kostenlose Virtualisierungs-Software von Microsoft und anderen Anbietern, mit der Sie virtuelle Maschinen betreiben und darauf auch beliebige Software installieren können [1]. Damit können Sie beliebig viele virtuelle Systeme erstellen und darauf die verschiedenen Access-Versionen laufen lassen. Tipp: Installieren Sie die jeweils neueste und damit ressourcenhungrigste Version direkt auf Ihrem Rechner und die anderen Versionen auf virtuellen PCs. Dieses Verfahren eignet sich auch gut, um Setup-Routinen auf den verschiedenen Betriebssystemen zu testen.
Starten von Access-Datenbanken unterschiedlicher Versionen Wenn Sie beispielsweise Access 97 und Access 2003 auf einem Rechner installiert haben, gibt es beim Aufruf von Datenbankdateien per Doppelklick manchmal das Problem, dass die falsche Version von Access geöffnet wird. Mit Anwendungen im Access 2007Format haben Sie das Problem nicht mehr, da diese ja mit .accdb eine andere Dateiendung haben und somit automatisch die richtige Version geöffnet wird. Das Verhalten für .mdb-Datenbanken bleibt indes unverändert: Wenn Sie zuletzt eine .accdb-Datenbank geöffnet hatten, öffnet Access auch die als nächste geöffnete .mdb-Datei mit Access 2007. Besonders nachteilig ist dies im umgekehrten Fall: Wenn Sie zuvor eine ältere Access-Anwendung verwendet haben und dann eine Datenbank mit Access 2007 öffnen möchten, können Sie rund eine Minute Pause einplanen – es setzt dann zunächst eine Reparaturinstallation ein, die den Zugriff auf die Setup-Dateien von Office (gegebenenfalls von CD) erfordert.
19.2 Weitergabe von Access-Datenbanken Nicht jeder Access-Entwickler erstellt mit Access ausschließlich Anwendungen für den Eigenbedarf. Die meisten entwickeln Anwendungen für Kunden oder – wenn sie in ei nem Unternehmen arbeiten – auch für die eigenen Mitarbeiter. Wenn Sie Glück haben, verfügt der künftige Benutzer der neuen Access-Anwendung über Microsoft Access in der einen oder anderen Version. Wenn nicht, gibt es zwei Möglichkeiten: Entweder der Benutzer legt sich eine Lizenz zu oder Sie erstellen ihm eine Runtime-Version. Leider stehen die Developer Extensions und die Runtime von Access 2007 zum Zeitpunkt der Drucklegung dieses Buches noch nicht zum Download bereit, aber es gibt eine sehr gute Nachricht: Microsoft stellt diese Software kostenlos zur Verfügung, und damit spart
958
Installation, Betrieb und Wartung
man im Vergleich zum passenden Entwickler-Paket für Access 2003 einige hundert Euro ein. Microsoft liefert hiermit natürlich ein sehr gutes Argument für den Wechsel zu Access 2007. Weitere Informationen zur Runtime von Access 2007 finden Sie auf der Webseite zu diesem Buch (http://www.access-entwicklerbuch.de), wenn die Runtime veröffentlicht ist. Runtime-Versionen einer Access-Anwendung enthalten nicht nur die pure .accdb-Datei plus gegebenenfalls weitere Dateien wie ein Backend, sondern liefern Access direkt mit. Dabei handelt es sich zwar um eine eingeschränkte Version, mit der man beispielsweise keine Datenbankanwendung administrieren kann, aber immerhin wird der Benutzer ohne weitere Kosten für eine eigene Access-Lizenz in der Lage sein, mit der AccessAnwendung zu arbeiten – so, wie dies schon früher bei anderen Office-Dokumenten mit Tools wie dem Word-, Excel- und Powerpoint-Viewer möglich war. Wie bei der Verwendung von verschiedenen Access-Versionen auf demselben Rechner (siehe auch Abschnitt 19.1, »Verschiedene Access-Versionen auf demselben Rechner«) gibt es auch hier eine Menge Vorgänge, die schieflaufen können, wenn Sie eine AccessRuntime auf einem Rechner mit einer anderen Access-Version installieren. Die Installa tion einer Access 2007-Runtime sorgt etwa dafür, dass diese Version beim Starten als standardmäßig zu verwendende Access-Version eingesetzt werden soll, wenn man etwa per Doppelklick eine Access-Anwendung öffnet. Dies könnte bei Benutzern zu Ungemach führen. Eine ausführliche Diskussion der Möglichkeiten und Probleme beim Erstellen von In stallationsroutinen für die Runtime-Version von Access ist in diesem Buch aus Platz gründen leider nicht möglich. Die folgenden Tipps sollen aber helfen, Probleme mit der Access-Anwendung selbst zu verhindern.
19.2.1 Benutzerdefinierte Menüs In Runtimes gibt es keinen Navigationsbereich und auch die eingebauten Ribbon-Tabs werden nicht angezeigt. Letzteres liegt in der Natur der Sache, denn die Runtime-Version von Access liefert nur die Funktionen, die zum Laufen der Datenbankanwendung selbst – also der Benutzeroberfläche einschließlich Formularen, Berichten und Menüs – und von VBA notwendig sind. Die wichtigste Konsequenz hieraus ist: Sie müssen selbst Sorge tragen, dass der Benutzer auf irgendeinem Wege die benötigten Formulare und Berichte öffnen beziehungsweise die VBA-Routinen aufrufen kann. Das kann durch ein Formular geschehen, das beim Start der Anwendung angezeigt wird und die möglichen Optionen enthält. Sinnvoller, da ständig sichtbar, ist eine benutzerdefinierte Anpassung des Ribbons mit Elementen zum Aufrufen von Formularen und Berichten und anderen Funktionen wie dem Drucken oder Beenden der Datenbankanwendung (weitere Informationen siehe Kapitel 12, »Ribbons«).
959
Kapitel 19
19.2.2 Fehlerbehandlung Access-Runtimes sind mit einer äußerst sparsamen Fehlerbehandlung ausgestattet. Das bedeutet, dass im Falle eines Fehlers höchstens eine wenig aussagekräftige Fehlermel dung erscheint (»Die Ausführung dieser Anwendung wurde wegen eines Laufzeitfeh lers angehalten. Die Anwendung wird beendet.«) und die Anwendung sich daraufhin rasch verabschiedet. Das ist nicht nur für den Benutzer unbefriedigend, sondern auch für den Entwickler: Der Benutzer kann dem Entwickler lediglich mitteilen, an welcher Stelle der Fehler aufgetreten ist und was dazu geführt hat. Es gibt aber weder eine aussagekräftige Fehlermeldung noch Informationen über den fehlerhaften Code. Deshalb sollten Sie gerade in Datenbanken, die Sie als Runtime weitergeben, unbedingt eine umfassende Fehlerbehandlung integrieren. Das gilt natürlich auch für »normale« zur Weitergabe bestimmte Datenbankanwendungen, aber hier ist es besonders wichtig. Weitere Informationen zum Thema Fehlerbehandlung finden Sie in Kapitel 13, »Debug ging, Fehlerbehandlung und Fehlerdokumentation«.
19.2.3 Runtime-Simulation Wer eine Anwendung zur Weitergabe als Runtime-Version entwickelt, muss nicht jedes Mal eine Runtime-Version erstellen und diese neu installieren, um zu prüfen, ob alles läuft wie geplant. Sie können auch einfach die Dateiendung von .accdb auf .accdr ändern. Es ist also nicht mehr nötig, wie bei älteren Access-Versionen einen Aufruf der msaccess. exe mit der gewünschten Datenbank und dem Parameter /runtime abzusetzen. Wenn Sie dies ausprobieren, werden Sie schnell feststellen, dass sämtliche Elemente der Benutzeroberfläche mit Ausnahme der Office-Menü-Schaltfläche und einigen Ein trägen im Office-Menü nicht mehr angezeigt werden. Um die Benutzeroberfläche mit Leben zu füllen, erstellen Sie entweder eine passende Ribbon-Definition und weisen diese der Datenbank zu oder stellen ein Übersichtsformular bereit, das beim Öffnen der Datenbank angezeigt wird. Das anzuzeigende Formular können Sie in den AccessOptionen unter Aktuelle Datenbank|Anwendungsoptionen|Formular anzeigen einstellen; Informationen zum Erstellen einer geeigneten Ribbon-Definition finden Sie in Kapi tel 10, »Ribbons«.
19.2.4 Weitergabe ohne Runtime Die Weitergabe von Datenbankanwendungen an Benutzer, die bereits über die benötigte Access-Version verfügen, ist in den meisten Fällen völlig unproblematisch. Am einfachsten ist dies natürlich, wenn Sie nur eine einzige .accdb-Datei oder .accde-Datei weitergeben müssen – hier spielt es nicht einmal eine Rolle, wo der Benutzer die Anwendung speichert.
960
Installation, Betrieb und Wartung
Einige Anwendungen sind aber etwas aufwändiger: So könnte es beispielsweise sein, dass die Datenbank aus Frontend und Backend besteht und im Frontend noch der neue Standort des Backends angegeben werden muss (weitere Informationen zu Frontendund Backend-Datenbanken in Abschnitt 19.5). Oder die Datenbank verweist auf Bilddateien oder Ähnliches, wozu ein entsprechender Ordner einzurichten ist. Wenn Sie die Installation nicht selbst vor Ort vornehmen, können Sie natürlich eine Dokumentation erstellen, die alle notwendigen Informationen zu den Speicherorten und Verknüpfungen enthält. Das ist aber nicht besonders kundenfreundlich. Alternativ können Sie ein Setup erstellen, das den Benutzer beim Einrichten der Anwen dung unterstützt, indem es beispielsweise das Verzeichnis abfragt, in dem die Dateien installiert werden sollen, oder ob im Startmenü oder auf dem Desktop Verknüpfungen anzulegen sind. Für diese Aufgaben gibt es sogar kostenlose Tools wie etwa Inno Setup. Es bietet praktisch alle Möglichkeiten professioneller Setup-Tools. Sie finden das Tool unter folgender Internetadresse: http://www.jrsoftware.org. In Access-Kreisen hat sich Inno Setup einen Namen gemacht, weil es die Erstellung professioneller Runtime-Setups mit Access 97 ermöglicht hat. Für Access 97 gibt es im Internet ausreichend Beispielskripte. Leider ist die Installation von Runtimes neuerer Access-Versionen zu komplex geworden, sodass bisher keine als zuverlässig geltenden Skripte zur Erstellung von Runtimes mit Inno Setup und neueren Access-Versionen existieren. Auch der Windows Installer ist eine interessante Alternative. Einige Informationen finden Sie unter folgender Internetadresse: http://www.installsite.org. Auch das kostenlose Runtime-Paket von Access 2007 wartet mit einem passenden Tool auf: Der Verpackungs-Assistent hilft dann beim Erstellen eines MSI-Setups für die Access-Runtime.
19.3 Aktionen beim Starten oder Beenden der Datenbank durchführen Einige Aktionen wie etwa Komprimieren der Datenbank, Einbinden von Tabellen aus Backend-Datenbanken oder Sichern einer Datenbank sollten regelmäßig durchgeführt werden. Dazu bieten sich der Start oder das Beenden einer Datenbank an. Während das Ausführen von Code oder das Aufrufen eines Formulars beim Starten einer Datenbank recht einfach zu realisieren sind, ist für Aktionen, die beim Beenden der Datenbank anwendung durchgeführt werden sollen, schon ein kleiner Trick notwendig.
961
Kapitel 19
19.3.1 Code beim Starten einer Datenbank ausführen Um eine Routine beim Starten einer Datenbankanwendung auszuführen, benötigen Sie ein Makro namens Autoexec. Ein Makro mit diesem Namen wird von Access automatisch beim Start einer Anwendung ausgeführt – außer natürlich, der Benutzer hält beim Start die Umschalt-Taste gedrückt. Das Autoexec-Makro aus Abbildung 19.1 ruft die eingebaute Funktion Meldung auf. Statt dieser können Sie jede beliebige öffentliche Funktion innerhalb der Datenbank angeben.
Abbildung 19.1: Dieses Autoexec-Makro zeigt beim Starten einer Anwendung ein Meldungsfenster an
Beachten Sie, dass Ihnen die Sicherheitseinstellungen von Access einen Strich durch die Rechnung machen können: Mehr darüber erfahren Sie in Kapitel 18, »Sicherheit von Access-Datenbanken« in Abschnitt 18.4, »Vertrauensstellungscenter«.
19.3.2 Formular beim Starten einer Datenbank anzeigen Wenn Sie den Benutzer Ihrer Datenbank mit einem Begrüßungsformular beglücken möch ten, können Sie beim Öffnen dieses Formulars die beim Start der Anwendung auszuführenden Routinen aufrufen. Dazu fügen Sie die gewünschten Aufrufe einfach der Prozedur hinzu, die durch die Ereigniseigenschaft Beim Anzeigen ausgelöst wird. Dass Access das Formular beim Starten der Anwendung anzeigen soll, teilen Sie ihm in den Access-Optionen unter Aktuelle Datenbank|Anwendungsoptionen|Formular anzeigen mit.
19.3.3 Aktion beim Schließen einer Datenbank ausführen Access bietet nur Ereignisse für Formulare, Berichte, Steuerelemente und andere Ob jekte, aber keine globalen Ereignisse. Während dieser Mangel sich beim Start einer Da
962
Installation, Betrieb und Wartung
tenbank mit den oben genannten und für diesen Zweck vorgesehenen Methoden um gehen lässt, ist für das Ausführen von Aktionen beim Schließen einer Datenbank ein wenig Fantasie gefordert. Der Clou ist, dass Formulare ein Ereignis bieten, das beim Entladen eines Formulars ausgelöst wird, und dass Access alle Formulare, die beim Schließen der Anwendung noch geöffnet sind (ob sichtbar oder unsichtbar) entlädt. Das heißt: Sie brauchen nur dafür zu sorgen, dass die beim Beenden der Datenbank auszuführende Routine in der Ereignisprozedur Beim Entladen eines Formulars enthalten ist und dass dieses Formular beim Schließen der Anwendung sicher noch geöffnet ist. Das lässt sich einfach realisieren: Öffnen Sie beim Starten der Anwendung ein solches Formular und machen Sie es direkt unsichtbar. Sofern der Benutzer dieses Formular nicht explizit öffnet und dann wieder schließt beziehungsweise in der Entwurfsansicht anzeigt, ist das Formular beim Schließen der Anwendung noch geöffnet. Damit der Benutzer keinen Unsinn mit dem benötigten Formular anstellt, zeigen Sie ihm einfach den Navigationsbereich nicht – der sollte übrigens in professionellen Anwendungen ohnehin nie zu sehen sein (Einstellung in den Access-Optionen unter Aktuelle Datenbank|Navigationsbereich anzeigen). Sie können natürlich auch das Start-Formular für das Auslösen einer Prozedur beim Schließen der Datenbank verwenden. Das Formular aus Abbildung 19.2 ist in der Beispieldatenbank InstallationBetriebWartung.accdb als beim Start anzuzeigendes Formu lar angegeben.
Abbildung 19.2: Startformular mit Pep
Die Schaltfläche OK löst die folgende Routine aus und macht das Formular damit praktisch unsichtbar: Private Sub cmdOK_Click() Me.Visible = False End Sub Listing 19.1: Hokuspokus – Formular verschwindibus
Die Beispiele zu diesem Kapitel finden Sie auf der Buch-CD in der Datenbank \Kap_19\InstallationSicherungUndWartung.accdb.
963
Kapitel 19
Alternativ können Sie ein Formular auch direkt unsichtbar öffnen. Dazu verwenden Sie wiederum das Autoexec-Makro und legen dort einen Eintrag mit der Aktion ÖffnenFor mular mit dem gewünschten Formularnamen und dem Fenstermodus Ausgeblendet an. Es verharrt dann im unsichtbaren Zustand, bis die Anwendung geschlossen wird. Dies löst nämlich das Ereignis Beim Entladen des Formulars aus, was in der Ausführung der folgenden Routine resultiert. In diesem Fall zeigt die Routine einfach nur ein Meldungs fenster an; Sie können dort aber beliebige Funktionen etwa zum Erstellen einer Sicher heitskopie anlegen: Private Sub Form_Unload(Cancel As Integer) MsgBox "Das Formular frmStart wird nun entladen." End Sub Listing 19.2: Das Formular wird beim Schließen der Anwendung entladen und das löst diese Prozedur aus
19.4 Datenbanken komprimieren und reparieren Datenbanken wachsen mit der Zeit – zumindest wenn Sie regelmäßig Daten anlegen. Falls Sie Daten im gleichen Maße löschen, sollte der Umfang der Datenbank eigentlich abnehmen – was aber nicht der Fall ist. Der Grund ist, dass Access gelöschte Daten erst beim Komprimieren einer Datenbank endgültig aus der Datenbank entfernt. Dabei ist mit Komprimieren nicht das Packen in ein anderes Format wie .zip, .tar, .arj oder Ähn liches mit einem externen Komprimierungsprogramm gemeint, sondern die Verwen dung der Access-eigenen Funktion. Diese lösen Sie mit dem Office-Menü-Eintrag Verwalten|Datenbank komprimieren und reparieren aus. Das Komprimieren löscht nicht nur alle noch vorhandenen Datenfragmente, sondern initialisiert auch die Autowert-Felder der in der Datenbank enthaltenen Tabellen. Das bedeutet, dass der Zähler beim nächsten Aufruf beim Folgewert des größten enthaltenen Wertes ansetzt. Leere Tabellen, die zwischenzeitlich einmal Daten enthalten haben, werden somit – rein autowert-technisch betrachtet – in einen jungfräulichen Zustand zurückversetzt. Zwischenräume in Tabellen mit bestehenden Daten werden dadurch allerdings nicht gefüllt. Gleichzeitig ordnet Access auch die in den Tabellen enthaltenen Daten neu an, sodass mit Abfragen schneller darauf zugegriffen werden kann. Den seit einigen Access-Versionen mit dem Reparieren zusammengefassten Kompri miervorgang können Sie automatisch beim Beenden einer Datenbank durchführen lassen. Dazu aktivieren Sie einfach die Access-Option Aktuelle Datenbank|Anwendungsoptio nen|Beim Schließen komprimieren.
964
Installation, Betrieb und Wartung
19.5 Mehrbenutzerbetrieb mit Access-Datenbanken Der Mehrbenutzerbetrieb von reinen Access-Datenbanken setzt voraus, dass Sie die enthaltenen Daten auf der einen und die Benutzeroberfläche und die Anwendungslogik auf der anderen Seite in einzelnen Access-Dateien speichern. Das hört sich wilder an, als es ist – selbst wenn Sie nicht die Dienste des Assistenten zur Datenbankaufteilung in Anspruch nehmen, ist eine Datenbank im Nu gegliedert. Die aufgeteilten Datenbanken finden Sie auf der Buch-CD unter \Kap_19/Nordwind_ Frontend.accdb und Nordwind_Backend.accdb.
19.5.1 Aufteilen einer Access-Datenbank Dazu sind nur die folgenden drei Schritte erforderlich: Importieren aller Tabellen in eine neue, leere Datenbank Löschen der Tabellen aus der ursprünglichen Datenbank Exportierte Tabellen als verknüpfte Tabellen einbinden Die daraus entstandenen Datenbanken heißen Frontend und Backend. Das Frontend können Sie beliebig oft kopieren und an alle Benutzer verteilen, die auf die im Backend enthaltenen Daten zugreifen sollen.
19.5.2 Tabellen in neue Datenbank importieren Zum Importieren der Tabellen in das zukünftige Backend haben Sie zwei Möglichkeiten: Sie können eine neue, leere Datenbank anlegen und dort den Ribbon-Eintrag Externe Daten|Importieren|Access wählen, den Namen der Quelldatenbank auswählen und festlegen, dass Sie die Tabellen importieren möchten. Wählen Sie dann im Dialog Objekte importieren die gewünschten Tabellen aus und klicken Sie auf die Schaltfläche OK. In diesem Dialog können Sie im Übrigen weitere Optionen anzeigen und sich vergewissern, dass die Tabellen auch inklusive Daten importiert werden (siehe Abbildung 19.3). Die zweite Variante ist ein wenig brachialer: Kopieren Sie einfach die Ausgangsdatenbank und löschen Sie alle Objekte außer den Tabellen.
19.5.3 Tabellen aus der Ausgangsdatenbank löschen Dieser Schritt ist noch einfacher als der vorhergehende und der folgende: Löschen Sie einfach alle Tabellen aus der Ausgangsdatenbank. Vielleicht hakt es hier und da, weil
965
Kapitel 19
zusätzlich Verknüpfungen gelöscht werden müssen, aber davon sollten Sie sich nicht beirren lassen. Access 2007 ermöglicht übrigens das Markieren und Löschen mehrerer Tabellen gleichzeitig.
Abbildung 19.3: Auswahl der zu importierenden Daten
19.5.4 Tabellen als Verknüpfung einbinden Im letzten Schritt rufen Sie in der Ausgangsdatenbank den Ribbon-Eintrag Externe Daten|Importieren|Access auf, wählen im Externe Daten-Dialog die neue BackendDatenbank als Datenquelle und legen diesmal fest, dass eine Verknüpfung zu den Daten hergestellt werden soll. Am Beispiel der Nordwind-Datenbank sieht der Teil des Navigationsbereichs mit den Tabellen wie in Abbildung 19.4 aus.
19.5.5 Erneutes Einbinden der Tabellen nach Umbenennen oder Verschieben des Backends Solange Sie das Backend immer an der gleichen Stelle aufbewahren, ist das alles auch kein Problem: Der Pfad des Backends ist absolut in der Datenbank gespeichert. Interessant wird es, wenn Sie die Datenbank weitergeben. Wenn beim Endanwender nicht gerade genau die gleichen Rechner- und Verzeichnisnamen wie in der Entwicklungsumgebung vorliegen, sind Änderungen an den Pfaden der eingebundenen Tabellen notwendig. Am
966
Installation, Betrieb und Wartung
besten versorgen Sie die Datenbank mit einer Funktion, die selbst überprüft, ob sich das Backend an der geplanten Stelle befindet, und die ansonsten einen Dateidialog anzeigt, mit dem der Benutzer den neuen Standort des Backends eingeben kann.
Abbildung 19.4: Datenbankfrontend mit verknüpften Tabellen aus dem Backend
Im Internet kursieren einige Lösungen, die zwar dynamisch alle Verknüpfungen aktualisieren, aber keine Absicherung dagegen bieten, dass der Benutzer einmal die falsche .accdb-Datei als Ziel der Verknüpfungen auswählt. Infolgedessen werden dort zwar die bestehenden Verknüpfungen gelöscht, aber mangels passender Tabellen im Backend nicht wieder hergestellt. Beim nächsten Versuch fehlt dann zumindest eine Verknüpfung, die bis zum Bemerken des Fehlers gelöscht wurde; das komplette Datenmodell kann dann ohne manuellen Eingriff nicht wieder hergestellt werden. Die folgende Lösung für das automatische Wiedereinbinden von Backend-Tabellen erfordert zwar einmalig die Angabe aller zu verknüpfenden Tabellen, ist aber unempfindlicher gegenüber dem Einbinden falscher Backends. Die Routine erwartet zwei Parameter: Den kompletten Pfad inklusive Dateiname der Backend-Datenbank sowie ein Array mit den einzubindenden Tabellen. Public Function BackendEinbinden(strBackendPfad As String, _ strTabellenliste() As String) As String Dim Dim Dim Dim
lngAnswer As Long db As Database tdf As TableDef strBackendName As String
967
Kapitel 19 Dim Dim Dim Dim
strBackendpath As String i As Integer strFehler As String boolBackendVorhanden As Boolean
strBackendName = Mid(strBackendPfad, InStrRev(strBackendPfad, "\") + 1) CheckPath: 'Prüfen, ob angegebenes Backend vorhanden On Error Resume Next boolBackendVorhanden = Not Dir(strBackendPfad) = "" If Not boolBackendVorhanden Or Err.Number <> 0 Then On Error GoTo 0 'Falls nicht gefunden: 'Fragen, ob ein anderes Backend ausgewählt werden soll If MsgBox("Die Backend-Datenbankdatei kann nicht im Pfad '" _ & strBackendPfad & "' gefunden werden. Klicken Sie auf " _ & "'Ja', um die Datei auszuwählen und auf 'Nein', um die " _ & "Anwendung zu beenden.", vbYesNo) = vbYes Then strBackendPfad = OpenFileName(CurrentProject.Path, _ "Backend auswählen", _ "Access-Datenbank (*.accdb)|Alle Dateien(*.*)") 'Wenn keine Datei ausgewählt oder Dialog abgebrochen: 'Anwendung beenden. If strBackendPfad = "" Then MsgBox "Sie haben die Aktion abgebrochen. " _ & "Die Anwendung wird geschlossen." DoCmd.Quit End If Set db = CurrentDb 'Prüfen, ob eine vorhandene Datei ausgewählt wurde 'und gegebenenfalls Auswahl neu starten GoTo CheckPath Else DoCmd.Quit End If Else On Error GoTo 0 Set db = CurrentDb 'Alle Tabellen durchlaufen... For Each tdf In db.TableDefs '... und mit der übergebenen Tabellenliste abgleichen For i = LBound(strTabellenliste()) To _ UBound(strTabellenliste())
968
Installation, Betrieb und Wartung 'Wo Übereinstimmungen sind: If tdf.Name = strTabellenliste(i) Then 'Versuchen, die Tabelle neu zu verknüpfen 'und gegebenenfalls Fehlerliste aktualisieren tdf.Connect = ";database=" & strBackendPfad On Error Resume Next tdf.RefreshLink If Err.Number > 0 Then strFehler = strFehler & "- Die Tabelle '" _ & strTabellenliste(i) _ & "' konnte nicht eingebunden werden." _ & vbCrLf End If On Error GoTo 0 End If Next i Next tdf End If 'Falls es Fehler gab: Meldung ausgeben If Len(strFehler) > 0 Then MsgBox "Es sind Fehler beim Einbinden der " _ & "Backend-Datenbank aufgetreten: " & vbCrLf & strFehler, _ vbOKOnly + vbExclamation, "Fehler beim Einbinden" End If End Function Listing 19.3: Routine zum Wiedereinbinden von Tabellen aus einer Backend-Datenbank
Die Routine wird beispielsweise wie in folgender Prozedur aufgerufen. Die Prozedur erstellt ein Array mit der gewünschten Anzahl Felder und trägt die einzubindenden Tabellen ein. Dann ruft die Prozedur die Routine BackendEinbinden auf. Der Vorteil bei der Übergabe der einzubindenden Tabellen per Parameter ist, dass Sie auch mit Anwen dungen arbeiten können, die ihre Tabellen aus mehr als einem Backend beziehen. In die sem Fall rufen Sie die Routine BackendEinbinden einfach mehrmals mit den unterschied lichen Parametern auf. Die Funktion VerknuepfungenPruefen testet zuvor, ob die Tabellen überhaupt neu eingebunden werden müssen. Den Code dieser Routine finden Sie im nächsten Abschnitt. Public Function BackendEinbindenBeispielaufruf() Dim strTabellen(8) As String strTabellen(1) = "Artikel" strTabellen(2) = "Bestelldetails" strTabellen(3) = "Bestellungen"
969
Kapitel 19 strTabellen(4) = "Kategorien" strTabellen(5) = "Kunden" strTabellen(6) = "Lieferanten" strTabellen(7) = "Personal" strTabellen(8) = "Versandfirmen" If VerknuepfungenPruefen(strTabellen()) = False Then BackendEinbinden CurrentProject.Path _ & "\Nordwind_Backend.accdb", strTabellen End If End Function Listing 19.4: Beispielaufruf der Funktion zum Einbinden von Tabellen des Backends
19.5.6 Zeitpunkt zum Wiedereinbinden von Tabellen Zwischen zwei Arbeitstagen oder auch zwischen zwei Datenbanksitzungen kann eine Menge passieren: Ein Benutzer löscht zwischendurch die Backend-Datenbank, benennt sie um oder entschließt sich, die Verzeichnisstruktur auf dem Server ein wenig zu überarbeiten. Daher ist es sinnvoll, die Routine BackendEinbinden regelmäßig auszuführen – am besten bei jedem Start der Anwendung. Wie Sie einen geeigneten Aufruf der Prozedur beim Start ausführen können, erfahren Sie in Abschnitt 19.3, »Aktionen beim Starten oder Beenden der Datenbank durchführen«. Sie sollten in diesem Fall allerdings zuvor prüfen, ob die Verknüpfungen nicht bereits funktionieren – bei größeren Anwendungen mit vielen Tabellen würde das ständige Neuverknüpfen beim Start sonst zu lange dauern. Das erledigen Sie mit der folgenden kleinen Prozedur: Public Function VerknuepfungenPruefen(strTabellen() As String) As Boolean Dim db As DAO.Database Dim rst As DAO.Recordset Set db = CurrentDb On Error Resume Next For i = LBound(strTabellen()) To UBound(strTabellen()) Set rst = db.OpenRecordset(strTabellen(i)) If Err.Number = 3024 Then VerknuepfungenPruefen = False rst.Close Next i End Function Listing 19.5: Prüfen, ob alle verknüpften Tabellen vorhanden sind
970
Installation, Betrieb und Wartung
19.6 Sichern von Access-Datenbanken Ein für Access-Entwickler sehr heikles Thema ist das Sichern von Access-Datenbanken. Das Problem ist, dass sich wie bei vielen anderen Datenbanksystemen eine Sicherung im laufenden Betrieb nicht realisieren lässt – zumindest nicht mit der Sicherheit, dass Ihnen im Fall der Fälle ein hundertprozentig funktionsfähiges Backup der Datenbankanwendung zur Verfügung steht. In vielen Unternehmen werden die Backend-Datenbanken oder gar die kompletten Datenbanken auf den Server gelegt, weil die dortigen Daten mit dem täglichen Siche rungslauf auf die sichere Seite gebracht werden. Greift jedoch jemand zum Zeitpunkt der Sicherung auf die Datenbank zu, kann es passieren, dass die im Backup enthaltenen Daten oder gar der im VBA-Projekt enthaltene Code beschädigt wird – denn auch dieser wird in einer internen Tabelle gespeichert. Die nachfolgend vorgestellten Listings finden Sie in der Datenbankdatei \Kap_19\Install ationBetriebUndWartung.accdb im Modul mdlBackup.
19.6.1 Voraussetzungen und Vorbereitungen Der optimale Zeitpunkt zum Sichern einer Datenbank ist der, an dem diese geschlossen ist. Sollte dies im Fall Ihrer Anwendung zu fest definierten Zeitpunkten der Fall sein – zum Beispiel nachts – steht einer automatischen Sicherung etwa durch einen geplanten Task nichts im Wege. In allen anderen Fällen können die folgenden VBA-Routinen große Hilfe leisten – das Backup lässt sich hiermit auch zur Laufzeit durchführen. Voraussetzung für die nachfolgend beschriebene Vorgehensweise zum Erstellen eines Backups ist, dass die zu sichernden Daten getrennt vom Frontend in einer BackendDatenbank gehalten werden. Wie Sie dies realisieren, haben Sie bereits in Abschnitt 19.5, »Mehrbenutzerbetrieb mit Access-Datenbanken« dieses Kapitels erfahren. Die erste Routine durchläuft alle geöffneten Objekte der aktuellen Datenbank und schließt sie gegebenenfalls. Dabei werden nur Tabellen, Abfragen, Formulare und Be richte sowie geöffnete Datensatzgruppen berücksichtigt. Public Sub AlleObjekteSchliessen() Dim obj As Object Dim strTemp As String On Error Resume Next 'Alle Formulare schließen For Each obj In Forms
971
Kapitel 19 DoCmd.Close acForm, obj.Name, acSaveYes Next obj 'Alle Berichte schließen For Each obj In Reports DoCmd.Close acReport, obj.Name, acSaveYes Next obj 'Eventuell offene Tabellen oder Abfragen schließen Do strTemp = vbNullString strTemp = Screen.ActiveDatasheet.Name DoCmd.Close acTable, strTemp DoCmd.Close acQuery, strTemp DoEvents Loop Until Len(strTemp) = 0 'Eventuell offene Recordsets schließen For Each obj In DBEngine(0)(0).Recordsets obj.Close Next obj Set obj = Nothing End Sub Listing 19.6: Schließen aller offenen Datenbankobjekte
Die zweite Routine beendet alle gegebenenfalls noch offenen Schreibvorgänge. Dazu startet sie eine Transaktion (weitere Informationen in Kapitel 9, Abschnitt 9.11, »Transaktionen«), um direkt im Anschluss alle begonnenen Transaktionen mit CommitTrans und der Option dbForceOSFlush zu beenden. Das vorherige Starten einer Transaktion ist notwendig, weil CommitTrans einen Fehler erzeugt, wenn gar keine Transaktion läuft. Der anschließende Aufruf der Idle-Methode bewirkt, dass noch offene Hintergrundprozesse ausgeführt werden. Private Sub SchreibvorgaengeBeenden() '"Nulltransaktion": 'Eventuell ausstehende Schreibvorgänge erzwingen DBEngine.BeginTrans DBEngine.CommitTrans dbForceOSFlush 'Datenbank-Engine "resetten": DBEngine.Idle End Sub Listing 19.7: Beenden der Schreibvorgänge
972
Installation, Betrieb und Wartung
Schließlich muss geprüft werden, ob das Backend geschlossen ist – die vorhergehenden Routinen haben das für das aktuelle Frontend bereits erledigt, es könnten aber noch andere Benutzer das Backend im Zugriff haben. Das ist am einfachsten möglich, indem Sie die Existenz einer entsprechenden .laccdb-Datei nachweisen. Sie wird immer angelegt, wenn man eine Datenbank öffnet. Ist die Datei vorhanden, ist die Backend-Datenbank nicht geschlossen und kann dementsprechend auch nicht risikolos kopiert werden. Außerdem muss die Backend-Datenbank für den exklusiven Zugriff vorbereitet sein, was Sie durch entsprechendes Öffnen prüfen. Private Function BackendBereit(strBackend As String) As Boolean Dim strLDB If Len(Dir(strBackend)) = 0 Then Exit Function 'Existenz des zugehörigen LDB-Files ermitteln strLDB = Left(strBackend, InStrRev(strBackend, ".") - 1) & ".LACCDB" If Len(Dir(strLDB)) = 0 Then BackendBereit = True On Error Resume Next 'Zur Sicherheit Möglichkeit des Vollzugriff auf Backend ermitteln Open strBackend For Binary Access Read Lock Read Write As #1 If Err.Number <> 0 Then BackendBereit = False Close #1 End Function Listing 19.8: Prüfen, ob das Backend geschlossen und für den Vollzugriff vorbereitet ist
Public Function BackupErstellen(strBackend As String) As Boolean AlleObjekteSchliessen SchreibvorgaengeBeenden If BackendBereit(strBackend) Then 'BackupErstellen = BackendKopieren(strBackend, strBackendKopie) '... BackupErstellen = True Else MsgBox "Backend kann momentan nicht ausgeführt werden." & vbCrLf _ & "Versuchen Sie es zu einem späteren Zeitpunkt." End If End Function Listing 19.9: Aufruf der vorherigen drei Routinen und Start des Backup-Vorgangs, wenn das Backend geschlossen und zugreifbar ist
973
Kapitel 19
19.6.2 Einfaches Kopieren mit FileCopy Die einfachste Variante verwendet die FileCopy-Methode von VBA. Diese Methode kann allerdings nur Dateien kopieren, auf die gerade nicht zugegriffen wird. Die folgende Funktion platzieren Sie im gleichen Modul wie die oben beschriebenen Funktionen. Der Aufruf erfolgt durch einen in der Funktion BackupErstellen befindlichen, bisher noch auskommentierten Aufruf: Private Function BackendKopieren(strBackend As String, _ strBackendKopie As String) As Boolean On Error GoTo BackendKopieren_Err 'Variante I: FileCopy FileCopy strBackend, strBackendKopie BackendKopieren = True 'Fehlerbehandlung BackendKopieren_Exit: 'Restarbeiten Exit Function BackendKopieren_Err: BackendKopieren = False GoTo BackendKopieren_Exit End Function Listing 19.10: Funktion zum Kopieren einer Datenbankdatei
19.6.3 Kopieren per API-Funktion Eine weitere Möglichkeit zum Kopieren bietet die API-Funktion SHFileOperation. Die Deklaration der Funktion und der verwendeten Konstanten finden Sie im Modul mdlFile Copy in der Beispieldatenbank \Kap_19\InstallationBetriebUndWartung.accdb. Sie bietet einige Optionen wie etwa eine Fortschrittsanzeige bei länger dauernden Kopiervorgängen, beim Kopieren von Dateiberechtigungen und mehr. Der Aufruf der ebenfalls in diesem Modul befindlichen Wrapper-Funktion sieht etwa folgendermaßen aus: BackendKopieren CurrentDb.Name, _ "c:\Sicherung\Sicherung.accdb", _ eFC_DateiNichtUeberschreiben Or eFC_OhneBerechtigungen _ Or eFC_OhneFehlermeldungen
Die einzelnen Optionen und ihre Beschreibung finden Sie ebenfalls im Modul mdlFile Copy.
974
Installation, Betrieb und Wartung
19.6.4 Kopieren und komprimieren Wenn Sie die Datenbank anschließend komprimieren möchten, können Sie auch die CompactDatabase-Methode des DBEngine-Objekts verwenden. Dabei setzen Sie einfach die folgende Anweisung für die jeweilige Kopieranweisung der oben vorgestellten Funktion BackendKopieren ein. DBEngine.CompactDatabase strBackend, strBackendKopie
19.6.5 Kopieren und zippen Natürlich können Sie die kopierte Datenbankdatei auch direkt mit WinZip oder ähnlichen Tools packen. Die meisten Zip-Tools bieten die Möglichkeit der Steuerung per Kommandozeile. Unter WinZip würde die notwendige Anweisung etwa wie folgt aussehen: Shell "c:\programme\winzip\wzzip -a " & strBackendKopie & ".zip " _ & strBackend
19.6.6 Sicherungsstrategie Ein Punkt ist noch offen: Welche Strategie wird beim Sichern verfolgt, das heißt, wann wird gesichert, wie heißen die Dateinamen der Sicherungen und in welchem Verzeichnis befinden sich diese, werden Daten nach einem bestimmten Zeitraum überschrieben und vor allem: Wie teilen Sie dem oder den Benutzern mit, dass in Kürze ein Sicherungsvorgang ansteht? Letzteres ist natürlich nur relevant, wenn die Sicherung tatsächlich während des laufenden Betriebs stattfinden muss und nicht nachts erfolgen kann. Wenn sichergestellt ist, dass nachts niemand auf die Datenbank zugreift, kann man den Vorgang mit einem VB-Skript ausführen, das mit dem Tool Geplante Tasks von Windows aufgerufen wird. Ein einfaches Skript könnte wie folgt aussehen und wird mit der Datei-Endung .vbs gespeichert: Set objFSO = CreateObject("Scripting.FileSystemObject") strZeit = Year(Date()) & Right("0" & Month(date()), 2) _ & Right("0" & Day(date()), 2) & "_" & Right("0" & Hour(Time()),2) _ & Right("0" & Minute(Time),2) & Right("0" & Second(Time),2) objFSO.CopyFile "c:\Nordwind_Backend.accdb", "c:\Nordwind_Backend" _ & strZeit & ".accdb"
Es kopiert die Datei und fügt dem neuen Dateinamen vor dem Suffix das aktuelle Datum und die Uhrzeit im Format yyyymmdd_hhmmss hinzu. Der resultierende Dateiname würde also beispielsweise Nordwind_Backend20070223_005830.accdb lauten.
975
Kapitel 19
Das Tool Geplante Tasks starten Sie mit dem gleichnamigen Eintrag der Systemsteuerung. Im Originalzustand enthält das Tool keine Einträge (siehe Abbildung 19.5).
Abbildung 19.5: Das Tool »Geplante Tasks«
Um einen Aufruf eines Skripts anzulegen, klicken Sie doppelt auf den Eintrag Geplanten Task hinzufügen. Ein Assistent wird gestartet und fragt Sie zunächst nach der auszuführenden Datei. Hier geben Sie die gewünschte .vbs-Datei an und im nächsten Schritt einen Tasknamen und die Frequenz der Ausführung – in diesem Fall »Täglich« (siehe Abbildung 19.6).
Abbildung 19.6: Name des Tasks und Frequenz festlegen
Der folgende Dialog fragt ab, zu welcher Zeit der Task gestartet werden soll und ob dies täglich, nur werktags oder alle x Tage zu erfolgen hat. Außerdem geben Sie hier das Startdatum ein (siehe Abbildung 19.7).
976
Installation, Betrieb und Wartung
Abbildung 19.7: Angabe der Uhrzeit und der Tage
Schließlich geben Sie den Benutzer samt Kennwort ein, in dessen Kontext der Task durchgeführt werden soll. Gegebenenfalls sollen die Dateien in einem Verzeichnis gespeichert werden, auf das nur bestimmte Benutzer Zugriff haben – beachten Sie dies beim Anlegen des Tasks. Nach dem Beenden des Assistenten können Sie die Eigenschaften des Tasks mit einem Doppelklick auf den entsprechenden Eintrag in der Liste der geplanten Tasks ansehen.
Dateiname der Sicherung Im Beispiel des Taskmanagers mit VB Skript haben Sie bereits eine Möglichkeit für den Dateinamen einer Backup-Datei kennen gelernt – dabei wird der Dateiname einfach um die genaue Zeit des Backups erweitert.
Dateien regelmäßig überschreiben Wenn Sie beispielsweise nur den aktuellen Stand der letzten sieben einzelnen Tage sichern möchten, können Sie am aktuellen Tag jeweils die vor einer Woche angelegte Kopie überschreiben. Sie benötigen dann nur sieben verschiedene Dateinamen, die entweder die Wochentage oder eine entsprechende Zahl im Dateinamen tragen.
Benutzer vor dem Backup benachrichtigen Unter Umständen möchten Sie die Daten vielleicht mehrmals am Tag sichern, weil es sich um sehr wichtige Informationen handelt, die auch noch regelmäßig geändert werden. In diesem Fall kann das ausschließlich nächtliche Speichern durchaus fatale
977
Kapitel 19
Folgen haben, beispielsweise wenn kurz vor Feierabend die Datenbank durch einen Netzwerkfehler oder Ähnliches zerstört wird. Der erste Tipp für diesen Fall ist: Verwenden Sie keine Access-Datenbank, sondern den Microsoft SQL Server 2005, die passende Express Edition oder auch ein Nicht-MicrosoftProdukt wie etwa MySQL – weitere Informationen hierzu erhalten Sie in Kapitel 18, »Sicherheit von Access-Datenbanken« – als Backend. Diese loggen die Datenänderungen teilweise auch abseits von Backups mit und können die zum Zeitpunkt des Absturzes enthaltenen Daten fast völlig wieder herstellen. Davon abgesehen können Sie etwa einen Dump einer MySQL-Datenbank per Kommandozeile erstellen. Falls es dennoch eine Access-Datenbank sein muss, gibt es verschiedene Möglichkeiten, die hier nur angerissen werden können: Sorgen Sie dafür, dass alle geöffneten Frontends zur gleichen Zeit eine Meldung ausgeben, dass beispielsweise in fünf Minuten eine Sicherung durchgeführt wird und dazu alle Tabellen, Abfragen, Formulare und Berichte geschlossen sein müssen. Nun stellt sich erstens die Frage, wie Sie die Frontends dazu bringen, zur gleichen Zeit diese Meldung anzuzeigen und dann auch möglichst gleichzeitig alle nicht geöffneten Objekte zu schließen. Eine Rolle wird dabei ein ständig im Hintergrund geöffnetes Formular spielen, das regelmäßig die Zeit abfragt und beim Erreichen eines bestimmten Zeitpunktes die Meldungen anzeigt. Bleibt das Problem mit der synchronen Anzeige der Meldungen: Nicht alle Rechner haben zwingend die gleiche Systemzeit. Sie müssen bei dieser Lösung also dafür sorgen, dass die Zeit zentral ermittelt wird. Die zweite Variante ist, dass Sie den Sicherungsvorgang starten, wenn kein Benutzer aktiv ist. Um zu ermitteln, wann das der Fall ist, könnten Sie immer, wenn ein Benutzer den Fokus auf ein anderes Formular, Steuerelement oder sonstiges Objekt verschiebt, einen Datensatz in einer speziell für diesen Fall angelegten Tabelle im Backend auf die aktuelle Zeit einstellen. Wiederum per ständig geöffnetem, aber am besten nicht sichtbarem Formular wird per OnTimer-Ereignis regelmäßig abgefragt, wann der letzte Benutzer mit der Datenbank gearbeitet hat. Nach einer bestimmten Zeit ohne Aktionen werden dann automatisch die Objekte aller Frontends geschlossen, um Zugriffe auf das Backend auszuschließen, und das Backup erstellt.
19.7 Datenbank reparieren Es gibt verschiedene Möglichkeiten, wie Access-Datenbanken dem Benutzer mitteilen, dass sie defekt sind und eine Reparatur benötigen. Sie lassen sich einfach nicht mehr öffnen oder stürzen in bestimmten Situationen komplett ab.
978
Installation, Betrieb und Wartung
19.7.1 Symptome Typische Symptome einer beschädigten Datenbank sind folgende: Es erscheint die Fehlermeldung »Nicht erkennbares Datenbankformat: , Fehlernummer 3343. Access meldet, dass die Reparatur nicht erfolgreich durchgeführt werden konnte«. Es erscheint ein Dialog zur Eingabe eines Kennworts, obwohl die Datenbank nicht geschützt ist. Beschädigungen, die sich durch diese oder andere Symptome bemerkbar machen, entstehen zum Beispiel durch einen Absturz der Datenbank, ein Backend auf einem überlasteten Server, falsche Netzwerkeinstellungen am Server, Netzwerkausfälle, Fehler im Filesystem oder fehlerhaft wiederhergestellte Backups (etwa durch Lesefehler beim Kopieren von CD). Wenn Sie Glück haben, passiert so etwas bereits während der Entwicklungsphase, in der Sie hoffentlich immer über eine aktuelle Sicherungskopie verfügen. Wenn Sie aber Pech haben, tritt dieser Fall ein, wenn sich die Datenbank bereits beim Kunden im vollen Einsatz befindet. Hoffentlich haben Sie ihm bereits vorher mitgeteilt, dass AccessAnwendungen keine Wunderkisten sind und auch streiken können, und ihm empfohlen, seinerseits regelmäßig Sicherungen anzulegen. Wenn keine Sicherung existiert oder diese lange zurückliegt, ist der Versuch, die Datenbankanwendung und die enthaltenen Daten zu retten, sicher zu empfehlen. Sie müssen dabei allerdings einige Voraussetzungen beachten. Leider greifen die nachfolgend vorgestellten Möglichkeiten nur zum Teil. Wenn Sie selbst keine Chance mehr sehen, die Daten zu retten, dies aber unabdingbar ist, können Sie immer noch einen professionellen Dienstleister mit der Reparatur der Datenbank beauftragen. Eine erfolgreiche Wiederherstellung der Datenbank kann aber auch dieser nicht garantieren – es gibt Fälle, in denen sie irreparabel beschädigt ist.
19.7.2 Sicherung geht vor Nicht nur bei der Arbeit mit Access-Datenbanken, sondern auch beim Versuch, eine Datenbank zu reparieren, sollten Sie zuvor eine Sicherungskopie der scheinbar beschädigten Datenbank anlegen. Um auszuschließen, dass ein Festplattendefekt schuld an dem Schaden ist und gegebenenfalls auch die Sicherungskopie dadurch zerstört wird, speichern Sie diese auf einem externen Medium wie einer CD oder einem externen Laufwerk. Wenn Sie versuchen, eine defekte Access-Anwendung mit den eingebauten Befehlen zu komprimieren und zu reparieren, können Sie diese möglicherweise für Rettungsversuche
979
Kapitel 19
durch entsprechende Dienstleister nicht mehr verwenden – das gilt prinzipiell für alle Reparaturversuche.
19.7.3 Allgemeine Reparaturversuche Wenn Sie die Datenbank gesichert haben, können Sie versuchen, die Datenbank selbst zu reparieren. Falls es möglich ist, sie mit Access zu öffnen, bevor sie abstürzt, können Sie die dort angebotene Funktion zum Komprimieren und Reparieren der Datenbank verwenden (Office-Menü-Eintrag Verwalten|Datenbank komprimieren und reparieren). In manchen Fällen hilft es auch, eine neue Datenbank anzulegen und alle Objekte aus der beschädigten Datenbank in die neue Datenbank zu importieren. Falls irgendetwas auf Probleme mit dem in der Datenbank enthaltenen Code hindeutet (etwa Formulare, die sich nicht mehr öffnen lassen, oder Meldungen wie »Es konnte auf den OLE-Server nicht zugegriffen werden« oder »Die Netzwerkverbindungen konnten nicht hergestellt werden«), können Sie versuchen, die Datenbank zu dekompilieren. Dabei hilft der Befehlszeilenschalter /decompile. Erstellen Sie entweder eine Verknüpfung mit folgendem Inhalt oder setzen Sie die folgende Anweisung im Ausführen…-Dialog von Windows ab, wobei Sie gegebenenfalls noch den Pfad zur Datei MSACCESS.EXE anpassen müssen: "C:\Programme\Microsoft Office\OFFICE12\MSACCESS.EXE" /decompile
19.7.4 Weitere Informationen Die hier genannten Rettungsmöglichkeiten umfassen nur allgemeine Lösungsansätze. Zu diesem Thema finden Sie zahlreiche Artikel in der Knowledge-Base von Microsoft; dort gibt es Lösungsansätze für spezielle Probleme. Auch die Internetseiten professioneller Dienstleister bieten weitere Informationen; dort können Sie auch Preise und Möglichkeiten für die Reparatur einer beschädigten Datenbank erfragen. Zusätzlich gibt es natürlich auch entsprechende Tools wie AccessRecovery oder EasyRecovery. Hinweise dazu finden Sie auf http://www.access-entwicklerbuch.de. Damit Sie nicht in die Verlegenheit geraten, für die Reparatur einer Datenbankanwen dung tief in die Tasche greifen zu müssen, sollten Sie mit entsprechenden Backups vorsorgen – Strategien und technische Möglichkeiten dazu finden Sie weiter oben.
19.8 Verweise und Probleme mit Verweisen Verweise werden unter Access verwendet, um externe Bibliotheken mit Klassenobjekten, Eigenschaften, Methoden und Ereignissen unter VBA verfügbar zu machen. Dabei han-
980
Installation, Betrieb und Wartung
delt es sich um Type Libraries (.tlb), Object Libraries (.olb), Control Libraries (.ocx) oder auch Access-Datenbanken (*.accdb, *.accde). Es lassen sich aber auch ActiveX-DLLs (.dllDateien) verwenden. Standardmäßig sind unter Access 2007 vier Bibliotheken eingestellt (siehe Abbildung 19.8).
Abbildung 19.8: Der Verweise-Dialog mit den Standardverweisen
Durch Setzen eines Verweises auf externe Bibliotheken wie etwa Office-Anwendungen wie Word, Excel oder Outlook machen Sie deren Objektbibliotheken für den Zugriff per VBA verfügbar. Sie können genauso auf das Objektmodell der Anwendungen zugreifen, als ob Sie direkt mit diesen Anwendungen arbeiteten. Das Gleiche gilt auch für andere Komponenten von Microsoft und auch von externen Anbietern. Beim Öffnen prüft Access, ob die angegebenen Dateien geladen sind. Ist das nicht der Fall, sucht Access nach einer Datei mit der gleichen GUID, anschließend nach einer Datei mit dem gleichen Dateinamen wie die im Verweis enthaltene Datei. Schließlich gibt es noch die ProgID (etwa »Word.Application«) als möglichen Anhaltspunkt. Wenn auch hier nichts zu holen ist, wird der Verweis im Verweise-Dialog als NICHT VORHANDEN gekennzeichnet.
19.8.1 Meldung bei fehlenden Verweisen Access 2007 meldet fehlende Verweise direkt beim Öffnen der Anwendung (siehe Abbildung 19.9). Dieses Feature existiert seit Access 2003 und ist eine Verbesserung gegenüber früheren Versionen, die den Anwender erst beim Auftreten eines konkreten Problems mehr oder weniger aussagekräftig informierten (siehe oben).
981
Kapitel 19
Neu ist, dass Access 2007 beim ersten Öffnen einer Datenbank mit fehlenden Verweisen im Anwendungsverzeichnis sowie in den darunter liegenden Verzeichnissen nach der passenden Datei sucht. Bei allen folgenden Aufrufen erfolgt keine Suche mehr.
Abbildung 19.9: Access hat ein Problem mit einem Verweis entdeckt
Nach dem Auftauchen der Meldung aus Abbildung 19.9 lohnt sich ein Blick in den Verweise-Dialog, den Sie mit dem Menüeintrag Extras/Verweise in der VBA-Entwicklungs umgebung öffnen. Dort wird ein fehlender Verweis auf eine externe Datenbank angezeigt (siehe Abbildung 19.10).
Abbildung 19.10: Anzeige eines fehlenden Verweises
19.8.2 Ohne Verweise arbeiten? Fast alle Anweisungen, die man unter VBA verwendet, werden über so genannte OLEObjekte zur Verfügung gestellt. Diese OLE-Objekte lassen sich innerhalb von VBA unter anderem mit der Methode CreateObject erzeugen – falls Access sie nicht schon beim Start der Anwendung selbst erzeugt hat, wie etwa das DBEngine-Objekt der DAO-Bibliothek. Voraussetzung ist, dass
982
Installation, Betrieb und Wartung
Sie die ProgID des Objekts kennen. Meist ist diese aber offensichtlich: So greifen Sie auf Word mit Word.Application oder Excel mit Excel.Application zu. Der Vorteil ist: Access sucht sich so immer die Bibliothek mit der angegebenen ProgID heraus (also etwa Word.Application) und muss nicht auf die Verwendung der richtigen Version achten. Für die Abwärtskompatibilität haben Sie in diesem Fall allerdings selbst zu sorgen – wenn Sie eine Datenbank weitergeben, die Funktionen einer OfficeAnwendung in der Version 2007 enthält, müssen Sie bei einem System mit Office 2000 mit dem Scheitern rechnen.
19.8.3 Late Binding und Early Binding Late Binding und Early Binding heißen die beiden Techniken, die Objekte unter VBA zur Verfügung stellen. Beide haben Sie in den vorherigen Abschnitten bereits kennen gelernt: Early Binding bedeutet, dass Sie einen Verweis auf das gewünschte Objekt erstellen und dann über dieses Objekt zugreifen; beim Late Binding erstellen Sie den Verweis erst über das Füllen einer entsprechenden Objektvariable mit der CreateObject- oder der GetObject-Methode – wobei man Letztere verwendet, wenn schon eine Instanz dieses Objekts läuft.
19.8.4 Verweise und die Weitergabe von Anwendungen Bei weitergegebenen Anwendungen bringen Verweise auf Office-Anwendungen oft Pro bleme, weil sie auf dem Zielrechner andere Versionen der entsprechenden Bibliotheken vorfinden als erwartet. Befindet sich auf dem Zielrechner eine jüngere Version als angenommen, bedeutet dies in der Regel kein Problem, eine ältere Version dagegen schon. Daher ist es empfehlenswert, eine für die Weitergabe bestimmte Anwendung mit der ältesten Version von Access beziehungsweise Office zu erstellen, die auf dem Zielrechner laufen kann. Für Zielrechner mit Office 97 kommt da quasi nur die Version 97 in Be tracht. Anders ist es ab Access 2000: Wenn Sie eine Anwendung mit Access 2000 und mit Verweisen auf Office 2000-Anwendungen erstellen, ist diese in der Regel auch unter Access 2002, Access 2003 und Access 2007 einsetzbar.
19.8.5 Auf Nummer Sicher Sie können programmatisch sicherstellen, dass auf dem Zielrechner die aktuellste Ver sion eines Verweises verwendet wird, wenn Sie beim Start eine passende Routine aufru fen. Welche Probleme Sie damit umgehen können, sehen Sie an folgendem Beispiel: Le gen Sie in einer mit Access 2007 erstellten Datenbank (Standarddateiformat Access 2000) die Verweise aus Abbildung 19.11 an.
983
Kapitel 19
Abbildung 19.11: Diese Office-Verweise sollen gleich in einer älteren Office-Version zum Einsatz kommen
Öffnen Sie die Datenbank anschließend auf einem Rechner, der mit Office 2003 ausgestattet ist. Die Datenbank lässt sich zwar ohne Probleme öffnen, aber ein Blick in die Verweise bringt schlechte Nachricht, wie Abbildung 19.12 zeigt. Bis auf die Access- und die Microsoft Office-Bibliothek konnte Access die Verweise nicht anpassen.
Abbildung 19.12: Die meisten Verweise fehlen unter Office 2000
Eine Lösung sieht so aus, dass Sie vor der Weitergabe der Datenbank alle Verweise, die kritisch sein könnten, manuell aus dem Verweise-Dialog entfernen. Zusätzlich fügen Sie der Anwendung eine Funktion hinzu, die alle notwendigen Verweise manuell anlegt – und zwar in der aktuellsten Fassung. Das erledigt die folgende Routine:
984
Installation, Betrieb und Wartung Public Function VerweiseAnpassen() On Error Resume Next References.Remove References("Outlook") References.AddFromGuid "{00062FFF-0000-0000-C000-000000000046}", References.Remove References("Word") References.AddFromGuid "{00020905-0000-0000-C000-000000000046}", References.Remove References("Excel") References.AddFromGuid "{00020813-0000-0000-C000-000000000046}", References.Remove References("Office") References.AddFromGuid "{2DF8D04C-5BFA-101B-BDE5-00AA0044DE52}",
0, 0 0, 0 0, 0 0, 0
End Function Listing 19.11: Automatisches Anlegen von Verweisen
Sie finden die Funktion in der Datenbankdatei \Kap_19\Verweise.accdb. Interessant ist dabei jeweils die AddFromGuid-Methode. Sie enthält die GUID, unter der die jeweiligen Bibliotheken im System registriert werden. Diese ist im Übrigen für alle Versionen gleich. Die übrigen beiden Parameter enthalten die Werte für die Eigenschaften Major und Minor, die stellvertretend für die Versionsnummer mit folgendem Format sind: <Major>.<Minor> Durch die Wahl von 0 und 0 wird automatisch die aktuellste Version verwendet. Auf einem Rechner mit Windows 2000 und Office 2000 sieht das Verweise-Fenster anschließend etwa wie in Abbildung 19.12 aus.
Abbildung 19.12: Access 2000 mit korrekt eingebundenen Verweisen
985
Kapitel 19
19.8.6 Gleichnamige Objekte, Eigenschaften und Methoden in Bibliotheken Gelegentlich kommt es vor, dass ein VBA-Projekt Verweise auf zwei oder mehr Bib liotheken enthält, die mit gleich lautenden Elementen bestückt sind. Das bekannteste Beispiel dafür sind die beiden Bibliotheken DAO und ADO. Leider haben gleich lautende Elemente nicht immer genau die gleiche Funktion, ja noch nicht einmal die gleiche Syntax, sodass Sie sicherstellen müssen, dass Sie mit der richtigen Version arbeiten. Unter Access 2007 ist standardmäßig nur die neue DAO-Bibliothek eingebunden. Sie müssen sich also eigentlich nur dann Gedanken machen, wenn Sie zusätzlich noch die ADO-Bibliothek referenzeiren. Wie stellen Sie dann fest, mit welcher Fassung des Recordset-Objekts Sie gerade arbeiten? Die erste Regel lautet: Bei gleichnamigen Ele menten verwendet VBA immer die aus der in der Verweisliste höher angeordnete Fas sung. Daraus resultiert die zweite Regel: Verlassen Sie sich nicht darauf, wie die Biblio theken angeordnet sind, und setzen Sie bei der Deklaration explizit den Bezug auf die entsprechende Bibliothek, beispielsweise folgendermaßen: Dim rst As DAO.Recordset
oder Dim rst As ADODB.Recordset
Unter Access 2007 gilt übrigens noch eine dritte Regel: Verwenden Sie nicht mehr das Recordset-, sondern das Recordset2-Objekt.
Quellen zu diesem Kapitel [1] Microsoft VirtualPC: http://www.microsoft.com/downloads/details.aspx?FamilyId= 6D58729D-DFA8-40BF-AFAF-20BCB7F01CD1&displaylang=en
986
Index Symbole .accda (Neu in Access 2007) 37 .accdb (Neu in Access 2007) 37 .accde 926 Neu in Access 2007 37 .accde-Datei (Performance) 756 .accdr (Neu in Access 2007) 38 .accdt (Neu in Access 2007) 37 .accfl 68 Neu in Access 2007 38 .lccdb (Neu in Access 2007) 37 .ldb 37 .mda 37 .mdb 37 .mde 37 .mdw 38 1:1-Beziehung 119 Beispiel 120 einsetzen 124 im Formular 231 in Abfragen 172 1:n-Beziehung 113 Beispiele 113 Datensatzquelle für Bericht 400 im Bericht 400 im Formular 233 per Listenfeld 238 per Unterformular 234 reflexiv 125 reflexiv in Abfragen 184 TreeView 316 verknüpfte Daten auswählen 77
A Abbrechen-Schaltfläche im Detailformular 219 Abfrage 147
B Back (Anlage-Feld) 604 Backend 965 BackStyle 293 Anlage-Feld 604 Bearbeiten eines Datensatzes (DAO) 550 Bearbeitungsformular für Listenelemente 295 Feldeigenschaft 79 Bedingte Formatierung (Bericht) 366 BeginTrans 268 ADO 594 DAO 563 Behandlung doppelter Datensätze (SQL) 480 Bei Aktivierung (Bericht) 378, 384 Bei Aktivierung (Formular) 213 Bei Änderung (Formular) 214, 215 Bei angewendetem Filter (Bericht) 419 Bei Deaktivierung (Bericht) 378, 384 Bei Entladen (Bericht) 419 Bei Fehler (Bericht) 378, 385 Bei Filter (Bericht) 419 Bei Fokuserhalt (Bericht) 419 Bei Fokuserhalt (Formular) 214, 215 Bei Fokusverlust (Bericht) 419 Bei Fokusverlust (Formular) 214, 215 Bei Geändert (Formular) 211, 214, 215 Bei Größenänderung 303 Bei Größenänderung (Bericht) 419 Bei Größenänderung (Formular) 210, 213 Bei Laden (Bericht) 419 Bei Löschbestätigung (Formular) 212 Beim Aktivieren Formularereignis 209 Beim Anzeigen 830 Beim Anzeigen (Bericht) 419 Beim Anzeigen (Formular) 211, 212, 213 Formularereignis 208, 209 Beim Deaktivieren (Formular) 209 Beim Doppelklicken (Bericht) 419 Beim Drucken (Bericht) 388, 416 Beim Entladen (Formular) 210 Beim Formatieren (Bericht) 387
991
Index
Beim Hingehen (Formular) 214, 215, 261 Beim Klicken (Bericht) 419 Beim Klicken (Formular) 215 Beim Laden (Formular) 209, 213 Beim Löschen (Formular) 212 Beim Öffnen (Bericht) 378, 416 Beim Öffnen (Formular) 209, 213 Beim Öffnen filtern (Formular) 205 Beim Schließen Formular 209 Bericht 378 Beim Verlassen (Formular) 214, 215 Bei nicht in Liste 215, 259 Bei Ohne Daten (Bericht) 378, 385 Bei Rückgängig (Formular) 211 Bei Seite (Bericht) 378, 385 Bei Zeitgeber 770 Formular 203 Bericht 419 Benutzerdefinierte Auflistung 798 Benutzerdefinierte Fehler 709 Benutzerdefinierte Fehlermeldung 701 Benutzerdefinierte Klasse 770 Benutzerdefiniertes Ereignis 790 Benutzerdefinierte Typen 440 Benutzeroberfläche (Neu in Access 2007) 32 Berechnung im Bericht 412 in Formularen 412 Berechnungen auf Spaltenbasis (Neu in Access 2007) 47 Bereich auf neuer Seite 414 Bereich wiederholen (Bericht) 395 Bericht 361 acDetail 380, 381 acDialog 421 acFooter 381 acFormAdd 421 acHeader 381 acPageFooter 381 acPageHeader 381 alternierende Hintergrundfarbe 366 anlegen 362 anzeigen 368
992
Autoformate 366 Bedingte Formatierung 366, 367 Bei Aktivierung 384 Bei Deaktivierung 384 Bei Fehler 385 Beim Klicken 421 Beim Öffnen 368, 369 Beim Schließen 378 Bei Ohne Daten 385 Bei Seite 385 Bereiche 361, 367, 376 Bereich wiederholen 395 Berichtsansicht 368 Berichtsansichten 368 Berichtsbereiche 367, 376 Berichtskopf 367 Berichtsvorschau 371 Beschriftung verschieben 397 Caption 383 DataMode 421 Datensätze durchstreichen 390 Detailansicht einfacher Daten 396 DoCmd.OpenReport 369, 370 Eigenschaften 390 einfaches Layouten (Bericht) 364 Entwurfsansicht 368 Ereignisse 377 erstellen 361 filtern 369 FilterName 369 FilterOn 370 FilterOnLoad 370 Fußzeilenbereich 375, 391 gestapeltes Layout 365 Gitternetzlinien 367 Gruppieren nach 392, 400 modal 369 Performance 742 Seite einrahmen 386 Seitenfuß 367 Seitenkopf 367 sortieren 368, 369 tabellarisches Layout 364 Übersicht einfacher Daten 398 Unterbericht 361, 395, 396, 404
Index
Berichtsansicht 368, 419 Beispiel 420 Neu in Access 2007 48 Berichtsansichten 368 Berichtsbereiche 367, 376 Zugriff 380 Berichtsfuß 367 Berichtskopf 367 Berichtsvorschau filtern 371 sortieren 371 Beschriftung Feldeigenschaft 74 BETWEEN … AND (SQL) 468 Beziehung reflexiv 124 Beziehungen 107 Dialog 76 halbautomatisch festlegen 110 herstellen 75 Beziehungsarten im Formular 216 Bild als Anlage speichern 602 aus OLE-Feld in Formular anzeigen 624 aus OLE-Feld wiederherstellen 617, 627 binär im OLE-Feld speichern 615 im OLE-Feld einbetten 614 mit OLE-Feld verknüpfen 614 speichern in verschiedenen Formaten 628 von Festplatte im Formular anzeigen 619 von Festplatte in Bericht anzeigen 619 Bild-Steuerelement (Performance) 739 Bild aus Anlage-Feld auf Festplatte speichern 608 im Bericht anzeigen 607 im Formular anzeigen 604 Bilddateien Format im Anlage-Feld 66 Bilddatei hinzufügen (Ribbon) 647 Bilder 601 Bildlaufleisten (Formular) 227
D DAO 501 AbsolutePosition 537 Access-Projekt 501 ADO 501 Aktuelle Datenbank referenzieren 511, 513 Aktuelle Position des Datensatzzeigers ermitteln 537 Alle Datensätze durchlaufen 536 Alle Datensätze mit einem bestimmten Kriterium finden 544 Alle Tabellen ausgeben 526 AllowMultipleValues 521 AllowValueListEdits 521 Anlegen eines Datensatzes 549
E Early Binding 983 Performance 745 Edit (DAO) 535, 550 editBox (Ribbon) 650, 681 Eigenschaft nicht öffentlich 782 öffentlich 782 Eindeutig Indexeigenschaft 71 Eindeutige Datensätze 105 Eindeutigen Index anlegen (SQL) 494 Eindeutiger Schlüssel in UNION-Abfragen 167 Einfache Indizes anlegen (SQL) 497 Einfaches Formular Formularansicht 194 Eingabe erforderlich Feldeigenschaft 103 Eingabeformat Übersicht 105 Eingabevalidierung (Formular) 275 Eingebaute Objekte 774 enabled (Ribbon) 669, 683 Endlosformular Daten anzeigen (Formular) 220 Formularansicht 195 Entitätsintegrität 105 Entwerfen in der Datenblattansicht (Neu in Access 2007) 45 Entwicklungsumgebung 869 Anpassen (VBE) 869 Objektmodell (VBE) 876 Programmieren 874
Index
Entwurfsänderungen zulassen (Formular) 205 Entwurfsansicht Bericht 368 Formular 192 Enum 436 EOF ADO 580 DAO 534, 538 Ereignis abfangen 790 anlegen 793 auslösen 795 hinzufügen 795 Ereigniseigenschaften (Ribbon) 643 Ereignisprozedur hinzufügen (VBE) 900 Ereignisse bei der Datenbearbeitung 210 beim Öffnen von Formularen 209 beim Schließen von Formularen 209 im Formular anlegen 207 in Berichten 377 von Formularen 206 von Steuerelementen 212 von Datensatzgruppen (ADO) 599 Err 704 Ersetzen eines Attachments (DAO) 558 Erstellen einer Beziehung (DAO) 524 einer Tabelle (DAO) 514 eines Index (DAO) 522 von Eigenschaften (DAO) 526 Erstellen einer Beziehung (ADO) 575 Erstellen eines Index (ADO) 574 Erste Normalform 93 Esc (Formular) 262 Eval (Performance) 759 Event 795 Execute 251, 252 ADO 585, 593 DAO 562 SQL 462 Exit 445, 448 Exit For 445
Exit Sub 276 Exklusiver Zugriff (Performance) 756 ExpandedImage 357 Export (Neu in Access 2007) 53 Export-Template (Neu in Access 2007) 53 Extremwerte per Unterabfrage 178 Extremwert ermitteln per Abfrage 177 von Gruppierungen 177
F Fahrtenbuch 145 Farbauswahldialog (Neu in Access 2007) 52 Fehler auswerten 704 bei API-Aufrufen 710 benutzerdefiniert 709 in Ribbon-XML-Dokumenten 641 logische Fehler 695 Fehlerarten 691 Fehlerbehandlung 691 als Vereinfachung 702 Aufbau 703 ausschalten 705 funktional 706 hinzufügen 716 in Formularen 719 in Runtimes 960 in VBA 701 per Knopfdruck (VBE) 872 Fehlerbeschreibung 705 Fehlerdokumentation 691, 711 Fehlerinformationen 712 Fehlermeldung benutzerdefiniert 701 Fehlernummer 705 Fehlerübermittlung 711 Feld 68 ausgliedern in separate Tabelle 114 hinzufügen 58, 60, 68 löschen 60
1001
Index
Felddatentyp 58 auswählen 58 Felder einschränken per Abfrage 147 Feldgröße Feldeigenschaft 73 voreinstellen 73 Feldliste 189 Feldname 89 für Fremdschlüsselfelder 90 für Primärschlüsselfelder 89 Indexeigenschaft 71 Field (DAO) 523 Field2 (DAO) 518 Fields ADO 583 DAO 535 FileCopy 974 FileData (Anlage-Feld) 604 FileName 553 Anlage-Feld 604 FileTimeStamp 553 FileType 553 FileURL 553 Filter ADO 590 DAO 535 Filtern ADO 589 beim Laden eines Formulars (Neu in Access 2007) 46 beim Öffnen eines Formulars 205 Bericht 369 Berichtsvorschau (Bericht) 371 im Formular 281 in der Datenblattansicht 281 in der Datenblattansicht (Neu in Access 2007) 46 Listenfeld 284 mit der Filter-Eigenschaft (DAO) 548 von Datensätzen (DAO) 546 FilterName (Bericht) 369 FilterOn (Bericht) 370 FilterOnEmptyMaster 302 FilterOnEmptyMaster (Formular) 261
Logische Fehler 695 Lokal-Fenster 701 Long Integer Felddatentyp 59 LONGTEXT (SQL) 491 Lookup-Beziehung 113 im Formular 232 Lookup-Tabelle 116 Lookup Join (Performance) 730 Loop Until 448 Loop While 447 Löschen Beziehung(ADO) 577 Beziehung (DAO) 526 Datei in Attachment-Feldern (DAO) 558 Datensatz (DAO) 550 Datensatz im Formular 212 Feld (SQL) 500 Index (ADO) 575 Index (DAO) 523 Tabelle (ADO) 573 Tabelle (DAO) 521 Löschweitergabe 107 Löschweitergabe (SQL) 496 Löschweitergabe an verwandte Datensätze 112 lvwReport 331, 341
M m:n-Beziehung 117 Beispiel 117, 119 Hauptformular 241 im Bericht 403 im Formular 240 mit mehrwertigen Feldern verwalten 119 per Listenfeld 246 reflexiv 126, 186 Suchen in 169 Unterformular 242 verknüpfte Daten auswählen 80 MakeThumb 629
Index
Makros (Neu in Access 2007) 49 Manipulation des Datenmodells ADO 571 DAO 514 Mastertabelle 76, 108 Max (SQL) 471 maxLength (Ribbon) 651, 684 MDI (Neu in Access 2007) 40 Me (Performance) 748 Mehrbenutzerbetrieb 965 Mehrere Werte zulassen Feldeigenschaft 80, 84 Mehrfachauswahl 301 Mehrfachauswahl im Navigationsbereich (Neu in Access 2007) 33 Mehrschichtige Anwendung (Objektorientierung) 846 Mehrwertige Felder anlegen (DAO) 518 ausgeben (ADO) 580 DAO 559 Daten bearbeiten 82 in Abfragen 149 Neu in Access 2007 41 SQL 485 Mehrwertsteuer 409 Memo Besonderheiten 61 Felddatentyp 59 Menü (Ribbon) 661 menu (Ribbon) 661, 681 Menüleisten aus Access 2003 678 menuSeparator (Ribbon) 662, 681 MergeImages 629 Merge Join (Performance) 730 Methode 788 Microsoft ListView Control 329 Microsoft Office 12.0 Access database engine Objects 501 Microsoft Office 12.0 Object Library (Ribbon) 636 Microsoft Visual Studio 6.0 871 Mid 888 Min (SQL) 471 Mitarbeiterverwaltung 136
Schaltflächen 646 Schnellzugriffsleiste 632, 633 showImage 684 showItemImage 684 showItemLabel 684 showLabel 684 size 649, 651 sizeString 684 Sonderzeichen 670 Splitbutton 663 splitButton 663 startFromScratch 638 Symbol hinzufügen 646 Symbolleiste für den Schnellzugriff 632, 633 tab 632, 633 tabSet 680, 682 tag 684 Tastenkombinationen 666 Textfeld 650, 651 title 662 Trennstrich 665 Umgang mit 644 Umschaltflächen 658 USysRibbons 637 XML-Schema-Datei 632 XML Notepad 2007 633 XSD 632 XSD-Datei 632, 633 ribbon 682 Ribbon (Neu in Access 2007) 32, 35 Rich-Text 289 in Memofeldern 61 Rich-Text (Neu in Access 2007) 42 RIGHT OUTER JOIN (SQL) 478 RightPadding 202, 306 Rollback 270 unter DAO 564 RollbackTrans (ADO) 594 Routine 450 alle verwenden 457 Arten 450 lose Kopplung 453 Rückgabewerte 453 starker Zusammenhalt 453
1015
Index
Routinennamen 451 Länge 452 Rows (ADO) 583 rows (Ribbon) 659, 684 RowSource 153, 239, 521 RowSourceType 294, 521 Rückgabewerte einer Routine 453, 455 Runtime 958 Neu in Access 2007 38 Simulation 960 Rushmore (Performance) 730 Rushmore Restriction (Performance) 729
S Sandbox 935 Sandbox-Modus 935 Saturation 629 Save (ADO) 594 SaveFileToOLEField 615 SaveImage 629 SaveOLEFieldToFile 617 ScaleHeight 386 ScaleLeft 386 ScaleMode 386 ScaleTop 386 ScaleWidth 386 Schaltfläche 292 anpassen (Ribbon) 649 mit Bild und Text (Neu in Access 2007) 43 Performance 739 Ribbon 646 Schleifen 445 Schlüssel alle anzeigen 71 hinzufügen Entwurfsansicht 69 Schnellauswahl per Kombinationsfeld 282, 829 Schnellzugriffsleiste (Ribbon) 632 anwendungsspezifisch 636 Eintrag hinzufügen 672 positionieren 635
1016
Schnittstelle 811 erstellen 815 implementieren 815 Schnittstellenvererbung 812 Schriftarten (Neu in Access 2007) 53 Schriftgröße Datenblattansicht 225 Schutz vor bösartigen SQL-Statements 934 Screen 227 Screen.ActiveControl 279 screentip (Ribbon) 649, 684 SDI (Neu in Access 2007) 40 SearchDirection (ADO) 589 Section 380 Seek (ADO) 586 Seek (DAO) 535, 542 SeekOption (ADO) 587 Seite einrahmen 386 Seitenansicht 368 Seitenfuß 367 auf bestimmten Seiten 415 in Formularen 192 Seitenkopf 367 auf bestimmten Seiten 415 in Formularen 192 Sekundärschlüssel 70 SELECT (SQL) 463 SELECT * (SQL) 464 Select Case 444 SELECT Count Beispiel 183 Selected 295 SelectedImage 312, 357 SelectedItem 325, 326 SelectedVBComponent (VBE) 878 SELECT INTO (SQL) 489 Semantische Integrität 104 SendKeys 278 separator (Ribbon) 665, 682 SeparatorCharacters 295 SET (SQL) 487 SetData 348 SetFocus (Formular) 228
Index
SetWarnings (DoCmd-Methode) 223 sharedControls 682 SharePoint (Neu in Access 2007) 54 SHOW DATABASES (MySQL) 943 ShowDatePicker 291 showImage (Ribbon) 684 showItemImage (Ribbon) 684 showItemLabel (Ribbon) 684 showLabel (Ribbon) 684 ShowOnlyRowSourceValues 521 SHOW TABLES (MySQL) 944 ShowWindow 710 ShutDownGDIP 629 Sicherheit 925 unter MySQL 945 Sicherheitsfunktionen (Neu in Access 2007) 39 Sicherheitssystem 925 entfallenes Feature 38 Sichern einer Datenbank 971 Single Felddatentyp 59 Single Document Interface (Neu in Access 2007) 40 size (Ribbon) 649, 684 sizeString (Ribbon) 651, 684 SizeToFit (Anlage-Feld) 604 Skalare Variable 783 lesen 786 SkipRecords (ADO) 588 SMALLINT (SQL) 491 Snapshot 229 Sonderzeichen (Ribbon) 670 Sort ADO 590 DAO 535, 546 SortByOn 370 Sorted 333 Sortieren ADO 590 beim Laden eines Formulars (Neu in Access 2007) 46 beim Öffnen eines Formulars 205 Bericht 368 Berichtsvorschau 371
Datenblattansicht (Neu in Access 2007) 46 Datensätze (DAO) 546 Layoutansicht 371 SQL 470 Sortierung 376 SortKey 333, 334 SortOrder 333 Spaltenanzahl 229, 248 Feldeigenschaft 78 Spaltenbreite 248 Spaltenbreiten 229 Feldeigenschaft 79 Spaltenköpfe 331 Spaltenüberschriften 229 Speichern von Bildern 601 Dateien 601 Daten in einem Array (ADO) 583 Sperren von Daten während der Bearbeitung (DAO) 530 Sperrung von Daten (ADO) 579 Split 383 Splitbutton (Ribbon) 663 splitButton (Ribbon) 663, 682 SplitFormDatasheet (Formular) 198 SplitFormOrientation (Formular) 198 SplitFormPrinting (Formular) 198 Split Forms (Formular) 196 SplitFormSize (Formular) 198 SplitFormSplitterBar (Formular) 198 SplitFormSplitterBarSave (Formular) 198 Split View (Neu in Access 2007) 46 Sprungmarken 449 SQL 459 ADD 499 ADD COLUMN 499 Aggregatfunktionen 471 Aktualisierungsweitergabe 496 ALL 481 ALTER TABLE 499 AND 467 Ändern eines Feldes 499 AS 465 ASC 470
1017
Index
SQL (Fortsetzung) Attachment 490 Avg 471 Bedingungen 466 Behandlung doppelter Datensätze 480 BETWEEN … AND 468 BINARY 492 BIT 492 BYTE 491 CHAR 491 CHARACTER 491 ComplexType 490 CONSTRAINT 491 Count 471 Count(*) 471 COUNTER 492 CREATE INDEX 497 CREATE TABLE 490 DATE() 470 Daten aktualisieren 486 Daten auswählen 463 DATETIME 492 DELETE 487 DESC 470 DISTINCT 481 DISTINCTROW 481 DROP 500 Eindeutigen Index anlegen 494 Einfache Indizes anlegen 497 Einsatzbereiche 462 Feldnamen ersetzen 465 First 471 FLOAT 491 FOREIGN KEY 496 Fremdschlüsselfelder festlegen 495 FROM 463 Funktionen 470 GROUP BY 472 Gruppieren von Daten 472 GUID 492 Gültigkeitsregel festlegen 493 HAVING 474 Hinzufügen eines Feldes 499 IMAGE 492 IN 468
1018
INDEX 500 Index löschen 500 INNER JOIN 476 INSERT INTO 487 INTEGER 491 INTO 489 IS NULL 468 Last 471 LEFT OUTER JOIN 478 LIKE 468 LONGTEXT 491 Löschen eines Feldes 500 Löschweitergabe 496 Max 471 Mehrwertige Felder 485 Min 471 MONEY 492 Neue Tabelle mit Daten erstellen 489 NOT 468 NULL 470 NUMERIC 492 ON 476 ON DELETE CASCADE 497 ON UPDATE CASCADE 497 Operatoren 467 OR 467 ORDER BY 470 PARAMETERS 483 Parameter verwenden 483 PERCENT 483 Primärschlüssel anlegen 494 PRIMARY KEY 494 REAL 491 REFERENCES 496 RIGHT OUTER JOIN 478 Schreibweise vereinfachen 466 SELECT 463 SELECT * 464 SELECT INTO 489 SET 487 SMALLINT 491 Sonderzeichen 465 Sortieren 470 Tabelle ändern 499 Tabelle löschen (SQL) 500
Index
SQL (Fortsetzung) Tabellen erstellen (SQL) 490 TABLE (SQL) 500 TEXT (SQL) 491 TOP (SQL) 482 UNION (SQL) 484 UNIQUE (SQL) 494 Unterabfragen (SQL) 480 UPDATE (SQL) 486 VALUES (SQL) 488 Var (SQL) 471 Varianz (SQL) 471 VarP (SQL) 471 Vergleiche mit Datumsangaben (SQL) 469 Vergleiche mit dem Null-Wert (SQL) 469 Vergleiche mit Zahlen (SQL) 468 Vergleiche mit Zeichenketten (SQL) 468 Vergleich mit den Werten einer Menge (SQL) 468 Vergleich mit Null-Werten (SQL) 468 Vergleichsausdrücke (SQL) 467 Vergleichsumkehr (SQL) 468 Verknüpfen von Tabellen (SQL) 475 WHERE (SQL) 466 WITH COMPRESSION (SQL) 491 Zugriff auf Anlage-Felder (SQL) 485 Zugriff auf externe Datenquellen (SQL) 485 Zugriff auf mehrwertige Felder (SQL) 485 Zusammengesetzter eindeutiger Schlüssel (SQL) 498 Zusammengesetzter Primärschlüssel (SQL) 498 zwischen zwei Werten (SQL) 468 SQL-Ausdruck als Datensatzquelle 152 SQL-Versionen 459 SQLDatum 161 SQL Server 2005 Express Edition 937 SQL Server Management Studio Express Edition 937 SSH 953
Structured Query Language (SQL) 459 Strukturierte Abfragesprache (SQL) 459 Style 312, 313 Sub 451, 788 Suchen im Formular 280 im Formular, schnell 280 im Navigationsbereich (Neu in Access 2007) 34 in Dynaset-Recordsets (DAO) 543 in m:n-Beziehungen per Abfrage 169 in Snapshot-Recordsets (DAO) 543 in Table-Recordset (DAO) 542 Sum (SQL) 471 Summen Berichtslayout 375 Summenbildung 412 supertip (Ribbon) 684 Symbol hinzufügen (Ribbon) 647 im TreeView 315 Symbolleiste für den Schnellzugriff (Ribbon) 633 Symbolleisten Access 2003 678 Syntaxfehler 692 Syntaxprüfung 693
T tab (Ribbon) 638, 682 ausblenden 668 einblenden 668 Tabellarisches Layout (Neu in Access 2007) 47 Tabelle 55 als Datensatzquelle 151 ändern (SQL) 499 anlegen per Entwurfsansicht 57 aufteilen und wieder verknüpfen 121 Berichtslayout 364 Eigenschaften festlegen 72 erstellen (SQL) 490
1020
Formularlayout 202, 305 importieren 965 löschen 965 löschen (SQL) 500 Performance 723 verknüpfen 966 wiedereinbinden 970 Tabellennamen 87 für Detailtabellen in 1:1-Beziehungen 88 für Lookup-Tabellen 88 für Temporäre Tabellen 89 für Verknüpfungstabellen 88 Tabellenvorlagen 67 Table (ADO) 571 Table (DAO) 524 TABLE (SQL) 500 TableDefs (DAO) 506, 526 Tables(ADO) 571 Table Scan (Performance) 729 tabs 632, 638, 682 Tabs (Neu in Access 2007) 35 tabSet (Ribbon) 682 tag (Ribbon) 684 Tastenkombination 57, 278 Tastenkombinationen (Ribbon) 666 Tastensteuerung (Neu in Access 2007) 35 Teilerleiste des geteilten Formulars (Formulareigenschaft) 198 Temporäre Variable 438 TempVars (Neu in Access 2007) 50 Text 214, 312 Felddatentyp 59 TEXT (SQL) 491 Textabstand 306 Steuerelementeigenschaft 202 Textfeld 289 Ereignisse 214 Ribbon 650 TextFormat 290 Textformat 290 Feldeigenschaft 61 Timer 770 title (Ribbon) 662, 684 To 444
Index
toggleButton (Ribbon) 658, 682 Toolwindow beim Start anzeigen 915 benutzerdefiniert 903 füllen 917 Toolwindow anlegen (VBE) 904 Toolwindows (VBE) 902 TOP 178 TOP (SQL) 482 TopPadding 202, 306 Transaktion 263 abbrechen 270 ADO 593 DAO 562 durchführen 270 durchführen (DAO) 564 starten (DAO) 563 verwerfen (DAO) 564 TreeView 306 1:n-Beziehung 316 anlegen 307 bei Bedarf füllen 321 Daten aus verknüpften Tabellen 316 Drag and Drop 324 Eigenschaften 311 Elementeigenschaften 314 Element hinzufügen 313 Erzeugen 311 leeren 313 mit Tabellendaten füllen 315 mit vielen Daten füllen 320 Neuzeichnen verhindern 323 reflexive Daten 318 Stil einstellen 313 Symbole 315 Trennlinien Formulareigenschaft 227 Trennstrich (Ribbon) 665 Tunnel 954 tvwChild 311 tvwFirst 311 tvwLast 311 tvwNext 311 tvwPictureText 313
tvwPlusMinusText 314 tvwPlusPictureText 314 tvwPrevious 311 tvwTextOnly 314 tvwTreelinesPictureText 314 tvwTreelinesPlusMinusPictureText 314 tvwTreelinesPlusMinusText 314 tvwTreelinesText 314 Type 440 Type (ADO) 572 Type (VBE) 878
U Übertrag 413 UBound 439 UBound (ADO) 584 Umschaltfläche 303 Ribbon 658 Undo im Formular 219, 262 Ungebundene Formulare 189 Ungebundene Recordsets verwenden (ADO) 595 ungleich (SQL) 468 UNION 296 UNION (SQL) 484 UNION-Abfragen 165 in Kombinationsfeldern 165 mit eindeutigem Schlüssel 167 mit INSERT INTO 168 Unique (ADO) 574 UNIQUE (SQL) 494 Unterabfrage Extremwerte ermitteln 178 SQL 480 Unterbericht 404 einbinden 404 mehrere Seiten 407 Unterdatenblätter 75 Unterdatenblätter (Performance) 757 Unterformular 302 Datenblattansicht 224 Besonderheiten 260
V Validieren abhängige Felder 277 beim Eingeben 275 feldbasiert 74 Formular 275 Sonderfälle 279 vor dem Speichern 276 Value 214 DAO 533, 541 VALUES (SQL) 488 Var (SQL) 471 Variable 437 alle verwenden 441 Deklaration erzwingen 694 global 441 Namen 437 Performance 744, 748 Varianz (SQL) 471 VarP (SQL) 471 VBA 425 Arrays 439 Aufzählungstypen 436 Benutzerdefinierte Typen 440 Case 444 Case Else 444 Case Is 444 Const 435 Dim 439 Do…Loop 447
1022
Do Until 447 Do While...Loop 447 Enum 436 Exit 445, 448 Exit For 445 For...Next 445 For Each 446, 448 Function 451 Globale Variable 441 GoTo 449 If...Then 441 If...Then…Else 441 Kommentare 433 Konstanten 434 Kontrollstrukturen 441 Laufvariable 438 LBound 439 Loop Until 448 Loop While 447 MsgBox 434 Namenskonvention 426 On Error GoTo 449 Option Base 1 439 Option Explicit 437 Parameter 451 Performance 744 Performance in Formularen 741 Preserve 439 Prozedur 451 Public 436 Public Function 451 ReDim 439 Routinenarten 450 Rückgabewerte 453 Schleifen 445 Select Case 444 Sprungmarken 449 Step 445 String 439 Sub 451 To 444 Type 440 UBound 439 Variable 437 Variablen 437
Zusammengesetzter Primärschlüssel (SQL) 498 Zusammenhalten 393 Zweite Normalform 98 Daten überführen in 99 Zwischensumme 413
1025
Und? Schon genug über Access gelesen?
Nein? Dann ist das hier was für Sie!
Access
im Unternehmen Das Magazin für Access-Entwickler • Alle zwei Monate 72 Seiten Know-how rund um die Datenbankentwicklung mit Microsoft Access • Beiträge von Access- und SQL-Experten wie Karl Donaubauer, Thomas Möller, Uwe Ricken, Bernd Jungbluth, Sascha Trowitzsch, Ruprecht Dröge und André Minhorst • Zugriff auf das größte deutschsprachige Access-Archiv mit mehr als 250 Artikeln, 80 Musterlösungen und zahlreichen Beispieldatenbanken Auf der CD zu diesem Buch finden Sie im Verzeichnis \Buchdaten\Bonus einige Artikel zum Probelesen plus Beispieldatenbanken. Informationen zum Magazin und zum Jahresabo finden Sie hier: www.access-im-unternehmen.de