This is the second installment in a series of posts on my pet project,
SDDM, coming soon to a Linux distro near you.
I chose
Qt as the UI library I would use for SDDM, after reading about its features.
Qt is kind of unique in that it's a pretty good C++ UI class library, but also features a "meta-object compiler" to allow things like runtime reflection, events, properties, and other features not supported natively by C++. It also includes a lot of non-UI things like XML parsing, URLs, filesystem access, container classes, in addition to a large set of UI elements. Custom controls are pretty easy to create as well.
EventsIn any desktop UI library, you need the notion of "events" so that UI elements on a window can communicate their status to the parent window, where the application-level logic is written. For example, take a window with a button on it. What you need is some mechanism for a button (or any other control) to tell its parent window (in effect): "Hey, this thing just happened, take whatever action you need to." This is basic "observer pattern" stuff, and there's a variety of approaches used by the various UI environments.
The "Java approach", used in AWT, Swing, and SWT, is to define interfaces for "listeners", which respond to events posted by controls. In effect, what you do is create a class that implements a specific "listener" interface, with the implementation providing the application-level event handling, for each event you need to intercept. As you might imagine, this adds up to a lot of classes, since a separate class is needed in most cases to handle every different event. However, Java provides a way to define anonymous inline classes. This makes it unnecessary to actually create a new class, decide on a name for it, and so on.Typically, the code you write to listen to an event from a control is pretty concise, looking something like this (fictional) example:
myButton.addListener(new ButtonListener() { public void clicked(Event evt) { // process the click. }});This is still kind of messy, but much cleaner than having to write a discrete class for each case where you need to handle an event from a control.
Languages like VB and Delphi are tailor-made for desktop UI programming, and incorporate the notion of "events" right into the language. Delphi's approach involves assigning function addresses to event pointers on controls, so the controls can call the functions directly to communicate their status. This is probably the most efficient approach of all, since there's no intermediate layer between the control and the function it's calling.
VB's approach, I don't really care about. Last time I used VB, it achieved events through some "mystery meat" process involving COM and OLE, which probably resembles the manufacture of sausage. It wasn't nearly as flexible as Delphi's approach. Delphi allows one event-handling function to be assigned to potentially many event-handler pointers in one or more controls, whereas VB didn't. In my experience, it (like VB itself) was minimally useful at best.
In the case of C++, none of these approaches is practical. C++ doesn't allow the inline definition of an anonymous class like Java does, so if you take the Java approach to events with listeners and the like, you end up with lots of little single-purpose classes lying around. You can adopt the function-pointer approach that Delphi uses, but it gets messy pretty quickly too. One problem is the fact that C++ member functions don't actually have an address that you can assign to a pointer. Actually, they do, but in order to take a member function's address, you have to include the name of the class in the address, using this syntax:
// simple class with a function to point tostruct someClass { int f(int a, int b) { return a+b; }};someClass inst;// take the addressint (someClass::*func)(int,int) = &someClass::f;// call the function through the pointerint result = (inst.*func(3,4));Straightforward enough I guess, but notice how the name of the class has to be included in the pointer declaration. The pointer named "func" isn't just a pointer to a function, it's a
pointer to a function in someClass. An important distinction. In an application with a UI, it's a pointer to a function in your application's parent (window/dialog/widget) class. Which means a reference to the parent widget class has to appear in the class of the control you're listening for events on. This is something you definitely don't want in a reusable component.
There might be some kind of macro-based voodoo you could perform to get around this limitation. It would work fine I'm sure, except for the "macro-based voodoo" part. I can't think of how it would work, in any case.
That leaves templates. Essentially, you define the control as a template class, with the type parameter referring to your application-level class the control will call through function pointers.
This definitely works, but also adds a lot of bulk to an application. Consider a button class like this:
template MyButton: public BaseWidget { ... // function pointer void (T::*clickFunction)(const MyButton*);};Consider a nominal-sized application with, say, 30 classes representing various kinds of parent widgets in it, doing this sort of thing:
class MyWindow: public Window { ... MyButton okButton; MyWindow(const char *caption): Window(caption) { // Assign the "click" event to our handler okButton.clickFunction = &MyWindow::okButtonClicked; } void okButtonClicked(const MyButton *button) { // handle button click }};The compiler generates a separate class for each different window class which uses the MyButton template class. In the example above, you get a "MyButton_MyWindow" class (or a class with some similarly-mangled name), and 29 or so others, one for each class that makes use of the button. Follow that example for every other kind of control you can have, and you get the idea.
In case you somehow didn't get the idea, it's this: You end up with Mt. Everest-sized executables. See
Visual Be++ for an example of this approach. In that GUI designer's host environment, you can create an application the "native" way with a couple of buttons and some edit controls, and the finished executable will be around 20kB in size. Use Visual Be++'s templated-based UI library (which adds events, properties, and some other Delphi-isms) for the same application, and you end up with an executable about 240kB in size.
Put simply, C++ doesn't support a convenient way for controls on windows to notify their parent windows of something. To do this sort of thing without using an old-skool Windows-style window procedure (driving yourself nuts in the process) or any of the approaches noted above, you almost have to resort to some sort of language augmentation and specialized tool set.
That is exactly what Qt does. Qt's approach to the problem falls deeply into the "mystery meat" zone. Trust me, there is a
lot of sausage manufactured here. It involves a combination of specialized tools (the aforementioned "meta-object compiler" and
qmake), and some Qt-centric quasi-keywords.
I'm not generally a fan of language augmentation. It ties you to a specific tool set, I'm also not crazy about specialized tools being required for a build, since it limits support for a certain type of program by available build tools.
But, hey, so what? If I stopped with that, this would be a short and pointless blog entry where I did nothing but gripe about the suckiness of C++'s support for UI events. Qt has a lot of features I want to use. The tool set, though specialized, is easy to use. The benefit I get from Qt far outweighs the annoyance of having to stick with a specific tool chain. The "sausage" in this case is tasty enough that I'm willing to overlook the manufacturing process and keep my mind off of what (or who) might have fallen into the sausage press.
Signals and SlotsThe Qt Meta layer adds the notion of "signals" and "slots". A "signal" is something emitted by an object to whoever might be listening. A "slot" is something employed to listen for signals. So, for example, a button emits a "clicked" signal, and a dialog box with a button on it can define a slot to capture that signal.
Here is a simple Qt control class declaration, showing the additional Qt keywords and macros in use:
class SomeWidget: public QWidget{ Q_OBJECTpublic: SomeWidget(QWidget * parent = 0, const char *name = 0); virtual ~SomeWidget(); // Respond to a Qt mouse event. virtual void mouseEvent(QMouseEvent *evt);signals: void clicked(int);};In the implementation, you "emit" an event signal when something of interest takes place:
void SomeWidget::mouseEvent(QMouseEvent *evt){ // blah blah, decide whether to emit a "clicked" event, because the user clicked in this control's client area.
// This is basically it. The keyword "emit", followed by a function call. emit clicked();}The
Q_OBJECT macro marks the widget as something the MOC (meta-object compiler) should generate extra MOC code for. You'll find the extra MOC code in your project directory as .cpp files with names beginning with "moc_". One of these files is generated for each C++ translation unit where the Q_OBJECT macro is used. (Take a look in there. There's a sausage press, a large cage full of frightened-looking cats, and a crew of sweaty men in stovepipe hats, furiously smelting.)
A parent widget with the SomeWidget control on it would be defined something like this:
class ParentWidget: public QWidget { Q_OBJECTpublic: ParentWidget(QWidget * parent); ...private: SomeWidget *someWidget;private slots: void someWidgetClicked();};The
slots Qt-keyword indicates that the methods below are intended to be called by controls emitting events with the
emit Qt-keyword.
In the constructor, you another quasi-keyword,
connect, to join the signals emitted by various controls to handlers in the parent widget.
ParentWidget::ParentWidget(QWidget *parent): QWidget(parent){ // initialize someWidget this->someWidget = new SomeWidget(this); // connect someWidget's clicked signal to our slot connect(someWidget, SIGNAL(clicked()), this, SLOT(someWidgetClicked()));}// This gets called whenever someWidget emits a clicked() event.void ParentWidget::someWidgetClicked(){ // someWidget was clicked
}And that's basically it. It's pretty clean, and probably more convenient than the approach used by any UI library I've used to date.
Next up: Custom controls in Qt.