Monday, October 4, 2010

Qt: Write once, #ifdef everywhere?

Despite what the title of this post might suggest, I really like Qt. But as a developer, I also know that "write once, run everywhere" isn't realistic without writing some special-cased platform-specific code. Qt does take care of many platform-specific things, and I think it's the closest you can get to "write once, run everywhere" right now.

This post should serve two purposes: To provide a real-world example of what needs to be done in the code for the app to work on both Maemo 5 and Symbian^3 (with example code), and to get suggestions on what parts I could rewrite in a more platform-agnostic manner with existing APIs (so please comment if you are in the know!).

Here's the story: In early September, I've rewritten my game "That Rabbit Game" to use QGraphicsView on the N900, and in the last weeks, I've ported it to Symbian^3. I use the macro Q_OS_SYMBIAN to check for Symbian and Q_WS_MAEMO_5 to check for Fremantle. Here's the current main menu on a N900 and N8 (different screen resolutions and aspect ratios):

Qt modules: In the qmake project file, I can use linux-g++-maemo5 to add Maemo-specific configuration, and symbian for Symbian-specific settings. I use D-Bus module on Maemo, but obviously not on Symbian, so my project file contains something like this:

linux-g++-maemo5 {
QT += dbus
}

This is nice, because I only have to maintain one project file, and the block structure is very readable (and I can even use different Qt submodules for each platform).

Screen orientation: Symbian has auto-rotation for Qt apps by default, and Maemo 5 has landscape-only mode by default. For my game, portrait mode does not make sense, so I have to request landscape-only on Symbian (similarly, if I want auto-rotation everywhere, I have to request it on Maemo 5 and do nothing in Symbian). For this specific case, I need some libs in Symbian, so I add this to the project file:

symbian {
LIBS += -lcone -leikcore -lavkon
}

I also need to add this to the top of my main source file:

#ifdef Q_OS_SYMBIAN
#include <AknAppUi.h>
#endif

And finally, I have to copy'n'paste a code block into my main() function before I create the first window:

#ifdef Q_OS_SYMBIAN
CAknAppUi* appUi = dynamic_cast<CAknAppUi*> (CEikonEnv::Static()->AppUi());
TRAPD(error,
if (appUi) {
// Lock application orientation into landscape
appUi->SetOrientationL(CAknAppUi::EAppUiOrientationLandscape);
}
);
#endif

It would be nice if Qt (or Qt Mobility?) has some generic API for this, where I just need to do something along the lines of QRotation::setMode(QRotation::LandscapeOnly); and let it take care of the platform-specific stuff.

Accelerated QGraphicsView: On Symbian^3, QGraphicsView is automatically accelerated via OpenVG (I think), but on Maemo 5, it can use OpenGL for this (but does not by default), so in the code where I create my QGraphicsView, I have to have this at the top:

#ifdef Q_WS_MAEMO_5
# include <QGLWidget>
#endif

And then somewhere down that file where I create the QGraphicsView, this code goes there:

#ifdef Q_WS_MAEMO_5
QGLWidget *glw = new QGLWidget(QGLFormat(QGL::DoubleBuffer));
view->setViewport(glw);
#endif

It's not that difficult, and is already cross-platform (on platforms where OpenGL is available), but maybe QGraphicsView can be OpenGL-accelerated on Maemo 5 by default. Maybe there is some non-obvious side effect that using an OpenGL viewport has that I'm not aware of, and that's why one has to do it manually.

Accelerometer readings: This might actually be a bug in Qt Mobility. For my game, I have to read the accelerometer's Y axis on Symbian, and its X axis on Maemo to determine the rotation for the same holding position of the device, so there's another platform-specific #ifdef there. Again, this will hopefully be fixed in a future release of Qt Mobility, and will most likely be fixed for MeeGo as well.

Task switcher: It's customary on Maemo to have a task switcher button in the upper left corner. There's no platform-agnostic way of going to the task switcher, so I needed to special-case it for Maemo 5, and have not bothered implementing it on Symbian^3 (suggestions welcome!). First, I need to include D-Bus headers for this (I've already mentioned how to request the D-Bus module in the qmake project file above) :

#if defined(Q_WS_MAEMO_5)
# include <QDBusConnection>
# include <QDBusMessage>
#endif

When I'm in the handler code where I need to do the actual task switching, I can utilize it to activate Maemo 5's task switcher:

#if defined(Q_WS_MAEMO_5)
QDBusConnection c = QDBusConnection::sessionBus();
QDBusMessage m = QDBusMessage::createSignal("/", "com.nokia.hildon_desktop", "exit_app_view");
c.send(m);
#endif

This is very much Maemo 5-specific, and will very likely not work on MeeGo Harmattan. Again, here it would be nice to have a cross-platform way of doing window management (in Qt or Qt Mobility?). I can imagine this being useful not only on handsets, but also on netbooks and tablets (from a MeeGo PoV). Again, trying to come up with pseudo-APIs here, QWindowManager::showTaskSwitcher(); could be a nice way to handle this in a cross-platform way, hiding platform-specific implementations. From what I've seen of MeeGo Touch, activating the task switcher there simply iconifies the window on the Desktop, so maybe if I would iconify my main window, it should activate the task switcher on Maemo 5 and Symbian. It does not work on Maemo 5 right now, though, and I haven't tested it on Symbian.

Screen resolution: This was (thanks to QGraphicsView) less of a problem than I thought it would be. Symbian^3 (or at least the N8) uses 640x360 as its resolution, and Maemo 5 uses 800x480. Maybe the MeeGo Harmattan device will use yet another resolution. I still configure my QGraphicsScene object manually via #ifdefs to get a good resolution, but the code could just measure the resolution and configure the scene as well. Here's what I use:

#if defined(Q_WS_MAEMO_5)
setSceneRect(0, 0, 800, 480);
#elif defined(Q_OS_SYMBIAN)
setSceneRect(0, 0, 640, 360);
#endif

I then use the sceneRect() of my scene to calculate a scale factor for all contents and call setScale() on all root items of the scene to scale the contents to the current screen size. You might have to take care of different aspect ratios on different devices, too - but it was unproblematic for my use case (the game) this time.

No comments:

Post a Comment