Cross-platform code and styles

It is a fairly regular occurrence in my job in Qt Software Support that I get asked how to change the look and feel of a given widget and usually this means changing the style. If the application is going to only be released for one platform, for instance Windows XP, then this is simple because all you need to do is subclass QWindowsXPStyle and make the necessary change by reimplementing one of the functions.

However, if your application is going to be targeting more than one platform, for instance Windows and Mac, then in order to get the same change you would need to subclass QWindowsXPStyle and QMacStyle, and maybe you want to cover Windows Vista as well, and before you know it you have to maintain 7 different style subclasses just so that you can modify a tiny aspect of the look and feel for a widget!

Luckily help is at hand in the form of a proxy style, this was originally mentioned way back in Qt Quarterly 9 (all those years ago) using a class that subclasses QStyle and had a member variable which would point to the original style being used. For the curious the article can be found here.

On the face of it, this looks like it will do the trick, and it looks nice, however it is not as robust as it appears. The problem with this approach is due to the fact that inside the styles themselves they call the style functions directly. For instance in QWindowsStyle::drawControl() it will call styleHint() directly meaning it won't call your reimplemented styleHint() function in the ProxyStyle class and its turns out to not be so useful at all anymore.

So the problem still remains, how do we make this smarter? The proxy style approach is still the one we want to go with as this will give the result we want, however its the implementation of it that we need to change to cope with the situation better. Therefore we need to somehow have subclasses of the existing styles that use the proxy style approach.

Firstly we create a ProxyStyle class that isn't a subclass of QStyle (all will make sense later I promise :) In this example I want to change all styles to always underline shortcuts so to do this I need to reimplement styleHint() in the styles.

class ProxyStyle
{
public:
ProxyStyle(const QString &baseStyle) { style = QStyleFactory::create(baseStyle); }
int proxyStyleHint(QStyle::StyleHint hint, const QStyleOption *option = 0,
const QWidget *widget = 0, QStyleHintReturn *returnData = 0) const
{
if (hint == QStyle::SH_UnderlineShortcut)
return 1;
return style->styleHint(hint, option, widget, returnData);
}
private:
QStyle *style;
};

This is similar to the approach that is given in Qt Quarterly 9 except the difference is that we don't have a styleHint() function but we have a proxyStyleHint() function, this is delibrate due to the following:

#define ADDSTYLESUBCLASS(BaseStyleClass, BaseStyleName)
class My##BaseStyleClass : public BaseStyleClass, public ProxyStyle
{
public:
My##BaseStyleClass() : BaseStyleClass(), ProxyStyle(BaseStyleName) {}
int styleHint(StyleHint hint, const QStyleOption *option = 0, const QWidget *widget = 0, QStyleHintReturn *returnData = 0) const
{
return proxyStyleHint(hint, option, widget, returnData);
}
};

This is where we are actually subclassing the styles themselves, it has been added as a macro because we want to make it easier to just add styles that a subclass should be provided for. So if for example a Windows 7 style was added in 4.6, it would be easy to update this relevant code. We only need to reimplement the functions that we are interested in, so in this case we add a styleHint() reimplementation that calls our proxyStyleHint() function.

So to make all styles available we just use the macro to define the classes like such (delibrately cut just for illustration purposes):

ADDSTYLESUBCLASS(QCleanlooksStyle, "cleanlooks");
ADDSTYLESUBCLASS(QPlastiqueStyle, "plastique");
ADDSTYLESUBCLASS(QMotifStyle, "motif");
ADDSTYLESUBCLASS(QWindowsStyle, "windows");
ADDSTYLESUBCLASS(QCDEStyle, "cde");

Now its possible to use MyQWindowsStyle and this will always call our proxyStyleHint() implementation even if its called from QWindowsStyle itself or directly on the style.

There is just one thing left to do to make this easy to replace any existing style either on a widget or on the application. In order to be able to determine which subclassed style should be used a function is added that utilizes qobject_cast to determine which one should be used (again cut for illustration purposes).

QStyle *returnSubclassStyleForBaseStyle(QStyle *baseStyle)
{
if (qobject_cast<QWindowsStyle *>(baseStyle)) {
if (qobject_cast<QCleanlooksStyle *>(baseStyle)) {
#ifdef Q_OS_X11
if (qobject_cast<QGtkStyle *>(baseStyle))
return new MyQGtkStyle;
#endif
return new MyQCleanlooksStyle;
}
...
if (qobject_cast<QMotifStyle *>(baseStyle)) {
if (qobject_cast<QCDEStyle *>(baseStyle))
return new MyQCDEStyle;
return new MyQMotifStyle;
}
return baseStyle;
}

Leaving just one line needed to set the proxy style on the application in our example:

app.setStyle(returnSubclassStyleForBaseStyle(app.style()));

And there you have it, the application still looks native with the exception of the change that has been done in the style. The way that this is implemented makes it easy to add your own changes in, as you only need to change it in one place and also makes it easy to add support for new styles too.

Unfortunately there is a problem with this approach and that is that it does not work with styles that are implemented as plugins, the approach used here needs to be able to know about the classes themselves. So for example, this will not work exactly as expected when the KDE Oxygen style is used as the code does not know about this style. However, this problem would occur too if an application developer wanted to subclass the Oxygen style to make the change directly (without using the proxy style approach). If the Oxygen style can be linked against in some way then it would be possible to utilize this approach, since all that is needed is to be done is to add an ADDSTYLESUBCLASS line and add a couple of lines to the returnSubclassStyleForBaseStyle() function.

The whole code for the proxy style approach is available here.


Blog Topics:

Comments