Programming: Principles and Practice Using C++ (2014)

Part II: Input and Output

16. Graphical User Interfaces

“Computing is not about
computers any more.
It is about living.”

—Nicholas Negroponte

A graphical user interface (GUI) allows a user to interact with a program by pressing buttons, selecting from menus, entering data in various ways, and displaying textual and graphical entities on a screen. That’s what we are used to when we interact with our computers and with websites. In this chapter, we show the basics of how code can be written to define and control a GUI application. In particular, we show how to write code that interacts with entities on the screen using callbacks. Our GUI facilities are built “on top of” system facilities. The low-level features and interfaces are presented in Appendix E, which uses features and techniques presented in Chapters 17 and 18. Here we focus on usage.


16.1 User interface alternatives

16.2 The “Next” button

16.3 A simple window

16.3.1 A callback function

16.3.2 A wait loop

16.3.3 A lambda expression as a callback

16.4 Button and other Widgets

16.4.1 Widgets

16.4.2 Buttons

16.4.3 In_box and Out_box

16.4.4 Menus

16.5 An example

16.6 Control inversion

16.7 Adding a menu

16.8 Debugging GUI code


16.1 User interface alternatives

Image

Every program has a user interface. A program running on a small gadget may be limited to input from a couple of push buttons and to a blinking light for output. Other computers are connected to the outside world only by a wire. Here, we will consider the common case in which our program communicates with a user who is watching a screen and using a keyboard and a pointing device (such as a mouse). In this case, we as programmers have three main choices:

• Use console input and output: This is a strong contender for technical/professional work where the input is simple and textual, consisting of commands and short data items (such as file names and simple data values). If the output is textual, we can display it on the screen or store it in files. The C++ standard library iostreams (Chapters 1011) provide suitable and convenient mechanisms for this. If graphical output is needed, we can use a graphics display library (as shown in Chapters 1215) without making dramatic changes to our programming style.

• Use a graphical user interface (GUI) library: This is what we do when we want our user interaction to be based on the metaphor of manipulating objects on the screen (pointing, clicking, dragging and dropping, hovering, etc.). Often (but not always), that style goes together with a high degree of graphically displayed information. Anyone who has used a modern computer knows examples where that is convenient. Anyone who wants to match the “feel” of Windows/Mac applications must use a GUI style of interaction.

• Use a web browser interface: For that, we need to use a markup (layout) language, such as HTML, and usually a scripting language. Showing how to do this is beyond the scope of this book, but it is often the ideal for applications that require remote access. In that case, the communication between the program and the screen is again textual (using streams of characters). A browser is a GUI application that translates some of that text into graphical elements and translates the mouse clicks, etc. into textual data that can be sent back to the program.

Image

To many, the use of GUI is the essence of modern programming, and sometimes the interaction with objects on the screen is considered the central concern of programming. We disagree: GUI is a form of I/O, and separation of the main logic of an application from I/O is among our major ideals for software. Wherever possible, we prefer to have a clean interface between our main program logic and the parts of the program we use to get input and produce output. Such a separation allows us to change the way a program is presented to a user, to port our programs to use different I/O systems, and — most importantly — to think about the logic of the program and its interaction with users separately.

That said, GUI is important and interesting from several perspectives. This chapter explores both the ways we can integrate graphical elements into our applications and how we can keep interface concerns from dominating our thinking.

16.2 The “Next” button

How did we provide that “Next” button that we used to drive the graphics examples in Chapters 1215? There, we do graphics in a window using a button. Obviously, that is a simple form of GUI programming. In fact, it is so simple that some would argue that it isn’t “true GUI.” However, let’s see how it was done because it will lead directly into the kind of programming that everyone recognizes as GUI programming.

Our code in Chapters 1215 is conventionally structured like this:

// create objects and/or manipulate objects, display them in Window win:
win.wait_for_button();

// create objects and/or manipulate objects, display them in Window win:
win.wait_for_button();

ve input from the keyboard. For example: // create objects and/or manipulate objects, display them in Window win:
win.wait_for_button();

Each time we reach wait_for_button(), we can look at our objects on the screen until we hit the button to get the output from the next part of the program. From the point of view of program logic, this is no different from a program that writes lines of output to a screen (a console window), stopping now and then to receive input from the keyboard. For example:

// define variables and/or compute values, produce output
cin >> var;      // wait for input

// define variables and/or compute values, produce output
cin >> var;      // wait for input

// define variables and/or compute values, produce output
cin >> var;      // wait for input

Image

From an implementation point of view, these two kinds of programs are quite different. When your program executes cin >> var, it stops and waits for “the system” to bring back characters you typed. However, the system (the graphical user interface system) that looks after your screen and tracks the mouse as you use it works on a rather different model: the GUI keeps track of where the mouse is and what the user is doing with the mouse (clicking, etc.). When your program wants an action, it must

• Tell the GUI what to look for (e.g., “Someone clicked the ‘Next’ button”)

• Tell what is to be done when someone does that

• Wait until the GUI detects an action that the program is interested in

What is new and different here is that the GUI does not just return to our program; it is designed to respond in different ways to different user actions, such as clicking on one of many buttons, resizing windows, redrawing the window after it has been obscured by another, and popping up pop-up menus.

For starters, we just want to say, “Please wake me up when someone clicks my button”; that is, “Please continue executing my program when someone clicks the mouse button and the cursor is in the rectangular area where the image of my button is displayed.” This is just about the simplest action we could imagine. However, such an operation isn’t provided by “the system” so we wrote one ourselves. Seeing how that is done is the first step in understanding GUI programming.

16.3 A simple window

Image

Basically, “the system” (which is a combination of a GUI library and the operating system) continuously tracks where the mouse is and whether its buttons are pressed or not. A program can express interest in an area of the screen and ask “the system” to call a function when “something interesting” happens. In this particular case, we ask the system to call one of our functions (a “callback function”) when the mouse button is clicked “on our button.” To do that we must

• Define a button

• Get it displayed

• Define a function for the GUI to call

• Tell the GUI about that button and that function

• Wait for the GUI to call our function

Let’s do that. A button is part of a Window, so (in Simple_window.h) we define our class Simple_window to contain a member next_button:

struct Simple_window : Graph_lib::Window {
          Simple_window(Point xy, int w, int h, const string& title);

          void wait_for_button();     // simple event loop
private:
          Button next_button;         // the “Next” button
          bool button_pushed;       // implementation detail

          static void cb_next(Address, Address);       // callback for next_button
          void next();  // action to be done when next_button is pressed
};

Obviously, Simple_window is derived from Graph_lib’s Window. All our windows must be derived directly or indirectly from Graph_lib::Window because it is the class that (through FLTK) connects our notion of a window with the system’s window implementation. For details of Window’s implementation, see §E.3.

Our button is initialized in Simple_window’s constructor:

Simple_window::Simple_window(Point xy, int w, int h, const string& title)
          :Window{xy,w,h,title},
          next_button{Point{x_max()–70,0}, 70, 20, "Next", cb_next},
          button_pushed{false}
{
          attach(next_button);
}

Unsurprisingly, Simple_window passes its location (xy), size (w,h), and title (title) on to Graph_lib’s Window to deal with. Next, the constructor initializes next_button with a location (Point{x_max()–70,0}; that’s roughly the top right corner), a size (70,20), a label ("Next"), and a “callback” function (cb_next). The first four parameters exactly parallel what we do for a Window: we place a rectangular shape on the screen and label it.

Finally, we attach() our next_button to our Simple_window; that is, we tell the window that it must display the button in its position and make sure that the GUI system knows about it.

The button_pushed member is a pretty obscure implementation detail; we use it to keep track of whether the button has been pushed since last we executed next(). In fact, just about everything here is implementation details, and therefore declared private. Ignoring the implementation details, we see

struct Simple_window : Graph_lib::Window {
          Simple_window(Point xy, int w, int h, const string& title);

          void wait_for_button();     // simple event loop

          // . . .
};

That is, a user can make a window and wait for its button to be pushed.

16.3.1 A callback function

Image

The function cb_next() is the new and interesting bit here. This is the function that we want the GUI system to call when it detects a click on our button. Since we give the function to the GUI for the GUI to “call back to us,” it’s commonly called a callback function. We indicate cb_next()’s intended use with the prefix cb_ for “callback.” That’s just to help us — no language or library requires that naming convention. Obviously, we chose the name cb_next because it is to be the callback for our “Next” button. The definition of cb_next is an ugly piece of “boilerplate.”

Before showing that code, let’s consider what is going on here:

Image

Image

Our program runs on top of several “layers” of code. It uses our graphics library that we implement using the FLTK library, which is implemented using operating system facilities. In a system, there may be even more layers and sub-layers. Somehow, a click detected by the mouse’s device driver has to cause our function cb_next() to be called. We pass the address of cb_next() and the address of our Simple_window down through the layers of software; some code “down there” then calls cb_next() when the “Next” button is pressed.

The GUI system (and the operating system) can be used by programs written in a variety of languages, so it cannot impose some nice C++ style on all users. In particular, it does not know about our Simple_window class or our Button class. In fact, it doesn’t know about classes or member functions at all. The type required for a callback function is chosen so that it is usable from the lowest level of programming, including C and assembler. A callback function returns no value and takes two addresses as its arguments. We can declare a C++ member function that obeys those rules like this:

static void cb_next(Address, Address);   // callback for next_button

Image

The keyword static is there to make sure that cb_next() can be called as an ordinary function, that is, not as a C++ member function invoked for a specific object. Having the system call a proper C++ member function would have been much nicer. However, the callback interface has to be usable from many languages, so this is what we get: a static member function. The Address arguments specify that cb_next() takes arguments that are addresses of “something in memory.” C++ references are unknown to most languages, so we can’t use those. The compiler isn’t told what the types of those “somethings” are. We are close to the hardware here and don’t get the usual help from the language. “The system” will invoke a callback function with the first argument being the address of the GUI entity (Widget) for which the callback was triggered. We won’t use that first argument, so we don’t bother to name it. The second argument is the address of the window containing that Widget; for cb_next(), that will be our Simple_window. We can use that information like this:

void Simple_window::cb_next(Address, Address pw)
// call Simple_window::next() for the window located at pw
{
          reference_to<Simple_window>(pw).next();
}

The reference_to<Simple_window>(pw) tells the compiler that the address in pw is to be considered the address of a Simple_window; that is, we can use reference_to<Simple_window>(pw) as a reference to a Simple_window. In Chapters 17 and 18, we will return to the issue of addressing memory. In §E.1, we present the (by then, trivial) definition of reference_to. For now, we are just glad that we finally obtained a reference to our Simple_window so that we can access our data and functions exactly as we like and are used to. Finally, we get out of this system-dependent code as quickly as possible by calling our member function next().

Image

We could have written all the code we wanted to execute in cb_next(), but we — like most good GUI programmers — prefer to keep messy low-level stuff separate from our nice user code, so we handle a callback with two functions:

• cb_next() simply maps the system conventions for a callback into a call to an ordinary member function (next()).

• next() does what we want done (without having to know about the messy conventions of callbacks).

Image

The fundamental reason for using two functions here is the general principle that “a function should perform a single logical action”: cb_next() gets us out of the low-level system-dependent part of the system and next() performs our desired action. Whenever we want a callback (from “the system”) to one of our windows, we define such a pair of functions; for example, see §16.57. Before going further, let’s repeat what is going on here:

• We define our Simple_window.

• Simple_window’s constructor registers its next_button with the GUI system.

• When we click the image of next_button on the screen, the GUI calls cb_next().

• cb_next() converts the low-level system information into a call of our member function next() for our window.

• next() performs whatever action we want done in response to the button click.

That’s a rather elaborate way of getting a function called. But remember that we are dealing with the basic mechanism for communicating an action of a mouse (or other hardware device) to a program. In particular:

• There are typically many programs running.

• The program is written long after the operating system.

• The program is written long after the GUI library.

• The program can be written in a language that is different from that used in the operating system.

• The technique deals with all kinds of interactions (not just our little button push).

• A window can have many buttons; a program can have many windows.

However, once we understand how next() is called, we basically understand how to deal with every action in a program with a GUI interface.

16.3.2 A wait loop

So, in this — our simplest — case, what do we want done by Simple_window’s next() each time the button is “pressed”? Basically, we want an operation that stops the execution of our program at some point, giving us a chance to see what has been done so far. And, we want next() to restart our program after that wait:

// create some objects and/or manipulate some objects, display them in a window
win.wait_for_button();           // next() causes the program to proceed from here
// create some objects and/or manipulate some objects

Actually, that’s easily done. Let’s first define wait_for_button():

void Simple_window::wait_for_button()
          // modified event loop:
          // handle all events (as per default), quit when button_pushed becomes true
          // this allows graphics without control inversion
{

          while (!button_pushed) Fl::wait();
          button_pushed = false;
          Fl::redraw();
}

Image

Like most GUI systems, FLTK provides a function that stops a program until something happens. The FLTK version is called wait(). Actually, wait() takes care of lots of things because our program gets woken up whenever anything that affects it happens. For example, when running under Microsoft Windows, it is the job of a program to redraw its window when it is being moved or becomes visible after having been hidden by another window. It is also the job of the Window to handle resizing. The Fl::wait() handles all of these tasks in the default manner. Each time wait() has dealt with something, it returns to give our code a chance to do something.

So, when someone clicks our “Next” button, wait() calls cb_next() and returns (to our “wait loop”). To proceed in wait_for_button(), next() just has to set the Boolean variable button_pushed to true. That’s easy:

void Simple_window::next()
{
          button_pushed = true;
}

Of course we also need to define button_pushed somewhere:

bool button_pushed;   // initialized to false in the constructor

After waiting, wait_for_button() needs to reset button_pushed and redraw() the window to make sure that any changes we made can be seen on the screen. So that’s what it did.

16.3.3 A lambda expression as a callback

So for each action on a Widget, we have to define two functions: one to map from the system’s notion of a callback and one to do our desired action. Consider:

struct Simple_window : Graph_lib::Window {
          Simple_window{Point xy, int w, int h, const string& title};

          void wait_for_button();     // simple event loop
private:
          Button next_button;         // the “Next” button
          bool button_pushed;       // implementation detail

          static void cb_next(Address, Address);       // callback for next_button
          void next();                  // action to be done when next_button is pressed
};

By using a lambda expression (§15.3.3), we can eliminate the need to explicitly declare the mapping function cb_next(). Instead, we define the mapping in Simple_window’s constructor:

Simple_window::Simple_window(Point xy, int w, int h, const string& title)
          :Window{xy,w,h,title},
          next_button{Point{x_max()–70,0}, 70, 20, "Next",
                    [](Address, Address pw) { reference_to<Simple_window>
                    (pw).next(); }

},
          button_pushed{false}
{
          attach(next_button);
}

16.4 Button and other Widgets

We define a Button like this:

struct Button : Widget {
          Button(Point xy, int w, int h, const string& label, Callback cb);
          void attach(Window&);
};

Image

So, a Button is a Widget with a location (xy), a size (w,h), a text label (label), and a callback (cb). Basically, anything that appears on a screen with an action (e.g., a callback) associated is a Widget.

16.4.1 Widgets

Yes, widget really is a technical term. A more descriptive, but less evocative, name for a widget is a control. We use widgets to define forms of interaction with a program through a GUI (graphical user interface). Our Widget interface class looks like this:

class Widget {
          // Widget is a handle to an Fl_widget — it is *not* an Fl_widget
          // we try to keep our interface classes at arm’s length from FLTK
public:
          Widget(Point xy, int w, int h, const string& s, Callback cb);

          virtual void move(int dx,int dy);
          virtual void hide();
          virtual void show();
          virtual void attach(Window&) = 0;

          Point loc;
          int width;
          int height;
          string label;
          Callback do_it;
protected:
          Window* own;  // every Widget belongs to a Window
          Fl_Widget* pw;  // connection to the FLTK Widget
};

A Widget has two interesting functions that we can use for Button (and also for any other class derived from Widget, e.g., a Menu; see §16.7):

• hide() makes the Widget invisible.

• show() makes the Widget visible again.

A Widget starts out visible.

Just like a Shape, we can move() a Widget in its Window, and we must attach() it to a Window before it can be used. Note that we declared attach() to be a pure virtual function (§14.3.5): every class derived from Widget must define what it means for it to be attached to a Window. In fact, it is in attach() that the system-level widgets are created. The attach() function is called from Window as part of its implementation of Window’s own attach(). Basically, connecting a window and a widget is a delicate little dance where each has to do its own part. The result is that a window knows about its widgets and that each widget knows about its window:

Image

Note that a Window doesn’t know what kind of Widgets it deals with. As described in §14.4, we are using basic object-oriented programming to ensure that a Window can deal with every kind of Widget. Similarly, a Widget doesn’t know what kind of Window it deals with.

We have been slightly sloppy, leaving data members accessible. The own and pw members are strictly for the implementation of derived classes so we have declared them protected.

The definitions of Widget and of the widgets we use here (Button, Menu, etc.) are found in GUI.h.

16.4.2 Buttons

A Button is the simplest Widget we deal with. All it does is to invoke a callback when we click on it:

class Button : public Widget {
public:
          Button(Point xy, int ww, int hh, const string& s, Callback cb)
                    :Widget{xy,ww,hh,s,cb} { }

          void attach(Window& win);
};

That’s all. The attach() function contains all the (relatively) messy FLTK code. We have banished the explanation to Appendix E (not to be read until after Chapters 17 and 18). For now, please just note that defining a simple Widget isn’t particularly difficult.

Image

We do not deal with the somewhat complicated and messy issue of how buttons (and other Widgets) look on the screen. The problem is that there is a near infinity of choices and that some styles are mandated by certain systems. Also, from a programming technique point of view, nothing really new is needed for expressing the looks of buttons. If you get desperate, we note that placing a Shape on top of a button doesn’t affect the button’s ability to function — and you know how to make a shape look like anything at all.

16.4.3 In_box and Out_box

We provide two Widgets for getting text in and out of our program:

struct In_box : Widget {
          In_box(Point xy, int w, int h, const string& s)
                    :Widget{xy,w,h,s,0} { }
          int get_int();
          string get_string();

          void attach(Window& win);
};

struct Out_box : Widget {
          Out_box(Point xy, int w, int h, const string& s)
                    :Widget{xy,w,h,s,0} { }
          void put(int);
          void put(const string&);

          void attach(Window& win);
};

An In_box can accept text typed into it, and we can read that text as a string using get_string() or as an integer using get_int(). If you want to know if text has been entered, you can read using get_string() and see if you get the empty string:

string s = some_inbox.get_string();
if (s =="") {
          // deal with missing input
}

An Out_box is used to present some message to a user. In analogy to In_box, we can put() either integers or strings. §16.5 gives examples of the use of In_box and Out_box.

Image

We could have provided get_floating_point(), get_complex(), etc., but we did not bother because you can take the string, stick it into a stringstream, and do any input formatting you like that way (§11.4).

16.4.4 Menus

We offer a very simple notion of a menu:

struct Menu : Widget {
          enum Kind { horizontal, vertical };
          Menu(Point xy, int w, int h, Kind kk, const string& label);
          Vector_ref<Button> selection;
          Kind k;
          int offset;
          int attach(Button& b);                // attach Button to Menu
          int attach(Button* p);                 // attach new Button to Menu

          void show()                                   // show all buttons
          {
                    for (Button& b : selection) b.show();
          }
          void hide();                                  // hide all buttons
          void move(int dx, int dy);         // move all buttons

          void attach(Window& win);     // attach all buttons to Window win
};

A Menu is basically a vector of buttons. As usual, the Point xy is the top left corner. The width and height are used to resize buttons as they are added to the menu. For examples, see §16.5 and §16.7. Each menu button (“a menu item”) is an independent Widget presented to the Menu as an argument to attach(). In turn, Menu provides an attach() operation to attach all of its Buttons to a Window. The Menu keeps track of its Buttons using a Vector_ref (§13.10, §E.4). If you want a “pop-up” menu, you have to make it yourself; see §16.7.

16.5 An example

To get a better feel for the basic GUI facilities, consider the window for a simple application involving input, output, and a bit of graphics:

Image

This program allows a user to display a sequence of lines (an open polyline; §13.6) specified as a sequence of coordinate pairs. The idea is that the user repeatedly enters (x,y) coordinates in the “next x” and “next y” boxes; after each pair the user hits the “Next point” button.

Initially, the “current (x,y)” box is empty and the program waits for the user to enter the first coordinate pair. That done, the starting point appears in the “current (x,y)” box, and each new coordinate pair entered results in a line being drawn: a line from the current point (which has its coordinates displayed in the “current (x,y)” box) to the newly entered (x,y) is drawn, and that (x,y) becomes the new current point.

This draws an open polyline. When the user tires of this activity, there is the “Quit” button for exiting. That’s pretty straightforward, and the program exercises several useful GUI facilities: text input and output, line drawing, and multiple buttons. The window above shows the result after entering two coordinate pairs; after seven we can get this:

Image

Let’s define a class for representing such windows. It is pretty straightforward:

struct Lines_window : Window {
          Lines_window(Point xy, int w, int h, const string& title);
          Open_polyline lines;
private:
          Button next_button;          // add (next_x,next_y) to lines
          Button quit_button;
          In_box next_x;
          In_box next_y;
          Out_box xy_out;

          void next();
          void quit();
};

The line is represented as an Open_polyline. The buttons and boxes are declared (as Buttons, In_boxes, and Out_boxes), and for each button a member function implementing the desired action is defined. We decided to eliminate the “boilerplate” callback function and use lambdas instead.

Lines_window’s constructor initializes everything:

Lines_window::Lines_window(Point xy, int w, int h, const string& title)
          :Window{xy,w,h,title},
          next_button{Point{x_max()–150,0}, 70, 20, "Next point",
                    [](Address, Address pw) {reference_to<Lines_window>(pw).next();},
          quit_button{Point{x_max()–70,0}, 70, 20, "Quit",
                    [](Address, Address pw) {reference_to<Lines_window>(pw).quit();},
          next_x{Point{x_max()–310,0}, 50, 20, "next x:"},
          next_y{Point{x_max()–210,0}, 50, 20, "next y:"},
          xy_out{Point{100,0}, 100, 20, "current (x,y):"}
{
          attach(next_button);
          attach(quit_button);
          attach(next_x);
          attach(next_y);
          attach(xy_out);
          attach(lines);
}

That is, each widget is constructed and then attached to the window.

The “Quit” button deletes the Window. That’s done using the curious FLTK idiom of simply hiding it:

void Lines_window::quit()
{
          hide();          // curious FLTK idiom to delete window
}

All the real work is done in the “Next point” button: it reads a pair of coordinates, updates the Open_polyline, updates the position readout, and redraws the window:

void Lines_window::next()
{
          int x = next_x.get_int();
          int y = next_y.get_int();
          lines.add(Point{x,y});

          // update current position readout:
          ostringstream ss;
          ss << '(' << x << ',' << y << ')';
          xy_out.put(ss.str());

          redraw();
}

That’s all pretty obvious. We get integer coordinates from the In_boxes using get_int(). We use an ostringstream to format the string to be put into the Out_box; the str() member function lets us get to the string within the ostringstream. The final redraw() here is needed to present the results to the user; until a Window’s redraw() is called, the old image remains on the screen.

So what’s odd and different about this program? Let’s see its main():

#include "GUI.h"

int main()
try {
          Lines_window win {Point{100,100},600,400,"lines"};
          return gui_main();
}
catch(exception& e) {
          cerr << "exception: " << e.what() << '\n';
          return 1;
}
catch (. . .) {
          cerr << "Some exception\n";
          return 2;
}

There is basically nothing there! The body of main() is just the definition of our window, win, and a call to a function gui_main(). There is not another function, if, switch, or loop — nothing of the kind of code we saw in Chapters 6 and 7 — just a definition of a variable and a call to the function gui_main(), which is itself just a call of FLTK’s run(). Looking further, we can find that run() is simply the infinite loop

while(wait());

Except for a few implementation details postponed to Appendix E, we have seen all of the code that makes our “lines” program run. We have seen all of the fundamental logic. So what happens?

16.6 Control inversion

Image

What happened was that we moved the control of the order of execution from the program to the widgets: whichever widget the user activates, runs. For example, click on a button and its callback runs. When that callback returns, the program settles back, waiting for the user to do something else. Basically, wait() tells “the system” to look out for the widgets and invoke the appropriate callbacks. In theory, wait() could tell you, the programmer, which widget requested attention and leave it to you to call the appropriate function. However, in FLTK and most other GUI systems,wait() simply invokes the appropriate callback, saving you the bother of writing code to select it.

A “conventional program” is organized like this:

Image

A “GUI program” is organized like this:

Image

Image

One implication of this “control inversion” is that the order of execution is completely determined by the actions of the user. This complicates both program organization and debugging. It is hard to imagine what a user will do and hard to imagine every possible effect of a random sequence of callbacks. This makes systematic testing a nightmare (see Chapter 26). The techniques for dealing with that are beyond the scope of this book, but we encourage you to be extra careful with code driven by users through callbacks. In addition to the obvious control flow problems, there are also problems of visibility and difficulties with keeping track of which widget is connected to what data. To minimize hassle, it is essential to keep the GUI portion of a program simple and to build a GUI program incrementally, testing at each stage. When working on a GUI program, it is almost essential to draw little diagrams of the objects and their interactions.

How does the code triggered by the various callbacks communicate? The simplest way is for the functions to operate on data stored in the window, as was done in the example in §16.5. There, the Lines_window’s next() function, invoked by pressing the “Next point” button, reads data from the In_boxes (next_x and next_y) and updates the lines member variable and the Out_box (xy_out). Obviously, a function invoked by a callback can do anything: it could open files, connect to the web, etc. However, for now, we’ll just consider the simple case in which we hold our data in a window.

16.7 Adding a menu

Let’s explore the control and communication issues raised by “control inversion” by providing a menu for our “lines” program. First, we’ll simply provide a menu that allows the user to change the color of all lines in the lines member variable. We add the menu color_menu and its callbacks:

struct Lines_window : Window {
          Lines_window(Point xy, int w, int h, const string& title);

          Open_polyline lines;
          Menu color_menu;

          static void cb_red(Address, Address);         // callback for red button
          static void cb_blue(Address, Address);       // callback for blue button
          static void cb_black(Address, Address);      // callback for black button

          // the actions:
          void red_pressed() { change(Color::red); }
          void blue_pressed() { change(Color::blue); }
          void black_pressed() { change(Color::black); }
          void change(Color c) { lines.set_color(c); }

          // . . . as before . . .
};

Writing all of those almost identical callback functions and “action” functions is tedious. However, it is conceptually simple, and offering something that’s significantly simpler to type in is beyond the scope of this book. If you prefer, you can eliminate the cb_ functions by using lambdas (§16.3.3). When a menu button is pressed, it changes the lines to the requested color.

Having defined the color_menu member, we need to initialize it:

Lines_window::Lines_window(Point xy, int w, int h, const string& title)
          :Window(xy,w,h,title),
          // . . . as before . . .
          color_menu{Point{x_max()–70,40},70,20,Menu::vertical,"color"}
{
          // . . . as before . . .
          color_menu.attach(new Button{Point{0,0},0,0,"red",cb_red});
          color_menu. attach(new Button{Point{0,0},0,0,"blue",cb_blue});
          color_menu. attach(new Button{Point{0,0},0,0,"black",cb_black});
          attach(color_menu);
}

The buttons are dynamically attached to the menu (using attach()) and can be removed and/or replaced as needed. Menu::attach() adjusts the size and location of the button and attaches it to the window. That’s all, and we get

Image

Having played with this for a while, we decided that what we really wanted was a “pop-up menu”; that is, we didn’t want to spend precious screen space on a menu except when we are using it. So, we added a “color menu” button. When we press that, up pops the color menu, and when we have made a selection, the menu is again hidden and the button appears.

Here first is the window after we have added a few lines:

Image

We see the new “color menu” button and some (black) lines. Press “color menu” and the menu appears:

Image

Note that the “color menu” button is now hidden. We don’t need it until we are finished with the menu. Press “blue” and we get

Image

The lines are now blue and the “color menu” button has reappeared.

To achieve this we added the “color menu” button and modified the “pressed” functions to adjust the visibility of the menu and the button. Here is the complete Lines_window after all of our modifications:

struct Lines_window : Window {
          Lines_window(Point xy, int w, int h, const string& title);
private:
          // data:
          Open_polyline lines;

          // widgets:
          Button next_button;   // add (next_x,next_y) to lines
          Button quit_button;   // end program
          In_box next_x;
          In_box next_y;
          Out_box xy_out;
          Menu color_menu;
          Button menu_button;

          void change(Color c) { lines.set_color(c); }

          void hide_menu() { color_menu.hide(); menu_button.show(); }

          // actions invoked by callbacks:
          void red_pressed() { change(Color::red); hide_menu(); }
          void blue_pressed() { change(Color::blue); hide_menu(); }
          void black_pressed() { change(Color::black); hide_menu(); }
          void menu_pressed() { menu_button.hide(); color_menu.show(); }
          void next();
          void quit();

          // callback functions:
          static void cb_red(Address, Address);
          static void cb_blue(Address, Address);
          static void cb_black(Address, Address);
          static void cb_menu(Address, Address);
          static void cb_next(Address, Address);
          static void cb_quit(Address, Address);
};

Note how all but the constructor is private. Basically, that Window class is the program. All that happens, happens through its callbacks, so no code from outside the window is needed. We sorted the declarations a bit hoping to make the class more readable. The constructor provides arguments to all of its sub-objects and attaches them to the window:

Lines_window::Lines_window(Point xy, int w, int h, const string& title)
          :Window{xy,w,h,title},
          next_button{Point{x_max()–150,0}, 70, 20, "Next point", cb_next},
          quit_button{Point{x_max()–70,0}, 70, 20, "Quit", cb_quit},
          next_x{Point{x_max()–310,0}, 50, 20, "next x:"},
          next_y{Point{x_max()–210,0}, 50, 20, "next y:"},
          xy_out{Point{100,0}, 100, 20, "current (x,y):"},
          color_menu{Point{x_max()–70,30},70,20,Menu::vertical,"color"},
          menu_button{Point{x_max()–80,30}, 80, 20, "color menu", cb_menu}
{
          attach(next_button);
          attach(quit_button);
          attach(next_x);
          attach(next_y);
          attach(xy_out);
          xy_out.put("no point");
          color_menu.attach(new Button{Point{0,0},0,0,"red",cb_red));
          color_menu.attach(new Button{Point{0,0},0,0,"blue",cb_blue));
          color_menu.attach(new Button{Point{0,0},0,0,"black",cb_black));
          attach(color_menu);
          color_menu.hide();
          attach(menu_button);
          attach(lines);
}

Image

Note that the initializers are in the same order as the data member definitions. That’s the proper order in which to write the initializers. In fact, member initializers are always executed in the order their data members were declared. Some compilers (helpfully) give a warning if a base or member constructor is specified out of order.

16.8 Debugging GUI code

Once a GUI program starts working it is often quite easy to debug: what you see is what you get. However, there is often a most frustrating period before the first shapes and widgets start appearing in a window or even before a window appears on the screen. Try this main():

int main()
{
          Lines_window {Point{100,100},600,400,"lines"};
          return gui_main();
}

Image

Do you see the error? Whether you see it or not, you should try it; the program will compile and run, but instead of the Lines_window giving you a chance to draw lines, you get at most a flicker on the screen. How do you find errors in such a program?

• By carefully using well-tried program parts (classes, function, libraries)

• By simplifying all new code, by slowly “growing” a program from its simplest version, by carefully looking over the code line by line

• By checking all linker settings

• By comparing the code to already working programs

• By explaining the code to a friend

Image

The one thing that you will find it hard to do is to trace the execution of the code. If you have learned to use a debugger, you have a chance, but just inserting “output statements” will not work in this case — the problem is that no output appears. Even debuggers will have problems because there are several things going on at once (“multi-threading”) — your code is not the only code trying to interact with the screen. Simplification of the code and a systematic approach to understanding the code are key.

So what was the problem? Here is the correct version (from §16.5):

int main()
{
          Lines_window win{Point{100,100},600,400,"lines"};
          return gui_main();
}

We “forgot” the name of the Lines_window, win. Since we didn’t actually need that name that seemed reasonable, but the compiler then decided that since we didn’t use that window, it could immediately destroy it. Oops! That window existed for something on the order of a millisecond. No wonder we missed it.

Image

Another common problem is to put one window exactly on top of another. This obviously (or rather not at all obviously) looks as if there is only one window. Where did the other window go? We can spend significant time looking for nonexistent bugs in the code. The same problem can occur if we put one shape on top of another.

Image

Finally — to make matters still worse — exceptions don’t always work as we would like them to when we use a GUI library. Since our code is managed by a GUI library, an exception we throw may never reach our handler — the library or the operating system may “eat” it (that is, they may rely on error-handling mechanisms that differ from C++ exceptions and may indeed be completely oblivious of C++).

Image

Common problems found during debugging include Shapes and Widgets not showing because they were not attached and objects misbehaving because they have gone out of scope. Consider how a programmer might factor out the creation and attachment of buttons in a menu:

// helper function for loading buttons into a menu
void load_disaster_menu(Menu& m)
{
          Point orig {0,0};
          Button b1 {orig,0,0,"flood",cb_flood};
          Button b2 {orig,0,0,"fire",cb_fire};
          // . . .
          m.attach(b1);
          m.attach(b2);
          // . . .
}

int main()
{
          // . . .
          Menu disasters {Point{100,100},60,20,Menu::horizontal,"disasters"};
          load_disaster_menu(disasters);
          win.attach(disasters);
          // . . .
}

This will not work. All those buttons are local to the load_disaster_menu function and attaching them to a menu will not change that. An explanation can be found in §18.6.4 (Don’t return a pointer to a local variable), and an illustration of the memory layout for local variables is presented in §8.5.8. The essence of the story is that after load_disaster_menu() has returned, those local objects have been destroyed and the disasters menu refers to nonexistent (destroyed) objects. The result is likely to be surprising and not pretty. The solution is to use unnamed objects created by newinstead of named local objects:

// helper function for loading buttons into a menu
void load_disaster_menu(Menu& m)
{
          Point orig {0,0};
          m.attach(new Button{orig,0,0,"flood",cb_flood});
          m.attach(new Button{orig,0,0,"fire",cb_fire});
          // . . .
}

The correct solution is even simpler than the (all too common) bug.

Image Drill

1. Make a completely new project with linker settings for FLTK (as described in Appendix D).

2. Using the facilities of Graph_lib, type in the line-drawing program from §16.5 and get it to run.

3. Modify the program to use a pop-up menu as described in §16.7 and get it to run.

4. Modify the program to have a second menu for choosing line styles and get it to run.

Review

1. Why would you want a graphical user interface?

2. When would you want a non-graphical user interface?

3. What is a software layer?

4. Why would you want to layer software?

5. What is the fundamental problem when communicating with an operating system from C++?

6. What is a callback?

7. What is a widget?

8. What is another name for widget?

9. What does the acronym FLTK mean?

10. How do you pronounce FLTK?

11. What other GUI toolkits have you heard of?

12. Which systems use the term widget and which prefer control?

13. What are examples of widgets?

14. When would you use an inbox?

15. What is the type of the value stored in an inbox?

16. When would you use a button?

17. When would you use a menu?

18. What is control inversion?

19. What is the basic strategy for debugging a GUI program?

20. Why is debugging a GUI program harder than debugging an “ordinary program using streams for I/O”?

Terms

button

callback

console I/O

control

control inversion

dialog box

GUI

menu

software layer

user interface

visible/hidden

waiting for input

wait loop

widget

Exercises

1. Make a My_window that’s a bit like Simple_window except that it has two buttons, next and quit.

2. Make a window (based on My_window) with a 4-by-4 checkerboard of square buttons. When pressed, a button performs a simple action, such as printing its coordinates in an output box, or turns a slightly different color (until another button is pressed).

3. Place an Image on top of a Button; move both when the button is pushed. Use this random number generator from std_lib_facilities.h to pick a new location for the “image button”:

#include<random>

inline int rand_int(int min, int max)
{
          static default_random_engine ran;
          return uniform_int_distribution<>{min,max}(ran);
}

It returns a random int in the range [min,max).

4. Make a menu with items that make a circle, a square, an equilateral triangle, and a hexagon, respectively. Make an input box (or two) for giving a coordinate pair, and place the shape made by pressing a menu item at that coordinate. Sorry, no drag and drop.

5. Write a program that draws a shape of your choice and moves it to a new point each time you click “Next.” The new point should be determined by a coordinate pair read from an input stream.

6. Make an “analog clock,” that is, a clock with hands that move. You get the time of day from the operating system through a library call. A major part of this exercise is to find the functions that give you the time of day and a way of waiting for a short period of time (e.g., a second for a clock tick) and to learn to use them based on the documentation you found. Hint: clock(), sleep().

7. Using the techniques developed in the previous exercises, make an image of an airplane “fly around” in a window. Have a “Start” and a “Stop” button.

8. Provide a currency converter. Read the conversion rates from a file on startup. Enter an amount in an input window and provide a way of selecting currencies to convert to and from (e.g., a pair of menus).

9. Modify the calculator from Chapter 7 to get its input from an input box and return its results in an output box.

10. Provide a program where you can choose among a set of functions (e.g., sin() and log()), provide parameters for those functions, and then graph them.

Postscript

GUI is a huge topic. Much of it has to do with style and compatibility with existing systems. Furthermore, much has to do with a bewildering variety of widgets (such as a GUI library offering many dozens of alternative button styles) that would make a traditional botanist feel quite at home. However, little of that has to do with fundamental programming techniques, so we won’t proceed in that direction. Other topics, such as scaling, rotation, morphing, three-dimensional objects, shadowing, etc., require sophistication in graphical and/or mathematical topics which we don’t assume here.

Image

One thing you should be aware of is that most GUI systems provide a “GUI builder” that allows you to design your window layouts graphically and attach callbacks and actions to buttons, menus, etc. specified graphically. For many applications, such a GUI builder is well worth using to reduce the tedium of writing “scaffolding code” such as our callbacks. However, always try to understand how the resulting programs work. Sometimes, the generated code is equivalent to what you have seen in this chapter. Sometimes more elaborate and/or expensive mechanisms are used.