What's the matter with Qt?

Over the years, I have used countless APIs to program user interfaces. None have been as seductive and yet ultimately disastrous as Nokia's Qt toolkit has been.

While I strongly hate to criticize the free work of others, I feel it necessary to warn aspiring developers of the dangers of using this toolkit.

The good

Qt brings a lot to the table. Cross-platform development, 'native-like' widgets, classes for just about anything you could want to do (even things completely unrelated to GUIs), and a very clean coding style.

When compared to trainwreck APIs like native GTK+, it's very easy to decide on Qt at first glance. Let's compare the two:

Setting the current row in a Qt tree view:

treeView->setCurrentIndex(row);

Setting the current row in a GTK+ tree view:

void TreeView::setSelection(unsigned row) {
  GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(object->subWidget));
  GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(object->subWidget));
  gtk_tree_selection_unselect_all(selection);

  GtkTreeIter iter;
  if(gtk_tree_model_get_iter_first(model, &iter;) == false) return;
  if(row == 0) {
    gtk_tree_selection_select_iter(selection, &iter;);
    return;
  }
  for(unsigned i = 1;; i++) {
    if(gtk_tree_model_iter_next(model, &iter;) == false) return;
    if(row == i) {
      gtk_tree_selection_select_iter(selection, &iter;);
      return;
    }
  }
}

I wish I was simply being facetious, but you just can't make something that bad up. Cocoa fares no better ... let's compare making a label.

Creating a label with Qt:

layout->addWidget(new QLabel("Hello, world"));

Creating a label with Cocoa:

nameLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 70, 285, 20)];
[nameLabel setAlignment:NSCenterTextAlignment];
[nameLabel setBordered:NO];
[nameLabel setDrawsBackground:NO];
[nameLabel setEditable:NO];
[nameLabel setFont:nameFont];
[nameLabel setStringValue:@"Hello, world"];
[[self contentView] addSubview:nameLabel];

You can argue that you'll use Interface Builder, which is great if you have a static interface that never changes. But then you could use a GUI designer to write AS/400 or XForms code. That doesn't mean those APIs are suddenly just as good as Cocoa, does it?

With Qt, you will definitely be able to write GUI code far faster than with pretty much any other major API. But as nice as Qt is to program for, your users will pay the price for your convenience.

The bad

Unfortunately, the cold hard reality of programming is that it's not about developer comfort. Development is something done by a small number of people, whereas everyone has to use the end result. Even though GTK+ is far more painful to use, it is a better choice in the end for GNOME/Xfce users.

So what are the shortcomings of Qt, then?

Feature bloat

The most obvious problem is Qt's sheer size. Qt weighs in at a massive 150MB download. The run-time DLLs just for the core application are about 15MB. Add in WebKit, and all the other nice features, and you're looking at 40MB.

If you are writing the next Photoshop or Opera, this certainly isn't a huge deal. But when your hello, world app needs 15MB of DLLs that no computer out there has already, it can become a problem. When you have more than 100 users and you have to pay the hosting bill, it can become a problem. When you have dial-up users, it can be a problem.

The absolute best you can do is to sacrifice speed, and compile Qt yourself. A process that ordinarily takes hours, but you can reduce it to about a half an hour if you explicitly strip off all the parts you don't want. Once finished, you can further shrink the Qt run-times by using the UPX executable packer, so long as you don't mind half-assed anti-virus software (read the majority of anti-virus software out there) detecting it as a threat, because one or two viruses just so happened to use UPX themselves at one point. All of this effort will net you a 4MB Qt run-time with a fraction of the feature set. And now anyone who wants to compile your software also gets to download the 150MB package, modify the build rules, type out seven lines of configuration options, and wait half an hour.

Yes, it may be really nice being able to draw your widgets on a 3D canvas, but you will pay a heavy price for that flexibility.

Reinventing the standard library

This is something I am guilty of myself. Let's face it, the C++ standard library leaves a lot to be desired, at least in C++98. Ask any serious programmer and they'll at least recommend boost.

While Qt provides some very nice containers, it's one more hassle for your project to now have std::string and QString, std::list and QList, and on and on. And you get to convert back and forth constantly.

Qt internally uses UTF-16

One of the dumbest mistakes ever made was Microsoft rushing to implement Unicode support. UTF-16 is the most useless encoding ever conceived. The whole point of Unicode was to represent every possible character, but 64k characters was hopelessly naive for every language in the world. So of course, they ran out of characters, and you now have surrogate pairs. The whole point of using UTF-16 was to allow O(1) addressing of individual characters and to be able to assert that string length was equal to character length, but you can't do that unless you want to rule out thousands of Chinese characters. UTF-32 at least serves a purpose.

Microsoft, rather than admit they made a mistake in creating two versions of every last data structure and API function, and go back and allow a UTF-8 locale that was 100% backward-compatible with their existing ANSI functions, kept on with their heads buried in the sand. And Qt was happy to march along with them.

This will come back to haunt you when you are working with operating systems that get it right with UTF-8 like OS X and Linux. You will become very familiar with QString::toUtf8().constData() and QString::fromUtf8().

Signals and slots

The Qt developers have apparently never heard of function pointers, and so decided to implement their own custom pre-processor called moc. So rather than simply tell Qt which function you want called in response to a user interface action, you have to create a new class, inherit from QObject, declare the Q_OBJECT macro, create a custom "public slots:" section, run moc on this file, and include the output file into your project, and make sure that moc is run any time the header is changed. Forget about trying to have a single source file with the interface and implementation, it's not going to happen.

The Qt developers have a great solution, they want you to replace your make with their very own qmake. Which is great for hello world apps, not so great for real-world applications with very complex build systems. It's quite the nightmare, but it is at least technically possible to automate moc generation.

Perhaps the most tragic flaw of this is that it makes taking advantage of C++0x lambda expressions impossible. Let's compare the two.

Using callbacks with Qt:

//file.moc.hpp
class SaveButton : public QObject {
  Q_OBJECT
  ...
public slots:
  void doSave();
};

//file.cpp
connect(this, SIGNAL(triggered()), saveButton, SLOT(doSave()));
void SaveButton::doSave() {
  file.saveToDisk();
}

Using callbacks with C++0x:

saveButton.onClick = []() { file.saveToDisk(); };

But these are all aesthetic issues, you're going to suffer some bad design no matter which popular API you go with. Now, the real problem with Qt ...

BUGS, BUGS, AND MORE BUGS

Quite likely because of the feature bloat, Qt has more bugs than Joe's Apartment.

The whole point of cross-platform development is write-once, run-anywhere. And Qt fails miserably. Because you will find new and 'exciting' bugs on every platform you try and run it on. This is the insidious part. You won't know it until you start coding for Qt and running into them yourself. Or more accurately, when your users start running into them for you, because the bugs can just be so ridiculously subtle, things you'd never think to test in a million years. I can promise you that your users won't appreciate feeling like your beta testers. I'll go over some of the ones I have encountered.

QDir has O(n^2) complexity, but only on Windows

QDir is used to get the files in a directory. For those not familiar with Big O notation, O(n^2) means there is quadratic growth overhead. A folder scanning API should always be O(n), or linear growth.

Linear growth: 1, 2, 3, 4, 5, 6, 7, 8, ... 999
Quadratic growth: 1, 4, 9, 16, 25, 36, 49, 64, ... 998001

As you can see, it becomes increasingly slow as more files appear in a directory. Modern computers are so fast that you won't notice until you get to about 1,000 files or so. And then things will literally slow to a crawl. Regardless of what you think about folders with that many files, the fact is that people do have folders like that. And telling the user not to do that is not an acceptable solution.

The worst part about this bug? The Qt devs have known about it since 2002. They don't care.

Choice quote: "Yes, I see. now I am using FindFirstFile/FindNextFile to replace QDir when my program is compiled for Win32. it speeds up about 5000%." Five-thousand percent.

As a result of this bug, I found my business logic application absolutely deadlocking when we added a scan destination folder that had 15,000 files. After half an hour, it hadn't finished reading in that one single folder. An emergency workaround was needed as this was a live production application.

QObject::sender() returns mystery values

Here's a function that lets you know what signal triggered your slot. It's really handy when you have, say, ten menu items that are identical in nature, but they affect different save slots, for instance. Why implement ten callback functions when you can just see which slot you were coming from via sender()?

A great way to do that, store the slot# in the QAction::data() field, and dynamic_cast your sender() to a QAction. dynamic_casting a pointer is fantastic, it even safely handles null pointers passed to it, and never throws an exception. But there is one thing that will kill it: passing it a pointer that isn't of the base type at all. And this is exactly what Qt does, but only on Qt 4.6.0+ for Windows.

As a result of this bug, the load/save state menu I made and successfully and extensively tested on Linux ended up crashing when used on Windows.

Pressing a key or moving the mouse cuts application performance in half

This bug was introduced in the 4.6 betas, and was mere days away from being in 4.6.0 official. It only affected Windows.

I know, this one is hard to believe. See for yourself.

As a result of this bug, when users would use their keyboard or mouse for input in my SNES emulator, their framerate would instantly be cut in half until they stopped pressing any keys.

As I used a gamepad that Qt didn't poll, I did not notice the problem until it was too late: after a public release had been made.

Menus attached to buttons are invisible with the Windows XP theme

Relevant link. This one was fun, and resulted in users not being able to assign mouse buttons for input nor turn off a side panel in my custom image browser window.

QDialog does not redraw on user actions when a QTimer is active

Relevant link. I used an external DLL to implement 7-zip multi-archive loading, and since the API call needed to return the file name, I needed the sub-dialog to choose the file inside the archive to be modal. I chose to use a QDialog for that, while my main application had a QTimer that was used for various GUI updates. As a result, Windows users were presented with a ROM list that was completely unresponsive to any of their commands. But only via painting. It was working, they just couldn't see the results of choosing different files and clicking the OK button.

Style sheets are extremely unstable

I've had countless problems with style sheets. In the 4.5 series, there were severe inheritance problems if you tried to use style sheets on any widgets derived from a core Qt class. Working around that required turning:

class MyButton : public QButton {};

Into the more austere:

class MyButton : public QObject { QButton *button; };

And then there was a fun bug as of 4.6 for Windows. After spending 20 minutes building a profiled binary, I realized that my qlineargradient effect on a checkbox background resulted in the checkbox itself being pitch black, regardless of its state.

QFileDialog::getOpenFileName() shows a blank window

This was a bug with the GTK+ style theme on Linux. Because my main window had a timer, it was superceding the creation and drawing events of the file dialog. Another bug I missed, this time because I was using the Cleanlooks theme (I personally don't feel the GTK+ theme is a very good impersonation of GTK+, it ends up with that Uncanny Valley effect.)

So I guess not only am I supposed to test every possible GUI feature on every single platform, I'm also supposed to test every single theme engine for every single platform as well. Really makes me just not want to ever bother touching the GUI.

Clicking on a QComboBox does nothing, and text is truncated everywhere

Just some of the fun rendering bugs I've run into with the first few major releases of Qt for Cocoa. They've been mostly resolved by this point.

The main menu bar doesn't work at all

Remember all my problems with timers above? I tried to avoid that by using QApplication::processEvents() to force a poll-driven interface rather than the more standard event-driven interface. Worked great on Windows and Linux, but unfortunately Qt for Cocoa didn't get the memo. None of the menu items were actually clickable as a result. You must use QApplication::exec() if you want your application to work on OS X.

Oh, and be prepared for all kinds of fun with menus on OS X. It's a real train wreck, even with all the help Qt gives you.

Resizing windows is next to impossible

Take a look at all the code I need just to resize a window.

void Utility::resizeMainWindow() {
  //process all pending events to ensure window size is correct (after fullscreen state change, etc)
  usleep(2000);
  QApplication::processEvents();

  unsigned screenWidth, screenHeight;
  if(config().video.isFullscreen == false) {
    screenWidth = QApplication::desktop()->availableGeometry(mainWindow).width();
    screenHeight = QApplication::desktop()->availableGeometry(mainWindow).height();
  } else {
    screenWidth = mainWindow->canvasContainer->size().width();
    screenHeight = mainWindow->canvasContainer->size().height();
  }

  unsigned region = config().video.context->region;
  unsigned multiplier = config().video.context->multiplier;
  unsigned &width; = display.outputWidth;
  unsigned &height; = display.outputHeight;
  width = 256 * multiplier;
  height = (region == 0 ? 224 : 239) * multiplier;

  //ensure window size will not be larger than viewable desktop area
  constrainSize(height, width, screenHeight);
  constrainSize(width, height, screenWidth);

  if(config().video.isFullscreen == false) {
    mainWindow->canvas->setFixedSize(width, height);
    mainWindow->show();
  } else {
    mainWindow->canvas->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    mainWindow->canvas->setFixedSize(width, height);
    mainWindow->canvas->setMinimumSize(0, 0);
  }

  //workaround for Qt/Xlib bug:
  //if window resize occurs with cursor over it, Qt shows Qt::Size*DiagCursor;
  //so force it to show Qt::ArrowCursor, as expected
  mainWindow->setCursor(Qt::ArrowCursor);
  mainWindow->canvasContainer->setCursor(Qt::ArrowCursor);
  mainWindow->canvas->setCursor(Qt::ArrowCursor);

  //workaround for DirectSound(?) bug:
  //window resizing sometimes breaks audio sync, this call re-initializes it
  updateAvSync();
}

Qt really, really, really doesn't like it when you toggle between a fixed window size and a variable window size. You have to jump through many hoops, and force it to reprocess pending messages, otherwise your following commands have no effect.

I used to have window centering code, and to get it to work on Xorg, I actually had to call it eight times in a row. It usually finally took and centered properly after the second or third try. Rarely, not even eight tries would be enough, even with QApplication::processEvents() between every call.

Oh and look, another Qt bug in the comments there. I forgot about that one.

The context menu fails to work and fails to go away

So I keep mentioning Qt 4.6.0, why not upgrade to a higher version? Every version after has a problem where if you click on the context menu (the program icon at the top left of your window), it will appear on top of your form, but not respond to any user input, and it won't go away. It just keeps obstructing your main window's view.

The first checkbox item in a QTreeWidget does not repaint

A bug in the 4.5.x series, and the 4.6 betas. Only affects Windows.

And best of all ...

All of these bugs I've listed here? They're just off the top of my head. Had I kept a list, I'm sure I would have at least twice as many to report here.

So what do I recommend?

Unfortunately, by being so ambitious, by having virtually no QA testing, and by basically wrapping all of the native OS functions, you end up with a lot of additional bugs. By very definition, there can't possibly be less bugs since it's mostly a wrapper around the OS you are using Qt on.

It may be absolute torture to code for APIs like Cocoa and GTK+, but if you want a truly native look and feel, and if you care about your users having a stable, bug-free application, it's your only choice.

But what about other toolkits?

There are plenty of them. Most notably, wxWidgets has an MFC-complex and each port that isn't for Windows suffers from varying degrees of incompleteness. But they all end up like Qt in the end, it's indemic to their very design.

In closing

I don't believe the solution to this problem is to create super-monolithic APIs that try and encapsulate everything you'd ever want to do in your life.

The approach I've taken to is to once again create an API wrapper of my own, but to limit the scale of it as much as possible. Use only the things I absolutely need, and not give in to the desire to make the application look like candy. As nice as it is, stability is more important than aesthetic. A lesson I've learned very well over the past few years thanks to Qt.

Once you have this minimal API abstraction, you will of course have to port the application over to any new OS. But unlike trying to port something impossibly big like Qt, a minimal API wrapper with only the controls you actually use and need is a weekend project at best. Anyone can do it.

And the best part is, you get to code your way. No more crazy API inconsistencies. No more annoying design decisions.

Still, it's certainly not ideal. Unfortunately, there is no silver bullet to cross-platform GUI design, in any language. Operating system manufacturers are simply far too eager to differentiate themselves from the competition, and that makes trying to target exactly one interface impossible. There's just no avoiding it, even with your minimalist abstraction, you're still going to have to hand-tweak each platform you target to handle their subtle human interface guidelines. But that doesn't mean you can't be smart about it and minimize the amount of code duplication.

Corrections

An honest mistake, in the "QDir has O(n^2) growth" section, I listed an example that was O(2^n), which was misleading. That has been corrected. But please understand that quadratic growth in place of linear growth is still a very, very serious design flaw in Qt's QDir implementation.

Part of Qt's use of its own standard library and pre-processor was a result of these features not being available for Qt3. However, that doesn't excuse their continued use in Qt4, which is not API-compatible with Qt3 anyway.

Copyright © 2004–2013 byuu


Re-stored by Kawa.