Crossplatform Thoughts on Psi/Mac and the Lowest Common Denominator
I’ve been thinking about the issues of crossplatform programming lately now that I want to improve Psi/Mac. The program feels alien to MacOS X for two reasons:
- Widgets do not match those of native applications, both in looks and in behavior
- The organization of the application does not make sense when compared to one that is native.
Native widgets are a hot issue. Qt works by emulating the widget look & behavior of the platform. Under Windows, this is very easy to do, since the widgets are quite basic. Also, the notion of a “native widget” under Windows is less defined. Many applications get away with having their own widgets, and so the Windows look and feel has never been drastically changed. Even Windows XP, which supports theming, does so by grafting a simple look & feel system atop the usual widgets. Qt supports XP themes quite well. Under X11, there is no such thing as a standard widget. I suppose you could say that would be Motif, and Qt does a fine job of emulating it (even though I can’t stand it), but these days people run all sorts of fancy KDE desktops using advanced styles like Keramik. However, KDE is Qt-native, so this isn’t really any trouble for Qt. Instead of Qt trying to figure out how to look like the desktop, the desktop is telling Qt how to look. Try Qt under GNOME, and we lose all sense of native look and feel. The Mac is by far the most troublesome style to emulate, not just because of complexity (it is complex), but also because it is so high-level that Apple can do things like change the TabWidget look and all apps conform to it. Qt/Mac on Panther (OS X 10.3) shows the older TabWidget style, emphasizing this problem.
Why does Qt not use native widgets? Trolltech will tell you it is more powerful this way. Certainly it is, you can do whatever you want with your Qt widgets, like drawing a line across a button or making your own freaky QScrollBar clone that has extra gadgets on it or whatever. Because the Qt style system breaks down high level widgets into separate parts for rendering, you could actually invent a widget that does not exist on Mac, but still has an Aqua look and feel. However, in many cases, this is overkill. Yes, custom widgets are handy, but do we often need custom widgets that look derived from native widgets? I say no. Consider the ‘BusyWidget’ from Psi, which just shows the logo as a bunch of spinning panels. This is a custom widget. It does not exist on any platform, it is something I wrote. However, it is not something impossible to write without Qt. I could easily make it with Objective-C using native Cocoa and it would look exactly the same. We do not want to rule out custom widgets. What we do want to rule out are customized variations of native widgets. We can trade this flexibility for true native look and feel, which is essentially what the wxWindows toolkit does. So why not just use wxWindows? Well, having native widget wrapping is only part of this battle. Qt is a more ideal framework in my opinion. It is almost like a platform of its own, and even defines extensions to the C++ language like signals, slots, objects, rtti, etc. It turns C++ into a wannabe Objective-C / Java. Whether this is a good idea or not is left up to the reader, but I consider it very powerful to be able to do these extras while retaining the use of C++.
Now, at some point, someone is bound to bring up the issue of the Lowest Common Denominator. This is generally what crossplatform applications have problems with, the rule being that if you can’t do something everywhere, then you just don’t do it. Personally I think this is a stupid limitation. Crossplatform applications should take advantage of platform-specific functionality whenever possible, otherwise the value of the underlying platform is lost. With Qt, we are not stuck in a box. We are natively compiled C++, not confined in a VM like Java, which means we can do whatever we please on each platform. The goal with crossplatform code then, in my opinion, is to have highly portable code, but not necessarily “write once run anywhere” code. We want to share as much code as we can to avoid redundancy, but beyond that we want to write code that is specific to each platform in question.
Consider the text-entry widget on Mac. It has built in on-the-fly spell checking. This is a great move by Apple, and it reminds me of KDE. Instead of simple widgets like on Windows where apps have to supply most of the functionality, apps under Mac can use highly advanced widgets that have a lot of functionality on their own already, and can be upgraded through the OS easily. This allows for more power, consistency, and rapid development of apps. How can we use this widget with Qt? Well, we can’t. This is because QTextEdit, like all Qt widgets, is not a native widget. Could Qt be altered to use the native Cocoa widget? Presumably. At what loss? The ability to do things we would never want, like drawing all over it with QPainter. I think it is clear that in the majority of cases, we would rather use the native widget. Then, of course, we also need a way to access the properties of the native widget so that we can do further platform-specific functions to it. On the Mac, we would like to enable the spell check mode (if we have to do that), but this function would not exist on other platforms. How do you access Cocoa widgets from C++ ? Good question, perhaps it is not easily possible. wxWindows probably does not support this. Qt does not support native widgets, so this is not possible either. So what is the answer, write the whole GUI in Cocoa?
No! Well, sort of. In the end it will all be Cocoa, whether we are creating widgets via Objective-C widgets or using them through a wrapper. However, it is simply not efficient to have to recode everything in Objective-C. Just because you want to port to another platform should not mean rewriting all of your code. The point I made earlier was that emphasis should be placed on code reusability. Some may say that abstracting the GUI completely out of an app is the logical thing to do. In some cases, I agree. If you are porting to a PDA, sure. If you are porting to Mac, you’d probably want to do this to some extent. Licq, the unix ICQ client, has a completely abstracted GUI system, and there are both Qt and Gtk versions. However, there is a ton of redundancy here.
I can appreciate the need to rework a GUI for a particular platform. When you consider the Mac, the GUI design guidelines are drastically different from Windows. Configuration dialogs generally take effect on-the-fly. There are no “OK” or “Apply” buttons. Apps use a global menubar at the top of the screen. Some apps use that weird “brushed metal” look. It is sensible then, that one might want to recreate their GUI completely for Mac. However, consider all of the various dialogs that we have. Do they all need to be recreated on Mac? And if so, do the entire dialogs need adjustment or only partly? For instance, some dialogs might be perfectly fine in their current state, aside from the ordering and alignment of the buttons at the base of the dialog. It would make more sense to reuse the code for this dialog, but to change the button positioning to match the Mac. Why code the use of a scrollbar in Objective-C when you’ve already done it in C++? It’s not like a scrollbar is only available on certain platforms. The solution, then, is to have generic bindings to all native widgets, and also platform-specific bindings so that extra functionality can be used if necessary. Qt is in a great position to do this, and even have fallback to emulated widgets if we really really wanted to make a frankenstein widget with native look. However, for some dialogs it might be more optimal to recode them completely, and this could be done with some Objective-C code wrapped in a C/C++ library. Psi has a ton of backend code, from the XMPP library to all of the internal contact management and accounting. It makes sense to be able to reuse as much as possible when making a MacOS X Jabber client, instead of starting from scratch. However, it should be used in such a way that it is not at the loss of native look, behavior, and functionality.
This, I think, sums up all of my thoughts about cross-platform programming. Rule of the day: code should be as portable and reusable as possible.