2024/02/21

ODBC

ODBC(Open Database Connectivity)提供了一組標準的 API 來訪問資料庫管理系統(DBMS)。 這些 API 利用 SQL 來完成其大部分任務。 ODBC 是針對 C 的 API,不過很多的程式語言都有相關的 bindings。如果想使用 C 來寫 ODBC 程式, 可以從 ODBC from C Tutorial Part 1 開始學習如何撰寫。

The ODBC architecture has four components:

  • Application Performs processing and calls ODBC functions to submit SQL statements and retrieve results.

  • Driver Manager Loads and unloads drivers on behalf of an application. Processes ODBC function calls or passes them to a driver.

  • Driver Processes ODBC function calls, submits SQL requests to a specific data source, and returns results to the application. If necessary, the driver modifies an application's request so that the request conforms to syntax supported by the associated DBMS.

  • Data Source Consists of the data the user wants to access and its associated operating system, DBMS, and network platform (if any) used to access the DBMS.

In ODBC there are four main handle types and you will need to know at least three to do anything useful:

  • SQLHENV - environment handle

    This is the first handle you will need as everything else is effectively in the environment. Once you have an environment handle you can define the version of ODBC you require, enable connection pooling and allocate connection handles with SQLSetEnvAttr and SQLAllocHandle.

  • SQLHDBC - connection handle

    You need one connection handle for each data source you are going to connect to. Like environment handles, connection handles have attributes which you can retrieve and set with SQLSetConnectAttr and SQLGetConnectAttr.

  • SQLHSTMT - statement handle

    Once you have a connection handle and have connected to a data source you allocate statement handles to execute SQL or retrieve meta data. As with the other handles you can set and get statement attributes with SQLSetStmtAttr and SQLGetStmtAttr.

  • SQLHDESC - descriptor handle

    Descriptor handles are rarely used by applications even though they are very useful for more complex operations. Descriptor handles will be covered in later tutorials.

如果要查詢目前 unixODBC 的版本,使用下列的指令:

odbcinst --version

Listing Installed Drivers and Data Sources

如果是使用 unixODBC,可以使用下列命令列出已安裝的資料來源:

odbcinst -q -s

如果自己寫一個程式列出來已安裝的資料來源:

#include <sql.h>
#include <sqlext.h>
#include <stdio.h>

int main() {
    SQLHENV env = NULL;
    char driver[256];
    char attr[256];
    SQLSMALLINT driver_ret;
    SQLSMALLINT attr_ret;
    SQLUSMALLINT direction;
    SQLRETURN ret;

    ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
    if (ret != SQL_SUCCESS) {
        printf("SQLAllocHandle failed.\n");
    }

    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;
        printf("%s - %s\n", driver, attr);
        if (ret == SQL_SUCCESS_WITH_INFO)
            printf("\tdata truncation\n");
    }

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

在 Windows 平台,需要連結 odbc32 library,使用 UnixODBC 平台則需要連結 libodbc (-lodbc) 才行。

接下來使用 PostgreSQL ODBC driver 測試,測試是否能夠正確連接到資料庫。

#include <sql.h>
#include <sqlext.h>
#include <stdio.h>

void extract_error(char *fn, SQLHANDLE handle, SQLSMALLINT type) {
    SQLINTEGER i = 0;
    SQLINTEGER native;
    SQLCHAR state[7];
    SQLCHAR text[256];
    SQLSMALLINT len;
    SQLRETURN ret;

    fprintf(stderr,
            "\n"
            "The driver reported the following diagnostics whilst running "
            "%s\n\n",
            fn);

    do {
        ret = SQLGetDiagRec(type, handle, ++i, state, &native, text,
                            sizeof(text), &len);
        if (SQL_SUCCEEDED(ret))
            printf("%s:%ld:%ld:%s\n", state, i, native, text);
    } while (ret == SQL_SUCCESS);
}

int main() {
    SQLHENV env = NULL;
    SQLHDBC dbc = NULL;
    SQLRETURN ret;
    SQLCHAR outstr[1024];
    SQLSMALLINT outstrlen;

    ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return(1);
    }

    SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0);
    ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return(1);
    }

    ret = SQLDriverConnect(dbc, NULL, "DSN=PostgreSQL;", SQL_NTS, outstr,
                           sizeof(outstr), &outstrlen, SQL_DRIVER_COMPLETE);
    if (SQL_SUCCEEDED(ret)) {
        printf("Connected\n");
        printf("Returned connection string was:\n\t%s\n", outstr);
        if (ret == SQL_SUCCESS_WITH_INFO) {
            printf("Driver reported the following diagnostics\n");
            extract_error("SQLDriverConnect", dbc, SQL_HANDLE_DBC);
        }
        SQLDisconnect(dbc); /* disconnect from driver */
    } else {
        fprintf(stderr, "Failed to connect\n");
        extract_error("SQLDriverConnect", dbc, SQL_HANDLE_DBC);
    }

    if (dbc)
        SQLFreeHandle(SQL_HANDLE_DBC, dbc);

    if (env)
        SQLFreeHandle(SQL_HANDLE_ENV, env);

    return 0;
}

下面列出目前資料庫中表格的資料。

#include <sql.h>
#include <sqlext.h>
#include <stdio.h>

int main() {
    SQLHENV env = NULL;
    SQLHDBC dbc = NULL;
    SQLHSTMT stmt = NULL;
    SQLRETURN ret;
    SQLSMALLINT columns; /* number of columns in result-set */
    SQLCHAR buf[5][64];
    int row = 0;
    SQLLEN indicator[5];
    int i;

    ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0);
    ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    ret = SQLDriverConnect(dbc, NULL, "DSN=PostgreSQL;", SQL_NTS, NULL, 0, NULL,
                           SQL_DRIVER_COMPLETE);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "Failed to connect\n");
        goto End;
    }

    ret = SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "Failed to connect\n");
        goto End;
    }

    SQLTables(stmt, NULL, 0, NULL, 0, NULL, 0, "TABLE", SQL_NTS);
    SQLNumResultCols(stmt, &columns);
    for (i = 0; i < columns; i++) {
        SQLBindCol(stmt, i + 1, SQL_C_CHAR, buf[i], sizeof(buf[i]),
                   &indicator[i]);
    }

    /* Fetch the data */
    while (SQL_SUCCEEDED(SQLFetch(stmt))) {
        /* display the results that will now be in the bound area's */
        for (i = 0; i < columns; i++) {
            if (indicator[i] == SQL_NULL_DATA) {
                printf("  Column %u : NULL\n", i);
            } else {
                printf("  Column %u : %s\n", i, buf[i]);
            }
        }
    }

End:
    if (stmt)
        SQLFreeHandle(SQL_HANDLE_DBC, stmt);

    if (dbc)
        SQLFreeHandle(SQL_HANDLE_DBC, dbc);

    if (env)
        SQLFreeHandle(SQL_HANDLE_ENV, env);

    return 0;
}

接下來的程式使用 SQLExecDirect 與 SQLFetch 取得 PostgreSQL 的版本資訊。

#include <sql.h>
#include <sqlext.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    SQLHENV henv = SQL_NULL_HENV;
    SQLHDBC hdbc = SQL_NULL_HDBC;
    SQLHSTMT hstmt = SQL_NULL_HSTMT;

    SQLRETURN ret;
    SQLCHAR Version[255];
    SQLLEN siVersion;

    char *sqlStatement = "Select version() as version";

    ret = SQLAllocHandle(SQL_HANDLE_ENV, NULL, &henv);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0);
    ret = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    ret = SQLDriverConnect(hdbc, NULL, "DSN=PostgreSQL;", SQL_NTS, NULL, 0,
                           NULL, SQL_DRIVER_COMPLETE);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "Failed to connect\n");
        goto End;
    }

    ret = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLAllocHandle(SQL_HANDLE_STMT) failed.\n");
        goto End;
    }

    ret = SQLExecDirect(hstmt, sqlStatement, strlen(sqlStatement));
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLExecDirect() failed.\n");
        goto End;
    }

    while (1) {
        ret = SQLFetch(hstmt);
        if (ret == SQL_ERROR || ret == SQL_SUCCESS_WITH_INFO) {
            fprintf(stderr, "SQLFetch(hstmt) failed.\n");
            goto End;
        }

        if (ret == SQL_NO_DATA) {
            break;
        }

        if (ret == SQL_SUCCESS) {
            ret = SQLGetData(hstmt, 1, SQL_C_CHAR, Version, 255, &siVersion);
            printf("Versoin:\n%s\n", Version);
        }
    }

End:

    if (hstmt != SQL_NULL_HSTMT) {
        SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
        hstmt = SQL_NULL_HSTMT;
    }

    if (hdbc != SQL_NULL_HDBC) {
        SQLDisconnect(hdbc);
        SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
        hdbc = SQL_NULL_HDBC;
    }

    if (henv != SQL_NULL_HENV) {
        SQLFreeHandle(SQL_HANDLE_ENV, henv);
        hstmt = SQL_NULL_HSTMT;
    }

    return 0;
}

下面是 SQLPrepare/SQLExecute 的範例(包含使用 SQLBindParameter)。

#include <sql.h>
#include <sqlext.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    SQLHENV henv = SQL_NULL_HENV;
    SQLHDBC hdbc = SQL_NULL_HDBC;
    SQLHSTMT hstmt = SQL_NULL_HSTMT;

    SQLRETURN ret;
    int RetParam = 1;
    SQLLEN cbRetParam = SQL_NTS;
    SQLCHAR Name[40];
    SQLLEN lenName = 0;
    SQLSMALLINT NumParams;

    char *sqlstr = NULL;

    ret = SQLAllocHandle(SQL_HANDLE_ENV, NULL, &henv);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0);
    ret = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    ret = SQLDriverConnect(hdbc, NULL, "DSN=PostgreSQL;", SQL_NTS, NULL, 0,
                           NULL, SQL_DRIVER_COMPLETE);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "Failed to connect\n");
        goto End;
    }

    ret = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLAllocHandle(SQL_HANDLE_STMT) failed.\n");
        goto End;
    }

    sqlstr = "drop table if exists person";
    ret = SQLPrepare(hstmt, (SQLCHAR *)sqlstr, strlen(sqlstr));
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLPrepare() failed.\n");
        goto End;
    }

    ret = SQLExecute(hstmt);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLExecute(drop) failed.\n");
        goto End;
    }

    sqlstr = "create table if not exists person (id integer, name varchar(40) "
             "not null)";
    ret = SQLPrepare(hstmt, (SQLCHAR *)sqlstr, strlen(sqlstr));
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLPrepare(create) failed.\n");
        goto End;
    }

    ret = SQLExecute(hstmt);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLExecute() failed.\n");
        goto End;
    }

    ret = SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER,
                           0, 0, &RetParam, 0, &cbRetParam);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLBindParameter(1) failed.\n");
        goto End;
    }

    ret = SQLBindParameter(hstmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, 40,
                           0, Name, 40, &lenName);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLBindParameter(2) failed.\n");
        goto End;
    }

    ret = SQLPrepare(hstmt,
                     (SQLCHAR *)"insert into person (id, name) values (?, ?)",
                     SQL_NTS);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLPrepare(insert) failed.\n");
        goto End;
    }

    SQLNumParams(hstmt, &NumParams);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLNumParams() failed.\n");
        goto End;
    }

    printf("Num params : %i\n", NumParams);
    strcpy(Name, "Orange");
    lenName = strlen(Name);

    ret = SQLExecute(hstmt);
    if (ret == SQL_SUCCESS || ret == SQL_SUCCESS_WITH_INFO) {
        printf("Status : Success\n");
    } else {
        fprintf(stderr, "SQLExecute(insert) failed.\n");
    }

End:

    if (hstmt != SQL_NULL_HSTMT) {
        SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
        hstmt = SQL_NULL_HSTMT;
    }

    if (hdbc != SQL_NULL_HDBC) {
        SQLDisconnect(hdbc);
        SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
        hdbc = SQL_NULL_HDBC;
    }

    if (henv != SQL_NULL_HENV) {
        SQLFreeHandle(SQL_HANDLE_ENV, henv);
        hstmt = SQL_NULL_HSTMT;
    }

    return 0;
}

C++ Regular Expression

Posix Regular Expression

Posix Regular Expression 是 POSIX 所建立的其中一個標準(並且有 Basic 與 Extended 的分別), 大多數有支援 POSIX 標準的 libc library 都有實作(包含 glibc), 不過實作的細節可能在各個實作上會略有不同。因此,雖然 glibc 有內建,除非要相容於 GNU 工具程式的正規表示式, 否則不一定要使用 Posix Regular Expression。 對於 C 而言,最常被考慮的跨平台 Regular Expression library 為 PCRE 或者是 PCRE2。

下面是 1-9 位數不重複印出來的練習問題(在 Linux 測試,使用 glibc):

#include <stdio.h>
#include <math.h>
#include <regex.h>

int main() {
    regex_t re;
    int number = 0;
    long max = 0;

    printf("Please give a number: ");
    scanf("%d", &number);

    if (number < 1 || number > 9) {
        printf("Out of range.\n");
        return (1);
    }

    max = round(pow(10, number)) - 1;
    regcomp(&re, "([0-9]).*\\1", REG_EXTENDED);
    for (long index = 1; index <= max; index++) {
        int value;
        char str[10];
        sprintf(str, "%ld", index);
        value = regexec(&re, str, 0, NULL, 0);

        if (value == REG_NOMATCH) {
            printf("%ld\n", index);
        }
    }

    regfree(&re);
    return 0;
}

PCRE 或者是 PCRE2 都有提供 Posix Regular Expression 相容的 API, 只是 Regular Expression 語法就沒有 Basic 與 Extended 的分別, 而是使用 PCRE 本身的語法。以 PCRE2 來說,只要改為 include pcre2posix.h, 連結時要加上 -lpcre2-posix-lpcre2-8 即可。

#include <stdio.h>
#include <math.h>
#include <pcre2posix.h>

int main() {
    regex_t re;
    int number = 0;
    long max = 0;

    printf("Please give a number: ");
    scanf("%d", &number);

    if (number < 1 || number > 9) {
        printf("Out of range.\n");
        return (1);
    }

    max = round(pow(10, number)) - 1;
    regcomp(&re, "([0-9]).*\\1", 0);
    for (long index = 1; index <= max; index++) {
        int value;
        char str[10];
        sprintf(str, "%ld", index);
        value = regexec(&re, str, 0, NULL, 0);

        if (value == REG_NOMATCH) {
            printf("%ld\n", index);
        }
    }

    regfree(&re);
    return 0;
}

C++ Regular Expression

自 C++11 開始,C++ 標準函式庫提供了 Regular Expression library。

下面是 1-9 位數不重複印出來的練習問題:

#include <cmath>
#include <iostream>
#include <regex>
#include <string>

int main() {
    int number = 0;
    long max = 0;

    std::cout << "Please give a number: ";
    std::cin >> number;

    if (number < 1 || number > 9) {
        printf("Out of range.\n");
        return (1);
    }

    max = round(pow(10, number)) - 1;
    std::regex re("([0-9]).*\\1",  std::regex_constants::ECMAScript);
    for (long index = 1; index <= max; index++) {
        std::string s = std::to_string(index);

        std::smatch m;
        std::regex_search(s, m, re);

        if (m.empty()) {
            std::cout << index << std::endl;
        }
    }

    return 0;
}

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

2024/02/03

GNU Make

GNU Make 是一個工具程式, 經由讀取 Makefile 或者是 makefile 的檔案(也可以使用 -f 指定),自動化建構軟體。 Makefile 是由很多相依性項目(dependencies)和規則(rules)所組成。

GNU Makefile 可以在各個程式語言使用,而不僅限於 C 或者是 C++。
不過因為通常是使用 C 或者是 C++,所以接下來使用 C 作為例子,考慮一個簡單的程式 hello.c:

void say(const char *name);

int main() {
    say("Orange");

    return 0;
}

以及 say procedure 的實作:

#include <stdio.h>

void say(const char *name) { printf("Hello, %s.\n", name); }

那麼在命令列編譯的指令如下:

gcc hello.c say.c -o hello

Makefile 的規則如下:

target [target ...]: [component ...]
	Tab ↹[command 1]
		.
		.
		.
	Tab ↹[command n]

Makefile 支援 Suffix rules,例子如下:

.SUFFIXES: .txt .html

# From .html to .txt
.html.txt:
    lynx -dump $<   >   $@

Makefile 支援 Pattern rules,例子如下:

# From %.html to %.txt
%.txt : %.html
    lynx -dump $< > $@

其中 $@ 或者是 $<, $^, $? 都是 Makefile 的巨集。$@ 代表工作目標的完整檔案名稱,$< 代表觸發建制目標的檔案名稱。 $^ 代表所有的依賴檔案,並以空格隔開這些檔名。$? 代表比目標還要新的依賴檔案列表。而 $* 代表工作目標的主檔名(也就是不包含副檔名)。

撰寫一個 Makefile 如下:

CC = /usr/bin/gcc
CFLAGS= -O2 -Wall
PROGRAM = hello
SRCS := $(wildcard *.c)
OBJS := $(patsubst %.c,%.o,$(SRCS))

RM = rm

all: $(PROGRAM)

$(PROGRAM): $(OBJS)
	$(CC) $(CFLAGS) $(OBJS) -o $@

%.o : %.c
	$(CC) $(CFLAGS) -c $< -o $@

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

.PHONY: all clean