Original URL: http://www.theregister.co.uk/2006/11/19/cross_platform_development_win_mac/

Cross platform development for Windows and Mac OS X

Killing two birds with one stone

By Dave Jewell

Posted in Developer, 19th November 2006 19:22 GMT

Hands on I’ve been fascinated by cross-platform programming for more years than I care to remember, and my interest has recently been sharpened by the acquisition of a number of Apple Macs – both Intel and PPC (PowerPC). This article focuses primarily on some technical aspects of Qt, Trolltech’s cross-platform C++ toolkit which, as you may know, is the architectural core behind the KDE desktop on Linux. At the end, I show how easy it is to create a simple application without writing a line of code.

Making a MOC-ery Of It All…

One of the most interesting aspects of Qt is the signal-slot mechanism. This connects an event such as a button press, mouse-click etc., to a consumer of that event. Assuming that you’re familiar with C# (and by now, you jolly well should be!) you’ll know that the VS.NET form designer connects an event to its consumer by doing something like this:


this.OKButton.Click += new System.EventHandler(this.OKButton_Click);

Here, anytime the OKButton is clicked, the OKButton_Click method will be called. Under the hood, this is accomplished with C#’s delegate mechanism, which provides a type-safe link between the event consumer and producer. .NET delegates are also multicast, hence the += operator in the code above; one event can be “broadcast” to multiple recipients.

All fine and dandy, but not hugely portable unless you’re using the Mono C# compiler (more on that here). Qt’s creators wanted to be able to compile code using any reasonably modern, bog-standard C++ compiler. Accordingly, they created moc. Moc is a pre-processor which converts the special C++ “superset” used by Qt into plain-vanilla C++.

Note: If this seems odd, then think back to 1983 and the advent of C++. Stroustrup’s original Cfront pre-processor effectively translated C++ into straight C, making the new language available on any platform with a decent C compiler.

To get some idea of what moc does, (that’s Meta-Object Compiler, by the way) take a look at the following class declaration taken from Trolltech’s documentation:


class MyClass : public QObject
{
  Q_OBJECT

  public:
    MyClass(QObject *parent = 0);
    ~MyClass();

  signals:
    void mySignal();

  public slots:
    void mySlot();
};

The signals part of the class declaration identifies any signals that can be raised by the class. In this case, we have one signal; mySignal. The QLCDNumber class for example (one of the many user-interface gadgets in Qt) will emit an overflow signal if it’s asked to display a too-large number. You can thus think of signals as being somewhat like exceptions. However, a raised signal will do nothing if it’s not connected to a slot, unlike the way in which exceptions “percolate up” the calling stack until they find a handler. A signal is also very lightweight, requiring no special run-time support from the C++ library. Most important of all, the resulting standard C++ is completely portable.

Figure 1: This screenshot shows the output from moc.

Most of the grungy-looking stuff you can see is provided by the all-important Q_OBJECT macro, which must be the first thing in the class declaration. The class must inherit (directly or indirectly) from QObject, but you’ve probably already figured that out. In Figure 1 (the moc output screenshot), look in particular at the declaration of MyClass:mySignal. This calls QMetaObject::activate to send the signal to a connected slot. The other side of the coin is that an incoming signal maps down to a call on MyClass::qt_metacall which results in mySlot being invoked.

Note: Well, actually, it’s a tad more complex than that. You’ll notice that MyClass::qt_metacall can also invoke mySignal. That’s because another class might potentially want to raise your signal/s. If you don’t fancy someone else raising your signals (and who would?), you can always mark them private in the class declaration. Similarly, MyClass::qt_metacall is also used to change property values. (Qt supports properties which can be browsed at design-time.)

Slotting It All Together

Having delved around under the hood, how do things look from the perspective of an application developer? Well firstly, the plumbing mentioned above is pretty much hidden from the developer. If you use Qt’s qmake build utility, moc is invoked behind the scenes, and automatically takes care of handling the moc output. This gets linked into the executable or #include’d with the class implementation file (depending on whether the original class declaration was in a header or .cpp file respectively).

To raise a signal, you just use the emit keyword, like this:

emit mySignal();

The pre-processor converts this into the appropriate call to MyClass::qt_metacall. Signals and slots can take one or more arguments, just like ‘regular’ C++ methods. However, a signal can only be connected to a slot with a compatible signature, thus ensuring a type-safe connection. E.g.:

myButton = new QPushButton(this);
connect (myButton, SIGNAL(clicked()), this, SLOT(buttonClicked()));

This creates a new push button and connects the clicked signal of the button (first two parameters) to the buttonClicked slot of the current class (second two parameters). Interestingly, a signal that provides – for example – three arguments can also be connected to a slot that expects only two, one or zero. As long as the arguments expected by the slot are type compatible with what’s provided by the signal, the connection can be made; extra arguments are simply ignored. As with the .NET multicast delegates, a single slot can be connected to multiple signals and you can likewise connect a single signal to multiple slots.

Figure 2: screenshot Qt Designer running.

Qt comes with a visual design tool called…ummm…Designer. This can be used to lay out a Qt form complete with your choice of user interface widgets. As you’d expect from a cross-platform programming tool, life isn’t quite as simple as with (say) Delphi or .NET because you need to use additional layout components (vertical layout, horizontal layout and grid layout) to put together your form in a way that will look good regardless of platform and also cope with changes in form size. This will hold no surprises for Java developers.

Once done, Designer can connect up the various widgets with the appropriate signals and slots. Figure 2 shows part of Designer running on my iMac. Notice the way in which the clicked signals emitted by the two pushbuttons (OK and Cancel) on the right are automatically connected to the dialog window’s accept and reject slots (Cunningly marked with electrical ‘Earth’ symbols to show the slot is on the form). Hitting Return in the ‘UserName’ edit box emits a returnPressed signal which is linked to the OK button’s click slot.

The lower window shows the Signal/Slot editor. (When you’re not in Signal/Slot edit mode, you don’t have the connections overlaying the design-time form). As you can see, this is laid out in (Sender-Signal)(Receiver-Slot) order, just like the QObject::connect call’s arguments.

VS.NET 2005 Goes Cross-Platform

The Designer tool can be executed stand-alone, but if you’re developing on Windows, it can be tightly integrated into VS.NET 2005 through a VSIP plug-in. (Like what Borland should have done with Delphi, but don’t get me started on that…) With the plug-in installed, VS.NET loses its mild-mannered .NET persona, ripping off its T-shirt to reveal a fully-fledged C++ cross platform development system with a very credible GUI front-end. Did the ill-fated Borland C++BuilderX project get you slobbering after the idea of a cross-platform version of C++ Builder? Well, slobber no more; this is the reality.

What’s especially cool is the way in which the plug-in handles all the build steps behind the scenes. What Designer generates is an XML file with an extension of .ui – for user interface. The XML describes all the widgets on the form, their property settings, even the position of the little signal/slot connection boxes when viewing in this mode. The .ui file is passed as input to another Qt utility – uic, or User interface Compiler, which converts the user interface arrangement into a C++ header file prefixed with “ui_” that’s integrated it into your project. Like I said, all these machinations are transparent; it’s just a matter of hitting F5 in VS.NET and it all happens.

To show how this all pans out in practice, I took a very simple program from page 7 of the excellent book “C++ GUI Programming with Qt3” by Jasmin Blanchette and Mark Summerfield (Note: I understand that there’s a Qt 4 update to this book available – I’ve just ordered it from Amazon! [you can also buy this book at Register Books - Ed]) and built it on the Mac using Trolltech’s command-line tools.

Figure 3: Screenshot of a sample Qt program running on the Mac.

Unfortunately, the program didn’t work “as is” because I’m using Qt 4 and some of the classes used by the authors (most notably QHBox, one of the layout controls) are now deprecated.

For copyright reasons, I can’t show you the revised code listing because it’s probably too similar to theirs but, basically, I used a QWidget control as a containing box, adding the spin-box and slider to the box. I then created a horizontal layout control, QHBoxLayout, and used the addWidget method to add the two controls to this layout. Finally, the setLayout method is used to add the horizontal layout control to the containing box. This is all explained in Trolltech’s excellent reference documentation; and you can see the final result running on the Mac in Figure 3.

I typed this code in by hand, but I wouldn’t want to do this manually with a complex user interface. So, over to Windows and I repeated the exercise, this time entering absolutely no code, but using the Designer to lay out the form and connect the relevant slots on the slider and spin-box.

The heart of what’s generated by the uic compiler is a class called Ui_QTVCDemoClass (the project was called QTVCDemo, and the base form class is QTVCDemoClass). When the program starts running, a special method of Ui_QTVCDemoClass, setupUi, is called, passing it the instance of the form class that needs to be initialised. The code below shows the un-retouched output generated by uic:


#ifndef UI_QTVCDEMO_H
#define UI_QTVCDEMO_H

#include <QtCore/QVariant>
#include <QtGui/QAction>
#include <QtGui/QApplication>
#include <QtGui/QButtonGroup>
#include <QtGui/QHBoxLayout>
#include <QtGui/QMainWindow>
#include <QtGui/QSlider>
#include <QtGui/QSpinBox>
#include <QtGui/QStatusBar>
#include <QtGui/QWidget>

class Ui_QTVCDemoClass
{
public:
    QWidget *centralWidget;
    QHBoxLayout *hboxLayout;
    QHBoxLayout *hboxLayout1;
    QSpinBox *spinBox;
    QSlider *horizontalSlider;
    QStatusBar *statusBar;

    void setupUi(QMainWindow *QTVCDemoClass)
    {
    QTVCDemoClass->setObjectName(QString::fromUtf8("QTVCDemoClass"));
    QTVCDemoClass->resize(QSize(346, 58).expandedTo(QTVCDemoClass->minimumSizeHint()));
    centralWidget = new QWidget(QTVCDemoClass);
    centralWidget->setObjectName(QString::fromUtf8("centralWidget"));
    hboxLayout = new QHBoxLayout(centralWidget);
    hboxLayout->setSpacing(6);
    hboxLayout->setMargin(9);
    hboxLayout->setObjectName(QString::fromUtf8("hboxLayout"));
    hboxLayout1 = new QHBoxLayout();
    hboxLayout1->setSpacing(6);
    hboxLayout1->setMargin(0);
    hboxLayout1->setObjectName(QString::fromUtf8("hboxLayout1"));
    spinBox = new QSpinBox(centralWidget);
    spinBox->setObjectName(QString::fromUtf8("spinBox"));
    spinBox->setMaximum(130);
    spinBox->setValue(35);

    hboxLayout1->addWidget(spinBox);

    horizontalSlider = new QSlider(centralWidget);
    horizontalSlider->setObjectName(QString::fromUtf8("horizontalSlider"));
    horizontalSlider->setMaximum(130);
    horizontalSlider->setValue(35);
    horizontalSlider->setOrientation(Qt::Horizontal);

    hboxLayout1->addWidget(horizontalSlider);

    hboxLayout->addLayout(hboxLayout1);

    QTVCDemoClass->setCentralWidget(centralWidget);
    statusBar = new QStatusBar(QTVCDemoClass);
    statusBar->setObjectName(QString::fromUtf8("statusBar"));
    statusBar->setGeometry(QRect(0, 39, 346, 19));
    QTVCDemoClass->setStatusBar(statusBar);
    retranslateUi(QTVCDemoClass);
    QObject::connect(spinBox, SIGNAL(valueChanged(int)), horizontalSlider, SLOT(setValue(int)));
    QObject::connect(horizontalSlider, SIGNAL(valueChanged(int)), spinBox, SLOT(setValue(int)));

    QMetaObject::connectSlotsByName(QTVCDemoClass);
    } // setupUi

    void retranslateUi(QMainWindow *QTVCDemoClass)
    {
    QTVCDemoClass->setWindowTitle(QApplication::translate("QTVCDemoClass", 
                              "Enter Your Age", 0, QApplication::UnicodeUTF8));
    Q_UNUSED(QTVCDemoClass);
    } // retranslateUi
};

namespace Ui {
    class QTVCDemoClass: public Ui_QTVCDemoClass {};
} // namespace Ui

#endif // UI_QTVCDEMO_H

Notice the way that the slider’s valueChanged signal causes the spin-box value to update, whereas the spin-box valueChanged signal does the same for the slider. Notice also that the User Interface compiler automatically generates calls to QApplication::translate, making it dead easy to localise your application using other tools supplied by Trolltech. Speaking of which, all strings are UTF8 Unicode as well. (Hello, Borland? Are you listening?)

The key point here, of course, is that although there’s a lot of code, I didn’t have to write any of it! Just a few moments spent with Designer and then the job’s done. .NET-savvy readers will immediately realise that, from a functional point of view, Ui_QTVCDemoClass:: setupUi corresponds directly to the InitializeComponent method of the form class spitted out by the .NET form designer.

Figure 4: Screenshot of the previous sample Qt program running on Windows.

Figure 4 shows the Designer-generated code running on Windows XP. Not quite as pretty as the Mac OS X version, but you knew I was going to say that, didn’t you?

Summary:

Cross platform programming has come a long way since the days of XVT, Zinc and MEWEL. Qt gives you a solid RAD-based development toolkit for creating software indistinguishable from ‘native’ applications. In fact, on the Mac, I find the combination of Qt and C++ far more intuitive than the weird Objective-C language and the frighteningly non-intuitive Interface Builder. If you haven’t yet dipped a toe into cross-platform programming, give it a go! In future articles, I hope to look at other cross-platform solutions such as wxWidgets and Free Pascal. ®