2017-01-28

Beschreibung

tl;dr

Dies ist einfaches ein Qt-Beispielprojekt in C++ und QML zum:
  • Herunterladen von Kalenderdaten von calDAV-Servern wie ownCloud, Nextcloud oder anderen
    (z.B. https://server.tld/owncloud/remote.php/dav/calendars/username/calendarname/)
  • Herunterladen von Kalenderdaten im iCalendar-Dateiformat über HTTP/HTTPS
    (z.B. https://server.tld/folder/filename.ics)
  • Laden von Kalenderdaten aus lokalen Dateien
    (z.B. file:///../../subdirectory/filename.ics)
  • das iCalendar-Format verarbeiten, einschließlich der gebräuchlichsten Wiederholungsregeln für regelmäßige Ereignisse / Terminserien
  • mehrere Kalenderquellen verwalten; einschließlich Speichern und Laden der Einstellungen in einer INI-Datei
  • Ermitteln einer Liste von Terminen für ein bestimmtes Datum
  • Editieren, Hinzufügen und Löschen von Terminen
  • Termine in einer GUI anzeigen

Download: iCalendar_example_code.zip (987 KB)

Screenshot

Screenshot
Linke Spalte: Liste von Kalendern, Mitte: Liste von Terminen für diesen Tag, rechts: grafischer Kalender

Lizenz und Hinweise

Das Beispielprojekt und die zugehörigen Dateien stehen unter CC BY-NC-SA 3.0.
Es wird die SimpleCrypt-Bibliothek von Andre Somers verwendet, © 2001.
Bereitstellung ohne Gewähr.

Wie dieses Projekt entstand

Für ein paar Webseiten (vgl. cccfr.de und roboterclub-freiburg.de) hatte ich kürzlich Erweiterungen entwickelt, um anstehende Ereignisse auf einem Kalender anzuzeigen.
Dabei hatte ich mich erstmals mit dem iCalendar-Dateiformat zu tun. Anders, als der Name vermuten lässt, ist dies nichts Apple-Proprietäres, sondern ein Standard zum Austausch von Kalenderdaten wie Ereignisse und Terminserien. Beispielsweise Outlook, Thunderbird, Google Calendar, Apple Calendar und Lotus Notes unterstützen das Format.

Und da ich zeitgleich Nextcloud auf einem meiner Server für private Zwecke installierte, kam mir die Idee, doch mal einen Kalender in meinen Star Trek Küchen-Computer zu integrieren, der sich mit dem Nextcloud-Server synchronisiert und mir immer meine anstehenden Termine anzeigt.
Neben dem wohl-bekannten Dateiverwaltungs-Feature bietet Nextcloud nämlich auch einen calDAV-Dienst, der es externen Anwendungen ermöglicht über HTTP/HTTPS Kalenderdaten im iCalendar-Format herunterzuladen / hochzuladen.

Also war der Plan, eine leicht zu integrierende C++-Klasse oder API zur Verbindung mit meinem Nextcloud-Server zu nehmen, anstehende Termine abzurufen und sie in der QML-GUI anzuzeigen.
Leider gab es (zu diesem Zeitpunkt) keine brauchbaren Code-Beispiele womit ich hätte anfangen können. Das Beste was ich finden konnte waren Libical (zu komplex für mein kleines Projekt) und rrule.js, eine JavaScript-Bibliothek um Wiederholungsregeln von Terminserien zu handhaben (aber nicht um Kalenderdaten über calDAV abzurufen).
Ich kam zum Schluss, dass alle Funktionen von Libical zu verstehen und in ein Projekt zu integrieren vermutlich deutlich länger dauern würde, als einfach kurzerhand eine schlanke Lösung von Null auf selbst zu entwickeln.
Eine sehr nützliche Hilfe dabei war diese Anleitung auf sabre.io und - natürlich - die offizielle iCalendar-Spezifikation.

Anwendung

Kalender verwalten

Wenn man das Projekt startet, bekommt man eine QML-Anwendung zu sehen, die anfangs mangels angegebener Kalenderquellen noch recht leer ist.
Also flugs auf den Knopf unten links "Add calendar" geklickt und ein Dialog zur Einrichtung eines server- oder dateibasierten Kalenders öffnet sich:
Add calendar dialog

Type ICS für lokale oder auf einem Server liegende Dateien im iCalendar-Format oder calDAV für server-basierte Kalender (z.B. ownCloud oder Nextcloud)
Name Anzuzeigender Name des Kalenders (wird bei calDAV vom Server überschrieben)
URL Im Falle von ICS der Pfad zu einer lokalen Datei wie file:///../../filename.ics oder file:///c:/directory/filename.ics oder eine Datei auf einem Server im Schema https://server.tld/folder/filename.ics.
Wenn calDAV ausgewählt ist, eine Kalender-URL wie https://server.tld/owncloud/remote.php/dav/calendars/username/calendarname/. Bitte das abschließende /-Zeichen nicht vergessen.
Color Auf dieses Rechteck klicken um einen Farbauswahldialog zu öffnen und dem Kalender eine Farbe zuweisen.
Diese Einstellung kann durch Kalenderinformationen überschrieben werden, wenn das CALENDAR_OVERWRITE_COLOR-Compilerflag in CalendarClient.h als 1 definiert ist.
Username Benutzername zum Anmelden am calDAV-Dienst (wird nur für calDAV-Kalender benötigt)
Password Passwort zum Anmelden am calDAV-Dienst (wird nur für calDAV-Kalender benötigt)

Für einen ersten Test wählen wir den ICS-Typ, geben https://cccfr.de/calendar.ics im URL-Feld ein, nennen den Kalender "CCCFr" und klicken auf OK.
Die Anwendung sollte nun einen Kalender in der linken Spalte anzeigen:
Screenshot
Diese soeben erstellte Kalender zeigt uns die Termine des Chaos Computer Club Freiburg an.

Wenn es nicht funktioniert, dann sicherstellen, dass die DLLs libeay32.dll und ssleay32.dll im gleichen Verzeichnis liegen wie die CalDAV_Client.exe und die Kalendereinstellungen durch Klick auf den "edit"-Knopf überprüfen.

Nun probieren wir das Gleiche mit einem calDAV-Kalender.
Es gibt dankenswerterweise einen öffentlichen ownCloud-Server unter nimmerland.de der uns hier als Spielwiese dienen wird. Nochmals auf "Add calendar" klicken und Folgendes eingeben:
Screenshot

Type calDAV
Name Nimmerland
URL https://basic.nimmerland.de/remote.php/dav/calendars/demo.user/pers%c3%b6nlich1/
Username demo.user
Password berlin

Nach Bestätigen mit OK sollte der Nimmerland-Kalender der Anwendung hinzugefügt werden.

Termine verwalten

Auf den "Add Event"-Knopf des Nimmerland-Kalenders klicken und ein Dialogfester wird geöffnet. Hier ein paar Beispieldaten eingeben und auf "Save" klicken:
Screenshot
Die Anwendung wird nun den Kalender aktualisieren und den soeben erstellten Termin anzeigen:
Screenshot

Auch mal ausprobieren: Editieren und Löschen von Terminen (geht nur mit calDAV-Kalendern).

Terminserien (Wiederholungsregeln)

Wenn eine Wiederholungsregel angegeben wurde wie oben abgebildet, wird die Kalenderansicht nicht nur einen einzelnen Termin anzeigen, sondern eine Serie von Terminen.
Die Wiederholungsregeln müssen dazu der iCalendar-RRULE-Spezifikation entsprechen. Der gegenwärtig implementierte Satz von FREQ-Werten beinhaltet MONTHLY, WEEKLY und YEARLY.
Ein Angabe FREQ=WEEKLY;INTERVAL=2 wird also einen Termin erstellen, der jede zweite Woche wiederholt wird.

Weitere Beispiele:
FREQ=YEARLY;INTERVAL=1 = jedes Jahr an diesem Tag (z.B. für Geburtstage).
FREQ=MONTHLY;BYDAY=FR,2MO,-1SA;INTERVAL=1 = jeden Monat an jedem Freitag, jedem 2. Montag und jeden letzten Samstag.
FREQ=WEEKLY;BYDAY=WE;INTERVAL=2;COUNT=10 = jede 2. Woche immer Mittwochs - aber nicht mehr als 10 mal.

Bitte beachten, dass die unterstützten RRULE-Regeln auch vom jeweiligen Server abhängen.
ownCloud-Server beispielsweise unterstützen derzeit (Januar 2017) anscheinend keine RRULE-Regeln mit negativen Werten wie FREQ=MONTHLY;BYDAY=-1FR;INTERVAL=1 (jeden letzten Freitag des Monats).

Exdates

Exdates sind Ausschlussdaten - Daten, an denen eine Terminserie nicht stattfindet, bzw. abgesagt ist. Exdates müssen der iCalendar-EXDATE-Spezifikation entsprechen.
Um dieses Feature zu nutzen, eine Liste von Komma-getrennten Daten angeben, an denen die Terminserie nicht stattfinden soll.
In unserem Beispiel oben wurde ein Termin definiert, der sich alle 2 Wochen wiederholt und daher auch am 14. Februar stattfinden müsste. Aber da 20170214T000000Z im "Canceled on:"-Feld angegeben wurde, wird der Termin nicht angezeigt:
Screenshot
In der Liste der Termine für diesen Tag steht der Termin durchgestrichen und wird als CANCELED markiert.

Kompilierung

Voraussetzungen

  • Qt 5.5 oder höher (vielleicht geht es auch mit älteren Versionen, aber das habe ich nicht getestet)
  • Qt Creator
  • C++-Compiler (wird für gewöhnlich mit Qt mitinstalliert)

Kompilierung unter Windows

  1. Das Beispielprojekt von hier (987 KB) herunterladen und entpacken.
  2. Die CalDAV_Client.pro-Datei öffnen und die Build-Konfiguration auswählen.
  3. qmake ausführen und das Projekt and build erstellen.
  4. Das Ausgabeverzeichnis öffnen wo die CalDAV_Client.exe liegt und die DLLs libeay32.dll und ssleay32.dll hierher kopieren (diese liegen dem Projektarchiv bei und werden für HTTPS-Verbindungen benötigt).

Über den Code

Struktur

Das zentrale Software-Element ist die CalendarManager-Klasse, welche eine Liste von CalendarClient-Objekten verwaltet.
CalendarClient ist die abstrakte Basisklasse für die Klassen CalendarClient_CalDAV (zum Verbinden mit calDAV-Servern) und CalendarClient_ICS (zum Handhaben von iCalendar-Dateien).
Software architecture class diagram
Während die abgeleiteten Klassen auf das Einlesen von verschiedenen Kalenderquellen spezialisiert sind, beinhaltet die CalendarClient-Klasse die Funktionalität zum Verarbeiten des iCalendar-Datenformats (Beispieldatei hier).

Verhalten

In main.c wird eine Instanz von CalendarManager erstellt und als Property für den QML-Kontext registriert:
main():
engine.rootContext()->setContextProperty("calendarManager", &calendarManager);

In der QML-GUI existiert ein grafisches Kalender-Element. Wann immer Jahr oder Monat dieses Elements geändert werden, wird das calendarManager-Objekt aktualisiert um Termine für diesen Monat zu laden:
main.qml:
Calendar {
  id: calendar
  width: parent.width
  height: parent.height
  frameVisible: true
  weekNumbersVisible: true
  selectedDate: new Date()
  focus: true
  style: calstyle
  onVisibleMonthChanged: {
    calendarManager.date = new Date(calendar.visibleYear, calendar.visibleMonth, 1)
  }
  onVisibleYearChanged: {
    calendarManager.date = new Date(calendar.visibleYear, calendar.visibleMonth, 1)
  }
}
Dies ermöglicht es dem CalendarManager seine CalendarClient_CalDAV-Objekte anzuweisen, nicht einfach eine riesige Datei mit sämtlichen Termine vom Anbeginn der Zeit bis zum Ende des Universums zu downloaden, sondern gezielt und begrenzt auf einen bestimmten Monat:
in CalendarClient_CalDAV_SendRequest.cpp, sendRequestChanges():
<C:time-range start=\"" + QString::number(m_Year) + monthString + "01T000000Z\" end=\"" + QString::number(m_Year) + monthString + lastDayString + "T235959Z\"/>\r\n"
Wenn nun das ausgewählte Datum des QML-Kalenders geändert wird, wird die QML-invokable Funktion eventsForDate() des CalendarManager aufgerufen, welche eine Liste von Terminen für dieses Datum zurückgibt.

Sonstiges Wissenswertes

Synchronisations-Timer

Zum Zwecke der regelmäßigen Synchronisation hat jeder CalendarClient einen eigenen QTimer (m_SynchronizationTimer) welcher den internen Zustandsautomaten veranlasst die Kalenderdaten neu zu laden. Wenn ein früheres Neuladen erforderlich sein sollte, weil sich Jahr/Monat geändert haben oder Änderungen am Kalender oder einem Termin gemacht wurden, ruft der CalendarManager startSynchronization() auf.
Standardmäßig ist das Synchronisierungsintervall auf 60 Sekunden im Konstruktor von CalendarClient eingestellt.

Synchronisierungs-Zustand

Neben dem internen Zustandsautomaten, welcher die Übergänge und Aktivitäten in Abhängigkeit des CalendarClient-Typ (calDAV oder ICS) handhabt, gibt es ein public syncState-Property welches den Synchronisierungszustand repräsentiert und folgende Werte haben kann:
  • E_STATE_IDLE - Kalender wartet auf nächste Synchronisation
  • E_STATE_BUSY - Kalender synchronisiert gerade
  • E_STATE_ERROR - Kalender hat einen Fehler
Wenn letzterer Zustand der Fall sein sollte, hat der Kalender entweder eine falsche Konfiguration (z.B. falsche URL) oder es gibt ein temporäres Verbindungsproblem. Man kann versuchen diese Situation aufzulösen, indem man CalendarClient::recover() aufruft, was den internen Zustandsautomaten zurücksetzt und eine Re-Synchronisation initiiert.
Auf der GUI kann der Anwender dies durch Klicken auf das Error-Symbol des Kalenders tun.

Debug-Ausgabe

Bei etwaigen Fehlern kann die Debugausgabe durch Setzen der DEBUG_****-Flags auf 1 in den Code-Dateien aktiviert werden.

Passwortverschlüsselung

Alle Kalendereinstellungen (URL, Benutzername, Passwort, etc.) werden vom CalendarManager in einer INI-Datei gespeichert, deren Pfad als Konstruktor-Parameter angegeben wird:
in main():
CalendarManager calendarManager(QString(app.applicationDirPath()+"/CalendarManager.ini"));
Um zu vermeiden, dass irgendwelche Dateien mit Passwörtern im Klartext auf der Festplatte herumliegen, verschlüsselt CalendarManager die calDAV-Passwörter mit SimpleCrypt.
Der Entschlüsselungs-Key ist allerdings im Code als Definition hinterlegt (PWD_CRYPT in CalendarManager.c) und bietet daher nur minimalen Schutz und sollte für sensible Informationen durch etwas Stärkeres ersetzt werden.

Links und Dateien

Download Qt-Beispielprojekt: iCalendar_example_code.zip (987 KB)

Wie man einen calDAV-Client programmiert
SimpleCrypt-Webseite
IETF iCalendar-Spezifikation
iCalendar-RRULE-Spezifikation
iCalendar-EXDATE-Spezifikation