Ich bin ein großer Fan des Programms MyPhoneExplorer von Franz Josef Wechselberger und nutze sein tolles Programm seit vielen Jahren (und ja: ich habe auch schon mehrfach eine Spende überwiesen, weil das Programm es wert ist!)

Wer MyPhoneExplorer (MPE) nicht kennt: Es handelt sich um ein Tool, das es u.a. ermöglicht die Adressbuch-, Kalender-, Notiz- und Bilder-Daten auf dem Smartphone per USB, WLAN oder Bluetooth mit dem PC zu verwalten und zu synchronisieren…und vieles mehr 🙂

Meine Termine verwalte ich mittlerweile nur noch per Smartphone, dass ich eigentlich fast immer dabei habe. Wenn ich vor dem PC sitze, fehlt mir aber manchmal ein Art Vorschau der demnächst anstehenden Termine oder Geburtstage, da ich den MPE nicht ständig geöffnet habe, sondern dieser minimiert im Systemtray liegt.

Deshalb habe ich mir in C# ein solches kleines Helferlein programmiert.

Ansicht der Terminvorschau

In dem Menü „Settings“ kann man die Anzahl an Tagen angeben, für die anstehende Termine angezeigt werden sollen. Dort wählt man auch die Kalenderdatei aus. Beim MPE heißt diese Datei (derzeit) Organizer.dat und liegt typischerweise im Verzeichnis %APPDATA%\MyPhoneExplorer und dann im Unterordner für das betreffende Smartphone.

Diese Datei Organizer.dat ist (wie es scheint) eine Kalenderdatei im Format iCalendar, welche mit jedem Texteditor (z.B. Notepad++) gelesen werden kann. Das Format iCalendar ist (nur) auf den ersten Blick recht einfach strukturiert, wird aber sehr schnell komplex, wenn es um nicht ganz so einfache Termine geht, wie zum Beispiel Serientermine mit Wiederholungsregeln. Hierzu zwei kleine Beispiele für Termine:

BEGIN:VEVENT
SUMMARY:Abholung Brille
DESCRIPTION;CHARSET=UTF-8:309,50€
DTSTART:20200708T083000Z
DTEND:20200708T093000Z
CLASS:DEFAULT
TRANSP:0
X-COLOR:FF008000
X-CALENDAR:Phone@com.android.huawei.phone/NULL;Phone
X-IRMC-LUID:000010000673
END:VEVENT
BEGIN:VEVENT
SUMMARY:Geburtstag BRD
DESCRIPTION:1949
DTSTART:19490523
DTEND:19490523
RRULE:YM1 23 #0
CLASS:DEFAULT
TRANSP:0
X-CALENDAR:Phone@com.android.huawei.phone/NULL;Phone
X-IRMC-LUID:000010000033
END:VEVENT

Jeder Termin beginnt mit dem Eintrag „BEGIN:VEVENT“ und endet mit dem Eintrag „END:VEVENT“. Dazwischen befindet sich die Beschreibung des Termins. So enthält z.B. die Zeile „SUMMARY“ den Betreff des Termins. Allerdings kann hier hinter SUMMARY noch wie bei der Zeile „DESCRIPTION“ noch ein Zeichensatz angegeben sein, wenn besondere Zeichen wie Umlaute oder hier das Euro-Zeichen verwendet werden. Der eigentliche Inhalt beginnt immer hinter dem Doppelpunkt.

Die Zeilen „DTSTART“ und DTEND geben den Start und das Ende des Termins an. Zuerst folgt hinter dem Doppelpunkt das Datum und dann hinter dem T kommt die Uhrzeit. Bei ganztägigen Terminen entfällt die Uhrzeit. Die Zeitangaben sind alle in UTC, wobei dies durch das „Z“ am Ende ausgedrückt wird.

Datums- und Zeitangaben sind im Hinblick auf die Zeitzonen u.U auch ein größeres Problem beim Ex- und Import von Kalenderdateien in verschiedenen Programmen.

Im rechten Beispiel findet sich nun solch eine Zeile „RRULE“ (Recurrence Rule = Wiederholungsregel). Diese Regeln können m.E. ziemlich komplex sein 🙂

Ich hatte nun nicht das Bestreben, eine Klasse zu implementieren, die beliebige iCalendar-Dateien lesen kann. Ich habe mir lediglich die bereits o.g. Datei Organizer.dat vom MPE angesehen und für mein Vorschauprogramm wollte ich lediglich das Startdatum und den Betreff haben, um beides anzeigen zu können.

Anzeigen der Vorschau-Termine

Hierzu habe ich mir eine Klasse namens PreviewEvent überlegt, in der ich jeden einzelnen Termin für die Anzeige in der verwendeten Listbox kapsele. Die Implementation hierzu ist folgende:

// Termin für die Vorschau in der Listbox
public class PreviewEvent: IComparable
{
    public DateTime Date { private set; get; }
    public string Description { private set; get; }

    public PreviewEvent(DateTime date, string description)
    {
        Date = date;
        Description = description;
    }

    // Interface IComparable
    // um die Termine sortieren zu können anhand Datum
    public int CompareTo(object obj)
    {
        int rc = 0;
        PreviewEvent pe = (PreviewEvent)obj;
        if (this.Date < pe.Date) rc = -1;
        if (this.Date > pe.Date) rc = 1;
        return rc;
    }
}

Die Klasse PreviewEvent implementiert das Interface IComparable, damit ich die Objekte in der generischen Liste List<PreviewEvent> previewEvents mittels Sort()-Methode nach Datum sortieren kann. Die Ausgabe erfolgt dann in der Listbox durch Iterieren dieser List<PreviewEvent> previewEvents durch Erzeugung eines Strings aus den beiden Properties Date und Description.

Lesen der iCalendar-Datei

Für das Lesen der iCalendar-Datei vom MPE habe ich mir ebenfalls eine Klasse namens OrganizerReader überlegt. In dieser Klasse wird nun die angegebene Datei gelesen und es werden einige mögliche Fehler per try…catch gefangen. Jeder Termin beginnt ja wie oben dargestellt mit der Zeile „BEGIN:VEVENT“ und endet mit „END:VEVENT“.

Alle diese Textzeilen werden in einer String-Liste List<string> eventdescription gespeichert, so dass prinzipiell für eventuelle Erweiterungen bereits alle verfügbaren Informationen zu einem Termin vorhanden sind.

public class OrganizerReader
{
    // Zu lesende Kalenderdatei
    public string Filename { private set; get; }

    // Zeitspanne, für die die Termine im Voraus
    // angezeigt werden sollen
    public TimeSpan Timespan { private set; get; }

    // Liste mit Termin-Objekten
    public List<OrganizerEvent> organizerEvents = new List<OrganizerEvent>();

    public OrganizerReader(string filename, TimeSpan timespan)
    {
        Filename = filename;
        Timespan = timespan;

        try
        {
            readOrganizerData();
        }
        catch (IOException ex)
        {
            throw ex;
        }

        // Sortieren nach Datum per IComparable
        organizerEvents.Sort();
    }

    // Lesen der Kalenderdatei und Erstellen der Termin-Objekte
    private void readOrganizerData()
    {
        using (StreamReader sw = new StreamReader(Filename))
        {
            if (!sw.EndOfStream)
            {
                // Erste Zeile sollte BEGIN:VCALENDAR sein!
                string line = sw.ReadLine();
                if (line.ToUpper() != "BEGIN:VCALENDAR")
                {
                    throw new IOException("Fileformat seems not to be a calenderfile");
                }
            }

            // Lesen bis Dateiende, Zeile für Zeile
            while (!sw.EndOfStream)
            {
                string line = sw.ReadLine();

                // Termine beginnen mit BEGIN:VEVENT
                if (line.ToUpper() == "BEGIN:VEVENT")
                {
                    // String-Liste erstellen für Termin-Objekt
                    // und die Zeilen bis END:VEVENT darin speichern
                    List<string> eventdescription = new List<string>();
                    eventdescription.Add(line);

                    while((line = sw.ReadLine())!="END:VEVENT")
                    {
                        eventdescription.Add(line);
                    }
                    eventdescription.Add(line);

                    // Termin-Objekt erstekllen und die Liste mitliefern
                    OrganizerEvent oe = new OrganizerEvent(eventdescription);

                    // Termin-Objekt in Liste aller Termin-Objekte einhängen
                    organizerEvents.Add(oe);
                }
            }
        }
    }
}

Das Lesen der Datei erfolgt in einer Methode namens readOrganizerData(). Innerhalb der Lese-Schleife wird dann ein Objekt der Klasse OrganizerEvent erstellt, das ich mir überlegt habe. Dieses Objekt erhält nun bei der Konstruktion des Objekts diese String-Liste. Die Klasse hat drei Properties, nämlich DateStart, Description und IsBirthday.

// Kapselt die Termine aus der Kalenderdatei
// Nur rudimentär, soweit benötigt
public class OrganizerEvent : IComparable
{
    // Startdatum eines Termins (DTSTART)
    public DateTime DateStart { private set; get; }

    // Beschreibung des Termins (SUMMARY)
    public string Description { private set; get; }

    public bool IsBirthday { private set; get; }

    // String-Liste mit den Textzeilen eines Termins
    // aus der Kalenderdatei; von OrganizerReader
    // per Konstruktor
    public List<string> EventText { private set; get; }

    public OrganizerEvent(List<string> eventtext)
    {
        EventText = eventtext;

        IsBirthday = false;

        // Iterieren der String-Liste
        foreach (string item in EventText)
        {
            // Zeile mit DTSTART auswerten als Starttermin
            // un diesen im Property DateStart eintragen
            if (item.StartsWith("DTSTART"))
            {
                int position = item.IndexOf(":");
                string date = item.Substring(position + 1, 8);
                int year = Convert.ToInt32(date.Substring(0, 4));
                int month = Convert.ToInt32(date.Substring(4, 2));
                int day = Convert.ToInt32(date.Substring(6, 2));
                DateStart = new DateTime(year, month, day);
            }
            // Zeile mit SUMMARY auswerten und im
            // Property Description eintragen.
            // Nur der Text hinter dem ersten Doppelpunkt!
            // Zeitweise ist hinter SUMMARY noch ein Zeichensatz
            // angegeben (SUMMARY;CHARSET=UTF-8:)
            else if (item.StartsWith("SUMMARY"))
            {
                int position = item.IndexOf(":");
                Description = item.Substring(position + 1);
            }
            // Für Geburtstage als jährliche Wiederholungstermine
            else if (item.StartsWith("RRULE"))
            {
                int position = item.IndexOf(":");
                if (item.Substring(position + 1, 3).ToUpper() == "YM1")
                    IsBirthday = true;
            }
        }
    }
    // Interface IComparable
    // um die Termine sortieren zu können anhand Datum
    public int CompareTo(object obj)
    {
        int rc = 0;
        OrganizerEvent oe = (OrganizerEvent)obj;

        if (this.DateStart < oe.DateStart) rc = -1;
        if (this.DateStart > oe.DateStart) rc = 1;
        return rc;
    }
}

In dieser Klasse wird nun die Beschreibung des Termins untersucht und es wird das Startdatum des Termins und dessen Betreff in die beiden erstgenannten Properties kopiert. Bei den Geburtstagen, die ja jährliche Serientermine sind, wird das Property IsBirthday gesetzt. Andere Serientermine bleiben hierbei unberücksichtigt!

Dieses Objekt vom Typ OrganizerEvent wird dann in einer generischen Liste List<OrganizerEvent> organizerEvents, die zur Klasse OrganizerReader gehört, eingefügt. Hierin befinden sich nun alle berücksichtigten Termin-Objekte.

In der Formular-Klasse Form1 wird nach der Verarbeitung der Kalenderdatei mittels der Klasse OrganizerReader nun die die Anzeige vorbereitet. In der Methode displayEvents() werden nun alle Objekte in der Liste List<OrganizerEvents> organizerEvents iteriert. Wenn der betreffende Termin aufgrund des Startdatums in dem darzustellenden Bereich liegt, wird er in die generische Liste List<PreviewEvent> previewEvents aufgenommen, die dann sortiert wird. Wie bereits oben beschrieben wird ein String mittels der Properties DateStart und Description erzeugt, der dann in die Listbox eingefügt wird.

Einige Grundeinstellungen wie z.B. die Anzahl der Tage im Voraus und die zu verwendende Kalenderdatei werden in einer Einstellungsdatei user.config gespeichert, die sich in AppData\Local\IBH befindet. Ebenfalls gespeichert wird das Intervall in Sekunden, in dem die Kalenderdatei neu eingelesen und verarbeitet wird. Sicherlich hätte man das eleganter mit einem Observer Pattern (Beobachter-Muster) lösen können 🙂

Und zuletzt wird noch die aktuelle Größe des Formulars und dessen Position auf dem Desktop in den Einstellungen gespeichert.

Wer sich das mal ansehen möchte, kann sich die Projektmappe von Visual Studio 2015 herunterladen:

Hier das ausführbare OrganizerPreview.exe (in der Release-Version) für das .NET-Framework 4.5.2: