一年多以来,Rust 一直是我最喜欢的编程语言。它使我能够创建高性能软件,这与我过去使用的语言不同。由于它的优势,我和许多其他人一样,一直渴望用它来构建 GUI 应用程序。Electron 因其高内存使用率而臭名昭著,因此轻量级、跨平台的 GUI 库需求量很大,而 Rust 将是满足这一需求的绝佳语言。然而,截至撰写本文时,只有一小部分 Rust GUI 库,甚至只有一小部分 GUI 库足够可靠地使用。
但在过去几年中,Iced 的贡献者对其库进行了重大改进,使其成为制作 GUI 应用程序的可行选择。它仍未达到 1.0 版,但凭借正确的知识,它仍可让您构建功能强大的应用程序。
在本教程中,我将向您展示如何在 Iced 中构建基本应用程序。我们将深入了解其小部件的工作原理、如何更新应用程序内的状态以及您必须围绕其构建代码的总体结构。
本教程的唯一先决条件是熟悉使用 Rust 进行编写。
本教程的目标是创建一个简单的购物清单应用程序。我们希望允许在购物清单中添加和删除商品。由于本教程尽量简单,我们不会将清单中的商品存储在文件或数据库中,因此每次重新启动应用程序时,商品都会丢失。因此,遗憾的是,您无法将此应用程序用作真正的购物清单。下次再说吧。
Elm 架构
在编写任何代码之前,我们必须首先了解 Iced 所基于的结构:Elm 架构。它是 GUI 库使用的架构,最初用于 Elm 编程语言。其核心原理很简单。它基于三个概念构建:模型、视图和更新。
- Model,即应用程序的状态。这是所有将根据用户交互等更新的数据。它将用于确定我们希望如何呈现 UI。在整个教程中,我们将其称为应用程序的模型或状态。
- View,显示UI的功能。
- Update,可以更新状态的函数。只有在这个函数中,你的应用的状态才可以被更新。
创建新的 Iced 应用程序
首先,我们将使用 Cargo 启动一个新项目。
cargo new grocery-list-app |
现在,导航到cargo.toml新创建的文件夹中的文件并添加 Iced 作为依赖项。
[package] |
现在,在src您的应用程序文件夹中,我们将这些导入添加到您的main.rs文件中。我们稍后会解释它们的作用。
use iced::{Element, Sandbox, Settings}; |
现在,我们将创建最基本的应用程序。首先,我们必须创建应用程序的主要结构。我们将其称为“GroceryList”。现在,我们将其留空。
struct GroceryList {} |
我们还将创建一个枚举,该枚举也将为空。这将用于指示我们的应用如何更新状态。我们稍后会详细了解这一点,但现在,让我们创建一个名为“Message”的枚举
#[derive(Debug, Clone)] |
现在我们将实现Sandbox特征。这是我们结构的一个重要特征GroceryList。这就是魔法发生的地方。我们还将定义我们的Message枚举来处理应用程序的更新。我们将其写为type Message = Message。
impl Sandbox for GroceryList { |
最后,我们将GroceryList结构添加到函数中main并运行它。
fn main() -> iced::Result { |
我们刚刚写了大量代码,让我们来仔细检查一下。
代码解释:
为了启动我们的应用程序,我们要为 GroceryList 实现一个名为 "Sandbox "的trait特性。 正如我之前所说,"Sandbox "trait对于启动我们的应用程序非常重要。
但它为什么叫 "Sandbox "呢? 沙盒特质是另一个trait "应用程序 "的简化版本。 Sandbox 主要用于制作演示版或简单的应用程序,因此被称为 "Sandbox"。
Application trait允许制作更复杂、更可定制的应用程序。 例如,如果你想为应用程序创建一个新主题,或添加自定义订阅,如来自 HTTP 客户端的请求。 所有这些都不在本教程的讨论范围内,因此我们自然不会涉及应用程序trait。
现在让我们来谈谈Sandbox的方法:
- new 方法用于初始化我们的 GroceryList。 由于此应用程序没有任何状态,因此此结构为空(暂时)。
- title 方法应返回一个我们选择的字符串。 该字符串将用作窗口的标题。 在我的操作系统中,标题显示在窗口顶部。 你可以把这个字符串改成任何你想显示的标题,但它对我们的应用程序并不重要。
- update 方法对我们的应用程序非常重要。 如果你还记得我对 Elm 架构的解释,update 方法用于更新应用程序的状态。 对应用程序所使用数据的所有更改都必须通过 update 方法。
- 最后是view方法。 我们将在这里展示应用程序的用户界面。 在这里,我们使用的是之前导入的部件 "text"。 它将显示传入的字符串。 我们运行该方法,将部件转换为Iced 元素。
运行我们的应用程序
要运行我们的应用程序,我们将像运行其他 Rust 项目一样运行它。在文件夹的根目录中,运行以下命令:
cargo run --release
太棒了!应用运行良好。
更复杂一点
让我们让我们的用户界面更有吸引力一点。
首先,让我们更新导入。我们将添加一个新的小部件。
use iced::{alignment, Element, Length, Sandbox, Settings}; |
接下来我们换一种view方式。
fn view(&self) -> Element<Self::Message> { |
让我们分解一下我们刚刚添加的内容。在我们的view方法中,我们添加了一个新的小部件,container并将我们的text小部件放在其中。容器小部件类似于divhtml 中的。其目的是存储其他元素,在我们的例子中是小部件。与divHTML 不同,容器只能存储一个元素。我们将使用此小部件将文本置于应用程序中的中心。
此外,我们还链式连接了四个方法,用于为容器设计样式。 宽度和高度会改变容器的大小。 我们通过属性 Length::Fill 将容器设置为尽可能大。 接下来,我们设置 align_x 和 align_y,告诉容器内的部件应该放在哪里。 我们指定元素应居中。
最后,我将更改应用程序的默认主题。 我将在 GroceryList 结构中添加一个新方法来更改主题。 这是可选项,但我更喜欢 Iced 的暗色模式。
impl Sandbox for GroceryList { |
Iced 0.12似乎有几个新主题。 因为它不会改变应用程序的功能,所以你可以随意设置主题! 例如,你可以将 iced::Theme::Dark 替换为 iced::Theme::Dracula。
这样看起来更好! 文字在窗口中居中,而且暗色模式对我的眼睛影响较小。
显示杂货Grocery
现在,我们的应用程序非常简单。 我在本教程的前面部分解释了状态,但目前这个应用程序没有状态。 我们将改变这一点。 我们将对当前的应用程序进行两处修改。 为我们的应用程序添加状态。 我们将使用一个外部函数来将一堆部件组合在一起。 这有利于保持应用程序的模块化。
- 为我们的应用程序添加状态。
- 我们将使用一个外部函数来将一系列部件组合在一起。 这有利于保持应用程序的模块化。
在我们做任何事情之前,让我们先改变我们的导入:
use iced::{alignment, Element, Length, Sandbox, Settings}; |
我们还来改变结构体的定义GroceryList。我们将添加一个字符串向量来表示购物清单中的商品。这将是我们的状态。
struct GroceryList { |
为了使其正常工作,我们还将改变方法,new以便正确初始化结构。
impl Sandbox for GroceryList { |
接下来,我们将创建一个新函数。该函数将显示杂货商品列表。老实说,没有必要为此功能创建新函数。我们可以将小部件传递到我们的view方法中。但将代码模块化是一种很好的做法。
impl Sandbox for GroceryList { |
最后,我们将在我们的方法中使用我们的函数view。
fn view(&self) -> Element<Self::Message> { |
这里有很多东西需要解开。让我们来一一分析一下。
首先,我们为应用程序添加了状态。 所有状态都必须作为字段添加到 GroceryList 结构中。 在下一节中,我们将更新状态。
其次,我们添加了一个新部件 column。 我们在 item_list_view 函数中使用这个部件。 我们对它的初始化与文本或容器不同,因为默认情况下我们希望它是空的。 但别误会,它和其他部件一样,也是一个部件!
列column 与容器container 类似,但与容器container 不同的是,它可以包含多个部件,并垂直显示这些部件。 我们传递 spacing 方法,这样每个项目之间就有了一定的间距。
我们在函数中循环传递项目items ,并将它们添加到我们的列中。 这是我发现的将杂货添加到列中的最佳方法。
在函数的最后,我们将列传递到一个具有固定高度和宽度的容器container 中。
我们在视图方法中使用 items_list_view 函数。
如果容器和列的工作方式让你联想到 HTML 和 CSS,这可能并非巧合。列的工作方式很像带有 "列 "方向的 flexbox。 将元素的大小设置为 Length::Fill 与 CSS 中的值 "100%"非常相似。 元素甚至可以像 CSS 一样拥有填充和边框。
添加用户输入
让我们为用户提供一种与我们的应用程序最终交互的方式。我们将添加两种用户输入方式button和一个text_input。
首先,让我们再次更新我们的导入。
use iced::{alignment, Element, Length, Padding, Sandbox, Settings}; |
现在我们要再次更新我们的视图方法。
fn view(&self) -> Element<Self::Message> { |
您会注意到我们正在使用一个新的小部件row。它几乎与列小部件相同,但是,它不是将项目显示在彼此之上,而是水平显示它们。
我们初始化行的方式与之前创建列的方式不同。 我们使用的是 Iced 库提供的宏。 它允许我们初始化一行,就像 vec! 微函数初始化向量中的项目一样。 因此,我们可以在不调用 push 方法的情况下,指定要放入行中的每个元素。 列column也有一个类似的宏,我们也可以在视图view 方法中调用它。
我们还要为行column添加填充。 这将为我们的输入提供一些空间。
Update更新
我们介绍了 Elm 架构的两个核心方面:视图View和状态State。 现在终于到了介绍Update更新的时候了。
在教程的开头,我们创建了一个名为 Message 的枚举。 Message 将用于让我们知道如何更新应用程序的状态。 每个可以接收输入的部件(文本输入、按钮等......)都会发送消息。 我们可以定义要发送的消息类型。 从 widget 发送信息后,我们将在更新update方法中处理这些信息。
在开始之前,让我们先更新一下我们的导入。
use iced::{alignment, widget::{button, column, container, row, scrollable, text, text_input, Column}, Element, Length, Padding, Sandbox, Settings}; |
接下来,让我们设置要发送和接收的消息。我们将更改枚举Message。
#[derive(Debug, Clone)] |
这些信息将代表我们从用户那里接收到的输入。 text_input 和按钮的输入值将分别发送 InputValue 和 Submitted 消息。
我们还必须对状态做一个小改动。 由于我们将从 text_input 接收值,我们必须将这些值存储在某个地方。 因此,我们要在 GroceryList 结构中添加另一个字段。
struct GroceryList { |
与往常一样,我们还必须更改 GroceryList 的初始化方法。
/* Initialize your app */ |
现在,让我们更改视图方法,以便在用户与我们的部件交互时发送这些信息。
fn view(&self) -> Element<Self::Message> { |
在这里,我们添加了一些方法,这些方法将在用户与部件交互时创建消息。 对于按钮,我们使用 on_press 方法来发送 "提交Submitted "消息。
对于文本输入,我们有两个交互方法。
- 当用户按下回车键时,on_submit 将被调用。 我们将发送与点击按钮相同的信息。
- 我们还有 on_input 方法。 该方法在用户键入时触发。 我们将传递一个回调函数,该函数接受一个字符串并返回一条消息。 该消息将存储字符串,以便我们使用该消息更新应用程序。
最后,经过一些准备工作后,我们就可以更新应用程序的状态了。 在未触及的 update 方法中,我们将处理传入该方法的消息。 该函数将传递从用户输入:按钮button 和文本输入text_input接收到的信息。
fn update(&mut self, message: Self::Message) { |
我们正在处理我们创建的两条信息。
- 每当用户在text_input 中添加文本时,我们就会将其作为状态存储到我们创建的字段中。
- 每当用户提交文本时,我们就会将该字符串推送到我们的grocery_items杂货项目中。
- 我们还想清除用户之前输入的值,这样text_input widget 就可以为空。
在运行项目之前,我们需要对用户界面做一个小小的改动。 在我们之前创建的 items_list_view 函数中。
fn items_list_view(items: &Vec<String>) -> Element<'static, Message> { |
我们只需添加一个可滚动scrollable 的小部件来显示杂货清单中的物品。
只要scrollable 部件widget 的内容大于部件widget 本身,用户就可以选择滚动该部件。
现在,如果用户添加了大量的杂货清单项目,用户就可以滚动到不可见的项目。
如果现在运行它,看起来应该与上次运行应用程序时几乎一样,但这次我们可以与它进行实际交互。
我必须承认 我并没有完全诚实。 我曾多次声称,更新应用程序状态的唯一安全方法是使用 update 方法。 这是因为 Rust 变量突变的本质。 除非指定变量为 mut,否则不能对其进行突变。 我们唯一一次使用 GroceryList 结构的可变引用是在 update 方法中。 不过有一个小窍门。 您可以使用标准库中的可变容器(如 Cell、RefCell 和 OnceCell)来修改值,而不需要可变引用。 另一种方法是使用原始可变指针,但这需要使用不安全的 Rust。
删除项目
我们已经学会了如何创建杂货商品,现在我们将通过展示如何删除商品来完成本教程。就像将商品添加到杂货清单一样,我们需要一条消息才能删除它们。
#[derive(Debug, Clone)] |
我们在信息中添加了一个新项目。 DeleteItem 变体将传递一个数字,代表我们要删除的 grocery_items 项目的索引。 让我们将这一更改添加到我们的 update 方法中。
fn update(&mut self, message: Self::Message) { |
这种更改非常简单。 我们只需从矢量中移除指定的项目即可。 现在,让我们通过更改用户界面来完成这个应用程序。 每个杂货清单项目旁边都将有一个按钮。 这个按钮将允许我们的用户删除杂货项目。 让我们创建一个名为 "grocery_item "的新函数。
fn grocery_item(index: usize, value: &str) -> Element<'static, Message> { |
既然已经添加了这个功能,我们之前的函数:items_list_view 也必须更改。 我们将传递 grocery_item 中每个杂货的索引和一个字符串片段。
fn items_list_view(items: &Vec<String>) -> Element<'static, Message> { |
我们的应用程序中应该有删除杂货所需的一切。
经过最后的修改,我们就完成了!
经过所有这些更改后,我的代码库看起来是这样的:
use iced::{alignment, widget::{button, column, container, row, scrollable, text, text_input, Column}, Element, Length, Padding, Sandbox, Settings}; |
最后说明
通过本教程,您将能够使用 iced 库创建一些基本应用程序。但本教程未涵盖 iced 中一些更复杂的主题,这些主题可能会成为您旅程中的障碍。
- 自定义样式。如果您想要自定义按钮、更改容器的背景颜色以及更改小部件的边框或阴影,那么这会更加复杂。如果您想创建自定义主题,情况就更是如此。
- 自定义订阅。现在我们根据用户输入生成消息。但是其他输入呢?也许您希望在经过一定时间后收到消息。或者在收到 HTTP 或 WebSocket 响应后收到消息。这称为订阅。要完全理解它的工作原理需要付出一些努力。
- 您是否缺少某个小部件?您的应用程序是否需要该功能,但库未提供该功能?您可以使用自定义小部件。虽然这是 Iced 的一项更高级的功能,但它是最有用的功能之一。
- 您的应用是否滞后?您不知道如何提高渲染和消息的性能吗?有一些明显的和一些巧妙的方法可以提高应用的性能。截至撰写本文时,关于这些主题的文档并不多。