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

Part V: Appendices

E. GUI Implementation

“When you finally understand
what you are doing,
things will go right.”

—Bill Fairbank

This appendix presents implementation details of callbacks, Window, Widget, and Vector_ref. In Chapter 16, we couldn’t assume the knowledge of pointers and casts needed for a more complete explanation, so we banished that explanation to this appendix.


E.1 Callback implementation

E.2 Widget implementation

E.3 Window implementation

E.4 Vector_ref

E.5 An example: manipulating Widgets


E.1 Callback implementation

We implemented callbacks like this:

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

Once you have understood Chapter 17, it is pretty obvious that an Address must be a void*. And, of course, reference_to<Simple_window>(addr) must somehow create a reference to a Simple_window from the void* called addr. However, unless you had previous programming experience, there was nothing “pretty obvious” or “of course” about that before you read Chapter 17, so let’s look at the use of addresses in detail.

As described in §A.17, C++ offers a way of giving a name to a type. For example:

typedef void* Address;           // Address is a synonym for void*

This means that the name Address can now be used instead of void*. Here, we used Address to emphasize that an address was passed, and also to hide the fact that void* is the name of the type of pointer to an object for which we don’t know the type.

So cb_next() receives a void* called addr as an argument and — somehow — promptly converts it to a Simple_window&:

reference_to<Simple_window>(addr)

The reference_to is a template function (§A.13):

template<class W> W& reference_to(Address pw)
           // treat an address as a reference to a W
{
             return *static_cast<W*>(pw);
}

Here, we used a template function to write ourselves an operation that acts as a cast from a void* to a Simple_window&. The type conversion, static_cast, is described in §17.8.

The compiler has no way of verifying our assertion that addr points to a Simple_window, but the language rule requires the compiler to trust the programmer here. Fortunately, we are right. The way we know that we are right is that FLTK is handing us back a pointer that we gave to it. Since we knew the type of the pointer when we gave it to FLTK, we can use reference_to to “get it back.” This is messy, unchecked, and not all that uncommon at the lower levels of a system.

Once we have a reference to a Simple_window, we can use it to call a member function of Simple_window. For example (§16.3):

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

We use the messy callback function cb_next() simply to adjust the types as needed to call a perfectly ordinary member function next().

E.2 Widget implementation

Our Widget interface class looks like this:

class Widget {
          // Widget is a handle to a Fl_widget — it is *not* a 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)
                        :loc(xy), width(w), height(h), label(s), do_it(cb)
{ }

           virtual ~Widget() { }                // destructor

           virtual void move(int dx,int dy)
                     { hide(); pw–>position(loc.x+=dx, loc.y+=dy); show(); }

           virtual void hide() { pw–>hide(); }
           virtual void show() { pw–>show(); }

           virtual void attach(Window&) = 0;   // each Widget defines at least
                                                                                // one action for a window

           Point loc;
           int width;
           int height;
           string label;
           Callback do_it;

protected:
  Window* own;           // every Widget belongs to a Window
  Fl_Widget* pw;          // a Widget “knows” its Fl_Widget
};

Note that our Widget keeps track of its FLTK widget and the Window with which it is associated. Note that we need pointers for that because a Widget can be associated with different Windows during its life. A reference or a named object wouldn’t suffice. (Why not?)

It has a location (loc), a rectangular shape (width and height), and a label. Where it gets interesting is that it also has a callback function (do_it) — it connects a Widget’s image on the screen to a piece of our code. The meaning of the operations (move(), show(), hide(), and attach()) should be obvious.

Widget has a “half-finished” look to it. It was designed as an implementation class that users should not have to see very often. It is a good candidate for a redesign. We are suspicious about all of those public data members, and “obvious” operations typically need to be reexamined for unplanned subtleties.

Widget has virtual functions and can be used as a base class, so it has a virtual destructor (§17.5.2).

E.3 Window implementation

When do we use pointers and when do we use references instead? We examine that general question in §8.5.6. Here, we’ll just observe that some programmers like pointers and that we need pointers when we want to point to different objects at different times in a program.

So far, we have not shown one of the central classes in our graphics and GUI library, Window. The most significant reasons are that it uses a pointer and that its implementation using FLTK requires free store. As found in Window.h, here it is:

class Window : public Fl_Window {
public:
           // let the system pick the location:
           Window(int w, int h, const string& title);
           // top left corner in xy:
           Window(Point xy, int w, int h, const string& title);

           virtual ~Window() { }

           int x_max() const { return w; }
           int y_max() const { return h; }

           void resize(int ww, int hh) { w=ww, h=hh; size(ww,hh); }

           void set_label(const string& s) { label(s.c_str()); }

           void attach(Shape& s) { shapes.push_back(&s); }
           void attach(Widget&);

           void detach(Shape& s);             // remove w from shapes
           void detach(Widget& w);          // remove w from window
                                                                       // (deactivates callbacks)

           void put_on_top(Shape& p);  // put p on top of other shapes
protected:
           void draw();
private:
           vector<Shape*> shapes;          // shapes attached to window
           int w,h;                                         // window size

           void init();
};

So, when we attach() a Shape we store a pointer in shapes so that the Window can draw it. Since we can later detach() that shape, we need a pointer. Basically, an attach()ed shape is still owned by our code; we just give the Window a reference to it. Window::attach() converts its argument to a pointer so that it can store it. As shown above, attach() is trivial; detach() is slightly less simple. Looking in Window.cpp, we find:

void Window::detach(Shape& s)
           // guess that the last attached will be first released
{
           for (vector<Shape*>::size_type i = shapes.size(); 0<i; ––i)
                     if (shapes[i–1]==&s)
                                shapes.erase(shapes.begin()+(i–1));
}

The erase() member function removes (“erases”) a value from a vector, decreasing the vector’s size by one (§20.7.1).

Window is meant to be used as a base class, so it has a virtual destructor (§17.5.2).

E.4 Vector_ref

Basically, Vector_ref simulates a vector of references. You can initialize it with references or with pointers:

• If an object is passed to Vector_ref as a reference, it is assumed to be owned by the caller who is responsible for its lifetime (e.g., the object is a scoped variable).

• If an object is passed to Vector_ref as a pointer, it is assumed to be allocated by new and it is Vector_ref’s responsibility to delete it.

An element is stored as a pointer — not as a copy of the object — into the Vector_ref and has reference semantics. For example, you can put a Circle into a Vector_ref<Shape> without suffering slicing.

template<class T> class Vector_ref {
           vector<T*> v;
           vector<T*> owned;
public:
           Vector_ref() {}
           Vector_ref(T* a, T* b = 0, T* c = 0, T* d = 0);

           ~Vector_ref() { for (int i=0; i<owned.size(); ++i) delete owned[i]; }

           void push_back(T& s) { v.push_back(&s); }
           void push_back(T* p) { v.push_back(p); owned.push_back(p); }

           T& operator[](int i) { return *v[i]; }
           const T& operator[](int i) const { return *v[i]; }

           int size() const { return v.size(); }
};

Vector_ref’s destructor deletes every object passed to the Vector_ref as a pointer.

E.5 An example: manipulating Widgets

Here is a complete program. It exercises many of the Widget/Window features. It is only minimally commented. Unfortunately, such insufficient commenting is not uncommon. It is an exercise to get this program to run and to explain it.

Basically, when you run it, it appears to define four buttons:

#include "../GUI.h"
using namespace Graph_lib;

class W7 : public Window {
           // four ways to make it appear that a button moves around:
           // show/hide, change location, create new one, and attach/detach
public:
           W7(int w, int h, const string& t);

           Button* p1;              // show/hide
           Button* p2;
           bool sh_left;

           Button* mvp;          // move
           bool mv_left;

           Button* cdp;          // create/destroy
           bool cd_left;

           Button* adp1;        // activate/deactivate
           Button* adp2;
           bool ad_left;

           void sh();                // actions
           void mv();
           void cd();
           void ad();

           static void cb_sh(Address, Address addr)            // callbacks
                        { reference_to<W7>(addr).sh(); }
           static void cb_mv(Address, Address addr)
                        { reference_to<W7>(addr).mv(); }
           static void cb_cd(Address, Address addr)
                        { reference_to<W7>(addr).cd(); }
           static void cb_ad(Address, Address addr)
                        { reference_to<W7>(addr).ad(); }
};

However, a W7 (Window experiment number 7) really has six buttons; it just keeps two hidden:

W7::W7(int w, int h, const string& t)
          :Window{w,h,t},
          sh_left{true}, mv_left{true}, cd_left{true}, ad_left{true}
{
         p1 = new Button{Point{100,100},50,20,"show",cb_sh};
         p2 = new Button{Point{200,100},50,20, "hide",cb_sh};

         mvp = new Button{Point{100,200},50,20,"move",cb_mv};

         cdp = new Button{Point{100,300},50,20,"create",cb_cd};

         adp1 = new Button{Point{100,400},50,20,"activate",cb_ad};
         adp2 = new Button{Point{200,400},80,20,"deactivate",cb_ad};

         attach(*p1);
         attach(*p2);
         attach(*mvp);
         attach(*cdp);
         p2–>hide();
         attach(*adp1);
}

There are four callbacks. Each makes it appear that the button you press disappears and a new one appears. However, this is achieved in four different ways:

void W7::sh()                              // hide a button, show another
{
          if (sh_left) {
                     p1–>hide();
                     p2–>show();
          }

          else {
                     p1–>show();
                     p2–>hide();
          }
          sh_left = !sh_left;
}

void W7::mv() // move the button
{
          if (mv_left) {
                     mvp–>move(100,0);
          }
          else {
                    mvp–>move(–100,0);
          }
          mv_left = !mv_left;

}
void W7::cd()      // delete the button and create a new one
{
          cdp–>hide();
          delete cdp;
          string lab = "create";
          int x = 100;
          if (cd_left) {
                    lab = "delete";
                    x = 200;
          }
          cdp = new Button{Point{x,300}, 50, 20, lab, cb_cd};
          attach(*cdp);
          cd_left = !cd_left;
}

void W7::ad()  // detach the button from the window and attach its replacement
{
          if (ad_left) {
                    detach(*adp1);
                    attach(*adp2);
          }
          else {
                   detach(*adp2);
                   attach(*adp1);
          }
          ad_left = !ad_left;
}

int main()
{
          W7 w{400,500,"move"};
          return gui_main();
}

This program demonstrates the fundamental ways of adding and subtracting widgets to/from a window — or just appearing to.