在終端機環境下,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();
}
內容解密:
Cursive::default()用於建立一個新的Cursive例項。TextView::new(cat_text)建立一個新的TextView,用於顯示貓的ASCII藝術。siv.add_layer()將TextView新增至Cursive例項中。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();
}
內容解密:
siv.add_global_callback()新增一個全域回呼函式,用於處理ESC鍵的按下事件。|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();
}
內容解密:
Dialog::around()用於包裝TextView。.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())
);
}
內容解密:
CatsayOptions結構體用於儲存表單資料。input_step()函式建立一個表單對話方塊。result_step()函式根據表單資料顯示貓的圖片。siv.pop_layer()用於移除目前的對話方塊。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)
}),
);
內容解密:
siv.add_layer:新增一個新的 UI 圖層。Dialog::new():建立一個對話方塊。.title("請填寫貓咪資訊表單"):設定對話方塊的標題。- **
.content(ListView::new()...:設定對話方塊的內容為一個列表檢視,包含輸入框和核取方塊。 EditView::new().with_id("message"):建立一個輸入框並賦予 ID “message”,以便稍後檢索其內容。Checkbox::new().with_id("dead"):建立一個核取方塊並賦予 ID “dead”,用於檢查其狀態。.button("確定", |s| {...}):新增一個按鈕,當點選時執行回呼函式。call_on_id:根據元件 ID 取得元件的參照,並執行閉包函式以擷取其值。
從 TUI 到 GUI 的轉變
雖然 TUI 在某些場景下很有用,但它有其侷限性,例如解析度有限和視覺效果不佳。因此,我們決定轉向使用圖形使用者介面(GUI)。在 Rust 中,一個流行的 GUI 函式庫是 gtk-rs,它是 GTK 函式庫的 Rust 繫結。GTK 是一個成熟且廣泛使用的跨平台 GUI 函式庫。
為什麼選擇 gtk-rs?
- 成熟度:GTK 本身是一個非常成熟的函式庫,而
gtk-rs是其官方的 Rust 繫結,因此具有很高的可靠性和社群支援。 - 跨平台支援:GTK 支援多個平台,包括 Linux、Windows 和 macOS。
- 豐富的元件:GTK 提供了大量的 UI 元件,可以輕鬆建立複雜的 GUI 應用程式。
- 良好的檔案和社群支援:由於 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<_>>());
}
程式碼解析:
- 首先,使用
Application::new建立一個GTK應用程式,需要指定一個應用程式ID。GTK使用反向DNS樣式作為ID。 - 在應用程式啟動時,會觸發
startup事件。在這個事件處理函式中,建立一個ApplicationWindow,並設定其標題和大小。 - 為視窗的
delete_event註冊事件處理函式,當使用者嘗試關閉視窗時,會觸發這個事件,並銷毀視窗。 - 最後,呼叫
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();
});
// ...
程式碼解析:
- 建立一個垂直方向的
Box容器,用於容納多個元件。 - 建立一個
Label用於顯示文字,並將其加入容器中。 - 使用
Image::new_from_file建立一個圖片元件,載入指定路徑的圖片,並將其加入容器中。 - 將容器加入視窗中,並顯示所有元件。
偵錯工具
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>
詳細解說
- 宣告 UI 結構:Glade XML 檔案宣告了 UI 的結構,包括視窗、容器和 widget。
- 使用 GTKBuilder 載入 UI:GTKBuilder 物件可以用來載入 XML 檔案並建立 UI。
- 分離視覺呈現和邏輯:使用 Glade 和 XML 佈局定義,可以將視覺呈現與動作和行為分開。
程式碼實作
載入 Glade XML 檔案
// 建立 GTKBuilder 物件
let builder = gtk::Builder::from_file("layout.glade");
// 從 Builder 中取得物件
let window: gtk::ApplicationWindow = builder.get_object("applicationwindow1").unwrap();
詳細解說
- 建立 GTKBuilder 物件:使用
gtk::Builder::from_file方法建立 GTKBuilder 物件,載入 Glade XML 檔案。 - 取得物件:使用
get_object方法從 Builder 中取得物件,例如視窗。