2026/02/04

Apache Geode

簡介

Apache Geode 是一個數據管理平台, 它為廣泛分佈的雲端架構中的資料密集型應用程式提供即時、一致的存取, 一般而言作為 In-Memory Data Grid (IMDG)、快取 (cache) 以及需要即時處理的場合使用。 Apache Geode 提供了 SQL-like 查詢語言,稱為 OQL (Object Query Language), Apache Geode 以 Java object 的方式儲存資料,所以選擇使用 OQL 查詢儲存的資料。

下載 Apache Geode 以後解壓縮放到某個目錄。gfsh 為 Apache Geode 用來管理的 shell tool。

執行 gfsh。下面是在 gfsh 執行的指令,資料來自於 Apache Geode in 15 Minutes or Less 的教學。

Locator 是 Geode 行程 (processes),它告訴新連線的成員正在執行的成員在哪裡,並為伺服器使用提供負載平衡。

start locator --name=locator1

Geode 提供了 web 界面的監控界面,下面是啟動的指令。預設使用者為 admin,密碼為 admin。

start pulse

Geode server 是一個行程 (process),它作為叢集中一個長期運行且可配置的成員而存在。 Geode server 主要用於託管長期運行的資料區域,以及運行標準的 Geode 行程,例如用戶端/伺服器配置中的伺服器。

start server --name=server1 --server-port=40411

Regions 是 Geode 叢集的核心建置模組,用於組織資料。在此練習中建立的 Region 採用複製機制在叢集成員之間複製數據, 並利用持久化機制將資料儲存到磁碟。

create region --name=regionA --type=REPLICATE_PERSISTENT

列出目前的 regions:

list regions

列出 Geode 叢集的成員:

list members

描述 Geode region regionA 的資料:

describe region --name=regionA

下面使用 put 新增資料以及使用 query 查詢資料。

put --region=regionA --key="1" --value="one"
put --region=regionA --key="2" --value="two"
query --query="select * from /regionA"

如果你需要刪除一個 region,可以這樣做:

destroy region --name=regionA

如果要停止 server:

stop server --name=server1

關閉系統,包括 locator。

shutdown --include-locators=true

REST

Geode 讓使用者能夠使用 REST 介面存取資料。

啟動一個 locator。

start locator --name=locator1

並且使用以下的設定:

configure pdx --read-serialized=true --disk-store

然後在啟動 Geode server 時加入 --start-rest-api 選項。

start server --name=server1 --server-port=40411 \
--start-rest-api=true \
--http-service-port=8080 --http-service-bind-address=localhost

使用 curl 驗證是否可以使用:

curl -i http://localhost:8080/geode/v1

SSL

使用 keytool 建立 keystroe:

keytool -genkeypair -alias server \
-dname "CN=localhost, OU=IT Department, O=Orange Inc. ,L=Taipei, S=Taiwan,C=TW" \
-ext SAN=DNS:localhost,IP:127.0.0.1 \
-keyalg RSA -keysize 2048 -sigalg SHA256withRSA -storetype PKCS12 \
-validity 3650 \
-keypass password -keystore ./trusted.keystore -storepass password

在 Geode 的 config 目錄下建立一個新的檔案 gfsecurity.properties。 Apache Geode 使用 ssl-enabled-components 設定不同組件間的通訊是否需要使用 SSL/TLS。 all 表示全部都要使用,這裡設定為 web,表示使用在 REST 介面。

ssl-enabled-components=web
ssl-protocols=TLSv1.2,TLSv1.3
ssl-ciphers=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_256_GCM_SHA384
ssl-keystore=/home/danilo/Programs/geode/config/trusted.keystore
ssl-keystore-password=password
ssl-keystore-type=pkcs12
ssl-truststore=/home/danilo/Programs/geode/config/trusted.keystore
ssl-truststore-password=password
ssl-truststore-type=pkcs12

使用 gfsh 啟動一個 locator。

start locator --name=locator1 --port=12345 \
--security-properties-file=/home/danilo/Programs/geode/config/gfsecurity.properties

並且使用以下的設定:

configure pdx --read-serialized=true --disk-store

使用 gfsh 啟動一個 server。

start server --name=server1 --server-port=40411 \
--start-rest-api=true \
--http-service-port=8080 --http-service-bind-address=localhost \
--security-properties-file=/home/danilo/Programs/geode/config/gfsecurity.properties

使用 curl 驗證是否可以使用:

curl -k -i https://localhost:8080/geode/v1

Authentication

以下的方式在 Java 24/25 以後,因為 SecurityManager 被禁止而無法使用。 In Java 17, the Security Manager was deprecated for removal under JEP 411. With JDK 24, its functionality will be effectively disabled. So you could not setup HTTP Basic Authentication support for Apache Geode by using SecurityManager since JDK 24.

將下列的內容儲存為 security.json,並且放到各個 locator 與 server 的目錄下。

{
  "roles": [
    {
      "name": "cluster",
      "operationsAllowed": [
        "CLUSTER:MANAGE",
        "CLUSTER:WRITE",
        "CLUSTER:READ"
      ]
    },
    {
      "name": "data",
      "operationsAllowed": [
        "DATA:MANAGE",
        "DATA:WRITE",
        "DATA:READ"
      ]
    },
    {
      "name": "region1&2Reader",
      "operationsAllowed": [
        "DATA:READ"
      ],
      "regions": ["region1", "region2"]
    }
  ],
  "users": [
    {
      "name": "super-user",
      "password": "1234567",
      "roles": [
        "cluster",
        "data"
      ]
    },
    {
      "name": "joebloggs",
      "password": "1234567",
      "roles": [
        "data"
      ]
    }
  ]
}

使用 gfsh 啟動一個 locator。

start locator --name=locator1 --port=12345 \
--security-properties-file=/home/danilo/Programs/geode/config/gfsecurity.properties \
--J=-Dgemfire.security-manager=org.apache.geode.examples.security.ExampleSecurityManager \
--classpath=.

連線到 locator 需要驗證:

connect --locator=localhost[12345] --user=super-user --password=1234567

並且使用以下的設定:

configure pdx --read-serialized=true --disk-store

使用 gfsh 啟動一個 server。

start server --name=server1 --locators=localhost[12345] --server-port=40411 \
--start-rest-api=true \
--http-service-port=8080 --http-service-bind-address=localhost \
--security-properties-file=/home/danilo/Programs/geode/config/gfsecurity.properties \
--J=-Dgemfire.security-manager=org.apache.geode.examples.security.ExampleSecurityManager \
--classpath=. --user=super-user --password=1234567

Memcached

Apache Geode 提供了 memcached 協議相容的介面。

使用 gfsh 啟動一個 locator。

start locator --name=locator1

並且在 config 新增或者修改 cache.xml,使用下列的設定:

<?xml version="1.0" encoding="UTF-8"?>
<cache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
       xmlns="http://geode.apache.org/schema/cache" 
       xsi:schemaLocation="http://geode.apache.org/schema/cache http://geode.apache.org/schema/cache/cache-1.0.xsd"
       version="1.0">
  <region name="gemcached">
    <region-attributes refid="PARTITION" />
  </region>
</cache>

使用 gfsh 啟動一個 server。

start server --name=server1 --server-port=40411 \
--memcached-port=11211 --memcached-bind-address=localhost \
--memcached-protocol=BINARY \
--cache-xml-file=/home/danilo/Programs/geode/config/cache.xml

--memcached-protocol 可以設為 ASCII 或者是 BINARY。 ASCII 是 libMemcached 的預設值,如果要使用 BINARY 需要設定。

使用 memcached-for-Tcl 進行驗證。

package require Memcache

memcache server add localhost 11211
memcache behavior MEMCACHED_BEHAVIOR_BINARY_PROTOCOL 1
memcache set moo "cows go moo"
memcache get moo result
puts $result

相關連結

2026/02/02

BaseX database

BaseX 是一個使用 Java 撰寫的 XML 資料庫, 也可以作為 XQuery processor 使用,採用 BSD-3-Clause 授權, 使用 XQuery 作為其查詢語言,實作 XQuery 多項相關標準, 並且設計了自己的 Server Protocol。

basexserver 預設的 port 為 1984,初次啟動需要使用 -cPASSWORD 設定 admin 的密碼。

basexserver -cPASSWORD

下面使用 basexclient 連線到 server(-U 為 username,-P 為 password)。

basexclient -Uadmin -Padmin

建立一個新的使用者 danilo,並且給予全域的 CREATE 權限(BaseX 將權限分為 global 與 local, global 有 NONE, READ, WRITE, CREATE, ADMIN 的區別,而 local 則是 NONE, READ, WRITE)。

CREATE USER danilo password
ALTER PASSWORD danilo password
GRANT CREATE TO danilo

其中 CREATE USER 可以在建立使用者的時候同時設定密碼,在之後如果要修改密碼可以使用 ALTER PASSWORD 修改。

如果要移除使用者:

DROP USER danilo

使用 basexclient 建立新的資料庫:

  • Create an empty database
    CREATE DB <database_name>
    
  • To create a database with initial data
    CREATE DB <database_name> <path/to/input>
    
  • To add multiple documents to a newly created empty database
    CREATE DB store
    ADD factbook.xml
    ADD https://files.basex.org/xml/xmark.xml
    

如果使用 XQuery 建立資料庫:

db:create('myDatabaseName', 'path/to/input.xml')

如果要列出目前的資料庫:

LIST

如果使用 XQuery 列出目前的資料庫:

db:list()

使用 basexclient 刪除資料庫:

DROP DB [name]

如果使用 XQuery 刪除資料庫:

db:drop($name)

Tools

BaseX 提供了一些工具可以使用。 basexserver 為 BaseX database server,而 basexclient 則是其 client 的程式。 basexgui 使用 Java Swing 撰寫,是 BaseX 的圖型界面管理工具,可以用來管理其資料庫以及測試一些 XQuery 語句。 basex 可以用來執行 BaseX 或者是 XQuery 的指令。

下面是一個使用 basex 的例子,用來解 1-9位數不重複印出來的練習問題,
使用者輸入1 印1-9
使用者輸入2 印1-98 (11, 22, 33等重複的不印)
使用者輸入3 印1-987 (121, 988, 667等有重複的不印)

basex -bnumber=9 -Qcount.xq

參數 -b 用來設定外部變數。count.xq 的內容如下:

declare variable $number external;
if (xs:integer($number) lt 1 or xs:integer($number) gt 9) then (
    <error>
      <message>Invalid data</message>
    </error>
) else (
    let $max := xs:integer(math:exp10($number) - 1)
    for $i in 1 to $max
    return
    if(not(matches(fn:string($i), '1.*1|2.*2|3.*3|4.*4|5.*5|6.*6|7.*7|8.*8|9.*9|0.*0'))) then (
        $i
    )
)

相關連結

XQuery 學習筆記

XML 簡介

XML stands for Extensible Markup Language. It is a text-based markup language derived from Standard Generalized Markup Language (SGML).

XML is a markup language that defines set of rules for encoding documents in a format that is both human-readable and machine-readable. Following example shows how XML markup looks, when embedded in a piece of text −

<message>
   <text>Hello, world!</text>
</message>

You can notice there are two kinds of information in the above example −

  • Markup (tag)
  • The text, or the character data

XML 文件可以使用樹 (tree) 來表示。一個 XML 樹開始於 root element, 並且從 root element 開始其 child elements 的分支。 XML elements 可以有屬性 (attributes),例如下面的例子:

<note date="2023/04/28">
  <name>Orange</name>
</note> 

The XML document can optionally have an XML declaration. It is written as follows −

<?xml version = "1.0" encoding = "UTF-8"?>

XPath 簡介

XPath (XML Path Language) is an expression language designed to support the query or transformation of XML documents. It was defined by the World Wide Web Consortium (W3C) in 1999.

In XPath, there are seven kinds of nodes: element, attribute, text, namespace, processing-instruction, comment, and root nodes.

The most important kind of expression in XPath is a location path. A location path consists of a sequence of location steps. Each location step has three components:

  • an axis
  • a node test
  • zero or more predicates.
Axis specifiers in XPath
Full syntax Abbreviated syntax Notes
ancestor
ancestor-or-self
attribute @ @abc is short for attribute::abc
child xyz is short for child::xyz
descendant
descendant-or-self // // is short for /descendant-or-self::node()/
following
following-sibling
namespace
parent .. .. is short for parent::node()
preceding
preceding-sibling
self . . is short for self::node()

Node tests may consist of specific node names or more general expressions. In the case of an XML document in which the namespace prefix gs has been defined, //gs:enquiry will find all the enquiry elements in that namespace, and //gs:* will find all elements, regardless of local name, in that namespace.

Other node test formats are:

comment()
finds an XML comment node, e.g. <!-- Comment -->
text()
finds a node of type text excluding any children, e.g. the hello in <k>hello<m> world</m></k>
processing-instruction()
finds XML processing instructions such as <?php echo $a; ?>. In this case, processing-instruction('php') would match.
node()
finds any node at all.

Predicates, written as expressions in square brackets, can be used to filter a node-set according to some condition. For example, a returns a node-set (all the a elements which are children of the context node), and a[@href='help.php'] keeps only those elements having an href attribute with the value help.php.

XQuery 簡介

XQuery 是由 W3C 定義的查詢語言,程式風格為函數式程式設計 (functional programming), 建立在 XPath 的基礎上,使用 Xpath 表達要查詢的路徑資訊, 專門用於在結構化或半結構化 XML 資料中進行搜尋、操作和轉換,類似 SQL 之於關聯式資料庫。 在 XQuery 3.1 增加了對於 JSON 的支援,所以 XQuery 也可以用來處理 JSON 資料(如果你想要這樣做的話)。

目前 XQuery 的版本如下:

  • XQuery 1.0 became a W3C Recommendation on January 23, 2007
  • XQuery 3.0 became a W3C Recommendation on April 8, 2014
  • XQuery 3.1 became a W3C Recommendation on March 21, 2017

XQuery 所有用於執行計算的 XQuery 語句都是表達式 (expressions),其核心是 FLWOR,用於更複雜的查詢:

  • For: 循環存取資料項目。
  • Let: 賦值。
  • Where: 設定篩選條件。
  • Order By: 排序結果。
  • Return: 定義輸出結果。

下面是一個 XQuery 的 Hello World 例子:

let $message := 'Hello World!'
return
<results>
   <message>{$message}</message>
</results>

下面是一個 XML 檔案 books.xml:

<?xml version="1.0" encoding="UTF-8"?>
<books>
   
   <book category="JAVA">
      <title lang="en">Learn Java in 24 Hours</title>
      <author>Robert</author>
      <year>2005</year>
      <price>30.00</price>
   </book>
   
   <book category="DOTNET">
      <title lang="en">Learn .Net in 24 hours</title>
      <author>Peter</author>
      <year>2011</year>
      <price>70.50</price>
   </book>
   
   <book category="XML">
      <title lang="en">Learn XQuery in 24 hours</title>
      <author>Robert</author>
      <author>Peter</author> 
      <year>2013</year>
      <price>50.00</price>
   </book>
   
   <book category="XML">
      <title lang="en">Learn XPath in 24 hours</title>
      <author>Jay Ban</author>
      <year>2010</year>
      <price>16.50</price>
   </book>
   
</books>

XQuery 可以使用 doc() 函數取得 XML 檔案的內容。下面就是一個 XQuery 的例子:

(: XQuery Comment :)
let $books := (doc("books.xml")/books/book)
return <results>
{
   for $x in $books
   where $x/price>30
   order by $x/price
   return $x/title
}
</results>

XQuery 可以使用 for 執行迴圈任務,如下面所示:

for $n in 1 to 10
return
    <result>{$n}</result>

Sequences represent an ordered collection of items where items can be of similar or of different types. Sequences are created using parenthesis with strings inside quotes or double quotes and numbers as such. XML elements can also be used as the items of a sequence.

Viewing items in a sequence

let $sequence := ('a', 'b', 'c', 'd', 'e', 'f')
let $count := count($sequence)
return
   <results>
      <count>{$count}</count>
      <items>
       {
         for $item in $sequence
         return
           <item>{$item}</item>
       }
      </items>
   </results>

XQuery 內建支援 Regular Expressions,下面是一個例子:

let $input := 'TutorialsPoint Simply Easy Learning'
return (
  matches($input, 'Hello') =  true(),
  matches($input, 'T.* S.* E.* L.*') =  true()
)

XQuery 使用 if-then-else 支援條件判斷。

<result>
{
   for $book in doc("books.xml")/books/book
   return
   if ($book/@category = "XML") then (
     $book/title
   )
}
</result>

XQuery 3.0 加入 lambda functions 的支援,下面是一個例子:

let $fn := function($x, $y) { $x + $y }
return $fn(99, 2)

XQuery 3.0 加入 switch 的支援,下面是一個例子:

for $fruit in ("Apple", "Pear", "Peach")
return switch ($fruit)
  case "Apple" return "red"
  case "Pear"  return "green"
  case "Peach" return "pink"
  default      return "unknown"

XQuery 3.0 加入 try catch 的支援,下面是一個例子:

try {
  1 + '2'
} catch * {
  'Error [' || $err:code || ']: ' || $err:description
}

XQuery 3.0 加入 || operator 作為 String Concatenations 使用,其實際上為 concat() 函數的快捷方式。

'Hello' || ' ' || 'Universe'

XQuery 3.0 加入 Simple Map Operator !,用於將第一個表達式的結果應用於第二個表達式,下面是一個例子:

(1 to 10) ! element node { . }

XQuery 3.1 加入 Arrow Operator operator =>,提供了一種方便的替代語法,用於將函數傳遞給值。 運算子前面的表達式將提供作為箭頭後面函數的第一個參數。

'w e l c o m e' => upper-case() => tokenize() => string-join('-')

下面則是沒有 Arrow Operator operator 之前的寫法:

string-join(tokenize(upper-case('w e l c o m e')), '-')

XQuery 3.1 加入了 Map 與 Array 支援對於 JSON 資料格式的處理。 Map 是將一組鍵與值關聯起來的函數,從而產生一組鍵/值對,用來處理 JSON 的 object。 Array 是將一組位置(以正整數表示的鍵)與值關聯起來的功能。Array 中的第一個位置對應整數 1,用來處理 JSON 的 array。

let $map := map { 'foo': 42, 'bar': 'baz', 123: 456 }
return for-each(map:keys($map), $map)
let $array := array { 48 to 52 }
for $i in 1 to array:size($array)
return $array($i)

Lookup operator 提供了一種語法糖,用於存取 Map 或 Array 元素的值。它以問號 (?) 開頭,後面跟著一個說明符。說明符可以是:

  1. A wildcard *,
  2. The name of the key,
  3. The integer offset, or
  4. Any other parenthesized expression.
let $map := map { 'R': 'red', 'G': 'green', 'B': 'blue' }
return (
  $map?*           (: returns all values; same as: map:keys($map) ! $map(.) :),
  $map?R           (: returns the value for key 'R'; same as: $map('R') :),
  $map?('G', 'B')  (: returns the values for key 'G' and 'B' :)
)
let $maps := (
  map { 'name': 'Guðrún', 'city': 'Reykjavík' },
  map { 'name': 'Hildur', 'city': 'Akureyri' }
)
return $maps[?name = 'Hildur'] ?city

XQuery 3.1 提供了 JSON Serialization,下面是一個例子:

declare option output:method 'json';
map { "key": "value" }

XQuery 3.1 使用 fn:parse-json() 執行 JSON deserialization 的工作:

let $json-input := '{ "firstName": "John", "lastName": "Smith", "address": { "city": "New York" }, "phoneNumbers": ["212-732-1234", "646-123-4567"] }'
let $json-data := fn:parse-json($json-input)
return
  $json-data

XQuery 3.1 支援讀取外部的 JSON 文件檔案。

let $json-data := fn:json-doc("/path/to/data.json")
return $json-data

也可以將 JSON 轉換為 XML 文件:

let $json-string := '{ "name": "John", "age": 30, "city": "New York" }'
return fn:json-to-xml($json-string)

參考資料

2025/11/29

Boost.JSON

Boost.JSON 是一個 C++ JSON parser 函式庫, 提供了雖然不是最快但是也足夠快的執行效率、以及雖然不是最方便但是足以滿足使用者需要的便利使用方式, 就綜合條件來說,我認為是十分優秀的 C++ JSON parser 函式庫。 他有二個使用方式,第一種需要連結函式庫:

#include <boost/json.hpp>

第二種是 header-only:

#include <boost/json/src.hpp>

下面是從一個字串分析 JSON 的測試:

#include <boost/json.hpp>
#include <iostream>
#include <string>

namespace json = boost::json;

int main() {
    const std::string json_str = R"(
        {
            "user": "johndoe",
            "id": 12345,
            "active": true,
            "numbers": [1, 2, 3, 4, 5]
        }
    )";

    // Parse the JSON string
    json::value data = json::parse(json_str);

    // Access the values
    std::string username = json::value_to<std::string>(data.at("user"));
    int user_id = json::value_to<int>(data.at("id"));
    bool is_active = json::value_to<bool>(data.at("active"));

    std::cout << "Username: " << username << std::endl;
    std::cout << "ID: " << user_id << std::endl;
    std::cout << "Active: " << (is_active ? "Yes" : "No") << std::endl;

    // For array
    json::array &arr = data.at("numbers").as_array();
    std::vector<int> numbers;
    for (auto const &value : arr) {
        numbers.push_back(json::value_to<int>(value));
    }

    std::cout << "Parsed Numbers: ";
    for (int num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

使用 CMake 編譯,CMakeLists.txt 的內容如下:

cmake_minimum_required(VERSION 3.18)

project(parse)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)

find_package(Boost 1.89.0 REQUIRED CONFIG COMPONENTS json)

add_executable(parse parse.cpp)
target_link_libraries(parse PRIVATE Boost::json)

如果採用 header-only 的方式,CMakeLists.txt 的內容如下:

cmake_minimum_required(VERSION 3.18)

project(parse)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)

find_package(Boost 1.89.0 REQUIRED CONFIG COMPONENTS)

add_executable(parse parse.cpp)
target_link_libraries(parse)

下面是建立 JSON 內容的測試:

#include <boost/json.hpp>
#include <iostream>
#include <string>

namespace json = boost::json;

int main() {
    // Create a JSON object
    json::object obj;
    obj["user"] = "johndoe";
    obj["id"] = 12345;
    obj["active"] = true;

    // Create a JSON array
    json::array numbers;
    numbers.push_back(1);
    numbers.push_back(2);
    numbers.push_back(3);
    numbers.push_back(4);
    numbers.push_back(5);

    obj["numbers"] = numbers;

    // Serialize the object to a string
    std::string serialized_json = json::serialize(obj);

    std::cout << "Generated JSON: " << serialized_json << std::endl;

    return 0;
}

參考連結

Asio C++ Library

Asio C++ Library 是一個免費、開放原始碼、跨平台的 C++ 網路程式庫。 它為開發者提供一致的非同步 I/O 模型(包含 Timer、File、Pipe、Serial Port 以及網路協定 TCP, UDP 與 ICMP), Boost.Asio 在 20 天的審查後,於 2005 年 12 月 30 日被 Boost 函式庫接納。 目前 Asio C++ Library 提供二種函式庫,一種可以獨立使用的 Asio C++ library,一種是與 Boost 函式庫整合的 Boost.Asio, 二種函式庫的核心相同,差別在於 Boost.Asio 跟隨 Boost 函式庫的發佈時程(這表示當 bugs 修正的時候, 有時候會慢一點才會隨著 Boost 的新版更正)。因為已經有安裝 Boost 函式庫,所以我使用的是 Boost.Asio。

Asio 在設計上使用 Proactor pattern。 Proactor 是一種用於事件處理的軟體設計模式,其中耗時較長的活動在非同步部分運行(在 Asio 就是 I/O 處理的部份)。 非同步部分終止後,會呼叫完成處理程序。 所有使用 asio 的程式都需要至少一個 I/O execution context,例如 io_context 或 thread_pool 物件。 I/O execution context 提供對 I/O 功能的存取。如果是非同步的操作,那麼需要實作 completion handler 來提供工作完成之後的通知目標。

下面是一個測試的程式,來自 Asio 教學網頁的 Using a timer synchronously。 boost::asio::io_context 就是執行 I/O 的部份。

#include <boost/asio.hpp>
#include <iostream>

int main() {
    boost::asio::io_context io;

    boost::asio::steady_timer t(io, boost::asio::chrono::seconds(3));
    t.wait();

    std::cout << "Hello, world!" << std::endl;

    return 0;
}

使用 CMake 編譯,CMakeLists.txt 的內容如下:

cmake_minimum_required(VERSION 3.18)

project(timer)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)

find_package(Boost 1.89.0 REQUIRED CONFIG COMPONENTS)

add_executable(timer timer.cpp)

Using a timer asynchronously

使用 asio 的非同步功能意味著需要一個 completion token,該 token 決定了非同步操作完成後如何將結果傳遞給完成處理程序。 在這裡使用 print 函數,該函數將在非同步等待結束後被呼叫。

務必記住,在呼叫 boost::asio::io_context::run() 之前,要先給 io_context 一些工作。 如果沒指定一些工作(在本例中是 steady_timer::async_wait()),boost::asio::io_context::run() 會立即返回。

#include <boost/asio.hpp>
#include <iostream>

void print(const boost::system::error_code & /*e*/) {
    std::cout << "Hello, world!" << std::endl;
}

int main() {
    boost::asio::io_context io;

    boost::asio::steady_timer t(io, boost::asio::chrono::seconds(3));
    t.async_wait(&print);

    io.run();

    return 0;
}

Binding arguments to a completion handler

要使用 asio 實作重複定時器,需要在完成處理程序中更改定時器的過期時間,然後啟動新的非同步等待。 這意味著 completion handler 需要能夠存取定時器物件。

#include <boost/asio.hpp>
#include <functional>
#include <iostream>

void print(const boost::system::error_code & /*e*/,
           boost::asio::steady_timer *t, int *count) {
    if (*count < 5) {
        std::cout << *count << std::endl;
        ++(*count);

        t->expires_at(t->expiry() + boost::asio::chrono::seconds(1));
        t->async_wait(
            std::bind(print, boost::asio::placeholders::error, t, count));
    }
}

int main() {
    boost::asio::io_context io;

    int count = 0;
    boost::asio::steady_timer t(io, boost::asio::chrono::seconds(1));
    t.async_wait(
        std::bind(print, boost::asio::placeholders::error, &t, &count));

    io.run();

    std::cout << "Final count is " << count << std::endl;

    return 0;
}

Using a member function as a completion handler

std::bind 函式對類別成員函式和函式同樣有效。由於所有非靜態類別成員函數都有一個隱式的 this 參數,我們需要將 this 綁定到函數上。 std::bind 將我們的 completion handler(現在是成員函數)轉換為函數對象。

#include <boost/asio.hpp>
#include <functional>
#include <iostream>

class printer {
public:
    printer(boost::asio::io_context &io)
        : timer_(io, boost::asio::chrono::seconds(1)), count_(0) {
        timer_.async_wait(std::bind(&printer::print, this));
    }

    ~printer() { std::cout << "Final count is " << count_ << std::endl; }

    void print() {
        if (count_ < 5) {
            std::cout << count_ << std::endl;
            ++count_;

            timer_.expires_at(timer_.expiry() +
                              boost::asio::chrono::seconds(1));
            timer_.async_wait(std::bind(&printer::print, this));
        }
    }

private:
    boost::asio::steady_timer timer_;
    int count_;
};

int main() {
    boost::asio::io_context io;
    printer p(io);
    io.run();

    return 0;
}

Synchronising completion handlers in multithreaded programs

strand class template 是 executor adapter,它保證透過它分發的處理程序,在下一個處理程序啟動之前, 目前正在執行的處理程序必須完成。無論呼叫 boost::asio::io_context::run() 的執行緒數是多少,此保證都有效。 當然,這些處理程序仍然可能與其他未透過 strand 分發的處理程序,或透過不同 strand 物件分發的處理程序並發執行。


#include <boost/asio.hpp>
#include <functional>
#include <iostream>
#include <thread>

class printer {
public:
    printer(boost::asio::io_context &io)
        : strand_(boost::asio::make_strand(io)),
          timer1_(io, boost::asio::chrono::seconds(1)),
          timer2_(io, boost::asio::chrono::seconds(1)), count_(0) {
        timer1_.async_wait(boost::asio::bind_executor(
            strand_, std::bind(&printer::print1, this)));

        timer2_.async_wait(boost::asio::bind_executor(
            strand_, std::bind(&printer::print2, this)));
    }

    ~printer() { std::cout << "Final count is " << count_ << std::endl; }

    void print1() {
        if (count_ < 10) {
            std::cout << "Timer 1: " << count_ << std::endl;
            ++count_;

            timer1_.expires_at(timer1_.expiry() +
                               boost::asio::chrono::seconds(1));

            timer1_.async_wait(boost::asio::bind_executor(
                strand_, std::bind(&printer::print1, this)));
        }
    }

    void print2() {
        if (count_ < 10) {
            std::cout << "Timer 2: " << count_ << std::endl;
            ++count_;

            timer2_.expires_at(timer2_.expiry() +
                               boost::asio::chrono::seconds(1));

            timer2_.async_wait(boost::asio::bind_executor(
                strand_, std::bind(&printer::print2, this)));
        }
    }

private:
    boost::asio::strand<boost::asio::io_context::executor_type> strand_;
    boost::asio::steady_timer timer1_;
    boost::asio::steady_timer timer2_;
    int count_;
};

int main() {
    boost::asio::io_context io;
    printer p(io);
    std::thread t([&] { io.run(); });
    io.run();
    t.join();

    return 0;
}

File

Linux io_uring 在 Kernel 5.1 加入,其主要目標是透過高效率的非同步 I/O 框架,解決傳統 I/O 模型中系統呼叫和上下文切換的效能瓶頸, 移除傳統同步I/O 與 epoll 就緒通知模型需要頻繁切換使用者空間與核心空間的負擔,進而大幅提升系統在處理大量並發 I/O 操作時的效能。 liburing 是 Jens Axboe 維護的輔助函式庫,其主要目的是簡化 io_uring 的使用。 Asio 對於 Linux liburing 提供了包裝(目前需要使用者使用 flag 啟用),下面是我測試的程式, 讀取 /etc/os-release 取得 Linux Distribution Name:

#include <boost/asio.hpp>
#include <boost/asio/stream_file.hpp>
#include <filesystem>
#include <iostream>
#include <vector>

namespace asio = boost::asio;
namespace fs = std::filesystem;

std::vector<std::string> split(const std::string &str,
                               const std::string &delim) {
    std::vector<std::string> tokens;
    size_t prev = 0, pos = 0;
    do {
        pos = str.find(delim, prev);
        if (pos == std::string::npos)
            pos = str.length();
        std::string token = str.substr(prev, pos - prev);
        if (!token.empty())
            tokens.push_back(token);
        prev = pos + delim.length();
    } while (pos < str.length() && prev < str.length());

    return tokens;
}

void read_next_line(asio::stream_file &file, asio::streambuf &buffer) {
    asio::async_read_until(file, buffer, '\n',
                           [&](const boost::system::error_code &ec,
                               std::size_t bytes_transferred) {
                               if (!ec) {
                                   std::istream is(&buffer);
                                   std::string line;
                                   std::getline(is, line);

                                   auto splitArray = split(line, "=");
                                   if (splitArray[0].compare("NAME") == 0) {
                                       std::cout << splitArray[1] << std::endl;
                                   } else {
                                       read_next_line(file, buffer);
                                   }
                               } else if (ec == asio::error::eof) {
                                   std::cout << "End of file reached."
                                             << std::endl;
                               } else {
                                   std::cerr
                                       << "Error reading file: " << ec.message()
                                       << std::endl;
                               }
                           });
}

int main() {
    fs::path test_file_path = "/etc/os-release";

    asio::io_context io_context;

    boost::system::error_code ec_open;
    asio::stream_file file(io_context);
    file.open(test_file_path.string(), asio::stream_file::read_only, ec_open);

    if (ec_open) {
        std::cerr << "Failed to open file: " << ec_open.message() << std::endl;
        return 1;
    }

    asio::streambuf buffer;
    read_next_line(file, buffer);

    io_context.run();
    file.close();

    return 0;
}

使用 CMake 編譯,CMakeLists.txt 的內容如下:

cmake_minimum_required(VERSION 3.18)

project(name)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)

find_package(PkgConfig REQUIRED)
pkg_check_modules(uring REQUIRED IMPORTED_TARGET liburing)

find_package(Boost 1.89.0 REQUIRED CONFIG COMPONENTS)

add_executable(name name.cpp)
target_link_libraries(name PRIVATE PkgConfig::uring)
target_compile_definitions(name PRIVATE BOOST_ASIO_HAS_IO_URING BOOST_ASIO_DISABLE_EPOLL)

Tcp

A synchronous TCP daytime client

我們需要將作為參數傳遞給應用程式的伺服器名稱轉換為 TCP 端點。為此,我們使用 ip::tcp::resolver 物件。 resolver 接收主機名稱和服務名,並將它們轉換為端點列表。 程式接下來建立並連接 Socket。上面獲得的端點列表可能同時包含 IPv4 和 IPv6 端點,因此我們需要逐一嘗試,直到找到可用的端點。 這樣可以確保客戶端程式與特定的 IP 版本無關。boost::asio::connect() 函數會自動執行此操作。

#include <array>
#include <boost/asio.hpp>
#include <iostream>

namespace asio = boost::asio;

int main(int argc, char *argv[]) {
    try {
        if (argc != 2) {
            std::cerr << "Usage: client <host>" << std::endl;
            return 1;
        }

        asio::io_context io_context;

        asio::ip::tcp::resolver resolver(io_context);
        asio::ip::tcp::resolver::results_type endpoints =
           resolver.resolve(argv[1], "daytime");

        asio::ip::tcp::socket socket(io_context);
        asio::connect(socket, endpoints);

        for (;;) {
            std::array<char, 128> buf;
            boost::system::error_code error;

            size_t len = socket.read_some(asio::buffer(buf), error);

            if (error == asio::error::eof)
                break; // Connection closed cleanly by peer.
            else if (error)
                throw boost::system::system_error(error); // Some other error.

            std::cout.write(buf.data(), len);
        }
    } catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

A synchronous TCP daytime server

需要建立一個 ip::tcp::acceptor 物件來監聽新連線。它被初始化為監聽 TCP 連接埠 13,支援 IP 版本 6。

#include <boost/asio.hpp>
#include <ctime>
#include <iostream>
#include <string>

namespace asio = boost::asio;

std::string make_daytime_string() {
    std::time_t now = std::time(0);
    return std::ctime(&now);
}

int main() {
    try {
        asio::io_context io_context;

        asio::ip::tcp::acceptor acceptor(
            io_context, asio::ip::tcp::endpoint(asio::ip::tcp::v6(), 13));

        for (;;) {
            asio::ip::tcp::socket socket(io_context);
            acceptor.accept(socket);

            std::string message = make_daytime_string();

            boost::system::error_code ignored_error;
            asio::write(socket, boost::asio::buffer(message), ignored_error);
        }
    } catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

An asynchronous TCP daytime server

#include <boost/asio.hpp>
#include <ctime>
#include <functional>
#include <iostream>
#include <memory>
#include <string>

namespace asio = boost::asio;

std::string make_daytime_string() {
    std::time_t now = std::time(0);
    return std::ctime(&now);
}

class tcp_connection : public std::enable_shared_from_this<tcp_connection> {
public:
    typedef std::shared_ptr<tcp_connection> pointer;

    static pointer create(asio::io_context &io_context) {
        return pointer(new tcp_connection(io_context));
    }

    asio::ip::tcp::socket &socket() { return socket_; }

    void start() {
        message_ = make_daytime_string();

        asio::async_write(socket_, asio::buffer(message_),
                          std::bind(&tcp_connection::handle_write,
                                    shared_from_this(),
                                    asio::placeholders::error,
                                    asio::placeholders::bytes_transferred));
    }

private:
    tcp_connection(asio::io_context &io_context) : socket_(io_context) {}

    void handle_write(const boost::system::error_code & /*error*/,
                      size_t /*bytes_transferred*/) {}

    asio::ip::tcp::socket socket_;
    std::string message_;
};

class tcp_server {
public:
    tcp_server(asio::io_context &io_context)
        : io_context_(io_context),
          acceptor_(io_context,
                    asio::ip::tcp::endpoint(asio::ip::tcp::v6(), 13)) {
        start_accept();
    }

private:
    void start_accept() {
        tcp_connection::pointer new_connection =
            tcp_connection::create(io_context_);

        acceptor_.async_accept(new_connection->socket(),
                               std::bind(&tcp_server::handle_accept, this,
                                         new_connection,
                                         asio::placeholders::error));
    }

    void handle_accept(tcp_connection::pointer new_connection,
                       const boost::system::error_code &error) {
        if (!error) {
            new_connection->start();
        }

        start_accept();
    }

    asio::io_context &io_context_;
    asio::ip::tcp::acceptor acceptor_;
};

int main() {
    try {
        asio::io_context io_context;
        tcp_server server(io_context);
        io_context.run();
    } catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

下面是我的練習程式,將 client 改寫為 asynchronous:

#include <boost/asio.hpp>
#include <iostream>
#include <vector>

namespace asio = boost::asio;

const int BUFFER_SIZE = 128;

void handle_read(const boost::system::error_code &error,
                 std::size_t bytes_transferred, asio::ip::tcp::socket &socket,
                 std::vector<char> &buffer) {
    if (!error) {
        for (std::size_t i = 0; i < bytes_transferred; ++i) {
            std::cout << buffer[i];
        }
    } else {
        std::cerr << "Error during read: " << error.message() << std::endl;
    }
}

int main(int argc, char *argv[]) {
    try {
        if (argc != 2) {
            std::cerr << "Usage: client <host>" << std::endl;
            return 1;
        }

        asio::io_context io_context;

        asio::ip::tcp::resolver resolver(io_context);
        asio::ip::tcp::resolver::results_type endpoints =
            resolver.resolve(argv[1], "daytime");

        asio::ip::tcp::socket socket(io_context);
        asio::connect(socket, endpoints);

        std::vector<char> buffer(BUFFER_SIZE);
        socket.async_read_some(asio::buffer(buffer),
                               std::bind(handle_read, std::placeholders::_1,
                                         std::placeholders::_2,
                                         std::ref(socket), std::ref(buffer)));

        io_context.run();
        socket.close();
    } catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

UDP

A synchronous UDP daytime client

我們使用 ip::udp::resolver 物件,根據主機名稱和服務名稱尋找要使用的正確遠端端點。 透過 ip::udp::v6() 參數,查詢被限制為僅傳回 IPv6 端點。 如果 ip::udp::resolver::resolve()函數沒有失敗,則保證至少會傳回清單中的一個端點。這意味著直接解引用回傳值是安全的。

#include <boost/asio.hpp>
#include <array>
#include <iostream>

namespace asio = boost::asio;

int main(int argc, char *argv[]) {
    try {
        if (argc != 2) {
            std::cerr << "Usage: client <host>" << std::endl;
            return 1;
        }

        asio::io_context io_context;

        asio::ip::udp::resolver resolver(io_context);
        asio::ip::udp::endpoint receiver_endpoint =
            *resolver.resolve(asio::ip::udp::v6(), argv[1], "daytime").begin();

        asio::ip::udp::socket socket(io_context);
        socket.open(asio::ip::udp::v6());

        std::array<char, 1> send_buf = {{0}};
        socket.send_to(asio::buffer(send_buf), receiver_endpoint);

        std::array<char, 128> recv_buf;
        asio::ip::udp::endpoint sender_endpoint;
        size_t len =
            socket.receive_from(asio::buffer(recv_buf), sender_endpoint);

        std::cout.write(recv_buf.data(), len);
    } catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

A synchronous UDP daytime server

#include <boost/asio.hpp>
#include <array>
#include <ctime>
#include <iostream>
#include <string>

namespace asio = boost::asio;

std::string make_daytime_string() {
    std::time_t now = std::time(0);
    return std::ctime(&now);
}

int main() {
    try {
        asio::io_context io_context;

        asio::ip::udp::socket socket(
            io_context, asio::ip::udp::endpoint(asio::ip::udp::v6(), 13));

        for (;;) {
            std::array<char, 1> recv_buf;
            asio::ip::udp::endpoint remote_endpoint;
            socket.receive_from(asio::buffer(recv_buf), remote_endpoint);

            std::string message = make_daytime_string();

            boost::system::error_code ignored_error;
            socket.send_to(asio::buffer(message), remote_endpoint, 0,
                           ignored_error);
        }
    } catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

An asynchronous UDP daytime server

#include <boost/asio.hpp>
#include <array>
#include <ctime>
#include <functional>
#include <iostream>
#include <memory>
#include <string>

namespace asio = boost::asio;

std::string make_daytime_string() {
    std::time_t now = std::time(0);
    return std::ctime(&now);
}

class udp_server {
public:
    udp_server(asio::io_context &io_context)
        : socket_(io_context,
                  asio::ip::udp::endpoint(asio::ip::udp::v6(), 13)) {
        start_receive();
    }

private:
    void start_receive() {
        socket_.async_receive_from(
            asio::buffer(recv_buffer_), remote_endpoint_,
            std::bind(&udp_server::handle_receive, this,
                      asio::placeholders::error,
                      asio::placeholders::bytes_transferred));
    }

    void handle_receive(const boost::system::error_code &error,
                        std::size_t /*bytes_transferred*/) {
        if (!error) {
            std::shared_ptr<std::string> message(
                new std::string(make_daytime_string()));

            socket_.async_send_to(
                asio::buffer(*message), remote_endpoint_,
                std::bind(&udp_server::handle_send, this, message,
                          asio::placeholders::error,
                          asio::placeholders::bytes_transferred));

            start_receive();
        }
    }

    void handle_send(std::shared_ptr<std::string> /*message*/,
                     const boost::system::error_code & /*error*/,
                     std::size_t /*bytes_transferred*/) {}

    asio::ip::udp::socket socket_;
    asio::ip::udp::endpoint remote_endpoint_;
    std::array<char, 1> recv_buffer_;
};

int main() {
    try {
        asio::io_context io_context;
        udp_server server(io_context);
        io_context.run();
    } catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

下面是我的練習程式,將 client 改寫為 asynchronous:

#include <array>
#include <boost/asio.hpp>
#include <iostream>

namespace asio = boost::asio;

class udp_client {
public:
    udp_client(asio::io_context &io_context, const std::string &host)
        : socket_(io_context) {
        asio::ip::udp::resolver resolver(io_context);
        remote_endpoint_ =
            *resolver.resolve(asio::ip::udp::v6(), host, "daytime").begin();

        socket_.open(asio::ip::udp::v6());

        start_send();
    }

private:
    void start_send() {
        socket_.async_send_to(asio::buffer(send_buffer_), remote_endpoint_,
                              std::bind(&udp_client::handle_send, this,
                                        asio::placeholders::error,
                                        asio::placeholders::bytes_transferred));
    }

    void handle_send(const boost::system::error_code &error,
                     std::size_t /*bytes_transferred*/) {
        if (!error) {
            socket_.async_receive_from(
                asio::buffer(recv_buffer_), remote_endpoint_,
                std::bind(&udp_client::handle_receive, this,
                          asio::placeholders::error,
                          asio::placeholders::bytes_transferred));
        } else {
            std::cerr << "Error during send: " << error.message() << std::endl;
        }
    }

    void handle_receive(const boost::system::error_code &error,
                        std::size_t bytes_transferred) {

        if (!error) {
            std::cout.write(recv_buffer_.data(), bytes_transferred);
        } else {
            std::cerr << "Error during receive: " << error.message()
                      << std::endl;
        }
    }

    asio::ip::udp::socket socket_;
    asio::ip::udp::endpoint remote_endpoint_;
    std::array<char, 1> send_buffer_ = {{0}};
    std::array<char, 128> recv_buffer_;
};

int main(int argc, char *argv[]) {
    try {
        if (argc != 2) {
            std::cerr << "Usage: client <host>" << std::endl;
            return 1;
        }

        asio::io_context io_context;

        udp_client client(io_context, argv[1]);

        io_context.run();
    } catch (std::exception &e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

相關連結

2025/11/07

SOCI

SOCI (Simple Oracle Call Interface) 一開始是由 Maciej Sobczak 在 CERN 工作時開發, 作為 Oracle 資料庫函式庫的 abstraction layer 並且在 CERN 的工作環境中使用,之後則又加入了數個資料庫的支援。

SOCI 目前支援 Oracle, MySQL, PostgreSQL, SQLite3 等資料庫以及 ODBC 作為通用的 backend。 下面是在 openSUSE 安裝 SOCI core 以及 SOCI SQLite3 開發檔案的指令:
sudo zypper in soci-devel soci-sqlite3-devel

(注意:SOCI 本身的體積並不大,但是安裝時需要 boost-devel,而 boost-devel 是個包含很多模組的函式庫, 如果之前就有安裝 boost-devel 那麼這就不是一個問題)

下面是一個使用 SOCI 取得 SQLite3 版本的測試程式:

#include <soci/soci.h>
#include <soci/sqlite3/soci-sqlite3.h>

#include <iostream>
#include <string>

int main() {
    try {
        soci::session sql("sqlite3", "db=:memory:");

        std::string version;
        sql << "select sqlite_version()", soci::into(version);
        std::cout << version << std::endl;
    } catch (soci::soci_error const &e) {
        std::cerr << "Failed: " << e.what() << std::endl;
    } catch (std::runtime_error const &e) {
        std::cerr << "Unexpected standard exception occurred: " << e.what()
                  << std::endl;
    } catch (...) {
        std::cerr << "Unexpected unknown exception occurred." << std::endl;
    }

    return 0;
}

使用下列的指令編譯:

g++ version.cpp -lsoci_core -lsoci_sqlite3 -o version

下面是在 openSUSE 安裝 SOCI core 以及 SOCI ODBC 開發檔案的指令:

sudo zypper in soci-devel soci-odbc-devel

下面是一個使用 SOCI ODBC 取得 PostgreSQL 版本的測試程式:

#include <soci/odbc/soci-odbc.h>
#include <soci/soci.h>

#include <iostream>
#include <string>

int main() {
    try {
        soci::session sql("odbc",
                          "DSN=PostgreSQL; UID=postgres; PWD=postgres;");

        std::string version;
        soci::rowset<std::string> rs = (sql.prepare << "select version()");

        for (soci::rowset<std::string>::const_iterator it = rs.begin();
             it != rs.end(); ++it) {
            std::cout << *it << std::endl;
        }
    } catch (soci::soci_error const &e) {
        std::cerr << "Failed: " << e.what() << std::endl;
    } catch (std::runtime_error const &e) {
        std::cerr << "Unexpected standard exception occurred: " << e.what()
                  << std::endl;
    } catch (...) {
        std::cerr << "Unexpected unknown exception occurred." << std::endl;
    }

    return 0;
}

使用下列的指令編譯:

g++ version.cpp -lsoci_core -lsoci_odbc -o version

參考連結

2025/10/30

Beyond 國語歌曲歌單

歌單的歌曲不固定,這只是我現在的歌單。

  • 大地

    收錄在 1990 年發行的專輯《大地》中。劉卓輝作詞,黃家駒作曲,黃貫中擔任主唱,粵語版為《大地》。大地來自於黃家駒看到兩岸開放,國民黨老兵回到中國大陸探親的新聞報導,結果產生了相關的靈感。歌曲的主角可以視為在台灣的國民黨老兵,或者是因為戰亂而離開中國大陸,在改革開放後又重新回到中國大陸的老一輩,是一首十分有時代意義的歌曲。

  • 漆黑的空間

    收錄在 1990 年發行的專輯《大地》中。粵語版為《灰色軌跡》,由黃家駒主唱。

  • 短暫的溫柔

    收錄在 1990 年發行的專輯《大地》中。粵語版為《未曾後悔》,由黃貫中主唱。

  • 不需要太懂

    收錄在 1990 年發行的專輯《大地》中。粵語版為《是錯也再不分》,由黃家強主唱。

  • 九十年代的憂傷

    收錄在 1990 年發行的專輯《大地》中。粵語版為《相依的心》。

  • 你知道我的迷惘

    收錄在 1990 年發行的專輯《大地》中。由劉卓輝重新填詞,粵語版為《真的愛你》。

  • 送給不懂環保的人(包括我)

    收錄在 1990 年發行的專輯《大地》中。粵語版為《送給不知怎去保護環境的人(包括我)》。

  • 和自己的心比賽

    收錄在 1990 年發行的專輯《大地》中。粵語版為《戰勝心魔》。

  • 破壞遊戲的孩子

    收錄在 1990 年發行的專輯《大地》中。粵語版為《衝開一切》。

  • 今天有我

    收錄在 1990 年發行的專輯《大地》中。粵語版為《秘密警察》。

  • 光輝歲月

    收錄在 1991 年發行的專輯《光輝歲月》中。周治平/何啟弘作詞,黃家駒作曲,粵語版為《光輝歲月》。 主打歌《光輝歲月》的粵語版是一首讚美南非的非洲人國民大會主席納爾遜·曼德拉的歌曲,以歌頌他在南非種族隔離時期為黑人所付出的努力, 當時曼德拉在監禁 28 年後剛被釋放,光輝歲月表達他的一生。在國語版裡面,這首《光輝歲月》是為激勵年輕人努力拼搏而作, 而當中的種族議題被淡化。

  • 午夜怨曲

    收錄在 1991 年發行的專輯《光輝歲月》中。粵語版為《午夜怨曲》。

  • 射手座咒語

    收錄在 1991 年發行的專輯《光輝歲月》中。粵語版為《亞拉伯跳舞女郎》。

  • 歲月無聲

    收錄在 1991 年發行的專輯《光輝歲月》中。粵語版為《歲月無聲》。

  • 曾經擁有

    收錄在 1991 年發行的專輯《光輝歲月》中。粵語版為《曾是擁有》。

  • 心中的太陽

    收錄在 1991 年發行的專輯《光輝歲月》中。粵語版為《又是黃昏》。

  • 兩顆心

    收錄在 1991 年發行的專輯《光輝歲月》中。何惠晶作詞,黃家強作曲,粵語版為《兩顆心》。

  • 長城

    收錄在 1992 年發行的專輯《信念》中。詹德茂作詞,黃家駒作曲,粵語版為《長城》。喜多郎創作的前奏十分優秀,我非常喜歡的其中一首國語歌。

  • 關心永遠在

    收錄在 1992 年發行的專輯《信念》中。粵語版為《遙望》。

  • 愛過的罪

    收錄在 1992 年發行的專輯《信念》中,陳昇作詞,黃貫中作曲。粵語版為《繼續沉醉》。

  • 今天就做

    收錄在 1992 年發行的專輯《信念》中。狗毛作詞,黃家駒作曲,粵語版為《不可一世》。我非常喜歡的其中一首國語歌。

  • 農民

    收錄在 1992 年發行的專輯《信念》中。粵語版為《農民》。《農民》的內容大抵是描述一個中國農民的生活,如何在艱困的生活中逆境自強。但廣東話和國語的版本卻有著極大不同。廣東話版本是由劉卓輝填詞,是對山區農民生活的影射;而國語版則由姚若龍填詞,內容更為廣泛,是描述北方人重視固有生活的個性。這是 Beyond 創作中十分嚴肅的作品。

  • 最想念妳

    收錄在 1992 年發行的專輯《信念》中。粵語版為《早班火車》。

  • 可否衝破

    收錄在 1992 年發行的專輯《信念》中。粵語版為《可否衝破》。

  • 年輕

    收錄在 1992 年發行的專輯《信念》中。粵語版為《快樂王國》。

  • 愛不容易說

    收錄在 1993 年發行的專輯《海闊天空》中。《海闊天空》是 Beyond 發行的第 4 張國語專輯,1993 年 6 月 24 日黃家駒發生意外昏迷,6 月 30 日逝世, 原定計畫無法實現,滾石唱片公司以這張專輯保留黃家駒原聲以及詞曲創作來完成黃家駒未完成的計畫和心願。黃家駒逝世後, 其餘 3 名成員決定進入錄音室,由黃家強和黃貫中演唱完成計畫中的國語專輯。歌曲結合粵語專輯《樂與怒》和日語專輯《This Is Love 1》。 雖然維基百科上說這是第 4 張國語專輯,但我個人感覺更像是個精選輯。

  • 一輩子陪我走

    收錄在 1994 年發行的專輯《Paradise》中。《一輩子陪我走》則是以一個輕快的伴奏,高歌四子手足之情。

  • 無名英雄

    收錄在 1994 年發行的專輯《Paradise》中。鄭智化填詞,黃家強與黃貫中作曲,黃家強擔任主唱。

  • 溫柔殺手

    收錄在 1994 年發行的專輯《Paradise》中。厲曼婷填詞,黃家強作曲,黃家強擔任主唱。

  • 和平世界

    收錄在 1994 年發行的專輯《Paradise》中。Michael 填詞,黃家強作曲,黃家強擔任主唱。

  • 因為有你有我

    收錄在 1994 年發行的專輯《Paradise》中。厲曼婷填詞,黃家強作曲,黃家強擔任主唱。

  • 對嗎

    收錄在 1994 年發行的專輯《Paradise》中。

  • 唯一

    收錄在 1995 年發行的專輯《愛與生活》中。《唯一》《Love》《活得精彩》《夢》四首歌都是國語歌曲創作, 而只有《活得精彩》後來有錄製粵語版本。

  • Love

    收錄在 1995 年發行的專輯《愛與生活》中。

  • 最後的答案

    收錄在 1995 年發行的專輯《愛與生活》中,黃舒駿作詞,黃貫中作曲,黃貫中擔任主唱。

  • 活得精彩

    收錄在 1995 年發行的專輯《愛與生活》中。

  • 夢

    收錄在 1995 年發行的專輯《愛與生活》中。

  • 糊塗的人

    收錄在 1995 年發行的專輯《愛與生活》中。粵語版為《門外看》。

  • 荒謬世界

    收錄在 1995 年發行的專輯《愛與生活》中。粵語版為《教壞細路》。

  • 忘記你

    收錄在 1998 年發行的專輯《這裡那裡》中。

  • 想你

    收錄在 1998 年發行的專輯《這裡那裡》中。

  • 緩慢

    收錄在 1998 年發行的專輯《這裡那裡》中。

  • 候診室

    收錄在 1998 年發行的專輯《這裡那裡》中。粵語版為《不再猶豫》。我非常喜歡的其中一首國語歌。

  • 情人

    收錄在 1998 年發行的專輯《這裡那裡》中。

  • 熱情過後

    收錄在 1998 年發行的專輯《這裡那裡》中。

  • 十字路口

    收錄在 1998 年發行的專輯《這裡那裡》中。

  • Amani

    收錄在 1998 年發行的專輯《這裡那裡》中。

  • 命運是我家

    收錄在 1998 年發行的專輯《這裡那裡》中。《這裡那裡》當中有七首是把黃家駒的作品,重新填上國語詞及編曲, 《命運是我家》這首的編曲是保留黃家駒原曲,為的是要保留黃家駒彈奏吉他的部份,是對黃家駒的敬意與尊重。

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)

MESSAGE ( STATUS "  FLTK_FOUND :          " ${FLTK_FOUND} )

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

注意:如果自行編譯 FLTK,其預設安裝位置為 /usr/local,但是使用者可以設定其安裝位置。 下面是安裝到自己家目錄下子目錄的例子。

cmake .. -DCMAKE_INSTALL_PREFIX=/home/danilo/Programs/fltk

如果改變預設安裝位置,結果 CMake 無法找到 FLTK,可以嘗試設定 CMAKE_PREFIX_PATH。

cmake .. -DCMAKE_PREFIX_PATH=/home/danilo/Programs/fltk

也可以在 CMakeLists.txt 設定:

cmake_minimum_required(VERSION 3.15)

project(hello)

set(CMAKE_PREFIX_PATH "/home/danilo/Programs/fltk")
set(FLTK_DIR "/home/danilo/Programs/fltk"
CACHE FILEPATH "FLTK installation or build directory")

find_package(FLTK 1.4 CONFIG REQUIRED)

MESSAGE ( STATUS "  FLTK_FOUND :          " ${FLTK_FOUND} )

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

下面更新的 Hello 例子,加入了設定整體的前景顏色與背景顏色,並且使用 Fl::scheme 設定視窗元件的顯示方案。

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

int main(int argc, char **argv) {
    uchar r, g, b;
    Fl::get_color(FL_BLACK, r, g, b);
    Fl::foreground(r, g, b);
    Fl::get_color(FL_WHITE, r, g, b);
    Fl::background(r, g, b);
    Fl::get_color(FL_WHITE, r, g, b);
    Fl::background2(r, g, b);

    Fl::scheme("gtk+");

    Fl_Window *window = new Fl_Window(640, 480);

    Fl_Box *box = new Fl_Box(20, 40, 450, 100, "Hello, World!");
    box->box(FL_UP_BOX);
    box->color(FL_BLACK);
    box->labelfont(FL_BOLD + FL_ITALIC);
    box->labelsize(36);
    box->labeltype(FL_SHADOW_LABEL);
    box->labelcolor(FL_RED);

    Fl_Box *box2 = new Fl_Box(20, 180, 450, 100, "Fast Light Toolkit");
    box2->box(FL_NO_BOX);
    box2->labelsize(36);
    box2->labeltype(FL_SHADOW_LABEL);

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

FLTK 提供了一些顯示文字的視窗元件,下面是使用 Fl_Text_Display 的例子。

// Fl_Text_Display example. -erco
#include <FL/Fl.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Text_Display.H>

Fl_Double_Window *app_win = nullptr;
Fl_Text_Buffer *buff = nullptr;
Fl_Text_Display *disp = nullptr;

void my_build_app_window() {
    app_win = new Fl_Double_Window (640, 480, "Display");

    buff = new Fl_Text_Buffer();
    disp = new Fl_Text_Display(20, 20, 640 - 40, 480 - 40, "Display Text");
    disp->buffer(buff);
    app_win->resizable(*disp);

    buff->text("line 0\nline 1\nline 2\n"
               "line 3\nline 4\nline 5\n"
               "line 6\nline 7\nline 8\n"
               "line 9\nline 10\nline 11\n"
               "line 12\nline 13\nline 14\n"
               "line 15\nline 16\nline 17\n"
               "line 18\nline 19\nline 20\n"
               "line 21\nline 22\nline 23\n");
}

int main (int argc, char **argv) {
    my_build_app_window();
    app_win->show(argc, argv);
    return (Fl::run());
}

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();
}

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

FLTK 也可以使用 Lambda Expression 作為回呼函數,下面是改寫的版本:

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

int main(int argc, char **argv) {
    auto lambda = [](Fl_Widget *, void *) -> void {
        if (Fl::event() == FL_SHORTCUT && Fl::event_key() == FL_Escape)
            return;

        exit(0);
    };

    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(lambda);

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

下面是使用了 Fl_Check_Button 與 Fl_Output 的程式:

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

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

    auto lambda = [](Fl_Widget *w, void *data) -> void {
        Fl_Check_Button *button = static_cast<Fl_Check_Button *>(w);
        Fl_Output *output = static_cast<Fl_Output *>(data);

        if (button->value()) {
            output->value("Checkbox is ON");
            output->redraw();
        } else {
            output->value("Checkbox is OFF");
            output->redraw();
        }
    };

    Fl_Window *window = new Fl_Window(350, 250, "FLTK Example");

    Fl_Check_Button *check_button =
        new Fl_Check_Button(50, 50, 150, 30, "Enable Feature");

    Fl_Output *output = new Fl_Output(50, 100, 200, 30);
    output->align(FL_ALIGN_BOTTOM);
    output->tooltip("Display checkbox status.");
    output->value("Checkbox is OFF");

    check_button->callback(lambda, output);

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

下面是使用了 Fl_Round_Button 的程式:

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

void button_cb(Fl_Widget *w, void *data) {
    Fl_Round_Button *button = static_cast<Fl_Round_Button *>(w);
    const char *label = button->label();

    Fl_Box *output_box = static_cast<Fl_Box *>(data);
    if (button->value()) {
        output_box->copy_label(label);
    }
    output_box->redraw();
}

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

    Fl_Window *window = new Fl_Window(300, 200, "Example");

    Fl_Box *output_box = new Fl_Box(20, 150, 260, 30, "Selected: None");
    output_box->box(FL_FLAT_BOX);
    output_box->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
    output_box->labelcolor(FL_BLUE);

    Fl_Round_Button *button1 = new Fl_Round_Button(20, 20, 100, 30, "Option A");
    button1->type(FL_RADIO_BUTTON);
    button1->callback(button_cb, output_box);

    Fl_Round_Button *button2 = new Fl_Round_Button(20, 60, 100, 30, "Option B");
    button2->type(FL_RADIO_BUTTON);
    button2->callback(button_cb, output_box);

    Fl_Round_Button *button3 =
        new Fl_Round_Button(20, 100, 100, 30, "Option C");
    button3->type(FL_RADIO_BUTTON);
    button3->callback(button_cb, output_box);

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

下面是使用了 Fl_Slider 的程式:

#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Slider.H>
#include <FL/Fl_Window.H>
#include <iomanip>
#include <sstream>
#include <string>

void slider_callback(Fl_Widget *w, void *data) {
    Fl_Slider *slider = static_cast<Fl_Slider *> (w);
    double value = slider->value();

    std::stringstream stream;
    stream << std::fixed << std::setprecision(2) << value;
    std::string myvalue = stream.str();

    Fl_Box *box = static_cast<Fl_Box *> (data);
    box->copy_label(myvalue.c_str()); // Update label
    box->redraw(); // Update UI
}

int main() {
    Fl::scheme("gtk+");
    Fl_Window *window = new Fl_Window(400, 250, "Example");

    Fl_Slider *slider = new Fl_Slider(50, 50, 300, 40);
    slider->type(FL_HORIZONTAL);
    slider->value(50.0);
    slider->range(0.0, 100.0);
    slider->step(0.5);

    std::stringstream stream;
    stream << std::fixed << std::setprecision(2) << slider->value();
    std::string myvalue = stream.str();

    Fl_Box *value_box = new Fl_Box(50, 150, 300, 30);
    value_box->box(FL_DOWN_BOX);
    value_box->label(myvalue.c_str());

    slider->callback(slider_callback, value_box);

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

下面是使用了 Fl_Choice 的程式:

#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Choice.H>
#include <FL/Fl_Window.H>
#include <FL/fl_ask.H>

void choice_callback(Fl_Widget *w, void *data) {
    Fl_Choice *choice = static_cast<Fl_Choice *>(w);
    Fl_Box *box = static_cast<Fl_Box *>(data);

    int selected_index = choice->value();
    const char *selected_text = choice->text(selected_index);

    box->copy_label(selected_text);
}

void button_callback(Fl_Widget *w, void *data) {
    Fl_Choice *choice = static_cast<Fl_Choice *> (data);
    int selected_index = choice->value();
    const char *selected_text = choice->text(selected_index);

    // Display the selection in an alert box
    fl_message("You selected: %s (Index: %d)", selected_text, selected_index);
}

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

    Fl_Window *win = new Fl_Window(400, 300, "Fl_Choice Example");
    win->begin();

    Fl_Choice *choice = new Fl_Choice(100, 50, 150, 30, "Option:");
    choice->add("Ada");
    choice->add("C#");
    choice->add("C++");
    choice->add("Erlang");
    choice->add("Fortran");
    choice->add("Lua");
    choice->add("Java");
    choice->add("JavaScript");
    choice->add("Object Pascal");
    choice->add("Perl");
    choice->add("PHP");
    choice->add("Python");
    choice->add("Rexx");
    choice->add("Tcl");

    Fl_Box *box = new Fl_Box(100, 100, 150, 30);
    box->box(FL_UP_BOX);
    choice->callback(choice_callback, box);

    Fl_Button *button = new Fl_Button(100, 150, 150, 30, "Show Selection");
    button->callback(button_callback, choice);

    win->end();
    win->show(argc, argv);

    return Fl::run();
}

Fl_Browser 視窗元件顯示一個可捲動的文字行列表,並管理所有文字的儲存。下面是修改自 FLTK 範例程式的練習。

#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Hold_Browser.H>
#include <FL/Fl_Multi_Browser.H>

// Hold Browser's callback
void HoldBrowserCallback(Fl_Widget *w, void *data) {
    Fl_Hold_Browser *brow = static_cast<Fl_Hold_Browser *>(w);
    Fl_Box *box = static_cast<Fl_Box *>(data);

    int line = brow->value();
    box->copy_label(brow->text(line));
}

int main(int argc, char *argv[]) {
    Fl::scheme("gtk+");
    Fl_Double_Window *win = new Fl_Double_Window(360, 270, "Simple Browser");
    win->begin();

    Fl_Hold_Browser *brow = new Fl_Hold_Browser(10, 10, win->w() - 20, 100);
    brow->callback(HoldBrowserCallback); // callback for hold browser
    brow->add("One");
    brow->add("Two");
    brow->add("Three");
    brow->add("Four");
    brow->select(1);

    Fl_Box *box = new Fl_Box(10, 120, win->w() - 20, 100, "One");
    box->box(FL_UP_BOX);
    box->labelfont(FL_BOLD + FL_ITALIC);
    box->labelsize(36);
    box->labelcolor(FL_BLUE);

    brow->callback(HoldBrowserCallback, box); // callback for hold browser

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

下面是一個簡單的 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 = static_cast<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());
}

下面是修改自 Fl_Chart 範例,以及使用 FL_MENU_RADIO flag 的 Fl_Menu_Bar 的測試程式。

#include <FL/Fl.H>
#include <FL/Fl_Chart.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Menu_Bar.H>
#include <cmath>
#include <cstdio>

Fl_Double_Window *g_win = nullptr;
Fl_Menu_Bar *g_menubar = nullptr;
Fl_Chart *g_chart = nullptr;

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

    if (item->flags & FL_MENU_RADIO) {
        g_chart->type((uchar)item->argument()); // apply change
        g_chart->redraw();
    }
}

int main(int argc, char **argv) {
    g_win = new Fl_Double_Window(720, 486);

    g_menubar = new Fl_Menu_Bar(0, 0, g_win->w(), 25);
    g_menubar->add("Options/FL_BAR_CHART", 0, MyMenuCallback,
                   (void *)FL_BAR_CHART, FL_MENU_RADIO);
    g_menubar->add("Options/FL_HORBAR_CHART", 0, MyMenuCallback,
                   (void *)FL_HORBAR_CHART, FL_MENU_RADIO);
    g_menubar->add("Options/FL_LINE_CHART", 0, MyMenuCallback,
                   (void *)FL_LINE_CHART, FL_MENU_RADIO);
    g_menubar->add("Options/FL_FILL_CHART", 0, MyMenuCallback,
                   (void *)FL_FILL_CHART, FL_MENU_RADIO);
    g_menubar->add("Options/FL_SPIKE_CHART", 0, MyMenuCallback,
                   (void *)FL_SPIKE_CHART, FL_MENU_RADIO);
    g_menubar->add("Options/FL_PIE_CHART", 0, MyMenuCallback,
                   (void *)FL_PIE_CHART, FL_MENU_RADIO);
    g_menubar->add("Options/FL_SPECIALPIE_CHART", 0, MyMenuCallback,
                   (void *)FL_SPECIALPIE_CHART, FL_MENU_RADIO);

    {
        Fl_Menu_Item *item =
            (Fl_Menu_Item *)g_menubar->find_item("Options/FL_BAR_CHART");
        item->value(1);
    }

    g_chart = new Fl_Chart(20, 40, g_win->w() - 40, g_win->h() - 80, "Chart");
    g_chart->bounds(-125.0, 125.0);
    const double start = 1.5;
    const double end = start + 15.1;
    for (double t = start; t < end; t += 0.5) {
        double val = sin(t) * 125.0;
        static char val_str[20];
        sprintf(val_str, "%.0lf", val);
        g_chart->add(val, val_str, (val < 0) ? FL_RED : FL_GREEN);
    }

    g_win->end();
    g_win->resizable(g_win);
    g_win->show();

    return (Fl::run());
}

Fl_Terminal 是 FLTK 1.4 系列新增的視窗元件,用來提供一個可滾動的顯示區域來模擬終端 (Terminal)。 下面是我修改 Fl_Terminal 範例程式的練習。

#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Terminal.H>
#include <time.h>

#define TERMINAL_HEIGHT 550

// Globals
Fl_Double_Window *g_win = nullptr;
Fl_Box *g_msg = nullptr;
Fl_Button *g_button = nullptr;
Fl_Terminal *g_tty = nullptr;
Fl_Box *status_bar = nullptr;

// Append a date/time message to the terminal every 1 seconds
void tick_cb(void *data) {
    time_t lt = time(NULL);
    g_tty->printf("Timer tick: \033[32m%s\033[0m\n", ctime(&lt));
    Fl::repeat_timeout(1.0, tick_cb, data);
}

void button_cb(Fl_Widget *w, void *data) {
    Fl_Button *button = static_cast<Fl_Button *>(w);

    int status = Fl::has_timeout(tick_cb);
    if (status == 0) {
        Fl::add_timeout(0.5, tick_cb);
        status_bar->label("Running.");
    }
}

int main(int argc, char **argv) {
    Fl::scheme("gtk+");
    g_win = new Fl_Double_Window(840, 630, "Example");
    g_win->begin();

    g_msg = new Fl_Box(50, 10, g_win->w() - 150, 30, "Fl_Terminal example");
    g_msg->labelcolor(FL_BLUE);
    g_button = new Fl_Button(g_win->w() - 80, 10, 60, 30, "Run");
    g_button->callback(button_cb);

    g_tty = new Fl_Terminal(0, 50, g_win->w(), TERMINAL_HEIGHT);
    g_tty->ansi(true);

    // Creating a status bar in FLTK by using Fl_Box
    status_bar = new Fl_Box(0, g_win->h() - 30, g_win->w(), 30);
    status_bar->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
    status_bar->box(FL_DOWN_BOX);
    status_bar->label("Ready.");

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

下面是我的另外一個 Fl_Terminal 練習程式,使用 Fl_Input 取得使用者的輸入,而後使用 popen() 嘗試執行。

#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Input.H>
#include <FL/Fl_Menu_Bar.H>
#include <FL/Fl_Terminal.H>
#include <FL/fl_ask.H>
#include <cstdio>
#include <cstdlib>
#ifdef _WIN32
#define popen _popen
#define pclose _pclose
#endif

#define TERMINAL_HEIGHT 530

// Globals
Fl_Double_Window *g_win = nullptr;
Fl_Menu_Bar *g_menu = nullptr;
Fl_Input *g_input = nullptr;
Fl_Button *g_button = nullptr;
Fl_Terminal *g_tty = nullptr;

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

    if (strcmp(item->label(), "&Quit") == 0) {
        exit(0);
    } else if (strcmp(item->label(), "&About") == 0) {
        fl_message_title("About");
        fl_message("Welcome.\n"
                   "It is a FLTK program to test Fl_Terminal widget.");
    }
}

void button_cb(Fl_Widget *w, void *data) {
    Fl_Button *button = static_cast<Fl_Button *>(w);

    char text[1024];
    sprintf(text, "%s 2>&1", g_input->value()); // stderr + stdout
    g_tty->printf("\nEXECUTING: %s\n", text);

    // The popen() function opens a process by creating a pipe, forking,
    // and invoking the shell.
    FILE *fp = popen(text, "r");
    if (fp == 0) {
        g_tty->printf("Failed to execute: '%s'\n", text);
    } else {
        char s[1024];
        while (fgets(s, sizeof(s) - 1, fp)) {
            g_tty->printf("%s", s);
        }
        pclose(fp);
    }
}

int main(int argc, char **argv) {
    Fl::scheme("gleam");
    g_win = new Fl_Double_Window(840, 630, "Example");
    g_win->begin();

    g_menu = new Fl_Menu_Bar(0, 0, g_win->w(), 30);
    g_menu->add("&File/&Quit", "^q", MyMenuCallback);
    g_menu->add("&Help/&About", 0, MyMenuCallback);

    g_input = new Fl_Input(50, 40, g_win->w() - 150, 30, "Input: ");
    g_input->maximum_size(1000);
    g_button = new Fl_Button(g_win->w() - 80, 40, 60, 30, "Run");
    g_button->callback(button_cb);

    g_tty = new Fl_Terminal(0, 90, g_win->w(), TERMINAL_HEIGHT);
    g_tty->ansi(true);

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

Layout and containers

Fl_Scroll, Fl_Tabs 與 Fl_Wizard 是所謂的容器 (containers),基本上是直接固定視窗元件的的佈局功能, 繼承 Fl_Group 的功能加上一些加強的能力。

下面是使用 Fl_Tabs 與 Fl_Widget::when() 方法的例子。 Fl_Widget::when() 設定用於決定何時呼叫 callback function 的標記。

#include <FL/Enumerations.H>
#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Group.H>
#include <FL/Fl_Input.H>
#include <FL/Fl_Tabs.H>
#include <FL/Fl_Window.H>

void tab_closed_cb(Fl_Widget *w, void *data) {
    auto parent = w->parent();
    parent->remove(w);
}

int main(int argc, char **argv) {
    Fl::scheme("gleam");
    Fl_Window window(Fl::w() / 2, Fl::h() / 2, "test");
    Fl_Box windowBox(0, 32, window.w(), window.h() - 32);
    window.resizable(&windowBox);
    Fl_Tabs mainTabs(0, 32, window.w(), window.h() - 64);

    // First tab
    Fl_Group tab1(0, 64, window.w(), window.h() - 64, "Tab 1");
    Fl_Button *button1 = new Fl_Button(20, 100, 100, 25, "Button 1");
    Fl_Box *box1 = new Fl_Box(20, 145, 100, 50, "Text display");
    tab1.when(FL_WHEN_CLOSED);     // Let user can remove this tab
    tab1.callback(tab_closed_cb);
    tab1.end();

    // Second tab
    Fl_Group tab2(0, 64, window.w(), window.h() - 64, "Tab 2");
    Fl_Button *button2 = new Fl_Button(20, 100, 100, 25, "Button 2");
    Fl_Input *input = new Fl_Input(65, 145, 250, 25, "Input:");
    input->align(FL_ALIGN_LEFT);
    tab2.when(FL_WHEN_CLOSED);
    tab2.callback(tab_closed_cb);
    tab2.end();

    // Third tab
    Fl_Group tab3(0, 64, window.w(), window.h() - 64, "Tab 3");
    Fl_Button *button3 = new Fl_Button(20, 100, 100, 25, "Button 3");
    tab3.when(FL_WHEN_CLOSED);
    tab3.callback(tab_closed_cb);
    tab3.end();

    mainTabs.end();
    window.end();
    window.show(argc, argv);
    return Fl::run();
}

除了直接固定視窗元件的位置,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();
}

Fl_Tile 類別可以讓使用者透過拖曳子元件之間的邊框來調整其子元件的大小。下面是使用的例子:

#include <FL/Fl.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Tile.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Pack.H>
#include <FL/fl_draw.H>
#include <cstdlib>

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

    exit(0);
}

// A custom box with a label and color
class ColorBox : public Fl_Box {
public:
    ColorBox(int x, int y, int w, int h, const char* label, Fl_Color c)
        : Fl_Box(x, y, w, h, label) {
        box(FL_FLAT_BOX);
        labelcolor(FL_BLACK);
        labelsize(24);
        labelfont(FL_BOLD);
        color(c);
    }
};

int main(int argc, char **argv) {
    Fl_Double_Window* window = new Fl_Double_Window(600, 400, "Fl_Tile Example");
    window->begin();

    Fl_Tile* tile = new Fl_Tile(0, 0, 600, 400);
    tile->begin();

    ColorBox* box1 = new ColorBox(0, 0, 200, 400, "Left", FL_YELLOW);
    ColorBox* box2 = new ColorBox(200, 0, 400, 200, "Top Right", FL_GREEN);
    ColorBox* box3 = new ColorBox(200, 200, 400, 200, "Bottom Right", FL_CYAN);

    tile->end();

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

下面是 Fl_Grid 的例子。

#include <FL/Fl.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Grid.H>
#include <cstdlib>

int main(int argc, char **argv) {
    Fl_Double_Window *win =
        new Fl_Double_Window(320, 180, "3x3 Fl_Grid with Buttons");

    // create the Fl_Grid container with five buttons
    Fl_Grid *grid = new Fl_Grid(0, 0, win->w(), win->h());
    grid->layout(3, 3, 10, 10);
    grid->color(FL_WHITE);

    Fl_Button *b0 = new Fl_Button(0, 0, 0, 0, "New");
    Fl_Button *b1 = new Fl_Button(0, 0, 0, 0, "Options");
    Fl_Button *b3 = new Fl_Button(0, 0, 0, 0, "About");
    Fl_Button *b4 = new Fl_Button(0, 0, 0, 0, "Help");
    Fl_Button *b6 = new Fl_Button(0, 0, 0, 0, "Quit");
    b6->color(FL_BLACK);
    b6->labelcolor(FL_WHITE);
    b6->callback([](Fl_Widget *, void *) -> void { exit(0); });

    // assign buttons to grid positions
    grid->widget(b0, 0, 0);
    grid->widget(b1, 0, 1, 1, 2);
    grid->widget(b3, 1, 1);
    grid->widget(b4, 2, 0);
    grid->widget(b6, 2, 2);
    // grid->show_grid(1);     // enable to display grid helper lines
    grid->end();

    win->end();
    win->resizable(grid);
    win->size_range(300, 100);
    win->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();
}

再來改寫上面的程式,使用 FL_MOVE 事件取得目前滑鼠指標的位置,並且顯示在標題列。

#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>
#include <sstream>
#include <string>

class MouseEvent_Window : public Fl_Window {
    int handle(int e) {
        std::stringstream stream;
        std::string myvalue;

        switch (e) {
        case FL_MOVE:
            stream << "(" << Fl::event_x() << ", " << Fl::event_y() << ")";
            myvalue = stream.str();
            copy_label(myvalue.c_str());

            break;
        default:
            break;
        }

        return (Fl_Window::handle(e));
    }

public:
    MouseEvent_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) {
    MouseEvent_Window *window = new MouseEvent_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)

我在測試的時候不需要加入尋找 OpenGL 的部份,如果發現有相關的錯誤而需要加入,首先在 CMakeLists.txt 加入 find_package:

find_package(OpenGL REQUIRED)

MESSAGE ( STATUS "  OPENGL_FOUND :        " ${OPENGL_FOUND} )
MESSAGE ( STATUS "  OPENGL_INCLUDE_PATH:  " ${OPENGL_INCLUDE_PATH} )

更新 target_include_directories,然後在連結的部份加入:

target_link_libraries(hello PRIVATE fltk::fltk fltk::gl OpenGL::GL)

相關連結