Rust GUI库egui/eframe初探入门(二):更换图标和字体,实现中文界面
在上一篇中,我们为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))));
}
编译运行试一下:
我们可以观察到,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 }));
编译运行后,发现窗口图标已经替换为我们自定义的图标:
静态插入图片
我们目前已经实现了静态地插入图标给程序使用。同样我们也可以在编译期静态插入图片,使程序在运行中显示图片。为了方便地实现图片显示,我们需要添加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());
});
}
}
此时编译运行程序,界面如下:
结语
目前我们已经能够静态地在编译期从文件读入图片了。下一次我们将尝试在运行时动态地从文件读入图片。