在終端機環境下,Cursive 提供了便捷的方式來建立互動式 TUI 程式。藉由組合 TextView、Dialog 和事件回呼,開發者可以快速構建具備基本互動功能的介面。然而,當需要更豐富的視覺效果和使用者經驗時,GUI 就成為更佳的選擇。GTK 作為一個成熟的跨平台 GUI 函式庫,搭配 gtk-rs 繫結,讓 Rust 開發者能輕鬆地建立視窗程式並顯示圖片。Glade UI 設計工具更簡化了介面設計流程,透過視覺化拖曳元件,開發者可以更直觀地設計 UI 佈局,並將介面描述儲存為 XML 檔案,再由程式載入和渲染。從 TUI 到 GUI 的轉變,不僅提升了使用者經驗,也為應用程式帶來了更廣泛的應用場景。

使用Cursive建立互動式終端使用者介面

Cursive是一個用於在終端機上建立文字使用者介面(TUI)的Rust函式庫。本章節將介紹如何使用Cursive建立互動式TUI程式。

建立基本TUI程式

首先,我們需要建立一個基本的TUI程式。以下是一個簡單的範例,顯示一隻貓的ASCII藝術:

use cursive::views::TextView;
use cursive::Cursive;

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

內容解密:

  1. Cursive::default()用於建立一個新的Cursive例項。
  2. TextView::new(cat_text)建立一個新的TextView,用於顯示貓的ASCII藝術。
  3. siv.add_layer()TextView新增至Cursive例項中。
  4. siv.run()啟動Cursive的事件迴圈。

處理鍵盤輸入

目前為止,我們的TUI程式只能顯示輸出,但無法處理輸入。我們可以新增一個全域回呼函式來處理ESC鍵的按下事件:

use cursive::event::Key;
use cursive::views::TextView;
use cursive::Cursive;

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

內容解密:

  1. siv.add_global_callback()新增一個全域回呼函式,用於處理ESC鍵的按下事件。
  2. |s| s.quit()是一個閉包,用於離開Cursive程式。

新增對話方塊

我們可以使用Dialog來包裝TextView,使其具有更豐富的外觀和感覺:

use cursive::views::{Dialog, TextView};
use cursive::Cursive;

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

內容解密:

  1. Dialog::around()用於包裝TextView
  2. .button()新增一個按鈕,用於離開Cursive程式。

多步驟對話方塊

我們可以建立一個多步驟的流程,讓使用者填寫表單並顯示貓的圖片:

use cursive::traits::Identifiable;
use cursive::views::{Checkbox, Dialog, EditView, ListView, TextView};
use cursive::Cursive;

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

fn input_step(siv: &mut Cursive) {
    siv.add_layer(
        Dialog::new()
            .title("Please fill out the form for the cat")
            .content(
                ListView::new()
                    .child("Message:", EditView::new().with_id("message"))
                    .child("Dead?", Checkbox::new().with_id("dead"))
            )
            .button("OK", |s| {
                let message = s.call_on_id("message", |t: &mut EditView| t.get_content()).unwrap();
                let is_dead = s.call_on_id("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!("{}\n\\\\\n/\\_/\\\n( {} {} )\n=( I )=", options.message, eye, eye);
    siv.pop_layer();
    siv.add_layer(
        Dialog::around(TextView::new(cat_text))
            .title("The cat says...")
            .button("OK", |s| s.quit())
    );
}

內容解密:

  1. CatsayOptions結構體用於儲存表單資料。
  2. input_step()函式建立一個表單對話方塊。
  3. result_step()函式根據表單資料顯示貓的圖片。
  4. siv.pop_layer()用於移除目前的對話方塊。
  5. siv.add_layer()用於新增新的對話方塊。

從終端機使用者介面(TUI)到圖形使用者介面(GUI)的進階

在前面的章節中,我們已經瞭解瞭如何使用 Cursive 函式庫在 Rust 中建立一個簡單的終端機使用者介面(TUI)應用程式。現在,讓我們探討這個範例,並進一步瞭解如何從 TUI 轉換到圖形使用者介面(GUI)。

TUI 程式流程解析

首先,我們來分析 main 函式的流程:

fn main() {
    let mut siv = Cursive::default();
    input_step(&mut siv); // 設定初始介面
    siv.run(); // 執行 Cursive 事件迴圈
}

main 函式中,我們建立了一個 Cursive 物件並呼叫 input_step 函式來設定初始的 TUI 介面。接著,我們執行了 siv.run() 來啟動 Cursive 的事件迴圈。

input_step 函式解析

siv.add_layer(
    Dialog::new()
        .title("請填寫貓咪資訊表單")
        .content(
            ListView::new()
                .child(
                    "訊息:",
                    EditView::new().with_id("message") // 輸入框
                )
                .child(
                    "是否已逝?",
                    Checkbox::new().with_id("dead") // 核取方塊
                ),
        )
        .button("確定", |s| {
            // 取得輸入的訊息和核取方塊狀態
            let message = s.call_on_id("message", |t: &mut EditView| t.get_content()).unwrap();
            let is_dead = s.call_on_id("dead", |t: &mut Checkbox| t.is_checked()).unwrap();
            
            // 將取得的值封裝成 CatsayOptions 結構
            let options = CatsayOptions {
                message: &message,
                dead: is_dead,
            };
            
            // 呼叫 result_step 顯示結果
            result_step(s, &options)
        }),
);

內容解密:

  1. siv.add_layer:新增一個新的 UI 圖層。
  2. Dialog::new():建立一個對話方塊。
  3. .title("請填寫貓咪資訊表單"):設定對話方塊的標題。
  4. **.content(ListView::new()...:設定對話方塊的內容為一個列表檢視,包含輸入框和核取方塊。
  5. EditView::new().with_id("message"):建立一個輸入框並賦予 ID “message”,以便稍後檢索其內容。
  6. Checkbox::new().with_id("dead"):建立一個核取方塊並賦予 ID “dead”,用於檢查其狀態。
  7. .button("確定", |s| {...}):新增一個按鈕,當點選時執行回呼函式。
  8. call_on_id:根據元件 ID 取得元件的參照,並執行閉包函式以擷取其值。

從 TUI 到 GUI 的轉變

雖然 TUI 在某些場景下很有用,但它有其侷限性,例如解析度有限和視覺效果不佳。因此,我們決定轉向使用圖形使用者介面(GUI)。在 Rust 中,一個流行的 GUI 函式庫是 gtk-rs,它是 GTK 函式庫的 Rust 繫結。GTK 是一個成熟且廣泛使用的跨平台 GUI 函式庫。

為什麼選擇 gtk-rs?

  1. 成熟度:GTK 本身是一個非常成熟的函式庫,而 gtk-rs 是其官方的 Rust 繫結,因此具有很高的可靠性和社群支援。
  2. 跨平台支援:GTK 支援多個平台,包括 Linux、Windows 和 macOS。
  3. 豐富的元件:GTK 提供了大量的 UI 元件,可以輕鬆建立複雜的 GUI 應用程式。
  4. 良好的檔案和社群支援:由於 GTK 本身有龐大的社群和豐富的檔案,因此在使用 gtk-rs 時也可以參考 GTK 的檔案和討論。

使用GTK建立視窗與顯示圖片

首先,我們嘗試使用GTK建立一個視窗。gtk-rs函式庫依賴於系統的GTK函式庫。在Ubuntu上安裝GTK函式庫,只需執行以下指令:

sudo apt-get install libgtk-3-dev

接著,建立一個新的Cargo專案,並在Cargo.toml中加入以下相依套件:

[dependencies]
gio = "0.6.0" 
[dependencies.gtk]
version = "0.6.0" 
features = ["v3_22"] 

這裡的設定稍微複雜一些,因為gtk-rs依賴於系統的GTK函式庫,所以使用Cargo的「feature」來控制目標系統GTK函式庫的版本。因此,version = "0.6.0"指定的是gtk-rs套件本身的版本,而v3_22則是目標系統GTK函式庫的版本。如果不知道系統中安裝的GTK函式庫版本,可以執行dpkg -l libgtk-3-0來查詢。

建立主視窗

建立src/main.rs檔案,並將以下程式碼複製進去:

extern crate gio;
extern crate gtk;

use gio::prelude::*;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow};
use std::env;

fn main() {
    let app = Application::new(
        "com.shinglyu.catsay-gui",
        gio::ApplicationFlags::empty()
    ).expect("Failed to initialize GTK.");

    app.connect_startup(|app| {
        let window = ApplicationWindow::new(app);
        window.set_title("Catsay");
        window.set_default_size(350, 70);
        
        window.connect_delete_event(|win, _| {
            win.destroy();
            Inhibit(false) 
        });

        window.show_all();
    });

    app.connect_activate(|_| {});

    app.run(&env::args().collect::<Vec<_>>());
}

程式碼解析:

  1. 首先,使用Application::new建立一個GTK應用程式,需要指定一個應用程式ID。GTK使用反向DNS樣式作為ID。
  2. 在應用程式啟動時,會觸發startup事件。在這個事件處理函式中,建立一個ApplicationWindow,並設定其標題和大小。
  3. 為視窗的delete_event註冊事件處理函式,當使用者嘗試關閉視窗時,會觸發這個事件,並銷毀視窗。
  4. 最後,呼叫window.show_all()顯示視窗。

顯示圖片

現在,我們在視窗中新增一些程式碼來顯示貓圖片。將以下程式碼加入main()函式中:

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

// ...

app.connect_startup(|app| {
    let window = ApplicationWindow::new(app);
    // ... 設定視窗標題、大小和delete_event
    
    let layout_box = Box::new(Orientation::Vertical, 0);
    let label = Label::new("Meow!\n \\\n \\");
    layout_box.add(&label);

    let cat_image = Image::new_from_file("./images/cat.png");
    layout_box.add(&cat_image);

    window.add(&layout_box);
    window.show_all();
});

// ...

程式碼解析:

  1. 建立一個垂直方向的Box容器,用於容納多個元件。
  2. 建立一個Label用於顯示文字,並將其加入容器中。
  3. 使用Image::new_from_file建立一個圖片元件,載入指定路徑的圖片,並將其加入容器中。
  4. 將容器加入視窗中,並顯示所有元件。

偵錯工具

GTK提供了視覺化的偵錯工具,可以用來檢查元件樹和元件在視窗中的位置。只需將環境變數GTK_DEBUG設為interactive,即可啟動偵錯工具。例如:

GTK_DEBUG=interactive cargo run

這樣,就可以在執行程式的同時,開啟偵錯工具,檢查元件的位置和大小。

使用 Glade 設計使用者介面

在前面的章節中,我們以程式化的方式建立了使用者介面。這意味著我們需要使用 Rust 程式碼來建立元件、將它們放入容器中、將容器放入更大的容器中,然後將它們附加到視窗上,最後顯示它們。當程式越來越龐大時,這種工作方式很容易出錯且難以視覺化。另一種方法是宣告式地定義使用者介面佈局。

宣告式 UI 佈局

宣告式 UI 佈局的方式是描述你想要的佈局結構,而不是逐步建立它。例如,你可以宣告一個包含 GtkLabel 和 GtkImage 的 GtkBox,然後將 GtkBox 放入視窗中。GTK 提供了一種使用 XML(可擴充套件標記語言)標記來進行宣告的方法。XML 檔案包含了 widget 佈局的靜態宣告,可以使用 GTKBuilder 物件載入。UI 可以在執行時建立。

使用 Glade 建立 UI

手寫 XML 檔案非常繁瑣。GTK 提供了一個名為 Glade 的 UI 設計工具,可以用來產生 XML 檔案。你可以使用以下命令安裝 Glade:

sudo apt-get install glade

在 Glade 中,你可以使用所見即所得(WYSIWYG)的編輯器拖曳 widget。你也可以調整個別 widget 的引數並獲得即時回饋。使用 Glade(和 XML 佈局定義),你可以將視覺呈現與動作和行為分開。你可以將大部分的視覺設計和佈局保留在 XML 中,只將事件處理邏輯留在 Rust 程式碼中。

使用 Glade 建立表單

你可以拖曳一個類別似於圖 3-12 的表單;它的 widget 組織如圖 3-13 所示。然後,點選「檔案」選單並選擇「另存為」將其儲存為名為 layout.glade 的 XML 檔案(如下所示的清單 3-9)。

Glade 佈局 XML

<?xml version="1.0" encoding="UTF-8"?>
<!-- 由 glade 3.18.3 產生 -->
<interface>
  <requires lib="gtk+" version="3.12"/>
  <object class="GtkApplicationWindow" id="applicationwindow1">
    <property name="can_focus">False</property>
    <property name="title" translatable="yes">Catsay</property>
    <child>
      <object class="GtkBox" id="global_layout_box">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="orientation">vertical</property>
        <child>
          <object class="GtkBox" id="form_box">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <property name="orientation">vertical</property>
            <child>
              <!-- 表單元件 -->
            </child>
          </object>
        </child>
      </object>
    </child>
  </object>
</interface>

詳細解說

  1. 宣告 UI 結構:Glade XML 檔案宣告了 UI 的結構,包括視窗、容器和 widget。
  2. 使用 GTKBuilder 載入 UI:GTKBuilder 物件可以用來載入 XML 檔案並建立 UI。
  3. 分離視覺呈現和邏輯:使用 Glade 和 XML 佈局定義,可以將視覺呈現與動作和行為分開。

程式碼實作

載入 Glade XML 檔案

// 建立 GTKBuilder 物件
let builder = gtk::Builder::from_file("layout.glade");

// 從 Builder 中取得物件
let window: gtk::ApplicationWindow = builder.get_object("applicationwindow1").unwrap();

詳細解說

  1. 建立 GTKBuilder 物件:使用 gtk::Builder::from_file 方法建立 GTKBuilder 物件,載入 Glade XML 檔案。
  2. 取得物件:使用 get_object 方法從 Builder 中取得物件,例如視窗。