2025/10/19

Fast Light Toolkit

FLTK 由 Bill Spitzak 個人開始開發並且目前仍然是專案的領導者, 是一個跨平台的 C++ GUI toolkit,用在 UNIX/Linux、微軟 Windows 和 Mac OS X 上, 使用 OpenGL 相關的功能提供 3D 繪圖的支援,並且有一個簡單的界面設計工具 FLUID。 FLTK 被設計足夠小和模組化以被靜態連結,但作為共享庫也工作良好。FLTK 的授權使用 LGPL v2,不過允許使用靜態連結。

一般而言,跨平台 GUI toolkit 有以下二個主要的策略:

  • 使用一組共同的 API 包裝各平台提供的底層函式庫或者是工具箱,如果該平台沒有提供相關的視窗部件 (widget) 再自行撰寫。 使用此策略著名的 GUI toolkit 為 wxWidgets。
  • 使用各個平台的繪圖功能繪製出最基本的視窗,再由最基本的視窗建構各種視窗部件。 使用此策略著名的 GUI toolkit 為 GTK 與 Qt。

FLTK 的跨平台策略為使用各個平台的繪圖功能繪製出最基本的視窗,再由最基本的視窗建構各種視窗部件。 這個策略的優點是移植到不同的平台較為容易,缺點是缺少 native look and feel,需要使用佈景主題 (theme) 之類的方法改進視覺效果。

FLTK 是一個輕量化的函式庫,適合撰寫一些小工具或者是提供前端界面,不過雖然提供了大多數常見的視窗部件, 但是如果是很複雜的部件則未必有提供(有可能需要自行撰寫),如果要撰寫極為複雜的圖形使用者介面應用程式,那麼建議使用其它的 GUI toolkit。

從 FLTK 1.4 開始,編譯 FLTK 建議使用的 build system 為 CMake。


所有 FLTK 應用程式都基於事件處理模型。使用者滑鼠移動、按鈕點擊和鍵盤活動等操作都會產生事件, 並發送給應用程式。然後,應用程式可能會忽略事件或回應用戶。

下面就是 FLTK 的 Hello 例子:

#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include <FL/Fl_Box.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 1.3.x,需要宣告 <FL/Fl.H> 才行,FLTK 1.4 則放鬆了這個限制,不過宣告了也不會有問題。

FLTK 提供了 fltk-config 可以用來查詢編譯與連結時需要的參數,下面是在 UNIX/Linux 環境下編譯的指令:

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

如果要使用 CMake 作為 build system,那麼建立一個 CMakeLists.txt,內容如下:

cmake_minimum_required(VERSION 3.15)

project(hello)

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

find_package(FLTK 1.4 CONFIG REQUIRED)

add_executable       (hello WIN32 MACOSX_BUNDLE hello.cxx)
target_link_libraries(hello PRIVATE fltk::fltk)

Callbacks

Callbacks 是當視窗部件的值改變時所呼叫的函數。 下面是使用 Fl_Button 與 callback function 的程式,還示範了應該如何設置 shortcut。

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

void close_callback(Fl_Widget *, void *) {
    if (Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
        return;

    exit(0);
}

int main(int argc, char **argv) {
    Fl_Window *window = new Fl_Window(340, 180);

    Fl_Button *button = new Fl_Button(20, 40, 300, 100, "Hello, World!");
    button->type(FL_NORMAL_BUTTON);
    button->color(FL_WHITE);
    button->labelcolor(FL_BLACK);    
    button->shortcut(FL_ALT + 'q');
    button->callback(close_callback);

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

通常只有當視窗部件的值發生變化時才會執行 callback,使用者可以使用 Fl_Widget::when() 方法進行相關的設定。


下面是一個簡單的 right-click context menu 例子。

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

void Menu_CB(Fl_Widget *w, void *d) {
    exit(0);
}

int main(int argc, char **argv) {
    Fl_Window *window = new Fl_Window(640, 480);
    window->tooltip("Use right-click for popup menu...");

    Fl_Menu_Button *menu = new Fl_Menu_Button(0, 0, 640, 480, "Popup Menu");
    menu->type(Fl_Menu_Button::POPUP3);  // pops menu on right click
    menu->add("Quit", "^q", Menu_CB, 0); // ctrl-q hotkey
    menu->menu_end();

    Fl_Multiline_Input *input =
        new Fl_Multiline_Input(100, 200, 350, 50, "Input");
    input->value("Right-click anywhere on gray window area\nfor popup menu");

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

下面是一個簡單的 Fl_Menu_Bar 例子。

#include <FL/Fl.H>
#include <FL/Fl_Menu_Bar.H>
#include <FL/Fl_Window.H>
#include <cstdio>
#include <cstdlib>
#include <cstring>

static void MyMenuCallback(Fl_Widget *w, void *) {
    Fl_Menu_Bar *bar = (Fl_Menu_Bar *)w;
    const Fl_Menu_Item *item = bar->mvalue();

    char ipath[256];
    bar->item_pathname(ipath, sizeof(ipath));
    fprintf(stderr, "callback: You picked '%s'",
            item->label());                              // Print item picked
    fprintf(stderr, ", item_pathname() is '%s'", ipath); // ..and full pathname

    fprintf(stderr, "\n");
    if (strcmp(item->label(), "&Quit") == 0) {
        exit(0);
    }
}

int main() {
    Fl::scheme("gleam");
    Fl_Window *win = new Fl_Window(640, 480, "menubar");

    Fl_Menu_Bar *menu = new Fl_Menu_Bar(0, 0, 640, 25);
    menu->add("&File/&Quit", "^q", MyMenuCallback);
    menu->add("&Help/&About", 0, MyMenuCallback);

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

Layout

除了直接固定視窗部件的位置,FLTK 提供了一些佈局用的視窗部件可以用來作為自動佈局視窗部件的位置。

下面是 Fl_Flex 的例子。

#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Flex.H>
#include <FL/fl_ask.H>

void exit_cb(Fl_Widget *w, void *) {
    fl_message("The '%s' button closes the window\nand terminates the program.",
               w->label());
    w->window()->hide();
}

void button_cb(Fl_Widget *w, void *) {
    fl_message("The '%s' button does nothing.", w->label());
}

int main(int argc, char **argv) {
    Fl_Double_Window window(640, 60, "Simple Fl_Flex Demo");
    Fl_Flex flex(5, 5, window.w() - 10, window.h() - 10, Fl_Flex::HORIZONTAL);
    Fl_Button b1(0, 0, 0, 0, "File");
    Fl_Button b2(0, 0, 0, 0, "New");
    Fl_Button b3(0, 0, 0, 0, "Save");
    Fl_Box bx(0, 0, 0, 0); // empty space
    Fl_Button eb(0, 0, 0, 0, "Exit");

    // assign callbacks to buttons
    b1.callback(button_cb);
    b2.callback(button_cb);
    b3.callback(button_cb);
    eb.callback(exit_cb);

    // set gap between adjacent buttons and extra spacing (invisible box size)
    flex.gap(10);
    flex.fixed(bx, 30);

    // end() groups
    flex.end();
    window.end();

    // set resizable, minimal window size, show() window, and execute event loop
    window.resizable(flex);
    window.size_range(300, 30);
    window.show(argc, argv);
    return Fl::run();
}

Drawing

只有在某些特定的地方才可以執行繪圖到螢幕顯示器的 FLTK 程式碼。在其他地方呼叫這些函數將會導致未定義的行為!

  • 最常見的地方是在 Fl_Widget::draw() 方法內部。要在此處編寫程式碼, 必須將現有 Fl_Widget 類別之一子類化並實作自己的 draw() 版本。
  • 也可以建立自訂 boxtypes 與 labeltypes。這些涉及編寫可由現有 Fl_Widget::draw() 方法呼叫的小程序。 這些「類型」由儲存在 widget 的 box(), labeltype() 和可能的其他屬性中的 8 位元索引標識。
  • 可以呼叫 Fl_Window::make_current() 來對視窗部件進行增量更新。使用 Fl_Widget::window() 來尋找視窗。

下面的程式是畫一個 X 的示範程式。

#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) {}

    virtual void draw() FL_OVERRIDE {
        fl_color(FL_YELLOW);
        fl_rectf(x(), y(), w(), h());
        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 win(400, 400, "Draw X");
    DrawX draw_x(10, 10, win.w() - 20, win.h() - 20);
    draw_x.color(FL_YELLOW);
    win.resizable(draw_x);
    win.show();
    return (Fl::run());
}

Event

每次使用者移動滑鼠指標、點擊按鈕或按下按鍵時,都會產生一個事件 (event) 並發送到使用者的應用程式。 事件也可以來自其他程序,例如視窗管理器 (window manager)。

在 FLTK 中事件由傳遞給 handle() 方法的整數參數標識,該方法覆蓋 Fl_Widget::handle() virtual method。 有關最近事件的其他資訊儲存在靜態位置,並透過呼叫獲取 Fl::event_∗() 方法。 此靜態資訊一直有效,直到從視窗系統讀取下一個事件為止,因此可以在 handle() 方法之外查看它,例如在 callbacks 中。

下面是一個繼承 Fl_Window,並且在 handle() 方法印出事件名稱的例子。

#include <FL/Fl.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Window.H>
#include <FL/names.h> // FLTK 1.1.8 and up
#include <cstdlib>

class PrintEevent_Window : public Fl_Window {
    int handle(int e) {
        fprintf(stderr, "EVENT: %s(%d)\n", fl_eventnames[e], e);
        return (Fl_Window::handle(e));
    }

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

void close_callback(Fl_Widget *, void *) {
    if (Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
        return;

    exit(0);
}

int main(int argc, char **argv) {
    PrintEevent_Window *window = new PrintEevent_Window(340, 180);

    Fl_Button *button = new Fl_Button(20, 40, 300, 100, "Exit");
    button->type(FL_NORMAL_BUTTON);
    button->color(FL_WHITE);
    button->labelcolor(FL_BLACK);
    button->shortcut(FL_ALT + 'q');
    button->callback(close_callback);

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

OpenGL

FLTK 提供了 Fl_Gl_Window,可以繼承此類別並且在 draw() 函式繪制自己需要的內容。

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

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

        glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        glColor3f(0.0, 0.0, 0.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(int argc, char **argv) {
    Fl_Window win(640, 480, "OpenGL sample");
    MyGlWindow mygl(10, 10, win.w() - 20, win.h() - 20);
    win.end();
    win.callback([](Fl_Widget *, void *) {
        if (Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
            return;

        exit(0);
    });

    win.resizable(mygl);
    win.show(argc, argv);
    return (Fl::run());
}

CMakeLists.txt 需要增加 FLTK OpenGL 的部份:

target_link_libraries(sample PRIVATE fltk::fltk fltk::gl)

相關連結

沒有留言:

張貼留言

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