12 - 处理事件

在上一步中,我们为应用添加了更多状态,以使幻灯片更加动态。

回顾我们的计划:

  • 首先,我们添加更多状态。
  • 然后,使用这些状态来响应用户事件。

我们已经添加了状态,现在将使用它来处理用户事件。

注意:如果你不想手动输入代码,可以在这里找到本步骤的所有代码:https://github.com/makepad/image_viewer/tree/main/step_12

你将学到的内容

  • Makepad 中事件与动作的流程是如何工作的。
  • 如何响应用户事件。

添加辅助方法

为了让后续代码更易编写,我们将为 App 结构体添加更多辅助函数。

app.rs 文件中,添加以下代码到 App 结构体的 impl 块中:

1pub fn navigate_left(&mut self, cx: &mut Cx) {
2        if let Some(image_idx) = self.state.current_image_idx {
3            if image_idx > 0 {
4                self.set_current_image(cx, Some(image_idx - 1));
5            }
6        }
7    }
8
9    pub fn navigate_right(&mut self, cx: &mut Cx) {
10        if let Some(image_idx) = self.state.num_images() {
11            if image_idx + 1 < self.state.image_paths.len() {
12                self.set_current_image(cx, Some(image_idx + 1));
13            }
14        }
15    }

这些方法的作用如下:

  • navigate_left 会先将 current_image_idx 减 1,除非已经是第一张图片。
  • navigate_right 会先将 current_image_idx 加 1,除非已经是最后一张图片。
  • 两个方法都会调用 set_current_image 来应用更改,并安排幻灯片重绘。

事件-动作流程

我们刚刚添加了几个辅助方法,用来方便在运行时更新幻灯片。下一步是将这些方法连接起来,使得当用户与 UI 交互时(点击幻灯片中的按钮,或者按键盘上的左右方向键),这些方法能够被调用。为此,我们将使用一种叫做“动作”(actions)的机制。

动作是通知某个控件(widget)发生了有趣事件的通知。例如,Button 控件有一个 clicked 动作,当按钮被点击时,它会向应用的其他部分发送该通知。

相比之下,Makepad 中的事件(event)是用户进行了某种操作的通知。例如,用户点击鼠标按钮时,会触发一个 mouse-down 事件,并发送给应用。

下面是 Makepad 中事件-动作流程的工作原理:

  • 事件被派发给应用,通知应用用户执行了某个操作。
  • 事件从应用根节点向下传播,传递到控件树的最底层。
  • 当事件到达一个准备处理它的控件时,该控件会:
    • 根据事件更新自身状态。
    • 可能派发一个动作,通知应用的其他部分该控件发生了有趣的事情。
  • 动作从控件向上传播,直到应用的根节点。
  • 树中更高层的控件可以监听并响应这些动作。

这种模式有助于将底层的输入处理与高层的 UI 行为分离开来。

实现MatchEvent Trait

为了处理我们应用中的动作,我们可以使用 MatchEvent trait。

MatchEvent trait 提供了几个可以重写的方法,这些方法会在特定事件发生时被调用。当你在 MatchEvent trait 上调用 match_event 方法并传入一个事件时,它会自动将该事件转发给对应的方法。

我们将从在 App 结构体的 handle_event 方法中添加对 match_event 的调用开始:

app.rs 文件中,用下面的代码替换 App 结构体的 AppMain trait 实现:

1impl AppMain for App {
2    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
3        self.match_event(cx, event);
4        self.ui
5            .handle_event(cx, event, &mut Scope::with_data(&mut self.state));
6    }
7}

接下来,我们将为 App 结构体实现 MatchEvent trait。对于我们的使用场景,重点关注的方法是 handle_actions

请在 app.rs 中添加以下代码:

1impl MatchEvent for App {
2    fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
3        if self.ui.button(id!(navigate_left)).clicked(&actions) {
4            self.navigate_left(cx);
5        }
6        if self.ui.button(id!(navigate_right)).clicked(&actions) {
7            self.navigate_right(cx);
8        }
9
10        if let Some(event) =
11            self.ui.view(id!(slideshow.overlay)).key_down(&actions)
12        {
13            match event.key_code {
14                KeyCode::ArrowLeft => self.navigate_left(cx),
15                KeyCode::ArrowRight => self.navigate_right(cx),
16                _ => {}
17            }
18        }
19    }
20}

让我们更详细地看看这段代码的作用。

处理按钮点击事件

以下代码:

1if self.ui.button(id!(navigate_left)).clicked(&actions) {
2        self.navigate_left(cx);
3    }
4    if self.ui.button(id!(navigate_right)).clicked(&actions) {
5        self.navigate_right(cx);
6    }

这段代码会检查幻灯片中的某个按钮是否被点击。如果是,它会调用相应的辅助方法(navigate_leftnavigate_right)来更新幻灯片。

注意:当一个或多个动作由某个控件触发时,会派发一个包含这些动作列表的动作事件。当对 MatchEvent trait 调用 match_event 方法并传入动作事件时,会导致 handle_actions 方法被调用,并传入触发的动作列表。为了处理动作,每个控件都会提供一个或多个辅助方法,比如上面 Button 上的 clicked 方法。当你使用动作列表调用该方法时,它会检查列表中是否有动作表示按钮被点击,如果有则返回 true

处理按键事件

以下代码:

1if let Some(event) =
2        self.ui.view(id!(slideshow.overlay)).key_down(&actions)
3    {
4        match event.key_code {
5            KeyCode::ArrowLeft => self.navigate_left(cx),
6            KeyCode::ArrowRight => self.navigate_right(cx),
7            _ => {}
8        }
9    }

这段代码会检查幻灯片覆盖层是否具有键盘焦点,并且是否按下了左箭头或右箭头键。如果是,它会调用相应的辅助方法(navigate_leftnavigate_right)来更新幻灯片。

检查目前的进度

让我们检查一下到目前为止的进展。

确保你当前在你的包目录下,然后运行:

1cargo run --release

如果一切正常,你应该会看到和之前一样的幻灯片播放效果。

不过这一次,点击左右两边的按钮应该分别导航幻灯片到上一张或下一张图片。或者,按键盘上的左箭头或右箭头键也应产生相同的效果:

下一步

我们现在已经有了一个相当不错的幻灯片实现。它可以显示真实的图片,并且能响应用户事件,比如按钮点击和鼠标点击,从而导航到上一张或下一张图片。

我们暂时先保持幻灯片的现状。在接下来的几个步骤中,我们将实现能够在图片查看器和幻灯片之间切换的功能。