在許多情況下,命令列工具足以應付單純的文字輸入輸出和檔案處理。然而,當應用程式需要更豐富的視覺互動,特別是 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();
}
內容解密:
input_step函式:設定了一個包含輸入欄位的表單,並在按下「OK」按鈕時讀取輸入值。result_step函式:根據輸入值生成貓的 ASCII 藝術,並顯示在新的對話方塊中。siv.pop_layer():移除目前的層(表單層),並新增新的層(貓的 ASCII 藝術層)。call_on_name:用於根據元件的名稱取得其參考,並讀取其值。
讀取使用者輸入
程式如何將使用者的輸入(訊息和「是否死亡?」標誌)從表單傳遞到貓圖片對話方塊?在 input_step() 中,首先設定了輸入欄位,並為每個欄位分配了一個唯一的名稱,以便稍後檢索它們的值。
內容解密:
EditView和Checkbox:分別用於輸入文字和選擇選項。with_name:為元件設定唯一的名稱。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();
}
程式碼解析
- 建立GTK應用程式:使用
Application::new()建立一個GTK應用程式,需要提供一個應用程式ID。 - 設定啟動事件:使用
connect_activate()方法設定應用程式啟動時的事件處理函式。 - 建立視窗:在事件處理函式中,建立一個
ApplicationWindow並設定其標題和大小。 - 顯示視窗:使用
show_all()方法顯示視窗。 - 啟動主事件迴圈:使用
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);
程式碼作用與邏輯
- 建立垂直佈局盒:使用
GtkBox::new()建立一個垂直佈局盒,用於容納其他元件。 - 建立標籤和圖片元件:分別使用
Label::new()和Image::new_from_file()建立標籤和圖片元件。 - 將元件新增到佈局盒:使用
add()方法將標籤和圖片元件新增到佈局盒中。 - 將佈局盒新增到視窗:使用
add()方法將佈局盒新增到視窗中。
這樣,我們就完成了一個簡單的GUI應用程式,能夠顯示一張貓咪圖片和一段文字。接下來,你可以繼續探索GTK的其他功能,來豐富你的應用程式。