Rust GUI库egui/eframe初探入门(二):更换图标和字体,实现中文界面

发布时间 2024-01-04 15:31:32作者: AbsalomT

在上一篇中,我们为GUI界面添加了一些控件,理解了egui/eframe的工作方式:
Rust GUI库egui/eframe初探入门(一):添加一些控件,理解egui/eframe的工作方式
但由于egui默认的字体并不支持中文或其它非拉丁字符,所以我们在界面中始终无法正常显示中文,现在我们来解决这一问题。

支持自定义字体

首先我们新建一个项目,参照Rust GUI库egui/eframe初探入门(〇):生成第一个界面的方式完成一个egui/eframe界面程序的最小实现。
然后我们自定义这样一个函数用来读入及指定字体:

fn load_fonts(ctx: &egui::Context) {
    let mut fonts = egui::FontDefinitions::default();
    fonts.font_data.insert("my_font".to_owned(),
    egui::FontData::from_static(include_bytes!("xxxxx.ttf")));
    fonts.families.get_mut(&egui::FontFamily::Proportional).unwrap()
        .insert(0, "my_font".to_owned());
    fonts.families.get_mut(&egui::FontFamily::Monospace).unwrap()
        .push("my_font".to_owned());
    ctx.set_fonts(fonts);
}

第一条语句定义了一个默认的字体定义类型,随后的一条一句向其中插入了一个新的字体:fonts.font_data.insert("my_font".to_owned(),egui::FontData::from_static(include_bytes!("xxxxx.ttf")));当中的include_bytes!("xxxxx.ttf")是在静态编译环境下,加入我们指定字体文件的代码。也就是说我们编译好的程序不会再去动态地读取字体文件,而是在编译期就已经将字体文件静态读入。所以我们只要将我们的字体文件放在main.rs的同级目录下就可以了。当然也可以放在其目录下的相对路径或其它绝对路径,只要改变导入字体文件代码的路径就可以了。"xxxxx.ttf"就是字体文件的文件名(也可以包含路径),字体文件支持ttf格式或otf格式。

代码中的fonts.font_data实际上是一个BTreeMap类型的数据容器,也就是以BTree数据结构存储的键值对,使用方式来说类似于其它编程语言中的字典。所以在插入我们的字体时,以"my_font"为键,后续操作中也可以用"my_font"这个键来检索我们新插入的这个字体。

后续的fonts.families.get_mut(&egui::FontFamily::Proportional).unwrap().insert(0, "my_font".to_owned());表示将我们导入的字体加入到比例字体系族的第一个位置。
fonts.families.get_mut(&egui::FontFamily::Monospace).unwrap().push("my_font".to_owned());表示将我们导入的字体加入到等宽字体系族的最后一个位置。

我们将load_fonts()这个函数在MyEguiApp结构体调用new()方法初始化时执行一次,就能够保障运行窗口前字体文件能被正确初始化。代码如下:

impl MyEguiApp {
    fn new(cc: &eframe::CreationContext<'_>) -> Self {
        load_fonts(&cc.egui_ctx);
        Self::default()
    }
}

然后在update的UI布局中,实现中文的控件:

impl eframe::App for MyEguiApp {
   fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
       egui::CentralPanel::default().show(ctx, |ui| {
           ui.heading("还是中文好,洋文看不懂");
           ui.label("在你的世界学你说abcd,在我的土地对不起请说华语");
       });
   }
}

同时,我们可以在main()函数中将程序的窗口也改为中文的(以前的篇章已经实现过):

fn main() {
    let native_options = eframe::NativeOptions::default();
    eframe::run_native("不要再卷了", native_options, Box::new(|cc| Box::new(MyEguiApp::new(cc))));
}

编译运行试一下:
image
我们可以观察到,UI中的文字已经切换成为我导入的字体,可以正常地显示中文。但标题栏的字体不受影响。所以即便不导入我们的自定义字体,标题栏都是可以正常显示中文的。

自定义图标

当前界面支持中文了,标题栏的名称也已经改了,但标题栏上的图标还是eframe的默认图标。我们可能会希望改成我们自己喜欢的图标。现在来实现一下。
首先为了操作图片,我们要导入image库。需要在终端中运行cargo add image;然后在程序开头导入image库。同时我们还需要导入智能指针Arc。以及为了方便,我们导入一下eframe::egui::IconData,库引入区如下:

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

然后我们在main()函数中将native_options的声明改为可变变量的声明,并加入改变图标代码如下:

fn main() {
    let mut native_options = eframe::NativeOptions::default();
    let icon_data = include_bytes!("icon.png");
    let img = image::load_from_memory_with_format(icon_data, image::ImageFormat::Png).unwrap();
    let rgba_data = img.into_rgba8();
    let (w,h)=(rgba_data.width(),rgba_data.height());
    let raw_data: Vec<u8> = rgba_data.into_raw();
    native_options.viewport.icon=Some(Arc::<IconData>::new(IconData { rgba:  raw_data, width: w, height: h }));
    eframe::run_native("不要再卷了", native_options, Box::new(|cc| Box::new(MyEguiApp::new(cc))));
}

在上述代码中为了方便理解,将导入图片的过程拆分成了若干行,而没有采用链式传值。
我们首先是将图标的png文件以二进制形式读入进了icon_data变量。由于此处读取icon图片和我们前面读入字体一样,是编译期静态读取,所以和导入字体时一样,需要确保编译期在我们输入的路径下文件存在,才能正常编译。上面的"icon.png"则代表文件为处在和main.rs同级目录下的icon.png。同样我们可以增加相对路径或绝对路径,将图标文件放在别处,但都要确保编译时这个路径可以找到该文件。
下面一行的let img = image::load_from_memory_with_format(icon_data, image::ImageFormat::Png).unwrap();是将读入的文件以png形式解析后,存放入DynamicImage类型的变量img内。随后将该变量类型转换为rgba8再赋值给rgba_data这个变量。rgba_data变量就存储了图片的宽高信息和各像素的RGBA值。将宽高信息取出后,再用let raw_data: Vec<u8> = rgba_data.into_raw();将RGBA值全部传递给一个u8类型的可变数组。
最后再利用这些数据创建一个IconData的结构体并用Arc指针封装后传递给native_options.viewport.icon如下:native_options.viewport.icon=Some(Arc::<IconData>::new(IconData { rgba: raw_data, width: w, height: h }));
编译运行后,发现窗口图标已经替换为我们自定义的图标:
image

静态插入图片

我们目前已经实现了静态地插入图标给程序使用。同样我们也可以在编译期静态插入图片,使程序在运行中显示图片。为了方便地实现图片显示,我们需要添加egui_extras库,来实现image loader的功能。
我们在cargo.toml中的[dependencies]下,添加egui_extras = { version = "0.24.2", features = ["all_loaders"] }
然后在MyEguiApp结构体的new()下,添加如下代码:
egui_extras::install_image_loaders(&cc.egui_ctx);
保证在初始化时安装image_loader。
在结构体MyEguiApp中增加一个ImageSource的变量img:egui::widgets::ImageSource<'static>,由于该变量类型不支持defalut方法,所以删除掉#[derive(Default)]宏。并在结构体的new()方法中手动定义初始化需要返回的结构体,其中暂时包含一个变量,使用img: include_image!("JAYChow.jpg")来赋初值。同样双引号中为我放置在main.rs同层级目录中的一张图片。Rust会在编译的时候静态读取该图片。
之后我们在ui区增加上ui.image(self.img.to_owned());即可实现显示图片。完整的代码如下:

use eframe::egui::{self, include_image,IconData};
use std::sync::Arc;
use image;

fn load_fonts(ctx: &egui::Context) {
    let mut fonts = egui::FontDefinitions::default();
    fonts.font_data.insert("my_font".to_owned(),
    egui::FontData::from_static(include_bytes!("ChillRoundGothic_Bold.ttf")));
    fonts.families.get_mut(&egui::FontFamily::Proportional).unwrap()
        .insert(0, "my_font".to_owned());
    fonts.families.get_mut(&egui::FontFamily::Monospace).unwrap()
        .push("my_font".to_owned());
    ctx.set_fonts(fonts);
}

fn main() {
    let mut native_options = eframe::NativeOptions::default();
    let icon_data = include_bytes!("icon.png");
    let img = image::load_from_memory_with_format(icon_data, image::ImageFormat::Png).unwrap();
    let rgba_data = img.into_rgba8();
    let (w,h)=(rgba_data.width(),rgba_data.height());
    let raw_data: Vec<u8> = rgba_data.into_raw();
    native_options.viewport.icon=Some(Arc::<IconData>::new(IconData { rgba:  raw_data, width: w, height: h }));
    eframe::run_native("不要再卷了", native_options, Box::new(|cc| Box::new(MyEguiApp::new(cc))));
}

struct MyEguiApp {
    img:egui::widgets::ImageSource<'static>,
}

impl MyEguiApp {
    fn new(cc: &eframe::CreationContext<'_>) -> Self {
        load_fonts(&cc.egui_ctx);
        egui_extras::install_image_loaders(&cc.egui_ctx);
        Self { img: include_image!("JAYChow.jpg") }
    }
}

impl eframe::App for MyEguiApp {
   fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
       egui::CentralPanel::default().show(ctx, |ui| {
           ui.heading("还是中文好,洋文看不懂");
           ui.label("在你的世界学你说abcd,在我的土地对不起请说华语");
           ui.image(self.img.to_owned());
       });
   }
}

此时编译运行程序,界面如下:
image

结语

目前我们已经能够静态地在编译期从文件读入图片了。下一次我们将尝试在运行时动态地从文件读入图片。