I have been busy now for a while with Stroustrup’s Programming book for novice programmers [1], which I had bought from a bookshop in Delft a long time ago.
One of the main strengths of the book are the chapters dedicated to graphics and graphical user interfaces (chapters 12 to 16), where the novice C++ programmer gets a visual feeling of how classes and objects work together for doing something useful. Computer graphics has always been one of the top applications since the beginning of modern computing, although not always appreciated as such. Back in high school, I can still remember how our computer science teacher immediately dismissed the request of learning some computer graphics as silly!
Of course, it doesn’t make sense to read a programming book and not try out what you learn, so when I got to the graphical part I decided to reimplement the classes while I was following along. The result is that you can run the examples found in [1] with some little change by using the graphics implementation that you find explained here. Some screenshots are given below.
Installation
You can download the Xcode project (version 13.4.1) from GitHub here, which is based on the FLTK GUI toolkit version 1.3.8. When you download FLTK, pronounced "fulltick", unzip it to a folder named fltk-1.3.8 and follow the instructions that you find inside the file README.OSX.txt for building it. Usually, you should be ready to go by just running the two commands configure and make from inside fltk-1.3.8. Run the make command also inside the subfolders png and zlib because we need those 2 libraries as well. After building everything, the content of the lib folder should look like this:
For building against the FTLK library from XCode you need to set the linker and compiler options, as well as the header and library search paths.
Set the linker options to
-lfltk -lfltk_images -lfltk_jpeg -lfltk_png -lfltk_z
-lpthread
-framework Cocoa
and the C++ compiler options to
-D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE
-D_THREAD_SAFE -D_REENTRANT
Finally, set the header search path to
<your absolute path>/fltk-1.3.8
and the library search path to
<your absolute path>/fltk-1.3.8/lib
Once the examples build and main() runs, keep pressing the Next button in order to go through them.
Class hierarchy
The following pictures illustrate the implemented class hierarchy:
By following chapter 9.1 of [2] , the classes Generic_Window and Widget extend and customise the corresponding FLTK classes deriving directly from them. As a consequence, this leads to a tight coupling to the underlying FLTK framework, but offers the advantage of reusing its existing implementation. In addition, I can add a Widget directly to an Fl_Window. On the other hand, should you decide to use this implementation with a different graphical framework, then some major redesign is needed.
Let's have a closer look into the classes Generic_window, Simple_window, Widget and Shape. Compared to [1], there are of course similarities, but also differences, since the implementation is not exactly the same.
The Generic_window class
The Generic_window class displays a window on the screen and provides attach() methods for attaching Widgets to a window. Its full class declaration is shown here:
class Generic_window : public Fl_Window
{
public:
// constructor
Generic_window(Point tl, int w, int h, const char *l);
// virtual destructor
virtual ~Generic_window() { clear(); }
// attach to window
virtual void attach(Fl_Widget& w) { add(w); }
virtual void attach(Widget& w);
// remove from window
virtual void detach(Fl_Widget& w) { remove(w); }
// set title of the window
void set_title(const string& t) { label(t.c_str()); }
// put on top a widget
void put_on_top(Fl_Widget& w) { add(w); }
// callback
void exit_callback(Fl_Widget *w) { exit(0); }
static void static_exit_callback(Fl_Widget *w, void *win) {
static_cast<Generic_window *>(win)->exit_callback(w);
}
// override show
void show();
// wait for window close
void wait_for_exit();
// maximum x and y
int x_max() { return w(); }
int y_max() { return h(); }
};
The constructor enables manual addition of widgets and registers the exit callback when the window is closed:
// constructor
Generic_window::Generic_window(Point tl, int w, int h, const char *l = nullptr) : Fl_Window{tl.x,tl.y,w,h,l}
{
// manual addition of widgets
Fl_Group::current(NULL);
// exits when window is closed
this->callback(static_exit_callback,this);
}
The exit callback just calls exit(0) and it is implemented as recommended in chapter 5.9 of [2]:
// callback
void exit_callback(Fl_Widget *w) { exit(0); }
static void static_exit_callback(Fl_Widget *w, void *win) {
static_cast<Generic_window *>(win)->exit_callback(w);
}
There are two attach() methods available, one for FLTK Fl_Widget objects and one for our Widget objects. For an Fl_Widget instance the only thing to do is call the add() method (inherited from Fl_Group as shown in chapter 31.153 of [2]):
virtual void attach(Fl_Widget& w) { add(w); }
For a Widget object, since a Widget is an Fl_Widget, it can be added directly to the window with add():
// attach widget to the window
void Generic_window::attach(Widget& w)
{
add(&w); // add widget to the window
w.attach(this); // attach the window to the widget
}
But in turn, the window is also attached to the widget, in order to allow the latter to add to the window any other widget it is composed of. This is required because of how the underlying FLTK framework draws widgets: it calls one by one the draw() method of all the widgets added to the window. That's why a widget needs to add itself and any other widgets it is made of, otherwise the framework will miss to call all the draw() methods and the widget will not be shown correctly.
The operation that causes something to be shown on the screen is show():
// override show
void Generic_window::show()
{
Fl_Window::show();
wait_for_expose();
Fl::wait(0);
}
By calling Fl_Window::show() the underlying FLTK framework will show the window on the screen. As explained in chapter 31.153.3.45 of [2], in order to be sure that the window gets displayed, we wait by calling the function wait_for_expose(), and finally trigger the event loop once by calling Fl::wait(0), which makes sure that everything gets drawn inside the window, causing all draw() methods to be called (see chapter 31.2.3.49 of [2]).
In order to enter the FLTK main control loop (see chapter 4.1.7 of [2]), we need to call wait_for_exit():
// wait for window close
void Generic_window::wait_for_exit()
{
redraw();
show();
while (1) Fl::wait();
}
This makes sure that everything gets drawn before entering the main control loop, where FLTK processes all incoming events (like movements of the mouse or button clicks) and waits for the window to be closed, terminating the application.
As an example, the following lines of code:
Generic_window win(Point{100,100},640,480,"Generic_window");
win.wait_for_exit();
displays the following (empty) window on the screen:
The Simple_window class
The Simple_window class specialises Generic_window by showing a Next button at the top-right corner. Its full class declaration is shown here:
//
// Simple window
//
class Simple_window : public Generic_window
{
public:
// constructor
Simple_window(Point tl, int w, int h, const char *l);
// virtual destructor
virtual ~Simple_window() { delete b; }
// override of attach
void attach(Fl_Widget& w);
void attach(Widget& w);
// callbacks
void next_callback(Fl_Widget *w) { button_pressed = true; }
static void static_next_callback(Fl_Widget *w, void *win) {
static_cast<Simple_window *>(win)->next_callback(w);
}
// helper methods
void wait_for_button();
bool is_button_pressed();
private:
Fl_Button* b; // the Next button that appears top right
bool button_pressed = false;
};
In addition to the Generic_window constructor invocation, the constructor allocates an Fl_Button and adds it to the window:
// constructor
Simple_window::Simple_window(Point tl, int w, int h, const char *l = nullptr) : Generic_window{tl,w,h,l}
{
b = new Fl_Button(w-100,0,100,50,"Next");
b->type(FL_NORMAL_BUTTON);
b->when(FL_WHEN_RELEASE);
b->callback(static_next_callback,this);
add(*b);
}
The button is also in charge of calling the next_callback() when the Next button is released:
// callbacks
void next_callback(Fl_Widget *w) { button_pressed = true; }
static void static_next_callback(Fl_Widget *w, void *win) {
static_cast<Simple_window *>(win)->next_callback(w);
}
The callback just sets the private flag button_pressed to true, whose usage will be described shortly.
The attach() methods are overridden by Simple_window as follows:
// override of Generic_window::attach
void Simple_window::attach(Fl_Widget& w)
{
Generic_window::attach(w);
// keeps the button in the foreground all the time!
add(*b);
}
void Simple_window::attach(Widget& w)
{
Generic_window::attach(w);
// keeps the button in the foreground all the time!
add(*b);
}
Notice how the parent Generic_window::attach() method is invoked for actually attaching the widget to the window, followed by the addition of the button. By adding every time the button, we make sure that it stays in the foreground all the time, being the last widget to be drawn inside the window and on the screen.
In order to enter the FLTK main control loop and leave it without terminating the application, the Simple_window class allows us to call wait_for_button(), which shows the window by drawing all the attached widgets and waits for the user to press the Next button before continuing.
// helper methods
void Simple_window::wait_for_button()
{
redraw();
show();
while( !is_button_pressed() ) Fl::wait();
}
bool Simple_window::is_button_pressed()
{
if (button_pressed)
{
button_pressed = false;
return true;
}
else return false;
}
Once the program has entered the main control loop, the execution is stuck inside the while loop of wait_for_button(), but when the button is pressed and released the flag button_pressed is set to true, which causes the program to leave the while loop and stop calling Fl::wait(), continuing normal program execution.
As an example, the following lines of code:
Simple_window win(Point{100,100},640,480,"Simple_window");
win.wait_for_button();
displays the following (again empty) window on the screen, where the Next button can be seen.
The abstract Widget class
The abstract Widget class extends Fl_Widget by managing its own widget position and size, its transparency, and a pointer win to the Generic_Window to which the widget is attached. Its full class declaration is shown here:
//
// Widget
//
class Widget : public Fl_Widget
{
public:
// no copy constructor allowed
Widget(const Widget&) = delete;
// no copy assignment allowed
Widget& operator=(const Widget&) = delete;
// virtual destructor
virtual ~Widget() {}
// virtual draw() method to be provided by
// the derived class, overrides Fl_Widget::draw
virtual void draw() = 0;
// virtual move() method to be provided by
// the derived class
virtual void move(int dx, int dy) = 0;
virtual void move(Point p) { move(p.x-get_tl().x,p.y-get_tl().y ); }
// overrides Fl_Widget::redraw()
void redraw();
// attach internal FLTK widgets to the window
virtual void attach(Generic_window* w) { win = w; }
// getter and setter methods
Generic_window* get_win() const { return win; }
void set_transparency(Transparency_type t) {set_transparency_widget(t);}
Transparency_type get_transparency() const { return trans; }
int get_w() const {return (br.x-tl.x); }
int get_h() const {return (br.y-tl.y); }
Point get_tl() const { return tl; }
Point get_br() const { return br; }
// helper methods
bool is_visible() { return (trans == Transparency_type::visible); }
protected:
// Widget is an abstract class, no instances of Widget can be created!
Widget() : Fl_Widget(0,0,0,0) {}
// resize methods to be overridden by derived classes
virtual void resize_widget(Point a, Point b);
virtual void resize_widget(Point p, int w, int h);
// call Fl_Widget::resize
virtual void resize_widget() { resize(tl.x,tl.y,br.x-tl.x,br.y-tl.y); }
// protected setter methods
void set_transparency_widget(Transparency_type t) { trans = t; }
void set_tl(Point p) { tl = p; }
void set_br(Point p) { br = p; }
// other helper methods
// update top-left and bottom-right corners
void update_tl_br(const Point& p);
private:
Transparency_type trans{Transparency_type::visible};
Point tl{}; // top-left corner
Point br{}; // bottom-right corner
Generic_window *win; // pointer to the containing window
};
The Widget class has to be used as a parent class for creating other widget classes. For example, a menu is composed of several Button widgets, so the idea here is to create a new subclass Menu of Widget forming a composition of such Buttons, where each Button is added to the parent window by using the win pointer. The virtual methods attach(), draw() and move() have to be overridden for attaching, drawing and moving the menu, taking care of attaching, drawing and moving each button.
It is an abstract class because the draw() and move() methods are declared as pure virtual functions, meaning that derived classes must provide an implementation for them:
// virtual draw() method to be provided by
// the derived class, overrides Fl_Widget::draw
virtual void draw() = 0;
// virtual move() method to be provided by
// the derived class
virtual void move(int dx, int dy) = 0;
The attach() method only sets the win pointer to the window to which the widget is attached:
// attach internal FLTK widgets to the window
virtual void attach(Generic_window* w) { win = w; }
Note that since the protected Widget constructor calls the Fl_Widget constructor with position and size all set to 0:
Widget() : Fl_Widget(0,0,0,0) {}
any subclass must properly resize the widget as soon as the position and size information are available or whenever they get updated. Failing to do so will have the widget not shown when draw() is called. For this purpose Widget provides the following protected virtual functions:
// resize methods
void Widget::resize_widget(Point a, Point b)
{
set_tl(a);
set_br(b);
resize(tl.x,tl.y,br.x-tl.x,br.y-tl.y);
}
void Widget::resize_widget(Point p, int w, int h)
{
set_tl(p);
set_br(Point{p.x+w,p.y+h});
resize(p.x,p.y,w,h);
}
which set the top-left tl and bottom-right br corners and call the Fl_Widget::resize() method accordingly. Notice how the top-left tl and bottom-right br corners define the area of a rectangle that covers the area occupied by the widget.
Another useful method is update_tl_br(), which given a point p belonging to the widget, will updated the top-left tl and bottom-right br corners as required, so that it can be resized correctly.
// other helper methods
// update top-left and bottom-right corners
void Widget:: update_tl_br(const Point& p)
{
if (p.x < tl.x) tl.x = p.x;
else if (p.x > br.x) br.x = p.x;
if (p.y < tl.y) tl.y = p.y;
else if (p.y > br.y) br.y = p.y;
}
The abstract Shape class
The abstract Shape class extends Widget for drawing shapes such as a lines, rectangles, circles, or even more complex ones like functions. It manages also attributes like line style and colour. Its full class declaration is shown here:
//
// Shape
//
class Shape : public Widget
{
public:
// no copy constructor allowed
Shape(const Shape&) = delete;
// no copy assignment allowed
Shape& operator=(const Shape&) = delete;
// virtual destructor
virtual ~Shape() {}
// overrides Fl_Widget::draw()
void draw();
// moves a shape relative to the current
// top-left corner (call of redraw()
// might be needed)
void move(int dx, int dy);
// setter and getter methods for
// color, style, font, transparency
// (call of redraw() might be needed)
void set_color(Color_type c);
void set_color(int c);
Color_type get_color() const { return to_color_type(new_color); }
void set_style(Style_type s, int w);
Style_type get_style() const { return to_style_type(line_style); }
void set_font(Font_type f, int s);
protected:
// Shape is an abstract class, no instances of Shape can be created!
Shape() : Widget() {}
// protected virtual methods to be overridden
// by derived classes
virtual void draw_shape() = 0;
virtual void move_shape(int dx, int dy) = 0;
// protected setter methods
virtual void set_color_shape(Color_type c) {
new_color = to_fl_color(c);
}
virtual void set_color_shape(int c) {
new_color = to_fl_color(c);
}
virtual void set_style_shape(Style_type s, int w);
virtual void set_font_shape(Font_type f, int s);
// helper methods for FLTK style and font
void set_fl_style();
void restore_fl_style();
void set_fl_font();
void restore_fl_font() { fl_font(old_font,old_fontsize); }
// test method for checking resize calls
void draw_outline();
private:
Fl_Color new_color{Fl_Color()}; // color
Fl_Color old_color{Fl_Color()}; // old color
Fl_Font new_font{0}; // font
Fl_Font old_font{0}; // old font
Fl_Fontsize new_fontsize{0}; // font size
Fl_Fontsize old_fontsize{0}; // old font size
int line_style{0}; // line style
int line_width{0}; // line width
};
Different line styles and colours can be set when drawing the shape. Any subclass of Shape must draw and move the actual shape by overriding the protected methods draw_shape() and move_shape() :
// overrides Widget::draw()
void Shape::draw()
{
set_fl_style();
if ( is_visible() ) draw_shape();
restore_fl_style();
}
// moves a shape relative to the current
// top-left corner (call of redraw()
// might be needed)
void Shape::move(int dx, int dy)
{
hide();
move_shape(dx,dy);
show();
}
As shown later, the actual drawing is done inside draw_shape() by invoking the underlying FLTK drawing functions such as fl_line(), fl_rect() or fl_arc().
The remaining methods of Shape deal mainly with setting or restoring style and colour attributes, converting each FLTK attribute to its own enumeration type Style_type and Colour_type and vice versa.
We will see in the implementation examples, that resizing is an important operation for shapes, which might be required each time a point is added. Depending on the type of the shape, determining the exact size might be more or less complex. By overriding the resize_widget() methods, a shape can implement how the size shall be determined each time.
Implementation examples
The Button widget
The example code below
// example usage of a button
void buttons()
{
Simple_window win(Point{100,100},640,480,"Click me button");
Button button{Point{100,100},100,50,"click me",
[](Fl_Widget* w, void* a){},nullptr};
button.set_button(Button_type::normal);
button.set_when(When_type::release);
win.attach(button);
win.wait_for_button();
}
produces a window to appear on the screen with a "click me" button:
Let"s go through the code to see how it works.
The line
Simple_window win(Point{100,100},640,480,"Click me button");
instantiates a Simple_window at position (100,100) and size 640x480 pixels having the title "Click me button".
The button is instantiated next
Button button{Point{100,100},100,50,"click me",[](Fl_Widget* w, void* a){},nullptr};
We want the button to appear at position (100,100) inside the window and having size 100x50 pixels. The string on the button shall be "click me". The callback to be registered is an empty lambda function, i.e. nothing happens when the button is pressed and released. The button shall be a normal one and the empty callback shall be called when the button is released.
Finally, we attach() the button to the Simple_window and we call its wait_for_button() method, which triggers the window to be shown on the screen, and stops execution waiting for the user to press the Next button or close the window.
At this point, the user can press the button "click me" as many times as desired and nothing will happen (because of the empty callback). Not really a useful application!
Let's have a closer look at how Button is implemented:
//
// Button
//
class Button : public Widget
{
public:
// constructor
Button(Point p, int w, int h, const string s, Callback_type cb, void* d);
// virtual destructor
virtual ~Button() { if (b) delete b; }
// show button
void show() { set_transparency(Transparency_type::visible); }
// hide button
void hide() { set_transparency(Transparency_type::invisible); }
// attach internal button
void attach(Generic_window* w) { Widget::attach(w); w->add(*b); }
// overridden member methods
void draw();
void move(int dx, int dy);
// setter and getter functions
void set_when(When_type w);
When_type get_when() const { return to_when_type(fl_when); }
void set_button(Button_type b);
Button_type get_button() const { return to_button_type(fl_button); }
void set_label(string s) { text = s; }
string get_label() const { return text; }
private:
Fl_Button *b{nullptr}; // pointer to FLTK button
Fl_When fl_when{FL_WHEN_RELEASE}; // when the callback is to be called
int fl_button{FL_NORMAL_BUTTON}; // button type
string text; // text to display on the button
void *data; // pointer data to the button callback
};
As can be seen from the private attributes, Button is essentially a wrapper for the FLTK Fl_Button widget, collecting together all the other required attributes such as fl_when, i.e. when the callback shall be called, fl_button, i.e. what type of button shall be shown, and text, i.e. what string shall be shown on the button.
The pointer b to the FLTK button is allocated inside the constructor:
// constructor
Button::Button(Point p,int w,int h,const string s,Callback_type cb,void *d)
{
text = s;
data = d;
b = new Fl_Button(p.x,p.y,w,h,text.c_str());
if (b)
{
b->callback(cb,data);
b->type(fl_button);
b->when(fl_when);
resize_widget(p,w,h);
}
else throw runtime_error("Button::Button(): Button allocation failed!");
}
and deallocated in the virtual destructor:
// virtual destructor
virtual ~Button() { if (b) delete b; }
The constructor requires the top-left corner to be provided as a Point p, together with the width w and height h in pixels. The text to be shown on the button is provided by the string parameter s. Finally, cb and d represent the callback to be called when the button is pressed and the data parameter to be provided to such callback.
Once the attributes are updated, the constructor tries to allocate the Fl_Button. If it succeeds, it will continue by registering the callback cb, setting the button and invocation types. Next, as already pointed out previously, the constructor needs to resize the widget, otherwise it will not be shown. This is done by invoking the resize_widget() method. Since the position and size are provided already to the constructor, there is no need to override the existing Widget::resize_widget() method and the existing one can simply be called with the provided parameters.
The attach() method is a short one but does something important.
// attach internal button
void attach(Generic_window* w) { Widget::attach(w); w->add(*b); }
As already described before, it attaches the window to Button and it allows us in turn to add the Fl_Button pointed by b to the window. If we miss this step, no button will be shown on the screen.
Finally, we need to have a look at draw():
// overridden member methods
void Button::draw()
{
b->type(fl_button);
b->when(fl_when);
b->label(text.c_str());
if (is_visible())
b->show();
else
b->hide();
}
Remember that widgets belonging to a window are drawn when the underlying FLTK framework calls the draw() method of all widgets added to it. When a Button is attached to a window, it adds first itself and then the allocated Fl_Button pointed by b. This means that the framework will first call the method Button::draw() above, which in turn sets all the desired attributes of b (but no real drawing is happening yet), and then calls draw() on b which will finally draw the button with the desired attributes.
The Open_polyline Shape
The example code below:
// example usage of the polyline classes:
// Open_polyline, Closed_polyline, Polygon
void polylines()
{
Simple_window win(Point{100,100},640,480,"Polylines and polygon");
Open_polyline open_poly = { Point{100,100}, Point{200,100},
Point{150,50}, Point{150,150} };
open_poly.set_color(Color_type::magenta);
open_poly.set_style(Style_type::solid, 4);
win.attach(open_poly);
Closed_polyline closed_poly = { Point{300,300}, Point{400,300}, Point{350,250}, Point{350,350} };
closed_poly.set_color(Color_type::green);
closed_poly.set_style(Style_type::solid, 4);
win.attach(closed_poly);
Polygon poly = { Point{200,150},Point{250,25},
Point{250,300}, Point{150,350} };
poly.set_color(Color_type::yellow);
poly.set_style(Style_type::solid, 4);
win.attach(poly);
win.wait_for_button();
}
produces a window to appear on the screen with several type of shapes such as an open polyline, a closed polyline and a polygon:
Notice how the shapes are initialised by an initialiser list of Points:
Open_polyline open_poly = { Point{100,100}, Point{200,100},
Point{150,50}, Point{150,150} };
It should be straightforward to understand how the rest of the code above works and hence we will focus now on the implementation of the Open_polyline class.
Let's have a closer look at how an Open_polyline is implemented:
//
// Open_polyline
//
class Open_polyline : public Shape
{
public:
// constructors
Open_polyline() {}
Open_polyline(initializer_list<Point> lst) {
for (auto pnt : lst) add_point(pnt);
}
// virtual destructor
virtual ~Open_polyline() { for (auto pnt: vp) delete pnt; }
// add a new point
void add_point(Point p);
// remove the i-th point
void remove_point(size_t i);
// getter and setter methods
Point get_point(size_t i) const { return *vp.at(i); }
void set_point(size_t i, Point pnt);
// helper methods
size_t get_nb_points() const { return vp.size(); }
bool empty_points() const { return vp.empty(); }
protected:
// overridden member methods
void draw_shape();
void move_shape(int dx,int dy);
void resize_widget();
private:
vector<Point*> vp; // vector of points to be connected
};
An open poly line is represented internally as a vector of points to be connected:
vector<Point*> vp; // vector of points to be connected
If we don't provide an initialiser list to the constructor, the user needs to add each point one at a time by means of the add_point() method:
// add a new point
void Open_polyline::add_point(Point p)
{
vp.push_back(new Point{p});
// after any update of the vector of points
// resize must be called
resize_widget();
}
add_point() simply allocates a new point and adds it to the internal vector vp, calling resize_widget() before leaving the function, since with the addition of new points the overall position and size of the widget might change.
If we provide an initialiser list to the constructor, the constructor will add each point to the polyline for us:
Open_polyline(initializer_list<Point> lst) {
for (auto pnt : lst) add_point(pnt);
}
The resize_widget() method takes advantage of Widget::update_tl_br() by simply looping over all points stored in the internal representation and calling update_tl_br() on each of them. This will update the corners tl and br until all points of the polyline are covered by the rectangle defined by tl and br.
void Open_polyline::resize_widget()
{
// nothing to do if empty vector of points
if (vp.empty())
return;
// depending on where the points are, the
// overall size of the widget must be updated
set_tl(*vp[0]);
set_br(*vp[0]);
for (auto p : vp)
update_tl_br(*p);
Widget::resize_widget();
}
Finally, in order to draw the open polyline we need to connect the points together as shown below:
void Open_polyline::draw_shape()
{
// connect each consecutive points
for (size_t n=0; n < vp.size()-1; n++)
fl_line(vp[n]->x, vp[n]->y, vp[n+1]->x, vp[n+1]->y);
}
fl_line() belongs to FLTK drawing functions and it is used above in order to draw the connecting line from the n-th to the (n+1)-th point, i.e. between consecutive points stored in vp.
Putting it all together
As done in [1], let's consider a final example putting together several widgets for implementing a simple drawing application. Here is the code:
// example of an application
void lineswindow()
{
Lines_window win{Point{100,100},640,480,"lines"};
//win.wait_for_exit();
win.wait_for_button();
}
When run, an instance win of the class Lines_window is created and the FLTK main control loop is entered, displaying the following window on the screen:
In the input fields "next x" and "next y" we can insert the coordinates in pixels of a point that we wish to add. By pressing the "Add point" button, the point will be added and a line will appear connecting it to the previous point (except for the very first point). Each time that a point is added, its coordinates are also shown in the output field "current (x,y)". Here is an example when adding the points (200,200), (400,300), (100,400):
By pressing the "Menu color" button, a new menu of buttons appears allowing us to change the colour of the polyline:
By pressing the "Red" button, the colour of the polyline changes to red and the "Menu color" button appears again:
This is how the class Lines_window is declared:
// Class definition for a little demo application
class Lines_window : public Simple_window
{
public:
// constructor
Lines_window(Point xy,int w,int h,const char* title);
// virtual destructor
virtual ~Lines_window() { color_menu.destroy(); }
private:
Open_polyline lines; // the polyline
Button add_button; // button for adding a point to the polyline
Button menu_button; // button for revealing the color buttons
Menu color_menu; // menu of the color buttons
In_box next_x; // input box for reading the abscissa
In_box next_y; // input box for reading the ordinate
Out_box xy_out; // output box for showing the point added
void add_pressed(); // callback for the add button
void menu_pressed(); // callback for the menu button
void red_pressed(); // callback for the red color button
void blue_pressed(); // callback for the blue color button
void green_pressed(); // callback for the green color button
};
The class is essentially a wrapper of all the elements that have to be shown in the window and of the corresponding callbacks. The constructor takes care of properly initialising the widgets in its initialiser list:
// constructor
Lines_window::Lines_window(Point xy,int w,int h,const char* title) :
Simple_window(xy, w, h, title),
add_button{Point{x_max()-250,0},100,50,"Add point",
[](Fl_Widget* w, void* lw) {static_cast<Lines_window*>(lw)->add_pressed();},this},
menu_button{Point{x_max()-250,100},100,50,"Menu color",
[](Fl_Widget* w, void* lw) {static_cast<Lines_window*>(lw)->menu_pressed();},this},
color_menu(Point{x_max()-250,100},Menu_type::vertical),
next_x{Point{100,0},50,20,"next x:"},
next_y{Point{250,0},50,20,"next y:"},
xy_out{Point{100,50},100,20,"current (x,y):"}
{
// add buttons to the menu
color_menu.add_button(new Button{Point{0,0},100,50,"Red",[](Fl_Widget* w, void* lw) {static_cast<Lines_window*>(lw)->red_pressed();},this});
color_menu.add_button(new Button{Point{0,0},100,50,"Blue",[](Fl_Widget* w, void* lw) {static_cast<Lines_window*>(lw)->blue_pressed();},this});
color_menu.add_button(new Button{Point{0,0},100,50,"Green",[](Fl_Widget* w, void* lw) {static_cast<Lines_window*>(lw)->green_pressed();},this});
// hide the color menu
color_menu.hide();
// attach all the widgets to the window
attach(add_button);
attach(menu_button);
attach(color_menu);
attach(next_x);
attach(next_y);
attach(xy_out);
attach(lines);
}
In the body, the remaining 3 buttons for changing the colour are allocated. Then the colour menu is hidden and all the widgets are attached to the window. By hiding color_menu, we avoid that it gets shown immediately, because we must show it only when the "Menu color" button is pressed.
When the FLTK main control loop is entered, the framework waits for any event to happen, such as a click on a button or a value entered inside an input field.
The input widgets next_x and next_y collect any value entered in their corresponding input fields as soon as a digit is entered. When the "Add point" button is pressed, the button callback does the rest:
// callback called when the add button is pressed
void Lines_window::add_pressed()
{
int x = next_x.get_input_integer();
int y = next_y.get_input_integer();
lines.add_point(Point{x,y});
xy_out.set_output_text(string{"("+to_string(x)+","+
to_string(y)+")"});
redraw();
}
It collects the values from the input widgets, creates a Point and adds it to the polyline lines, sets the value of the output widget xy_out, and triggers a redraw, causing the polyline to be drawn with the added point and line.
If we want to change the polyline colour, we have to trigger the menu button callback:
// callback called when the menu button is pressed
void Lines_window::menu_pressed()
{
// hide the menu button and show now the color buttons
menu_button.hide();
color_menu.show();
redraw();
}
It simply hides the menu button and shows the color_menu one, triggering a final redraw of the window.
By pressing a colour button we trigger its corresponding callback, as shown here for the red button:
// callback called when the red button is pressed
void Lines_window::red_pressed()
{
// change the polyline color to red
lines.set_color(Color_type::red);
// hide the color buttons and show the menu button
color_menu.hide();
menu_button.show();
redraw();
}
The callback simply sets the colour of the polyline to red, hides the colour menu and shows the menu button again.
Improvements
There is of course a lot of room for improvement and probably there are also many inconsistencies that should be fixed.
One improvement that I would like to mention concerns the Widget class. Actually, the Widget class kind of duplicates functionalities already provided by Fl_Widget when it comes to managing the position and size. Those functionalities could be removed and the Widget and Shape class could be merged together, instead of keeping them separate.
References
[1] Bjarne Stroustrup. Programming: Principles and Practice Using C++, 2nd edition. Addison Wesley, 2015
[2] FLTK 1.3.8 Programming Manual
Comments