在許多情況下,命令列工具足以應付單純的文字輸入輸出和檔案處理。然而,當應用程式需要更豐富的視覺互動,特別是 2D 或 3D 圖形操作時,圖形使用者介面(GUI)就變得不可或缺。本文將以 Rust 語言為例,示範如何使用 GTK 框架建立跨平台的 GUI 應用程式,並以一個簡化的 catsay 程式作為範例,逐步講解從 TUI 到 GUI 的開發流程。首先,我們會先建立一個根據文字的使用者介面(TUI),藉此理解 GUI 程式中常見的事件驅動架構。接著,我們將使用 GTK 框架,從建立視窗、新增元件到處理使用者互動,完整示範 Rust 原生 GUI 應用程式的開發過程,並提供程式碼範例和圖解說明。

建立圖形使用者介面(GUI)

在不需要太多視覺互動的情況下,例如批次處理,命令列工具非常方便。但是,由於命令列程式只能處理文字輸入/輸出和檔案,因此如果需要3D(甚至2D)視覺互動,它並不理想。因此,在本章中,您將打破命令列的限制,實作圖形使用者介面(GUI)。

本章的目標是展示如何使用Rust建立跨平台的桌面應用程式。雖然有像Electron這樣的框架,可以讓您使用HTML、CSS和JavaScript建立桌面應用程式,但它們實際上是在內部包裝了一個瀏覽器引擎。因此,開發者體驗將更接近於建立網站或網頁應用程式,而不是編寫原生桌面應用程式。在本章中,您將使用GTK框架,它展示了在Rust中建立原生應用程式的體驗。

作為命令列程式和實際GUI應用程式之間的橋樑,您將首先了解根據文字的使用者介面(TUI)。TUI看起來像GUI,但它是用文字字元繪製的。因此,它可以在終端環境中建立。但是,由於TUI使用文字字元繪製,解析度很低,螢幕空間非常有限。儘管如此,TUI是瞭解GUI程式中常見的事件驅動架構的高階概念的好方法。一旦您掌握了TUI程式結構的知識,您就可以將該知識應用於實作GTK中的完整GUI程式。

3.1 您正在建立什麼?

為了避免複雜的業務邏輯的幹擾,並專注於程式碼的結構,您將建立簡化版的catsay程式作為TUI和GUI。對於TUI,您將建立以下內容:

  • 一個互動式表單,用於接收訊息。(圖3-1)
  • 一個用於--dead選項的核取方塊。
  • 一個對話方塊,顯示貓說出的訊息。(圖3-2)

然後,您將建立一個具有與TUI程式相同輸入的GUI。但是這次,將使用真實貓的照片,而不是ASCII藝術貓。(圖3-3)

圖3-1 TUI程式的輸入表單

圖3-2 TUI程式的對話方塊

圖3-3 具有貓照片的GUI程式(貓圖片來自https://pix-abay.com/photos/cat-kitten-to-sit-isolated-red-2669554/,Pixabay授權)

您將使用gtk3-rs建立GUI,它是GTK3函式庫及其底層函式庫的Rust繫結。您將首先使用純Rust程式碼建立GUI,然後切換到Glade,一種使用者介面設計工具,可以幫助您以更直觀、更易於管理的方式設計佈局。

3.2 建立根據文字的使用者介面

在第2章中,您使用了println!()進行大多數輸出。它的問題在於您一次只能輸出一行。雖然您可以透過仔細對齊輸出的行來建立ASCII藝術影像,但如果您想要繪製視窗、對話方塊和按鈕,則很難擴充套件,更不用說處理鍵盤輸入和滑鼠點選,並讓UI對這些輸入做出反應。幸運的是,有一類別稱為根據文字的使用者介面函式庫,可以幫助您輕鬆建立UI元件。例如,ncurses就是這樣一個函式庫。

// 使用 ncurses 建立 TUI 的範例程式碼
use ncurses::*;

fn main() {
    // 初始化 ncurses
    initscr();
    // 設定輸入模式
    cbreak();
    // 隱藏遊標
    curs_set(CURSOR_VISIBILITY::CURSOR_INVISIBLE);
    // 建立視窗
    let win = newwin(10, 20, 5, 5);
    // 繪製視窗邊框
    box_(win, 0, 0);
    // 重新整理螢幕
    refresh();
    // 等待使用者輸入
    getch();
    // 清理 ncurses
    endwin();
}

內容解密:

此範例程式碼展示瞭如何使用ncurses函式庫建立一個簡單的TUI視窗。首先,我們初始化ncurses並設定輸入模式。然後,我們隱藏遊標並建立一個新的視窗。接著,我們繪製視窗邊框並重新整理螢幕。最後,我們等待使用者輸入並清理ncurses。

此範例程式碼的作用是:

  • 初始化ncurses並設定輸入模式,以便能夠接收使用者輸入。
  • 建立一個新的視窗,並繪製視窗邊框,以提供一個基本的UI元件。
  • 等待使用者輸入,並在使用者按下鍵盤後清理ncurses,以確保程式能夠正常離開。

透過這個範例,您可以看到如何使用ncurses函式庫建立一個簡單的TUI程式。這對於瞭解GUI程式中的事件驅動架構非常有幫助。

建立圖形化使用者介面(GUI)

簡介與基礎設定

在開發命令列工具之餘,我們也可以為應用程式建立圖形化使用者介面(GUI)。本章節將介紹如何使用 Rust 語言的 cursive 函式庫來建立一個簡單的終端使用者介面(TUI)。

cursive 是一個 Rust 的函式庫,它提供了一個抽象層,讓開發者能夠在不同的 TUI 後端函式庫上建立應用程式。預設情況下,cursive 使用 ncurses 作為後端。要使用 ncurses,需要在系統上安裝它。在 Ubuntu 上,可以執行以下命令來安裝:

$ sudo apt install libncursesw5-dev

接著,建立一個新的 Rust 專案,並將 cursive 加入到 Cargo.toml 中:

$ cargo new catsay-tui
$ cd catsay-tui
$ cargo add cursive

基本的 TUI 程式架構

src/main.rs 中,使用以下程式碼建立一個基本的 TUI 程式:

// src/main.rs
fn main() {
    let mut siv = cursive::default();
    siv.run(); // 啟動事件迴圈
}

內容解密:

  • cursive::default() 建立了一個預設的 Cursive 根物件。
  • siv.run() 啟動了事件迴圈,這是 GUI 程式中的一個基本概念,用於處理使用者的輸入。

顯示對話方塊

執行 cargo run 後,會看到一個藍色的畫面。要顯示貓咪的 ASCII 藝術,可以在 src/main.rs 中加入以下程式碼:

// src/main.rs
use cursive::views::TextView;

fn main() {
    let mut siv = cursive::default();
    let cat_text = "Meow!
\\
\\
/\\_/\\
( o o )
=( I )=";
    siv.add_layer(TextView::new(cat_text));
    siv.run();
}

內容解密:

  • TextView::new(cat_text) 建立了一個用於顯示靜態文字的檢視。
  • siv.add_layer()TextView 新增為一個層,層的概念用於建立元件的堆積疊檢視。

處理簡單的鍵盤輸入

目前為止,TUI 程式還不能處理任何輸入。為了使程式能夠回應 ESC 鍵的按下並正常離開,可以修改 src/main.rs 如下:

// src/main.rs
use cursive::event::Key;

fn main() {
    let mut siv = cursive::default();
    let cat_text = "Meow!
\\
\\
/\\_/\\
( o o )
=( I )=";
    siv.add_layer(TextView::new(cat_text));
    siv.add_global_callback(Key::Esc, |s| s.quit());
    siv.run();
}

內容解密:

  • siv.add_global_callback(Key::Esc, |s| s.quit()) 設定了一個全域回撥,當按下 ESC 鍵時,程式會正常離開。
  • 事件迴圈是非阻塞的,允許程式回應多種使用者輸入。

建立圖形化使用者介面(GUI)

新增對話方塊

為了讓程式具有更精緻的外觀和體驗,可以使用 Dialog 包裹 TextView(圖 3-5)。修改 src/main.rs 檔案如下:

// src/main.rs
use cursive::views::{Dialog, TextView};

fn main() {
    let mut siv = cursive::default();
    let cat_text = // ...
    siv.add_layer(
        Dialog::around(TextView::new(cat_text))
            .button("OK", |s| s.quit())
    );
    siv.run();
}

使用 Dialog::around()TextView 包裹起來,這會在 TextView 周圍新增一個對話方塊(圖 3-6)。還可以新增一個帶有「OK」標籤的按鈕和一個回呼函式(|s| s.quit())。當按鈕被點選時,這個回呼函式將被觸發。Cursive 的一個好處是它支援鍵盤和滑鼠互動,因此可以透過按下 ENTER 鍵或雙擊按鈕來關閉程式。

由於將 TextView 包裹在 Dialog 中來顯示文字對話方塊是非常常見的操作,Cursive 提供了一個簡寫語法 Dialog::text()。因此,可以將 src/main.rs 中的程式碼重寫為:

siv.add_layer(
    Dialog::text(cat_text).button("OK", |s| s.quit())
);

多步驟對話方塊

不僅限於一次只顯示一個靜態層。可以建立一個多步驟流程。在第一步中,提示使用者填寫表單並按下「OK」,然後隱藏表單並使用表單中提供的資訊顯示貓的 ASCII 藝術。修改 src/main.rs 檔案如下(清單 3-1):

// src/main.rs
use cursive::traits::Nameable;
use cursive::views::{Checkbox, Dialog, EditView, ListView};
use cursive::Cursive;

struct CatsayOptions<'a> {
    message: &'a str,
    dead: bool,
}

fn input_step(siv: &mut Cursive) {
    siv.add_layer(
        Dialog::new()
            .title("請填寫貓的表單")
            .content(
                ListView::new()
                    .child("訊息:", EditView::new().with_name("message"))
                    .child("是否死亡?", Checkbox::new().with_name("dead"))
            )
            .button("OK", |s| {
                let message = s.call_on_name("message", |t: &mut EditView| t.get_content()).unwrap();
                let is_dead = s.call_on_name("dead", |t: &mut Checkbox| t.is_checked()).unwrap();
                let options = CatsayOptions { message: &message, dead: is_dead };
                result_step(s, &options)
            })
    );
}

fn result_step(siv: &mut Cursive, options: &CatsayOptions) {
    let eye = if options.dead { "x" } else { "o" };
    let cat_text = format!(
        "{msg}\n\
         \\\n\
         /\\_/\\\n\
         ( {eye} {eye} )\n\
         =( I )=",
        msg = options.message,
        eye = eye
    );
    siv.pop_layer();
    siv.add_layer(
        Dialog::text(cat_text)
            .title("貓說...")
            .button("OK", |s| s.quit())
    );
}

fn main() {
    let mut siv = cursive::default();
    input_step(&mut siv);
    siv.run();
}

內容解密:

  1. input_step 函式:設定了一個包含輸入欄位的表單,並在按下「OK」按鈕時讀取輸入值。
  2. result_step 函式:根據輸入值生成貓的 ASCII 藝術,並顯示在新的對話方塊中。
  3. siv.pop_layer():移除目前的層(表單層),並新增新的層(貓的 ASCII 藝術層)。
  4. call_on_name:用於根據元件的名稱取得其參考,並讀取其值。

讀取使用者輸入

程式如何將使用者的輸入(訊息和「是否死亡?」標誌)從表單傳遞到貓圖片對話方塊?在 input_step() 中,首先設定了輸入欄位,並為每個欄位分配了一個唯一的名稱,以便稍後檢索它們的值。

內容解密:

  1. EditViewCheckbox:分別用於輸入文字和選擇選項。
  2. with_name:為元件設定唯一的名稱。
  3. call_on_name:根據名稱取得元件的參考,並讀取其值。

從TUI轉向圖形使用者介面(GUI)

在之前的章節中,我們已經探討瞭如何使用Rust建立一個簡單的終端使用者介面(TUI)。然而,由於TUI的解析度限制和有限的螢幕空間,它並不總是能夠提供最佳的使用者經驗。因此,我們將進一步探索如何使用Rust建立一個圖形使用者介面(GUI)。

為什麼選擇GTK?

GTK(GIMP Toolkit)是一個免費且開源的GUI工具包,廣泛用於Linux和其他平台。它提供了豐富的UI元件和工具,使得開發者能夠輕鬆地建立跨平台的GUI應用程式。Rust的gtk crate為GTK提供了Rust語言的繫結,使得我們能夠在Rust中使用GTK的功能。

建立一個GTK視窗

首先,我們需要在Ubuntu上安裝GTK開發函式庫:

$ sudo apt install libgtk-3-dev

接下來,建立一個新的Cargo專案並新增gtk crate作為依賴:

$ cargo new catsay-gui
$ cd catsay-gui
$ cargo add gtk --features v3_24

Cargo.toml中,我們需要指定gtk crate的版本和特性:

[package]
name = "catsay-gui"
version = "0.1.0"
edition = "2021"

[dependencies.gtk]
gtk = { version = "0.17.1", features = ["v3_24"] }

建立視窗的程式碼

現在,我們可以開始撰寫建立視窗的程式碼。在src/main.rs中:

use gtk::prelude::*;
use gtk::{Application, ApplicationWindow};

fn main() {
    let app = Application::new(
        Some("com.shinglyu.catsay-gui"),
        Default::default()
    );
    app.connect_activate(|app| {
        let window = ApplicationWindow::new(app);
        window.set_title("Catsay");
        window.set_default_size(350, 70);
        window.show_all();
    });
    app.run();
}

程式碼解析

  1. 建立GTK應用程式:使用Application::new()建立一個GTK應用程式,需要提供一個應用程式ID。
  2. 設定啟動事件:使用connect_activate()方法設定應用程式啟動時的事件處理函式。
  3. 建立視窗:在事件處理函式中,建立一個ApplicationWindow並設定其標題和大小。
  4. 顯示視窗:使用show_all()方法顯示視窗。
  5. 啟動主事件迴圈:使用app.run()啟動應用程式的主事件迴圈。

執行cargo run,你應該可以看到一個空白的視窗。

在視窗中顯示圖片

接下來,我們將在視窗中顯示一張貓咪圖片。首先,需要匯入必要的GTK元件:

use gtk::{Application, ApplicationWindow, Box as GtkBox, Image, Label, Orientation};

然後,在connect_activate()的閉包中新增以下程式碼:

let layout_box = GtkBox::new(Orientation::Vertical, 0);
let label = Label::new(Some("Meow!\n \\\n \\"));

顯示圖片與文字

// 建立一個垂直佈局盒
let layout_box = GtkBox::new(Orientation::Vertical, 0);
// 建立一個標籤
let label = Label::new(Some("Meow!\n \\\n \\"));
// 將標籤新增到佈局盒中
layout_box.add(&label);
// 建立一個圖片元件
let image = Image::new_from_file("path/to/your/cat/image.jpg");
// 將圖片新增到佈局盒中
layout_box.add(&image);
// 將佈局盒新增到視窗中
window.add(&layout_box);

程式碼作用與邏輯

  1. 建立垂直佈局盒:使用GtkBox::new()建立一個垂直佈局盒,用於容納其他元件。
  2. 建立標籤和圖片元件:分別使用Label::new()Image::new_from_file()建立標籤和圖片元件。
  3. 將元件新增到佈局盒:使用add()方法將標籤和圖片元件新增到佈局盒中。
  4. 將佈局盒新增到視窗:使用add()方法將佈局盒新增到視窗中。

這樣,我們就完成了一個簡單的GUI應用程式,能夠顯示一張貓咪圖片和一段文字。接下來,你可以繼續探索GTK的其他功能,來豐富你的應用程式。