2 - 创建一个空窗口

在上一步中,我们创建了一个新的空的 Cargo 包。在这一步中,我们将把这个空包变成一个最小的 Makepad 应用,仅包含一个空窗口。

你将学到

在这一节中,你将学习:

  • 如何添加 Makepad 作为依赖项。
  • 如何使用 DSL 代码定义应用的布局和样式。
  • 如何创建一个空窗口。

💡 如果你不想跟着手动输入,可以直接查看这一步的完整代码:GitHub 链接


添加 Makepad 作为依赖

我们从向项目添加 Makepad 依赖开始。

Makepad 的顶层 crate 是 makepad-widgets,它包含了构建应用所需的一切。

运行以下命令来添加依赖:

1cargo add makepad-widgets

添加代码

接下来,我们将添加所需的代码。

将以下文件复制到src目录中(我们随后会解释代码的作用):

app.rs:

1use makepad_widgets::*;
2
3live_design! {
4    use link::widgets::*;
5
6    App = {{App}} {
7        ui: <Root> {
8            <Window> {
9                body = <View> {}
10            }
11        }
12    }
13}
14
15#[derive(Live, LiveHook)]
16pub struct App {
17    #[live]
18    ui: WidgetRef,
19}
20
21impl AppMain for App {
22    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
23        self.ui.handle_event(cx, event, &mut Scope::empty());
24    }
25}
26
27impl LiveRegister for App {
28    fn live_register(cx: &mut Cx) {
29        makepad_widgets::live_design(cx);
30    }
31}
32
33app_main!(App);

lib.rs:

1pub mod app;

main.rs:

1fn main() {
2    ::app::app_main()
3}

检查当前进展

确保你在项目目录下,运行:

1cargo run --release

如果一切正常,你应该会看到一个空窗口:

代码解析

Live Design 块

以下代码定义了一个live design块:

1live_design! {
2    use link::widgets::*;
3
4    App = {{App}} {
5        ui: <Root> {
6            <Window> {
7                body = <View> {}
8            }
9        }
10    }
11}

Live Design 块用于定义 Makepad 的DSL 代码。DSL 在 Makepad 中用于描述布局和样式,类似 CSS。

让我们逐行拆解:

  • use link::widgets::*'; 导入内置组件,比如 Root,WindowView

  • App = {{App}} { ... } 定义了一个 App

  • {{App}} 语法表示该 DSL 定义与 Rust 中的 App 结构体相关联。

  • 在 App 中,我们将 ui 字段定义为组件树的根节点:

    • <Root> { ... } 是我们的顶层容器。

    • <Window> { ... } 是屏幕上的一个窗口。

    • body = <View> {} 是一个命名为 body 的空视图。

Live 设计系统

当我们运行应用程序时,Makepad 使用一种叫做 Live 设计系统 的机制,将 DSL 代码与 Rust 代码进行桥接。

其核心思想如下:

  • 应用的布局和样式是通过 DSL 代码定义的。

  • Live 设计系统会将 DSL 中的定义匹配到对应的 Rust 结构体,并用 DSL 中的值初始化这些结构体。

  • 如果稍后在 Makepad Studio 中修改了 DSL 代码,相关的 Rust 结构体会在运行时自动更新,无需重新启动应用程序。

Live 设计系统使 Makepad 拥有强大的 运行时样式编辑能力。你可以在 Makepad Studio 中动态调整 UI 的布局、颜色以及其他样式属性,并且能在应用运行时立即看到修改效果 —— 无需重新编译!

App 结构体

以下代码定义了 App 结构体:

1#[derive(Live, LiveHook)]
2pub struct App {
3    #[live]
4    ui: WidgetRef,
5}

App 结构体是我们应用程序的核心。现在它仅包含我们组件树(widget tree)的根节点。稍后我们还会在其中添加应用所需的各种状态。

注意,我们为 App 结构体派生了两个 trait:LiveLiveHook。接下来我们将更详细地了解这两个 trait。

Live Trait

Live trait 使结构体能够与 Live 设计系统进行交互。

要为某个结构体派生 Live trait,该结构体中的每个字段都需要标注以下属性之一:

  • #[live] 属性表示该字段属于 Live 设计系统的一部分。
  • #[rust] 属性表示该字段是普通的 Rust 字段。

当 Live 设计系统遇到标有 #[live] 属性的字段时,它会在 DSL(领域特定语言)代码中查找该字段的匹配定义。如果找到了,就使用 DSL 中的定义来实例化该字段;如果没有找到,则会使用默认值来实例化该字段。

相对地,当系统遇到标有 #[rust] 属性的字段时,它会使用该字段类型的 Default::default 构造函数来实例化这个字段。

在我们的例子中,我们用 #[live] 属性标记了 App 结构体中的 ui 字段。由于我们在 DSL 中为该字段提供了相应的定义,Live 设计系统会自动根据 DSL 中的定义填充 ui 字段,构建出组件树:一个包含一个空 ViewWindow,由 Root 组件包裹。

LiveHook Trait

LiveHook trait 提供了一些可被重写(overridable)的方法,这些方法会在应用程序生命周期的不同阶段被调用。

目前我们暂时用不到这些方法,因此可以为 App 结构体定义一个空的 LiveHook 实现:

1impl LiveHook for App {}

或者,也可以像我们现在这样为 App 结构体派生(derive)LiveHook trait。这样会自动生成与上面相同的实现,从而省去了手动编写的工作。

等到我们真正需要用到这些方法时,再来详细解释 LiveHook trait 的具体作用。

AppMain Trait

以下代码为 App 结构体实现了 AppMain trait:

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

AppMain trait 用于将 App 结构体的一个实例挂接(hook)到主事件循环中。在我们的实现中,我们只是简单地将所有事件转发给组件树的根节点,由它来进行适当的处理。

在 Makepad 中,Scope(作用域) 是一个容器,用于在事件传递过程中携带应用级别的数据以及组件特定的属性。

目前我们还没有任何状态,因此暂时使用 Scope::empty() 来为每个事件创建一个空的作用域。关于作用域的更多内容,我们将在后面进一步介绍。

LiveRegister Trait

以下代码为 App 结构体实现了 LiveRegister trait:

1impl LiveRegister for App {
2    fn live_register(cx: &mut Cx) {
3        makepad_widgets::live_design(cx);
4    }
5}

LiveRegister trait 用于注册 DSL 代码。通过注册 DSL 代码,我们可以让整个应用访问这些定义。

前面我们提到过,live design 块用于定义 DSL 代码。在底层,每个 live design 块都会生成一个 live_design 函数,当这个函数被调用时,会将该块中的 DSL 代码注册进系统。而我们正是在 LiveRegister trait 的实现中调用这些 live_design 函数。

在我们的实现中,我们调用了 makepad_widgets::live_design 函数,用于注册 makepad_widgets crate 中的 DSL 代码。如果不调用这个函数,我们将无法使用任何内置组件,比如之前看到的 RootWindowView 等组件。

注意:由 app.rs 文件顶部的 live_design 宏生成的 live_design 函数是一个特殊函数。我们不需要在这里手动调用它,因为它会被 app_main 函数自动调用。而这个 app_main 函数是由 app_main 宏生成的,我们接下来会讲解它。

app_main

以下代码调用了 app_main 宏。

1app_main!(App);

app_main 宏会生成一个 app_main 函数,里面包含启动应用所需的所有代码。

app_main 函数的功能如下:

  • 使用 LiveRegister trait 注册额外的 DSL 代码。
  • 注册文件顶部的 DSL 代码。
  • 使用 live design 系统实例化 App 结构体。
  • 使用 AppMain trait 将该 App 结构体实例挂接到主事件循环中。

下一步

我们现在有了一个最简的 Makepad 应用程序,包含一个空窗口。接下来的步骤中,我们将开始为应用构建一个图片网格。