Kalendersteuerelement, Teil 1

Dieser Artikel ist Teil des Magazins 'Access im Unternehmen', Ausgabe 5/2017.

Kalendersteuerelement, Teil 1

Zur Ein- oder Ausgabe eines Datums macht sich ein geeignetes Kalendersteuerelement im Formular besser als ein schnödes Textfeld. Das kann fest im Formular integriert sein, oder als Popup zur Auswahl erscheinen. In Buchungssystemen im Web sind solche Kalenderelemente allgegenwärtig. Auch Access wurde mit der Version 2007 ein solches Popup-Element spendiert, welches sich aber leider in keiner Weise steuern lässt. Grund genug also, um sich nach Alternativen umzuschauen.

Existierende Kalenderelemente

Einst war ein Kalendersteuerelement in Form der ActiveX-Datei mscal.ocx optionaler Bestandteil der Office-Installation. Das hat sich seit einiger Zeit geändert. Man ist nunmehr allein auf das Popup angewiesen, das bei Datumsfeldern zur Auswahl erscheint, wenn in das zugehörige Textfeld geklickt wird. Eine dauerhafte Ansicht des Kalenders ist somit verwehrt.

Bild 1 zeigt das alte Kalender-ActiveX-Steuerelement links oben im Formular. Aus irgendeinem Grund befand es sich, möglicherweise aus älteren Office-Installationen, noch bei uns in englischer Version im System. Seine Darstellung lässt sich im Formularentwurf oder auch per VBA steuern. Das betrifft die Schriftarten und die Farbgebung. Es löst bei einigen Aktionen ein Ereignis aus und spiegelt in der Eigenschaft Value das markierte Datum wieder. Sollte auf ihrem System die ActiveX-Datei nicht installiert sein, so meldet Access beim Aufruf des Formulars der Beispieldatenbank den etwas eigenartigen Fehler In diesem Formular befindet sich kein Steuerelement. Entfernen Sie in diesem Fall im Entwurf des Formulars frmCalendarTest einfach den Platzhalter des Steuerelements.

Demo einiger Datums- und Kalendersteuerelemente im Testformular

Bild 1: Demo einiger Datums- und Kalendersteuerelemente im Testformular

Da von diesem Fall auszugehen ist, fragt sich, wie mit anderen Mitteln eine ähnliche Darstellung und Funktion zu erreichen wäre. Testweise haben wir ein Listenfeld (rechts unten in der Abbildung) und ein Microsoft Listview Control (links unten) als Kalender zu gestalten versucht. Mit einigen Zeilen Code und den entsprechenden Einstellungen für die Steuerelemente gelingt dies auch. Der inakzeptable Nachteil der Lösungen besteht darin, dass sich in diesen Steuerelementen nur ganze Zeilen markieren lassen, nicht aber einzelne Zellen. Eine Auswahl per Maus scheidet deshalb aus. Derlei ließe sich also lediglich zur Anzeige der Tage eines Monats verwenden. Doch das brauchen Sie wohl höchst selten. Bleiben noch zwei Alternativen: das Microsoft DateTime Picker Control und die Access-Textbox mit Datumsformatierung. Ersteres zeigt sich im Formular rechts oben. Es entspringt der ActiveX-Datei mscomct2.ocx, die früher ebenfalls mit Office installiert wurde (s. Bild 2).

Das Microsoft DateTime-Picker- ActiveX-Steuerelement

Bild 2: Das Microsoft DateTime-Picker- ActiveX-Steuerelement

Wir erwähnen es nur der Vollständigkeit halber, denn außer einer abweichenden Gestalt bietet es gegenüber der dem Popup von Access (s. Bild 3) keine sonderlichen Vorteile. Nur die Schriftarten und Farben können hier zusätzlich eingestellt werden. Die Funktionalität ist bei beiden jedoch gleich.

Das Popup-Element zur Datumsauswahl bei Access-Textboxen

Bild 3: Das Popup-Element zur Datumsauswahl bei Access-Textboxen

Normalerweise öffnet sich das Kalenderelement einer Textbox mit Datumsformatierung dann, wenn Sie auf das Symbol rechts neben dem Textfeld klicken. Es gibt aber eine Anweisung, die die Anzeige des Kalender-Popups erzwingt. Möchten Sie etwa, dass es sofort beim Eintritt in das Feld erscheint, oder auch beim Klicken in das Textfeld, so schreiben Sie die folgende Zeile in die entsprechenden Ereignisprozeduren:

Private Sub txtDate_Click()
     RunCommand acCmdShowDatePicker
End Sub
Private Sub txtDate_Enter()
     RunCommand acCmdShowDatePicker
End Sub

Diese RunCommand-Anweisung ermöglicht das Öffnen des Popups per Code, falls die Datums-Textbox gerade den Fokus besitzt. Andernfalls ereignet sich eine Fehlermeldung. Verwenden Sie sie deshalb besser nur in den Ereignissen der Textbox selbst.

Sicher finden Sie auch noch weitere Fremdsteuerelemente in Form von ActiveX-Dateien bei Drittanbietern. Es gilt jedoch die Maxime, wegen der möglichen Registrierungsprobleme solche ActiveX-Komponenten nur dann einzusetzen, wenn es absolut notwendig ist. Und das ist hier nicht gegeben, denn ein Kalendersteuerelement lässt sich gut auch mit Access-Bordmitteln selbst erstellen!

Kalender im Eigenbau

Natürlich könnte man einfach 31 Steuerelemente, etwa Buttons oder Textboxen, in einem Formular so anordnen, dass diese einen Kalender ergäben. Um auf eine Datumsauswahl zu reagieren, bräuchte es dafür dann eben auch 31 Ereignisprozeduren. Das wäre zwar recht unkompliziert, ist aber wenig elegant.

Dabei reicht es im Prinzip ja für die Tage der Woche nur sieben Steuerelemente im Detailbereich zu platzieren und diese Wochen untereinander zu wiederholen. Damit sind wir allerdings auf eine Tabelle angewiesen, die die Datensätze für alle Wochen eines Monats bereitstellt. Nur eine Tabelle oder Abfrage als Datensatzherkunft kann bewirken, dass sich der Detailbereich wiederholt.

Bevor es Schritt für Schritt an die Entwicklung des Kalenderformulars geht, sollte noch definiert werden, was der Ausdruck Steuerelement in unserem Zusammenhang bedeutet. Ein wirklich neues Steuerelement kann nun mal mit Access nicht erzeugt werden. Sie können lediglich aus bestehenden Elementen eine neue Funktionsgruppe erstellen, die den Anschein eines einzelnen Steuerelements erweckt. Damit das Ganze wiederverwendbar ist, sollte diese Funktionsgruppe möglichst in einem Formular ohne weitere Abhängigkeiten untergebracht werden, welches Sie später als Unterformular in andere einsetzen. Dieses Unterformular stellt dann quasi ein Pseudo-Steuerelement dar. In der Beispieldatenbank nennt es sich sfrmCalendar. Das Präfix s steht für Subform.

Wochentabelle für einen Monat

Die Basistabelle tblCalendar für das Kalenderformular in Bild 4 weist lediglich die sieben Datenfelder D1 bis D7 von Typ Long für die einzelnen Tage der Woche auf. Für die Namen der Felder kann man eher nicht Kürzel für Wochentage, wie Mo bis So, verwenden, da der Erste eines Monats eben selten ein Montag ist.

Die Tabelle tblCalendar als Basis für den Kalender im Eigenbau

Bild 4: Die Tabelle tblCalendar als Basis für den Kalender im Eigenbau

Die Nummerierung in den Namen der Felder ermöglicht später den gezielten Zugriff auf die Spalten der Tabelle. Ein Blick auf Bild 5 zeigt, was dahinter steckt.

Die gefüllte Tabelle tblCalendar im Datenblatt

Bild 5: Die gefüllte Tabelle tblCalendar im Datenblatt

Die Datensätze füllt nämlich eine VBA-Routine des Formulars zur Laufzeit unter Angabe des gewünschten Jahrs und Monats. Sie ordnet die Zahlen für die Tage so an, dass sich links immer die Montage befinden. Der Erste des Monats wiederum soll in der ersten Zeile auftauchen. Ist das kein Montag, so sind links von diesem Feld die Tage des Vormonats einzusetzen. Hier sind das der 28. bis 31.. Ähnliches gilt für den Letzten des Monats, welcher sich im letzten Datensatz befinden soll, aber in der Regel kein Sonntag ist. Folglich ergeben sich in der letzten Zeile rechts vom Monatsletzten meist noch weitere Tage des Folgemonats. Je nach Monat und Jahr sind in der Tabelle mindestens vier, maximal sechs, Datensätze zu erzeugen.

Meist sind die Tage, welche nicht zum angezeigten Monat gehören, also jene des Vor- und Folgemonats, in Kalendern ausgegraut dargestellt. Um dieses Feature auch unserem Kalender zu spendieren, verwenden wir für die Textfelder im Formular eine Bedingte Formatierung. Denn per VBA-Code kann zwar die Hintergrundfarbe einer Textbox über die Eigenschaft BackgroundColor gesteuert werden, doch das betrifft dann sämtliche Zellen einer Spalte, die sich von diesem Datenfeld ableiten, nicht jedoch einzelne Zellen.

Um Bedingte Formatierung kommt man hier also nicht herum. Da diese einen Vergleichsausdruck für die Steuerung des Formats verwendet, benötigen wir irgendein Indiz, welches die nicht zum Monat gehörigen Tage definiert. Und das ist hier das Minuszeichen. Alle auszugrauenden Tage haben einen negativen Wert. Damit ist auch klar, weshalb hier keine Datumstypen für die Felder der Tabelle zum Einsatz kommen, denn deren Werte können nicht negativ sein. Ein Long- oder ein Integer-Wert reichen zur Kennzeichnung aus.

Der direkte Bezug des Kalenders zu einer Tabelle bringt allerdings einen Nachteil mit sich. Benötigen Sie etwa zwei Kalendersteuerelemente in Ihrem Hauptformular, so wären diese ohne weiteres Zutun beide an dieselbe Tabelle gebunden und zeigten in der Folge auch die gleichen Daten an. In diesem Fall erstellen Sie eine Kopie der Tabelle und bezeichnen diese etwa mit tblCalendar2. Die folgend beschriebene Routine zum Füllen der Tabelle kann unterschiedliche Tabellen berücksichtigen.

Füllen der Tabelle per VBA

Die dafür verantwortliche Routine nennt sich FillCalendar und ist in Listing 1 abgebildet, wobei hier einige Teile entfernt wurden, die nicht unmittelbar zum Erzeugen der Datensätze gehören.

Private m_Table As String
Property Get Table() As String
     Table = m_Table
End Property
Property Let Table(ByVal Value As String)
     m_Table = Value
     FillCalendar
End Property 
Private Sub FillCalendar()
     Dim rs As DAO.Recordset
     Dim i As Long, j As Long
     Dim n As Long
     Dim StartDate As Date
     Me.RecordSource = m_Table
     n = Weekday(DateSerial(m_Year, m_Month, 1), vbMonday)
     StartDate = DateSerial(m_Year, m_Month, 1) - n
     CurrentDb.Execute "DELETE FROM " & m_Table
     Set rs = CurrentDb.OpenRecordset("SELECT * FROM " & m_Table, dbOpenDynaset)
     For j = 0 To 5
         rs.AddNew
         For i = 0 To 6
             StartDate = StartDate + 1
             n = VBA.Month(StartDate)
             rs.Fields("D" & CStr(1 + i)).Value = Day(StartDate) * IIf(n <> m_Month, -1, 1)
         Next i
         rs.Update
         If n <> m_Month Then Exit For
     Next j
     rs.Close
     Me.Requery
End Sub

Listing 1: Füllen einer Kalendertabelle über VBA-Code

Der Name der zu füllenden Tabelle steht im Kopf in der Eigenschaftsvariablen m_Table des Formularmoduls. Dieser Variablen muss also erst ein Wert zugewiesen werden, was die Property-Let-Prozedur Table übernimmt:

frm.Table = "tblCalendar"

Erst dann kann die eigentliche Routine aufgerufen werden. Sie weist dem Formular selbst zunächst als Datensatzherkunft (RecordSource) die nun in m_Table stehende Tabelle zu. Das Formular ist im Entwurf also noch nicht zwingend an eine Tabelle gebunden, sondern das geschieht hier zu Laufzeit.

Nun benötigen wir jenes Datum, welches in der linken oberen Ecke des Kalenders steht. Über Property-Prozeduren (hier nicht im Listing), wurde in den Variablen m_Month und m_Year der gewünschte Monat und das Jahr für den Kalender abgespeichert. Den Ersten dieses Monats erhalten Sie über die VBA-Funktion DateSerial:

DateSerial(m_Year, m_Month, 1)

Das ist indessen noch nicht das Datum, welches links oben steht. Um zu ermitteln, wie viele Tage des Vormonats zu berücksichtigen sind, kommt die VBA-Funktion Weekday zum Einsatz. Sie gibt eine Zahl zurück, die den Wochentag symbolisiert. Die 1 entspricht dabei Montag, die 7 dem Sonntag. Nun muss vom Ersten des Monats lediglich diese Zahl subtrahiert werden, und schon steht das Datum links oben fest. Es wird der Variablen StartDate vom Type Date zugewiesen. Nach diesen Vorarbeiten kann die Tabelle m_Table über zwei verschachtelte Schleifen mit Daten versehen werden.

Doch vorher muss die Tabelle noch über die Execute-Anweisung und den SQL-DELETE-Ausdruck geleert werden. Anschließend öffnet ein Recordset rs die Datensätze zum Beschreiben.

Die Zählervariable für die Schleife zum Hinzufügen von Datensätzen ist j. Da maximal sechs Zeilen im Kalender stehen können, ist ihr Bereich auf 0 bis 5 eingestellt. Nach jedem Durchlauf wird per AddNew ein neuer Datensatz erzeugt. Für den Zugriff auf die Feldwerte eines Datensatzes gibt es dann die folgende Schleife auf den Zähler i, deren Bereich sich für die einzelnen Wochentage von 0 bis 6 erstreckt. In dieser wird fortlaufend der Wert des Kalenderdatums in StartDate um eins erhöht. Das ist statthaft, weil ein Date-Type imgrunde ein Double-Wert ist, wobei die Nachkommastellen die Tageszeit angeben, die Vorkommastellen die Tage. Also führt Addition von 1 zum nächsten Tag.

Der Zugriff auf die Datenfelder geschieht namentlich über das Präfix D und den Zahler i, zu dem noch 1 addiert werden muss, weil die Felder von 1 bis 7 nummeriert sind, nicht von 0 bis 6. Der Wert eines Felds errechnet sich aus dem Tagesanteil des Datums, welchen die VBA-Funktion Day zurückgibt.

Im Prinzip wäre es das auch schon, wollten wir nicht die Tage außerhalb des Zielmonats mit einem Minuszeichen versehen werden. Um jene zu eruieren, wird in der Variablen n der Monatsanteil des Datums über die Funktion Month() zwischengespeichert. Weicht dieser Wert vom Zielmonat ab (n <> m_Month), so greift die IIf-Bedingungsfunktion (entspricht Wenn() in Abfragen), die entweder eine -1 oder eine 1 als Ergebnis zeitigt. Und das ist eben der Multiplikator für den Tageswert.

Da die äußere Schleife immer von 0 bis 5 zählt, würden auch immer sechs Zeilen im Kalender stehen. Um das etwa für den Monat Februar zu verhindern, vergleicht die Zeile nach der inneren Schleife die Monate abermals und verlässt die äußere, sobald der Folgemonat erreicht ist.

Damit ist das Werk vollbracht. Die Requery-Anweisung auf das Formular (Me) baut die Ansicht auf Grundlage der neuen Datensätze nun neu auf.

Entwurf des Kalenderformulars

Neben den eigentlichen Tagen des Kalenders im Detailbereich soll das Formular noch in den Spaltenköpfen die Wochentage anzeigen. Außerdem dienen zwei Kombinationsfelder im Formularkopf der Auswahl des gewünschten Jahres und Monats. Als zusätzliche Navigationselemente können die Werte dieser Comboboxen über darunter liegende Buttons jeweils um eins vor oder zurückgeschaltet werden. Bild 6 demonstriert den Aufbau.

Entwurfsansicht des Endlosformulars sfrmCalendar

Bild 6: Entwurfsansicht des Endlosformulars sfrmCalendar

Die Spaltenüberschriften sind durch Labels realisiert, die hier mit festen Wochentagen versehen sind. Im Detailbereich gibt es sieben Textboxen, die anfänglich an die Datenfelder D1 bis D7 der Tabelle tblCalendar gebunden waren. Das aber muss umgangen werden, weil sonst ja negative Tageswerte erschienen. Der Ausdruck Abs aber macht aus negativen Zahlen positive, lässt positive aber unverändert. Also ist der Steuerelementinhalt der ersten beiden Textboxen dieser:

= Abs([D1])
= Abs([D2])
...

Damit werden die Tage korrekt ausgegeben. Fehlt nur noch die graue Hinterlegung der nicht zum Monat gehörigen Tage des Kalenders über die Bedingte Formatierung.

Bedingte Formatierung der Tageswerte

Klicken Sie auf die erste Textbox im Detailbereich rechts und wählen im Kontextmenü den Eintrag Bedingte Formatierung.... Das ruft einen Dialog, wie in Bild 7, auf den Plan. Hier sind zunächst eine oder mehrere Bedingungsregeln anzulegen. Uns reicht eine. Mit Klick auf Neue Regel öffnet sich ein zweiter Dialog. Im Bereich für den Vergleichsausdruck dürfen Sie nicht die Option Feldwert wählen, welche sich auf den Wert bezöge, den das Textfeld gerade anzeigt. Und das ist ja über die Abs-Funktion immer ein positiver. Stattdessen wählen Sie Ausdruck und setzen in das nebenstehende Feld den String [D1]<0.

Dialoge zur Bedingten Formatierung der Datumtextfelder

Bild 7: Dialoge zur Bedingten Formatierung der Datumtextfelder

Immer dann, wenn das Datenfeld D1 kleiner ist, als 0, greift die Bedingung und damit die darunter eingestellte Formatierung. Für den Hintergrund wurde ein leichtes Grau genommen und für die Schriftfarbe ein Dunkelgrau, damit diese Tageswerte zur Laufzeit erscheinen, wie deaktivierte Steuerelemente. Nach dem Schließen beider Dialoge ist die Bedingte Formatierung abgeschlossen.

Den Vorgang wiederholen Sie für die weiteren Textboxen des Detailbereichs, wobei hier natürlich jeweils das richtige Datenfeld im Vergleichsausdruck gewählt werden muss. Also die Felder D2 bis D7. Das Ergebnis der Angelegenheit können Sie schon einmal in Bild 8 begutachten.

Das Formular sfrmCalendar zur Laufzeit ist hier standalone aufgerufen

Bild 8: Das Formular sfrmCalendar zur Laufzeit ist hier standalone aufgerufen

Manche Kalender hinterlegen die Spalten für das Wochenende ebenfalls grau. Dafür braucht es keine Bedingte Formatierung, weil es sich hier immer um die Textboxen txtD6 und txtD7 handelt. Setzen Sie also gegebenenfalls einfach den Hintergrund dieser Textfelder auf einen festen Wert abweichend von Weiß.

Navigation und Ereignisse

Die beiden oberen Kombinationsfelder müssen eingangs mit den passenden Einträgen versehen werden. Das geschieht im Ereignis Beim Laden (Form_Load) des Formulars über zwei Schleifen.

Die einfachere ist dabei die zum Setzen der Jahreszahlen:

For i= 1950 To 2030
     Me!cbYear.AddItem i
Next i

Die Monatsnamen können Sie auch fest im Entwurf des Formulars festlegen, indem Sie die Eigenschaft Herkunftstyp der Combobox auf Wertliste einstellen und in der Eigenschaft Datensatzherkunft die Monatsnamen semikolon-getrennt hineinschrieben. Dann aber gäbe das Kombinationsfeld cbMonth nach Auswahl eines Eintrags nur den Monatsnamen als Wert aus. Für die Variable m_Month zum Anlegen der Datensätze benötigen wir aber eher eine Zahl. Daher ist die Combobox auch zweispaltig ausgelegt. Die erste versteckte Spalte, an die auch der Wert des Steuerelements gebunden ist, enthält die Zahlen, die zweite Spalte die Monatsnamen. Auch dies könnte über die Eigenschaft Datensatzherkunft geschehen, was zu diesem String führt:

1;Januar;2;Februar;3;März;4;April;5;Mai;...

In unserer Version übernimmt das jedoch dieser Schleifen-Code in Form_Load:

For i = 1 To 12
     Me!cbMonth.AddItem CStr(i) & ";" & _
                              Format(DateSerial(m_Year, i, 1), "mmmm") & ";"
Next i

Jede Zeile der Combobox bekommt in der ersten Spalte über String-Verkettung den Schleifenzähler i verabreicht, gefolgt von einem Semikolon, an das sich der Monatsname anschließt. Den berechnen Sie einfach über ein künstliches Datum, welches die DateSerial-Funktion zurückgibt. Aus dem Datum erhalten Sie per Format-Funktion und den Formatierungsausdruck mmmm dann den Monatsnamen im Volltext als Anteil. Der Ausdruck mmm hingegen führt zu abgekürzten Monaten:

Jan; Feb; Mar; Apr; ...

Die Auswahl eines Eintrags aus diesen Comboboxen löst ein Ereignis aus, das zum erneuten Befüllen der Tabelle und Neuzeichnen des Kalenders über die Routine FillCalendar führt:

Private Sub cbMonth_AfterUpdate()
     m_Month = Me!cbMonth.Value
     <b>FillCalendar</b>
     RaiseEvent MonthChanged(m_Month)
End Sub
Private Sub cbYear_AfterUpdate()
     m_Year = Me!cbYear.Value
     <b>FillCalendar</b>
     RaiseEvent YearChanged(m_Year)
End Sub

Die entsprechenden Member-Variablen m_Year und m_Month werden also mit neuen Werten versehen, die die Grundlage für die weiteren Vorgänge bilden.

Nachdem der Kalender neu angelegt ist, kommt jeweils die Anweisung RaiseEvent ins Spiel. Dies löst ein Ereignis aus, das vom Formular, welches das Unterformularelement enthält, später abgefangen werden kann. Dazu sind zunächst über das Schlüsselwort Event Ereignisse im Kopf des Kalenderformulars deklariert:

Public Event YearChanged(ByVal NewYear As Long)
Public Event MonthChanged(ByVal NewMonth As Long)

Diese Ereignisse können dezidiert mit RaiseEvent aufgerufen werden, wobei als Parameter das geänderte Jahr oder der geänderte Monat übergeben werden. Um auf diese Ereignisse reagieren zu können, muss das Hauptformular das Kalenderunterformular dergestalt instanziieren:

Private WithEvents oCalendar As Form_sfrmCalendar

Dadurch lässt sich eine Ereignisprozedur für die Objektinstanz oCalender generieren:

Private Sub oCalendar_MonthChanged(ByVal NewMonth As Long)
     Debug.Print "Neuer Kalendermonat: " & NewMonth
End Sub

Im VBA-Direktfenster wird damit der neu im Kalendersteuerelement gewählte Monat zur Information ausgegeben. Natürlich kann der Wert anschließend auch für beliebige weitere Vorgänge im Hauptformular genutzt werden.

Die vier Schaltflächen zum Weiter- oder Zurückschalten von Jahr und Monat wirken ähnlich, wie die Auswahl eines Combobox-Eintrags. Sie führen ebenfalls zum Neuanlegen des Kalenders und zum Auslösen der Ereignisse. Die Click-Prozedur für den Button cmdMonthNext zum Weiterschalten des Monats etwa sieht so aus:

Private Sub cmdMonthNext_Click()
     m_Month = m_Month + 1
     If m_Month =13 Then m_Month = 12
     Me!cbMonth.Value = m_Month
     FillCalendar
     RaiseEvent MonthChanged(m_Month)
End Sub

m_Month wird um eins erhöht. Für den Dezember würde das bedeuten, dass nun die ungültige Zahl 13 herauskommt. Deshalb berücksichtigt die Routine diesen Umstand und macht über die If-Zeile aus der 13 eine eins für den folgenden Januar. Dem Monatskombinationsfeld cbMonth wird dann der neue Wert zugewiesen, damit sich die Auswahl auch in diesem zeigt. Der Rest verläuft analog dem schon Dargestellten.

Den Code für die weiteren Schaltflächen führen wir hier nicht auf, weil er prinzipiell gleich aufgebaut ist.

Reaktion auf Datumsauswahl im Kalender

Beim Klick auf einen Tag des Kalendersteuerelements soll dieses ein Ereignis auslösen, das das gewählte Datum als Parameter übergibt. Deshalb ist ein weiteres Ereignis im Modulkopf deklariert:

Public Event DateSelected(ByVal Value As Date)

Dieses Ereignis soll per RaiseEvent aufgerufen werden, wenn auf eine der sieben Textfelder geklickt wird. Die Ereignisprozeduren für die beiden ersten Textfelder:

Private Sub txtD1_Click()
     fuClick
End Sub
Private Sub txtD2_Click()
     fuClick
End Sub

Hier passiert weiter nichts, als dass die Hilfsprozedur fuClick aufgerufen wird. Die nämlich berechnet das Datum eigenständig (s. Listing 2). Dass die Routine keine Parameterübergabe benötigt, sieht man an ihrer ersten Zeile. Die spricht das im Formular gerade aktive Steuerelement an (ActiveControl), was infolge des Klicks eben das entsprechende Textfeld ist. Ihr Wert ist der über die Abs-Funktion des Steuerelementinhalts erhaltene Tag des Monats.

Private Sub fuClick()
     Dim n As Long
     Dim nAdd As Long
     
     n = Me.ActiveControl.Value
     If (Me.CurrentRecord < 2) And (n > 20) Then nAdd = -1
     If (Me.CurrentRecord > 5) And (n < 8) Then nAdd = 1
     m_SelectedDate = DateSerial(m_Year, m_Month + nAdd, n)
     Me!LblDate.Caption = Format(m_SelectedDate, "dddd, dd.mm.yyyy")
     RaiseEvent DateSelected(m_SelectedDate)
End Sub

Listing 2: Ermitteln des Datums aus der aktiven Textbox

Das Datum kann damit auf einfache Weise berechnet werden. Es ergibt sich über DateSerial aus dem Jahr (m_Year), Monat (m_Month), plus dem Offset des in n zwischengespeicherten Tags. Allerdings führt das zu einem Fehler, wenn ein außerhalb des Monats liegender Tag angeklickt wird. Das ist zum einen dann der Fall, wenn die Position des aktiven Datensatz (CurrentRecord) kleiner ist, als 2 und der Tageswert in n größer, als 20. Dann handelt es sich um den Vormonat und in die Variable nAdd wird der Wert -1 gesetzt. Zum anderen bedeutet eine Datensatzposition von größer als 5 und ein Tageswert von kleiner als 8, dass es sich um den Folgemonat handeln muss. Dann steht in nAdd der Wert 1, der ansonsten 0 beträgt, wenn keine der beiden Bedingungen erfüllt ist. Der Wert von nAdd wird schließlich dem Monat in m_Month hinzuaddiert, wodurch DateSerial wieder das korrekte Datum berechnet.

Die Routine löst am Ende das Ereignis DateSelected aus, schreibt aber zuvor noch in ein Label, welches sich im Fußbereich des Kalenderformulars befindet, das Datum formatiert aus, damit hier ein Feedback über die Auswahl geschieht. Ein letztes Ereignis gibt es noch:

Public Event NewHeight(ByVal H As Long)

Es wird immer dann ausgelöst, wenn sich die Höhe des Kalendersteuerelements ändert. Grund dafür ist der Umstand, dass, je nach Monat, der Kalender vier bis sechs Zeilen hoch sein kann. Durch die Endlosdarstellung der Datensätze ändert sich folglich die Gesamthöhe des Formulars. Im Ereignis NewHeight übergibt die Variable H dann diese Höhe. Darauf kann das Hauptformular reagieren und für das Unterformular eine neue Ausdehnung anweisen:

Private Sub oCalendar_NewHeight(ByVal H As Long)
     Me!ctlCalendar.Height = H
End Sub

Ob die fortwährende Änderung der Höhe des Kalendersteuerelements im Hauptformular besonders chic ist, müssen Sie entscheiden. Zwar eliminieren Sie damit seinen manchmal auftauchenden grauen Hintergrund, aber im Sinne von Oberflächenergonomie ist das Flackern des Elements nicht unbedingt.

Die Höhe H für das Event wird übrigens in der Prozedur FillCalendar so berechnet:

H = Me.Section(acHeader).Height + Me.Section(acFooter).Height    
H = H + Me.Recordset.RecordCount * Me.Section(acDetail).Height

Zunächst werden Höhe des Formularkopfs (adHeader) und -fußes (acFooter) zusammengezählt. Dann wird die Höhe des Detailbereichs (acDetail) mit der Anzahl der Datensätze multipliziert und der Gesamthöhe hinzuaddiert.

Vorauswahl eines Datums

Unter Umständen möchten Sie im Kalender per Code visuell ein Datum voreinstellen. Über die Eigenschaften Month und Year des Moduls lassen sich zwar Monat und Jahr voreinstellen, nicht aber der Tag. Dafür gibt es die öffentliche Prozedur SelectDate im Formular. Auch ihr Aufruf führt zu einem Neuzeichnen des Kalenders, wie die folgenden Zeilen belegen:

Public Sub SelectDate(ByVal D As Date)
     Me.Year = VBA.Year(D)
     Me.Month = VBA.Month(D)
     m_SelectedDate = D
     FillCalendar D
End Sub

Das Jahr wird aus dem in D übergebenen Datum über die Funktion VBA.Year ermittelt, der Monat über VBA.Month. Ohne diese Bibliothekspräfixe würde VBA denken, dass die im Formularcode deklarierten Prozeduren Year und Month gemeint sind. Die Werte werden nun aber diesen Property-Prozeduren Year und Month tatsächlich zugewiesen. Die zusätzliche Formularvariable m_SelectedDate erhält außerdem dieses Datum. Schließlich wird FillCalendar wieder aufgerufen, wobei in diesem speziellen Fall im optionalen Parameter das auszuwählende Datum D übergeben wird. Deren Deklarationszeile hat nämlich in der Beispieldatenbank, abweichend von Listing 1, diese Syntax:

Private Sub FillCalendar(Optional SelDate As Variant)

Der Teil in FillCalendar, welcher dieses Datum berücksichtig:

If Not IsMissing(SelDate) Then
     If StartDate = SelDate Then x = i + 1: y = j
End If

Wird FillCalendar ohne Parameter aufgerufen, dann trifft IsMissing zu und die Bedingung wird übergangen. Andernfalls schaut die nächste Zeile darauf, ob das in SelDate übergebene Datum mit dem von StartDate übereinstimmt. Zur Erinnerung: StartDate wird in einer Schleife andauernd erhöht, bis alle Tage des Kalendermonats durchlaufen sind. Ist das passende Datum identifiziert, so speichert die Routine die Zahler i und j in den Variablen x und y zwischen. Dabei entspricht x der Nummerierung der auszuwählenden Textbox und y der Position des Datensatzes im Endlosformular. Nach Durchlaufen aller Monatstage und damit der Neuanlage der Datensätze in der Tabelle kommt es zu einem Zweig, der die richtige Textbox anhand der Koordinaten auswählt:

If Not IsMissing(SelDate) Then
     Dim ctl As Access.TextBox
     Me.Recordset.AbsolutePosition = y
     Set ctl = Me.Controls("txtD" & CStr(x))
     ctl.SetFocus
     ctl.SelStart = 1: ctl.SelLength = 2
End If

Der Datenbankzeiger wird über die Eigenschaft AbsolutePosition des Formular-Recordsets zunächst auf die Koordinate y eingestellt. Das aktiviert automatisch den entsprechenden Datensatz im Formular. Die Objektvariable ctl vom Typ Textbox wird nun auf das durch x identifizierte Steuerelement gesetzt, also eines der Textfelder txtD1 bis txtD7. SetFocus selektiert schließlich das Textfeld, wobei das erst dann sichtbar wird, nachdem eine Markierung des Textinhalts über die Eigenschaften SelStart und SelLength vorgenommen wurde.

Änderungen am Layout

Die Gestalt des Kalendersteuerelements kann rudimentär beeinflusst werden. Das betrifft die Schriftart und -größe für alle enthaltenen Steuerelemente, sowie die Hintergrundfarbe. Sie setzen dazu Werte für die Eigenschaften FontName, FontSize und BackColor, welche intern in die Member-Variablen m_Fontname, m_Fontsize und m_BackColor abgespeichert werden. Das Setzen von BackColor wird in der Eigenschaftsprozedur direkt für die Hintergrundbereiche durchgeführt:

Property Let BackColor(ByVal Value As Long)
     m_Backcolor = Value
     Me.Section(acDetail).BackColor = Value
     Me.Section(acFooter).BackColor = Value
     Me.Section(acHeader).BackColor = Value
End Property

Zum Formatieren aller weiteren Steuerelemente gibt es eine Hilfsroutine FormatControls, die alle im Formular enthaltenen anhand der Controls-Auflistung in einer For-Each-Schleife durchläuft:

Private Sub FormatControls()
     Dim ctl As Access.Control
     
     For Each ctl In Me.Controls
         Select Case ctl.ControlType
         Case acTextBox, acLabel, acCheckBox
             ctl.FontName = m_Fontname
             ctl.FontSize = m_FontSize
             ctl.BackColor = m_Backcolor
         End Select
     Next ctl
End Sub

Dabei werden nur Textboxen, Labels und Checkboxen berücksichtig, denn andere Steuerelementtypen (ControlType), wie Linien, weisen etwa eventuell die Eigenschaft FontName nicht auf.

Die Hilfsroutine wird immer dann aufgerufen, wenn Sie den Let-Prozeduren für FontName, FontSize oder BackColor neue Werte zuweisen.

Einbau des Kalenders in Hauptformulare

Sie verwenden das Kalendersteuerelement, indem Sie es als Unterformular in ein anderes Formular einsetzen. Also legen Sie ein neues Unterformular an und wählen in der Eigenschaft Herkunftsobjekt sfrmCalendar aus. Positionieren Sie es nach Belieben. Die Abmessungen allerdings müssen etwa 5 cm in der Breite und 5,6 cm in der Höhe betragen. Mehr müssen Sie zunächst nicht tun. Wenn Sie aber auf die Ereignisse des Steuerelements reagieren oder das Layout beeinflussen möchten, so legen Sie besser eine Objektvariable für das Element an, der im Ereignis beim Laden des Hauptformulars der Kalender zugewiesen wird.

Wir möchten hier zusätzlich demonstrieren, wie der Umgang mit zwei Kalenderelementen aussieht. Im Entwurf sieht das aus, wie in Bild 9. Das linke Kalenderunterformular zeigt den erwartungsgemäßen Inhalt. Das rechte mit den identischen Einstellungen gibt nur einen Platzhaltertext an. Das liegt daran, weil Access ein Unterformular grundsätzlich editierbar darstellt. Da ein Bearbeiten nur an einer Stelle geschehen kann, zeigt sich dessen Design auch nur in einem Unterformularsteuerelement.

Zwei Kalender-Unterformulare befinden sich im Hauptformular neben einem Textfeld zum Loggen der Ereignisse

Bild 9: Zwei Kalender-Unterformulare befinden sich im Hauptformular neben einem Textfeld zum Loggen der Ereignisse

Über den Kalendern sind Schaltflächen untergebracht, die zeigen, wie per VBA zu einem bestimmten Datum in den Kalendern gesprungen werden kann. Das Textfeld ganz rechts loggt zur Kontrolle alle Ereignisse beider Kalender.

Beim Laden des Hauptformulars stellt die Ereignisprozedur einige Eigenschaften der Kalender ein, wobei diese zuerst zwei Objektvariablen oCalendar1 und oCalendar2 WithEvents zugewiesen werden:

Private WithEvents oCalendar1 As Form_sfrmCalendar
Private WithEvents oCalendar2 As Form_sfrmCalendar
Private Sub Form_Load()
     Set oCalendar1 = Me!ctlCalendar1.Form
     Set oCalendar2 = Me!ctlCalendar2.Form

In zwei With-Blöcken mit Bezug auf diese Objektvariablen nehmen Sie die gewünschten Einstellungen der Kalender vor:

     With oCalendar1
         .Table = "tblCalendar"
         .FontName = "Arial"
         .FontSize = 10
         .Year = 2017
         .Month = 6
     End With
     With oCalendar2
         .Table = "tblCalendar2"
         .FontName = "Courier New"
         .FontSize = 10
         .Year = 2018
         .Month = 7
         .BackColor = RGB(200, 208, 255)
     End With
End Sub

Eine Voraussetzung, damit das Ganze funktioniert, ist, dass es die beiden identischen Tabellen tblCalendar und tblCalendar2 gibt, die in der Eigenschaft Table angegeben werden. Beiden Kalendern werden unterschiedliche Monate des Jahres 2018 zugewiesen und die Schriftfarben, wie Hintergründe, sollen ebenfalls differieren. Zur Laufzeit zeigt sich das Formular frmCalendar schließlich, wie in Bild 10.

Das finale Testformular zeigt zwei Kalender auf Basis des sfrmCalendar mit unterschiedlichem Layout an

Bild 10: Das finale Testformular zeigt zwei Kalender auf Basis des sfrmCalendar mit unterschiedlichem Layout an

Hier sind bereits die beiden Schaltflächen oben betätigt worden, was zur Navigation zu den aufgedruckten Jahren und Monaten führt und den gewünschten Tag markiert. Das Datum steht dann jeweils im Label unten. Der linken Schaltfläche etwa ist diese Zeile hinterlegt:

oCalendar1.SelectDate "03.04.2019"

Obwohl die Prozedur SelectDate eigentlich einen Date-Typ als Parameter erwartet, kann man ebenso einen String zuweisen, weil VBA in diesem Fall intern eine Konvertierung vornimmt.

Das Navigieren zu anderen Monaten und Jahren, das Anklicken eines Datums, führen zu Ereignissen, die alle über die Prozedur AddToLog in der Textbox rechts geloggt werden, etwa

Private Sub oCalendar1_DateSelected(ByVal Value As Date)
     AddToLog "C1, Datum ausgewählt: " & Value
End Sub
Sub AddToLog(ByVal txt As String)
     Me!txtInfo.Value = txt & vbCrLf & Me!txtInfo.Value
End Sub

C1 betrifft dabei den linken, C2 den rechten Kalender. Sie sehen auch, dass die Höhe der Kalenderunterformulare unterschiedlich ist. Verantwortlich dafür ist die Reaktion auf das NewHeight-Ereignis:

Private Sub oCalendar1_NewHeight(ByVal H As Long)
     Me!ctlCalendar1.Height = H
     AddToLog "C1, Neue Höhe: " & H
End Sub

Vielleicht fällt Ihnen noch auf, dass der Mauszeiger über den Datumsfeldern in ein Hand-Symbol verwandelt. Das liegt daran, dass für alle Textboxen die Eigenschaft Ist Hyperlink aktiviert ist und die Eigenschaft Als Hyperlink anzeigen auf Immer steht. Deshalb sind die Datums-Strings auch unterstrichen dargestellt.

Möchten Sie das Kalendersteuerelement in eigenen Datenbanken verwenden, so importieren Sie lediglich das Formular sfrmCalendar und die Tabelle tblCalendar aus der Beispieldatenbank. Zusätzliche Verweise sind nicht zu setzen.

Bitte geben Sie die Zeichenfolge in das nachfolgende Textfeld ein

Die mit einem * markierten Felder sind Pflichtfelder.

Ich habe die Datenschutzbestimmungen zur Kenntnis genommen.