Qt

Qt Layouts Before Pixel Pushing

I fixed a dialog today that looked fine in English and ridiculous in German. The original code positioned every Qt widget with coordinates, so a longer label pushed straight through the line edit beside it. Apparently translations had failed to respect our geometry. Very inconsiderate of them.

The obvious fix was not to adjust the numbers. It was to remove them.

In Qt 3, layouts already know how to negotiate widget sizes. A QVBoxLayout stacks rows, a QHBoxLayout arranges each row, and QGridLayout handles label-and-field forms without pretending every label has the same width.

QGridLayout *layout = new QGridLayout(this, 2, 2, 8, 6);
layout->addWidget(new QLabel(tr("Server name:"), this), 0, 0);
layout->addWidget(serverEdit, 0, 1);
layout->addWidget(new QLabel(tr("User:"), this), 1, 0);
layout->addWidget(userEdit, 1, 1);
layout->setColStretch(1, 1);

The margins and spacing remain explicit, but the widgets provide size hints and the second column absorbs extra room. The dialog now survives longer translations, a different font, and a user resizing it.

One small trap is mixing layouts with manual setGeometry() calls on the same children. The layout will win, usually just after the hand-tuned version looked correct on the developer’s machine.

I still use fixed sizes when the thing really is fixed, such as a small colour swatch. For forms and text, coordinates are merely tomorrow’s bug written as two integers. Let the layout do the dull arithmetic. It has more patience than I do.

Moving a KDE 3 Application to 3.1

After installing KDE 3.1, I rebuilt a small KDE 3.0 application expecting either complete success or a useful compiler failure. It built cleanly and then displayed a toolbar assembled from an older installed XML file. Compatibility had done its job; my installation habits had not.

The move from 3.0 to 3.1 is not the upheaval that the KDE 2 to KDE 3 port was. The first task should be a clean rebuild against the new headers and libraries, not a rewrite:

make -f Makefile.cvs
mkdir build-31
cd build-31
../configure --prefix="$KDEDIR" --enable-debug
make
make install

I check the configure summary to ensure it found the intended Qt and KDE prefixes. Having 3.0 under one prefix and 3.1 under another can produce a convincing but incoherent mixture of headers, libraries, services, and data files. kde-config --prefix and kde-config --path data help explain what the running session will search.

Installed resources matter as much as the executable. XMLGUI files, icons, desktop entries, service descriptions, and translations may survive from an earlier build. During testing I inspect the installed paths and remove only obsolete copies belonging to my application. Otherwise a changed action name in the source can appear to have no effect because the shell merged an old resource file.

I then exercise integration points: actions and standard shortcuts, session restoration, DCOP interfaces, KIO URLs, printing, and embedded parts. These boundaries are more likely to expose an assumption than an ordinary button click. Running from a terminal catches connection warnings and missing service messages that a polished window politely conceals.

If I choose to use a new 3.1 facility, I make that a separate change after the compatible build works. This keeps the required minimum version honest. Merely compiling on my workstation does not mean every user has the same minor release.

I also keep the old configuration file for one run and start with an empty one for another. Migration defects and default-setting defects often cancel each other on a developer’s account. Testing both states prevents an old saved value from making the fresh installation appear correct.

My preference is to preserve KDE 3.0 compatibility unless the new feature materially improves the program. The caveat is that testing both versions then becomes real work, including compile tests against both sets of development files. Compatibility declared but never built is just a hopeful comment with better publicity.

Porting an Application to KDE 3

KDE 3.0 is out, which turned my experimental port into work that other people may actually try to compile. The application was modest, but it touched enough of KDE 2 and Qt 2 to make a straight rebuild unrealistic. I expected a grand rewrite. The successful approach was much duller: establish a clean baseline, move one boundary at a time, and keep the program runnable.

I started with a separate source copy and a fresh build directory. Building outside the source tree made stale generated files much easier to spot.

make -f Makefile.cvs
mkdir build-kde3
cd build-kde3
../configure --prefix="$KDEDIR" --enable-debug
make

The first pass was entirely mechanical. I fixed renamed headers, methods whose signatures changed, and code that depended on implicit conversions no longer accepted by Qt 3. I did not improve the design while doing this. Combining a port with a cleanup makes each failure ambiguous, and ambiguity is already supplied free of charge by C++ error messages.

Qt’s collection classes deserved special attention. Code that stored pointers in a QList-style class or relied on automatic deletion needed its ownership checked rather than merely adjusted until it compiled. Qt 3’s classes are familiar, but familiar is not identical. I wrote down who owned each object, especially widgets, list items, and objects parented to a QObject. Parent-child deletion is useful only when the parent is the intended owner.

Signals and slots were the next boundary. Every class using them needed Q_OBJECT, and every connection needed matching argument types. The string notation hides mistakes from the compiler:

connect(action, SIGNAL(activated()),
        this, SLOT(openDocument()));

I ran the application from a terminal and treated every QObject::connect warning as a real defect. It is tempting to ignore one because the window still appears. That tends to last until the ignored connection is the Save action.

The KDE layer was easier once Qt was quiet. I checked command-line handling through KCmdLineArgs, application setup through KApplication, actions, standard shortcuts, and XMLGUI resource files. An old .rc file can leave menus looking plausible while actions are missing or duplicated. Removing installed test copies before checking the new one prevented Konqueror and the application from finding an obsolete resource in another prefix.

Configuration code also needed a deliberate pass. I kept reads and writes together and checked defaults rather than assuming the old configuration file contained every key:

KConfig *config = KGlobal::config();
config->setGroup("View");
bool showStatus = config->readBoolEntry("ShowStatusBar", true);

For document handling, I resisted replacing everything at once. The application already separated its document widget reasonably well, so I first made that compile, then considered whether it should become a KPart. A port is a poor time to discover that the document, window, and network code have secretly been one object wearing three hats.

The most useful testing sequence was deliberately boring:

  1. Start with an empty configuration directory.
  2. Open a local file from the command line.
  3. Open it through the file dialog.
  4. Modify, save, and reopen it.
  5. Exercise every action and shortcut.
  6. Close with unsaved changes.
  7. Repeat with an existing KDE 2 configuration.

I also built with debugging enabled and ran under gdb. A port often exposes old lifetime errors because widget destruction order or an event sequence changes slightly. The new library did not necessarily create the dangling pointer; it merely stopped arranging the furniture around it.

Binary compatibility was not a goal for local plug-ins compiled against KDE 2. They had to be rebuilt against the KDE 3 and Qt 3 headers and libraries. Trying to preserve old C++ objects across that boundary would save a compilation and purchase a much stranger crash.

Before calling the port complete, I also built a source archive and compiled that archive in an empty directory. A working source tree can quietly depend on generated files, unlisted headers, or resources left by yesterday’s build. make distcheck is useful when the project supports it, but even the plain exercise of unpacking the archive elsewhere catches embarrassing omissions. I then installed into a temporary prefix and started a KDE session with that prefix in its search path. This tests what was shipped rather than what happens to be within reach of the compiler.

Warnings received their own pass after behavior was correct. I enabled the warnings supported by the older compiler and looked especially for suspicious casts, hidden overloads, and variables whose type changed under Qt 3. I did not turn every warning cleanup into part of the port, but I did investigate each one. A warning newly exposed by a library transition often points directly at an assumption that deserves daylight.

My preference after this exercise is to make the smallest source change that produces correct KDE 3 behavior, then do cleanup separately. The port remains reviewable, regressions have fewer hiding places, and users get a working application sooner. It is not heroic engineering, but heroic engineering has an alarming habit of requiring heroic debugging.

What moc Actually Generates

I added a signal to a small Qt 3 class today, rebuilt, and received an undefined reference to the class’s virtual table. That message sounds like a C++ problem. In this case it meant I had forgotten about moc.

Qt’s signals and slots need information that the C++ compiler does not produce. A class using them declares Q_OBJECT:

class Counter : public QObject
{
    Q_OBJECT
public:
    Counter(QObject *parent = 0, const char *name = 0);

signals:
    void changed(int value);

public slots:
    void setValue(int value);
};

The Meta Object Compiler reads that declaration and writes ordinary C++ containing the meta-object table and dispatch code. The generated file supplies, among other things, the pieces behind className(), signal activation, and calls made through the string-based connect() mechanism.

In a KDE autotools project the build rules normally run moc for headers containing Q_OBJECT. If I put the class declaration in a .cpp file, I include its generated output at the bottom:

#include "counter.moc"

After adding or removing Q_OBJECT, regenerating the build files is sometimes necessary. A clean rebuild is also cheaper than interpreting every vtable error as an omen.

I like signals and slots because the sender does not need to know the receiver. The caveat is that the compiler cannot check the old SIGNAL() and SLOT() strings fully. A connection can compile and then complain at run time, so I watch the diagnostic output and keep signatures simple.