• Rust GUI库 egui 的简单应用


    简介#

    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::::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

    参考资料#

  • 相关阅读:
    uniapp小程序文件下载保存
    【GIS前言技术】到底什么是实景三维?
    【软件测试】软件测试的基础概念
    WebRTC系列-网络传输之6-Connections裁剪
    ACPI规范概览-1
    [附源码]计算机毕业设计物品捎带系统Springboot程序
    HCIP之BGP路由反射器、联邦
    人工智能算法 上市公司,人工智能算法公司排名
    深入理解Linux系统IO调用接口,文件描述符,语言缓冲区
    与HTTP相关的各种概念
  • 原文地址:https://www.cnblogs.com/timefiles/p/18070897