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.

Zunächst jedoch sei die Frage gestattet, welches der GoF-Entwurfsmuster ("Gang of Four", Kurzform für die vier Autoren des Standardwerks zum Thema) QProxyStyle denn nun implementiert.

Lesern, die hier auf "Proxy" getippt haben, sei verziehen: Der Klassenname ist einfach zu suggestiv. Ein genaueres Studium des GoF-Buches zeigt jedoch, dass das Entwurfsmuster "Proxy" dem Kern nach der Zugriffskontrolle auf das RealSubject dient.

Die Intention hinter QProxyStyle ist aber nicht primär, den Zugriff auf den zugrunde liegenden QStyle zu kontrollieren, sondern diesen zu dekorieren. Der Autor tendiert daher zu "Decorator". Die für Decorator angeführten Beispiele "Dekoration von visuellen Komponenten mit einem Rahmen" und "Kompression eines Datenstromes" sind nach Meinung des Autors recht nah am Anwendungsfall eines QProxyStyle.

Zurück zum Problem. Jasmin Blanchette, damals technischer Chefredakteur bei Trolltech, beschreibt es wie folgt:

"Dieser Ansatz hat ein Problem, dessen wir gewahr sein müssen: Einige virtuelle Funktionen in Qt's eingebauten Styles sind vermöge anderer virtueller Funktionen implementiert. So benutzt zum Beispiel QWindowsStyle::drawComplexControl() die Funktion QWindowsStyle::drawPrimitive(), um den kleinen Pfeil einer QComboBox zu zeichnen. Reimplementieren wir drawPrimitive() in unserem MyStyle (die von ProxyStyle ableitet), so wird unsere Implementierung von QWindowsStyle ignoriert werden, der weiterhin seine eigene drawPrimitive()-Funktion aufrufen wird. (Dieses Verhalten entsteht deshalb, weil MyStyle nicht von QWindowsStyle ableitet, sondern ihn lediglich "wrappt".) Abhängig vom erwarteten Ergebnis kann das bedeuten, dass wir auch drawComplexControl() reimplementieren müssen."

Die hier geschilderte Situation ist im Übrigen ähnlich zu dem eingangs beschriebenen Verhalten von drawControl(): Für QStyle::CE_PushButton ruft sich die Funktion mit QStyle::CE_PushButtonBevel und QStyle::CE_PushButtonLabel selbst auf. Außerdem nutzt sie subElementRect() und drawPrimitive() (vergleiche im Listing 1 QCommonStyle::drawControl())

void QCommonStyle::drawControl(ControlElement element,
const QStyleOption *opt, QPainter *p, const QWidget *widget) const
{
Q_D(const QCommonStyle);
switch (element) {

case CE_PushButton:
if (const QStyleOptionButton *btn =
qstyleoption_cast<const QStyleOptionButton *>(opt)) {
drawControl(CE_PushButtonBevel, btn, p, widget);
QStyleOptionButton subopt = *btn;
subopt.rect = subElementRect(SE_PushButtonContents, btn, widget);
drawControl(CE_PushButtonLabel, &subopt, p, widget);
if (btn->state & State_HasFocus) {
QStyleOptionFocusRect fropt;
fropt.QStyleOption::operator=(*btn);
fropt.rect = subElementRect(SE_PushButtonFocusRect, btn, widget);
drawPrimitive(PE_FrameFocusRect, &fropt, p, widget);
}
}
break;
// ...

Sobald QPushButton::paintEvent() style()->drawControl( QStyle::CE_PushButton, ... ) aufruft, und der Proxy diesen Aufruf an den Basis-Style weiterleitet, wird der Kontrollfluss erst wieder in den Proxy-Style zurückkehren, wenn der weitergeleitete Aufruf zurückkehrt (vergleiche Abbildung 1).

Bei naiver Implementierung des Proxy-Styles bleibt der Kontrollfluss in aBaseStyle hängen (Abb. 1).

Hier liegt der Fehler: Die Reimplementierung von drawControl( CE_PushButtonLabel ) in PushButtonLabelStyleProxy wird niemals aufgerufen.

Folgt man Blanchettes Artikel, so könnte die Lösung darin liegen, drawControl() auch für CE_PushButton in PushButtonLabelStyleProxy zu implementieren. Das ist jedoch im Allgemeinen nicht machbar: Wer garantiert denn, dass alle QStyle-Subklassen für CE_PushButton die gleiche Implementierung nutzen wie QCommonStyle?

Tritt man einen Schritt zurück, so erkennt man, dass das zugrunde liegende Problem darin besteht, dass hier eine virtuelle Funktion eine andere virtuelle Funktion aufruft. Für den Einsatz des "Decorator"-Entwurfsmusters bedeutet das, dass solche Klassen im Allgemeinen nicht Component des Musters sein können.

Wohlgemerkt: im allgemeinen Fall. Es wird immer Anwendungsszenarien geben, in denen "Decorator" trotzdem funktioniert. Will der Entwickler zum Beispiel die Reihenfolge der Schaltflächen in QDialogButtonBox festlegen, so kann er ohne weiteres den Proxy-Style-Ansatz verwenden und in der Reimplementierung von styleHint() das gewünschte QDialogButtonBox::ButtonLayout für SH_DialogButtonLayout zurückliefern. Das funktioniert jedoch nur per Zufall, denn styleHint() wird vom QWidget direkt aufgerufen und nicht indirekt durch andere QStyle-Funktionen.

Nun gibt es jedoch seit Qt 4.6 einen QProxyStyle. Es ist aufschlussreich, sich anzuschauen wie dieser implementiert ist und warum das keine Lösung für das allgemeine "Decorator"-Entwurfsmuster ist.

Ein Blick auf Abbildung 2 zeigt zunächst einen gangbaren Ansatz in UML-Schreibweise.

Die Implementierung des QStyle in Qt 4.6 kehrt auf allen Ebenen für virtuelle Funktionen in den potenziellen QProxyStyle zurück (Abb. 2).

Hier kehrt aBaseStyle "irgendwie" in die virtuellen Methoden des Proxy-Styles zurück, und zwar auf allen Ebenen des Aufrufbaumes. Damit bleibt der Kontrollfluss nicht mehr in aBaseStyle hängen, Reimplementierungen von virtuellen Methoden auf aProxyStyle werden auf allen Ebenen zuverlässig aufgerufen.

Wenn der Entwickler Kontrolle über die Implementierung von aBaseStyle hat, so kann er recht einfach das UML-Diagramm in C++-Code gießen: Dazu implantiert er einen Zeiger proxy auf den (potenziellen) Proxy in aBaseStyle und ruft virtuelle Funktionen nur noch über diesen Zeiger auf: statt this->drawControl(...) also proxy->drawControl(...). Existiert kein Proxy, so setzt er proxy = this.

Hässlich, nicht wahr? Doch das beschreibt genau, wie die "Trolle" QProxyStyle implementiert haben, nachzulesen auf der Gitorious-Plattform[9]. Da der Commit, selbst ohne die Implementierung von QProxyStyle an sich zu umfassen, zu groß ist ("too large to be displayed in a browser"), zeigt das Listing 2 dessen "git diff-stat":

 src/gui/kernel/qapplication.cpp       |   81 +++---
src/gui/kernel/qapplication_p.h | 4 +-
src/gui/kernel/qapplication_x11.cpp | 76 +++--
src/gui/styles/qcleanlooksstyle.cpp | 148 +++++-----
src/gui/styles/qcommonstyle.cpp | 534 ++++++++++++++++----------------
src/gui/styles/qgtkstyle.cpp | 118 ++++----
src/gui/styles/qmacstyle_mac.mm | 92 +++---
src/gui/styles/qmotifstyle.cpp | 156 +++++-----
src/gui/styles/qplastiquestyle.cpp | 102 ++++----
src/gui/styles/qstyle.cpp | 34 ++-
src/gui/styles/qstyle.h | 5 +
src/gui/styles/qstyle_p.h | 14 +-
src/gui/styles/qwindowsstyle.cpp | 94 +++---
src/gui/styles/qwindowsvistastyle.cpp | 76 +++---
src/gui/styles/qwindowsxpstyle.cpp | 144 +++++-----
src/gui/styles/styles.pri | 4 +
tests/auto/qstyle/tst_qstyle.cpp | 42 +++-
17 files changed, 912 insertions(+), 812 deletions(-)

Dieses Monster zog (wie zu erwarten) Änderungen an jedem existierenden Style in Qt nach sich und versagt damit bei solchen Styles, die nicht in Qt selbst implementiert sind. Die Klassendokumentation weist darauf gesondert hin.

Das ist jedoch ganz und gar nicht das, was das Entwurfsmuster bezwecken soll. Vielmehr ist es ein QProxyStyle "egal, was es kostet" und damit kaum eine allgemeine Lösung. In seiner reinen Form kann "Decorator" auf eine Klassenhierarchie angewendet werden, ohne dass alle Komponenten von einem potenziellen "Decorator" Kenntnis haben und dafür, selbst wenn dieser nicht vorhanden ist, mit einer zusätzlichen Indirektion bezahlen. Es war jedoch der einzige Weg, einen Proxy-Style für die existierende QStyle-Hierarchie zu erstellen.

Als Quintessenz kann man formulieren:

"Soll ein Klassendesign Decorator unterstützen, so dürfen virtuelle Funktionen nicht einander aufrufen."

Und, als Nachtrag zur Diskussion des "Decorator"-Entwurfmusters im GoF-Buch:

"Sie können dieses Entwurfsmuster nicht anwenden, wenn Component virtuelle Methoden hat, die einander aufrufen."


Marc Mutz
arbeitet als Senior Software Engineer, Consultant und Trainer bei der KDAB (Deutschland) GmbH & Co. KG. Er möchte seinem Kollegen David Faure für die gemeinsamen Diskussionen danken, die diesem Artikel zugrunde liegen.
(rl)