注:此文适合于对rust有一些了解的朋友
iced是一个跨平台的GUI库,用于为rust语言程序构建UI界面。
这是一个系列博文,本文是第四篇,前三篇链接:
1、Rust UI开发(一):使用iced构建UI时,如何在界面显示中文字符
2、Rust UI开发(二):iced中如何为窗口添加icon图标
3、Rust UI开发(三):iced如何打开图片(对话框)并在窗口显示图片?
注:本篇是系列的第四篇,从本篇开始,将基于编写一个串口调试助手项目,以多个篇幅,来分别说明。
要编写一个调试助手,总的来说,可以分为UI设计、底层串口数据通讯、交互三个方面。
所以,此后数篇博文都将以“串口调试助手”项目为例,辅以各个功能的介绍说明如菜单栏如何创建、串口UI界面如何设计、串口通讯如何实现以及数据交互如何实现等,我希望最终能达到一个完整的可使用的以rust和iced为主要库构建的实例项目,可以实现与串口设备的通讯,至少能稳定实现数据交互。
本篇主要关注一个问题,就是菜单栏的创建,iced库中并没有包含menu部件,需要用到iced_aw库。github地址:
https://github.com/iced-rs/iced_aw
这是一个包含有许多额外部件的iced库,如颜色选择器、日期选择器、菜单、拆分、Tab组件等。
从本篇开始,将附加程序中用到的依赖项
Cargo.toml:
[dependencies]
iced.workspace=true
iced.features=["image","svg"]
iced_aw={ workspace=true, features = ["card","menu","quad","icon_text"] }
image.workspace=true
num-complex.workspace=true
[workspace.dependencies]
iced = "0.10"
iced_aw={ version = "0.7.0", default-features = false }
image="*"
num-complex="*"
项目文件结构:
实际效果预览:
声明:本篇参考iced_aw的官方关于menu的示例,但作了一些修改,以适应自定义项目的所需,所以,如果想要了解iced_aw源代码的,建议去参看官方示例。
关于iced如何构建窗口,以及窗口设置、图标设置等内容,可以看本系列前三篇,本篇主要实现这样的功能,创建菜单栏,当选择某个菜单项时,文本框中显示当前选择的菜单项名称。
菜单栏创建
1、添加依赖
参看前面的toml文件。
2、程序里导入
use iced_aw::menu::{menu_tree::MenuTree, CloseCondition, ItemHeight, ItemWidth, PathHighlight};
use iced_aw::quad;
use iced_aw::{helpers::menu_tree, menu_bar, menu_tree};
其中有一些是本篇内容没有用到的,删掉也可以,不删也没关系,不影响程序的测试。
3、menu部件创建
iced_aw的官方示例中,关于menu的创建,看起来比较复杂,原因是功能做的比较丰富,但如果不需要那么多功能,则可以对其进行修改。
官方示例程序,是层层递进的结构,并且写的复杂,是因为要设置菜单的样式、菜单的反馈对应的动作等。
但是,我们可以从简单的先来看,如果你只是要在窗口上实现一个菜单栏,那么:
menu_bar!(menu_tree!(button("文件")))
这个就可以实现,当然这样实现的菜单没有任何功能,也没有样式的变化。它就是在窗口创建了一个菜单项。
上图可以看到,这样创建的无样式的菜单项和旁边设置了主题样式的菜单项,有明显区别。
事实上,在这里的菜单项,就是一个个的按钮组成。通过设置按钮的样式,就可以改变菜单的样式。
所以,在官方示例中,是创建了一个ButtonStyle结构体:
struct ButtonStyle;
然后,将button部件的StyleSheet特性实现于创建的结构体ButtonStyle上:
impl button::StyleSheet for ButtonStyle {
type Style = iced::Theme;
//生成按钮的激活外观
fn active(&self, style: &Self::Style) -> button::Appearance {
button::Appearance {
text_color: style.extended_palette().background.base.text,
border_radius: [2.0; 4].into(),
background: Some(Color::TRANSPARENT.into()),
..Default::default()
}
}
//生成按钮的悬停外观
fn hovered(&self, style: &Self::Style) -> button::Appearance {
let plt = style.extended_palette();
button::Appearance {
background: Some(plt.primary.weak.color.into()),
text_color: plt.primary.weak.text,
border_radius:[6.0; 4].into(), //border_radius:倒角半径
..self.active(style)
}
}
}
上面的实现中,启用了button部件的active和hovered两个功能,这两个功能是当我们将鼠标放在按钮上悬停以及激活按钮时,按钮的样式会按照设定的程序来变化。可以看到,其中改变的是文本颜色、背景色以及部件的四边的倒角。
基于这个按钮样式数据ButtonStyle,我们可以创建一个基础按钮base_button:
//基础按钮
fn base_button<'a>(
content: impl Into<Element<'a, Message, iced::Renderer>>,
msg: Message,
) -> button::Button<'a, Message, iced::Renderer> {
button(content)
.padding([4, 8])
.style(iced::theme::Button::Custom(Box::new(ButtonStyle {})))
.on_press(msg)
}
在base_button的基础上,创建带标签按钮labeled_button:
//带标签按钮
fn labeled_button<'a>(label: &str, msg: Message) -> button::Button<'a, Message, iced::Renderer> {
base_button(
text(label)
.width(Length::Fill)
.height(Length::Fill)
.vertical_alignment(alignment::Vertical::Center),
msg,
)
}
然后创建测试按钮debug_button:
//测试按钮
fn debug_button<'a>(label: &str) -> button::Button<'a, Message, iced::Renderer> {
labeled_button(label, Message::InputChanged(label.into()))
}
最后,在菜单项中调用这个有样式设定的按钮即可:
fn debug_item<'a>(label: &str) -> MenuTree<'a, Message, iced::Renderer> {
menu_tree!(debug_button(label).width(Length::Fill).height(Length::Fill))
}
以上,都是官方示例给出的步骤,它是用函数块的形势,将每一步分解,这样方便多次调用,以及对某个涉及到的项进行动态更改。
但是,如果你不想这样多次,就想一个函数实现菜单项功能,那么可以自己重新写一个函数:
///简单的菜单调用
fn menu_simple<'a>(label:&str,_app:&Counter)->MenuTree<'a,Message,iced::Renderer>{
menu_tree( button("haha")
.padding([4, 8])
.style(iced::theme::Button::Custom(Box::new(ButtonStyle {})))
.on_press(Message::InputChanged(label.into()))
, vec![
//debug_item("label"),
menu_tree!(text("haha"))
]
)
}
可以看到,在我们自定义的函数里,使用menu_tree,可以直接创建菜单和子菜单,但数量少时还可以,数量多了,写在一个函数里,就不太方便了。
这里,我们在自定义函数里只是简单的编写了一些程序,目的是演示,如果去编写实际项目,建议按照官方示例的模板,因为那样写比较有清晰的结构。
现在,我们了解如何创建菜单,接下来响应菜单。其实这里就比较容易了,因为菜单是由一个个button这样的小部件组成的,只需要监控小部件的触发函数,就可以在交互中传回消息。以我们自定义的函数为例,菜单中button的on_press函数,绑定了消息参数:InputChanged,InputChanged附带一个String类型的返回值。
button("haha")
.padding([4, 8])
.style(iced::theme::Button::Custom(Box::new(ButtonStyle {})))
.on_press(Message::InputChanged(label.into())
在Updata函数里更新:
Message::InputChanged(value) =>{
self.value=value;
}
这里的逻辑是,当菜单项的按钮点击时,InputChanged消息响应,返回当前的菜单项文本值,在update中,我们把文本值传给定义的数据value。而value被我们绑定到一个文本框:
text(format!("当前所选菜单是:{:?}",self.value)).size(20)
这样,当窗口启动后,我们只要点击当前菜单项,其名称就通过消息传递给文本框显示。
这其中关于Sandbox的窗口的update、view等函数就不细说了,前面的博文里已经有过说明。
需要注意的是,本篇是说明了如何创建菜单及如何简单的应答,关于详细的对应于“串口调试助手”实例的菜单的创建程序将在后面的篇章中说明,本文就到此了。
动态演示:
完整代码:
完整代码里有一些另外的数据和函数,是本篇未提及的,但不影响本篇内容的测试。另外,所有代码都是测试中的程序,所以可能会不够干净清晰。
use iced::widget::{button, column,row, text,combo_box,pick_list,svg};
use iced::{Alignment,theme, Element, Color,Sandbox,Length, Settings, alignment};
use iced::widget::Column;
use iced::Font;
use iced::font::Family;
use iced::window;
use iced::window::icon;
use iced_aw::menu::{menu_tree::MenuTree, CloseCondition, ItemHeight, ItemWidth, PathHighlight};
use iced_aw::quad;
use iced_aw::{helpers::menu_tree, menu_bar, menu_tree};
extern crate image;
extern crate num_complex;
pub fn main() -> iced::Result {
//Counter::run(Settings::default()) //此处为使用默认窗口设置
let ff="微软雅黑"; //设置自定义字体
//第二种获取rgba图片的方法,利用Image库
let img2=image::open("../iced_test/img/img3.jpg");
let img2_path=match img2 {
Ok(path)=>path,
Err(error)=>panic!("error is {}",error),
};
let img2_file=img2_path.to_rgba8();
let ico2=icon::from_rgba(img2_file.to_vec(), 64, 64);
let ico2_file=match ico2{
Ok(file)=>file,
Err(error)=>panic!("error is {}",error),
};
Counter::run(Settings {
window:window::Settings{ //设置自定义窗口尺寸
size:(800,600),
//icon:Some(ico_file),
icon:Some(ico2_file),
..window::Settings::default()
},
default_font:Font{ //设置自定义字体,用于显示中文字符
family:Family::Name(ff),
..Font::DEFAULT},
..Settings::default()
})
}
//创建结构体struct
struct Counter{
value: String,
value2:String,
sli_value:f32,
baudrates:combo_box::State<Baudrate>,
selected_port:Option<Baudrate>,
selected_baudrate:Option<Baudrate>,
selected_databit:Option<Baudrate>,
selected_pabit:Option<Baudrate>,
selected_stopbit:Option<Baudrate>,
text:String,
}
#[derive(Debug, Clone)] //为下方的enum添加特性trait
enum Message {
Showtext,
InputChanged(String),
SliderChanged(f32),
Selected_port(Baudrate),
Selected_baud(Baudrate),
Selected_databit(Baudrate),
Selected_pabit(Baudrate),
Selected_stopbit(Baudrate),
OptionHovered(Baudrate),
Closed,
}
//sandbox是一个trait
impl Sandbox for Counter { //impl将sandbox添加给Counter,使Counter具有了sandbox的一些特性
type Message = Message;
fn new() -> Self { //初始化sandbox,返回初始值
Self {
value: String::new(),
value2:String::new(),
sli_value:0.0,
baudrates: combo_box::State::new(Baudrate::BAUD.to_vec()),
selected_port:None,
selected_baudrate: None,
selected_databit:None,
selected_pabit:None,
selected_stopbit:None,
text: String::new(),
}
}
fn title(&self) -> String { //返回sandbox的标题
String::from("iced_串口助手")
//self.value.clone()
}
fn update(&mut self, message: Message) { //此处书写更新逻辑程序,所有UI交互会在这里处理
match message {
Message::Showtext=> {
let ss=&self.value; //新建一个value的引用
self.value2=ss.to_string(); //将变量的引用字符化后传给value2
}
Message::InputChanged(value) =>{
self.value=value;
}
Message::SliderChanged(sli_value)=>{
self.sli_value=sli_value;
}
Message::Selected_port(port)=>{
self.selected_port=Some(port);
}
Message::Selected_baud(baudrate)=>{
self.selected_baudrate=Some(baudrate);
self.text=baudrate.hello().to_string();
}
Message::Selected_databit(databit)=>{
self.selected_databit=Some(databit);
}
Message::Selected_pabit(pabit)=>{
self.selected_pabit=Some(pabit);
}
Message::Selected_stopbit(stopbit)=>{
self.selected_stopbit=Some(stopbit);
}
Message::OptionHovered(baudrate)=>{
self.text=baudrate.hello().to_string();
}
Message::Closed=>{
self.text = self.selected_baudrate.map(|baudrate| baudrate.hello().to_string())
.unwrap_or_default();
}
}
}
fn view(&self) -> Element<Message> {
//let ff="Microsoft YaHei";
column![
row![
menu_bar!(
menu_1(self),menu_2(self),menu_group("工具",self),menu_group("帮助", self))//menu_3(self), menu_4(self))
.item_width(ItemWidth::Uniform(180))
.item_height(ItemHeight::Uniform(27)
),
//menu_bar!(menu_group(self)),
menu_bar!(menu_tree!(button("文件"))),
menu_bar!(menu_simple("简单",self))
]
.spacing(20),
//ser_group(self)
text(format!("当前所选菜单是:{:?}",self.value)).size(20)
]
.spacing(40)
.padding(6)
.into()
}
}
///serial group 布局,
///将同一个布局集中在一个函数中,
///这是一种模块化编程
fn ser_group<'a>(_app:&Counter)-> Column<'a,Message>{
column![
row![
text("端口:").size(16),
pick_list(&Baudrate::PORT[..],_app.selected_port,Message::Selected_port)
.placeholder("port")
.width(100),
button("获取端口").on_press(Message::Showtext)
].spacing(10),
row![
text("波特率:").size(16),
pick_list(&Baudrate::BAUD[..],_app.selected_baudrate,Message::Selected_baud)
.placeholder("baudrate")
.width(100)
].spacing(10),
row![
text("数据位:").size(16),
pick_list(&Baudrate::DATABIT[..],_app.selected_databit,Message::Selected_databit)
.placeholder("databit")
.width(100)
].spacing(10),
row![
text("校验位:").size(16),
pick_list(&Baudrate::PABIT[..],_app.selected_pabit,Message::Selected_pabit)
.placeholder("paritybit")
.width(100)
].spacing(10),
row![
text("停止位:").size(16),
pick_list(&Baudrate::STOPBIT[..],_app.selected_stopbit,Message::Selected_stopbit)
.placeholder("stopbit")
.width(100)
].spacing(10),
row![
button(text("连接").horizontal_alignment(alignment::Horizontal::Center)
.vertical_alignment(alignment::Vertical::Center))
.width(80).height(40)
.on_press(Message::Showtext),
button(text("断开").horizontal_alignment(alignment::Horizontal::Center)
.vertical_alignment(alignment::Vertical::Center))
.width(80).height(40)
.on_press(Message::Showtext)
].spacing(10),
]
.spacing(16)
.padding(16)
.align_items(Alignment::Center)
.into()
}
///自定义按钮样式,用于菜单栏使用
struct ButtonStyle;
//让ButtonStyle实现button的StyleSheet功能
impl button::StyleSheet for ButtonStyle {
type Style = iced::Theme;
//生成按钮的激活外观
fn active(&self, style: &Self::Style) -> button::Appearance {
button::Appearance {
text_color: style.extended_palette().background.base.text,
border_radius: [2.0; 4].into(),
background: Some(Color::TRANSPARENT.into()),
..Default::default()
}
}
//生成按钮的悬停外观
fn hovered(&self, style: &Self::Style) -> button::Appearance {
let plt = style.extended_palette();
button::Appearance {
background: Some(plt.primary.weak.color.into()),
text_color: plt.primary.weak.text,
border_radius:[6.0; 4].into(), //border_radius:四角倒圆半径
..self.active(style)
}
}
}
//基础按钮
fn base_button<'a>(
content: impl Into<Element<'a, Message, iced::Renderer>>,
msg: Message,
) -> button::Button<'a, Message, iced::Renderer> {
button(content)
.padding([4, 8])
.style(iced::theme::Button::Custom(Box::new(ButtonStyle {})))
.on_press(msg)
}
//带标签按钮
fn labeled_button<'a>(label: &str, msg: Message) -> button::Button<'a, Message, iced::Renderer> {
base_button(
text(label)
.width(Length::Fill)
.height(Length::Fill)
.vertical_alignment(alignment::Vertical::Center),
msg,
)
}
//测试按钮
fn debug_button<'a>(label: &str) -> button::Button<'a, Message, iced::Renderer> {
labeled_button(label, Message::InputChanged(label.into()))
}
//调试item
fn debug_item<'a>(label: &str) -> MenuTree<'a, Message, iced::Renderer> {
menu_tree!(debug_button(label).width(Length::Fill).height(Length::Fill))
}
//定义子菜单样式,附加有箭头图标
fn sub_menu<'a>(
label: &str,
msg: Message,
children: Vec<MenuTree<'a, Message, iced::Renderer>>,
) -> MenuTree<'a, Message, iced::Renderer> {
let handle = svg::Handle::from_path("../iced_test/img/caret-right-fill.svg");
let arrow = svg(handle)
.width(Length::Shrink)
.style(theme::Svg::custom_fn(|theme| svg::Appearance {
color: Some(theme.extended_palette().background.base.text),
}));
menu_tree(
base_button(
row![
text(label)
.width(Length::Fill)
.height(Length::Fill)
.vertical_alignment(alignment::Vertical::Center),
arrow
]
.align_items(iced::Alignment::Center),
msg,
)
.width(Length::Fill)
.height(Length::Fill),
children,
)
}
fn debug_sub_menu<'a>(
label: &str,
children: Vec<MenuTree<'a, Message, iced::Renderer>>,
) -> MenuTree<'a, Message, iced::Renderer> {
sub_menu(label, Message::InputChanged(label.into()), children)
//menu_tree!(debug_button(label).width(Length::Fill).height(Length::Fill))
}
fn menu_1<'a>(_app: &Counter) -> MenuTree<'a, Message, iced::Renderer> {
let sub_1 = debug_sub_menu(
"A sub menu",
vec![
debug_item("Item"),
debug_item("Item"),
//sub_2,
debug_item("Item"),
debug_item("Item"),
debug_item("Item"),
],
)
.width(220);
let root = menu_tree(
debug_button("文件"),
vec![
debug_item("新建"),
debug_item("打开文件"),
sub_1,
debug_item("打开文件夹"),
debug_item("保存"),
debug_item("关闭"),
],
)
.width(110);
root
}
fn menu_2<'a>(_app: &Counter) -> MenuTree<'a, Message, iced::Renderer> {
let sub_1 = debug_sub_menu(
"A sub menu",
vec![
debug_item("Item"),
debug_item("Item"),
//sub_2,
debug_item("Item"),
debug_item("Item"),
debug_item("Item"),
],
)
.width(220);
let root = menu_tree(
debug_button("通讯"),
vec![
debug_item("设置参数"),
debug_item("连接"),
sub_1,
debug_item("断开"),
debug_item("Item"),
debug_item("Item"),
],
)
.width(110);
root
}
//fn menu_3<'a>(_app: &Counter) -> MenuTree<'a, Message, iced::Renderer> {}
//fn menu_4<'a>(_app: &Counter) -> MenuTree<'a, Message, iced::Renderer> {}
///一个菜单的group
///
///
fn menu_group<'a>(label:&str,_app:&Counter)->MenuTree<'a,Message,iced::Renderer>{
menu_tree(
debug_button(label),
vec![
debug_item("关于"),
]
)
}
///简单的菜单调用
fn menu_simple<'a>(label:&str,_app:&Counter)->MenuTree<'a,Message,iced::Renderer>{
menu_tree( button("haha")
.padding([4, 8])
.style(iced::theme::Button::Custom(Box::new(ButtonStyle {})))
.on_press(Message::InputChanged(label.into()))
, vec![
//debug_item("label"),
menu_tree!(text("haha"))
]
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Baudrate{
//port
PortCOM0,
//baudrate
Rate9600,
#[default]
Rate19200,
Rate38400,
Rate57600,
Rate115200,
//databit
Databit8,
Databit7,
Databit6,
Databit5,
//paritybit
Pabitnone,
Pabiteven,
Pabitodd,
Pabitspace,
Pabitmark,
//stopbit
Stopbit1,
Stopbit1dot5,
Stopbit2,
}
impl Baudrate {
const PORT:[Baudrate;1]=[
Baudrate::PortCOM0,
];
const BAUD: [Baudrate; 5] = [
Baudrate::Rate9600,
Baudrate::Rate19200,
Baudrate::Rate38400,
Baudrate::Rate57600,
Baudrate::Rate115200,
];
const DATABIT:[Baudrate;4]=[
Baudrate::Databit8,
Baudrate::Databit7,
Baudrate::Databit6,
Baudrate::Databit5,
];
const PABIT:[Baudrate;5]=[
Baudrate::Pabitnone,
Baudrate::Pabiteven,
Baudrate::Pabitodd,
Baudrate::Pabitspace,
Baudrate::Pabitmark,
];
const STOPBIT:[Baudrate;3]=[
Baudrate::Stopbit1,
Baudrate::Stopbit1dot5,
Baudrate::Stopbit2,
];
fn hello(&self) -> &str {
match self {
Baudrate::PortCOM0=>"COM0",
Baudrate::Rate9600 => "9600",
Baudrate::Rate19200 => "9600",
Baudrate::Rate38400=> "9600",
Baudrate::Rate57600 => "9600",
Baudrate::Rate115200 => "9600",
Baudrate::Databit8=>"8",
Baudrate::Databit7=>"7",
Baudrate::Databit6=>"6",
Baudrate::Databit5=>"5",
Baudrate::Pabitnone=>"None",
Baudrate::Pabiteven=>"Even",
Baudrate::Pabitodd=>"Odd",
Baudrate::Pabitspace=>"Space",
Baudrate::Pabitmark=>"Mark",
Baudrate::Stopbit1=>"1",
Baudrate::Stopbit1dot5=>"1.5",
Baudrate::Stopbit2=>"2",
}
}
}
impl std::fmt::Display for Baudrate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Baudrate::PortCOM0=>"COM0",
Baudrate::Rate9600 => "9600",
Baudrate::Rate19200 => "19200",
Baudrate::Rate38400 => "38400",
Baudrate::Rate57600 => "57600",
Baudrate::Rate115200=> "115200",
Baudrate::Databit8=>"8",
Baudrate::Databit7=>"7",
Baudrate::Databit6=>"6",
Baudrate::Databit5=>"5",
Baudrate::Pabitnone=>"None",
Baudrate::Pabiteven=>"Even",
Baudrate::Pabitodd=>"Odd",
Baudrate::Pabitspace=>"Space",
Baudrate::Pabitmark=>"Mark",
Baudrate::Stopbit1=>"1",
Baudrate::Stopbit1dot5=>"1.5",
Baudrate::Stopbit2=>"2",
}
)
}
}