2017-01-28

Description

tl;dr

This is a simple Qt code example, written in C++ and QML to:
  • retrieve calendar data from calDAV-based servers like ownCloud, Nextcloud or others
    (i.e. https://server.tld/owncloud/remote.php/dav/calendars/username/calendarname/)
  • retrieve calendar date from remote iCalendar files
    (i.e. https://server.tld/folder/filename.ics)
  • retrieve calendar data from local iCalendar files
    (i.e. file:///../../subdirectory/filename.ics)
  • parse the iCalendar format, including some of the most common recurrence rules for regular appointments
  • manage multiple calendars; including saving and loading calendar settings to/from an INI file
  • generate a list of events for a specific date
  • edit, add and delete calendar events
  • display calendar events in a GUI

Download: iCalendar_example_code.zip (987 KB)

Screenshot

Screenshot
Left colum: list of calendars, middle: list of events for selected date, right: calendar

Licence and disclaimer

The example project and the corresponding code files are licensed under CC BY-NC-SA 3.0.
It uses the SimpleCrypt library from Andre Somers, © 2001.
This project comes with absolutely no warranty.

Project genesis

Recently, I wrote some calendar pages for websites to show upcoming events (see cccfr.de and roboterclub-freiburg.de).
During this I got to know the iCalendar file format for the first time. Unlike the name suggests, the iCalendar format is not Apple-proprietary, but a widely supported standard for exchanging calendar information such as events or regular appointments. For instance Outlook, Thunderbird, Google Calendar, Apple Calendar and Lotus Notes support the iCalendar format.

And since I also had to do with installing Nextcloud on one of my servers at that time, I thought it would be a very nice thing to integrate a calendar into my Star Trek Computer GUI which I have developed some time ago and synchronize it with the Nextcloud calendar server to display my upcoming events.
Among the better known file storage feature, Nextcloud also provides a calDAV service which allows clients to connect via HTTP/HTTPS and download or upload calendar information in iCalendar format.

So the plan was to have an easy to integrate C++ class or API to connect with my Nextcloud server, get my upcoming events and display them in a QML GUI.
Unfortunately, there weren't any suitable code examples to start with. The best I could find was Libical (too complex for my little project) and rrule.js, a JavaScript library to handle recurrence rules (but not to connect with calDAV servers).
I came to the conclusion that understanding all features of Libical and integrating it into a project would certainly take much more time than to develop a more lightweight solution from scratch by myself.
A very helpful resource in doing so was this guidance on sabre.io and - of course - the official iCalendar specification.

Application usage

Managing calendars

When running the project, you'll see a QML application which however is still quite empty since there aren't any calendar sources configured yet.
So click the button if the bottom left corner "Add calendar" and a dialog to specify a calDAV or file based calendar opens:
Add calendar dialog

Type ICS for local or remote files in iCalendar format or calDAV for calDAV based servers (i.e. ownCloud or Nextcloud)
Name Name to display for this calendar (will be overwritten by calendar information if calDAV is used)
URL In case of ICS type a local file path like file:///../../filename.ics or file:///c:/directory/filename.ics or a remote file in the scheme https://server.tld/folder/filename.ics.
If calDAV is selected, a calendar URL like https://server.tld/owncloud/remote.php/dav/calendars/username/calendarname/ needs to be entered. Note the trailing / character.
Color Click this rectangle to open a color picker dialog and chose a color to assign to this calendar.
Note that this setting might be overwritten by calendar information if the CALENDAR_OVERWRITE_COLOR flag in CalendarClient.h is defined to 1
Username Username to access calDAV server (irrelevant to ICS type calendars)
Password Password to access calDAV server (irrelevant to ICS type calendars)

For a first test select ICS type, enter https://cccfr.de/calendar.ics in the URL field, name the calendar "CCCFr" and click OK.
The application should now display a calendar in the left column:
Screenshot
The just created calendar will show you events for the Chaos Computer Club Freiburg.

If it doesn't sync with the calendar file, then make sure to have the DLL files libeay32.dll and ssleay32.dll in the same directory where the CalDAV_Client.exe is located and check the calendar URL by clicking the "edit" button.

Now let's try the same with a calDAV based calendar.
There is an example ownCloud server at nimmerland.de (English: "neverland") we will use for this. Click the "Add calendar" button again and enter the following:
Screenshot

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

After clicking OK, the Nimmerland calendar will be added to the application.

Managing events

Click the "Add Event" button of the Nimmerland calendar. An event options dialog will open. Enter some data and click "Save":
Screenshot
The application will update the calendar and display the just created event in the middle column:
Screenshot

You might also want to try editing or deleting events (this is supported for calDAV calendars only).

Event recurrences

If a recurrence rule has been specified as like above you will notice, that the calendar not just displays a single event but a series of repeated appointments.
The recurrence rules must follow the iCalendar RRULE specification. The currently supported set of FREQ comprises MONTHLY, WEEKLY and YEARLY occurrences.
So a setting of FREQ=WEEKLY;INTERVAL=2 will generate a event which repeats every 2nd week.

Others examples are:
FREQ=YEARLY;INTERVAL=1 = every year on this date (i.e. for birthdays).
FREQ=MONTHLY;BYDAY=FR,2MO,-1SA;INTERVAL=1 = every month on every Friday, every 2nd Monday and the last Saturday.
FREQ=WEEKLY;BYDAY=WE;INTERVAL=2;COUNT=10 = every 2nd week on every Wednesday - but not more than 10 times.

Please note that the set of supported RRULES also depends on the server implementation.
For instance ownCloud apparently does not support RRULES with negative values like FREQ=MONTHLY;BYDAY=-1FR;INTERVAL=1 (every last Friday of a month) at the time this project has been written (January 2017).

Exdates

Exdates are excluded dates - dates when a regularly appointment is canceled. Specifying an exdate must follow the iCalendar EXDATE specification.
To use this feature, enter a comma separated list of dates when the event is canceled.
In our example above we had entered a event which repeats every 2nd week and thus should also appear on February, 14th. But since we entered 20170214T000000Z into the "Canceled on:" field, it won't be shown:
Screenshot
In the list of events for this date the event is striked out and titled as CANCELED.

Build the code

Preconditions

  • Qt 5.5 or higher (maybe it will work with older versions too, but I never tested that)
  • Qt Creator
  • C++ compiler (usually installed together with Qt)

Building on Windows

  1. Download the example project from here (987 KB) and extract it anywhere on your hard drive.
  2. Open the CalDAV_Client.pro file with Qt Creator and configure the build configuration.
  3. Run qmake and build the project.
  4. Open the build directory where the CalDAV_Client.exe is located and paste the DLLs libeay32.dll and ssleay32.dll here (those are needed for HTTPS connections, you'll find them in the download archive).

About the code

Structure

The central software element is the CalendarManager class which manages a list of CalendarClient instances.
CalendarClient is the abstract base class for the classes CalendarClient_CalDAV (which can connect with calDAV servers) and CalendarClient_ICS (which can handle local or remote iCalendar files).
Software architecture class diagram
While the child classes are specialized to obtain calendar information from different sources, the CalendarClient class includes the functionality to parse the iCalendar data format (example file here).

Behavior

In main.c an instance of CalendarManager is created and registered as property for the QML context:
main():
engine.rootContext()->setContextProperty("calendarManager", &calendarManager);

The QML contains a Calendar item. Whenever the displayed month or year of this item changes, the calendarManager context property is updated to load events for that month:
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)
  }
}
This allows the CalendarManager to instruct its CalendarClient_CalDAV objects to not just download a vast file of all events from the beginning of time to the end of the universe but to limit it to a specific month:
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"
If now the selected date of the QML calendar changes, the QML-invokable function eventsForDate() of the CalendarManager is called which returns a list of events for that date.

Other things to know about

Synchronization timer

For the purpose of regular synchronization, every CalendarClient owns a QTimer (m_SynchronizationTimer) which triggers the internal state machine to reload the calendar information. If an earlier reload is required due to changed year/month or when the calendar or an event has changed, the CalendarManager calls startSynchronization().
By default, the synchronization interval is set to 60 seconds in the constructor of CalendarClient.

Synchronization state

Besides the internal state machine, which handles the transitions and activities depending on the type of CalendarClient (calDAV or ICS), there is also a public syncState property which represents the synchronization state with the calendar source and which can have the following values:
  • E_STATE_IDLE - calendar is waiting for next synchronization
  • E_STATE_BUSY - calendar is currently synchronizing
  • E_STATE_ERROR - calendar has encountered an error condition
If the later state applies, either the calendar has not been configured correctly (i.e. wrong URL) or there is a temporary problem. You can try to resolve this by calling CalendarClient::recover() which resets the internal state machine and initiates a re-synchronization.
On the GUI, the user can do so by clicking on the error icon of the calendar.

Debugging output

If you encounter a programming error, you can activate debugging output by setting the DEBUG_**** flags in the source files to 1.

Password encryption

All calendar settings (URL, username, password, etc.) are stored by the CalendarManager in a settings file which is specified as constructor parameter:
in main():
CalendarManager calendarManager(QString(app.applicationDirPath()+"/CalendarManager.ini"));
To avoid having files lying around your hard disk with plain-text passwords, the CalendarManager encryps the passowrds using SimpleCrypt.
The decryption key however is stored as a compiler define (PWD_CRYPT in CalendarManager.c) and thus provides just a very basic protection and should be replaced for sensible information by something stronger.

Links and files

Download Qt example project: iCalendar_example_code.zip (987 KB)

How to build a calDAV client
SimpleCrypt website
IETF iCalendar specification
iCalendar RRULE specification
iCalendar EXDATE specification