C/C++ Users Journal, January 2006

Hotter Events

Define and send your own custom events

By John Torjo

John Torjo is a freelancer and consultant who specializes in C++, generic programming, and streams. He can be reached at john@torjo.com.

Handling events is pretty easy—if you know what you want to handle (Listing 1). However, creating your own events and filtering events is still quite hard, and error prone. In this article, I'll show you new ways to create/filter events that are easy to use and as error-free as possible.

Dynamic Events

This is something many of you have asked for, and it's finally here: The ability to create your own events in an easy and error-free manner. Until now, Listing 2 would have been the way you created your own events. This required you to know the exact match between the arguments you pass to the event (WPARAM and LPARAM), and how the arguments are processed in the event-handler function. This is time consuming and prone to error. You could also get into trouble if some other event has the same ID as yours; thus, someone else could send that event expecting a different outcome.

Now, you can define a dynamic event quite easily:

struct tri_click : 
    dynamic_event<tri_click> {
  // ...
};

Every event needs to be uniquely identified. This way, when an event happens, the dispatcher knows which event happened, and can correctly dispatch it [1].

The same goes for dynamic events. Therefore, you'll need to define the following:

static event_id_type event_id();

This needs to return the ID of the dynamic event, and it must return the same ID every time the function is called (you are not allowed to return different IDs at different times).

To ease the above, I've provided two helpers: unique_event and registered_msg (for the Win32 platform).

Usually, you'll prefer the unique_event class. Its usage is simple and straightforward:

struct tri_click : 
    unique_event<tri_click> {
  ...
};

That's it! The event_id is automatically computed for you. However, its biggest advantage could be its biggest disadvantage as well. You cannot control the unique ID that gets assigned to it. This is okay most of the time, but it could be a problem when you want to send/receive events to/from other applications. If so, use the registered_msg class.

The registered_msg uses a string to generate a unique ID for this event. The same string will always return the same ID (even across different applications); different strings will always generate different IDs. In particular, internally, it uses the ::RegisterWindowMessage Win32 function.

Use registered_msg like this:

struct tri_click : 
    registered_msg<tri_click> {
  ...
};

You specify the string to generate the unique ID, like this:

static std::string event_str() { ... }

For example:

static std::string event_str() 
{return "internal_command"; }

Use this to communicate with other applications and/or instances of your application. However, make sure the event doesn't have any arguments [2].

Sending Events

First of all, differentiate between internal and external events. In other words, an internal event is useful only for a specific event handler internally. More to the point, you'll usually use internal events to communicate between a window class and its event handler. Listing 3 shows a simple example of using an internal event—wm_set_bitmap is used to communicate from bitmap_button to its event handler (bitmap_button_handler).

On the other hand, if your window class has defined some external events, you'll be able to send an event to an instance of your window class, anywhere in your program. Same goes for listening to events (being notified when an event occurred).

Once you've defined an event, sending it to a window is easy:

wnd<> w = ...;
w->send_event( evt_class( [args]));
w->post_event( evt_class( [args]));

evt_class, in this case, is the name of your event class, and the optional args are the arguments you can pass to its constructor.

If you use send_event, the event will be sent to the window, it will be processed, and the eventual result will be returned synchronously (that is, after the call ends, the event has been processed).

If you use post_event, it will be pushed into the window's queue and processed asynchronously (that is, after the call ends, the event is not guaranteed to have been processed).

Listing 4 shows you some examples of using send_event and post_event.

When you define your event class, you can specify some constraints: If the event can be sent and/or posted (by default, it can be both sent and posted). Sometimes, it doesn't make sense for an event to be sent or posted—you can express that in code. For instance, you could have an event that returns the number of items in your window. If so, it's very likely you need this result now, so posting an event is completely useless (and semantically wrong). To indicate if your event can be sent and/or posted, you set:

For example:

struct tri_click : ... {
  enum {
    can_be_posted = false
  };
};

In this case, you've specified that the tri_click event can't be posted.

Event Arguments

When you create an event, you'll typically need some event arguments as well. When defining an event class, you can specify as many arguments as you wish, and hold them as data members—as you would for any class (Listing 5). The library will make sure that the arguments are marshaled correctly.

Listening to Events

Once you've defined an event class, you can easily listen to it. There are two types of events:

  1. Events, which are sent to the window itself.
  2. Notifications, which are sent to the window's parent.

Assuming tri_click is an event, Listing 6(a) shows you how you'll listen to it. In case it's a notification, check out Listing 6(b).

Take another look at Listing 6(a)—you'll notice the argument: wm::tri_click::arg. From a usability point of view, you can consider wm::tri_click::arg as a typedef for 'wm::tri_click *'. Say tri_click looks like this:

struct tri_click : ... {
  ...
  point pos;
  time_t at;
};

In your event handler function, you can say:

time_t t = a->at;
point p = a->pos;

And, you'll always know the window that generated this event:

// note: I'm using a dot!
wnd<> w = a.src_wnd();

You'll notice that I've preserved existing practice—the convention is the same as for the existing Win32 events/notifications.

Also, in case your event is a notification, you'll specify that in your class definition:

struct tri_click : ... {
  enum {
    is_notification = true
  };
};

In this case, tri_click is a notification.

Finally, note that usually, an event of the first type is an internal event (it doesn't need to be made available to clients of your class). You could consider making it an external event, if you plan to extend from your window class, and the derived class might want to send and/or listen to the event as well.

And now, for the hard part: Defining the external events, so that clients of your class can listen to them in a straightforward manner. To preserve existing practice (Listings 6(a) and 6(b)), you need to have some discipline. In your class's header, after you define your class, you'll define the events and/or notifications:

template<int ctrl_id> struct 
                 events_for_class<ctrl_id,your_class_name> : 
                 public ev::your_class_name<ctrl_id> {};

Listing 7 is an example to get you started.

Event Filters

There are times when you want to listen to multiple related events at once. You can always do it the old fashioned way, but it's error prone and not reusable; see Listing 8.

Therefore, I've improved this process—you can now define event filters. Here's how:

struct mouse_events : 
  event_filter<mouse_events> {
    ...
};

You then override this function:

bool operator()(const win32_enhanced_msg & msg) const;

If an event matches your filter, you should return true. Otherwise, false.

To simply check if one or more events match, use the in() function:

return msg.in(
  wm::mouse::left_down,
  wm::mouse::right_down,
  ...);

This will check if the event is either left_down or right_down, and so on. To allow for this syntax, every event/notification has overloaded the comma (,) operator.

You can test for notifications coming from your child controls:

return msg.in( 
  m_ok_::ev::clicked,
  m_cancel_::ev::clicked);

This will return true if the "OK" or "Cancel" buttons were clicked.

You can test for notifications coming from any child control of a given type:

return msg.in(
  ev::edit<any_>::kill_focus,
  ev::list_box<any_>::kill_focus,
  ev::combo_box<any_>::kill_focus,
  ...);

This will return true if there was a kill_focus notification coming from any child control.

Listing 9 (available online at http://www.cuj.com/code/) shows how to filter all mouse events, all keyboard events, and all kill_focus notifications (from existing win32gui controls).

Once you've created an event filter, you can listen to it, just like you would listen to a regular event. The beauty is that this is totally transparent to you, the programmer.

handle_event mouse() {
 ...
  return event_ex<any_mouse>()
    .HANDLED_BY(&me::mouse);
}

You'll find this useful on many occasions:

  1. When you want to respond in the same way to a group of events (all mouse events, all keyboard events, all painting events, and the like). For example, say you have a small application that usually runs in the background and is shown in the tray. If there is neither mouse movement nor keyboard movement for two minutes, you can decide to minimize.
  2. When you want to emulate an event that does not exist on a platform or for a given control. For instance, say that the "double click" event does not exist for your platform. You could implement it as shown in Listing 10 (available online). Other examples: "selection changed" for list controls, on_timer_X—which is to be called only for a specific timer ID (not for all wm::timer events).

Conclusion

I started win32gui thinking that I'd go for only one platform (Win32). But after a few months, I've realized that a lot of the concepts I was implementing are platform independent. So, I've changed my focus—and most of the features I've added could be easily ported to other platforms. Win32 is pretty mature now, and my focus now is to:

For that, I'll need all the volunteers I can get, so if you want to help, just drop me an e-mail and welcome aboard!

In the future, this column will change its focus: I will delve into other libraries that deal with GUIs and also analyze other platforms, such as Longhorn/Avalon.

References

  1. For the Win32 platform, this unique ID is an integer. However, for other platforms, it might not be so.
  2. Note that there's no marshaling of arguments from one application to the other. Thus, in case the event has some arguments but the receiver application can't access the sender application's memory, an Access Violation could occur. In case you have such an event, I highly recommend that it doesn't have any arguments.

CUJ