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。此时我们就可以编译运行一下我们的程序,可以观察到窗口出现的位置会有区别。
随后我们再将下一行代码中的应用名称改为一个中文名"一个小例子",再次编译运行程序,会发现窗口标题栏名称已经改变为了我们所设置的名称。
image

添加控件

我们找到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");
           }
       });
   }
}

界面如下:
image
我们发现所有的控件默认竖直往下排列。
当然,目前的按钮可以点击,但没有任何效果。那么是否需要给按钮增加回调函数呢?实际上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");
           });
       });
   }
}

运行后的界面如下:
image

添加响应

接下来我们为按钮添加执行响应:
我们将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());分别为将应用的主题色设置为明亮和黑暗两种风格。现在我们编译运行程序,即能够在界面中点击按钮切换主题风格。
image

利用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的值显示出来。
此时的界面如下:
image
我们现在可以观察到,每当我的鼠标划过窗口区域内时,帧数都在不停变化,而鼠标在窗体外滑动时,帧数并不会变化。另外只要我们移动窗口,改变窗口大小,点击内区域、操作控件等,都会让帧数变化。甚至我们按键盘上的按键时,也会让帧数变化。但不与窗口产生交互时,则帧数并不变化。
这也说明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代表删除字符串可变数组中的最后一个元素。从而通过维护可变字符串数组的数量,实现了控件数量的增减。
编译运行后如下图:
image
接下来我们还能对这程序进行一点小的优化。前面我们在生成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()方法,则可以在鼠标移动到文本框时,显示一个提示气泡窗口。修改后的程序界面如下:
image
整个程序最终的完整的代码如下:

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创建界面的方式。但目前为止我们的主界面中还不能输入任何中文字符,下一次我们将来解决这个问题。

posted @ 2024-01-03 23:09  AbsalomT  阅读(1440)  评论(0编辑  收藏  举报