SCHWERPUNKT

Berichte per VBA bändigen

15.03.2006
Berichte sind in vielerlei Hinsicht Formularen ähnlich, doch durch die fehlende Interaktion ist ihr Funktionsumfang auch geringer. Man sagt, die Steuerung per VBA biete daher bei Berichten auch nicht so viele Möglichkeiten. Das ist prinzipiell richtig, doch gibt es durchaus einige VBA-Eigenschaften, die in Formularen keinen oder nur wenig Sinn machen würden. Der vorliegende Beitrag zeigt, welche Möglichkeiten die Steuerung von Berichten mit VBA offeriert.

In der Beitragsreihe „Ereignisreiche Berichte“ (ab Ausgabe 12/2005) haben Sie bereits erfahren, welche Ereignisse Berichte bereitstellen und für welche Zwecke sie sich einsetzen lassen. Neben diesen Ereignissen liefern Berichte einige Eigenschaften und Methoden, ohne die so manche Aufgabe nicht zu lösen wäre. Im vorliegenden Beitrag erfahren Sie, worum es sich dabei dreht und wie Sie die Eigenschaften und Methoden verwenden.

Steuerelemente und Berichtsbereiche referenzieren

Wenn Sie den Wert eines Steuerelements lesen oder schreiben möchten, greifen Sie wie in Formularen darauf zu: entweder über die entsprechende Auflistung, inklusive Objektname, oder, wenn die Referenz aus dem gleichen Objekt heraus erfolgt, mit dem Schlüsselwort Me.
Für die Variante mit dem Objektnamen gibt es mehrere Möglichkeiten, unter anderem die folgenden:
Reports.Item("rptBeispiel").Controls _("ctlBeispiel")
Reports!rptBeispiel!ctlBeispiel

Mit dem Schlüsselwort Me sieht der Zugriff etwa folgendermaßen aus:
Me!ctlBeispiel

Berichtsbereiche referenzieren

Auch die einzelnen Berichtsbereiche kann man referenzieren. Das macht Sinn, wenn man beispielsweise einen Bereich ein- oder ausblenden oder eine Eigenschaft ändern möchte. Der Verweis auf einen Berichtsbereich erfolgt über die Section-Auflistung eines Berichts. Dabei kommen für die Standardbereiche Zahlenwerte oder Konstanten in Frage, während man die Gruppenköpfe und -füße nur mit Zahlenwerten referenzieren kann. Den Detailbereich machen Sie etwa mit folgenden Anweisungen unsichtbar:
Me.Section(0).Visible = False
Me.Section(acDetail).Visible = False

Für die übrigen Bereiche verwenden Sie die folgenden Konstanten:

Die Zahlenwerte für die einzelnen Gruppierungsbereiche sehen folgendermaßen aus: Der Kopfbereich der ersten Gruppierungsebene erhält den Zahlenwert 5, der entsprechende Fußbereich den Zahlenwert 6. Die Kopf- und Fußbereiche der übrigen Gruppierungsebenen werden entsprechend durchnummeriert.

Neben der Konstanten und dem Zahlenwert gibt es eine weitere Möglichkeit, einen Berichtsbereich zu referenzieren. Dabei handelt es sich um den Wert der Eigenschaft Name des jeweiligen Bereichs. Den Namen können Sie nach Ihren eigenen Wünschen anpassen, die standardmäßig vergebenen Namen reichen allerdings meist aus (Bild 1).

Bild 1: Der Name der einzelnen Bereiche eines Berichts kann angepasst werden.

Über den Namen können Sie auf einen Bereich so zugreifen, als ob es sich dabei um ein Steuerelement des Berichts handelt. Die Bereichsnamen werden sogar von IntelliSense unterstützt (Bild 2). Die Routine aus Listing 1 zeigt, wie Sie die Namen und Höhen aller vorhandenen Berichtsbereiche im Direktfenster ausgeben können.

Bild 2: Referenzieren eines Berichtsbereichs über den Bereichsnamen.

Auch die Gruppierungsebenen eines Berichts bieten eine weitere Möglichkeit des Zugriffs, nämlich über die GroupLevel-Auflistung. Die folgende Anweisung gibt etwa den Namen des Feldes aus, nach dem die erste Gruppierungsebene gruppiert wird:

Debug.Print Me.GroupLevel(0).ControlSource

Doch Vorsicht: Die Gruppierungsebenen sind keinesfalls mit den Berichtsbereichen identisch und haben völlig andere Eigenschaften und Methoden.

Private Sub Report_Open(Cancel As Integer)
Dim i As Integer
On Error Resume Next
For i = 0 To 24
Debug.Print Me.Section(i).Name, Me.Section(i).Height
Next i
End Sub

Gruppierungen im Griff

Schauen Sie sich zunächst die Eigenschaften und Methoden der Gruppierungsebenen an. Die Eigenschaften sind weitgehend mit denen aus dem Dialog Sortieren und gruppieren identisch.

Unter VBA stehen die folgenden Eigenschaften zur Verfügung, wobei einige im Anschluss ausführlicher besprochen werden müssen:

Die Eigenschaften GroupOn und GroupInterval hängen vom Wert der Eigenschaft ControlSource beziehungsweise vom Datentyp des dort angegebenen Feldes ab.

Gruppierung nach einem Textfeld

Wenn Sie etwa nach einem Textfeld gruppieren, kann die Eigenschaft GroupOn die Werte 0 (gruppiert nach dem kompletten Feldwert) oder 1 (gruppiert nach den ersten x Zeichen) annehmen, wobei x dem Wert der Eigenschaft Group-Interval entspricht. Wenn Sie also die Artikel-Tabelle der Nordwind-Datenbank so gruppieren möchten, dass alle Artikel mit dem gleichen Anfangsbuchstaben gruppiert und der Anfangsbuchstabe im Gruppenkopf angezeigt werden soll, realisieren Sie das unter VBA mit der Ereignisprozedur aus Listing 2.

Private Sub Report_Open(Cancel As Integer)
'Erste Gruppierungsebene referenzieren
With Me.GroupLevel(0)
'Zu gruppierendes Feld festlegen
.ControlSource = "Artikelname"
'Gruppieren nach einer bestimmten Anzahl von Zeichen...
.GroupOn = 1
'...die hier festgelegt wird:
.GroupInterval = 1
'Gruppenkopf und Detailbereich sollen zusammengehalten werden.
.KeepTogether = 2
End With
End Sub

Bild 3: Entwurf eines Berichts zur Ausgabe eines Artikel-Katalogs.
Bild 4: Bericht mit alphabetischer Artikelliste.

Der zugrunde liegende Bericht sieht im Entwurf wie in Bild 3 aus. Das Ergebnis können Sie in Bild 4 betrachten.

Weitere Datentypen

Für die anderen Datentypen sieht das im Prinzip ganz ähnlich aus. Enthält das unter Control-Source angegebene Feld einen Datums-/Zeitdatentyp, sorgt der Wert 0 für die Eigenschaft GroupOn ebenfalls dafür, dass die Gruppierung nach dem ganzen Wert erfolgt. Sie können aber auch nach den im Datum enthaltenen Bestandteilen sortieren, also etwa nach dem Jahr. Dazu verwenden Sie für die Eigenschaft GroupOn die folgenden Zahlenwerte:

.

Der Clou ist, dass Sie nicht nur nach den genannten Einheiten gruppieren können, sondern auch nach deren Vielfachen. Sie können also auch jeweils 30 Minuten zusammenfassen. Dazu legen Sie einfach das gewünschte Vielfache mit der Eigenschaft GroupInterval fest.

Im folgenden Beispiel sorgen zwei Gruppierungen und eine Sortierung dafür, dass die Bestellungen der Nordwind-Datenbank nach Datum sortiert und nach Jahren und Monaten gruppiert werden – wobei jeweils zwei Monate zusammengefasst werden sollen.

Den Aufbau des Berichts finden Sie in Bild 5. Damit die Ausgabe wie in Bild 6 aussieht, stellen Sie die Eigenschaften der einzelnen Gruppierungsebenen mit der Prozedur aus Listing 3 ein.

Bild 5: Bericht zur Anzeige von Bestellungen in Gruppierungen von je zwei Monaten.
Bild 6: Gruppierung von Bestellungen nach Jahren und Monaten in Intervallen von je zwei Monaten.

Private Sub Report_Open(Cancel As Integer)
'Äußere Gruppierung: Jahre
With Me.GroupLevel(0)
'Gruppierung nach Bestelldatum
.ControlSource = "Bestelldatum"
'Gruppierungseinheit: Jahre
.GroupOn = 2
'Gruppierungsintervall: 1
.GroupInterval = 1
'Zusammenhalten: nein
.KeepTogether = 0
End With
'Innere Gruppierung: Monate
With Me.GroupLevel(1)
'Gruppierung wiederum nach dem Bestelldatum...
.ControlSource = "Bestelldatum"
'... hier allerdings nach Monaten...
.GroupOn = 4
'... und zwar in Zweier-Intervallen.
.GroupInterval = 2
'Zusammenhalten: mit ersten Detaildatensatz
.KeepTogether = 2
End With
End Sub

Wichtig ist in diesem Zusammenhang, dass die Gruppierungen in der Reihenfolge der Abarbeitung von 0 bis n durchnummeriert werden. Etwas Fummelarbeit ist die Ausgabe der betroffenen Monate, also etwa „Juli/August“. Der dazu verwendete Ausdruck für die Eigenschaft Steuerelementinhalt des entsprechenden Textfeldes lautet folgendermaßen:

=Format("1." & Wenn(Monat([Bestelldatum]) Mod 2=0;Monat([Bestelldatum])-1;Monat([Bestelldatum])) & "." & Jahr([Bestelldatum]);"mmmm") & "/" & Format("1." & Wenn(Monat([Bestelldatum]) Mod 2=0;Monat([Bestelldatum]); Monat([Bestelldatum]+1)) & ." & Jahr([Bestelldatum]);" mmmm")

Der Ausdruck verwendet das Bestelldatum des ersten Datensatzes der Gruppierung und prüft, ob die Monatszahl gerade oder ungerade ist. Als erster der zwei Monate soll immer ein ungerader Monat genannt werden, damit die Monate Januar/Februar, März/April und so weiter zusammen ausgegeben werden. Für die Gruppierung erledigt Access dies automatisch, aber für die Überschrift im Gruppenkopf ist das nicht selbstverständlich.

Weitere Bereichseigenschaften

Natürlich gibt es noch eine Menge weiterer Eigenschaften, mit denen sich Bereiche und damit auch Gruppierungen manipulieren lassen. Diese stehen jedoch nicht zur Laufzeit zur Verfügung, sondern nur beim Entwurf und sollen deshalb hier nicht im Detail besprochen werden.

Bereiche in Aktion

Im Gegensatz zu den zuvor genannten Eigenschaften enthalten die einzelnen Bereiche auch Eigenschaften, die zur Entwurfszeit gar nicht verfügbar sind – das heißt, dass diese auch nicht über das Eigenschaftsfenster der Berichtsbereiche geändert werden können.
Wenn Sie während der Ausgabe eines Berichts auf das Layout des Berichts Einfluss nehmen möchten, um beispielsweise mehrere Datensätze pro Zeile zu drucken, sind die Eigenschaften MoveLayout, NextRecord und PrintSection für Sie interessant.

Zeilenvorschub unterbinden

Im ersten Beispiel kommt die Eigenschaft Move-Layout zum Einsatz. Access-Berichte werden bereichsweise von oben nach unten abgearbeitet. Dabei werden neue Bereiche in der Regel unter dem zuvor ausgegebenen Bereich gedruckt. Mit der Eigenschaft MoveLayout können Sie dies in manchen Fällen gezielt beeinflussen: Wenn Sie diese etwa im Ereignis Beim Drucken des Detailbereichs auf False setzen, erfolgt kein Vorschub auf die nächste Druckposition, sondern der aktuelle Bereich wird auf der gleichen Höhe wie der vorherige Bereich ausgegeben.

Zur Veranschaulichung soll das folgende Beispiel einen Lottozettel mit den Zahlen von 1 bis 49 erzeugen. Weniger Geübte legen vielleicht nun 49 Textfelder oder Bezeichnungsfelder auf dem Bericht an und füllen diese mit den entsprechenden Zahlen. Wer mit VBA vertraut ist und die Eigenschaft MoveLayout kennt, legt nur ein Textfeld an und verwendet eine Tabelle mit den Zahlen 1 bis 49 als Grundlage. Der Bericht sieht dann im Entwurf wie in Bild 7 aus.

Bild 7: Entwurf des Lottoschein-Berichts.

Damit der Bericht eine Matrix von sieben mal sieben Zahlen anzeigt, sorgen Sie einfach dafür, dass der Bericht nur alle sieben Datensätze die Zeile wechselt. Ohne weiteres Zutun würden nun jeweils sieben Zahlen an der gleichen Stelle ausgegeben, was die Lesbarkeit natürlich etwas einschränkt. Also sorgen Sie noch dafür, dass das Textfeld jeweils um eine bestimmte Anzahl Pixel nach rechts verschoben wird.

Beides erledigt die Routine aus Listing 4, die beim Formatieren des Detailbereichs ausgelöst wird. Das Ergebnis ist in Bild 8 zu sehen.

Bild 8: Dieser Lottozettel besteht im Entwurf lediglich aus einem einzigen Textfeld.

Private Sub Detailbereich_Format(Cancel As Integer, FormatCount As Integer)
'Abstand des Textfeldes vom linken Rand festlegen
Me.Controls("txtZahl").Left = ((Me.txtZahl + 6) Mod 7) * 600
'Wenn die Zahl kein Mehrfaches von 7 ist, keine neue Zeile beginnen
If Me.txtZahl Mod 7 = 0 Then
Me.MoveLayout = True
Else
Me.MoveLayout = False
End If
End Sub

Datensatz nicht drucken

Die Eigenschaft PrintSection legt fest, ob der aktuelle Datensatz gedruckt wird oder nicht. Das kann man zwar auch durch entsprechende Einschränkung der Datenherkunft erreichen, aber im Gegensatz dazu bewirkt die Einstellung Print-Section = False, dass ein dem Bereich entsprechender Zeilenvorschub erfolgt, aber keine Inhalte ausgegeben werden.

Nicht zum nächsten Datensatz springen

Möchten Sie einen Datensatz nicht nur einmal, sondern mehrmals drucken, setzen Sie die Eigenschaft NextRecord ein. Die Einstellung Next- Record = False sorgt dafür, dass der aktuelle Datensatz erneut gedruckt wird.

Das folgende Beispiel zeigt, wie Sie die beiden Eigenschaften PrintSection und NextRecord kombinieren können, um nach jedem fünften Datensatz einen Abstand einzufügen. Dazu fügen Sie zu einem Bericht mit einer Auflistung von Artikeln einfach die Routine aus Listing 5 hinzu, die beim Formatieren des Detailbereichs ausgelöst wird. Bild 9 zeigt die Auswirkungen auf den Bericht.

Dim intAnzahl As Integer
Private Sub Detailbereich_Format(Cancel As Integer, FormatCount As Integer)
'Anzahl der gedruckten Detailbereiche erhöhen
intAnzahl = intAnzahl + 1
'Nach jedem fünften Datensatz einen leeren Bereich einfügen
If intAnzahl Mod 6 = 0 Then
Me.NextRecord = False
Me.PrintSection = False
Else
Me.NextRecord = True
Me.PrintSection = True
End If
End Sub

Bild 9: Bericht mit Zwischenräumen zum Verbessern der Übersicht.

Kombinationen der drei Eigenschaften Move-Layout, PrintSection und NextRecord erzielen die unterschiedlichste Wirkung. Das bekannteste Beispiel dürfte der Einsatz beim Drucken von Etiketten sein – hier geht es vor allem darum, eventuell bereits entnommene Etiketten auf dem zu bedruckenden Etikettenbogen auszulassen und je Etikett beliebig viele Kopien drucken zu können. Da dieses Beispiel jedoch im Internet in allen Varianten zu finden ist, geht dieser Beitrag nicht näher darauf ein. Interessanter sind da die unterschiedlichen Kombinationen der drei Eigenschaften: