 | Unser Angebot für Sie! Lesen Sie diesen Beitrag und 500 andere sofort im Onlinearchiv, und erhalten Sie alle zwei Monate brandheißes Access-Know-how auf 72 gedruckten Seiten! Plus attraktive Präsente, zum Beispiel das bald erscheinende Buch 'Access 2010 - Das Grundlagenbuch für Entwickler'! |
| | | | | |
Zusammenfassung
Erfahren Sie, wie Sie Termine wie in der Outlook-Tagesübersicht in Berichtsform ausgeben.
Techniken
Berichte, VBA
Voraussetzungen
Access 2000 oder höher
Beispieldateien
Termine.mdb
Shortlink
660
Termine in Berichten darstellen
André Minhorst, Duisburg
Was die Darstellung von Terminen und Kalendern angeht, ist Outlook der Platzhirsch. Es gibt kaum eine Ansicht, die sich damit nicht verwirklichen lässt. Oder doch? Nun: Ihre Kunden oder Sie selbst haben bestimmt Ideen, die selbst Outlooks Berichts-Engine an die Leistungsgrenze bringen. Und hier kommt Access ins Spiel: Verwenden Sie Berichte und bringen Sie Access dazu, Ihre Termine ganz nach Wunsch anzuzeigen.
Die Anzeige von Terminen in Access-Berichten ist seit jeher mit Bastelei verbunden - außer Sie begnügen sich damit, die Termine einfach tabellarisch aufzulisten. Dann brauchen Sie wirklich nicht viel mehr als einen handelsüblichen Bericht, in dessen Detailbereich Sie die vorliegenden Termine eintragen. Anspruchsvoller wird es dann schon, wenn der Bericht die Termine und deren Zeitaufwand hübsch grafisch aufbereitet darstellen soll - genau so, wie es auch beispielsweise in der Tagesübersicht von Outlook der Fall ist (s. Abb. 1).
Abb. 1: Anzeige der Termine in der Tagesübersicht von Outlook
Auch erfahrenen Berichtsdesignern dürfte der Nachbau eines solchen Layouts leichte Sorgenfalten auf die Stirn legen, ist man doch normalerweise gewohnt, die Daten schön untereinander anzuordnen und eventuelle Feinheiten durch Gruppierungen oder Unterberichte abzudecken. Wer schon in unseren Beitrag Berichte manuell füllen (Shortlink 659) hineingeschaut hat, ahnt bereits, dass es hier nicht mit rechten Dingen - äh, Steuerelementen - zuging, sondern dass wir hier von Berichtsmethoden wie Print und Line Gebrauch gemacht haben.
Das allein reicht allerdings längst nicht aus, um Termine so wie unter Outlook darzustellen (genau genommen werden wir nicht jedes Detail nachbilden, doch die wichtigsten Elemente berücksichtigen wir natürlich).
Beginnen wir jedoch bei den Daten, die unserem Bericht zugrunde liegen - und die sind bei Weitem das am wenigsten Komplizierte dieser Lösung.
Die Tabelle tblTermine sieht wie in Abb. 2 aus und enthält die folgenden Felder:
Abb. 2: Die Tabelle tblTermine in der Entwurfsansicht
- ID: Primärschlüsselfeld
- Termin: Beschreibung des Termins
- Termindatum: Datum, an dem der Termin stattfindet
- Startzeit: Zeit, zu welcher der Termin beginnt
- Endzeit: Zeit, zu welcher der Termin endet
- FarbeID: Fremdschlüsselfeld zur Tabelle tblFarben; legt die Hintergrundfarbe für den Termin im Bericht fest
- Bereich: Für das Layout des Kalenderberichts ist es wichtig zu wissen, ob Termine ein- oder mehrspaltig angezeigt werden. Termine, die in einem mehrspaltigen Bereich liegen, sind durch den gleichen Wert in diesem Feld zu erkennen.
- Spalte: Gibt an, in welcher Spalte sich dieser Termin befindet.
- Spaltenzahl: Gibt an, wieviele Spalten der Bereich umfasst, in dem sich dieser Termin befindet.
Die Funktion der letzten drei Felder wird weiter hinten erläutert, außerdem erfahren Sie dort, wie diese Felder gefüllt werden. Aktuell reicht es, wenn Sie wissen, dass die in diesem Beitrag vorgestellte Lösung Ihnen diese Aufgabe abnimmt.
Den Aufbau der Tabelle tblFarben entnehmen Sie Abb. 3. Sie enthält neben dem Primärschlüsselfeld ID ein Feld zur Bezeichnung der Farbe (Farbe) sowie ein Feld mit dem Zahlenwert, der die Farbe repräsentiert (Farbwert).
Abb. 3: Diese Tabelle speichert die Hintergrundfarben für die Termine im Bericht.
Das war schon fast alles, was Sie zum Modellieren eines Berichts wie in Abb. 5 benötigen - es fehlen nur noch ein paar Hilfstabellen, Abfragen und einige Zeilen VBA-Code, um den Bericht zu füllen.
Abb. 5: Beispiel für einen Terminbericht mit überlappenden Terminen
Termineingabe
Auf die Beschreibung von Formularen zur Eingabe der Termine verzichten wir in diesem Beitrag aus Platzgründen. Sie können die Termine allerdings in ein handelsübliches Formular eingeben oder diese auch aus Outlook importieren. Wie das geht, erfahren Sie beispielsweise im Beitrag Outlook-Termine im Griff (Shortlink 439).
Grundlagen des Kalenderberichts
Glücklicherweise gibt es auch Tage ohne Termine, und daher enthält ein Kalender auch nicht für jeden Tag Aufzeichnungen oder Einträge. Der Terminbericht sollte aber trotzdem für jeden Tag eine eigene Seite anzeigen - allein, damit man nicht immer auf das Datum schauen muss, wenn man den Kalender am Bildschirm oder auch in ausgedruckter Form durchblättert. Nach Montag soll Dienstag folgen und nicht schon der Mittwoch, wenn es am Dienstag keine Termine gibt.
Wie aber bringen wir Access dazu, ganz banal einen 365-seitigen Bericht als Grundlage für den Terminkalender eines Jahres zu liefern - ohne dass für jeden Tag ein Termin angelegt wäre?
Nun, das geht ganz einfach: Wir brauchen einfach nur eine Datenherkunft, die tatsächlich alle 365 Tage liefert. Die Tabelle tblKalenderdaten ist eine solche Datenherkunft: Sie enthält für jeden Tag des Jahres 2009 einen eigenen Datensatz mit einem Primärschlüssel und einem Datumsfeld (s. Abb. 4). Wenn Sie möchten, so können Sie diese Tabelle durch weitere Spalten wie Werktag oder Feiertag ergänzen und solche Tage später im Bericht anders formatieren.
Abb. 4: Diese Tabelle liefert die Grundlage für einen ganzjährigen Kalenderbericht.
Für Letzteres sollten Sie unbedingt einen eindeutigen Schlüssel festlegen, damit die Tabelle kein Datum doppelt enthalten kann. Damit das Füllen der Tabelle nicht soviel Arbeit macht, übernimmt die Routine aus Listing 1 diese Aufgabe. Sie erwartet das Start- und das Enddatum als Parameter und legt Datensätze für den angegebenen Zeitraum an.
Public Sub KalenderdatenFuellen(datStart As Date, datEnde As Date)
Dim dat As Date
Dim db As DAO.Database
Dim rst As DAO.Recordset
Set db = CurrentDb
db.Execute "DELETE FROM tblKalenderdaten", dbFailOnError
Set rst = db.OpenRecordset("tblKalenderdaten", dbOpenDynaset)
For dat = CDate(datStart) To CDate(datEnde)
rst.AddNew
rst!Kalenderdatum = dat
rst.Update
Next dat
End Sub
Bericht anlegen
In den folgenden Absätzen erfahren Sie, wie Sie den Bericht aus Abb. 5 anlegen und diesen mit Inhalt füllen. Damit Sie den Bericht Ihren Bedürfnissen anpassen können, bauen wir ihn Stück für Stück auf.
Den Start macht ein leerer Bericht, dem Sie übergangsweise die Tabelle tblKalenderdaten als Datenherkunft zuweisen.
Eine Seite pro Tag
Der Kalender soll jeden Tag auf einer eigenen Seite anzeigen. Daher reicht es nicht aus, einfach nur das Feld Kalenderdatum aus der Datenherkunft in den Detailbereich des Berichts zu ziehen. Sie müssen auch noch dafür sorgen, dass nach (oder vor) jedem Datensatz ein Seitenwechsel erfolgt. Theoretisch würde es reichen, den Detailbereich einfach groß genug zu gestalten.
Allerdings soll der Detailbereich möglichst nur den Kalender selbst enthalten, daher benötigen wir einen anderen Berichtsbereich als Unterkunft für das Feld Kalenderdatum (das ja im Bericht auch das Datum anzeigen soll).
Der Seitenbereich wäre eine gute Wahl, aber auch diesen sparen wir für eventuelle Elemente wie eine Überschrift auf. Die Kalenderdaten sollen in der richtigen Reihenfolge angezeigt werden, was Sie durch Anlegen einer Sortierung für das Feld Kalenderdatum im Bericht erreichen.
Und wenn Sie schon einmal dabei sind, können Sie auch gleich eine Gruppierung daraus machen und das Feld Kalenderdatum im Gruppenkopf unterbringen.
Dem Gruppenkopfbereich weisen Sie dann noch über das Setzen des Werts Vor Bereich für die Eigenschaft Neue Seite einen Seitenumbruch hinzu und stellen so sicher, dass jedes Datum auf einer neuen Seite angezeigt wird (s. Abb. 6).
Abb. 6: Dieser Bericht zeigt jeden Tag auf einer neuen Seite an.
In der Seitenansicht sieht der Bericht nun wie in Abb. 7 aus und liefert die erwarteten Datumsangaben auf je einer eigenen Seite.
Abb. 7: Die Grundlage für den Terminkalender mit Tagesübersicht
Stundenplan hinzufügen
Im nächsten Schritt fügen wir das Raster hinzu, das den Tag beziehungsweise den Detailbereich in 24 Elemente aufteilt und diese mit den entsprechenden Stundenangaben versieht (s. Abb. 8).
Abb. 8: Der Terminkalender besitzt nun ein Raster, in das man nur noch die Termine einfügen muss.
Dies geschieht in einer Routine, die im Ereignis Beim Drucken des Detailbereichs des Berichts ausgelöst wird:
Private Sub Detailbereich_Print(Cancel As Integer, PrintCount As Integer)
UhrzeitrasterAnlegen
End Sub
Die Routine UhrzeitrasterAnlegen (Listing 2) deklariert zunächst vier Variablen namens sngRepX1, sngRepX2, sngRepY1 und sngRepY2. Darin speichert sie die Koordinaten des Detailbereichs relativ zur linken oberen Ecke, die sie aus den Eigenschaften ScaleTop, ScaleWidth, ScaleLeft und ScaleHeight erhält.
Private Sub UhrzeitrasterAnlegen
'Koordinaten für den Bericht
Dim sngRepX1 As Single
Dim sngRepX2 As Single
Dim sngRepY1 As Single
Dim sngRepY2 As Single
Dim i As Integer
Dim lngColor As Long
With Me
sngRepY1 = .ScaleTop
sngRepX1 = .ScaleLeft
sngRepX2 = .ScaleWidth
sngRepY2 = .ScaleHeight
'Raster mit Zeiten bauen
For i = 1 To 25
lngColor = RGB(225, 225, 225)
Me.Line (sngRepX1, _
sngRepY1 + (i - 1) * (sngRepY2 - sngRepY1) / 24) _
-(sngRepX2, _
sngRepY1 + (i - 1) * (sngRepY2 - sngRepY1) / 24), _
lngColor
CurrentX = 0
If i < 25 Then
lngColor = RGB(0, 0, 0)
Me.ForeColor = lngColor
Me.FontSize = 9
Me.Print Format(DateAdd("h", i - 1, 0), "hh");
Me.FontSize = 5
Me.Print Format(DateAdd("h", i - 1, 0), " nn")
End If
Next i
End With
End Sub
Danach baut die Routine auch schon die Linienstruktur und die darin enthaltenen Stundenzahlen auf, und zwar in einer Schleife, welche die Werte 1 bis 25 durchläuft - immerhin gibt es 24 Stunden, von denen jede oben und unten durch eine Linie begrenzt werden möchte.
Beschriftungen brauchen wir aber ebenfalls nur 24, weshalb der untere Teil der Schleife nur ausgeführt wird, solange die Zählervariable einen Wert kleiner 25 aufweist. Man hätte auch zwei Schleifen aufbauen können - das ist aber sicher Geschmackssache.
Innerhalb der Schleife sorgt die Line-Anweisung zunächst dafür, dass 25 Linien sorgfältig auf die Höhe des Detailbereichs aufgeteilt werden. Die Breite wird dabei durch den ersten und dritten Parameter angegeben, wobei der erste den Wert 0 und der dritte einen Wert aufweist, welcher der Breite des Detailbereichs entspricht.
Die vertikale Position der Linie geben der zweite und der vierte Parameter der Line-Funktion an, welche die gleiche Formel enthalten. Diese liefert den dem Wert von i entsprechenden Anteil der Gesamthöhe des Detailbereichs in der aktuellen Einheit, standardmäßig also in Twips.
Termine vorbereiten
Nun geht es an den Teil des Berichts, in dem eine Menge Gehirnschmalz steckt - zumindest, wenn man dem Benutzer ermöglichen möchte, auch sich überschneidende Termine in den Kalender einzutragen. Man könnte nun sagen, dass dies keinen Sinn macht, da man normalerweise immer nur eine Tätigkeit zur gleichen Zeit betreibt - aber möglicherweise möchte der Benutzer ja auch weitere Ereignisse im Terminkalender unterbringen.
Schauen wir uns also an, was man braucht, um zwei oder mehr Termine im Bericht unterzubringen, die sich zumindest teilweise überschneiden. Die erste wichtige Erkenntnis ist dabei, dass die Termine nur dort auf mehrere Spalten aufgeteilt werden, wo auch mehrere sich überschneidende Termine angezeigt werden sollen. Dabei kann es sich um zwei Termine handeln, die sich ganz oder teilweise überlappen, vielleicht sind es aber auch drei oder mehr. Dabei gibt es dann auch wieder Varianten: Wenn der erste, zweite und dritte Termin einen gemeinsamen Zeitpunkt haben, dann brauchen Sie auch drei Spalten. Wenn sich hingegen der erste und der zweite Termin überschneiden, der dritte Termin aber erst nach dem ersten Termin beginnt, reichen zwei Zeilen aus - der dritte Termin kann dann ja in der ersten Zeile hinter dem ersten Termin eingefügt werden.
Danach folgen dann möglicherweise weitere Termine, die sich mit keinem anderen Termin überschneiden und die deshalb wieder über die ganze Breite des Berichts gestreckt werden sollen.
Das ist der erste Schritt zur Lösung: Wir müssen herausfinden, welche Bereiche es für den aktuellen Tag gibt und wie viele Spalten jeder Bereich aufweist. Einen Bereich mit x Spalten und einen Bereich mit y Spalten trennt ein, wenn auch noch so kleiner, Bereich ohne Termin. Und zusätzlich müssen wir ermitteln, wie viele Spalten der jeweilige Bereich benötigt.
Bei diesem Schritt hilft die Abfrage qryZeitSpaltenanzahl mit der Entwurfsansicht aus Abb. 9. Sie führt das Ergebnis der Abfragen qryZeit und qryTermine zusammen.
Abb. 9: Die Abfrage qryZeitSpaltenanzahl in der Entwurfsansicht
Die Abfrage qryZeit (s. Abb. 10) kombiniert den Inhalt der beiden Tabellen tblStunden und tblMinuten so, dass das Ergebnisfeld der Abfrage alle Minuten von 0:00 bis 23:59 inklusive Stundenangabe liefert. Die Abfrage qryTermine entspricht weitgehend der Tabelle tblTermine, enthält aber ein zusätzliches Feld namens Endzeit1 mit folgendem Inhalt:
Abb. 10: Die Abfrage qryZeit liefert alle Zeiten von 0:00 bis 23:59 im Minutenabstand.
Endzeit1: Wenn([Endzeit]>DatAdd("n";60;[Startzeit]);[Endzeit];DatAdd("n";60;[Startzeit]))
Dieser Ausdruck liefert den gleichen Wert wie das Feld Endzeit, wenn der Termin 60 Minuten oder länger dauert, sonst erhält Endzeit1 einen Wert, welcher der Startzeit plus 60 Minuten entspricht. Somit ergibt sich aus Startzeit und Endzeit1 immer ein Termin, der mindestens eine Stunde dauert - warum, erfahren Sie später.
Welches Ergebnis aber liefert die Abfrage qryZeitSpaltenanzahl? Betrachten wir zunächst die ersten beiden Felder. Diese liefern alle Kombinationen aus den Minuten eines Tages (von 0:00 bis 23:59) und den Terminen.
Wenn nun für den ersten Januar drei Termine vorliegen, wird jede Minute aus qryZeit für jeden Termin einmal mit den Datensätzen der Tabelle qryTermine verknüpft. Nun kommt noch eine Gruppierung hinzu, welche die beiden Felder Zeit aus qryZeit und Termindatum aus qryTermine betrifft. Jede Kombination aus Zeit und Termindatum wird somit nur noch einmal angezeigt. Fehlt noch das dritte Feld mit der Bezeichnung InBereich. Es liefert die Summe eines Ausdrucks, der aus zwei mit Und verknüpften boolschen Ausdrücken besteht:
Summe([Startzeit]<[Zeit] Und [Endzeit1]>[Zeit])
Der Ausdruck liefert den Wert True (also -1), wenn für den aktuellen Datensatz die Startzeit eines Termins vor dem Wert des Feldes Zeit liegt und der Inhalt von Endzeit1 hinter Zeit, und er liefert False (also 0), wenn eine der beiden Bedingungen nicht zutrifft.
Wenn es nun beispielsweise einen Termin gibt, dessen Start vor einem Zeitpunkt, sagen wir 10:00 Uhr, und dessen Ende nach diesem Zeitpunkt liegt, dann liefert der obige Ausdruck den Wert -1.
Vielleicht gibt es auch zwei oder mehr solcher Termine, dann liefert das Feld InBereich die Summe der True-Werte, also beispielsweise -1+(-1)+(-1)=-3. Das Vorzeichen ist uns derzeit egal, wichtig ist nur, dass der Ausdruck für jede Minute eines Tages die Anzahl der Termine liefert, die für diese Minute festgelegt sind.
Wenn Sie sich das Ergebnis ansehen, werden Sie beispielsweise Werte wie in Abb. 11 vorfinden. Dort gibt es bis 0:50 genau einen Termin und ab 0:51 sind es zwei. Ob der erste Termin vorher endete und dann zwei neue beginnen oder ob zum ersten Termin einer hinzukommt, ist egal - wichtig ist nur, dass Sie wissen, wie viele Termine in jeder Minute laufen.
Abb. 11: Um 0:51 kommt zum ersten Termin ein zweiter hinzu.
Wie können wir nun daraus ableiten, über wie viele Spalten sich die Termine erstrecken beziehungsweise welchen Zeitraum dieser Bereich einnimmt? Und wie will man eigentlich zwei Bereiche mit unterschiedlicher Spaltenzahl voneinander trennen, wenn die Termine etwa eines einspaltigen und eines mehrspaltigen Bereichs unmittelbar aufeinanderfolgen? Hier hilft ein Kunstgriff: Der Ausdruck, der ermittelt, ob ein Termin zu einer bestimmten Minute stattfindet oder nicht, lässt die letzte Minute des Terminzeitraums aus.
Auch wenn ein Termin etwa bis zu einer vollen Stunde wie 12:00 Uhr dauert, wird er nur bis 11:59 Uhr berücksichtigt ([Endzeit]>[Zeit]). Wenn der nächste Termin nun um 12:00 Uhr beginnt und kein Termin vor 12:00 Uhr beginnt und nach 12:00 Uhr endet, wird durch den Wert 0 für das Feld InBereich der Abfrage qryZeitSpaltenanzahl eine Zone gekennzeichnet, die keinen Termin enthält und somit den Wechsel zwischen zwei Bereichen markieren kann.
Das ist zum Beispiel in Abb. 12 zum Zeitpunkt 12:00 Uhr der Fall.
Abb. 12: Um 13:00 Uhr gehen zwei Termine zu Ende und ein weiterer beginnt - Zeit, die Anzahl der Spalten zu prüfen.
Nachdem Sie nun wissen, wie wir die Anzahl der Termine pro Minute und die Zeitpunkte für das Umstellen der Spaltenanzahl für die Ausgabe der Terminblöcke ermitteln, fehlt noch die Routine, mit der wir diese Informationen auswerten und in die Felder Spalte, Spaltenzahl und Bereich der Tabelle tblTermine eintragen können.
Diese Routine heißt BereicheUndSpaltenanzahlErmitteln (Listing 3) und wird einmal beim Öffnen des Berichts aufgerufen.
Public Sub BereicheUndSpaltenanzahlErmitteln()
Dim db As DAO.Database
Dim rst As DAO.Recordset
Dim intAktuellerBereich As Integer
Dim intBereichVorher As Integer
Dim intBereichSpaltenMax As Integer
Dim datBereichStart As Date
Dim datBereichEnde As Date
Dim datTermindatum As Date
Dim intSpalte As Integer
Dim strSQL As String
Dim i As Integer
Set db = CurrentDb
Set rst = db.OpenRecordset("SELECT * FROM qryZeitSpaltenanzahl", _
dbOpenDynaset)
Do While Not rst.EOF
If intBereichVorher < 0 And rst!InBereich = 0 Then
datBereichEnde = rst!Zeit
intAktuellerBereich = intAktuellerBereich + 1
strSQL = "UPDATE tblTermine " _
& "SET Spaltenzahl = " & -1 * intBereichSpaltenMax _
& ", Bereich = " & intAktuellerBereich _
& " WHERE Startzeit >= " & ISODatum(datBereichStart) _
& " AND Endzeit <= " & ISODatum(datBereichEnde) _
& " AND Termindatum = " & ISODatum(rst!Termindatum)
db.Execute strSQL, dbFailOnError
datTermindatum = rst!Termindatum
ElseIf intBereichVorher = 0 And rst!InBereich < 0 Then
datBereichStart = DateAdd("n", -1, rst!Zeit)
intBereichSpaltenMax = 0
End If
If intBereichSpaltenMax > rst!InBereich Then
intBereichSpaltenMax = rst!InBereich
End If
intBereichVorher = rst!InBereich
rst.MoveNext
Loop
For i = 1 To intAktuellerBereich
Set rst = db.OpenRecordset("SELECT * FROM qryTermine " _
& "WHERE Bereich = " & i & " ORDER BY Termindatum ASC, " _
& "Startzeit ASC, Endzeit DESC", dbOpenDynaset)
If Not rst.EOF Then
rst.Edit
rst!Spalte = 1
rst.Update
rst.MoveNext
Do While Not rst.EOF
For intSpalte = 1 To rst!Spaltenzahl
If IsNull(DLookup("ID", "qryTermine", _
& "Spalte = " & intSpalte & " AND Endzeit1 > " _
& ISODatum(rst!Startzeit))) Then
rst.Edit
rst!Spalte = intSpalte
rst.Update
End If
Next intSpalte
rst.MoveNext
Loop
End If
Next i
End Sub
Die Routine durchläuft zunächst alle Datensätze der soeben beschriebenen Abfrage qryZeitSpaltenanzahl. Der Wert einer temporären Variablen namens intAktuellerBereich ist beim Start 0 und wird dann jeweils auf den Wert von intBereich gesetzt - also entweder 0 oder einen von der Anzahl der gleichzeitigen Termine abhängigen negativen Wert.
Dies hat den Hintergrund, dass bei jedem Durchlauf geprüft werden soll, ob entweder die Anzahl der Termine in der vorherigen Minute gleich 0 war und nun höher ist oder ob es vorher einen oder mehrere Termine gab und jetzt keinen mehr.
Solange kein Bereichsübergang eines oder mehrerer Termine gefunden wird, prüft die Routine jeweils, ob es in der aktuellen Minute mehr Termine gibt, als es maximal in den bisherigen Minuten des aktuellen Bereichs gab.
Dies dient dazu, die Anzahl der Spalten für diesen Bereich zu ermitteln. Erst wenn die Anzahl der aktuellen Termine einer Minute 0 ist, erkennt die Routine einen Bereichswechsel und schreibt die maximale Terminanzahl dieses Bereichs in das Feld Spaltenzahl und eine laufende Nummer für den Bereich in das Feld Bereich der Tabelle tblTermine. Dies geschieht für alle Einträge der Abfrage qryZeitSpaltenanzahl.
Nun fehlt noch die Ermittlung der Spalte, in welche der Bericht die einzelnen Termine einfügen soll. Dies geschieht innerhalb einer For...Next-Schleife für jeden der zuvor ermittelten Bereiche. Innerhalb dieser Schleife öffnet die Routine ein Recordset, das alle Termine des aktuellen Bereichs enthält - und zwar in chronologischer Reihenfolge.
Der erste Termin gehört zweifelsohne in die erste Spalte, was auch direkt durch Eintragen der Zahl 1 in das Feld Spalte des entsprechenden Datensatzes manifestiert wird. Die übrigen werden in einer Do While-Schleife durchlaufen.
Innerhalb dieser Schleife durchläuft eine weitere For...Next-Schleife alle Spalten dieses Bereichs und prüft, ob in der aktuellen Spalte schon ein Termin liegt, der sich mit dem aktuellen Termin der Do While-Schleife überschneidet. Falls dies der Fall ist, prüft die Routine die nächste Spalte und so weiter, bis eine freie Spalte gefunden wurde. Auch hier wird die Spalte wieder in das Feld Spalte des aktuellen Datensatzes der Tabelle tblTermine eingetragen.
Dieses Spiel wiederholt sich so lange, bis alle Termine mit Bereich, Spaltennummer und Spaltenzahl ausgestattet sind. In einem einfachen Fall sieht dies dann wie in Abb. 13 aus.
Abb. 13: Termindatensätze mit den Informationen, die zur Anzeige in einem mehrspaltigen Kalenderbericht nötig sind
Termine in den Bericht bringen
Die Termine müssen Sie nun nur noch im Bericht unterbringen. Dazu brauchen Sie noch zwei weitere Abfragen. Die erste, qryRptTermineMehrspaltig, führt die Termine aus tblTermine mit den Farbbezeichnungen aus der Tabelle tblFarben zusammen (s. Abb. 14).
Abb. 14: Diese Abfrage führt Termine und Farben zusammen.
Nun müssen wir die Termine noch in eine Form überführen, die sie als Datenherkunft für den Terminbericht verwendbar macht. Dazu stellen Sie einfach eine weitere Abfrage zusammen, welche die zuvor erstellte Abfrage mit der Tabelle tblKalenderdaten zusammenführt. Zwischen dem Feld Kalenderdatum der Tabelle tblKalenderdaten und dem Feld Termindatum der Abfrage qryRptTermineMehrspaltig stellen Sie eine Beziehung her, die alle Datensätze der Tabelle tblKalenderdaten, aber nur diejenigen der Tabelle qryRptTermineMehrspaltig enthält, deren Termindatum mit dem Kalenderdatum des aktuellen Datensatzes zusammenhängt (s. Abb. 15).
Abb. 15: Zusammenführen von Terminen und Kalenderdaten im Entwurf ...
Die Datenblattansicht aus Abb. 16 zeigt das Ergebnis dieser Abfrage: Es sind alle Datumsangaben enthalten, egal ob dort ein Termin stattfindet oder nicht. Wenn mehrere Termine an einem Datum stattfinden, wird dieses in Form der entsprechenden Anzahl Datensätze realisiert.
Abb. 16: ... und in der Datenblattansicht
Diese Abfrage weisen Sie dem bereits vorbehandelten Bericht statt der bisherigen Datenherkunft tblKalenderdaten zu. Das bereits im Seitenkopf enthaltene Feld Kalenderdatum können Sie dort belassen, da es ja auch in der neuen Datenherkunft enthalten ist.
Die übrigen sieben Felder ziehen Sie einfach aus der Feldliste in den Detailbereich (s. Abb. 17). Stellen Sie die Eigenschaft Sichtbar der hinzugefügten Steuerelemente auf den Wert Nein ein.
Abb. 17: Der Bericht sieht in der Entwurfsansicht recht banal aus. Die Steuerelemente sollen alle unsichtbar sein.
Das Eintragen übernimmt die Routine TermineEinfuegen (Listing 4). Die Routine ermittelt wiederum zunächst die Abmessungen des Detailbereichs und kontrolliert, ob der aktuelle Datensatz einen konkreten Termin enthält (es könnte ja auch einfach ein Datum sein) und fügt nur dann den Termin hinzu. Dabei werden die Koordinaten der dazu verwendeten und mit der Line-Methode gezeichneten Rechtecke über die aktuellen Maße des Berichts sowie die Anzahl der Spalten und die Länge des Termins ermittelt.
Private Sub TermineEinfuegen()
Dim lngColor As Long
Dim sngRepX1 As Single, sngRepX2 As Single
Dim sngRepY1 As Single, sngRepY2 As Single
Dim strTermin As String
Dim sngDateX1 As Single, sngDateX2 As Single
Dim sngDateY1 As Single, sngDateY2 As Single
Dim sngDateX1A As Single, sngDateY2A As Single
Dim intSpalte As Integer, intSpaltenzahl As Integer
Dim sngGesamtbreite As Single, sngTextOffset As Single
Dim bolTooLong As Boolean
Dim sngMargin As Single
sngMargin = 5
With Me
.ScaleMode = 3
sngRepY1 = .ScaleTop
sngRepX1 = .ScaleLeft
sngRepX2 = .ScaleWidth
sngRepY2 = .ScaleHeight
If Not IsNull(Me!Termindatum) Then
'Termine einfügen
lngColor = Me.Farbwert
intSpalte = Me!Spalte
intSpaltenzahl = Me!Spaltenzahl
sngGesamtbreite = sngRepX2 - sngRepX1 - 200
sngDateX1 = 200 + (intSpalte - 1) * sngGesamtbreite _
/ intSpaltenzahl
sngDateX2 = sngDateX1 + sngGesamtbreite / intSpaltenzahl
sngDateY1 = sngRepY1 + CSng(Me.Startzeit) * sngRepY2
sngDateY2 = sngRepY2 * CSng(Me.Endzeit)
Me.Line (sngDateX1 + sngMargin, sngDateY1 + sngMargin) _
-(sngDateX2 - sngMargin, sngDateY2 - sngMargin), _
lngColor, BF
lngColor = RGB(200, 200, 200)
Me.Line (sngDateX1, sngDateY1)-(sngDateX2, sngDateY2), _
lngColor, B
If Me.Endzeit < Me.Endzeit1 Then
lngColor = Me.Farbwert
sngDateY2A = sngRepY2 * CSng(Me.Endzeit1)
sngDateX1A = sngDateX1 + 30
Me.Line (sngDateX1A + sngMargin, sngDateY1 + _
sngMargin)-(sngDateX2 - sngMargin, sngDateY2A _
- sngMargin), lngColor, BF
lngColor = RGB(200, 200, 200)
Me.Line (sngDateX1A, sngDateY1)-(sngDateX2, _
sngDateY2A), lngColor, B
sngTextOffset = 30
Else
sngTextOffset = 0
End If
Me.FontSize = 8
Me.FontBold = True
If Helligkeit(Me.Farbwert) > &H180 Then
Me.ForeColor = &H0
Else
Me.ForeColor = &HFFFFFF
End If
Me.CurrentX = sngDateX1 + 20 + sngTextOffset
Me.CurrentY = sngRepY1 + CSng(!Startzeit) * sngRepY2 + 20
strTermin = Me!Termin
Do While Me.TextWidth(strTermin) > _
sngDateX2 - sngDateX1 - sngTextOffset
bolTooLong = True
strTermin = Left(strTermin, Len(strTermin) - 1)
Loop
If bolTooLong = True Then
strTermin = Left(strTermin, Len(strTermin) - 4) & "..."
End If
Me.Print strTermin
Me.FontSize = 6
Me.CurrentX = sngDateX1 + 20 + sngTextOffset
Me.Print !Startzeit & " - " & !Endzeit
MoveLayout = False
Else
MoveLayout = True
End If
End With
End Sub
Wenn der Termin kürzer als eine Stunde ist, dann kommt das bereits weiter oben besprochene Feld Endzeit1 der Datenherkunft zum Tragen, dass Terminen von einer Dauer unter einer Stunde eine neue Endzeit zuweist, die den Termin auf die Dauer einer Stunde verlängert. Ist das der Fall, liegt also der Wert von Endzeit1 hinter Endzeit, zeichnet die Routine noch ein Rechteck über das bereits vorhandene Rechteck für den Termin. Dieses ist auf der linken Seite ein wenig schmaler, sodass dort eine kleine Zunge herausschaut, welche die eigentliche Dauer des Termins grafisch anzeigt, wie es auch unter Outlook geschieht (s. Abb. 18).
Abb. 18: Die Länge von Terminen, die weniger als eine Stunde dauern, wird durch eine kleine Zunge auf der linken Seite dargestellt.
Der eigentliche Körper des Termins ist dann dennoch hoch genug, um die Anzeige der Terminbezeichnung und der Dauer zu gewährleisten.
Nach dem Zeichnen der Kästen für die grafische Darstellung sind eben jene Texte an der Reihe. Da der Raum für die Terminbezeichnung in Abhängigkeit von der Anzahl der Spalten recht knapp werden kann und der Text nicht über den Rand der Terminbox hinaus gezeichnet werden soll, prüft die Funktion TextWidth des Report-Objekts die zu erwartende Länge der Terminbezeichnung und kürzt diese jeweils um einen Buchstaben, bis der Betreff in das Kästchen passt. Ist das erledigt, werden nochmals vier Zeichen abgeschnitten und durch drei Punkte (...) ersetzt, um die Kürzung zu verdeutlichen. Darunter schreibt die Routine schließlich noch die Dauer des Termins.
Termin für Termin
Was aber geschieht, wenn ein Tag nicht nur einen, sondern mehrere Termine hat? Eigentlich müsste doch dann jeweils eine neue Seite mit dem gleichen Datum, aber dem nächsten Termin angelegt werden? So wäre es auch, wenn die Routine TermineEinfuegen nicht am Ende den Wert der Eigenschaft MoveLayout des Berichts einstellen würde. Diese Eigenschaft legt fest, ob der nächste Datensatz im nächsten Detailbereich ausgegeben werden soll, was der Normalzustand ist, oder ob die Daten im gleichen Detailbereich erscheinen sollen.
Und genau das ist es, was diesen Bericht überhaupt möglich macht: Immerhin sind die einzelnen Detailbereiche eines Berichts unter Access naturgemäß untereinander angeordnet, was einer nebenläufigen Anordnung von Terminen entgegenstünde.
Also stellt die Routine MoveLayout auf False, wenn der aktuelle Datensatz einen Termin enthält, und verhindert so den Sprung nach unten zum nächsten Detailbereich.
Nun gibt es noch ein Problem: Das Raster zur Anzeige der Uhrzeiten und der Linien zu ihrer optischen Hervorhebung wird aktuell noch mit jedem Datensatz gedruckt.
Das ist nicht schlimm, wenn ein Tag nur einen oder keinen Termin enthält, weil die Routine UhrzeitrasterAnlegen dann ja auch nur einmal aufgerufen wird.
Wenn ein Tag allerdings zwei oder mehr Termine enthält, dann wird das Raster entsprechend der Anzahl der Termine gedruckt, was den gleichen Effekt hat, als wenn man die Uhrzeiten und Linien fett drucken würde.
Damit dies nicht geschieht, deklarieren Sie im Berichtsmodul eine Variable namens bolRaster:
Dim bolRaster As Boolean
Diese hat standardmäßig den Wert False, sodass Sie in der Routine, die durch das Ereignis Beim Drucken des Berichts ausgelöst wird, genau dann das Raster anlegen sollten, wenn bolRaster den Wert False hat:
Private Sub Detailbereich_Print(Cancel As _
Integer, PrintCount As Integer)
If bolRaster = False Then
UhrzeitrasterAnlegen
bolRaster = True
End If
TermineEinfuegen
End Sub
Danach wird bolRaster auf True eingestellt, damit beim nächsten Termin nicht wieder das komplette Raster ausgegeben wird.
Damit dies nach der Abarbeitung aller Termine einer Seite auf der folgenden Seite wieder geschieht, legen Sie noch eine weitere Routine an, die durch das Ereignis Beim Formatieren des Seitenkopfes ausgelöst wird. Diese Routine stellt bolRaster wieder auf False ein, damit das Raster im nächsten Detailbereich wieder gedruckt wird:
Private Sub Seitenkopfbereich_Format(Cancel As Integer, FormatCount As Integer)
bolRaster = False
End Sub
Zusammenfassung und Ausblick
Die hier vorgestellte Lösung ist ein Beispiel dafür, was man mit Berichten alles anstellen kann. Auf die gleiche Weise können Sie theoretisch mit vielen anderen Daten verfahren, die Sie bisher aufgrund der Einschränkungen durch die vertikale Anordnung der Berichtsbereiche nicht wie gewünscht ausgeben konnten.
Der in diesem Beitrag beschriebene Terminbericht kann ebenfalls noch erweitert werden. Denkbar wären etwa das vorherige Festlegen des Zeitraums, der pro Tag angezeigt werden soll. Auch könnte man die Terminboxen mit weiterem Schnickschnack wie einem schattierten Hintergrund versehen.
Vielleicht möchten Sie ja auch dem Berichtskopf noch eine Übersicht des aktuellen Monats hinzufügen, wie es in Outlook der Fall ist?
Eine andere Ansicht der Termine ist natürlich auch möglich. So könnten Sie die Termine für eine Woche nebeneinander anzeigen, wobei die Darstellung mehrerer Termine am gleichen Tag nebeneinander wohl nicht sinnvoll wäre.
Haben Sie weitere Ideen für Berichte, die wir in Access im Unternehmen vorstellen sollen? Dann teilen Sie uns diese unter der E-Mail-Adresse info@access-im-unternehmen.de mit.
|