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
沒有留言:
張貼留言
注意:只有此網誌的成員可以留言。