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.