Kalendersteuerelement, Teil 2

Lies diesen Artikel und viele weitere mit einem kostenlosen, einwöchigen Testzugang.

Das im ersten Teil dieser Ausgabe beschriebene Kalendersteuerelement eignet sich vornehmlich zur Auswahl eines Datums. Benötigen Sie aber eine übersicht, wie die Termine des Outlook-Kalenders mit der Markierung von Datumsbereichen, etwa zur Darstellung Ihrer Urlaubsplanung, so sind die Anforderungen ganz andere. Auch für diesen Zweck stellen wir ein Pseudo-Steuerelement vor, das allein mit Access-Bordmitteln realisiert ist.

Terminkalender

Einsatzbereiche für solche kalendarischen übersichten gibt es viele. Feiertage können darin eingetragen werden, Geburtstage und andere Jubilaren, die Buchung von Ferienwohnungen oder Autovermietungen, die Einsatzplanung von Mitarbeitern, oder Sie markieren hier Ihre Urlaubstage. Dabei geht es weniger um die Auswahl von Terminen, als um deren Darstellung. Bild 1 zeigt bereits, wie unser Bespiel aussieht.

Demo des Unterformularsteuerelements Monatskalender nach Einbau in ein Hauptformular mit Markierungen

Bild 1: Demo des Unterformularsteuerelements Monatskalender nach Einbau in ein Hauptformular mit Markierungen

Natürlich bauen wir hier keinen komplexen Terminkalender nach, wie den von Outlook. Das Kalendersteuerelement sfrmCalendarMonths ist eng an die einfache Version des Auswahlkalenders im ersten Teil dieser Ausgabe angelehnt, wie seine linke Seite schon vermuten lässt. Es dient in erster Linie der Visualisierung bereits vorliegender Termindaten. Beschreiben wir zunächst kurz seinen Aufbau.

Grundsätzlich werden hier drei Monate mit ihren Tagen angezeigt. Die Auswahl der Monate geschieht mit dem Kombinationsfeld links, die den mittleren Monat einstellt. Die übrigen Navigationselemente, wie die Jahres-Combo oder die Buttons zum Weiterschalten der Monate gleichen in ihrer Funktion genau der des einfachen Auswahlkalenders. Zusätzlich ist die Kalenderübersicht noch mit einer Titelzeile oben versehen, die Sie mit beliebigem Inhalt füllen können. Das eigentliche Feature aber sind die rot unterlegten Zellen, die durch die dem Steuerelement über eine öffentliche Funktion zugewiesene Datumsbereiche zustande kommen. Mehr gibt das Steuerelement in dieser Version nicht her. Es reagiert auch nicht auf Klicks in die Zellen. Das wäre eine Eigenschaft, die Sie mit dem Wissen aus dem ersten Teil dieser Ausgabe zum Auswahlkalender leicht selbst nachtragen könnten.

Basistabelle

ähnlich, wie beim Auswahlkalender, kommt für die Darstellung der Tage eine Tabelle zum Einsatz, deren Felder dann von 21 Textboxen im Endlosformular ausgegeben werden. Hier reichen sieben Felder natürlich nicht aus. Für jede Spalte des Dreimonatskalenders muss ein eigenes Feld her, wenn nicht etwa eine umständliche Kreuztabellenabfrage verwendet werden soll.

Die Felder der Tabelle tblCalendarMonths sind ebenfalls vom Zahlentyp Long. Die Nummerierung geschieht über das Präfix D, gefolgt von der Nummer des Wochentags und der Position des Monats im Kalender. D62 bezeichnet also etwa den sechsten Tag (Sonnabend) und die 2 darin den mittleren Monat. Bild 2 verdeutlicht dies ausschnittsweise. Auch diese Tabelle wird vom Formular selbst mit Datensätzen gefüllt.

Ausschnitt der Monatskalendertabelle in der Entwurfsansicht

Bild 2: Ausschnitt der Monatskalendertabelle in der Entwurfsansicht

Allerdings speichern wir hier nicht jeweils die Tage der Monate ab, da diese ja mehrfach vorkommen -pro Monat einmal. Stattdessen nimmt ein Feld jeweils tatsächlich einen Datumswert auf. Warum ein Datum in einem Long-Feld Sie ahnen es möglicherweise: Auch hier möchten wir Bedingte Formatierung zur farblichen Unterscheidung der Zellen benutzen und nehmen wieder negative Werte als Indiz für eine Markierung. Schauen Sie auf die gefüllte Tabelle in Bild 3.

So präsentiert sich die Tabelle tblCalendarMonths, nachdem sie über das Formular mit Daten gefüllt wurde.

Bild 3: So präsentiert sich die Tabelle tblCalendarMonths, nachdem sie über das Formular mit Daten gefüllt wurde.

Ein negativer Wert führt später zur roten Hervorhebung der Zelle. Der Absolutwert einer Zahl stellt das Datum dar. Wir erwähnten bereits, dass ein Access- oder VBA-Datum tatsächlich ein Double-Wert ist, dessen ganzzahliger Anteil den Tag angibt. Und diesen kann auch ein Long-Wert aufnehmen. Beispiel:

  Now()            -> Datum: 21.07.2017 08:33
  CDbl(Now())        -> Double: 42937,35625
  Clng(CDbl(Now()))        -> Long: 42937
  CDate(42938)        -> Datum: 21.07.2017

Benötigen Sie also den Zeitanteil eines Datums nicht, so reicht zu dessen Speicherung ein Long-Wert.

Die Tatsache, dass hier tatsächlich Datumswerte abgespeichert sind, macht es überdies später leichter, sie beim Klick auf die Textboxen zu ermitteln. Den Steuerelementinhalt unterziehen Sie dazu lediglich der Funktion CDate(). Eine Berechnung aus der Zellenposition, wie beim Auswahlkalender, ist hier nicht nötig. Schauen wir im Folgenden an, wie die Daten der Tabelle generiert werden. Auch hier nennt sich die verantwortliche Prozedur FillCalendar.

Daten der Basistabelle erzeugen

Die Prozedur FillCalendar wird im Formularmodul immer dann aufgerufen, wenn sich der Monat oder Tag ändern. Das kann durch Auswahl in den entsprechenden Kombinationsfeldern geschehen, durch Betätigung der Buttons zum Weiterschalten, oder durch Zuweisung an die Eigenschaftsprozeduren Year und Month. Listing 1 zeigt wieder eine gekürzte Version, in der nur die für die Erzeugung der Datensätze relevanten Teile abgebildet sind.

Private Sub FillCalendar()
     Dim rs As DAO.Recordset
     Dim i As Long, j As Long, n As Long, f As Long
     Dim StartDate As Date, EndDate As Date
     Dim MaxDate As Date, DTmp As Date
     
     StartDate = DateSerial(m_Year, m_Month - 1, 1)
     EndDate = DateSerial(m_Year, m_Month + 2, 0)
     
     CurrentDb.Execute "DELETE FROM tblCalendarMonths"
     Set rs = CurrentDb.OpenRecordset("tblCalendarMonths", dbOpenDynaset)
     For n = 0 To 4
         rs.AddNew
         For i = 1 To 3
             MaxDate = DateSerial(m_Year, m_Month + i - 1, 0)
             For j = 1 To 7
                 DTmp = DateAdd("m", i - 1, StartDate)
                 DTmp = DateAdd("d", n * 7 + j - 1, DTmp)
                 If DTmp > MaxDate Then Exit For
                 If IsInRanges(DTmp) Then f = -1 Else f = 1
                 rs.Fields("D" & CStr(j) & CStr(i)).Value = f * CLng(DTmp)
             Next j
         Next i
         rs.Update
     Next n
     
     rs.Close
     Me.Requery
End Sub

Listing 1: Befüllen der Tabelle tblCalendarMonths

Zunächst wird in StartDate der erste Tag des linken Monats ermittelt. Dazu wird vom Monat der Member-Variablen m_Month eins abgezogen und das Datum mit DateSerial berechnet. EndDate wiederum speichert den letzten Tag des rechten Monats. Da die Anzahl der Tage eines Monats variabel ist, kann für den Tag-Parameter in DateSerial kein Wert angegeben werden. Man behilft sich in diesem Fall mit dem Folgemonat (m_Month + 2) und dem Tag 0. Das entspricht dem ersten Tag des Monats minus eins.

Die Execute-Anweisung leert die Tabelle und OpenRecordset öffnet eine beschreibbare Datensatzgruppe auf sie. Die folgende Schleifenkonstruktion weicht von der des Auswahlkalenders ab. Wir benötigen hier drei ineinander verschachtelte Schleifen. Die äußere auf die Zählervariable n stellt die Zeilen der Kalender dar. Diese entspricht dann auch der Anzahl der Datensätze der Tabelle, weshalb der Durchlauf auch mit einem AddNew beginnt. Die nächste Schleife mit dem Zähler i betrifft die drei Monate, die innerste Schleife mit dem Zähler j deren Wochentage.

Zur Berechnung des Datums eines Datenfeldes wird hier StartDate nicht einfach fortlaufend erhöht. Stattdessen werden zu StartDate zweimal Werte über die DateAdd-Funktion addiert und das Ergebnis in der Date-Variablen DTmp zwischengespeichert. Die erste DateAdd-Funktion addiert die Nummer des Monats aus dem Monatsschleifenzähler i, wobei der Ausdruck m der Funktion erst sagt, dass Monate zu addieren sind. Das zweite DateAdd verwendet Tag-Werte (Ausdruck d) und berechnet über den Wochentag in j und das Siebenfache der betreffenden Woche n einen weiteren Offset. Im Prinzip könnte dieser Wert aus DTmp dann schon einem Tabellenfeld zugewiesen werden.

Da jedoch die Schleifendurchläufe dazu führen können, dass über die Addition der Tage ein über einen Monat hinausreichendes Datum ermittelt würde, gibt es eine Abbruchbedingung für die innere Schleife. Ist DTmp größer, als der letzte Tag des Monats, so wird die Schleife verlassen. Dieser letzte Tag ist in der Variablen MaxDate gespeichert und wird nach dem Beginn der zweiten Schleife jeweils neu berechnet.

Schließlich sind noch die Zeiträume zu berücksichtigen, die im Kalendersteuerelement rot zu unterlegen sind und durch negative Datumszahlen repräsentiert werden. Die Funktion IsInRanges, auf welche wir noch zu sprechen kommen, wird dazu befragt.

Befindet sich das Schleifendatum DTmp innerhalb eines der verabreichten Zeiträume, so gibt die Funktion True zurück. In diesem Fall nimmt die Variable f den Wert -1 an, andernfalls 1. Und f ist dann wieder der Multiplikator für den Datumswert, der dem Feld des Recordsets zugewiesen wird. Der Name des Felds ergibt sich aus dem Präfix D und den Zählvariablen j und i über String-Verkettung.

Zeiträume zuweisen

Dem Kalendersteuerelement können beliebig viele Datumsbereiche hinzugefügt werden, die dann in der Ansicht rot markiert werden. Sie können dabei auch außerhalb des dargestellten Bereichs liegen. Die Prozedur AddRange (s. Listing 2) bewerkstelligt deren Speicherung.

Private Type TRange
     StartDate As Date
     EndDate As Date
End Type 
Private arrRanges() As TRange
Public Sub AddRange(StartDate As Date, EndDate As Date)
     Dim n As Long
     
     On Error Resume Next
     n = UBound(arrRanges)
     If Err.Number <> 0 Then
         n = 0
     Else
         n = n + 1
     End If
     On Error GoTo 0
     ReDim Preserve arrRanges(n)
     arrRanges(n).StartDate = StartDate
     arrRanges(n).EndDate = EndDate
     
     FillCalendar
End Sub

Listing 2: Hinzufügen eines Zeitraums über AddRange

Als Parameter geben Sie das Startdatum des gewünschten Bereichs in StartDate an und das Enddatum in EndDate. Soll es nur ein Tag sein, dann müssen beide Werte identisch sein. Das Wertepaar speichert die Routine in einem modulweit gültigen Array arrRanges des benutzerdefinierten Typs TRange, der im Kopf des Moduls deklariert ist. Hier muss erst per UBound ermittelt werden, wie viele Elemente das Array bereits enthält. Ist noch kein Element vorhanden, so ist das Array noch nicht initialisiert, was bei UBound zu einem Fehler führen würde. Deshalb ist die Fehlerbehandlung eingangs per On Error Resume Next außer Kraft gesetzt. Die Variable n nimmt die Anzahl der Elemente entgegen und erhöht sie um eins.

Nun kann das Array mit ReDim neu dimensioniert werden, wobei das Schlüsselwort Preserve angibt, dass sein Inhalt dabei nicht verloren gehen soll. Im n-ten Element das Arrays werden schließlich Start- und Enddatum abgespeichert. Der abschließende Aufruf von FillCalendar führt dazu, dass der Zeitraum im Kalender auch sofort dargestellt wird.

Möchten Sie die Zeiträume modifizieren, so löschen Sie sie mit der Prozedur DeleteRanges alle auf einen Schlag und fügen die neuen wieder hinzu. Das Löschen einzelner Datumsbereiche erlaubt das Modul nicht.

Public Sub DeleteRanges()
     Erase arrRanges
     FillCalendar
End Sub 

Die angesprochene Funktion IsInRanges ermittelt dann, ob ein bestimmtes Datum sich innerhalb der Zeiträume des angelegten Arrays befindet (s. Listing 3). D ist hier der Datumsparameter, den Sie der Funktion übergeben. Alle Zeiträume werden in einer Schleife durchlaufen und StartDate, wie EndDate eines Elements, mit ihm verglichen. Sobald das für ein Bereichselement zutrifft, erhält der Rückgabewert der Funktion True und die Schleife wird verlassen.

Private Function IsInRanges(D As Date) As Boolean
     Dim i As Long, n As Long
     
     On Error Resume Next
     n = UBound(arrRanges)
     If Err.Number <> 0 Then
         On Error GoTo 0
         Exit Function
     End If
     On Error GoTo 0
     
     For i = 0 To n
         If (D >= arrRanges(i).StartDate) And (D <= arrRanges(i).EndDate) Then
             IsInRanges = True
             Exit For
         End If
     Next i
End Function

Listing 3: Die Funktion ermittelt, ob sich das Datum D innerhalb der gespeicherten Datumsbereiche befindet.

Entwurf des Steuerelementformulars

Die Entwurfsansicht des Formulars sfrmCalendarMonths (s. Bild 4) kommt ähnlich daher, wie die des Auswahlkalenders. Der Unterschied ist, dass hier natürlich mehr Textboxen im Detailbereich untergebracht sind, drei Labels für die Monatsnamen über den Wochentagen platziert sind und sich ein Bezeichnungsfeld für den Titel oben rechts befindet. Sowohl die Label für die Monatsnamen, wie die für die Wochentage, werden in der FillCalendar-Prozedur des Formulars per VBA beschriftet.

Das Formular sfrmCalendarMonths gleich in der Entwurfsansicht dem des Beitrags zum einfachen Kalender.

Bild 4: Das Formular sfrmCalendarMonths gleich in der Entwurfsansicht dem des Beitrags zum einfachen Kalender.

Bei den Monatsnamen ist die Geschichte noch einfach. Die Beschriftung der Labels LM1 bis LM3 leitet sich vom Startdatum des Kalenders ab:

Me!LM1.Caption = Format(StartDate, "mmmm")
Me!LM2.Caption = Format(StartDate + 31, "mmmm")
Me!LM3.Caption = Format(StartDate + 62, "mmmm")

Der Monatsname des ersten Einzelkalenders ergibt sich aus dem Startdatum (StartDate) über die Format-Funktion. Für den zweiten Monat werden dem Startdatum einfach 31 Tage hinzuaddiert, für den dritten 62.

Die Labels für die Wochentage waren im Auswahlkalender noch fest beschriftet. Hier geht das nicht, weil ja jeder Einzelkalender mit dem Ersten des Monats beginnt. Folglich variiert auch die Bezeichnung der jeweils ersten Spalte.

Ende des frei verfügbaren Teil. Wenn Du mehr lesen möchtest, hole Dir ...

Testzugang

eine Woche kostenlosen Zugriff auf diesen und mehr als 1.000 weitere Artikel

diesen und alle anderen Artikel mit dem Jahresabo

Schreibe einen Kommentar