Qt

Learning Graphics View the Slow Way

I wanted to draw a simple network diagram: a few hundred boxes, some labels, and lines joining them. My old version used a custom widget with a large paintEvent(). It worked until I added selection, dragging and zooming. Then every mouse press became a small geometry examination, and every repaint redrew the universe.

Qt 4.2 already had Graphics View, but I postponed learning it because the three principal classes sounded like bureaucracy. This week I finally moved the diagram to QGraphicsScene, QGraphicsView and a handful of QGraphicsItem subclasses. Naturally, my first implementation reproduced most of the mistakes from the custom widget, only with newer nouns.

I created every item at viewport coordinates and moved them whenever the scroll bars changed. That made scrolling jitter, selection rectangles miss their targets, and zooming move the diagram away from the mouse. I then tried compensating with more coordinate conversions. By noon I had code converting a point from the view to the scene, back to the view, and possibly into a neighbouring dimension.

The useful idea is that the scene owns a stable logical coordinate system. Items live in scene coordinates, while the view is merely one camera onto that scene. Scrolling and zooming alter the camera transform; they should not rearrange the objects. Mouse positions arriving at the viewport can be mapped into the scene once, at the boundary, and item-local coordinates can be left to the item.

After removing my scroll-bar corrections, panning became smooth. Scaling the view around an anchor also stopped corrupting the stored positions. More importantly, a second view could display the same scene at a different zoom without duplicating the graph. That one experiment explained the separation better than the class names did.

Painting required another correction. My naive item painted outside its boundingRect() to make room for a shadow. Sometimes the shadow remained on screen after the item moved, producing a trail that looked like a very cheap supernatural effect. The scene uses the bounding rectangle to decide what may need repainting and which items intersect an area. If the paint escapes that promise, the scene has no reason to clean it up.

I expanded the rectangle to include the pen and shadow, then supplied a tighter shape() for hit testing. Those two concepts need not be identical. The bounding rectangle should be cheap and conservative; the shape can describe the clickable outline more accurately. With that split, selecting a line no longer required clicking somewhere in its enormous rectangular aura.

Performance was my next concern. I assumed one item per node, label and connector would necessarily be slower than one giant paint function. At first it was, because each node recomputed text dimensions and connector paths during every paint. Caching those values when the data changed made a larger difference than reducing the item count. The view can already avoid painting items outside the exposed region, something my giant widget did not do intelligently.

There are still choices rather than magic switches. Device-coordinate caching helps items that do not change but are viewed repeatedly; it is less attractive for items constantly transformed. Indexing the scene accelerates lookup in a mostly stable diagram, but updating an index while thousands of objects move can cost more than a simple scan. The correct setting depends on whether the scene behaves like a map or like a box of agitated insects.

The new event handling is also cleaner. Instead of asking every object whether a mouse point belongs to it, I let the scene deliver events to the topmost suitable item. Movable and selectable flags cover ordinary interaction. A custom item only handles a mouse event when its behavior is genuinely special. This removed an entire home-grown selection system, including one bug where hidden nodes could still be dragged.

Qt 4.3 adds useful refinements, but the important lesson is already present: Graphics View is not a faster canvas. It is a retained scene with explicit geometry, coordinate mapping, event delivery and multiple views. It rewards honest descriptions of where an item is and what it paints. Lie about either and it retaliates with artifacts that appear random but are, annoyingly, deserved.

My practical conclusion is to use a plain custom widget for drawings that are truly one object and have simple interaction. Once the drawing contains independent things that move, select, overlap or need more than one view, Graphics View earns its three-class introduction. Start in scene coordinates, keep bounding rectangles accurate, cache computations rather than blindly caching pixels, and let the framework perform the hit testing. It is less code, but only after deleting the code written to defeat it.

KDE 4 Alpha on a Spare Partition

I installed a pre-alpha KDE 4 SVN snapshot on a spare partition because running it beside my normal files seemed like a more entertaining mistake than merely reading about it. The announced alpha release was due the next day; this was still a snapshot from the day before. The packages installed cleanly, the session appeared in the login manager, and that was approximately where normality ended.

My naive plan was to reproduce my KDE 3 setup immediately: restore the same panel arrangement, point everything at the same home directory, and judge whether the new desktop was faster. That plan survived for perhaps ten minutes. Several settings had no equivalent, applications still looked inconsistent, and the desktop shell was plainly being assembled while I watched.

What does work is more interesting than the rough edges. Qt 4 painting feels noticeably less burdened when windows resize. The new artwork gives the session a coherent direction rather than the collection-of-themes appearance I feared. A few applications already show cleaner separation between their data and their widgets, which should matter long after the screenshots stop being novel.

What does not work is still substantial. The panel is not something I would trust for a working day. Desktop actions occasionally disappear. Some configuration modules are present but ineffective, a particularly refined form of optimism. Logging out once left processes behind, which then made the next login look broken until I killed them from a console.

This is not KDE 3 with fresh icons. KDE 4 is changing the toolkit, desktop shell, hardware and multimedia layers, and a fair amount of application plumbing at once. That explains why familiar checkboxes cannot simply be carried over. It also means measuring this snapshot as a replacement desktop misses the point.

After a second installation with a clean test user, many of the mysterious failures vanished. Sharing an old configuration directory had introduced stale service files and assumptions from KDE 3. The snapshot still crashes and still lacks ordinary conveniences, but at least those failures now belong to the new code rather than to my archaeological collection of dot files.

Don’t install it on the account used to earn money. Still, the architecture is visible enough to poke at, and a bug report from a clean account may help. Complaining that a pre-alpha snapshot is unfinished is accurate, but not exactly investigative journalism.

Qt Model/View Without the Magic

I added sorting to a log viewer with QSortFilterProxyModel, clicked the third visible row, and opened the wrong log entry. The view had done exactly what I asked. I had used a proxy row as an index into the source list.

A proxy has its own indexes and its own order. Once sorting or filtering is active, row 3 in the view says nothing useful about row 3 in the source. The handoff needs to be explicit:

QModelIndex proxyIndex = view->currentIndex();
QModelIndex sourceIndex = proxy->mapToSource(proxyIndex);
if (sourceIndex.isValid())
    openEntry(sourceIndex.row());

The reverse direction matters too. When the parser reports a source index that should be selected, mapFromSource() finds its current position in the proxy. Calling view->setCurrentIndex() with the source index may look plausible, but the index still belongs to a different model and should not be handed to that view.

My next bug was staler. I changed a severity threshold stored by the proxy, emitted a signal from the dialog, and nothing disappeared. The filter result was cached. After changing the threshold, the proxy had to invalidate its filtering so that filterAcceptsRow() ran again:

void LogProxy::setMinimumLevel(int level)
{
    if (minimumLevel == level)
        return;

    minimumLevel = level;
    invalidateFilter();
}

Resetting the source model also made the screen update, but it was the wrong hammer. It discarded selections and told every attached view that the source structure had vanished when only the proxy’s mapping was stale.

Dynamic sorting has a similar edge. If a source value used by the sort or filter changes, the source must emit dataChanged() for that value. The proxy can then reconsider the affected mapping. Without that notification, asking the proxy to repaint merely gives it another chance to use the same old answer.

Selections and drag-and-drop code are where I now look first for mapping mistakes. Anything crossing the proxy boundary should say which side its index belongs to. That small bit of bookkeeping beats opening the wrong error at two in the morning.

Porting a Qt 3 Dialog to Qt 4

Qt 4.0 is out, so I chose a small Qt 3.3 dialog as a porting exercise. “Small” was important. Porting an entire application before understanding one dialog is how weekends acquire stack traces.

My first attempt was purely mechanical: change includes until the compiler became quiet. It became quieter, but not helpful. Qt 4 reorganizes classes into modules, changes parts of the collection and painting APIs, and separates concerns that older code often mixed together. A successful compile would not prove that I had preserved ownership, event behavior, or rendering.

I started again by writing down the dialog’s behavior. It displays a list of files, accepts a selection, paints a small preview, and emits a result. That description gave me four pieces to port and test independently instead of one pile of compiler diagnostics.

The build was the first useful boundary. The project now names the Qt modules it uses rather than assuming one enormous library. Includes should name the class headers actually required. This exposed accidental dependencies that had been hidden by broad headers in the old source.

Collections needed care, but not cleverness. I resisted making a grand compatibility wrapper around every changed type. This application has one current port, not a treaty organization. At each call site I checked iterator behavior, copying, and whether Qt or standard C++ ownership applied.

The list view was the largest conceptual change. Old code tended to let a widget store items and presentation behavior together. Qt 4’s item-view approach makes the model explicit. My naive response was to force the old item manipulation into the new widget. The cleaner solution was to expose the file data through a model and let the view handle selection and display.

That split initially looked like extra ceremony. It paid for itself when I wanted to sort the view without rearranging the underlying file objects. Presentation order and data ownership are not the same thing, and the API now encourages me to admit it.

Painting also demanded attention. The preview had drawn directly in a way that relied on Qt 3 behavior. In Qt 4 I kept custom drawing inside the paint event, used the supplied paint device, and treated coordinates and clipping as part of the callback contract.

void Preview::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    painter.setClipRegion(event->region());
    painter.drawPixmap(0, 0, previewPixmap);
}

This is deliberately plain. Caching the pixmap avoids repeating file work during repainting, while the widget remains responsible for drawing itself when requested. Calling painting code from arbitrary places had seemed direct in the old version; it was actually borrowing assumptions from the event loop.

Object ownership survived mostly intact because parent-child relationships still provide a clear lifetime for QObject instances. I nevertheless checked every allocation. A port is a good time to discover that an object was parented only by luck or that two cleanup paths both believed they were in charge.

Signals and slots remain familiar, which made them easy to overlook. I checked each important connect() and exercised the dialog through the keyboard as well as the mouse. Compilation catches C++ type errors; it does not guarantee that a runtime signal signature agrees with its slot.

I kept the Qt 3 version running beside the port and compared behavior with the same small directory. Selection changes, empty lists, unreadable files, cancellation, and repeated opening all went on a short test sheet. The empty case found a stale selection pointer that the happy path had concealed.

The temptation during a port is to redesign everything disliked in the old code. I limited changes to those required by the new APIs or needed to make the behavior testable. Otherwise a regression becomes impossible to classify: was it introduced by Qt 4, the port, or my spontaneous improvement campaign?

My early opinion is that Qt 4 asks C++ applications to make more structure explicit, particularly around data and views. That can feel verbose when porting a tiny widget, but it scales better than teaching every widget to own, sort, and display its own private universe.

The useful conclusion is to port by responsibility, not by compiler error. Establish a build boundary, preserve observable behavior, understand the replacement mechanism, and test ownership and callbacks. The compiler is an excellent assistant, but it has never used the dialog and is therefore a poor product manager.

QObject Ownership Is Not Garbage Collection

I found a leak in a small Qt 3.3 dialog and responded by deleting every child widget in the destructor. Then I noticed that the widgets already had the dialog as their parent. My heroic cleanup was redundant; the actual leak was an object with no parent at all.

Qt’s object trees are straightforward once I stop pretending they are magic. A QObject with a parent joins that parent’s child list. Destroying the parent destroys its children. Top-level objects and objects without parents remain my responsibility.

My useful check is now mechanical. At construction, I ask who owns each allocation. If the answer is a parent object, I do not delete it separately. If there is no parent, I give the lifetime an explicit home in C++ code. Layouts arrange widgets; they do not replace ownership reasoning.

QLabel *label = new QLabel(tr("Status"), this);

Here this is the relevant part, not new. The parent makes the intended lifetime visible and ties cleanup to the dialog.

This is not garbage collection. Qt does not discover arbitrary unreachable C++ objects, and a parent cannot rescue a dangling pointer used after destruction. It is a disciplined ownership convention layered over C++.

The conclusion is pleasantly small: establish one owner and make it obvious at construction. My destructor became shorter, which is about the nicest possible result of debugging C++ memory management.