Rust GUI庫 egui 的簡單應用

二次元攻城狮發表於2024-03-13

目錄
  • 簡介
  • 簡單示例
    • 建立專案
    • 介面設計
    • 切換主題
    • 自定義字型
    • 自定義圖示
  • 經典佈局
    • 定義導航變數
    • 實現導航介面
    • 實現導航邏輯
    • 實現主框架佈局
    • 除錯執行
  • 參考資料

簡介

egui(發音為“e-gooey”)是一個簡單、快速且高度可移植的 Rust 即時模式 GUI 庫,跨平臺、Rust原生,適合一些小工具和遊戲引擎GUI:
文件:https://docs.rs/egui/latest/egui/
演示:https://www.egui.rs/#demo
github:https://github.com/emilk/egui

關於即時模式GUI,可以參考 使用C++介面框架ImGUI開發一個簡單程式 裡面的介紹,ImGUI是C++的一個即時模式GUI庫。

image

簡單示例

建立專案

首先使用cargo工具快速構建專案:

cargo new eguitest

然後新增依賴:

cargo add eframe

egui只是一個圖形庫,而不是圖形介面開發框架,eframe是與egui配套使用的圖形框架

為了靜態插入圖片,還需要增加egui_extras依賴:

cargo add egui_extras

然後在Cargo.toml檔案中編輯features

egui_extras = { version = "0.26.2", features = ["all_loaders"] }

介面設計

開啟src/main.rc,編寫第一個eframe示例程式:

//隱藏Windows上的控制檯視窗
#![windows_subsystem = "windows"]

use eframe::egui;

fn main() -> Result<(), eframe::Error> {
    // 建立視口選項,設定視口的內部大小為320x240畫素
    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]),
        ..Default::default()
    };

    // 執行egui應用程式
    eframe::run_native(
        "My egui App", // 應用程式的標題
        options, // 視口選項
        Box::new(|cc| {
            // 為我們提供影像支援
            egui_extras::install_image_loaders(&cc.egui_ctx);
            // 建立並返回一個實現了eframe::App trait的物件
            Box::new(MyApp::new(cc))
        }),
    )
}

//定義 MyApp 結構體
struct MyApp {
    name: String,
    age: u32,
}

//MyApp 結構體 new 函式
impl MyApp {
    fn new(cc: &eframe::CreationContext<'_>) -> Self {        
        // 結構體賦初值
        Self {
            name: "Arthur".to_owned(),
            age: 42,
        }
    }
}

//實現 eframe::App trait 
impl eframe::App for MyApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        // 在中央皮膚上顯示egui介面
        egui::CentralPanel::default().show(ctx, |ui| {
            // 顯示標題
            ui.heading("My egui Application"); 
            // 建立一個水平佈局
            ui.horizontal(|ui| {
                // 顯示姓名標籤
                let name_label = ui.label("Your name: "); 
                // 顯示姓名輸入框(單行文字框)
                ui.text_edit_singleline(&mut self.name) 
                    .labelled_by(name_label.id); // 關聯標籤
            });

            // 顯示年齡滑塊
            ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); 

            if ui.button("Increment").clicked() {
                // 點選按鈕後將年齡加1
                self.age += 1;
            }

            // 顯示問候語
            ui.label(format!("Hello '{}', age {}", self.name, self.age));            
            // 顯示圖片,圖片放在main.rs的同級目錄下(可以自定義到其它目錄)
            ui.image(egui::include_image!("ferris.png")); 
        });
    }
}

執行結果如下:
image

切換主題

egui提供了明亮、暗黃兩種主題,在APP結構體上新增 theme_switcher 方法:

impl MyApp {
    // 切換主題
    fn theme_switcher(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
        ui.horizontal(|ui| {
            if ui.button("Dark").clicked() {
                ctx.set_visuals(egui::Visuals::dark());
            }
            if ui.button("Light").clicked() {
                ctx.set_visuals(egui::Visuals::light());
            }
        });
    }
}

然後在update函式中呼叫:

egui::CentralPanel::default().show(ctx, |ui| {
   //...
   // 切換主題
   self.theme_switcher(ui, ctx);
   // 顯示圖片
   ui.image(egui::include_image!("ferris.png")); 
});

egui的Style結構體可以自定義主題,不過一般預設主題就夠用了。

自定義字型

egui預設不支援中文,實現一個 setup_custom_fonts 函式:

//自定義字型
fn setup_custom_fonts(ctx: &egui::Context) {
    // 建立一個預設的字型定義物件
    let mut fonts = egui::FontDefinitions::default();

    //安裝的字型支援.ttf和.otf檔案
    //檔案放在main.rs的同級目錄下(可以自定義到其它目錄)
    fonts.font_data.insert(
        "my_font".to_owned(),
        egui::FontData::from_static(include_bytes!(
            "msyh.ttc"  
        )),
    );

    // 將字型新增到 Proportional 字型族的第一個位置
    fonts
        .families
        .entry(egui::FontFamily::Proportional)
        .or_default()
        .insert(0, "my_font".to_owned());

    // 將字型新增到 Monospace 字型族的末尾
    fonts
        .families
        .entry(egui::FontFamily::Monospace)
        .or_default()
        .push("my_font".to_owned());

    // 將載入的字型設定到 egui 的上下文中
    ctx.set_fonts(fonts);
}

然後再MyApp結構體的new方法中呼叫:

//...
impl MyApp {
    fn new(cc: &eframe::CreationContext<'_>) -> Self {
        //載入自定義字型
        setup_custom_fonts(&cc.egui_ctx);     
        //...
    }
}
//...

執行結果:
image

自定義圖示

先匯入image庫,在終端中執行:

cargo add image

還需要匯入std::sync::Arc、eframe::egui::IconData ,庫引入區如下:

use eframe::egui;
use eframe::egui::IconData;
use std::sync::Arc;
use image;

在main()函式中將native_options的宣告改為可變變數的宣告,並加入改變圖示程式碼:

fn main() -> Result<(), eframe::Error> {
    // 建立視口選項,設定視口的內部大小為320x240畫素
    let mut options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]),
        ..Default::default()
    };

    //匯入圖示,圖片就用上面的
    let icon_data = include_bytes!("ferris.png");
    let img = image::load_from_memory_with_format(icon_data, image::ImageFormat::Png).unwrap();
    let rgba_data = img.into_rgba8();
    let (width, height) =(rgba_data.width(),rgba_data.height());
    let rgba: Vec<u8> = rgba_data.into_raw();
    options.viewport.icon=Some(Arc::<IconData>::new(IconData { rgba, width, height}));
    
    // ...    
}

經典佈局

在上面示例的基礎上,實現一個上中下或左中右的經典三欄佈局,main函式不需要修改,只需要修改MyApp結構體的定義即可。

定義導航變數

先定義一個導航列舉,用來在標記當前要顯示的介面:

//導航列舉
enum Page {
    Test,
    Settings,
}

為了方便理解示例,在 MyApp 中只定義一個 page 欄位,並同步修改new函式:

//定義 MyApp 結構體
struct MyApp {
    page:Page,
}
//MyApp 結構體 new 函式
impl MyApp {
    fn new(cc: &eframe::CreationContext<'_>) -> Self {
        setup_custom_fonts(&cc.egui_ctx);     
        // 結構體賦初值
        Self {
            page:Page::Test,
        }
    }
}

實現導航介面

在 MyApp 中定義導航欄的介面,

impl MyApp {  

    //左側導航按鈕,egui沒有內建樹控制元件,有需要可以自己實現
    fn left_ui(&mut self, ui: &mut egui::Ui)  {   
        //一個垂直佈局的ui,內部控制元件水平居中並對齊(填充全寬)
        ui.vertical_centered_justified(|ui| {          
            
            if ui.button("測試").clicked() {
                self.page=Page::Test;
            }

            if ui.button("設定").clicked() {
                self.page=Page::Settings;
            }
            //根據需要定義其它按鈕
        });
    }

    //...其它方法
}

實現導航邏輯

在 MyApp 中定義一個 show_page 方法來進行介面排程,每個介面再單獨實現自己的UI函式

impl MyApp {  
    //...其它方法

    //根據導航顯示頁面
    fn show_page(&mut self, ui: &mut egui::Ui)  {   

        match self.page {
            Page::Test => {
                self.test_ui(ui);
            }
            Page::Settings => {
                //...
            }
        }       
    }

    //為了方便理解示例這裡只顯示一張圖片
    fn test_ui(&mut self, ui: &mut egui::Ui)  {         
        ui.image(egui::include_image!("ferris.png"));
    }

    //...其它方法
}

實現主框架佈局

在 MyApp 中間實現 main_ui 方法,可以根據自己的需要調整各個欄的位置:

impl MyApp {  
    //...其它方法
    //主框架佈局
    fn main_ui(&mut self, ui: &mut egui::Ui)  {        
        // 新增皮膚的順序非常重要,影響最終的佈局
        egui::TopBottomPanel::top("top_panel")
        .resizable(true)
        .min_height(32.0)
        .show_inside(ui, |ui| {
            egui::ScrollArea::vertical().show(ui, |ui| {
                ui.vertical_centered(|ui| {
                    ui.heading("標題欄");
                });
                ui.label("標題欄內容");
            });
        });

        egui::SidePanel::left("left_panel")
        .resizable(true)
        .default_width(150.0)
        .width_range(80.0..=200.0)
        .show_inside(ui, |ui| {
            ui.vertical_centered(|ui| {
                ui.heading("左導航欄");
            });
            egui::ScrollArea::vertical().show(ui, |ui| {
                self.left_ui(ui);
            });
        });

        egui::SidePanel::right("right_panel")
        .resizable(true)
        .default_width(150.0)
        .width_range(80.0..=200.0)
        .show_inside(ui, |ui| {
            ui.vertical_centered(|ui| {
                ui.heading("右導航欄");
            });
            egui::ScrollArea::vertical().show(ui, |ui| {
                ui.label("右導航欄內容");
            });
        });

        egui::TopBottomPanel::bottom("bottom_panel")
        .resizable(false)
        .min_height(0.0)
        .show_inside(ui, |ui| {
            ui.vertical_centered(|ui| {
                ui.heading("狀態列");
            });
            ui.vertical_centered(|ui| {
                ui.label("狀態列內容");
            });
        });

        egui::CentralPanel::default().show_inside(ui, |ui| {
            ui.vertical_centered(|ui| {
                ui.heading("主皮膚");
            });
            egui::ScrollArea::vertical().show(ui, |ui| {
                ui.label("主皮膚內容");

                self.show_page(ui);
            });
        });
    }       
}

除錯執行

在 main 函式中稍微調整一下視窗大小:

// 建立視口選項
let mut options = eframe::NativeOptions {
    viewport: egui::ViewportBuilder::default().with_inner_size([1000.0, 500.0]),
    ..Default::default()
};

在 update 函式中呼叫 main_ui 函式:

impl eframe::App for MyApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        //設定主題
        ctx.set_visuals(egui::Visuals::dark());
        // 在中央皮膚上顯示egui介面
       egui::CentralPanel::default().show(ctx, |ui| {
        self.main_ui(ui); 
       });        
    }
}

執行結果如下:
image

參考資料

  • Rust GUI庫egui/eframe初探入門(〇):生成第一個介面

  • Rust GUI庫egui/eframe初探入門(二):更換圖示和字型,實現中文介面

相關文章