fognl

Get off my lawn.

Friday, August 08, 2008

SDDM: Qt event handling with Signals and Slots

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.

Events
In 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 to
struct someClass {
int f(int a, int b) { return a+b; }
};

someClass inst;

// take the address
int (someClass::*func)(int,int) = &someClass::f;
// call the function through the pointer
int 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 Slots
The 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_OBJECT

public:
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_OBJECT
public:
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.

6 Comments:

  • At 2:21 AM , Blogger Ellen said...

    Dear Webmaster,

    Hi, my name is Jon and I work for an online promotion company. I am interested in buying text ad space on your site (http://fognl.blogspot.com/) for one of my clients - a well established, online gaming company.

    Please let me know if you are interested, and I will send you more details.


    I look forward to hearing from you.

    Regards,
    Jon Peter

     
  • At 1:02 AM , Blogger Erik I said...

    Nice explanation! Thanks Kelly!

     
  • At 7:02 PM , Blogger Dean said...

    Hi Kelly, I'm really keen to make use of sddm and possibly contribute to the project. I've been over to the google code page, but there does not seem to be much activity there so I'm leaving a comment here in hopes of a response. What is the best way to contact you.

    There does not seem to be a binary available and I'm having some difficulties compiling it.

     
  • At 10:08 AM , Blogger Kelly said...

    Hi Dean,

    I'm actually at a pretty good point where I can iron out some of the build-related issues, at least on Ubuntu. I have a machine that I haven't set up the SDDM dev environment on yet, and I'll see about using that time to get the build together.

    You're right about the recent lack of activity. I've been using SDDM occasionally, but other things are getting in the way of my working on the code much these days. I'd be interested in any contributions you feel like making to it.

    May I ask, are you using a specific set of commercial samples with SDDM, or are you using your own? I just bought a big Mapex drumset and one of my many todo-list items is to sample it for use in SDDM.

    Thanks,
    Kelly

     
  • At 5:49 PM , Blogger Dean said...

    Hi Kelly good to hear from you.

    I was actually trying to compile it on Xubuntu and have not had a go on Ubuntu at this point which I will try when I get some more time. For Xubuntu I was missing a lot of stuff (source code for snd, jackd, gt3, g++ compiler) but came to some type error, cant' remember exactly. I will find and send you the output.

    Do you have a package or the binaries that I could test it out with in the meantime?

    I'll be using it together with a megadrum drum trigger (http://www.megadrum.info/). At the moment I'm using hydrogen as the sample player but it's really not very good at managing the midi note numbers to instrument mapping, and is limited to 16 velocity layers. I read that their latest dev version is better in these areas, but I have not found the time to mess around compiling that either.

    Sddm seems more focused on the task I'm interested in. So in terms of contributions, If sddm works out for me I was initially thinking to make a script to convert hydrogens h2drumkit files for use with sddm. Perhaps contribute to documentation. Also not sure if you support midi CC messages? The megadrum uses these to communicate intermediate hi hat positions. That would be something I would be interested to add also.

    I've mostly been using the GSCW kit 2 (http://www.autodafe.net/samples/) and had a bit of a play with the NDK Free (ns_kit7free).

    If you ever got around to sampling the Mapex would you open source it? I'm sure it would be hugely popular

     
  • At 11:22 PM , Blogger Kelly said...

    Dean,

    Thanks, lots of good info in your comment.

    SDDM doesn't currently support CC messages, but that was something I thought about putting into it. I liked the idea of controlling the hi-hat openness via CC instead of a discrete note. Also I thought it would be cool to maybe do something with stick position via CC, playing samples recorded nearer the edge/center of a drum (for example) controlled by CC. I bought the NS7 "full" kit a couple of years ago, and it contains 5 DVDs worth of hits, with variations like that.

    I don't have a package put together for SDDM, I have never figured out how to make a .deb package unfortunately. Not enough hours in the day. :-) I did, however, put up the compiled SDDM binary on the Google Code site if you're interested in taking a look at it. I don't know for sure that it will run on your system, but I'd think it would.

    I really like the idea of a script to convert Hydrogen kits into SDDM kits. I was thinking of adding to SDDM the ability to just open Hydrogen kit files, but ended up thinking a script might be a better idea. There are some good kits for Hydrogen, and it would be nice to be able to leverage some of the work that's been done there.

    As for releasing the Mapex kit samples, you bet I'd be interested in open-sourcing them. I think it would be cool! I'll eventually get around to sampling it, and I'll most likely just zip up all the samples and make them available for download from the SDDM site.

    Thanks!
    Kelly

     

Post a Comment

Subscribe to Post Comments [Atom]

<< Home