2024/02/15

Fast Light Toolkit (FLTK)

Fast Light Toolkit (FLTK) 是一個跨平台、輕量級的 C++ GUI Toolkit, 支援 OpenGL,可在 UNIX/Linux (X11)、 Microsoft Windows 與 Mac OS X 上使用。 FLTK 授權為修改過後的 LGPL(主要是增加靜態連結的例外部份),預設使用靜態連結,不過動態連結也工作的很好。 FTLK 提供了各種常見的 widgets,如果只是要提供一個並不複雜的使用者界面,那麼 FLTK 是非常好用的工具。 不過在極為複雜的 widgets 部份上有可能找不到使用者所需要的,如果需要建構極為複雜的 GUI 程式, 使用者應該要評估是否採用其它的 GUI Toolkit。

一般來說,跨平台的 GUI Toolkit 有二個策略:

  • 使用一組共同的 API 去包裝各平台所提供的 API,如果該平台沒有提供才自行撰寫補足,使用這個策略的例子為 wxWidgets。
  • 使用底層的繪圖功能畫出一個視窗,再以此開發不同的 widget,使用這個策略的例子為 Qt。

使用第一個策略的優點是擁有良好的 native look and feel(因為大多數使用平台提供的元件)。 而第二個策略優點是較為容易移植與客製化,不過通常需要採用視覺主題提供 native look and feel,否則會看起來不像該平台的程式。 FLTK 採用的是第二個策略,也使得 FLTK 比較容易客製化自己的 widgets。

在工具程式方面, FTLK 提供了二個工具程式,fluid 是一個簡單的 UI designer, fltk-config 可以提供編譯程式時的資訊。

在 openSUSE Tumbleweed 上安裝 FLTK 開發檔案:

sudo zypper in fltk-devel

我也有嘗試在 Linux 上自行編譯 FLTK 1.3.9,採用靜態連結的方式,並且安裝到 /usr/local。

Hello World

下面就是 FLTK 的 Hello World 程式 hello.cxx:

#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Window.H>

int main(int argc, char **argv) {
    Fl_Window *window = new Fl_Window(340, 180);
    Fl_Box *box = new Fl_Box(20, 40, 300, 100, "Hello, World!");
    box->box(FL_UP_BOX);
    box->labelfont(FL_BOLD + FL_ITALIC);
    box->labelsize(36);
    box->labeltype(FL_SHADOW_LABEL);
    window->end();

    window->show(argc, argv);
    return Fl::run();
}

我們可以透過 fltk-config 提供的資訊編譯程式:

g++ hello.cxx -o hello `fltk-config --cxxflags --ldflags`

也可以撰寫一個簡單的 Makefile,如下:

CXX = /usr/bin/g++
CXXFLAGS= -O2 -g -Wall `/usr/local/bin/fltk-config --cxxflags`
LDFLAGS = `/usr/local/bin/fltk-config --ldflags`
PROGRAM = hello
SRCS := $(wildcard *.cxx)
OBJS := $(patsubst %.cxx,%.o,$(SRCS))

RM = rm -f

all: $(PROGRAM)

$(PROGRAM): $(OBJS)
    $(CXX) $(OBJS) -o $@ $(LDFLAGS)

%.o : %.cxx
    $(CXX) $(CXXFLAGS) -c $< -o $@

format: $(SRCS)
    clang-format -i $<

clean:
    $(RM) $(PROGRAM) *.o

.PHONY: all format clean

也可以使用 CMake,下面就是我使用的 CMakeLists.txt:

cmake_minimum_required(VERSION 3.15)

project(hello)

# specify the C++ standard
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

set(FLTK_DIR "/usr/local"
    CACHE FILEPATH "FLTK installation or build directory")

find_package(FLTK REQUIRED)

add_executable(hello WIN32 MACOSX_BUNDLE hello.cxx)
if (APPLE)
  target_link_libraries (hello PRIVATE "-framework cocoa")
endif (APPLE)

target_include_directories (hello PRIVATE ${FLTK_INCLUDE_DIRS})

target_link_libraries (hello PRIVATE fltk)

在這個程式中建立了一個視窗,接下來的所有 widgets 將自動成為該視窗的子視窗。

    Fl_Window *window = new Fl_Window(340, 180);

下面的述句告訴 FLTK 接下來不會在視窗中添加任何 widgets。

window->end();

最後顯示視窗並且進入 FLTK event loop:

window->show(argc, argv);
return Fl::run();

Callback function

在 FLTK 中,處理大多數事件的行為(例如按下按鈕時)的方式是告訴 FLTK 一個在事件發生時應該呼叫的函式,也就是 callback function。 接下來是使用 Fl_Button 的例子。

#include <cstdio>
#include <FL/Fl.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Window.H>

void clicked_cb(Fl_Widget *w, void *user_data) {
    Fl_Button *b = (Fl_Button *)w;
    const char *message = (const char *) user_data;

    printf("button_cb message: %s\n", message);
}


int main(int argc, char **argv) {
    const char *message = "Button Clicked";

    Fl::scheme("gtk+");
    Fl::get_system_colors();

    Fl_Window *window = new Fl_Window(640, 480);
    Fl_Button *b = new Fl_Button(210, 120, 100, 40, "Clicked me");
    b->callback(clicked_cb, (void *)message);

    window->end();

    window->show(argc, argv);
    return Fl::run();
}

接下來是 Lambda expression 作為 callback function 的例子。

#include <cstdlib>
#include <FL/Fl.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Window.H>

int main(int argc, char **argv) {
    Fl::scheme("gtk+");
    Fl::get_system_colors();

    Fl_Window *window = new Fl_Window(640, 480);
    Fl_Button *b = new Fl_Button(210, 120, 80, 40, "Exit");
    b->callback([](Fl_Widget *, void *) { exit(0); });

    window->end();

    window->show(argc, argv);
    return Fl::run();
}

FLTK 可以使用 Fl::scheme() 設定 widget scheme。
Fl::get_system_colors() 會嘗試取得目前系統的前景顏色與背景顏色並且設定為 widget 的預設值。


下面是 Input 的例子,並且使用 Fl::background()Fl::background2() 設定預設的背景顏色, Fl::foreground() 設定預設的前景顏色。

通常只有當 widget 的值發生變化時才會執行 callback。我們可以使用 Fl_Widget::when() 方法改變此設定。

#include <cstdlib>
#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Input.H>
#include <FL/Fl_Window.H>

Fl_Box *text;
Fl_Input *input;
Fl_Window *window;

void input_cb(Fl_Widget *, void *) {
    text->label(input->value());
    window->redraw();
}

int main(int argc, char **argv) {
    Fl::scheme("plastic");

    uchar r = 0, g = 0, b = 0;
    Fl::get_color(FL_WHITE, r, g, b);
    Fl::background(r, g, b);
    Fl::get_color(FL_BLUE, r, g, b);
    Fl::background2(r, g, b);
    Fl::get_color(FL_BLACK, r, g, b);
    Fl::foreground(r, g, b);

    window = new Fl_Window(640, 480, "Label demo");

    input = new Fl_Input(70, 375, 350, 25, "Label:");
    input->static_value("Orange is a cute cat.");
    input->when(FL_WHEN_CHANGED);
    input->callback(input_cb);
    input->tooltip("label text");

    text = new Fl_Box(FL_FRAME_BOX, 120, 75, 300, 100, input->value());
    text->align(FL_ALIGN_CENTER);

    window->end();

    window->show(argc, argv);
    return Fl::run();
}

More widgets

下面是 Fl_Check_Button 的使用例子。

#include <FL/Fl.H>
#include <FL/Fl_Check_Button.H>
#include <FL/Fl_Window.H>

static void Check_CB(Fl_Widget *w, void *data) {
    Fl_Check_Button *check = (Fl_Check_Button *)w;
    Fl_Window *window  = (Fl_Window *)data;

    if (1 == (int)check->value()) {
        window->label("CheckButton");
    } else {
        window->label("");
    }
}

int main(int argc, char **argv) {
    Fl::scheme("gtk+");
    Fl::get_system_colors();

    Fl_Window *window = new Fl_Window(400, 300, "CheckButton");
    Fl_Check_Button *check =  new Fl_Check_Button(30, 30, 120, 60, "CheckButton");
    check->value(1);
    check->callback(Check_CB, window);

    window->end();

    window->show(argc, argv);
    return (Fl::run());
}

下面是 Fl_Input_Choice(下拉式選單)的使用例子。

#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Input_Choice.H>

void choice_cb(Fl_Widget *w, void *userdata) {
    Fl_Input_Choice *choice = (Fl_Input_Choice *)w;
    Fl_Double_Window *win = (Fl_Double_Window *)userdata;

    const Fl_Menu_Item *item = choice->menubutton()->mvalue();

    if (item) {
        // Get the child widget (box)
        win->child(1)->copy_label(item->label());
    }

    win->redraw();
}

int main() {
    Fl_Double_Window *win;
    Fl_Input_Choice *choice;
    Fl_Box *box;

    win = new Fl_Double_Window(400, 300, "Input Choice");

    win->begin();
    choice = new Fl_Input_Choice(10, 10, 100, 40);
    choice->add("openSUSE");
    choice->add("Debian");
    choice->add("Fedora");
    choice->add("Manjaro");
    choice->add("Mint");
    choice->add("Ubuntu");
    choice->add("Zorin");

    box = new Fl_Box(10, 60, 90, 40, "...");
    box->box(FL_FLAT_BOX);
    box->labelfont(FL_BOLD);
    box->labelsize(14);
    box->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);

    choice->callback(choice_cb, win);

    win->end();
    win->show();
    return Fl::run();
}

下面是 Fl_Slider 的使用例子。

#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Slider.H>
#include <string>

void cb_slides(Fl_Widget *w, void *userdata) {
    Fl_Slider *slider = (Fl_Slider *)w;
    Fl_Double_Window *win = (Fl_Double_Window *)userdata;

    // Get the child widget (box)
    win->child(1)->copy_label(std::to_string(slider->value()).c_str());

    win->redraw();
}

int main() {
    Fl_Double_Window *win;
    Fl_Slider *slider;
    Fl_Box *box;

    Fl::scheme("gleam");
    Fl::get_system_colors();

    win = new Fl_Double_Window(640, 480, "Slider");

    win->begin();

    slider = new Fl_Slider(10, 40, 500, 30, "Slider");
    slider->type(FL_HOR_NICE_SLIDER);
    slider->labelfont(FL_COURIER);
    slider->labelsize(14);
    slider->minimum(0);
    slider->maximum(100);
    slider->step(1);
    slider->value(60);
    slider->align(FL_ALIGN_RIGHT);

    box = new Fl_Box(10, 100, 90, 40, "...");
    box->box(FL_FLAT_BOX);
    box->labelfont(FL_BOLD);
    box->labelsize(14);
    box->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);

    slider->callback(cb_slides, win);

    win->end();
    win->show();
    return Fl::run();
}

下面是按右鍵會有 popup menu 的例子。

#include <FL/Fl.H>
#include <FL/Fl_Menu_Button.H>
#include <FL/Fl_Multiline_Input.H>
#include <FL/Fl_Window.H>
#include <FL/fl_message.H>
#include <cstdlib>
#include <cstring>

Fl_Window *G_win = 0;
Fl_Menu_Button *G_menu = 0;
Fl_Multiline_Input *G_input = 0;

static void Menu_CB(Fl_Widget *, void *) {
    const char *text = G_menu->text();

    if (!text)
        return;

    if (strcmp(text, "Quit") == 0) {
        exit(0);
    }
}

int main(int argc, char *argv[]) {
    Fl::scheme("gtk+");
    Fl::get_system_colors();

    G_win = new Fl_Window(640, 480, "Simple popup menu");
    G_win->tooltip("Use right-click for popup menu..");

    G_menu = new Fl_Menu_Button(0, 0, 640, 480, "Popup Menu");
    G_menu->type(Fl_Menu_Button::POPUP3);
    G_menu->add("Quit", "^q", Menu_CB, 0); // ctrl-q hotkey

    G_input = new Fl_Multiline_Input(50, 50, 350, 50, "Input");
    G_input->value("Right-click anywhere on gray window area\nfor popup menu");
    G_win->end();
    G_win->show();
    return (Fl::run());
}

Fl_Text_Display 可以用來展示文字,下面就是使用 Fl_Text_Display 與 Fl_Menu_Bar 的例子, 用來列出 ODBC data sources:

#include <cstdlib>
#include <FL/Fl.H>
#include <FL/Fl_Menu_Bar.H>
#include <FL/Fl_Menu_Button.H>
#include <FL/Fl_Scroll.H>
#include <FL/Fl_Text_Display.H>
#include <FL/Fl_Window.H>
#include <FL/fl_message.H>
#include <sql.h>
#include <sqlext.h>

Fl_Window *window;
Fl_Text_Buffer *buff;
Fl_Text_Display *disp;

void exitcb(Fl_Widget *, void *) {
    switch (fl_choice("Are you sure you want to quit?", "No", "Yes", nullptr)) {
    case 1:
        exit(0);
        break;
    default:
        break;
    }
}

void aboutcb(Fl_Widget *, void *) {
    fl_message("It is a simple program written by FLTK.");
}

Fl_Menu_Item menutable[] = {
    {"&File", 0, 0, 0, FL_SUBMENU},
        {"&Quit", FL_ALT + 'q', exitcb, 0, FL_MENU_DIVIDER},
        {0},
    {"&Help", 0, 0, 0, FL_SUBMENU},
        {"&About", 0, aboutcb, 0, FL_MENU_DIVIDER},
        {0},
    {0}
};

int main(int argc, char **argv) {
    SQLHENV env = NULL;
    SQLCHAR driver[256];
    SQLCHAR attr[256];
    char result[512];
    SQLSMALLINT driver_ret;
    SQLSMALLINT attr_ret;
    SQLUSMALLINT direction;
    SQLRETURN ret;

    Fl::scheme("gtk+");
    Fl::get_system_colors();

    window = new Fl_Window(800, 600, "Data Sources");
    window->callback(exitcb);
    Fl_Menu_Bar menubar(0, 0, 800, 30);
    menubar.menu(menutable);

    buff = new Fl_Text_Buffer();
    disp = new Fl_Text_Display(1, 30, 800, 570);
    disp->buffer(buff);
    disp->textsize(14);

    // Try to get ODBC data sources
    ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
    if (ret == SQL_SUCCESS) {
        SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0);

        direction = SQL_FETCH_FIRST;
        while (SQL_SUCCEEDED(ret = SQLDrivers(env, direction, driver,
                                              sizeof(driver), &driver_ret, attr,
                                              sizeof(attr), &attr_ret))) {
            direction = SQL_FETCH_NEXT;
            sprintf(result, "%s - %s\n", driver, attr);
            disp->insert(result);
        }
    }

    if (env != NULL) {
        SQLFreeHandle(SQL_HANDLE_ENV, env);
    }

    disp->tooltip("Display ODBC data sources.");

    window->resizable(*disp);
    window->end();

    window->show(argc, argv);
    return Fl::run();
}

Event handling

在 FLTK widget 中使用 handle() 來處理事件 (event),並且使用 draw() 來繪製圖形。

接下來是一個捕抓滑鼠移動事件的例子。

#include <cstdio>
#include <FL/Fl.H>
#include <FL/Fl_Window.H>

class MyWindow : public Fl_Window {
public:
    MyWindow(int W, int H, const char *L = 0)
        : Fl_Window(W, H, L) {}

    int handle(int e) {
        int ret = Fl_Window::handle(e);
        switch (e) {
        case FL_MOVE:
            char message[20];
            int x = Fl::event_x();
            int y = Fl::event_y();
            sprintf(message, "(%d, %d)", x, y);
            this->label(message);
            this->redraw();
            return (1);
        }
        return (ret);
    }
};

int main() {
    MyWindow *g_win = new MyWindow(400, 300, "Window");

    g_win->show();
    return (Fl::run());
}

下面是一個簡單的自製 widget,會在畫面上畫一個藍色的 X。同時也使用 handle() 處理事件。

#include <FL/Fl.H>
#include <FL/Fl_Double_Window.H>
#include <FL/fl_draw.H>

class DrawX : public Fl_Widget {
public:
    DrawX(int X, int Y, int W, int H, const char *L = 0)
        : Fl_Widget(X, Y, W, H, L) {}

    int handle(int e) {
        static int offset[2] = {0, 0};
        int ret = Fl_Widget::handle(e);
        switch (e) {
        case FL_PUSH:
            offset[0] = x() - Fl::event_x();
            offset[1] = y() - Fl::event_y();
            return (1);
        case FL_RELEASE:
            return (1);
        case FL_DRAG:
            Fl_Window * win = Fl::first_window();
            position(offset[0] + Fl::event_x(), offset[1] + Fl::event_y());

            win->redraw();
            return (1);
        }
        return (ret);
    }

    void draw() {
        fl_color(FL_BLUE);
        int x1 = x(), y1 = y();
        int x2 = x() + w() - 1, y2 = y() + h() - 1;
        fl_line(x1, y1, x2, y2);
        fl_line(x1, y2, x2, y1);
    }
};

int main() {
    Fl_Double_Window *window = nullptr;
    DrawX *draw_x = nullptr;

    window = new Fl_Double_Window(400, 300);
    Fl::first_window(window);

    draw_x = new DrawX(0, 0, window->w(), window->h());
    window->resizable(*draw_x);

    window->end();

    window->show();
    return (Fl::run());
}

OpenGL

在 FLTK 中使用 OpenGL,一般的做法為實作 Fl_Gl_Window 子類別 (subclass), 並且在子類別中實作 draw() function。
下面是一個簡單的 OpenGL 例子(來自 Erco's FLTK Cheat Page):

#include <FL/Fl.H>
#include <FL/Fl_Gl_Window.H>
#include <FL/gl.h>

class MyGlWindow : public Fl_Gl_Window {
    void draw() {
        if (!valid()) {
            glLoadIdentity();
            glViewport(0, 0, w(), h());
            glOrtho(-w(), w(), -h(), h(), -1, 1);
        }

        // Clear screen
        glClear(GL_COLOR_BUFFER_BIT);

        // Draw white 'X'
        glColor3f(1.0, 1.0, 1.0);
        glBegin(GL_LINE_STRIP);
        glVertex2f(w(), h());
        glVertex2f(-w(), -h());
        glEnd();
        glBegin(GL_LINE_STRIP);
        glVertex2f(w(), -h());
        glVertex2f(-w(), h());
        glEnd();
    }

public:
    MyGlWindow(int X, int Y, int W, int H, const char *L = 0)
        : Fl_Gl_Window(X, Y, W, H, L) {}
};

int main() {
    Fl::scheme("gtk+");
    Fl::get_system_colors();

    Fl_Window win(640, 480, "OpenGL X");
    MyGlWindow mygl(10, 10, win.w() - 20, win.h() - 20);
    win.end();
    win.resizable(mygl);
    win.show();
    return (Fl::run());
}

使用 fltk-config 提供編譯資訊時要加上 --use-gl 才行。

g++ GlWindow.cxx -o GlWindow `fltk-config --use-gl --cxxflags --ldflags`

如果沒有正確的連結 OpenGL 函式庫,那麼嘗試下面的指令:

g++ GlWindow.cxx -o GlWindow `fltk-config --use-gl --cxxflags --ldflags` -lGL

沒有留言:

張貼留言

注意:只有此網誌的成員可以留言。