- 簡介
- 簡單示例
- 建立專案
- 介面設計
- 切換主題
- 自定義字型
- 自定義圖示
- 經典佈局
- 定義導航變數
- 實現導航介面
- 實現導航邏輯
- 實現主框架佈局
- 除錯執行
- 參考資料
簡介
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庫。
簡單示例
建立專案
首先使用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"));
});
}
}
執行結果如下:
切換主題
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庫,在終端中執行:
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);
});
}
}
執行結果如下:
參考資料
-
Rust GUI庫egui/eframe初探入門(〇):生成第一個介面
-
Rust GUI庫egui/eframe初探入門(二):更換圖示和字型,實現中文介面