在 Linux 系統上,使用 Rust 搭配 GTK 框架可以開發高效能且功能豐富的桌面應用程式。本文將以建構一個簡易的購物清單應用程式為例,逐步說明如何使用 GTK-rs 繫結進行開發,涵蓋從設定開發環境、定義資料結構、建立模型到設計使用者介面的完整流程。過程中會使用 GTK-3 繫結,並解釋如何將 Rust 的結構轉換為 GTK 的 GObjects,最終建立一個具備基本功能的桌面應用程式。
透過這個範例,讀者可以學習到如何使用 Rust 和 GTK 框架建立一個簡單的購物清單應用程式。這個應用程式將包含新增、編輯和刪除專案等功能,每個專案都有一個名稱和一個計數器,可以增加或減少。我們將使用 GTK-3 繫結,並逐步引導讀者完成應用程式的開發,從設定 GTK 應用程式、定義行資料、建立模型到組裝最終應用程式。
GTK的優缺點
在決定是否使用GTK之前,我們需要考慮其優缺點。GTK的主要優點是可以建立一個與GNOME桌面環境相容的應用程式,從而繼承其主題。但是,如果你不是GNOME使用者,或者使用其他桌面環境,則這個優點可能不那麼明顯。另外,GTK的複雜性和難度可能會讓一些開發者感到不舒服。
建立一個Grocery List應用程式
在這個章節中,我們將使用GTK-rs的範例建立一個簡單的Grocery List應用程式。這個應用程式允許你新增、編輯和刪除專案,每個專案都有一個名稱和一個計數器,可以增加或減少。
應用程式結構
我們的應用程式可以分為三個部分:
- 建立行資料:包含專案名稱和計數器的資料結構。
- 建立模型:使用行資料建立一個模型。
- 建立UI:使用模型和行資料建立UI並執行應用程式。
使用GTK-3繫結
由於GTK-4存在一些問題,我們將使用GTK-3繫結。GTK-3提供了一個相對穩定的API,可以用於建立桌面應用程式。
實作細節
在實作過程中,我們需要將Rust的結構轉換為GTK的GObjects,然後使用它們建立一個完全功能性的桌面應用程式。這個過程可能會有些複雜,但透過這個章節的學習,你將能夠掌握如何使用Rust建立一個根據GTK的桌面應用程式。
使用 GTK 框架開發 Linux 桌面應用程式
在這個章節中,我們將涵蓋以下主題:
- 設定 GTK 應用程式
- 行資料為我們的購物清單
- 將行資料儲存到模型中
- 組裝最終應用程式
目標
透過這個章節,讀者將能夠瞭解如何使用 GTK 框架或 Rust 中的相關函式函式庫來開發原生的 Linux 桌面應用程式。我們將探討 GTK 框架的優缺點,並親身經歷使用 Rust 開發 GUI 應用程式的過程,包括處理不同事件的方法。
設定 GTK 應用程式
在建立新的 Cargo 應用程式之前,我們需要在 Debian Linux 系統上安裝 GTK 函式函式庫。可以使用以下命令進行安裝:
# 更新和升級套件
$ sudo apt-get update && sudo apt-get upgrade
# 安裝 gtk-3 dev 函式函式庫
$ sudo apt-get install libgtk-3-dev
在其他系統上,建議按照安裝指示進行安裝。安裝完成後,可以使用 Cargo 建立新的應用程式並新增相應的依賴:
$ cargo new grocery_list
$ cargo add gtk glib once_cell gio
瞭解每個函式函式庫的作用對於我們的開發過程非常重要:
gtk
:提供 GTK 框架的繫結,用於建立桌面應用程式。glib
:提供 GLib 和 GObject 的繫結。once_cell
:提供單次指派的延遲型別,用於我們的行資料。gio
:提供繫結,用於 GTK 的一般目的 IO、網路等功能。
在專案中,我們需要建立 row_data
和 models
目錄來管理我們的行資料和模型。每個目錄中都會包含一個 imp.rs
檔案,用於包含所有實作。專案結構如下:
$ tree
├── Cargo.lock
├── Cargo.toml
├── src
│ ├── main.rs
│ ├── model
│ │ └── imp.rs
│ ├── model.rs
│ ├── row_data
│ │ └── imp.rs
內容解密:
上述過程中,我們首先安裝了必要的 GTK 函式函式庫,然後建立了一個新的 Cargo 專案,並增加了必要的依賴。瞭解每個函式函式庫的作用對於我們的開發過程非常重要。專案結構的設計使得我們可以清晰地組織程式碼,分別管理行資料和模型。這為我們的應用程式的開發奠定了基礎。
圖表翻譯:
graph LR A[安裝GTK函式函式庫] --> B[建立Cargo專案] B --> C[新增依賴] C --> D[設計專案結構] D --> E[開發應用程式]
圖表翻譯:
上述流程圖描述了我們的開發過程,從安裝 GTK 函式函式庫開始,到建立 Cargo 專案、新增依賴,然後設計專案結構,最終到開發應用程式。每一步驟都對應到我們的實際開發過程,清晰地展示了我們的專案是如何一步步建立起來的。
建立 GTK 應用程式的 RowData Struct
介紹
在本章中,我們將建立一個名為 RowData
的 Struct,該 Struct 將用於儲存我們的雜貨清單的資料。這個 Struct 將包含兩個欄位:item
和 count
,分別代表專案的名稱和數量。
實作 RowData Struct
use glib::subclass::prelude::*;
use gtk::{glib, prelude::*};
use std::cell::RefCell;
use glib::{ParamSpec, ParamSpecString, ParamSpecUInt, Value};
#[derive(Default)]
pub struct RowData {
item: RefCell<Option<String>>,
count: RefCell<u32>,
}
在上面的程式碼中,我們定義了 RowData
Struct,並使用 RefCell
來實作內部可變性。這樣,我們就可以在不需要 mutably 借用 RowData
的情況下修改其欄位。
讓 RowData 成為 GObject
#[glib::object_subclass]
impl ObjectSubclass for RowData {
const NAME: &'static str = "RowData";
type Type = super::RowData;
type ParentType = glib::Object;
}
在這裡,我們使用 glib::object_subclass
來讓 RowData
成為一個 GObject。這樣,我們就可以使用 GObject 的功能,例如設定和取得屬性。
實作 ObjectImpl Trait
impl ObjectImpl for RowData {
fn properties() -> &'static [ParamSpec] {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<ParamSpec>> = Lazy::new(|| {
vec![
ParamSpecString::new(
"item",
"Item",
"Item",
None,
glib::ParamFlags::READWRITE,
),
ParamSpecUInt::new(
"count",
"Count",
"Count",
0,
100,
0,
glib::ParamFlags::READWRITE,
),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _obj: &Self::Type, _id: usize, value: &Value, pspec: &ParamSpec) {
match pspec.name() {
"item" => {
let item = value.get::<Option<String>>().unwrap();
self.item.replace(item);
}
"count" => {
let count = value.get::<u32>().unwrap();
self.count.replace(count);
}
_ => unimplemented!(),
}
}
}
在這裡,我們實作了 ObjectImpl
Trait,並定義了 properties()
函式來傳回 RowData
的屬性。然後,我們實作了 set_property()
函式來設定 RowData
的屬性。
圖表翻譯:
classDiagram class RowData { - item: RefCell~Option~String~ - count: RefCell~u32~ } class GObject { + properties() + set_property() } RowData --|> GObject
在這個圖表中,我們展示了 RowData
和 GObject
之間的關係。RowData
繼承自 GObject
,並實作了 ObjectImpl
Trait。
內容解密:
在這個章節中,我們建立了一個名為 RowData
的 Struct,該 Struct 將用於儲存我們的雜貨清單的資料。然後,我們讓 RowData
成為一個 GObject,並實作了 ObjectImpl
Trait。這樣,我們就可以使用 GObject 的功能,例如設定和取得屬性。
將 Rust 和 GTK 整合:定義 RowData 和 Model
在這個章節中,我們將會定義 RowData
和 Model
,並將它們整合到 GTK 中。首先,我們需要定義 RowData
的屬性和方法。
定義 RowData
// 定義 RowData 的屬性
glib::wrapper! {
pub struct RowData(ObjectSubclass<imp::RowData>);
}
// 實作 RowData 的方法
impl RowData {
pub fn new(item: &str, count: u32) -> Self {
glib::Object::new(&[("item", &item), ("count", &count)]).unwrap()
}
}
定義 Model
接下來,我們需要定義 Model
並實作 ObjectSubclass
和 ObjectImpl
。
// 定義 Model
#[derive(Debug, Default)]
pub struct Model(pub RefCell<Vec<RowData>>);
// 實作 ObjectSubclass
#[glib::object_subclass]
impl ObjectSubclass for Model {
const NAME: &'static str = "Model";
type Type = super::Model;
type ParentType = Object;
type Interfaces = (gio::ListModel,);
}
// 實作 ObjectImpl
impl ObjectImpl for Model {}
// 實作 ListModelImpl
impl ListModelImpl for Model {
fn item_type(&self) -> Type {
// 傳回 item 的型別
}
fn n_items(&self) -> u32 {
// 傳回 item 的數量
}
fn item(&self, position: u32) -> Option<Object> {
// 傳回指定位置的 item
}
}
圖表翻譯
以下是 ListModelImpl
的實作流程圖:
flowchart TD A[開始] --> B[實作 ObjectSubclass] B --> C[實作 ObjectImpl] C --> D[實作 ListModelImpl] D --> E[定義 item_type()] E --> F[定義 n_items()] F --> G[定義 item()] G --> H[完成 Model 的實作]
圖表翻譯:
這個流程圖展示瞭如何實作 Model
的 ObjectSubclass
、ObjectImpl
和 ListModelImpl
。首先,實作 ObjectSubclass
來定義 Model
的基本屬性。接下來,實作 ObjectImpl
來定義 Model
的方法。最後,實作 ListModelImpl
來定義 item_type()
、n_items()
和 item()
方法。
內容解密
在這個章節中,我們定義了 RowData
和 Model
,並將它們整合到 GTK 中。RowData
代表了一行資料,而 Model
代表了一個資料模型。透過實作 ObjectSubclass
和 ObjectImpl
,我們可以將 Model
作為一個 GObject 使用。同時,透過實作 ListModelImpl
,我們可以定義 item_type()
、n_items()
和 item()
方法來存取和管理資料。
建立一個簡單的購物清單應用程式
在這個章節中,我們將會建立一個簡單的購物清單應用程式,使用 Rust 和 GTK+。
建立 RowData Struct
首先,我們需要建立一個 RowData
Struct,用於儲存每個購物清單專案的資料。
use glib::subclass::prelude::*;
use gtk::prelude::*;
#[derive(Default)]
pub struct RowData {
pub name: String,
pub quantity: u32,
}
impl RowData {
pub fn new(name: &str, quantity: u32) -> Self {
RowData {
name: name.to_string(),
quantity,
}
}
}
建立 Model Struct
接下來,我們需要建立一個 Model
Struct,用於儲存購物清單的資料。
use glib::subclass::prelude::*;
use gtk::prelude::*;
#[derive(Default)]
pub struct Model {
pub data: Vec<RowData>,
}
impl Model {
pub fn new() -> Self {
Model { data: vec![] }
}
pub fn append(&mut self, row: RowData) {
self.data.push(row);
}
pub fn remove(&mut self, index: usize) {
self.data.remove(index);
}
}
建立 GTK+ 應用程式
現在,我們可以建立一個 GTK+ 應用程式,使用我們剛剛建立的 Model
Struct。
use gtk::prelude::*;
fn main() {
// 建立 GTK+ 應用程式
let app = gtk::Application::new(Some("com.grocery_list"), Default::default());
// 建立主視窗
let window = gtk::Window::new(gtk::WindowType::Toplevel);
window.set_title("購物清單");
window.set_default_size(400, 300);
// 建立購物清單模型
let model = Model::new();
// 建立購物清單檢視
let list_view = gtk::ListView::new();
list_view.set_model(Some(&model));
// 建立購物清單專案
let row = RowData::new("蘋果", 2);
model.append(row);
// 顯示主視窗
window.show_all();
// 執行 GTK+ 主迴圈
app.run();
}
執行應用程式
現在,你可以執行應用程式,使用以下命令:
cargo run
這將會顯示一個簡單的購物清單應用程式,包含一個購物清單專案。
圖表翻譯:
graph LR A[購物清單應用程式] --> B[建立 RowData Struct] B --> C[建立 Model Struct] C --> D[建立 GTK+ 應用程式] D --> E[建立購物清單檢視] E --> F[顯示主視窗] F --> G[執行 GTK+ 主迴圈]
內容解密:
在這個章節中,我們建立了一個簡單的購物清單應用程式,使用 Rust 和 GTK+。我們首先建立了一個 RowData
Struct,用於儲存每個購物清單專案的資料。然後,我們建立了一個 Model
Struct,用於儲存購物清單的資料。接下來,我們建立了一個 GTK+ 應用程式,使用我們剛剛建立的 Model
Struct。最後,我們執行了應用程式,使用以下命令:cargo run
。
GTK+ 應用程式開發:建立使用者介面
在上一節中,我們已經建立了 GTK+ 應用程式的基本結構。現在,我們將繼續建立使用者介面。
建立應用程式視窗
首先,我們需要建立一個應用程式視窗。這可以使用 gtk::ApplicationWindow::new()
函式來完成:
let window = gtk::ApplicationWindow::new(application);
然後,我們可以設定視窗的屬性,例如標題、邊框寬度、位置和預設大小:
window.set_title("Grocery List");
window.set_border_width(10);
window.set_position(gtk::WindowPosition::Center);
window.set_default_size(320, 480);
建立垂直盒子
接下來,我們需要建立一個垂直盒子來存放我們的專案。這可以使用 gtk::Box::new()
函式來完成:
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 5);
建立模型和列表盒
然後,我們需要建立一個模型和列表盒。這可以使用 model::Model::new()
和 gtk::ListBox::new()
函式來完成:
let model = model::Model::new();
let listbox = gtk::ListBox::new();
繫結模型和列表盒
接下來,我們需要繫結模型和列表盒。這可以使用 bind_model()
函式來完成:
listbox.bind_model(Some(&model), move |item| {
// ...
});
在這個範例中,我們使用 clone!()
宏來傳遞強或弱參照的值給 closure。然後,我們定義了一個 closure,裡面會建立一個 ListBoxRow
和一個水平盒子,然後將專案繫結到水平盒子上。
建立水平盒子和專案
在 closure 裡面,我們需要建立一個水平盒子和專案。這可以使用 gtk::Box::new()
和 gtk::Label::new()
函式來完成:
let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 5);
let label = gtk::Label::new(None);
然後,我們可以繫結專案到水平盒子上:
item.bind_property("item", &label, "label")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE)
.build();
建立旋轉按鈕和編輯按鈕
最後,我們需要建立一個旋轉按鈕和編輯按鈕。這可以使用 gtk::SpinButton::with_range()
和 gtk::Button::with_label()
函式來完成:
let spin_button = gtk::SpinButton::with_range(0.0, 100.0, 1.0);
let edit_button = gtk::Button::with_label("Edit");
然後,我們可以繫結旋轉按鈕和編輯按鈕到專案上:
item.bind_property("count", &spin_button, "value")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL)
.build();
圖表翻譯:
graph LR A[GTK+ 應用程式] --> B[建立視窗] B --> C[設定視窗屬性] C --> D[建立垂直盒子] D --> E[建立模型和列表盒] E --> F[繫結模型和列表盒] F --> G[建立水平盒子和專案] G --> H[繫結專案到水平盒子] H --> I[建立旋轉按鈕和編輯按鈕] I --> J[繫結旋轉按鈕和編輯按鈕到專案]
這個圖表展示了 GTK+ 應用程式的建立流程,從建立視窗到繫結旋轉按鈕和編輯按鈕到專案。
建立對話方塊以編輯專案
為了讓使用者能夠編輯專案,我們需要建立一個對話方塊。這個對話方塊將包含多個按鈕,允許使用者輸入新標籤或增減專案的數量。
建立對話方塊
首先,我們使用 gtk::Dialog::with_buttons()
建立一個新對話方塊。這個函式需要標題、父視窗、標誌和按鈕等引數。以下是建立對話方塊的程式碼:
let dialog = gtk::Dialog::with_buttons(
Some("Edit Item"), // 標題
Some(&window), // 父視窗
gtk::DialogFlags::MODAL, // 標誌
&[("Close", ResponseType::Close)], // 按鈕
);
設定預設回應
接下來,我們需要設定預設回應為 ResponseType::Close
,並使用 connect_response()
連線回應事件:
dialog.set_default_response(ResponseType::Close);
建立內容區域
在對話方塊中,我們需要建立一個內容區域,以便新增不同的按鈕和輸入欄位。以下是建立內容區域的程式碼:
let content_area = gtk::Box::new(gtk::Orientation::Vertical, 0);
建立輸入欄位
現在,我們需要建立一個輸入欄位,允許使用者輸入新標籤。以下是建立輸入欄位的程式碼:
let entry = gtk::Entry::new();
item.bind_property("item", &entry, "text")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL)
.build();
建立計數按鈕
接下來,我們需要建立一個計數按鈕,允許使用者增減專案的數量。以下是建立計數按鈕的程式碼:
let spin_button = gtk::SpinButton::with_range(0.0, 100.0, 1.0);
item.bind_property("count", &spin_button, "value")
.flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL)
.build();
新增內容到對話方塊
現在,我們需要將內容新增到對話方塊中。以下是新增內容的程式碼:
content_area.add(&entry);
content_area.add(&spin_button);
顯示對話方塊
最後,我們需要顯示對話方塊。以下是顯示對話方塊的程式碼:
dialog.show_all();
圖表翻譯:
以下是對話方塊的流程圖:
graph LR A[使用者按下編輯按鈕] --> B[建立對話方塊] B --> C[設定預設回應] C --> D[建立內容區域] D --> E[建立輸入欄位] E --> F[建立計數按鈕] F --> G[新增內容到對話方塊] G --> H[顯示對話方塊]
內容解密:
以上程式碼建立了一個對話方塊,允許使用者編輯專案的標籤和數量。對話方塊包含一個輸入欄位和一個計數按鈕,允許使用者輸入新標籤和增減專案的數量。當使用者按下編輯按鈕時,對話方塊會顯示出來,允許使用者進行編輯。
建立一個簡單的 GTK 應用程式
從使用者經驗視角來看,使用 Rust 與 GTK 開發 Linux 桌面應用程式,兼具效能和原生介面整合的優勢。本文逐步講解了從設定開發環境、定義資料結構、建立模型到設計使用者介面的完整流程,更深入探討了GTK-3繫結的使用、RowData 與 Model 的整合,以及對話方塊的建立等關鍵技術環節。分析顯示,GTK-3 的穩定性和 GTK-rs 的易用性,讓 Rust 在 GUI 程式設計領域更具競爭力。然而,GTK 的複雜性以及 GTK-4 潛在的問題,仍是開發者需要面對的挑戰。未來,隨著 GTK-4 的逐步成熟和社群支援的完善,Rust 與 GTK 的結合有望在 Linux 桌面應用程式開發中扮演更重要的角色。對於追求原生體驗和高效能的開發者而言,採用 GTK 框架並持續關注其發展趨勢,將是提升應用程式價值的明智之舉。