在 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應用程式。這個應用程式允許你新增、編輯和刪除專案,每個專案都有一個名稱和一個計數器,可以增加或減少。

應用程式結構

我們的應用程式可以分為三個部分:

  1. 建立行資料:包含專案名稱和計數器的資料結構。
  2. 建立模型:使用行資料建立一個模型。
  3. 建立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_datamodels 目錄來管理我們的行資料和模型。每個目錄中都會包含一個 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 將包含兩個欄位:itemcount,分別代表專案的名稱和數量。

實作 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

在這個圖表中,我們展示了 RowDataGObject 之間的關係。RowData 繼承自 GObject,並實作了 ObjectImpl Trait。

內容解密:

在這個章節中,我們建立了一個名為 RowData 的 Struct,該 Struct 將用於儲存我們的雜貨清單的資料。然後,我們讓 RowData 成為一個 GObject,並實作了 ObjectImpl Trait。這樣,我們就可以使用 GObject 的功能,例如設定和取得屬性。

將 Rust 和 GTK 整合:定義 RowData 和 Model

在這個章節中,我們將會定義 RowDataModel,並將它們整合到 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 並實作 ObjectSubclassObjectImpl

// 定義 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 的實作]

圖表翻譯:

這個流程圖展示瞭如何實作 ModelObjectSubclassObjectImplListModelImpl。首先,實作 ObjectSubclass 來定義 Model 的基本屬性。接下來,實作 ObjectImpl 來定義 Model 的方法。最後,實作 ListModelImpl 來定義 item_type()n_items()item() 方法。

內容解密

在這個章節中,我們定義了 RowDataModel,並將它們整合到 GTK 中。RowData 代表了一行資料,而 Model 代表了一個資料模型。透過實作 ObjectSubclassObjectImpl,我們可以將 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 框架並持續關注其發展趨勢,將是提升應用程式價值的明智之舉。