注:此文适合于对rust有一些了解的朋友
iced是一个跨平台的GUI库,用于为rust语言程序构建UI界面。
这是一个系列博文,本文是第三篇,前两篇的链接:
1、Rust UI开发(一):使用iced构建UI时,如何在界面显示中文字符
2、Rust UI开发(二):iced中如何为窗口添加icon图标
本篇是系列第三篇,主要关注如何在窗口上显示图片,要在窗口显示一张图片,基本上需要解决两个问题,一是图片文件导入,二是图片文件显示。这两个功能对于其他成熟语言都不是问题,文件对话框和图片渲染都不是难事,但iced是缺少对话框部件的。
所以,就要借助于第三方库,下面我们将针对这两个方面做说明。
实际窗口效果预览:
至少目前为止(iced=0.10)iced中没有集成对话框功能,包括文件对话框、字体、颜色、消息等对话框都没有,但我看到其他支持rust的GUI库如egui、nwg(native-window-gui)等都是有对话框的,当然egui中是用rfd库来实现的。
所以,在本篇中,我们也是利用rfd来实现文件对话框功能。
rfd是Rusty File Dialogs的简写,是跨平台的rust库,提供打开/保存对话框的功能。
rfd的官方代码:
use rfd::FileDialog;
let files = FileDialog::new()
.add_filter("text", &["txt", "rs"])
.add_filter("rust", &["rs", "toml"])
.set_directory("/")
.pick_file();
使用起来也很简单,在你的项目的Cargo.toml中添加依赖:
rfd="0.12.1"
然后在main.rs中导入:
use rfd::FileDialog;
需要注意的是,FileDialog.pickfile()函数返回的是一个枚举类型Option,里面的数据就是文件的路径。
所以,我们可以使用Some来返回此路径。
if let Some(file)=FileDialog::new()
.set_directory("/")
.add_filter("all", &["*"]) //添加文件过滤,all是显示所有类型
.add_filter("文本文件(*txt)", &["txt", "rs"]) //只显示文本类型
.add_filter("图像文件(*png*jpg*bmp)", &["png","jpg","jpeg","bmp"]) //只显示图像类型
.set_title("打开图像")
.pick_file()
{
self.iamgepath=file.display().to_string();
};
这样我们打开的图像的路径,就赋给了self.imagepath。
我们现在已经得到了图像的路径,那么我们如何将图像显示在窗口上呢?这里需要用到iced提供的image这个功能,它是被定义为iced_widget的一个特性,即Features。Features是Rust中的一个概念,或者是一种机制。以下是rust官方手册关于Features的概念,大家自己理解一下。
本篇说明一下如何使用image这个Features,在你的项目的Cargo.toml文件中,添加了iced依赖后,添加以下语句:
iced.features=["image"]
然后可以在main.rs中导入image:
use iced::widget::{text, button,slider,column,image,container};
另外,我们在本系列第二篇提到过一个第三方的图像库Image,实际上iced中处理图像也用到了这个库,所以我们将Image也添加到依赖中:
image="0.24.7"
为了不混乱iced的image和第三方image,我们在导入第三方image时,如下:
extern crate image as img_image;
当然,as后面的名字,你可以自己随便定义,只要你知道它是用来代替第三方image的“命名空间”即可。
image部件显示图像代码:
image(hd).content_fit(ContentFit::Fill),
此处,image函数的参数是一个Handle,官方关于image的源代码:
/// Creates a new [`Image`].
///
/// [`Image`]: widget::Image
#[cfg(feature = "image")]
pub fn image<Handle>(handle: impl Into<Handle>) -> crate::Image<Handle> {
crate::Image::new(handle.into())
}
所以,我们使用时,需要将图像文件转为Handle类型:
let hd= if cfg!(target_arch = "wasm32") { //Wasm32是一种基于WebAssembly(Wasm)的32位虚拟机
image::Handle::from_path("iced_test/src/img1.png")
} else {
//image::Handle::from_path("../iced_test/src/img2.jpeg")
image::Handle::from_path(img_path)
};
如上,使用image-Handle-from_path函数,从图像路径获取image的Handle,然后将此Handle传给image部件即可。
完整代码:
use iced::widget::{text, button,slider,column,row,image,container};
use iced::{Alignment, Element, Length,Sandbox, Settings, ContentFit, alignment};
use iced::window;
use iced::window::icon;
use iced::window::Position;
use iced::Font;
use iced::font::Family;
extern crate image as img_image;
extern crate num_complex;
use rfd::FileDialog;
pub fn main() ->iced::Result{
//Counter::run(Settings::default())
let ff="微软雅黑";
//第二种获取rgba图片的方法,利用Image库
let img2=img_image::open("../iced_test/src/dota22.png");
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),
position:Position::Specific(100, 40),
icon:Some(ico2_file),
..window::Settings::default()
},
default_font:Font{ //设置UI界面的显示字体
family:Family::Name(ff),
..Font::DEFAULT},
..Settings::default()
})
}
pub struct Counter{
srcimgpath:String,
destimgpath:String,
slivalue:f32,
}
#[derive(Debug, Clone,Copy)]
pub enum Message {
OpenimgPressed,
SaveimgPressed,
SliderChanged(f32),
}
impl Sandbox for Counter {
type Message = Message;
fn new() -> Self {
let path=String::new();
Self { srcimgpath: path.to_string(), //to_string()类似于clone
destimgpath:path.to_string(),
slivalue:0.0}
}
fn title(&self) -> String {
String::from("iced_UI演示")
}
fn update(&mut self, message: Message) {
match message {
Message::OpenimgPressed => {
if let Some(file)=FileDialog::new()
.set_directory("D:\\008 rustpro\\iced_test\\src")
.add_filter("all", &["*"]) //添加文件过滤,all是显示所有类型
.add_filter("文本文件(*txt)", &["txt", "rs"]) //只显示文本类型
.add_filter("图像文件(*png*jpg*bmp)", &["png","jpg","jpeg","bmp"]) //只显示图像类型
.set_title("打开图像")
.pick_file()
{
self.srcimgpath=file.display().to_string();
};
//println!("{:?}",file);
}
Message::SaveimgPressed=> {
self.destimgpath="".to_string();
}
Message::SliderChanged(vl)=>{
self.slivalue=vl;
}
}
}
fn view(&self) -> Element<Message> {
let img_path=&self.srcimgpath;
let hd= if cfg!(target_arch = "wasm32") { //Wasm32是一种基于WebAssembly(Wasm)的32位虚拟机
image::Handle::from_path("iced_test/src/img1.png")
} else {
//image::Handle::from_path("../iced_test/src/img2.jpeg")
image::Handle::from_path(img_path)
};
// let hd2= if cfg!(target_arch = "wasm32") { //Wasm32是一种基于WebAssembly(Wasm)的32位虚拟机
// image::Handle::from_path("iced_test/src/img1.png")
// } else {
// image::Handle::from_path(img_path)
// };
//println!("hd is :{:?}",hd);
container(
column![
row![
//btn1
button(text("打开图像")
.horizontal_alignment(alignment::Horizontal::Center)
.vertical_alignment(alignment::Vertical::Center)
.size(15)
).on_press(Message::OpenimgPressed)
.padding(4),
//btn2
button(text("保存图像")
.horizontal_alignment(alignment::Horizontal::Center)
.vertical_alignment(alignment::Vertical::Center)
.size(15)
).on_press(Message::SaveimgPressed)
.padding(4),
].spacing(10).padding(10)
.align_items(Alignment::Start),
//text:source image path
text(format!("原图像路径:{:?}",self.srcimgpath)).size(15)
.horizontal_alignment(alignment::Horizontal::Center)
.vertical_alignment(alignment::Vertical::Center),
row![
text("图像尺寸调整:").size(15),
//slider
slider(0.0..=100.0, self.slivalue, Message::SliderChanged).step(0.01).width(200),
].spacing(20),
//text:dest image path
text(&self.destimgpath).size(15),
row![
image(hd).content_fit(ContentFit::Fill),
//image(hd2).width(Length::Fixed(100.0)).height(Length::Fixed(100.0)).content_fit(ContentFit::Fill)
].spacing(10)
.padding(10)
]
.spacing(10)
.padding(30)
.align_items(Alignment::Start)
)
.into()
}
}
以上代码中,不仅包含本篇涉及的内容,也包含前2篇中涉及的内容。
动态演示图: