<iced><rust><GUI>基于rust的GUI库iced的学习(00):一个典型的iced窗口的实现

156 阅读8分钟

前言

本专栏是基于rust的GUI库iced的介绍,主要包括iced部件、iced实例的讲解与学习。如果你也对iced感兴趣,不妨来看看。

屏幕截图 2025-02-25 101651.png iced库github地址: github.com/iced-rs/ice…

发文平台

稀土掘金

环境配置

  • 系统:window10
  • 平台:visual studio code
  • 语言:rust
  • 库:iced(0.13)、iced_aw

概述

本文是第一篇,算是一个序章,主要介绍一下如何实现一个典型的iced窗口,同时介绍一下如何实现自定义窗口图标以及如何显示中文字体。

1、创建iced窗口

iced库的作者描述了iced的架构:

屏幕截图 2025-02-25 102122.png

并直言是受到了Elm的启发,关于Elm,大家感兴趣的可以去其官网看看: elm-lang.org/

本文不多赘述。

我们主要关注iced的实现,下面将以一个简单的示例来介绍iced。

示例如下:一个简单的计数器,有两个按钮与一个文本标签,点击相应的按钮,文本的内容显示的数值将随之减少或者增加。与此同时,文本的尺寸将与数值的大小关联,也就是会随之变化。

我们首先来看一下iced官方给出的典型示例代码:

官方源码

use iced::widget::{button, column, text, Column};

fn main() -> iced::Result {
    iced::run("A cool counter", Counter::update, Counter::view)
}

#[derive(Default)]
struct Counter {
    value: i32,
}

#[derive(Debug, Clone, Copy)]
pub enum Message {
    Increment,
    Decrement,
}


impl Counter {
    pub fn view(&self) -> Column<Message> {
        // We use a column: a simple vertical layout
        column![
            // The increment button. We tell it to produce an
            // `Increment` message when pressed
            button("+").on_press(Message::Increment),

            // We show the value of the counter here
            text(self.value).size(50),

            // The decrement button. We tell it to produce a
            // `Decrement` message when pressed
            button("-").on_press(Message::Decrement),
        ]
    }

    pub fn update(&mut self, message: Message) {
        match message {
            Message::Increment => {
                self.value += 1;
            }
            Message::Decrement => {
                self.value -= 1;
            }
        }
    }
}

如果运行上面的代码,效果如下:

屏幕截图 2025-02-25 104335.png

可以看到,这是一个最简单的窗口,尺寸、位置、样式等都未进行设置。但是具备功能。

我们将在这个示例代码上进行修改,首先我们希望能对窗口进行调整,比如设置一个合适的尺寸,窗口的初始位置也希望进行设置。

iced提供对窗口参数的设置选项,即iced::window::Settings:

/// The window settings of an application.
#[derive(Debug, Clone)]
pub struct Settings {
    /// The initial logical dimensions of the window.
    pub size: Size,

    /// The initial position of the window.
    pub position: Position,

    /// The minimum size of the window.
    pub min_size: Option<Size>,

    /// The maximum size of the window.
    pub max_size: Option<Size>,

    /// Whether the window should be visible or not.
    pub visible: bool,

    /// Whether the window should be resizable or not.
    pub resizable: bool,

    /// Whether the window should have a border, a title bar, etc. or not.
    pub decorations: bool,

    /// Whether the window should be transparent.
    pub transparent: bool,

    /// The window [`Level`].
    pub level: Level,

    /// The icon of the window.
    pub icon: Option<Icon>,

    /// Platform specific settings.
    pub platform_specific: PlatformSpecific,

    /// Whether the window will close when the user requests it, e.g. when a user presses the
    /// close button.
    ///
    /// This can be useful if you want to have some behavior that executes before the window is
    /// actually destroyed. If you disable this, you must manually close the window with the
    /// `window::close` command.
    ///
    /// By default this is enabled.
    pub exit_on_close_request: bool,
}

我们可以看到,window参数包括许多方面,除了size、position,还有可见性、可调整大小、图标icon等,我们并不需要设置所有参数(当然如果你有需要可以全部设置),本例中,我们将调整size、position,以及添加icon。

我们首先修改main函数中的代码,添加对window的设置:

fn main() -> iced::Result {
    //iced::run("A cool counter", Counter::update, Counter::view)
    iced::application("A cool counter", Counter::update, Counter::view)
        .window(iced::window::Settings{
            size:iced::Size{width:400.0,height:400.0},
            position:iced::window::Position::Specific(iced::Point{x:100.0,y:40.0}),
            ..Default::default()
        })
        .run()
}

再运行一下看看:

屏幕截图 2025-02-25 105620.png

接下来,我们为窗口添加图标,首先我们看看iced窗口的图标的定义:

pub icon: Option<Icon>,

其中Icon定义:

/// An window icon normally used for the titlebar or taskbar.
#[derive(Debug, Clone)]
pub struct Icon {
    rgba: Vec<u8>,
    size: Size<u32>,
}

可以看到,Icon事实上就是一个字节数组,相当于图像的底层数据。如果我们要将常用的图片格式如png、jpg等转为iced的图标,也就是需要对图片进行处理,获取其图像数据转为Icon格式即可。

iced官方提供了icon模块,提供了一个from_rgba函数:

/// Builds an  [`Icon`] from its RGBA pixels in the `sRGB` color space.
pub fn from_rgba(
    rgba: Vec<u8>,
    width: u32,
    height: u32,
) -> Result<Icon, Error> {
    const PIXEL_SIZE: usize = mem::size_of::<u8>() * 4;

    if rgba.len() % PIXEL_SIZE != 0 {
        return Err(Error::ByteCountNotDivisibleBy4 {
            byte_count: rgba.len(),
        });
    }

    let pixel_count = rgba.len() / PIXEL_SIZE;

    if pixel_count != (width * height) as usize {
        return Err(Error::DimensionsVsPixelCount {
            width,
            height,
            width_x_height: (width * height) as usize,
            pixel_count,
        });
    }

    Ok(Icon {
        rgba,
        size: Size::new(width, height),
    })
}

所以,我们只需要提供要设置为窗口图标的图片的字节数组和长宽尺寸,就可以通过from_rgba函数转为Icon。

我们如何获取图片的字节数据呢?我们可以使用image这个图像处理库。

GitHub - image-rs/image: Encoding and decoding images in Rust

image可以处理大部分图片格式:

屏幕截图 2025-02-25 110617.png

要使用image,我们需要先添加image: cargo add image

然后导入它: extern crate image as eximg

此处我们导入时使用as为image重命名,是因为iced中本身也有image这个元素,以后会用到,所以为了防止名称冲突,此处为外部的image库重命名为eximg

好了,现在我们使用image库来对图片进行处理,我们可以创建一个函数img_to_icon

///
/// 将图片转为icon格式图标
/// 
pub fn img_to_icon(path:&str) -> Icon{
    
    let img2=eximg::open(path);    
    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(), 32, 32);
    let ico2_file=match ico2{
        Ok(file)=>file,
        Err(error)=>panic!("error is {}",error),
    };
    
    ico2_file
}

主要逻辑就是利用image将图片转为vec< u8>数组,然后再利用from_rgba将vec< u8>转为Icon格式。

现在,我们在主程序中调用此函数,然后在window中添加icon项,并赋予转换后的图标:

let myicon=imgsetup::img_to_icon("./icons/雪花.png");
iced::application("A cool counter", Counter::update, Counter::view)
        .window(iced::window::Settings{
            size:iced::Size{width:400.0,height:400.0},
            position:iced::window::Position::Specific(iced::Point{x:100.0,y:40.0}),
            icon:Some(myicon),
            ..Default::default()
        })
        .run()

运行一下看看:

屏幕截图 2025-02-25 111557.png

好了,图标问题解决了(图片转换时需要注意图片尺寸问题,不过这个问题我们留待以后再说),现在我们对view函数中的代码稍作修改,将按钮的文本改为中文试试:

pub fn view(&self) -> Column<Message> {
        // We use a column: a simple vertical layout
        column![
            // The increment button. We tell it to produce an
            // `Increment` message when pressed
            button("增加").on_press(Message::Increment),

            // We show the value of the counter here
            text(self.value).size(50),

            // The decrement button. We tell it to produce a
            // `Decrement` message when pressed
            button("减少").on_press(Message::Decrement),
        ]
    }

运行看看:

屏幕截图 2025-02-25 114730.png

可以看到,按钮的文本显示为乱码,这是因为iced的默认字体不支持中文,所以,我们可以修改其默认字体,比如设置为微软雅黑

 let myfont = "微软雅黑";
    let myicon=imgsetup::img_to_icon("./icons/雪花.png");
    iced::application("A cool counter", Counter::update, Counter::view)
        .window(iced::window::Settings{
            size:iced::Size{width:400.0,height:400.0},
            position:iced::window::Position::Specific(iced::Point{x:100.0,y:40.0}),
            icon:Some(myicon),
            ..Default::default()
        })
        .settings(iced::Settings{
            default_font:Font::with_name(myfont),
            ..Default::default()
        })
        .run()

再运行看看:

屏幕截图 2025-02-25 122717.png

2、调整布局,文本大小改变

在上面,我们已经实现了一个基本的iced窗口,并为其添加了图标,增加了中文支持。现在,我们对上述窗口的布局稍作调整,并增加功能,当数值变化时,文本的尺寸也相应变化。 我们先修改view中的代码:

// We use a column: a simple vertical layout
        column![
            row![
                // The increment button. We tell it to produce an
                // `Increment` message when pressed
                button("增加").on_press(Message::Increment),

                // We show the value of the counter here
                text(self.value).size(50),

                // The decrement button. We tell it to produce a
                // `Decrement` message when pressed
                button("减少").on_press(Message::Decrement),
            ],
            text(format!("当前size:{}",self.textsize)).size(self.textsize),
           
        ].padding(10).spacing(10)

我们增加一个row排布(关于布局以后再说,此处只是使用),并新增了一个text元素用于显示尺寸变化,text的size参数内,使用的是变量textsize,因此,我们需要在struct中新增变量:

#[derive(Debug)]
struct Counter {
    value: i32,
    textsize:u16,
}

但是,如果直接运行会报错,因为text元素的文本内容的尺寸不能为0,也就是文字的size至少要大于0,所以我们需要给变量textsize一个初始值:

 fn new() -> Self {
        Self {
            value: 10,
            textsize:10,
        }
    }

我们增加一个new函数,这个函数的作用就是为Counter结构体的变量赋予初始值。不过,需要注意,new函数并非iced中的“默认”函数,而是需要我们自定义添加的,因此,如果我们希望new作为初始值函数,就需要对其进行定义,rust中提供了Default这个特性,可以为类型返回一个默认值,我们可以利用此特性。

impl Default for Counter {
    fn default() -> Self {
        Self::new()
    }
}

这样,我们就具有了初始值,运行看看效果:

屏幕截图 2025-02-25 134846.png

但目前还只是初始值,如果我们点击按钮,value的值是会变化,但并没有将其与textsize联系起来,所以,我们还需要更改update中的代码:

Message::Increment => {
                self.value += 1;
                if self.value >= 50 {
                    self.textsize = 50;
                } else {
                   self.textsize = self.value as u16; 
                }
                
            }
            Message::Decrement => {
                self.value -= 1;
                if self.value <=5 {
                    self.textsize = 5;
                } else {
                  self.textsize = self.value as u16;  
                }
                
            }

主要目的是将当前的value值再赋予textsize变量,但由于字体尺寸既不能小于0,也不适合无限大,所以,对其进行了范围限制。

3、动态演示

这样一来,当我们点击按钮时,计数值会变化,而下面的text文本尺寸也会随之变化,我们可以看一下实例演示效果:

iced窗口演示.gif