Friday, January 26, 2007

In Praise of Connect()

In this post I'd like to address a common misconception about wxWidgets which is that it is somehow dirty and beneath consideration of "real" C++ programmers because it relies too much on preprocessor macros. While it is true that the traditional way to handle events in wxWidgets is using the macro-based event tables, there is nothing really dirty nor evil about these macros: they are there simply to save typing. In particular, they don't suffer from the usual macro pitfalls as they don't have any side effects and are type-safe (static_cast is used internally to ensure this).

However the main point is that the event tables macros are just one of two ways of connecting the event handlers to events in wxWidgets and it's perfectly possible to not use them at all. Moreover, there are good reasons to use the alternative way beyond the subjective dislike of macros. This other way is the Connect() method which, unsurprisingly, allows to connect a method of a class derived from wxEvtHandler (which includes, but is not limited to, any wxWindow-derived class) to an event.

The syntax is slightly more verbose which is, of course, one of the reasons for using the event table macros in the first place, they simply save some typing. So instead of

BEGIN_EVENT_TABLE(MyFrame, wxFrame)
  EVT_MENU(wxID_EXIT, MyFrame::OnQuit)
END_EVENT_TABLE()

you need to write (in the body of some method of MyFrame and not at global scope as with the event tables)

MyFrame::MyFrame(...)
{
  Connect(wxID_EXIT, wxEVT_COMMAND_MENU_SELECTED,
  wxCommandEventHandler(MyFrame::OnQuit));
}

Notice the use of wxCommandEventHandler which ensures that the method is of correct type by using static_cast in the same way as event table macros do it.

So we see that all uses of event table macros can be trivially replaced with Connect() calls. However Connect() is more powerful than macros and can do a few things that macros are incapable of:

  1. Event handlers can be connected at any moment, e.g. it's possible to do some initialization first and only connect the handlers if and when it succeeds. This can avoid the need to test that the object was properly initialized in the event handlers themselves: with Connect() they simply won't be called at all if it wasn't.

  2. As a slight extension of the above, the handlers can also be
    Disconnect()ed at any time. And maybe later reconnected again. Of course, it's also possible to emulate this behaviour with the classic static (i.e. connected via event tables) handlers by using an internal flag indicating whether the handler is currently enabled and returning from it if it isn't, but using dynamically connected handlers requires less code and is also usually more clear.

  3. Also notice that you must derive a class inherited from, say, wxTextCtrl even if you don't want to modify the control behaviour at all but just want to handle some of its events. This is especially inconvenient when the control is loaded from the XRC. Connecting the event handler dynamically bypasses the need for this unwanted subclassing.

  4. Last but very, very far from least is the possibility to connect an event of some object to a method of another object. This is impossible to do with event tables because there is no possibility to specify the object to dispatch the event to so it necessarily needs to be sent to the same object which generated the event. Not so with Connect() which has an optional eventSink parameter which can be used to specify the object which will handle the event. Of course, in this case the method being connected must belong to the class which is the type of the eventSink object! To give a quick example, people often want to catch mouse movement events happening when the mouse is in one of the frame children in the frame itself. Doing it in a naive way doesn't work:

    • A EVT_LEAVE_WINDOW(MyFrame::OnMouseLeave) line in the frame event table has no effect as mouse move (including entering and leaving) events are not propagated upwards to the parent window (by default, anyhow).
    • Putting the same line in a child event table will crash during run-time because the MyFrame method will be called on a wrong object.

    However writing

    MyFrame::MyFrame(...)
    {
      m_child->Connect(wxID_ANY,
        wxEVT_LEAVE_WINDOW,
        wxMouseEventHandler(MyFrame::OnMouseLeave),
        NULL, this);
    }

    will work exactly as expected. Note that you can get the object which generated the event -- and which is not the same as the frame -- via event.GetEventObject().


So the morale of the story is: don't hesitate to use Connect(), it's much more powerful than static event tables which should be reserved just for the most simple situations when you don't need any additional flexibility and wish to save some extra typing, or maybe not even then for consistency sake.

13 comments:

Anonymous said...

You should put this information in the API documentation. Right now it just tells you that using Connect() is possible, and refers to the event example. But then the reader will have to download the source code to get the example. You'll make it much easier for the reader if you include this information right into the API docs, with inline example code.

Anonymous said...

Totally agree that it should be in the docs - now the docs sound like Connect() is hard and not the right way to do it, so i had never tried.

Anonymous said...

I too vote for incluion in API docs. And more samples. While macros are explained in detail, judging from my experience and knowledge while reading docs, Connect() seemed more like experimental feature that nobody uses for real life problems.

VZ said...

The docs should probably indeed be updated, even though I don't really understand why does Connect() sound hard now. I'll see what can I do about it, especially in the event handling overview.

Anonymous said...

now that I read the post, they don't sound hard at all. It's just that tons of code are written with macros, and one can find a lot more examples for macro usage. But now, lots of complications can be avoided with connect. BTW, excellent post

Anonymous said...

Thank you very much ! It was exactly what I was looking for desperately in the official documentation. Especially to capture mouse events without deriving a new class :-)

Unknown said...

Nice explanation, I'm currently reading the docs as I'm (relatively) new to GUI programming. Overall I think the docs are a bit sparse on the event handling topic: particularly creating your own event classes. Some functions/macros are used in the example but not explained in the text. Also what is derived from what (what is global / local / evthandler class / event class / window class / etc.. is unclear, it would be nice to have a section explaining the different classes and their use...)

Anonymous said...

I wrote about just this thing a couple years back on the wxPyWiki. Have to say, Connect > EVT_MENU

R. Douglas Barbieri said...

It reminds me a little bit of how gtkmm handles event connections. They use a library called libsigc++ (http://libsigc.sourceforge.net/). It's a nifty templatized way of handing events.

Of course the Connect() way looks even simpler to implement. Nice touch! And definitely stick this in the docs!

Anonymous said...

I have to say that I love the handling through macros. It makes the code easy to read. However, yes there are cases when you get crazy trying to handle some events. The Connect() thing is really nice - now that we know how to use it! :D

Unknown said...

It should be noted that in the last "connect an event of some object to a method of another object" example, you should always remember to disconnect the event handler in the event handler object's destructor, otherwise your application may crash if/when the handler is called after your event handler object is destroyed.

Tinman said...

This blog post has been one of the MOST helpful explanations of wxEvents and wx event handling that I have encountered.

I agree with the other commenters, this post needs to be in the wxWiki.

Anonymous said...

Thank you. I am so adding a link to this blog on my blog. It took me two days to figure this out. Had I only found your blog post sooner.