Rust GUI库egui/eframe初探入门(一):添加一些控件,理解egui/eframe的工作方式
在上一篇中,我们实现了一个最简单的egui/eframe界面应用示例:
Rust GUI库egui/eframe初探入门(〇):生成第一个界面
现在,我们来对上一篇中的代码进行一些小的修改,让界面变得不一样。
修改初始化界面
首先我们将main()函数中的代码修改一下:
fn main() {
let mut native_options = eframe::NativeOptions::default();
native_options.centered=true;
eframe::run_native("一个小例子", native_options, Box::new(|cc| Box::new(MyEguiApp::new(cc))));
}
我们首先将native_options改为可变变量,然后在赋初值为default后,将其centered属性改为true。此时我们就可以编译运行一下我们的程序,可以观察到窗口出现的位置会有区别。
随后我们再将下一行代码中的应用名称改为一个中文名"一个小例子",再次编译运行程序,会发现窗口标题栏名称已经改变为了我们所设置的名称。
添加控件
我们找到update函数下的egui::CentralPanel::default().show()方法的闭包内部,在ui.heading("Hello World!");
这行代码下方,增加新的控件代码。例如,我们可以增加一个按钮ui.button("Button");
。在这里我们使用for循环增加10个按钮,然后看看界面效果:
impl eframe::App for MyEguiApp {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Hello World!");
for i in 0..10{
ui.button("Button");
}
});
}
}
界面如下:
我们发现所有的控件默认竖直往下排列。
当然,目前的按钮可以点击,但没有任何效果。那么是否需要给按钮增加回调函数呢?实际上egui的控件交互并不使用回调函数,而是直接使用if语句在每一帧更新执行update函数时进行各类交互判断,从而执行不同的逻辑。
接下来,我们做两件事情,其一是改变控件的排列方式,不再让其竖直向下排列;其二是为按钮添加一些功能。
首先我们使用ui.horizontal()这个方法对控件进行布局。写入ui.horizontal()后同样使用闭包在horizontal内部添加控件,当中的控件会横向排布。与ui.horizontal()相对应的方法还有ui.vertical()方法,在其内部可以将控件竖直排列。两种方法配合使用可以形成多种多样的布局形式。现在我们实现两个横向排列的按钮如下:
impl eframe::App for MyEguiApp {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Hello World!");
ui.horizontal(|ui|{
ui.button("Dark");
ui.button("Light");
});
});
}
}
运行后的界面如下:
添加响应
接下来我们为按钮添加执行响应:
我们将ui.button("xxx");
改为if ui.button("xxx").clicked(){...}
,即可在花括号内部写入按钮被按下时所执行的代码。
按照官方文档的说法if ui.button("click me").clicked() { take_action() }
实际上是let button = egui::Button::new("click me"); if ui.add(button).clicked() { take_action() }
的简写。
而上述代码又是let button = egui::Button::new("click me"); let response = button.ui(ui); if response.clicked() { take_action() }
的简写。
现在我们为之前我们添加的两个按钮增加改变主题的功能,改写后的代码如下:
impl eframe::App for MyEguiApp {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Hello World!");
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());
}
});
});
}
}
其中的ctx.set_visuals(egui::Visuals::light());
和ctx.set_visuals(egui::Visuals::dark());
分别为将应用的主题色设置为明亮和黑暗两种风格。现在我们编译运行程序,即能够在界面中点击按钮切换主题风格。
利用MyEguiApp结构体存储变量
为了执行某些逻辑,我们可能需要一些独立于各帧运行之外的全局变量。实际上这样的变量我们可以写入MyEguiApp结构体中。
例如,我们在MyEguiApp结构体中添加一个无符号整型变量,并且在其new()方法中正确初始化,代码如下:
struct MyEguiApp {
frames:u64,
}
impl MyEguiApp {
fn new(cc: &eframe::CreationContext<'_>) -> Self {
Self { frames:0 }
}
}
然后我们在update()的UI区添加入两条语句,分别是:ui.label(self.frames.to_string());
和self.frames+=1;
完整代码如下:
impl eframe::App for MyEguiApp {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Hello World!");
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());
}
});
ui.label(self.frames.to_string());
});
self.frames+=1;
}
}
这样每次更新渲染时,都会让结构体当中的frames变量自加1,同时会在界面中用一个label控件将当前frames的值显示出来。
此时的界面如下:
我们现在可以观察到,每当我的鼠标划过窗口区域内时,帧数都在不停变化,而鼠标在窗体外滑动时,帧数并不会变化。另外只要我们移动窗口,改变窗口大小,点击内区域、操作控件等,都会让帧数变化。甚至我们按键盘上的按键时,也会让帧数变化。但不与窗口产生交互时,则帧数并不变化。
这也说明egui/eframe虽然是即时模式的GUI库,每一帧会实时绘制UI区,但并不是每时每刻都在重新绘制UI区,而只是在有任何交互动作发生时,才会去执行更新函数。
既然观察到键盘有输入时,帧数也在快速变化,我们来尝试写入一些与键盘按键交互的逻辑。在UI区加入如下代码:
if ui.input(|k|{k.key_pressed(egui::Key::L)}){
ctx.set_visuals(egui::Visuals::light());
}
if ui.input(|k|{k.key_pressed(egui::Key::D)}){
ctx.set_visuals(egui::Visuals::dark());
}
上述代码在捕捉到键盘按下L或者D时,分别执行切换主题到Light或Dark,和前述的按钮功能相同。可以编译运行进行验证。
接下来我们再尝试利用组合快捷键动态添加或删除控件。在此之前我们把前面单个按键D与L改变主题的部分代码删除或注释掉。因为后面我们会使用文本输入框,输入一些单词时会改变主题。我们现在暂时不需要这样的单一键位快捷键了。
我们首先在MyEguiApp结构体中增加一个字符串的动态数组,并合理初始化。
struct MyEguiApp {
frames:u64,
strs:Vec<String>
}
impl MyEguiApp {
fn new(cc: &eframe::CreationContext<'_>) -> Self {
Self { frames:0,strs:vec![] }
}
}
随后在UI代码区添加
for str in &mut self.strs{
ui.horizontal(|ui|{
ui.text_edit_singleline(str);
if *str!="".to_string(){
ui.label("Hello ".to_string()+str);
}else{
ui.label("Please enter your name.");
}
});
}
if ui.input_mut(|k|{
k.consume_key(egui::Modifiers::CTRL,egui::Key::D)})
{
self.strs.pop();
}
if ui.input_mut(|k|{
k.consume_key(egui::Modifiers::CTRL,egui::Key::N)})
{
self.strs.push(String::from(""));
}
我们在for循环中遍历前面在self结构体中创建的字符串可变数组,根据数组的数量创建若干组水平排列的单行文本输入框和文本标签。文本输入框的字符串可变引用字符串数组中的元素,于是文本输入框中的字符串在输入更改后得以保存。当文本输入框未输入字符时,文本标签提示输入姓名;而当文本输入框输入后,文本标签向该姓名问好。
而在ui.input_mut()的闭包中调用consume_key()方法,实现了组合快捷键的应用。使用Ctrl+N代表向字符串可变数组中添加一个空字符串。而使用Ctrl+D代表删除字符串可变数组中的最后一个元素。从而通过维护可变字符串数组的数量,实现了控件数量的增减。
编译运行后如下图:
接下来我们还能对这程序进行一点小的优化。前面我们在生成UI控件时直接调用了ui.text_edit_singleline()方法。实际上我们还可以使用ui.add()方法来添加控件,使用该方法时,能让我们对添加的控件有更多的掌控空间。
例如,我们将前述添加文本框与文本标签的代码改为:
for str in &mut self.strs{
ui.horizontal(|ui|{
if *str!="".to_string(){
ui.add(egui::TextEdit::singleline(str).hint_text("Please enter your name."));
ui.label("Hello ".to_string()+str);
}else{
ui.add(egui::TextEdit::singleline(str).hint_text("Please enter your name."))
.on_hover_text("Your name?");
ui.label("Please enter your name.");
}
});
}
其中添加egui::TextEdit::singleline(str)
后使用的.hint_text()
方法可以让文本框在未输入文本时,显示一段提示文本。而链式调用的.on_hover_text()
方法,则可以在鼠标移动到文本框时,显示一个提示气泡窗口。修改后的程序界面如下:
整个程序最终的完整的代码如下:
use eframe::egui;
fn main() {
let mut native_options = eframe::NativeOptions::default();
native_options.centered=true;
eframe::run_native("一个小例子", native_options, Box::new(|cc| Box::new(MyEguiApp::new(cc))));
}
#[derive(Default)]
struct MyEguiApp {
frames:u64,
strs:Vec<String>
}
impl MyEguiApp {
fn new(cc: &eframe::CreationContext<'_>) -> Self {
Self { frames:0,strs:vec![] }
}
}
impl eframe::App for MyEguiApp {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Hello World!");
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());
}
});
// if ui.input(|k|{k.key_pressed(egui::Key::L)}){
// ctx.set_visuals(egui::Visuals::light());
// }
// if ui.input(|k|{k.key_pressed(egui::Key::D)}){
// ctx.set_visuals(egui::Visuals::dark());
// }
ui.label(self.frames.to_string());
for str in &mut self.strs{
ui.horizontal(|ui|{
if *str!="".to_string(){
ui.add(egui::TextEdit::singleline(str).hint_text("Please enter your name."));
ui.label("Hello ".to_string()+str);
}else{
ui.add(egui::TextEdit::singleline(str).hint_text("Please enter your name."))
.on_hover_text("Your name?");
ui.label("Please enter your name.");
}
});
}
if ui.input_mut(|k|{
k.consume_key(egui::Modifiers::CTRL,egui::Key::D)})
{
self.strs.pop();
}
if ui.input_mut(|k|{
k.consume_key(egui::Modifiers::CTRL,egui::Key::N)})
{
self.strs.push(String::from(""));
}
});
self.frames+=1;
}
}
结语
经过此次探索,我们已经基本弄清egui/eframe创建界面的方式。但目前为止我们的主界面中还不能输入任何中文字符,下一次我们将来解决这个问题。