news 2026/9/13 15:13:36

用 Slint + Rust 实现 MVC 架构:todo-mvc 示例应用全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 Slint + Rust 实现 MVC 架构:todo-mvc 示例应用全解析

用 Slint + Rust 实现 MVC 架构:todo-mvc 示例应用全解析

【免费下载链接】slintSlint is an open-source declarative GUI toolkit to build native user interfaces for Rust, C++, JavaScript, or Python apps.项目地址: https://gitcode.com/GitHub_Trending/sl/slint

导读

examples/todo-mvc是 Slint 官方仓库中一个以经典 Model-View-Controller(MVC) 模式组织代码的待办事项(Todo)示例应用。它演示了如何在 Rust 中把「业务数据模型」「控制器逻辑」与 Slint 声明式 UI 完全解耦:UI 层只依赖 adapter(适配器)与 Slint 全局单例(global)进行数据读写,数据层通过 trait 抽象出 repository 接口,并配有可直接运行的 Mock 实现。读完本文,你将掌握 Slint 应用中如何落地 MVC 分层、如何实现自定义Model驱动列表 UI、如何用global单例桥接视图与控制器,以及如何将同一个示例编译为桌面程序与 WebAssembly 在线预览。

该示例的完整文件结构为:.slint设计文件位于 examples/todo-mvc/ui,Rust 源码位于 examples/todo-mvc/rust/src。本文所有引用均以仓库根目录为基准。

一、示例概览:文件结构与角色分工

整个示例分为两个主要目录,各司其职:

目录内容职责
ui/.slint声明式界面文件View(视图)层,纯 UI 描述
rust/src/Rust 源码(mvc 子模块 + adapter)Model、Controller、Repository 与桥接逻辑
assets/add.svgclose.svgremove.svg图标资源

Rust 侧的结构体现了清晰的分层:

rust/src/ ├── main.rs # 二进制入口,仅调用库的 main() ├── lib.rs # 库入口:初始化窗口、控制器并完成连接 ├── callback.rs # 自实现的轻量回调封装 ├── ui.rs # include_modules!() 引入编译后的 .slint 类型 ├── mvc.rs # MVC 根模块:重导出 controllers/models/repositories └── mvc/ ├── controllers/ # 控制器:CreateTaskController、TaskListController ├── models/ # 数据模型:TaskModel、DateModel、TimeModel └── repositories/ # 仓库:trait 定义 + Mock 实现

main.rs只做一件事——调用库的入口函数:

// examples/todo-mvc/rust/src/main.rs fn main() { todo_lib_mvc::main(); }

业务逻辑全部收敛在库(lib.rs,crate 名为todo_lib_mvc)中,这样桌面二进制与 wasm 目标可以共享同一套初始化逻辑。

二、从零搭建:Cargo 依赖与入口初始化

2.1 依赖清单

示例的 Cargo.toml 展示了 Slint 应用的标准依赖组合:

[package] name = "todo-mvc" version = "1.18.0" edition.workspace = true build = "build.rs" license = "MIT" [lib] crate-type = ["lib", "cdylib"] # 同时支持原生链接与 wasm cdylib name = "todo_lib_mvc" [[bin]] name = "todo-mvc" [dependencies] slint = { path = "../../../api/rs/slint", default-features = false, features = ["compat-1-18", "std", "serde", "backend-android-activity-06"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" chrono = { version = "0.4" } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = { version = "0.2" } console_error_panic_hook = "0.1.5" [build-dependencies] slint-build = { path = "../../../api/rs/build" }

几个值得注意的细节:

  • slint依赖直接指向仓库内的 api/rs/slint,说明它作为工作区成员与其他 crate 一起构建;default-features = false后显式开启compat-1-18stdserdebackend-android-activity-06,其中backend-android-activity-06为 Android 目标预留了后端支持。
  • 库同时声明libcdylib两种 crate 类型:原生桌面程序使用lib,浏览器 wasm 场景则依赖cdylib配合wasm-bindgen导出start函数。
  • slint-build作为 build 依赖,在编译期把.slint文件编译成 Rust 代码。

2.2 入口初始化流程

lib.rs 是整个应用的装配核心,其init()函数按「创建视图 → 创建控制器 → 用 adapter 连接」三步完成启动:

fn init() -> ui::MainWindow { let view_handle = ui::MainWindow::new().unwrap(); let task_list_controller = mvc::TaskListController::new(mvc::task_repo()); ui::task_list_adapter::connect(&view_handle, task_list_controller.clone()); ui::navigation_adapter::connect_task_list_controller( &view_handle, task_list_controller.clone(), ); let create_task_controller = mvc::CreateTaskController::new(mvc::date_time_repo()); ui::create_task_adapter::connect(&view_handle, create_task_controller.clone()); ui::navigation_adapter::connect_create_task_controller(&view_handle, create_task_controller); ui::create_task_adapter::connect_task_list_controller(&view_handle, task_list_controller); view_handle }

关键点:

  • 两个控制器各自持有不同的仓库依赖:TaskListController注入task_repo()CreateTaskController注入date_time_repo()。这是典型的依赖注入写法——控制器不关心仓库是内存 Mock 还是数据库实现。
  • 控制器以clone()方式被多处共享,配合Rc实现引用计数共享(详见下文控制器实现)。
  • 页面跳转由navigation_adapter统一管理:任务列表页 → 创建任务页通过show_create_task回调触发invoke_next_page(),返回时触发invoke_previous_page()
  • 编译为 wasm 时,#[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))]main()成为浏览器中的自动启动入口。

三、数据层:Model 定义与 Repository 抽象

3.1 三个数据模型

Model 层是纯数据结构,不依赖任何 UI 类型。定义在 models 目录下:

// models/task_model.rs —— 任务项 #[derive(Clone, Default, Debug, PartialEq)] pub struct TaskModel { pub title: String, pub due_date: i64, // due date in milliseconds(毫秒时间戳) pub done: bool, } // models/date_model.rs —— 日期 #[derive(Copy, Clone, Default, Debug, PartialEq)] pub struct DateModel { pub year: i32, pub month: u32, pub day: u32, } // models/time_model.rs —— 时间 #[derive(Copy, Clone, Default, Debug, PartialEq)] pub struct TimeModel { pub hour: u32, pub minute: u32, pub second: u32, }

TaskModeldue_date以毫秒为单位存储,UI 层通过chrono::DateTime::from_timestamp_millis格式化为"Thu, Jun 6, 2024 16:29"这样的可读文本(见下文 adapter 一节)。derive(PartialEq)使模型可以直接参与assert_eq!断言,配合#[cfg(test)]单元测试使用。

3.2 Repository trait:把数据源抽象出来

仓库接口定义在 repositories/traits 下,全部以 trait 形式暴露,让上层只依赖抽象:

// traits/task_repository.rs pub trait TaskRepository { fn task_count(&self) -> usize; fn get_task(&self, index: usize) -> Option<mvc::TaskModel>; fn toggle_done(&self, index: usize) -> bool; fn remove_task(&self, index: usize) -> bool; fn push_task(&self, task: mvc::TaskModel) -> bool; } // traits/date_time_repository.rs pub trait DateTimeRepository { fn current_date(&self) -> mvc::DateModel; fn current_time(&self) -> mvc::TimeModel; fn date_to_string(&self, date: mvc::DateModel) -> String; fn time_to_string(&self, time: mvc::TimeModel) -> String; fn time_stamp(&self, date: mvc::DateModel, time: mvc::TimeModel) -> i32; }

注意TaskRepository的写操作(toggle_doneremove_taskpush_task)都返回bool,用于告知操作是否成功——例如索引越界时remove_task返回false,控制器据此决定是否通知 UI 刷新。这种设计让「数据变更是否生效」的判定权留在数据层。

3.3 Mock 实现与默认装配

repositories.rs 中提供了两个工厂函数,直接注入默认数据:

pub fn date_time_repo() -> impl traits::DateTimeRepository + Clone { MockDateTimeRepository::new( DateModel { year: 2024, month: 6, day: 11 }, TimeModel { hour: 16, minute: 43, second: 0 }, 1718183634, ) } pub fn task_repo() -> impl traits::TaskRepository + Clone { MockTaskRepository::new(vec![ TaskModel { title: "Learn Rust".into(), done: true, due_date: 1717686537151 }, TaskModel { title: "Learn Slint".into(), done: true, due_date: 1717686537151 }, TaskModel { title: "Create project with Rust and Slint".into(), done: true, due_date: 1717686537151 }, ]) }

MockTaskRepository(实现见 mock_task_repository.rs)内部用Rc<RefCell<Vec<TaskModel>>>保存数据,Clone时共享同一份底层数据——这正是控制器可以多处克隆、数据仍然一致的原因。MockDateTimeRepository则纯粹返回构造时给定的日期时间与时间戳,适合演示和测试。

从源码结构可以推断:后续要接入真实数据源,只需新增一个实现TaskRepository/DateTimeRepository的结构体(如数据库或网络实现),控制器的代码完全不用改动。

四、控制器层:行为逻辑的集中地

控制器目录 controllers 下有两个控制器,它们持有仓库引用与回调,向上对 adapter 暴露操作接口。

4.1 TaskListController:任务列表的行为入口

task_list_controller.rs 的核心是一个同时实现 SlintModeltrait 的私有TaskModel包装结构

#[derive(Clone)] pub struct TaskListController { task_model: TaskModel, // 私有模型包装 show_create_task_callback: Rc<Callback<(), ()>>, // “跳转到创建页”事件 }

对外提供的方法:

  • task_model() -> ModelRc<mvc::TaskModel>:把内部模型包装成 Slint 可直接绑定的ModelRc,交给 UI。
  • toggle_done(index)/remove_task(index)/create_task(title, due_date):增删改操作,内部先调用仓库,成功后再通知模型变更。
  • show_create_task()on_show_create_task(callback):事件发布/订阅接口,用于触发页面跳转。

4.2 自定义 Slint Model:让数据变更自动刷新 UI

关键技巧在于 TaskModel 的 Model 实现:

#[derive(Clone)] struct TaskModel { repo: Rc<dyn mvc::traits::TaskRepository>, notify: Rc<ModelNotify>, } impl Model for TaskModel { type Data = mvc::TaskModel; fn row_count(&self) -> usize { self.repo.task_count() } fn row_data(&self, row: usize) -> Option<Self::Data> { self.repo.get_task(row) } fn model_tracker(&self) -> &dyn ModelTracker { self.notify.as_ref() } }

这是 Slint 自定义模型的“标准三段式”:

  1. 数据来源不落地row_count/row_data直接透传给 repository,模型本身不缓存数据副本;
  2. 变更通知:每次写操作成功后调用self.notify.row_changed(index)row_removed(index, 1)row_added(row_count()-1, 1),通知 Slint 运行时某行变化,触发绑定重算与重绘;
  3. 写失败则静默toggle_doneremove_taskpush_task都先检查仓库返回值,false时直接return,不发出无效通知。

由于控制器持有了Rc<dyn TaskRepository>,当创建任务页提交新任务时,是通过create_task写入同一份仓库数据,列表页的模型因此自动感知变化——这是整个示例「跨页面数据同步」的底层原理。

4.3 CreateTaskController:日期时间与新建任务

create_task_controller.rs 负责创建任务页:提供current_date()current_time()date_string()time_string()time_stamp()等纯查询方法,以及back()/on_back()返回导航事件。它同样注入DateTimeRepository,格式化逻辑(如"2020/10/5""10:12")完全由仓库实现,控制器只做转发。

五、Adapter 层:View 与 Controller 的桥

Adapter 是 MVC 在这个示例中的独特设计:它把.slint中声明的global单例(如TaskListAdapterCreateTaskAdapterNavigationAdapter)与 Rust 控制器连接起来,所有回调都集中在一个文件中,便于维护。

5.1 TaskListAdapter:把 Model 映射成 UI 项

task_list_adapter.rs 完成两类工作:

(1)数据流(Model → View):用MapModelTaskModel映射为 UI 期望的SelectionListViewItem,然后通过set_tasks塞进全局单例:

view_handle .global::<ui::TaskListAdapter>() .set_tasks(Rc::new(MapModel::new(controller.task_model(), map_task_to_item)).into());

映射函数把业务字段翻译为 UI 字段,并顺带完成日期格式化:

fn map_task_to_item(task: TaskModel) -> ui::SelectionListViewItem { ui::SelectionListViewItem { text: task.title.into(), checked: task.done, description: DateTime::from_timestamp_millis(task.due_date) .unwrap() .format("%a, %b %d, %Y %H:%M") // 例如:Thu, Jun 6, 2024 16:29 .to_string() .into(), } }

(2)事件流(View → Controller):为TaskListAdapter的每个回调绑定控制器方法:

adapter.on_toggle_task_checked(move |index| controller.toggle_done(index as usize)); adapter.on_remove_task(move |index| controller.remove_task(index as usize)); adapter.on_show_create_task(move || controller.show_create_task());

5.2 CreateTaskAdapter 与类型映射

create_task_adapter.rs 除了连接回调,还演示了 Slint 结构体与 Rust 结构体之间的互相转换:DateModelui::DateTimeModelui::Time各有map_*_to_*双向函数,确保类型边界清晰。创建任务的流程是:UI 收集标题与日期时间 → 通过on_create(title, time_stamp)回调 →connect_task_list_controller中调用controller.create_task(title.as_str(), time_stamp as i64)写入仓库。

5.3 NavigationAdapter:页面导航

navigation_adapter.rs 只做两件事——把「创建页返回」绑定到invoke_previous_page(),把「列表页去创建」绑定到invoke_next_page()。它通过view_handle.as_weak()持有窗口弱引用,避免循环引用:

controller.on_back({ let view_handle = view_handle.as_weak(); move || { view_handle.unwrap().global::<ui::NavigationAdapter>().invoke_previous_page(); } });

5.4 自实现回调:callback.rs

控制器内部的事件机制没有直接使用 Slint API,而是仓库内的轻量封装 callback.rs:一个基于Cell<Option<Box<dyn FnMut>>>的单槽回调,提供on(f)注册与invoke(args)触发,invoke通过take/set实现 FnMut 的重复调用。源码还自带了test_invoke单元测试验证回调求和行为。这个小工具展示了一种「控制器不依赖 UI 框架」的隔离思路。

六、View 层:.slint 声明式界面

UI 文件位于 examples/todo-mvc/ui,组织方式同样模块化:

  • index.slint:根组件,负责页面切换(列表页 / 创建页)与资源导入;
  • views/task_list_view.slint:任务列表视图,绑定tasks模型,处理勾选、删除、新建入口;
  • views/create_task_view.slint:创建任务视图,包含日期时间选择与提交;
  • widgets:可复用控件,如action_button.slinticon_button.slinttext_button.slintselection_list_view.slintstate_layer.slintfocus_touch_area.slintstyling.slint

这些控件与global单例(TaskListAdapterCreateTaskAdapterNavigationAdapter)配合,构成了 View 层的全部行为面:.slint只负责「展示什么、触发什么回调」,而「数据从哪来、操作后做什么」全部交给 Rust 侧的 adapter 与控制器。

七、可验证性:单元测试与运行方式

7.1 控制器层自带单元测试

两个控制器都内嵌#[cfg(test)]测试模块,可以直接作为 MVC 分层正确性的回归保障:

  • TaskListController测试(task_list_controller.rs):构造两个任务的 Mock 仓库,验证row_countrow_datatoggle_done状态翻转、remove_task删除、create_task追加以及show_create_task回调触发。其中test_remove_task断言删除后行数从 2 变为 1,且剩余项顺序正确。
  • CreateTaskController测试(create_task_controller.rs):验证日期/时间查询、date_string输出"2020/10/5"time_string输出"10:12"back回调触发、time_stamp返回固定值。

这些测试印证了「控制器不依赖任何 UI 环境即可独立测试」——这也是把仓库抽象成 trait、把时间逻辑放进 repository 的直接收益。

7.2 如何运行

桌面运行(在仓库根目录执行):

cargo run --package todo-mvc

WebAssembly 在线预览:官方提供了基于master快照的在线模拟(参见 examples/todo-mvc/README.md 中的 Online wasm Preview 链接);本地构建 wasm 时,lib.rs中的wasm_bindgen(start)会作为自动启动入口。rust/目录下还提供了 index.html 作为 wasm 宿主页面。依赖的slint后端特性backend-android-activity-06表明该示例也预留了 Android 目标支持(源码末尾有// FIXME: android example注释,说明 Android 适配仍在完善中)。

八、模式总结:从示例提炼可复用的 MVC 骨架

综合整个示例,可以提炼出一套「Slint + Rust 的 MVC 落地套路」,适用于任何中小型应用:

  1. Model:用纯 Rust 结构体描述数据(如TaskModel),不 import 任何slint::类型;
  2. Repository:用 trait 抽象数据源,Mock 实现便于测试与演示,真实实现后续替换;
  3. Controller:持有 repository(Rc共享),对外暴露命令式操作方法与事件订阅接口;需要列表时,实现 Slint 的Modeltrait 并配合ModelNotify做变更通知;
  4. View.slint文件只声明界面结构与回调名,通过global单例与 UI 类型(如SelectionListViewItem)暴露数据与事件;
  5. Adapter:一个 adapter 对应一个global,集中完成「数据模型 → UI 项」映射与「UI 回调 → 控制器方法」绑定,使 View 与 Controller 零耦合;
  6. Navigation:把页面跳转也抽象成NavigationAdapter,由控制器事件驱动。

这一模式的直接收益是:控制器与视图可以独立开发、独立测试,数据源可以随时替换。对照仓库中的其他示例(如 examples/todo、examples/printerdemo)可以看到,它们是更直接的「UI + 逻辑」风格;而 todo-mvc 专门示范了在大一点的应用中如何用 MVC 维持代码秩序。

参考文件索引

  • 示例说明文档:examples/todo-mvc/README.md
  • UI 设计文件:examples/todo-mvc/ui/index.slint
  • 程序入口:examples/todo-mvc/rust/src/lib.rs、examples/todo-mvc/rust/src/main.rs
  • 控制器实现:task_list_controller.rs、create_task_controller.rs
  • 模型与仓库:models、repositories
  • Adapter 桥接:task_list_adapter.rs、create_task_adapter.rs、navigation_adapter.rs
  • 依赖配置:examples/todo-mvc/rust/Cargo.toml

【免费下载链接】slintSlint is an open-source declarative GUI toolkit to build native user interfaces for Rust, C++, JavaScript, or Python apps.项目地址: https://gitcode.com/GitHub_Trending/sl/slint

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 15:12:13

LunaTranslator视觉小说翻译器实测:30分钟跑通游戏内实时翻译

LunaTranslator视觉小说翻译器实测&#xff1a;30分钟跑通游戏内实时翻译 【免费下载链接】LunaTranslator 视觉小说翻译器 / Visual Novel Translator 项目地址: https://gitcode.com/GitHub_Trending/lu/LunaTranslator 游戏停在选项界面&#xff0c;屏幕上一行日文对…

作者头像 李华
网站建设 2026/9/13 15:11:24

UnoCSS preset-icons 深度指南:把任意 Iconify 图标变成 Pure CSS 图标

UnoCSS preset-icons 深度指南&#xff1a;把任意 Iconify 图标变成 Pure CSS 图标 【免费下载链接】unocss The instant on-demand atomic CSS engine. 项目地址: https://gitcode.com/GitHub_Trending/un/unocss UnoCSS 的 preset-icons 预置让开发者无需图标字体、无…

作者头像 李华
网站建设 2026/9/13 15:10:52

D3QN多步学习与SNARM框架:无人机3D路径规划实战

简介&#xff1a;基于深度强化学习技术&#xff0c;提出结合D3QN算法与多步学习的无人机三维路径优化算法&#xff0c;并进一步构建三维同步导航与无线电测绘&#xff08;SNARM&#xff09;新框架。该资源为人工智能、通信工程、自动化、电子信息、物联网等专业的学生、教师及企…

作者头像 李华
网站建设 2026/9/13 15:10:03

VMware虚拟机安装Ubuntu与VMware Tools配置完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华