在上一步中,我们创建了一个 ImageItem 来显示单张图片,作为构建应用图片网格的一部分。
回顾一下我们的图片网格结构:
ImageItem 中。ImageItem 水平排列在一个 ImageRow 中。ImageRow 垂直排列组成一个 ImageGrid。这一步中,我们将把多个 ImageItem 组合到一个 ImageRow 中,以显示一行图片。
注意:如果你不想跟着敲代码,可以在这里找到本步骤的全部代码:https://github.com/makepad/image_viewer/tree/main/step_4
在本步骤中,你将学习:
PortalList 来显示一组项目。PortalList 的内容。ImageRow我们首先添加一个 ImageRow 的定义。ImageRow 负责将多个 ImageItem 水平排列。
在 app.rs 中,将以下代码添加到 live design 块中,紧跟在 ImageItem 的定义之后:
这段代码定义了一个 ImageRow。{{ImageRow}} 语法将我们在 DSL 中定义的 ImageRow 关联到 Rust 代码中的 ImageRow 结构体(我们稍后会介绍这个结构体)。
在 ImageRow 内部,我们使用了一个 PortalList 来列出其中的条目。
这个 PortalList 具有以下属性:
height: 256:确保列表具有足够的垂直空间来显示每一个条目。flow: Right:确保列表的子元素从左到右排列。注意:
PortalList类似于一个标准列表,但它支持“无限滚动”:它只会渲染可见的条目,因此可以高效地处理大型列表。尽管我们其实不需要无限滚动的功能,但在撰写本文时,Makepad 还没有标准列表组件,因此我们使用PortalList作为替代方案。
与其他组件不同,PortalList 的内容并不是通过 DSL 代码来决定的,而是必须通过 Rust 代码动态生成。
因此,下面这一行代码:
它并不像你可能预期的那样定义了一个 ImageItem 实例。相反,它定义了一个 ImageItem 的模板。稍后,当我们在 Rust 代码中生成这个 PortalList 的内容时,就可以使用这个模板来创建所需的各个项的实例。
注意: 回忆一下,
{{ImageRow}}语法告诉 Makepad,ImageRow的定义关联到了 Rust 代码中的一个ImageRow结构体。正因为PortalList的内容必须由 Rust 代码生成,我们才需要使用一个 Rust 结构体来承载它。
现在我们已经定义了 ImageRow,接下来我们将更新 App 的定义,使其显示一个 ImageRow,而不是一个 ImageItem。
在 app.rs 中,将 live design 块中的 App 定义替换为下面这一段代码:
ImageRow 结构体在之前的 DSL 代码中,我们使用 {{ImageRow}} 语法将 ImageRow 链接到了 Rust 代码中的 ImageRow 结构体。通过定义这样一个结构体,我们可以使用 Rust 代码来重写 ImageRow 的行为。
将以下代码添加到 app.rs 中:
请注意,我们为 ImageRow 结构体派生了多个 trait。我们已经了解了 Live 和 LiveHook 这两个 trait 的作用,但 Widget 是新的。我们来更详细地看看这个 trait 的作用。
Widget traitWidget trait 允许我们自定义一个小部件(widget)的行为。
有些令人困惑的是,派生 Widget trait 并不会自动生成该 trait 的实现。相反,它会为 Widget 所依赖的一些辅助 trait 生成实现。这使得我们更容易手动实现 Widget trait,但我们仍然需要自己编写具体的实现代码,我们将在下一节中完成这部分工作。
#[deref] 属性在派生 Widget trait 时会用到。将此属性放在 ImageRow 结构体的 view 字段上,可以让我们像使用 View 一样使用 ImageRow:派生 Widget trait 会自动生成将 ImageRow 解引用为 View 的代码,并建立相应的 DSL 绑定。
Widget trait的实现增加这段代码到 app.rs :
Widget trait 包含两个方法:
handle_event 方法控制 ImageRow 如何响应事件。目前我们不需要对事件进行自定义处理,因此我们只是将所有事件转发给 view。draw_walk 方法控制小部件的绘制方式。由于 ImageRow 中的 PortalList 内容必须在 Rust 代码中定义,因此我们确实需要自定义绘制逻辑,所以该方法的实现会稍微复杂一些。下面我们将详细介绍这个实现。View 中绘制每个项目让我们仔细看看 draw_walk 方法:
在 draw_walk 方法内部,我们首先在一个循环中调用 self.view.draw_walk(cx, scope, walk) 来绘制视图中的每个项目。
view 上的 draw_walk 函数是一个所谓的可恢复函数(resumable function)——它的行为类似于迭代器,在绘制过程中逐个生成项目。
每次调用 draw_walk,它会返回一个特殊的 DrawStep 对象,表示绘制过程的当前状态。然后我们调用该对象的 step 方法,获取下一个应该绘制的项目。调用者(也就是我们)负责绘制每个项目,这使得我们可以自定义它的绘制方式。
当没有更多项目需要绘制时,调用 step 方法会返回 None,循环结束。
PortalListdraw_walk 方法中以下代码负责绘制 PortalList(回想一下,在 ImageRow 中,我们使用 PortalList 来列出其项目。)
对于每个要绘制的项目,我们首先调用 as_portal_list 来检查该项目是否是一个 PortalList。一旦获得对 PortalList 的引用:
list.set_item_range(cx, 0, 4),告诉列表我们需要 4 个项目。next_visible_item(cx) 来迭代每个可见项目的索引。对于每个索引:
list.item(...) 来获取该索引处项目的引用。item.draw_all(...) 将该项目绘制到屏幕上。每个 PortalList 有自己独立的索引命名空间 —— 这意味着每个列表中每个项目的索引是唯一的。调用 list.item(cx, item_index, live_id!(ImageItem)) 会检查给定索引是否已有对应的项目实例。如果没有,它会创建该项目的实例。
那么,调用 list.item(cx, item_index, live_id!(ImageItem)) 是如何知道要实例化什么呢?很简单:它使用我们之前在 DSL 中定义的 ImageItem 模板:
注意:
live_id!宏用于在 Makepad 中生成唯一标识符。在这里,live_id!(ImageItem)指的是 ImageItem 的模板。
让我们检查一下目前为止的进展。
确保你在你的包目录中,然后运行:
如果一切正常,屏幕上应该会出现一个包含一排占位符图片的窗口:

在这一步中,我们创建了一个 ImageRow 用来显示一排图片。下一步,我们将把多个 ImageRow 组合到一个 ImageGrid 中,以显示一个图片网格。