calDAV und iCalendar code example mit Qt, C++ und QML
Nachdem ich mich letztes Jahr schon hier und da mit Kalendern für Webseiten und dem iCalendar-Format beschäftigt hatte, habe ich letztens während dem 33C3 in Hamburg noch einen calDAV-Client in C++ und QML mit Qt geschrieben.
Zweck: den Küchen-Computer um eine Kalenderansicht mit meinen anstehenden Terminen erweitern.
Da ich keinen passenden Beispielcode finden konnte, der nicht entweder nur ansatzweise funktioniert oder völlig overfeatured ist, habe ich kurzerhand anhand der offiziellen Spezifikation selbst etwas geschrieben:
Features (u.a.):
- Verbindet sich mit calDAV-Servern (z.B. ownCloud, Nextcloud, etc.)
- Lädt iCalendar-Dateien von Festplatte oder über HTTP und HTTPS
- Verarbeitet Wiederholungsregeln für Terminserien und erstellt Listen von Terminen für einzelne Kalenderdaten
- Verwaltet mehrere Kalenderquellen simultan
- Anlegen, Editieren und Löschen von Terminen
- ... usw. ...
Qt: bloß keine Waisenkinder instantiieren
Wenn man in Qt eine Klasse implementiert, sollte man tunlichst darauf achten, dass alle instantiierten Attribute
Das Elternelement hier ist
Also besser gut aufpassen bei so was, sonst kann das Debugging richtig ätzend werden.
this
als parent haben.
Andernfalls kann es nämlich sein, dass irgendwann der Garbage-Kollektor auf das offenbar elternlose Kind-Objekt stößt und es im Sinne von "ist das Kunst oder kann das weg" in die Tonne wirft.
Und dann hat man den Salat.
Ist mir hiermit passiert:
- class Measurement : public Object
- {
- Q_OBJECT
- public:
- Measurement(QObject* parent = 0);
- ~Measurement();
- };
- class XYPlot : public QQuickPaintedItem
- {
- Q_OBJECT
- public:
- XYPlot(QQuickPaintedItem* parent = 0);
- private:
- Measurement m_Measurement;
- };
XYPlot
, welches ein Attribut der Klasse Measurement
besitzt.
Bei mir gab es unreproduzierbare SIGSEGV segmentation faults, was immer auf irgendein Problem mit ungültigem Pointer hinweist.
Beim Debuggen habe ich dann festgestellt, dass kurz zuvor der Dekonstruktor von Measurement
aufgerufen wurde - obwohl das zugehörige XYPlot-Objekt noch existierte.
Woran lag es? ⇒ Ich hatte vergessen, dem Kind-Objekt zu sagen wer sein parent ist. Im Konstruktor der Elternklasse XYPlot
also m_Measurement
mit this
instantiieren:
- XYPlot::XYPlot( QQuickPaintedItem* parent) : QQuickPaintedItem(parent), m_Measurement(this)
- {
- }
Globale SVN-Revisionsnummer im Projekt - und zwar richtig!
Wenn man ein Projekt aufsetzt, dann hat man meist eine Versionsnummer im Format
Und wird von SVN beim Commit ersetzt zu
Vorausgesetzt natürlich, dass die Datei die entsprechenden SVN-Keywords gesetzt hat:
So weit, so gut. In dem Beispiel oben ist die Revisionsnummer 178. Wenn es jetzt aber noch eine zweite Datei gibt und Änderungen an dieser comitted werden, dann bleibt in main.cpp die 178 stehen. Warum? Weil die SVN-Revisionsnummer dateibezogen ist - nicht projektbezogen! Das ist ja jetzt nicht ganz das, was wir haben wollten. Zum Glück, gibt es aber ein Tool, welches genau für unsere Zwecke gedacht ist: svnversion! Um eine globale Revisionsnummer zu erhalten erstellt man eine leere Datei revision.h, checkt sie nicht in SVN ein und schreibt folgendes in die *.pro-Projektdatei eines Qt-Projekts:
Zur Erklärung:
Als erstes wird qmake mitgeteilt, dass es eine zusätzliche Abhängigkeit gibt (PRE_TARGETDEPS, Zeile 1) - nämlich besagte revision.h-Datei.
Dann werden ein paar Variablen mit den SVN-Revisionsinformationen gefüllt.
Mit dem Kommando
a.b.c.d
, mita
= Hauptversionsnummer, ändert sich eigentlich nur bei massiven Änderungenb
= zeigt an, dass neue Features hinzugekommen sindc
= zeigt an, dass etwas gepatched bzw. geupdated wurded
= gibt den Revisionsstand der Software im Versionsverwaltungssystem an.
- /*!
- *******************************************************************************
- * File identification: $Id:$
- * Revision of last commit: $Rev:$
- * Author of last commit: $Author:$
- * Date of last commit: $Date:$
- *******************************************************************************
- */
- /*!
- *******************************************************************************
- * File identification: $Id: main.cpp 178 2016-02-08 14:42:36Z cypax $
- * Revision of last commit: $Rev: 178$
- * Author of last commit: $Author: cypax $
- * Date of last commit: $Date: 2016-02-08 15:42:36 +0100 (Mo, 08 Feb 2016) $
- *******************************************************************************
- */
So weit, so gut. In dem Beispiel oben ist die Revisionsnummer 178. Wenn es jetzt aber noch eine zweite Datei gibt und Änderungen an dieser comitted werden, dann bleibt in main.cpp die 178 stehen. Warum? Weil die SVN-Revisionsnummer dateibezogen ist - nicht projektbezogen! Das ist ja jetzt nicht ganz das, was wir haben wollten. Zum Glück, gibt es aber ein Tool, welches genau für unsere Zwecke gedacht ist: svnversion! Um eine globale Revisionsnummer zu erhalten erstellt man eine leere Datei revision.h, checkt sie nicht in SVN ein und schreibt folgendes in die *.pro-Projektdatei eines Qt-Projekts:
- PRE_TARGETDEPS += $$PWD/code/revision.h
- # Obtain SVN revision
- SVN_REVISION = $$system(svnversion -n)
- # split into list by ':'
- REVISION_LIST = $$split(SVN_REVISION,:)
- # get last item of list
- HEAD_REVISION = $$last(REVISION_LIST)
- # remove M (modified working copy)
- HEAD_REVISION = $$replace(HEAD_REVISION,M,)
- # remove S (switched working copy)
- HEAD_REVISION = $$replace(HEAD_REVISION,S,)
- # remove P (partial working copy, from a sparse checkout)
- HEAD_REVISION = $$replace(HEAD_REVISION,P,)
- QMAKE_EXTRA_TARGETS += revtarget
- revtarget.target = $$PWD/code/revision.h
- unix {
- revtarget.commands = "$$system(echo \'/* generated file - do not edit */\' > $$revtarget.target)"
- revtarget.commands += "$$system(echo \'$${LITERAL_HASH}ifndef REVISION_H\' >> $$revtarget.target)"
- revtarget.commands += "$$system(echo \'$${LITERAL_HASH}define REVISION_H\' >> $$revtarget.target)"
- revtarget.commands += "$$system(echo \'$${LITERAL_HASH}define SVN_REVISION \"$$SVN_REVISION\"\' >> $$revtarget.target)"
- revtarget.commands += "$$system(echo \'$${LITERAL_HASH}define HEAD_REVISION $$HEAD_REVISION\' >> $$revtarget.target)"
- revtarget.commands += "$$system(echo \'$${LITERAL_HASH}define HEAD_REVISION_STRING \"$$HEAD_REVISION\"\' >> $$revtarget.target)"
- revtarget.commands += "$$system(echo \'$${LITERAL_HASH}endif // REVISION_H\' >> $$revtarget.target)"
- }
- win32 {
- revtarget.commands = "$$system(echo '/* generated file - do not edit */' > $$revtarget.target)"
- revtarget.commands += "$$system(echo '$${LITERAL_HASH}ifndef REVISION_H' >> $$revtarget.target)"
- revtarget.commands += "$$system(echo '$${LITERAL_HASH}define REVISION_H' >> $$revtarget.target)"
- revtarget.commands += "$$system(echo '$${LITERAL_HASH}define SVN_REVISION \"$$SVN_REVISION\"' >> $$revtarget.target)"
- revtarget.commands += "$$system(echo '$${LITERAL_HASH}define HEAD_REVISION $$HEAD_REVISION' >> $$revtarget.target)"
- revtarget.commands += "$$system(echo '$${LITERAL_HASH}define HEAD_REVISION_STRING \"$$HEAD_REVISION\"' >> $$revtarget.target)"
- revtarget.commands += "$$system(echo '$${LITERAL_HASH}endif // REVISION_H' >> $$revtarget.target)"
- }
- revtarget.depends = FORCE
- QMAKE_DISTCLEAN += $$revtarget.target
svnversion -n
(Zeile 4) erhält man die SVN-Revisionsnummer. Der Parameter -n bewirkt, dass die Ausgabe keinen Zeilenumbruch enthält.
Die Variable SVN_REVISION enthält somit die Ausgabe von svnversion, ausgehend vom Pfad, in der die Projektdatei liegt.
Wenn die lokale Arbeitskopie allerdings modifiziert, unvollständig ausgecheckt oder zu einer anderen Revision geswitched wurde, ist die Revisionsnummer nicht einfach eine Zahl, sondern nach dem Schema [abc:]xyz[M|S|P] aufgebaut, wobei xyz die Headrevision ist (für Details einfach mal in der Konsole svnversion -help
eingeben).
Aus diesem Grund teilen wir die Ausgabe anhand des Trennzeichens ":" auf (Zeile 6), nehmen das letzte Element dieser Liste (Zeile 8) und entfernen alle M-, S- und P-Zeichen (Zeilen 10 - 14).
In Zeile 16 wird qmake mitgeteilt, dass es ein zusätzliches Target revtarget gibt (QMAKE_EXTRA_TARGETS), welches die, im Unterordner /code befindliche, revision.h-Datei ist und welche stets neu zu erstellen ist (Zeile 39).
Das Befüllen der revision.h erfolgt in den Zeilen 19 - 37. Zu beachten ist, dass die erste echo-Ausgabe mit einem einfachen >
umgeleitet wird. Dadurch wird der der Inhalt der Datei überschrieben. Anhängen weiterer Zeilen erfolgt mit >>
.
Die echo-Anweisungen für Unix müssen übrigens deshalb extra escaped werden, weil ein echo /* generated file - do not edit */
erst alle Dateien unter / auflisten würde, dann "generated file - do not edit" ausgibt und dann alle Dateien im aktuellen Verzeichnis auflistet.
Irrationales Programmieren mit QML
Ich habe Qt5 auf dem Raspberry Pi 2 kompiliert und installiert. Das war zwar ein bisschen umständlich, lief bis jetzt eigentlich ohne unüberwindbare Probleme.
Aber jetzt hol' ich gleich die Kettensäge und geb' dem Drecksteil den Rest!
Den Code des Küchencomputers portiere ich derzeit auf Qt5 und QtQuick 2.4 und mache ihn unabhängig von Plattform und Auflösung.
Er läuft inzwischen gleichermaßen problemlos auf Windows 7 wie auf Debian Wheezy mit LXDE.
Nur auf dem Raspberry nicht. Da werden manche Texte nicht angezeigt und das Layout ist verschoben.
Und QML wirft mir unzählige Fehlermeldungen entgegen - allen gemein ist, dass sie mit string-Properties zu tun haben.
Also habe ich in den letzten Stunden ein Minimalprojekt angelegt um das Problem nachzustellen, einzukreisen und dann eine passende Lösung zu entwickeln.
So was kostet Zeit, funktioniert aber immer.
Nur hier nicht. Dieser Mistcode bringt mich noch um den Verstand:
import QtQuick 2.4
import QtQuick.Window 2.2
Window {
id: myWindow
width: 400
height: 200
visible: true
Text {
id: myText
text: "12"
}
Text {
x: 30
text: myText.text
Component.onCompleted: {
myText.text = "99"
}
}
}
Das funktioniert genau so, wie es soll.
Wenn man aber "99"
durch "34"
ersetzt, bleibt der zweite Text leer und QML meldet
Unable to assign [undefined] to QStringin der Zeile
text: myText.text
.
Allerdings nur auf dem Raspberry. Unter Windows und einem x86-Linux macht es, was es soll.
Was zur Hölle soll das?
Warum gerade "34"
??
Warum nicht z.B. "42"
???
Nerv! Ich spiele schon mit dem Gedanken wieder zurück auf Qt4.8 zu portieren ...