Lesson 3: Adding features to our program


Contents

Lesson 2

Lesson 4

Code Reference Sidebars
mandelglow.cpp
mandeldata.h
mandeldata.cpp
mandelwind.h
mandelwind.cpp
Makefile
class Glow
class GlowIdleReceiver
class GlowMenu
class GlowMenuMessage
class GlowMenuReceiver
class GlowWindow
class TReceiver
class TSender
Senders and Receivers

Introduction

This lesson describes some additional basic user interface features. We will extend the user interface by adding contextual menus, and implement background processing so that we can interact with the window while it is still calculating. In the process, we'll see how to use GLOW menus and idle events, and look at how to implement a Receiver, which is something that we'll do a lot of when we start using widgets. At the end of this lesson, you should have a grasp of how to manage menus, and an understanding of GLOW's generalized event reporting mechanism.

Building a Menu

GLUT lets you create simple menus and attach them to mouse clicks in a window. GLOW provides an elegant object-oriented API for handling menus, and includes several features that were left out of GLUT. We'll create a simple set of menus for controlling the Mandelbrot Set viewer. The menus will let us reset the magnification to its initial state, change the color scheme, or quit the program. We'll have the menu pop up when the user clicks the left mouse button in the view window.

Creating a simple menu

To see how menus are implemented, first look at mandelwind.cpp. The constructor for the MandelWind class has been expanded to include some menu code. First, a menu is created by constructing a GlowMenu object.

GlowMenu* menu = new GlowMenu;

Next, two menu entries are added. The first parameter to the AddEntry() method is the text for the menu item, and the second is an identifying integer constant. When GLOW reports that a menu item has been chosen, it identifies the item using this constant.

menu->AddEntry("Reset zoom", RESETZOOM_ITEM);
menu->AddEntry("Quit", QUIT_ITEM);

Creating a submenu

Next, we create a second menu, called colorMenu, and add four items to this menu.

GlowMenu* colorMenu = new GlowMenu;
colorMenu->AddEntry("Red", RED_ITEM);
colorMenu->AddEntry("Green", GREEN_ITEM);
colorMenu->AddEntry("Blue", BLUE_ITEM);
colorMenu->AddEntry("Multi", MULTI_ITEM);

The color menu will be a submenu of the main menu. The next line calls AddSubMenu, which adds a menu item to the main menu, and specifies that the item should pop up the color submenu.

menu->AddSubmenu("Color", colorMenu);

The next line marks the first item of the color submenu.

colorMenu->SetItemMark(0, "=> ");

A mark is a short string that is prepended before the menu item text. It isn't part of the item name (and won't show up if you query the item for its name), but is a separate attribute of the menu item. You can mark and unmark menu items, and you can also query items to determine whether they are marked. For our purposes, we will mark the menu item corresponding to the current color setting. To start off, that will be red. Notice that menu items are numbered starting from 0.

Linking menus to mouse clicks

The next line binds the main menu we created to clicks of the right mouse button in our window.

SetMenu(Glow::rightButton, menu);

This has the effect of intercepting mouse events. Instead of reporting the events in OnMouseUp and OnMouseDown, GLOW will pop up the menu. You may bind a menu to any number of mouse buttons in any number of windows. If you call SetMenu() and give it the null pointer for the menu parameter, it will unbind the menu from that mouse button, causing that mouse button to report events normally again.

Menu hits are reported as events, so we need to update the event mask to receive them. Note that the constant Glow::menuEvents has been added to the event mask.

MandelWind::MandelWind(MandelData* data) :
GlowWindow("Mandelglow", GlowWindow::autoPosition, GlowWindow::autoPosition,
    data->Width(), data->Height(), Glow::rgbBuffer | Glow::doubleBuffer,
    Glow::mouseEvents | Glow::dragEvents | Glow::menuEvents)
{ ...

The last two lines involve setting up notification for when a menu item is selected. We'll look at this process in the next section.

Link toSource: mandelwind.cpp

Link toReference: class GlowMenu
Link toReference: class GlowWindow

Menu Events

Now that we've built a menu, we need a way to be notified when a menu item is hit. There are several ways we can do this, but the best and most flexible is to create a menu receiver. A menu receiver is an object that inherits from the abstract class GlowMenuReceiver, and implements its OnMessage() method. This method is called when a menu item is chosen.

Configuring a class to receive menu events

Take a quick look at mandelwind.h. Notice that the MandelWind class, in addition to inheriting from GlowWindow, now also inherits from GlowMenuReceiver. Furthermore, it implements the method OnMessage(const GlowMenuMessage&). (The class also inherits GlowIdleReceiver and implements another OnMessage() method-- we'll look at those later.)

class MandelWind :
public GlowWindow, public GlowMenuReceiver, public GlowIdleReceiver
{ ...
    virtual void OnMessage(const GlowMenuMessage& message);
    virtual void OnMessage(const GlowIdleMessage& message);
  ...

Now let's go back to mandelwind.cpp and look at those last two lines of the MandelWind constructor.

menu->Notifier().Bind(this);
colorMenu->Notifier().Bind(this);

The GlowMenu::Notifier() method retuns a reference to a sender object associated with the menu. A sender is a descendent of TSender, and its role is to send messages to receivers like the GlowMenuReceiver we created. Whenever a menu item is selected, the menu's sender will broadcast a message to any receivers listening to the sender. We call the Bind() method of the sender associated with each menu to bind it to the receiver, which is this, the window object. Now our window object will receive notifications of menu hits.

Writing the receiver method

To actually receive the message, we implement the OnMessage() method.

void MandelWind::OnMessage(const GlowMenuMessage& message) {
    switch (message.code) {
        case RESETZOOM_ITEM:
            ResetZoom();
            break;
        case QUIT_ITEM:
            exit(0);
            break;
        case RED_ITEM:
            SetColor(255, 0, 0);
            message.menu->UnmarkAllItems();
            message.menu->SetItemMark(0, "=> ");
            break;
  ...

Notice that it takes a parameter of type const GlowMenuMessage&, a reference to a structure that contains information about the menu item hit. The code field contains the identifier for the menu item. We switch on it to handle each item. Note that we call std::exit() in response to the "quit" menu item. This is a reasonable way to exit a GLOW program. Note also that selecting an item of the color submenu causes that item to be marked.

The message structure we receive gives several additional pieces of information about the menu hit, including a pointer to the menu object hit, a pointer to the window object that spawned the menu, and the x and y window coordinates of the mouse click that popped up the menu.

So what are these senders and receivers?

Senders and receivers are objects that are descendents of the templates TSender and TReceiver. The sidebar "Senders and Receivers" gives more detailed information about how these classes work. Basically, they implement a typechecked broadcaster-listener system. You can set up communication lines between senders and receivers using the Bind() method of the sender. Then, you can send a message, which is a C++ object, using the sender, and the sender will invoke the OnMessage() method of each receiver it is bound to. Senders and receivers are typechecked, so you can bind a sender only to receivers that are capable of receiving the message type sent by that sender.

Every GlowMenu object has associated with it a sender. You can get a reference to it using the Notifier() method. This sender is capable of sending messages of type const GlowMenuMessage&; thus, you can bind it to receivers of that type of message. GLOW provides a typedef for these kinds of receivers: GlowMenuReceiver. When GLOW detects a menu selection, it gathers information about the event, and then causes the menu's sender to broadcast a message to all receivers bound to it. In our case, there is one receiver bound to it: the window object.

You said there were other ways to receive menu notifications...

I guess I did. There are two other ways. First, you can have the menu call a special virtual method on the GlowWindow object that spawned the menu, OnDirectMenuHit().

virtual void OnDirectMenuHit(const GlowMenuMessage& message);

This method also reports the details of the menu item hit by passing a const GlowMenuMessage& parameter. To switch to this style of reporting, call SetBindState(GlowMenu::bindSubwindow) on the menu object. Finally, you can subclass GlowMenu and override the OnHit() virtual method.

virtual void OnHit(int code, GlowSubwindow* window, int x, int y);

Recall that GlowSubwindow is a superclass of GlowWindow. We could easily have used the bindSubwindow style of reporting, since we were using the window as our receiver anyway. However, I wanted to demonstrate the receiver method because it is used quite often elsewhere in GLOW, particularly for reporting widget events.

Link toSource: mandelwind.cpp
Link toSource: mandelwind.h

Link toReference: class GlowMenu
Link toReference: class GlowMenuMessage
Link toReference: class GlowMenuReceiver
Link toReference: class GlowWindow
Link toReference: class TSender
Link toReference: class TReceiver

Link toSidebar: Senders and Receivers

Background processing

Next, we'll add background processing to the program. The problem we're trying to address is that computing an entire image of the Mandelbrot Set, especially when you've zoomed in considerably, may take some time. Instead of "hanging" while that computation is going on, it would be better to show the progress of the computation and allow the user to continue to interact with the program during that time.

Idle event receivers

To accomplish this, we'll cause our window object to receive idle events. Idle events are raised continually when the application is idle (i.e. no other events are pending). What we'll do is respond to an idle event by performing a "little bit" of the computation, and then updating the image to show the progress. We'll then give control back to GLOW so it can look for other events such as mouse clicks. If the program is still idle, another idle event will be raised, and we can do a little bit more of the computation, until it is finished.

First, let's go back to mandelwind.h. The MandelWind class, in addition to GlowWindow and GlowMenuReceiver, also inherits from GlowIdleReceiver. An idle receiver must implement the method OnMessage(const GlowIdleMessage&). This method is called whenever the receiver receives an idle event.

class MandelWind :
public GlowWindow, public GlowMenuReceiver, public GlowIdleReceiver
{ ...
    virtual void OnMessage(const GlowIdleMessage& message);
  ...

Raising and handling idle events

Go back to mandelwind.cpp. Before we look at the new OnMessage() method, let's take a brief look at OnEndPaint(). Note that we've replaced the MandelData::Recalc() call to a small number of calls to the method RecalcOneLine(). This method recalculates a single line of the Mandelbrot image. So now, instead of recomputing the entire image before redrawing, the program computes only a small fraction of the image.

However, note the new lines added just below, the calls to Glow::RegisterIdle() and Glow::UnregisterIdle().

if (!_data->IsDataValid()) {
    // Recompute a small number of lines
    for (int i=0; i<10; ++i) _data->RecalcOneLine();
    // Rerender image here
      ...
    // Make sure idle receiver is registered
    Glow::RegisterIdle(this);
} else {
    // Done computing, make sure idle receiver is unregistered
    Glow::UnregisterIdle(this);
}

RegisterIdle() makes the given idle receiver eligible to receive idle events, and UnregisterIdle() makes it ineligible. So after computing a small fraction of the image, we make sure the window is receiving idle events, and if the image has finished being computed, we turn off idle events. It doesn't hurt to register an idle receiver that is already registered or unregister a receiver that is not registered.

Now, take a look at the idle event receiving method, OnMessage(const GlowIdleMessage&).

void MandelWind::OnMessage(const GlowIdleMessage& message)
{
    Refresh();
}

All it does is call Refresh(). This, in turn, causes the window to be redrawn, which, in turn, will cause the next small fraction of the image to be computed.

When are idle events raised?

Idle events are raised continuously when no other events are pending. This means, when the user isn't doing anything to cause user interface events to be raised, and when no timer events or other periodic events are being raised. So when the system is idle, you'll get a continuous stream of idle events. But while, say, the user is dragging the mouse in a window that receives drag events, idle events may not be raised at all. Make sure you keep this in mind when using idle events.

What about threading?

Well, that's a good question. Unfortunately, the current version of GLOW isn't thread-safe. Although that can be fixed, a bigger problem is that, as far as I can tell, thread-safety isn't in the GLUT specification either, so the underlying GLUT implementation may not be re-entrant. This doesn't necessarily mean you can't write a multithreaded application based on GLOW, but only one thread can be interacting with GLOW at a time. This means, in order for a thread to communicate with the thread that is running the GLOW main loop, you'll need to implement idle events anyway, and handle the synchronization and communication during idle time. I've done this once before. (It seems like it's the only way to write a program that enters a GLUT loop but is still able to continuously read from standard in.) But it's not the prettiest solution.

Link toSource: mandelwind.h
Link toSource: mandelwind.cpp

Link toReference: class Glow
Link toReference: class GlowIdleReceiver

Putting it together

Once again, we don't need to modify our main function in mandelglow.cpp. All the modifications were done to the window class.

Compile and run the program. Notice now that the window recalculates and updates in stages. The program doesn't "hang" while recalculating, so you can continue to zoom in or out without waiting for each image to be fully recomputed. You now also have the capability for further controlling the program through menus, and you can quit the program gracefully using the quit item.

Link toSource: mandelglow.cpp

Where to go from here

Now that you can create menus, you can experiment with adding additional commands to the program by adding more menu items. Try adding an item that saves the current image to a file on the disk.

Here's a harder exercise. You should now have enough knowledge about GLOW to implement a "progress bar" window for recomputation. Create a second window class that displays a progress bar. Have the window appear when a recomputation begins, update while the computation is taking place, and then disappear when the recomputation is complete. You can determine the status of the computation by querying the Height() and LinesRemaining() methods of MandelData.

GLOW also provides a timer event mechanism that allows you to schedule a piece of code for execution sometime in the future. You schedule a timer by implementing a GlowTimerReceiver and registering it with Glow::RegisterTimer(). Try experimenting with timers.

Link toSource: mandeldata.h

Link toReference: class Glow
Link toReference: class GlowTimerMessage
Link toReference: class GlowTimerReceiver



Contents

Lesson 2

Lesson 4


The GLOW Toolkit