
用 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-ControllerMVC 模式组织代码的待办事项Todo示例应用。它演示了如何在 Rust 中把「业务数据模型」「控制器逻辑」与 Slint 声明式 UI 完全解耦UI 层只依赖 adapter适配器与 Slint 全局单例global进行数据读写数据层通过 trait 抽象出 repository 接口并配有可直接运行的 Mock 实现。读完本文你将掌握 Slint 应用中如何落地 MVC 分层、如何实现自定义Model驱动列表 UI、如何用global单例桥接视图与控制器以及如何将同一个示例编译为桌面程序与 WebAssembly 在线预览。该示例的完整文件结构为.slint设计文件位于 examples/todo-mvc/uiRust 源码位于 examples/todo-mvc/rust/src。本文所有引用均以仓库根目录为基准。一、示例概览文件结构与角色分工整个示例分为两个主要目录各司其职目录内容职责ui/.slint声明式界面文件View视图层纯 UI 描述rust/src/Rust 源码mvc 子模块 adapterModel、Controller、Repository 与桥接逻辑assets/add.svg、close.svg、remove.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.rscrate 名为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-18、std、serde、backend-android-activity-06其中backend-android-activity-06为 Android 目标预留了后端支持。库同时声明lib与cdylib两种 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, }TaskModel的due_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) - Optionmvc::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_done、remove_task、push_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内部用RcRefCellVecTaskModel保存数据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: RcCallback(), (), // “跳转到创建页”事件 }对外提供的方法task_model() - ModelRcmvc::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: Rcdyn mvc::traits::TaskRepository, notify: RcModelNotify, } impl Model for TaskModel { type Data mvc::TaskModel; fn row_count(self) - usize { self.repo.task_count() } fn row_data(self, row: usize) - OptionSelf::Data { self.repo.get_task(row) } fn model_tracker(self) - dyn ModelTracker { self.notify.as_ref() } }这是 Slint 自定义模型的“标准三段式”数据来源不落地row_count/row_data直接透传给 repository模型本身不缓存数据副本变更通知每次写操作成功后调用self.notify.row_changed(index)、row_removed(index, 1)、row_added(row_count()-1, 1)通知 Slint 运行时某行变化触发绑定重算与重绘写失败则静默toggle_done、remove_task、push_task都先检查仓库返回值false时直接return不发出无效通知。由于控制器持有了Rcdyn 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单例如TaskListAdapter、CreateTaskAdapter、NavigationAdapter与 Rust 控制器连接起来所有回调都集中在一个文件中便于维护。5.1 TaskListAdapter把 Model 映射成 UI 项task_list_adapter.rs 完成两类工作1数据流Model → View用MapModel把TaskModel映射为 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 结构体之间的互相转换DateModel↔ui::Date、TimeModel↔ui::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一个基于CellOptionBoxdyn 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.slint、icon_button.slint、text_button.slint、selection_list_view.slint、state_layer.slint、focus_touch_area.slint、styling.slint。这些控件与global单例TaskListAdapter、CreateTaskAdapter、NavigationAdapter配合构成了 View 层的全部行为面.slint只负责「展示什么、触发什么回调」而「数据从哪来、操作后做什么」全部交给 Rust 侧的 adapter 与控制器。七、可验证性单元测试与运行方式7.1 控制器层自带单元测试两个控制器都内嵌#[cfg(test)]测试模块可以直接作为 MVC 分层正确性的回归保障TaskListController测试task_list_controller.rs构造两个任务的 Mock 仓库验证row_count、row_data、toggle_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-mvcWebAssembly 在线预览官方提供了基于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 落地套路」适用于任何中小型应用Model用纯 Rust 结构体描述数据如TaskModel不 import 任何slint::类型Repository用 trait 抽象数据源Mock 实现便于测试与演示真实实现后续替换Controller持有 repositoryRc共享对外暴露命令式操作方法与事件订阅接口需要列表时实现 Slint 的Modeltrait 并配合ModelNotify做变更通知View.slint文件只声明界面结构与回调名通过global单例与 UI 类型如SelectionListViewItem暴露数据与事件Adapter一个 adapter 对应一个global集中完成「数据模型 → UI 项」映射与「UI 回调 → 控制器方法」绑定使 View 与 Controller 零耦合Navigation把页面跳转也抽象成NavigationAdapter由控制器事件驱动。这一模式的直接收益是控制器与视图可以独立开发、独立测试数据源可以随时替换。对照仓库中的其他示例如 examples/todo、examples/printerdemo可以看到它们是更直接的「UI 逻辑」风格而 todo-mvc 专门示范了在大一点的应用中如何用 MVC 维持代码秩序。参考文件索引示例说明文档examples/todo-mvc/README.mdUI 设计文件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、repositoriesAdapter 桥接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),仅供参考