C++: Wie eine verspätete Klasse neues Licht auf ein bekanntes Entwurfsmuster wirft

In der Qt-Gemeinde existiert ein Mythos: "QProxyStyle". Auf der einen Seite steht die Behauptung, ein "QProxyStyle" sei unmöglich. Auf der anderen Seite Entwickler, die einen "QProxyStyle" entwickelt haben wollen. Eine Spurensuche deckt interessante Komplikationen auf.

In Pocket speichern vorlesen Druckansicht 4 Kommentare lesen
Lesezeit: 11 Min.
Von
  • Marc Mutz
Inhaltsverzeichnis

In der Qt-Gemeinde existiert ein Mythos: "QProxyStyle". Auf der einen Seite stehen Entwickler wie David Faure, seines Zeichens KDE-Urgestein und geschätzter Kollege des Autors, die behaupten, ein QProxyStyle sei unmöglich. Auf der anderen Seite stehen Entwickler von Qt-Erweiterungen, die einen QProxyStyle entwickelt haben wollen. Eine Spurensuche im Märchenwald deckt interessante Komplikationen bei der Anwendung eines bekannten Entwurfmusters auf.

Mehr Infos

Hinweis

Auch wenn der vorliegende Artikel Qt-Themen als Aufhänger nutzt, so sind die Folgerungen universell. Dem in Qt weniger versierten Leser sei daher ein Blick in den Kasten Nokia, Trolltech, Qt, QWidget und QStyle empfohlen, der die verwendete Technik kurz vorstellt.

Der Autor erinnert sich noch lebhaft an eine Diskussion, die vor einigen Jahren auf dem "qt4-tech-preview"-E-Mail-Verteiler geführt wurde: Ein Entwickler fragte, wie er Code nach Qt 4 portieren solle, der die in Qt 3 vorhandene virtuelle Methode void QButton::drawButtonLabel( QPainter* ) überschrieb, um einen MathML-Text auf einem QPushButton anzeigen zu können (QButton war in Qt 3 die Basisklasse von QPushButton, und hatte deutlich mehr Verantwortlichkeiten als ihr Qt 4-Pendant QAbstractButton).

Diese spezielle virtuelle Funktion strichen die Trolltech-Entwickler in Qt 4, zusammen mit einer stattlichen Anzahl ähnlicher Funktionen, um die Klassen-Schnittstellen zu verschlanken. Die Reduzierung von Relokationen zur Beschleunigung des Programmstarts spielte bei der Entscheidung ebenfalls eine Rolle (vergleiche Ulrich Drepper, "How To Write Shared Libraries", PDF).

Das Kopieren und Anpassen der QPushButton::paintEvent()-Implementierung in seine abgeleitete Klasse sah der Entwickler als einzig gangbaren Weg an.

Müßig zu erwähnen, dass die Lösung alles andere als perfekt ist: Sobald QPushButton::paintEvent() sich in einer neuen Qt-Version ändert, muss der Entwickler den Code in seiner Kopie nachziehen. Schlimmer noch: Der neue Code könnte ein neues Qt voraussetzen. Selbst wenn er mit gezielten #ifdefs den alten Code für ältere Qt-Versionen lauffähig hält, hat der Entwickler immer noch das Problem, dass sein Button – gegen eine ältere Qt-Version gebaut, jedoch zur Laufzeit gegen eine neue Qt-Version gebunden – unter Umständen sichtbar von "nativen" Buttons in einer Weise abweicht, die so nicht gewollt war.

Mehr Infos

Nokia, Trolltech, Qt, QWidget und QStyle
Eine kurze Einführung in die Qt-Klassenbibliothek und die Details des QStyle-Systems.

Die mittlerweile von Nokia übernommene norwegische Softwareschmiede Trolltech AS entwickelt seit Anfang der neunziger Jahre Qt (ausgesprochen wie engl. "cute"), eine auf die Entwicklung von GUI-Anwendungen fokussierte C++-Klassenbibliothek, die auf alle wichtigen Plattformen portiert ist. Aus der Zeit vor der Nokia-Übernahme stammt der Begriff "Troll", für Trolltech-Mitarbeiter.

Bis einschließlich Version 4 (die zu diesem Zeitpunkt aktuelle Version ist 4.7) ist QWidget die Basisklasse für alle GUI-Elemente, wie den im Artikel erwähnten QPushButton (das in der Planung befindliche Qt 5 soll diesen Ansatz nach fast 20 erfolgreichen Jahren aufgeben).

QWidget enthält unter anderem Ereignisbehandlungsroutinen (virtuelle Methoden), die Subklassen-Autoren überschreiben können, um das Verhalten des Widgets zu beeinflussen. Die für diesen Artikel wichtigste dieser Methoden ist der paintEvent(), den Entwickler reimplementieren können, um das Widget zu zeichnen.

Fast alle der in Qt implementierten Widgets zeichnen sich jedoch nicht selbst, sondern delegieren diese Arbeit an eine konkrete Implementierung des QStyle-Interfaces. Die konkreten QStyle-Implementierungen kümmern sich um das plattformspezifische Aussehen der Widgets. In den meisten Qt-Applikationen kann der Anwender mit der Option -style auf der Kommandozeile einen anderen als den Standard-Style auswählen.

Ein typischer paintEvent() mit QStyle-Delegation sieht damit wie folgt aus:

void ConcreteWidget::paintEvent() {
    QPainter p( this );
    QConcreteWidgetStyleOption opt;
    initStyleOption( &opt );
    style()->drawComplexControl( QStyle::CC_ConcreteWidget, &opt, &p );
}

Die Klasse QPainter stellt das API für die diversen Zeichenoperationen (drawRect(), drawText, etc) zur Verfügung; die Angabe von this als Konstruktorargument teilt dem Painter mit, auf welches Widget mit ihm gezeichnet werden soll.

Das eigentliche Zeichnen geschieht in der letzen Zeile, die dem QStyle::drawComplexControl()-Aufruf den Typ des zu zeichnenden Widgets (CC_ConcreteWidget), den Painter, und eine sogennante "Style-Option" übergibt. Die vorhergehenden Zeilen initialisieren diese "Style-Option" mit allen zum Zeichnen des Widgets erforderlichen Eigenschaften (Properties). Zum Zeichnen des Widgets ist daher im Grunde keine Widget-Instanz erforderlich. Das ist in vielen Fällen nützlich, es würde allerdings den Rahmen dieser Einführung sprengen, darauf genauer einzugehen.

Damit ist das Style-System ein klassisches Beispiel für das "Strategy"-Entwurfsmuster (vergleiche Gamma, Helm, Johnson, Vlissides: Design Patterns. Elements of Reusable Object-Oriented Software).

Neben der Kernfunktion des Zeichnens von Widgets hat QStyle noch andere Aufgaben. Im Artikel wird die styleHint()-Funktion erwähnt, vermöge der Plattformspezifika, wie die Reihenfolge von Schaltflächen in Dialogen, abgefragt werden können.

Weitergehende Informationen zum Styling-System von Qt finden sich in der Trolltech-Dokumentation.

Die Antwort, die er von einem "Troll" (hier: Trolltech-Mitarbeiter) erhielt, lautete in etwa so: "Benutze einen Proxy-Style".

Es klingt vielversprechend: Ein Proxy-Style, eingefügt zwischen QPainter und QPushButton, der lediglich das Malen des Button-Textes (QStyle::CE_PushButtonLabel) ändert, und alle anderen Style-Operationen an den zugrunde liegenden QStyle weiter leitet. Die Existenz eines QProxyStyle einmal angenommen, könnte die Lösung ungefähr wie folgt aussehen:

class PushButtonLabelProxyStyle : public QProxyStyle {
QWidget * const m_widget;
QStyle * const m_style;
public:
// ...
explicit PushButtonLabelProxyStyle( QWidget * w )
: QProxyStyle(),
m_widget( w ),
m_style( w ? w->style() : 0 )
{
if ( w ) w->setStyle( this );
setSourceStyle( m_style ); // register m_style to forward calls to
}

~PushButtonLabelProxyStyle() {
if ( m_widget ) m_widget->setStyle( m_style );
}

void drawControl( ControlElement ce, const QStyleOption * opt,
QPainter * p, const QWidget * w ) const {
if ( ce == CE_PushButtonLabel )
drawMyPushButtonLabel( opt, p );
else
QProxyStyle::drawControl( ce, opt, p, w );
            // forwards to source style
}

private:
void drawMyButtonLabel( const QStyleOption * opt, QPainter * p ) const {
// ...
}
};

void MyPushButton::paintEvent( QPaintEvent * e ) {
const PushButtonLabelStyleProxy proxy( this );
QPushButton::paintEvent( e );
}

Das sieht ordentlich aus, auch wenn im Listing etwas sorglos mit dem ursprünglichen QStyle umgegangen wird. Der Code verwendet sogar das RAII-Idiom.

Es brauchte jedoch sieben Releases der Qt 4-Serie, bis QProxyStyle in Qt verfügbar war: Erst Qt 4.6 enthält ihn letztlich. Was hat die Aufnahme einer solch nützlichen Klasse so lange hinausgezögert?

Zunächst ist anzumerken, dass nicht alles Nützliche bereits in Qt enthalten ist. Hier kommen Drittanbieter ins Spiel. KDAB bietet zum Beispiel KD Tools an. Ebenso ist Qxt zu nennen, das im Übrigen bereits einen QxtProxyStyle enthält, welcher genau die Aufgabe des QProxyStyle übernehmen soll.

Der geneigte Leser sei jedoch eingeladen, mit Qt 4.5 oder QxtProxyStyle, das oben gezeigte Codefragment zu einem tatsächlich ausführbaren Beispielprogramm auszuarbeiten. Zum Beispiel eines, das den Button-Text in einem Outline-Font zeichnet, oder etwas ähnlich offensichtlich anderes.

Ohne zu viel verraten zu wollen: Es stellt sich heraus, dass es nicht funktioniert. Der Debugger kann in der Ecke stehen bleiben; wie ein Artikel in Ausgabe 9 des Trolltech-Newsletters "Qt Quarterly" zeigt, ist das Problem seit langem bekannt. Die Analyse dieses Problems wird eine gewichtige Einschränkung eines bekannten Entwurfmusters aufzeigen.