Porting to Qt 4.2's Graphics View

by Andreas Aardal Hanssen

Graphics View is a new canvas framework for Qt 4. Superseding QCanvas, Graphics View has lots of new features, including item grouping, item interaction through events, and floating-point coordinates. Still, Graphics View's design is similar to QCanvas's, ensuring that the transition to the new framework is as painless as possible. In this article, we will port the "canvas" example included with Qt 3 to use Graphics View, demonstrating the porting process step by step.

Portedcanvas

Qt 3's canvas example consists of about 1000 lines of code. We will start with the original sources from the Qt 3 source package and port it one step at a time. At the end of the process, we will have two applications that behave exactly the same, but each using a different set of classes and a different version of Qt.

Making the Example Compile with Qt 4

We start by making a copy of the examples/canvas directory from Qt 3. Then, using a command-line interpreter set up for Qt 4.2, we run the qt3to4 porting tool on the example's canvas.pro file. This starts an automated in-place modification of the example, taking care of the most tedious parts of porting an existing Qt 3 application to Qt 4.

    $ qt3to4 canvas.pro
    Using rules file: qt-4.2/tools/porting/src/q3porting.xml
    Parsing...
    Convert file tmp/canvas/canvas.cpp? (Y)es, (N)o, (A)ll A
    Wrote to file: tmp/canvas/canvas.cpp
    Wrote to file: tmp/canvas/main.cpp
    Wrote to file: tmp/canvas/canvas.h
    Wrote to file: tmp/canvas/canvas.pro
    Writing log to portinglog.txt

The qt3to4 tool starts by asking whether or not it should convert canvas.cpp. Simply type A to tell it to port all files. The tool generates a log file called portinglog.txt that shows all modifications that were made.

The resulting program uses Q3Canvas and friends from the Qt3Support library. We must make a few manual modifications to make the example compile with Qt 4.

In this article's code snippets, we follow the diff program's convention and use a leading plus sign (+) to indicate that a code line is being added to the program. Similarly, a leading minus (-) identifies a line that must be removed.

  #include <qapplication.h>
  #include <qimage.h>
 +#include <QDesktopWidget>

In main.cpp, we must include <QDesktopWidget>, as QDesktopWidget is referenced indirectly from QApplication::desktop() at the end of the file. In Qt 3, this was not necessary because desktop() returned a plain QWidget pointer.

Two modifications are required in canvas.cpp:

 -  pixmap.convertFromImage(image, OrderedAlphaDither);
 +  pixmap.convertFromImage(image, Qt::OrderedAlphaDither);

 -  Main *m = new Main(canvas, 0, 0, WDestructiveClose);
 +  Main *m = new Main(canvas, 0, 0, Qt::WDestructiveClose);

In Qt 3, Qt was a class. Since most Qt classes derived directly or indirectly from Qt, we could usually omit the Qt:: prefix.

In Qt 4, Qt is a namespace, so we must either prefix every Qt member with the namespace name or add a using namespace Qt; directive at the beginning of our source files. The porting tool catches most of these cases but not all of them.

Porting from QCanvas to Graphics View

At this point, the canvas example is a stand-alone Qt 4 application that compiles and runs correctly. The only issue is that we rely on the Qt3Support library&emdash;more precisely on Q3Canvas. We have three options at our disposal:

In the rest of this article we will see how to carry out the third option. To help us in the process, we can consult the Porting to Graphics View page of the Qt online documentation.

Using the porting tables found there as a guide, we start by replacing Q3Canvas and Q3CanvasView with QGraphicsScene and QGraphicsView in all source files. We must also change the includes:

 -#include <q3canvas.h>
 +#include <QGraphicsItem>
 +#include <QGraphicsScene>
 +#include <QGraphicsView>

We also replace Q3CanvasItems with their closest equivalents in Graphics View:

Old ClassNew Class
Q3CanvasRectangleQGraphicsRectItem
Q3CanvasSplineQGraphicsPathItem
Q3CanvasSpriteQGraphicsPixmapItem
Q3CanvasTextQGraphicsSimpleTextItem

Unsurprisingly, if we try to compile the example now, we get many errors and warnings.

Compiling the Example Again

As a general rule when porting, it's a good idea to quickly bring the project up to a state where it compiles again, even if it doesn't work properly yet. Following this philosophy, the next step is to comment out any block of code that references functions that don't exist in the new API. This includes incompatible constructors and syntax errors. If a block of code accesses many functions that don't exist in the new API, we simply comment out the entire block.

For this example, commenting out all erroneous code is a five minute operation that quickly brings us to a point where the application compiles again. This approach is very useful, as it allows us to port one component at a time, testing it as we go. Once the example compiles, we can run it and see what it looks like:

Canvasport1

The result isn't very exciting yet: All we have is a dysfunctional menu system and a gray canvas area.

Fixing the Behavior

Let's start by fixing QGraphicsScene's constructor in main.cpp, so that the size of the canvas is back to what it was in the original example:

 -  QGraphicsScene canvas; // (800, 600);
 +  QGraphicsScene canvas(0, 0, 800, 600);

Unlike Q3Canvas, QGraphicsScene lets us specify an arbitrary top-left corner for the scene. To obtain the same behavior as before, we must explicitly pass (0, 0) to the constructor.

 FigureEditor::FigureEditor(QGraphicsScene &c,
       QWidget *parent, const char *name, Qt::WFlags f)
 -     // : QGraphicsView(&c, parent, name, f)
 +     : QGraphicsView(&c, parent)
  {
 +  setObjectName(name);
 +  setWindowFlags(name);
  }

In the FigureEditor constructor, we uncomment the base class initialization, we remove the name and flags arguments from QGraphicsView's constructor, and instead call setObjectName() and setWindowFlags() explicitly.

If we compile and run the example now, it will look exactly like the original example but without any items. This is a good indication that we are on the right track.

The next step is straightforward and quite fun to do: porting the item classes one at a time. For most cases, it's just a matter of dealing with code that we commented out ourselves. We will show how to port ImageItem, the base class for the butterfly and logo items.

 class ImageItem : public QGraphicsRectItem
  {
  public:
    ImageItem(QImage img, QGraphicsScene *canvas);
 -  int rtti() const { return imageRTTI; }
 +  int type() const { return imageRTTI; }
    bool hit(const QPoint &) const;

  protected:
 -  void drawShape(QPainter &);
 +  void paint(QPainter *, const QStyleOptionGraphicsItem *,
 +             QWidget *);

  private:
    QImage image;
    QPixmap pixmap;
  };

The rtti() virtual function in Q3CanvasItem is called type() in QGraphicsItem, so we simply rename it in the ImageItem subclass. Instead of drawShape(), we reimplement paint(), which plays the same role but has a different signature.

 ImageItem::ImageItem(QImage img, QGraphicsScene *canvas)
 -   // : QGraphicsRectItem(canvas), image(img)
 +   : QGraphicsRectItem(0, canvas), image(img)
  {
 -  // setSize(image.width(), image.height());
 +  setRect(0, 0, image.width(), image.height());

Since Graphics View items can be created as children of other items, we must pass a null pointer as the parent to the QGraphicsRectItem constructor to create a top-level item. Also, there is no setSize() function in QGraphicsRectItem; instead, we call setRect() and pass (0, 0) as the top-left corner.

 -void ImageItem::drawShape(QPainter &p)
 +void ImageItem::paint(QPainter *p,
 +                      const QStyleOptionGraphicsItem *,
 +                      QWidget *)
  {
  #if defined(Q_WS_QWS)
 -  p.drawImage(int(x()), int(y()), image, 0, 0, -1, -1,
 -              Qt::OrderedAlphaDither);
 +  p->drawImage(0, 0, image, 0, 0, -1, -1,
 +               Qt::OrderedAlphaDither);
  #else
 -  p.drawPixmap(int(x()), int(y()), pixmap);
 +  p->drawPixmap(0, 0, pixmap);
  #endif
  }

We turn the drawShape() implementation into a paint() implementation. The tricky part here is that with Graphics View, the QPainter is set up so that we must draw in local coordinates. As a consequence, we call drawPixmap() with (0, 0) as the top-left corner.

If we compile and run the application now, we obtain logos and butterflies stacked on top of each other in the windows's top-left corner:

Canvasport2

This shouldn't come as a surprise, considering that we've commented out several code sections, including the item placement code in Main::addButterfly(), Main::addLogo(), etc. Let's start by fixing addButterfly():

   QAbstractGraphicsShapeItem *i =
          new ImageItem(butterflyimg[rand() % 4], &canvas);
 -  // i->move(rand() % (canvas.width()
 -  //                   - butterflyimg->width()),
 -  //         rand() % (canvas.height()
 -  //                   - butterflyimg->height()));
 -  // i->setZ(rand() % 256 + 250);
 -  i->show();
 +  i->setPos(rand() % int(canvas.width()
 +                         - butterflyimg->width()),
 +            rand() % int(canvas.height()
 +                         - butterflyimg->height()));
 +  i->setZValue(rand() % 256 + 250);

QGraphicsItem::setPos() does the same as what move() did before, and setZ() is now called setZValue(). We also need a couple of int casts because QGraphicsScene's width() and height() functions now return floating-point values and % is only defined for integers.

Notice that we don't need to call show() anymore, because Graphics View items are visible by default. Similar changes are required in addLogo():

   QAbstractGraphicsShapeItem *i =
          new ImageItem(logoimg[rand() % 4], &canvas);
 -  // i->move(rand() % (canvas.width()
 -  //                   - logoimg->width()),
 -  //         rand() % (canvas.height()
 -  //                   - logoimg->width()));
 -  // i->setZ(rand() % 256 + 256);
 -  i->show();
 +  i->setPos(rand() % int(canvas.width()
 +                         - logoimg->width()),
 +            rand() % int(canvas.height()
 +                         - logoimg->width()));
 +  i->setZValue(rand() % 256 + 256);

If we run the example now, all butterflies and logos are placed at random positions as we would expect. Our example is starting to look more and more like the original. Next function: addSpline().

For Q3CanvasSpline, the natural transition is to port to QGraphicsPathItem, which is based on QPainterPath. QPainterPath supports curves, but its interface is different from that of Q3CanvasSpline, so some porting is required:

 -  // QGraphicsPathItem *i =
 -  //       new QGraphicsPathItem(&canvas);
 +  QGraphicsPathItem *i =
 +         new QGraphicsPathItem(0, &canvas);

    Q3PointArray pa(12);
    pa[0] = QPoint(0, 0);
    pa[1] = QPoint(size / 2, 0);
    ...
    pa[11] = QPoint(-size / 2, 0);

 -  i->setControlPoints(pa);
 +  QPainterPath path;
 +  path.moveTo(pa[0]);
 +  for (int i = 1; i < pa.size(); i += 3)
 +    path.cubicTo(pa[i], pa[(i + 1) % pa.size()],
 +                 pa[(i + 2) % pa.size()]);
 +  i->setPath(path);

Because QGraphicsPathItem is a generic vector path item (not a spline item), we must build the curve shape manually using QPainterPath's moveTo() and cubicTo() functions instead of calling Q3CanvasSpline::setControlPoints().

Porting the other items is straightforward, so let's skip ahead a bit and add some navigation support to the view instead. In Main::rotateClockwise(), we can replace three lines of code with one to get rotation support.

 void Main::rotateClockwise()
  {
 -  // QMatrix m = editor->worldMatrix();
 -  // m.rotate(22.5);
 -  // editor->setWorldMatrix(m);
 +  editor->rotate(22.5);
  }

We can do similar changes to the other functions accessible through the View menu (zoomIn(), zoomOut(), etc.). In Main::paint(), we must reintroduce printing support:

 void Main::print()
  {
    if (!printer) printer = new QPrinter;
    if (printer->setup(this)) {
      QPainter pp(printer);
 -    // canvas.drawArea(QRect(0, 0, canvas.width(),
 -    //                       canvas.height()),
 -    //                 &pp, FALSE);
 +    canvas.render(&pp);
    }
  }

The only change here is due to the differences in the signatures of Q3Canvas::drawArea() and QGraphicsScene::render(). It turns out we can leave out most arguments to drawArea() because suitable default values are provided with the new API.

That's it! We now have a fully functional canvas example based on the new Graphics View framework.

Conclusion

As you can see from this article, moving from Q3Canvas to Graphics View is a fairly straightforward operation, once we've understood the main differences between the two APIs.

There are a few pitfalls, though. Some Q3Canvas features that were targeted at 2D game programming, such as tiles and sprites, are not directly supported in Graphics View; the "Porting to Graphics View" page explains how to implement these features without too much hassle. In addition, existing code that implicitly relied on integer precision might still compile but behave differently with Graphics View.

The complete source code for the ported canvas example is distributed in Qt 4.2's examples/graphicsview/portedcanvas directory. Qt 4.2 also includes a portedasteroids example that corresponds to the Qt 3 asteroids example, which also used QCanvas.

To conclude, I would like to wish you good luck with porting to Graphics View! If you run into trouble, let us know so we can help and possibly improve the porting documentation.


This document is licensed under the Creative Commons Attribution-Share Alike 2.5 license.

Copyright © 2007 Trolltech Trademarks