6.11 Building your own Graphic Library based on FLTK - naver/lispe GitHub Wiki

Graphic Library

Version française

Adding a graphics library is an important element in the creation of a new language. This is where the natural artistic talent of any self-respecting computer scientist can finally find its consecration. Alas, since LispE is written in C++, we are facing a big issue:

C++ does not offer any native graphic library...

I already hear some people say, "that's normal, it's a low level language". Except that Java or Python offer their own graphics libraries and it's not too much of a stretch to imagine that C++ could also have its own solution.

Some people have decided to remedy this problem and there are now a number of graphical libraries in C++ that you can integrate into your code.

Our choice fell on FLTK.

FLTK

FLTK offers one of the simplest and richest APIs, in our opinion, to integrate a graphical engine within your code.

A window is created in three lines of code:

  //We create our window
  Fl_Window win(500, 500, "Hello to the World!");
  //We display it
  win.show();
  
  //Launch the event loop
  return Fl::run();

Adding graphic objects within this window is just as simple:

/**********************************************************************************************************************************/
static void recall(Fl_Widget *w, void *uData) {
  printf( The button has been pressed );
}

/**********************************************************************************************************************************/
int main() {
  //We create a window
  Fl_Window win(500, 70, "Single button");
  //We create a button
  Fl_Button button(20, 20, 100, 30, "Test button");
  // associated with a recall function
  callback button(recall, (void *)0);
  win.show();
  return Fl::run();
}

In fact, the Fl_Window object works as a parenthesis that is opened, in which all created objects are automatically saved.

Overload

It is also possible to override the methods to draw your own objects.

class MyWindow : public Fl_Window {
public:

  MyWindow(int x, int y, const char* txt): Fl_Window(x,y,txt) {}

  //We overload the draw method so that a red circle is drawn.
  void draw() {
    fl_color(FL_RED);
    fl_circle(100,100,20);
  }
};

int main(int argc, char *argv[]) {
   MyWindow wnd(100,100, "Test);
   wnd.show();
   Fl::run();
}

API

As you can see, the API is extremely simple and above all it is universal. FLTK is available on Windows, Mac OS and Linux. Moreover, on Linux platforms, the library is often pre-installed.

The API is rather rich in basic objects. There are all kinds of buttons, sliders, input/output classes... You can easily display text, images within windows, draw curves, choose colors.

Everything is derived from the base class: Fl_Widget...

A quick glance at the documentation will show you how rich this library is...

Interfacing

FLTK is not only very easy to use, but it is just as easy to interface with LispE.

LispE already provides a mechanism to create your own dynamic libraries.

We have created three kinds of classes:

Lispe_gui

This is the class that allows you to create graphic objects (windows, buttons, sliders etc.) and to call the different graphic methods for drawing, placing graphic objects or writing text.

Fltk_widget

This is the parent class of all graphical objects manipulated in the library.

Fltk_window

This class is a derivation of Fltk_widget; it manages the display of windows.

Fltk_widget, Fltk_input, Fltk_output_, Fltk_button, Fltk_slider

All these classes are derivations of Fltk_widget. These objects manage buttons and input/output areas. Note that we have not yet integrated all the graphical objects offered by FLTK. Enriching the GUI library will simply require another derivation of that class.

Doublewindow

It is a derivation of Fl_Double_Window, a class specific to FLTK whose draw method will be overloaded in order to execute LispE functions.

Callback functions

In fact, nothing is easier than passing a callback function in LispE. Just provide the atom that corresponds to the name of the function.

Operation

When loading the GUI library, we associate a function name with an object of type Lispe_gui which receives as argument a unique identifier whose list is in: typedef enum {...} fltk_action

The complete list of all exported functions can be found here

Here is an example of such a function:

lisp->extension( deflib fltk_create (x y w h label (function) (object)), new Lispe_gui(lisp, fltk_gui, fltk_widget, fltk_create));

Here we create a function: fltk_create which takes the following list of arguments: (x y w h label (function) (object)), which we associate with a Lispe_gui object to which we provide the fltk_create identifier, defined in fltk_action.

The other arguments: fltk_gui and fltk_widget are identifiers that define respectively the type (in the LispE sense) of a Lisp_gui object and the type of a Fltk_widget object.

This class includes its own derivation of: eval.

Element* Lispe_gui::eval(LispE* lisp) {
    try {
        switch (action) {
            case fltk_create: ...
            case fltk_run: ...
            case fltk_resize: ...
            case fltk_close: ...
            case fltk_end: ...

Thus, each function is associated with a unique instance of an object of type Lispe_gui whose evaluation will allow the execution of the instructions we are interested in.

Some of these instructions such as fltk_button or fltk_slider return a LispE object of type Fltk_button or Fltk_slider, each of which encapsulates a pointer to a FLTK object. Thus, functions that take an argument of this type will be able to communicate with the FLTK graphic library.

Finally, we have added a callback function: close_callback, which is executed when a window closes, so that we can clean the memory of all the graphic objects created until then.

Writing your own code

The use of this library in LispE is as simple as the C++ version presented above.

Here is how you create a window in a few lines of LispE:

; First we load our graphic library
(use 'gui)

; We create our window and store the result in 'wnd'
(setq wnd (fltk_create_resizable 100 100 "Test"))

; The window can be resized from a minimum size of 100x100 to 500x500.
(fltk_resize wnd 100 100 500 500)

; we finalize and display our window
(fltk_end wnd)

; Then we launch our event loop
(fltk_run wnd)

With a Button

Creating a button is just as simple.

You create a window, then you associate a button to it. The name of a function is integrated into the constructor of the button and that's it.

Note that the recall function: pushed requires two parameters:

  • the first parameter corresponds to the graphic object that has been activated.
  • the second parameter is the object (optional) supplied to the button's constructor .
(use 'gui)

; our callback function
; b therefore corresponds to the button
; o corresponds to the string: "my data" given as argument to the constructor below.

(defun pushed(b o)
   (printerrln "Pressed" (fltk_label b) o)
)

; We create a window of fixed size
(setq wnd (fltk_create 100 100 1000 1000 "Test"))

; Our button is associated with the 'pushed' function.
(setq button (fltk_button wnd 50 50 100 100 "ok" 'pushed "my data"))

; We finalize
(fltk_end wnd)

; Launch the event loop
(fltk_run wnd)

Drawing stuff

We can obviously draw within the window, to do so we just need to associate a callback function when defining our window.

We can also associate a timer to it, which allows us to recall this function at a fixed time.

In this case, the callback function must necessarily return a numerical value (which may be different from the initial value) for the timer to be reset again.

In other words, this timer forces the system to redraw the window every part of a second, allowing simple on-screen animations.


(use 'gui)

; These two macros allow you to increment locally. 
; in a list of numerical values
(defmacro ++(x i inc) (at x i (+ (at x i) inc))
(defmacro --(x i inc) (at x i (- (at x i) inc))

; First we create our drawing window...
; Important we give it as arguments: a callback function: draw_me and a list of coordinates...
(setq wnd (fltk_create 100 100 1000 1000 "Circle" 'draw_me '(30.0 40.0 20.0))))

; we finalize the creation of the window while providing a timer of 0.01s
(fltk_end wnd 0.01)

(setq direction true)

; o corresponds to the object provided to fltk_create
(defun draw_me(w o)
   ; We draw our two circles
   (fltk_circle w (at o 0) (at o 1) (at o 2) FL_BLUE)
   (fltk_circle w (at o 1) (at o 0) (at o 2) FL_RED)
   (check direction
      (++ o 0 1)
      (++ o 1 1)
      (++ o 2 1)
      (if (> (at o 2) 500)
         (setg direction nil)
      )
      ; we return a value that corresponds to the timer
      (return 0.01)
   )
   (-- o 0 1)
   (-- o 1 1)
   (-- o 2 1)
   (if (< (at o 2) 10)
      (setg direction true)
   )

    ; we return a value that corresponds to the timer
   (return 0.01)
)

; We launch our events loop
(fltk_run wnd)


The plot function

This function is perhaps the most powerful of the lot. It takes a list of coordinates, where each coordinate is a list of two items: (x,y), and it calculates a landmark so that each point on this list can be drawn on the screen. It returns a list containing all the information corresponding to this landmark:

XminWindow: Minimum width of the window
YminWindow: Minimum window height
XmaxWindow: Maximum width of the window
YmaxWindow: Maximum window height
XminValue: Minimum value of X in the points list.
YminValue: Minimum value of Y in the points list.
XmaxValue: Maximum value of X in the point list.
YmaxValue: Maximum value of Y in the point list.
incX: X drawing step
incY: Y drawing step
thickness: the thickness chosen to draw

The function itself has the following signature:

(deflib fltk_plot (window points thickness (landmark))

If the thickness is 0, then the points are all connected by lines between them. Otherwise, thickness corresponds to the radius of a point shown on the screen.

This function returns a landmark which can then be provided at successive calls of plot so that all curves can be drawn in the same plane.

Here is an example to draw a rosette:

; Macros to increment a value in a list at a given position
(defmacro ++(x i inc) (at x i (+ (at x i) inc))
(defmacro --(x i inc) (at x i (- (at x i) inc))

(use 'gui)

; maximum dimension of the curve
(setq maxx 100)
(setq maxy 900)
(setq direction true)

(setq incb 0)

; callback function for the slider
(defun slide(s)
   (setg incb (fltk_value s))
)

; -------------------- Window Creation ----------------------------
; We create a window
(setq wnd (fltk_create 100 100 1000 1000 "Polar" 'drawing '(0.0 0.0))))

; We add a slider
(setq incb_slider (fltk_slider wnd 30 900 200 20 "incb" FL_HOR_SLIDER true 'slide))

; whose value limits are between -100 and 100
(fltk_boundaries incb_slider -100 100)

; The window will be redrawn every 0.01s
(fltk_end wnd 0.01)

; --------------------------------------------------------------------

; initial values included between 0 and 2π in steps of 0.005
(setq values (range 0 (* 2 _pi) 0.005))

; We calculate our position using a simple equation 
(defun equation(o a)
   (+ (cos (* o (at a 0))) (at a 1))
)

; callback function for drawing
(defun drawing(w o)
   ; the incb value is stored in o
   ; at position 1: o[1] = incb
   (at o 1 incb)

   ; depending on the direction, one increments
   ; or one decrements o[0]
   (if direction
      (block
         (++ o 0 2)
         (if (> (at o 0) maxx)
            (setg direction nil)
         )
      )
      (block
         (-- o 0 2)
         (if (< (at o 0) 3)
            (setg direction true)
         )
      )
   )

   (setq coords ())
   
   ; we create our polar coordinates using the equation above
   (loop x values
      (setq v (equation x o))
      (push coords
         (list
            (* (cos x) v)
            (* (sin x) v)
         )
      )
   )

   (fltk_drawcolor w FL_RED)
   ; We then draw our curve by connecting each point by a line
   (fltk_plot w coords 0)

   ; the function is called every 0.01s
   (return 0.01)
)

(fltk_run wnd)

Conclusion

The list of classes and graphical objects available in FLTK is far from being completely integrated, but we are working on it. You can find a complete description here

Addendum

The binaries we provide for Windows and Mac OS already contain a compiled version of this library.

If you need to compile the LispE graphics library on a different Linux machine, you can refer to the following section: compiling gui