Rust 精通教程 3:GUI 开发
GUI(图形用户界面)开发是许多应用程序的重要组成部分。Rust 生态系统提供了多种 GUI 框架,从轻量级的原生绑定到功能丰富的跨平台框架。今天我们将深入探索 Rust 的 GUI 开发生态。
1. GUI 框架概览
Rust 的主要 GUI 框架:
| 框架 | 特点 | 适用场景 |
|---|---|---|
| egui | 即时模式、WebAssembly 支持、简单易用 | 工具、编辑器、Web 应用 |
| iced | 类 Elm 架构、跨平台、响应式 | 跨平台桌面应用 |
| slint | 声明式 UI、高性能、商业支持 | 嵌入式、桌面应用 |
| gtk-rs | GTK4/3 绑定、成熟稳定 | Linux 原生应用 |
| druid | 数据驱动、跨平台 | 数据密集型应用 |
| tauri | Web 技术栈、小巧高效 | 混合桌面应用 |
2. egui:即时模式 GUI
egui 是一个简单、快速的即时模式 GUI 库,特别适合工具和编辑器开发。
2.1 基本设置
# Cargo.toml
[dependencies]
eframe = "0.24" # egui 框架
egui = "0.24"
// src/main.rs
use eframe::egui;
struct MyApp {
name: String,
age: u32,
show_confirmation: bool,
}
impl Default for MyApp {
fn default() -> Self {
Self {
name: "张三".to_string(),
age: 25,
show_confirmation: false,
}
}
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("个人信息");
ui.separator();
// 文本输入
ui.horizontal(|ui| {
ui.label("姓名:");
ui.text_edit_singleline(&mut self.name);
});
// 数字输入
ui.horizontal(|ui| {
ui.label("年龄:");
ui.add(egui::DragValue::new(&mut self.age).speed(1.0));
});
ui.separator();
// 按钮
if ui.button("提交").clicked() {
self.show_confirmation = true;
}
// 确认对话框
if self.show_confirmation {
egui::Window::new("确认")
.collapsible(false)
.resizable(false)
.show(ctx, |ui| {
ui.label(format!("姓名: {}, 年龄: {}", self.name, self.age));
ui.label("确认提交吗?");
ui.horizontal(|ui| {
if ui.button("是").clicked() {
println!("提交数据: {} ({})", self.name, self.age);
self.show_confirmation = false;
}
if ui.button("否").clicked() {
self.show_confirmation = false;
}
});
});
}
});
}
}
fn main() -> Result<(), eframe::Error> {
let options = eframe::NativeOptions {
initial_window_size: Some(egui::vec2(400.0, 300.0)),
..Default::default()
};
eframe::run_native(
"egui 示例",
options,
Box::new(|_cc| Box::new(MyApp::default())),
)
}
2.2 高级组件
use eframe::egui;
use egui::{Color32, RichText};
#[derive(PartialEq)]
enum Tab {
Basic,
Layout,
Widgets,
Canvas,
}
struct AdvancedApp {
tab: Tab,
text: String,
slider_value: f32,
checkbox: bool,
radio_value: String,
combo_box_value: String,
colors: Vec<Color32>,
selected_color: Option<usize>,
}
impl Default for AdvancedApp {
fn default() -> Self {
Self {
tab: Tab::Basic,
text: "编辑我".to_string(),
slider_value: 50.0,
checkbox: true,
radio_value: "选项A".to_string(),
combo_box_value: "苹果".to_string(),
colors: vec![
Color32::RED,
Color32::GREEN,
Color32::BLUE,
Color32::YELLOW,
],
selected_color: None,
}
}
}
impl AdvancedApp {
fn ui_basic(&mut self, ui: &mut egui::Ui) {
ui.heading("基础控件");
// 按钮
if ui.button("点击我").clicked() {
println!("按钮被点击");
}
// 标签
ui.label("这是一个标签");
// 文本编辑
ui.text_edit_singleline(&mut self.text);
// 滑块
ui.add(egui::Slider::new(&mut self.slider_value, 0.0..=100.0).text("数值"));
// 复选框
ui.checkbox(&mut self.checkbox, "启用功能");
// 进度条
ui.add(egui::ProgressBar::new(self.slider_value / 100.0)
.show_percentage()
.animate(true));
}
fn ui_layout(&mut self, ui: &mut egui::Ui) {
ui.heading("布局示例");
// 水平布局
ui.horizontal(|ui| {
ui.label("水平布局:");
ui.button("按钮1");
ui.button("按钮2");
ui.button("按钮3");
});
ui.separator();
// 垂直布局
ui.vertical(|ui| {
ui.label("垂直布局:");
ui.button("顶部");
ui.button("中间");
ui.button("底部");
});
ui.separator();
// 网格布局
egui::Grid::new("grid")
.num_columns(2)
.spacing([40.0, 4.0])
.striped(true)
.show(ui, |ui| {
ui.label("姓名:");
ui.text_edit_singleline(&mut self.text);
ui.end_row();
ui.label("年龄:");
ui.add(egui::DragValue::new(&mut self.slider_value as &mut f32));
ui.end_row();
ui.label("状态:");
ui.checkbox(&mut self.checkbox, "活跃");
ui.end_row();
});
}
fn ui_widgets(&mut self, ui: &mut egui::Ui) {
ui.heading("高级控件");
// 单选按钮
ui.horizontal(|ui| {
ui.selectable_value(&mut self.radio_value, "选项A".to_string(), "选项A");
ui.selectable_value(&mut self.radio_value, "选项B".to_string(), "选项B");
ui.selectable_value(&mut self.radio_value, "选项C".to_string(), "选项C");
});
// 组合框
egui::ComboBox::from_label("选择水果")
.selected_text(&self.combo_box_value)
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.combo_box_value, "苹果".to_string(), "苹果");
ui.selectable_value(&mut self.combo_box_value, "香蕉".to_string(), "香蕉");
ui.selectable_value(&mut self.combo_box_value, "橙子".to_string(), "橙子");
ui.selectable_value(&mut self.combo_box_value, "葡萄".to_string(), "葡萄");
});
// 颜色选择
ui.horizontal(|ui| {
ui.label("选择颜色:");
for (i, &color) in self.colors.iter().enumerate() {
let response = ui.add(egui::Button::new("⬤").fill(color));
if response.clicked() {
self.selected_color = Some(i);
}
}
});
if let Some(idx) = self.selected_color {
ui.label(format!("选择的颜色索引: {}", idx));
}
// 可折叠区域
ui.collapsing("更多选项", |ui| {
ui.label("这里是更多设置...");
ui.add(egui::Slider::new(&mut self.slider_value, 0.0..=100.0));
});
}
fn ui_canvas(&mut self, ui: &mut egui::Ui) {
ui.heading("绘图画布");
// 创建绘图区域
let (response, painter) = ui.allocate_painter(
egui::vec2(300.0, 200.0),
egui::Sense::drag(),
);
let rect = response.rect;
if response.clicked() {
println!("画布被点击");
}
// 绘制矩形
painter.rect_filled(
rect.shrink(10.0),
5.0,
Color32::from_rgb(100, 100, 200),
);
// 绘制圆形
painter.circle_filled(
rect.center(),
30.0,
Color32::from_rgb(200, 100, 100),
);
// 绘制线条
painter.line_segment(
[rect.left_top(), rect.right_bottom()],
egui::Stroke::new(2.0, Color32::WHITE),
);
painter.line_segment(
[rect.right_top(), rect.left_bottom()],
egui::Stroke::new(2.0, Color32::WHITE),
);
// 绘制文本
painter.text(
rect.center() + egui::vec2(0.0, 50.0),
egui::Align2::CENTER_CENTER,
"拖拽我",
egui::FontId::proportional(16.0),
Color32::WHITE,
);
// 绘制带渐变的矩形
if response.hovered() {
painter.rect_filled(
rect,
0.0,
Color32::from_rgba_premultiplied(255, 255, 255, 50),
);
}
}
}
impl eframe::App for AdvancedApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// 顶部标签页
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.selectable_value(&mut self.tab, Tab::Basic, "基础");
ui.selectable_value(&mut self.tab, Tab::Layout, "布局");
ui.selectable_value(&mut self.tab, Tab::Widgets, "控件");
ui.selectable_value(&mut self.tab, Tab::Canvas, "画布");
});
});
// 中央面板
egui::CentralPanel::default().show(ctx, |ui| {
match self.tab {
Tab::Basic => self.ui_basic(ui),
Tab::Layout => self.ui_layout(ui),
Tab::Widgets => self.ui_widgets(ui),
Tab::Canvas => self.ui_canvas(ui),
}
});
}
}
fn main() -> Result<(), eframe::Error> {
let options = eframe::NativeOptions {
initial_window_size: Some(egui::vec2(500.0, 400.0)),
..Default::default()
};
eframe::run_native(
"egui 高级示例",
options,
Box::new(|_cc| Box::new(AdvancedApp::default())),
)
}
2.3 自定义样式
use eframe::egui;
use egui::{Style, Visuals, FontFamily, FontId};
struct StyledApp {
dark_mode: bool,
}
impl Default for StyledApp {
fn default() -> Self {
Self {
dark_mode: true,
}
}
}
fn configure_style(ctx: &egui::Context, dark_mode: bool) {
let mut style: Style = (*ctx.style()).clone();
if dark_mode {
style.visuals = Visuals::dark();
} else {
style.visuals = Visuals::light();
}
// 自定义字体
style.text_styles = [
(
egui::TextStyle::Heading,
FontId::new(25.0, FontFamily::Proportional),
),
(
egui::TextStyle::Body,
FontId::new(16.0, FontFamily::Proportional),
),
(
egui::TextStyle::Monospace,
FontId::new(14.0, FontFamily::Monospace),
),
(
egui::TextStyle::Button,
FontId::new(14.0, FontFamily::Proportional),
),
(
egui::TextStyle::Small,
FontId::new(12.0, FontFamily::Proportional),
),
]
.into();
// 自定义间距
style.spacing.item_spacing = egui::vec2(8.0, 4.0);
style.spacing.window_margin = egui::Margin::same(10.0);
ctx.set_style(style);
}
impl eframe::App for StyledApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// 应用样式
configure_style(ctx, self.dark_mode);
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("样式示例");
ui.horizontal(|ui| {
ui.label("主题:");
if ui.button("浅色").clicked() {
self.dark_mode = false;
}
if ui.button("深色").clicked() {
self.dark_mode = true;
}
});
ui.separator();
ui.label("这是一个普通文本");
ui.label(RichText::new("红色粗体文本")
.color(egui::Color32::RED)
.strong());
ui.horizontal(|ui| {
ui.style_mut().visuals.button_frame = true;
if ui.button("普通按钮").clicked() {}
let mut button_style = (*ui.style()).clone();
button_style.visuals.button_frame = false;
ui.set_style(button_style);
if ui.button("无边框按钮").clicked() {}
});
ui.add(egui::Slider::new(&mut self.dark_mode, false..=true).text("滑块"));
egui::Frame::group(ui.style())
.fill(ui.style().visuals.window_fill())
.stroke(ui.style().visuals.window_stroke())
.corner_radius(5.0)
.show(ui, |ui| {
ui.label("这是一个带边框的分组");
ui.label("可以包含多个控件");
});
});
}
}
fn main() -> Result<(), eframe::Error> {
eframe::run_native(
"样式示例",
eframe::NativeOptions::default(),
Box::new(|_cc| Box::new(StyledApp::default())),
)
}
3. iced:Elm 架构 GUI
iced 是一个跨平台 GUI 库,采用 Elm 架构,强调可测试性和模块化。
3.1 基本设置
# Cargo.toml
[dependencies]
iced = { version = "0.10", features = ["tokio"] }
// src/main.rs
use iced::widget::{button, column, text, text_input, Container};
use iced::{Alignment, Element, Length, Sandbox, Settings};
struct Calculator {
input: String,
result: String,
}
#[derive(Debug, Clone)]
enum Message {
InputChanged(String),
Calculate,
Clear,
}
impl Sandbox for Calculator {
type Message = Message;
fn new() -> Self {
Self {
input: String::new(),
result: String::new(),
}
}
fn title(&self) -> String {
String::from("计算器")
}
fn update(&mut self, message: Message) {
match message {
Message::InputChanged(value) => {
self.input = value;
}
Message::Calculate => {
match self.input.parse::<f64>() {
Ok(num) => {
self.result = format!("平方根: {}", num.sqrt());
}
Err(_) => {
self.result = "请输入有效的数字".to_string();
}
}
}
Message::Clear => {
self.input.clear();
self.result.clear();
}
}
}
fn view(&self) -> Element<Message> {
let input = text_input("输入数字...", &self.input)
.on_input(Message::InputChanged)
.padding(10)
.width(Length::Fixed(200.0));
let calculate_button = button("计算")
.on_press(Message::Calculate)
.padding(10);
let clear_button = button("清除")
.on_press(Message::Clear)
.padding(10);
let result_text = text(&self.result).size(20);
let content = column![
input,
calculate_button,
clear_button,
result_text,
]
.spacing(20)
.align_items(Alignment::Center)
.width(Length::Fill)
.height(Length::Fill);
Container::new(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
}
}
fn main() -> iced::Result {
Calculator::run(Settings::default())
}
3.2 复杂应用:待办事项
use iced::widget::{
button, checkbox, column, container, row, scrollable, text, text_input,
};
use iced::{Element, Length, Sandbox, Settings, Color};
use std::collections::HashMap;
#[derive(Debug, Clone)]
struct TodoItem {
id: usize,
description: String,
completed: bool,
}
#[derive(Debug, Clone)]
enum Message {
AddTodo,
InputChanged(String),
ToggleTodo(usize),
DeleteTodo(usize),
ClearCompleted,
FilterChanged(Filter),
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Filter {
All,
Active,
Completed,
}
struct TodoApp {
todos: HashMap<usize, TodoItem>,
next_id: usize,
input_value: String,
filter: Filter,
}
impl Sandbox for TodoApp {
type Message = Message;
fn new() -> Self {
let mut todos = HashMap::new();
todos.insert(
1,
TodoItem {
id: 1,
description: "学习 Rust".to_string(),
completed: false,
},
);
todos.insert(
2,
TodoItem {
id: 2,
description: "写 GUI 应用".to_string(),
completed: true,
},
);
Self {
todos,
next_id: 3,
input_value: String::new(),
filter: Filter::All,
}
}
fn title(&self) -> String {
String::from("待办事项")
}
fn update(&mut self, message: Message) {
match message {
Message::AddTodo => {
if !self.input_value.is_empty() {
let id = self.next_id;
self.todos.insert(
id,
TodoItem {
id,
description: self.input_value.clone(),
completed: false,
},
);
self.next_id += 1;
self.input_value.clear();
}
}
Message::InputChanged(value) => {
self.input_value = value;
}
Message::ToggleTodo(id) => {
if let Some(todo) = self.todos.get_mut(&id) {
todo.completed = !todo.completed;
}
}
Message::DeleteTodo(id) => {
self.todos.remove(&id);
}
Message::ClearCompleted => {
self.todos.retain(|_, todo| !todo.completed);
}
Message::FilterChanged(filter) => {
self.filter = filter;
}
}
}
fn view(&self) -> Element<Message> {
// 标题
let title = text("待办事项").size(40);
// 输入区域
let input = text_input("添加新任务...", &self.input_value)
.on_input(Message::InputChanged)
.on_submit(Message::AddTodo)
.padding(10);
let add_button = button("添加")
.on_press(Message::AddTodo)
.padding(10);
let input_row = row![input, add_button].spacing(10);
// 过滤按钮
let filter_row = row![
button(if self.filter == Filter::All { "✓ 全部" } else { "全部" })
.on_press(Message::FilterChanged(Filter::All)),
button(if self.filter == Filter::Active { "✓ 进行中" } else { "进行中" })
.on_press(Message::FilterChanged(Filter::Active)),
button(if self.filter == Filter::Completed { "✓ 已完成" } else { "已完成" })
.on_press(Message::FilterChanged(Filter::Completed)),
]
.spacing(10);
// 待办列表
let mut todos: Vec<TodoItem> = self.todos.values().cloned().collect();
todos.sort_by_key(|t| t.id);
let filtered_todos: Vec<&TodoItem> = todos
.iter()
.filter(|todo| match self.filter {
Filter::All => true,
Filter::Active => !todo.completed,
Filter::Completed => todo.completed,
})
.collect();
let todo_list = if filtered_todos.is_empty() {
column![text("没有待办事项").color(Color::from_rgb(0.5, 0.5, 0.5))]
} else {
let mut list = column![].spacing(5);
for todo in filtered_todos {
let checkbox = checkbox(
todo.description.clone(),
todo.completed,
move |_| Message::ToggleTodo(todo.id),
);
let delete_button = button("删除")
.on_press(Message::DeleteTodo(todo.id))
.padding(5);
let item = row![checkbox, delete_button]
.spacing(10)
.align_items(iced::Alignment::Center);
list = list.push(item);
}
list
};
// 底部统计
let active_count = todos.iter().filter(|t| !t.completed).count();
let completed_count = todos.iter().filter(|t| t.completed).count();
let stats = row![
text(format!("进行中: {}", active_count)),
text(format!("已完成: {}", completed_count)),
if completed_count > 0 {
button("清除已完成")
.on_press(Message::ClearCompleted)
.into()
} else {
button("清除已完成").into()
},
]
.spacing(20)
.align_items(iced::Alignment::Center);
// 主布局
let content = column![
title,
input_row,
filter_row,
scrollable(todo_list).height(Length::Fill),
stats,
]
.spacing(20)
.padding(20)
.width(Length::Fill)
.height(Length::Fill)
.align_items(iced::Alignment::Center);
container(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.into()
}
}
fn main() -> iced::Result {
TodoApp::run(Settings {
window: iced::window::Settings {
size: iced::Size::new(500.0, 600.0),
..Default::default()
},
..Settings::default()
})
}
4. slint:声明式 UI
slint 使用声明式语言描述 UI,性能优异,适合嵌入式系统。
4.1 基本设置
# Cargo.toml
[dependencies]
slint = "1.2"
// src/main.rs
slint::slint! {
export component MainWindow inherits Window {
width: 400px;
height: 300px;
title: "Slint 示例";
in-out property <string> name: "世界";
in-out property <int> counter: 0;
VerticalLayout {
padding: 20px;
spacing: 10px;
Text {
text: "你好,\{name}!";
font-size: 24px;
horizontal-alignment: center;
}
HorizontalLayout {
spacing: 10px;
TextEdit {
text: name;
width: 200px;
}
Button {
text: "更新";
clicked => {
name = "Rust";
}
}
}
Text {
text: "计数: \{counter}";
font-size: 18px;
horizontal-alignment: center;
}
HorizontalLayout {
spacing: 10px;
Button {
text: "增加";
clicked => {
counter += 1;
}
}
Button {
text: "减少";
clicked => {
counter -= 1;
}
}
Button {
text: "重置";
clicked => {
counter = 0;
}
}
}
}
}
}
fn main() -> Result<(), slint::PlatformError> {
let ui = MainWindow::new()?;
// 连接 Rust 回调
let ui_handle = ui.as_weak();
ui.on_request_increment(move || {
let ui = ui_handle.unwrap();
ui.set_counter(ui.get_counter() + 1);
});
ui.run()
}
4.2 复杂应用:音乐播放器
// src/main.rs
slint::slint! {
import { Button, Slider, ListView, StandardListView } from "std-widgets.slint";
export component MusicPlayer inherits Window {
width: 500px;
height: 600px;
title: "音乐播放器";
in-out property <[string]> playlist: [
"歌曲1 - 艺术家A",
"歌曲2 - 艺术家B",
"歌曲3 - 艺术家C",
"歌曲4 - 艺术家D",
"歌曲5 - 艺术家E"
];
in-out property <int> current-track: 0;
in-out property <bool> is-playing: false;
in-out property <float> volume: 0.7;
in-out property <float> progress: 0.3;
callback play-pressed();
callback pause-pressed();
callback next-pressed();
callback previous-pressed();
callback volume-changed(float);
callback track-selected(int);
VerticalLayout {
padding: 20px;
spacing: 15px;
// 标题
Text {
text: "音乐播放器";
font-size: 24px;
font-weight: 700;
horizontal-alignment: center;
}
// 当前播放信息
Rectangle {
background: #f0f0f0;
border-radius: 8px;
height: 100px;
VerticalLayout {
padding: 10px;
spacing: 5px;
Text {
text: "正在播放:";
font-size: 14px;
color: #666;
}
Text {
text: playlist[current-track];
font-size: 18px;
font-weight: 600;
overflow: elide;
}
}
}
// 进度条
VerticalLayout {
spacing: 5px;
HorizontalLayout {
Text { text: "0:00"; }
Slider {
value: progress;
maximum: 1.0;
width: 300px;
}
Text { text: "3:30"; }
}
}
// 控制按钮
HorizontalLayout {
alignment: center;
spacing: 20px;
Button {
text: "⏮";
min-width: 50px;
min-height: 40px;
clicked => {
previous-pressed();
}
}
Button {
text: is-playing ? "⏸" : "▶";
min-width: 60px;
min-height: 50px;
font-size: 20px;
clicked => {
if (is-playing) {
pause-pressed();
} else {
play-pressed();
}
}
}
Button {
text: "⏭";
min-width: 50px;
min-height: 40px;
clicked => {
next-pressed();
}
}
}
// 音量控制
HorizontalLayout {
alignment: center;
spacing: 10px;
Text { text: "音量:"; }
Slider {
value: volume;
maximum: 1.0;
width: 200px;
changed(value) => {
volume-changed(value);
}
}
Text { text: "\{round(volume * 100)}%"; }
}
// 播放列表
Text {
text: "播放列表";
font-size: 16px;
font-weight: 600;
}
ListView {
height: 200px;
for track[idx] in playlist: Rectangle {
height: 30px;
background: idx == current-track ? #e0e0ff : transparent;
HorizontalLayout {
padding: 5px;
Rectangle {
width: 20px;
height: 20px;
background: idx == current-track ? #00ff00 : #ccc;
border-radius: 10px;
}
Text {
text: track;
horizontal-stretch: 1;
}
Button {
text: "播放";
max-width: 60px;
clicked => {
track-selected(idx);
}
}
}
TouchArea {
clicked => {
track-selected(idx);
}
}
}
}
}
}
}
fn main() -> Result<(), slint::PlatformError> {
let ui = MusicPlayer::new()?;
// 设置回调
let ui_handle = ui.as_weak();
ui.on_play_pressed(move || {
let ui = ui_handle.unwrap();
ui.set_is_playing(true);
println!("播放: {}", ui.get_playlist()[ui.get_current_track() as usize]);
});
let ui_handle = ui.as_weak();
ui.on_pause_pressed(move || {
let ui = ui_handle.unwrap();
ui.set_is_playing(false);
println!("暂停");
});
let ui_handle = ui.as_weak();
ui.on_next_pressed(move || {
let ui = ui_handle.unwrap();
let next = (ui.get_current_track() + 1) % ui.get_playlist().len() as i32;
ui.set_current_track(next);
println!("下一曲: {}", ui.get_playlist()[next as usize]);
});
let ui_handle = ui.as_weak();
ui.on_previous_pressed(move || {
let ui = ui_handle.unwrap();
let prev = (ui.get_current_track() - 1 + ui.get_playlist().len() as i32)
% ui.get_playlist().len() as i32;
ui.set_current_track(prev);
println!("上一曲: {}", ui.get_playlist()[prev as usize]);
});
let ui_handle = ui.as_weak();
ui.on_volume_changed(move |volume| {
println!("音量: {}%", (volume * 100.0) as i32);
});
let ui_handle = ui.as_weak();
ui.on_track_selected(move |index| {
let ui = ui_handle.unwrap();
ui.set_current_track(index);
println!("选择歌曲: {}", ui.get_playlist()[index as usize]);
});
ui.run()
}
5. gtk-rs:原生 GTK 应用
gtk-rs 提供 GTK4 的 Rust 绑定,适合开发 Linux 原生应用。
5.1 基本设置
# Cargo.toml
[dependencies]
gtk = "0.7"
glib = "0.18"
// src/main.rs
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Button, Box, Label, Entry, Orientation};
fn main() {
let app = Application::builder()
.application_id("com.example.gtk-app")
.build();
app.connect_activate(build_ui);
app.run();
}
fn build_ui(app: &Application) {
// 创建窗口
let window = ApplicationWindow::builder()
.application(app)
.title("GTK 示例")
.default_width(400)
.default_height(300)
.build();
// 创建容器
let container = Box::new(Orientation::Vertical, 10);
container.set_margin(20);
// 标题
let title = Label::new(Some("欢迎使用 GTK-rs"));
title.add_css_class("title-1");
// 输入框
let entry = Entry::new();
entry.set_placeholder_text(Some("输入你的名字"));
// 标签
let label = Label::new(None);
// 按钮
let button = Button::with_label("点击我");
// 连接信号
let label_clone = label.clone();
let entry_clone = entry.clone();
button.connect_clicked(move |_| {
let name = entry_clone.text();
if name.is_empty() {
label_clone.set_text("请输入名字!");
} else {
label_clone.set_text(&format!("你好,{}!", name));
}
});
// 添加控件到容器
container.append(&title);
container.append(&entry);
container.append(&button);
container.append(&label);
window.set_child(Some(&container));
window.present();
}
5.2 复杂应用:文件浏览器
use gtk::prelude::*;
use gtk::{
Application, ApplicationWindow, Box, Button, Entry, HeaderBar,
ListStore, ScrolledWindow, TreeView, TreeViewColumn,
CellRendererText, Orientation, SelectionMode, TreeStore,
TreeIter, TreePath, TreeViewColumnSizing, ResponseType,
FileChooserDialog, FileChooserAction, MessageDialog,
MessageType, ButtonsType,
};
use std::fs;
use std::path::{Path, PathBuf};
struct FileBrowser {
window: ApplicationWindow,
path_entry: Entry,
tree_view: TreeView,
list_store: ListStore,
current_path: PathBuf,
}
impl FileBrowser {
fn new(app: &Application) -> Self {
// 创建窗口
let window = ApplicationWindow::builder()
.application(app)
.title("文件浏览器")
.default_width(800)
.default_height(600)
.build();
// 创建头栏
let header = HeaderBar::new();
header.set_title_widget(Some(>k::Label::new(Some("文件浏览器"))));
header.set_show_title_buttons(true);
window.set_titlebar(Some(&header));
// 创建主容器
let main_box = Box::new(Orientation::Vertical, 5);
main_box.set_margin(5);
// 工具栏
let toolbar = Box::new(Orientation::Horizontal, 5);
// 后退按钮
let back_button = Button::with_label("←");
toolbar.append(&back_button);
// 前进按钮
let forward_button = Button::with_label("→");
toolbar.append(&forward_button);
// 上级目录按钮
let up_button = Button::with_label("↑");
toolbar.append(&up_button);
// 刷新按钮
let refresh_button = Button::with_label("↻");
toolbar.append(&refresh_button);
// 路径输入框
let path_entry = Entry::new();
path_entry.set_hexpand(true);
toolbar.append(&path_entry);
// 转到按钮
let go_button = Button::with_label("转到");
toolbar.append(&go_button);
main_box.append(&toolbar);
// 创建列表存储
let list_store = ListStore::new(&[String::static_type(), String::static_type()]);
// 创建树视图
let tree_view = TreeView::with_model(&list_store);
tree_view.set_vexpand(true);
// 添加列
let icon_renderer = CellRendererText::new();
let icon_column = TreeViewColumn::new();
icon_column.pack_start(&icon_renderer, true);
icon_column.add_attribute(&icon_renderer, "text", 0);
icon_column.set_title("类型");
tree_view.append_column(&icon_column);
let name_renderer = CellRendererText::new();
let name_column = TreeViewColumn::new();
name_column.pack_start(&name_renderer, true);
name_column.add_attribute(&name_renderer, "text", 1);
name_column.set_title("名称");
name_column.set_expand(true);
tree_view.append_column(&name_column);
// 滚动窗口
let scrolled = ScrolledWindow::builder()
.child(&tree_view)
.build();
main_box.append(&scrolled);
window.set_child(Some(&main_box));
let mut browser = FileBrowser {
window,
path_entry,
tree_view,
list_store,
current_path: std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
};
// 连接信号
browser.connect_signals(
back_button, forward_button, up_button, refresh_button, go_button
);
browser.refresh_list();
browser
}
fn connect_signals(&mut self, back: Button, forward: Button, up: Button, refresh: Button, go: Button) {
// 上级目录
let browser_clone = self as *mut FileBrowser;
up.connect_clicked(move |_| {
let browser = unsafe { &mut *browser_clone };
if let Some(parent) = browser.current_path.parent() {
browser.current_path = parent.to_path_buf();
browser.refresh_list();
}
});
// 刷新
let browser_clone = self as *mut FileBrowser;
refresh.connect_clicked(move |_| {
let browser = unsafe { &mut *browser_clone };
browser.refresh_list();
});
// 转到
let browser_clone = self as *mut FileBrowser;
go.connect_clicked(move |_| {
let browser = unsafe { &mut *browser_clone };
let path = PathBuf::from(browser.path_entry.text().to_string());
if path.exists() {
browser.current_path = path;
browser.refresh_list();
} else {
browser.show_error("路径不存在");
}
});
// 双击打开
let browser_clone = self as *mut FileBrowser;
self.tree_view.connect_row_activated(move |view, path, _| {
let browser = unsafe { &mut *browser_clone };
if let Some(iter) = view.model().unwrap().iter(path) {
let name: String = view.model().unwrap()
.value(&iter, 1)
.get()
.unwrap();
let new_path = browser.current_path.join(name);
if new_path.is_dir() {
browser.current_path = new_path;
browser.refresh_list();
} else if new_path.is_file() {
browser.open_file(&new_path);
}
}
});
}
fn refresh_list(&mut self) {
self.list_store.clear();
self.path_entry.set_text(self.current_path.to_string_lossy().as_ref());
if let Ok(entries) = fs::read_dir(&self.current_path) {
let mut files = Vec::new();
let mut dirs = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name().to_string_lossy().to_string();
if path.is_dir() {
dirs.push(("📁".to_string(), name));
} else if path.is_file() {
// 根据扩展名选择图标
let icon = if path.extension().map_or(false, |ext| {
let ext = ext.to_string_lossy().to_lowercase();
matches!(ext.as_str(), "rs" | "toml" | "txt" | "md")
}) {
"📄"
} else {
"📎"
};
files.push((icon.to_string(), name));
}
}
// 目录排在前面
dirs.sort_by(|a, b| a.1.cmp(&b.1));
files.sort_by(|a, b| a.1.cmp(&b.1));
for (icon, name) in dirs {
self.list_store.insert_with_values(None, &[0, 1], &[&icon, &name]);
}
for (icon, name) in files {
self.list_store.insert_with_values(None, &[0, 1], &[&icon, &name]);
}
} else {
self.show_error("无法读取目录");
}
}
fn show_error(&self, message: &str) {
let dialog = MessageDialog::builder()
.transient_for(&self.window)
.message_type(MessageType::Error)
.buttons(ButtonsType::Ok)
.text(message)
.build();
dialog.connect_response(|dialog, _| {
dialog.destroy();
});
dialog.show();
}
fn open_file(&self, path: &Path) {
// 这里可以调用系统默认程序打开文件
println!("打开文件: {}", path.display());
let dialog = MessageDialog::builder()
.transient_for(&self.window)
.message_type(MessageType::Info)
.buttons(ButtonsType::Ok)
.text(&format!("打开文件: {}", path.display()))
.build();
dialog.connect_response(|dialog, _| {
dialog.destroy();
});
dialog.show();
}
fn show(&self) {
self.window.show();
}
}
fn main() {
let app = Application::builder()
.application_id("com.example.file-browser")
.build();
app.connect_activate(|app| {
let browser = FileBrowser::new(app);
browser.show();
});
app.run();
}
6. tauri:混合桌面应用
tauri 使用 Web 技术构建前端,Rust 作为后端,创建小巧高效的桌面应用。
6.1 项目初始化
# 安装 tauri CLI
cargo install tauri-cli
# 创建新项目
cargo tauri init
6.2 项目结构
my-tauri-app/
├── src-tauri/
│ ├── src/
│ │ └── main.rs
│ ├── Cargo.toml
│ └── tauri.conf.json
├── src/
│ ├── main.js
│ └── index.html
└── package.json
6.3 后端代码
// src-tauri/src/main.rs
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use tauri::{Manager, Window};
#[derive(Debug, Serialize, Deserialize)]
struct FileInfo {
name: String,
path: String,
is_dir: bool,
size: u64,
modified: u64,
}
#[derive(Debug, Serialize, Deserialize)]
struct Note {
id: String,
title: String,
content: String,
created: u64,
modified: u64,
}
// 读取目录
#[tauri::command]
fn read_directory(path: String) -> Result<Vec<FileInfo>, String> {
let path = Path::new(&path);
if !path.exists() {
return Err("路径不存在".to_string());
}
let mut files = Vec::new();
for entry in fs::read_dir(path).map_err(|e| e.to_string())? {
let entry = entry.map_err(|e| e.to_string())?;
let metadata = entry.metadata().map_err(|e| e.to_string())?;
files.push(FileInfo {
name: entry.file_name().to_string_lossy().to_string(),
path: entry.path().to_string_lossy().to_string(),
is_dir: metadata.is_dir(),
size: if metadata.is_file() { metadata.len() } else { 0 },
modified: metadata.modified()
.map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs())
.unwrap_or(0),
});
}
// 目录排在前面
files.sort_by(|a, b| {
if a.is_dir && !b.is_dir {
std::cmp::Ordering::Less
} else if !a.is_dir && b.is_dir {
std::cmp::Ordering::Greater
} else {
a.name.cmp(&b.name)
}
});
Ok(files)
}
// 读取文件
#[tauri::command]
fn read_file(path: String) -> Result<String, String> {
fs::read_to_string(&path).map_err(|e| e.to_string())
}
// 写入文件
#[tauri::command]
fn write_file(path: String, content: String) -> Result<(), String> {
fs::write(&path, content).map_err(|e| e.to_string())
}
// 删除文件
#[tauri::command]
fn delete_file(path: String) -> Result<(), String> {
let path = Path::new(&path);
if path.is_dir() {
fs::remove_dir_all(path).map_err(|e| e.to_string())
} else {
fs::remove_file(path).map_err(|e| e.to_string())
}
}
// 记事本应用
struct NotesState {
notes: Vec<Note>,
}
#[tauri::command]
fn get_notes(state: tauri::State<NotesState>) -> Result<Vec<Note>, String> {
Ok(state.notes.clone())
}
#[tauri::command]
fn create_note(state: tauri::State<NotesState>, title: String, content: String) -> Result<Note, String> {
let mut notes = state.notes.lock().unwrap();
let note = Note {
id: uuid::Uuid::new_v4().to_string(),
title,
content,
created: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
modified: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
};
notes.push(note.clone());
Ok(note)
}
#[tauri::command]
fn update_note(state: tauri::State<NotesState>, id: String, title: String, content: String) -> Result<(), String> {
let mut notes = state.notes.lock().unwrap();
if let Some(note) = notes.iter_mut().find(|n| n.id == id) {
note.title = title;
note.content = content;
note.modified = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
Ok(())
} else {
Err("笔记不存在".to_string())
}
}
#[tauri::command]
fn delete_note(state: tauri::State<NotesState>, id: String) -> Result<(), String> {
let mut notes = state.notes.lock().unwrap();
notes.retain(|n| n.id != id);
Ok(())
}
// 系统信息
#[tauri::command]
fn get_system_info() -> Result<serde_json::Value, String> {
let info = serde_json::json!({
"os": std::env::consts::OS,
"arch": std::env::consts::ARCH,
"version": std::env::consts::FAMILY,
"cpus": num_cpus::get(),
"current_dir": std::env::current_dir().unwrap_or_default().to_string_lossy(),
});
Ok(info)
}
fn main() {
tauri::Builder::default()
.manage(NotesState {
notes: std::sync::Mutex::new(Vec::new()),
})
.invoke_handler(tauri::generate_handler![
read_directory,
read_file,
write_file,
delete_file,
get_notes,
create_note,
update_note,
delete_note,
get_system_info,
])
.run(tauri::generate_context!())
.expect("运行 tauri 应用失败");
}
6.4 前端代码
<!-- src/index.html -->
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tauri 应用</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
margin: 0;
padding: 20px;
background: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 20px;
}
h1 {
text-align: center;
color: #333;
}
.toolbar {
display: flex;
gap: 10px;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
button {
padding: 8px 16px;
background: #007acc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #005a9e;
}
button.danger {
background: #dc3545;
}
button.danger:hover {
background: #c82333;
}
input[type="text"] {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
textarea {
width: 100%;
height: 200px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
margin-bottom: 10px;
}
.file-list {
border: 1px solid #eee;
border-radius: 4px;
max-height: 400px;
overflow-y: auto;
}
.file-item {
display: flex;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #eee;
cursor: pointer;
}
.file-item:hover {
background: #f5f5f5;
}
.file-item .icon {
width: 24px;
margin-right: 8px;
font-size: 18px;
}
.file-item .name {
flex: 1;
}
.file-item .size {
color: #666;
font-size: 12px;
margin-right: 10px;
}
.file-item .modified {
color: #666;
font-size: 12px;
}
.notes-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 15px;
margin-top: 20px;
}
.note-card {
background: #f9f9f9;
border-radius: 4px;
padding: 15px;
cursor: pointer;
border: 1px solid #eee;
}
.note-card:hover {
background: #f0f0f0;
}
.note-card h3 {
margin: 0 0 10px 0;
color: #333;
}
.note-card p {
margin: 0;
color: #666;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.note-card .date {
margin-top: 10px;
font-size: 12px;
color: #999;
}
.editor {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 20px rgba(0,0,0,0.2);
width: 500px;
max-width: 90%;
z-index: 1000;
}
.editor-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 999;
}
.tab-bar {
display: flex;
gap: 2px;
margin-bottom: 20px;
background: #f0f0f0;
padding: 5px;
border-radius: 4px;
}
.tab {
flex: 1;
padding: 8px;
text-align: center;
cursor: pointer;
border-radius: 4px;
}
.tab.active {
background: white;
font-weight: bold;
}
.status-bar {
margin-top: 20px;
padding-top: 10px;
border-top: 1px solid #eee;
color: #666;
font-size: 12px;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<h1>📁 Tauri 应用</h1>
<div class="tab-bar">
<div class="tab active" onclick="switchTab('files')">文件管理器</div>
<div class="tab" onclick="switchTab('notes')">记事本</div>
<div class="tab" onclick="switchTab('system')">系统信息</div>
</div>
<!-- 文件管理器标签页 -->
<div id="files-tab" class="tab-content">
<div class="toolbar">
<input type="text" id="path-input" value="/" placeholder="输入路径">
<button onclick="navigateTo()">转到</button>
<button onclick="refreshFiles()">刷新</button>
<button class="danger" onclick="deleteSelected()">删除</button>
</div>
<div id="file-list" class="file-list">
<!-- 文件列表动态生成 -->
</div>
</div>
<!-- 记事本标签页 -->
<div id="notes-tab" class="tab-content" style="display: none;">
<div class="toolbar">
<button onclick="showNoteEditor()">新建笔记</button>
</div>
<div id="notes-list" class="notes-list">
<!-- 笔记列表动态生成 -->
</div>
</div>
<!-- 系统信息标签页 -->
<div id="system-tab" class="tab-content" style="display: none;">
<div id="system-info" style="padding: 20px;">
<!-- 系统信息动态生成 -->
</div>
</div>
<div id="status-bar" class="status-bar">
就绪
</div>
</div>
<!-- 笔记编辑器对话框 -->
<div id="editor-overlay" class="editor-overlay" style="display: none;" onclick="hideEditor()"></div>
<div id="editor" class="editor" style="display: none;">
<h3 id="editor-title">新建笔记</h3>
<input type="text" id="note-title" placeholder="标题" style="width: 100%; margin-bottom: 10px; padding: 8px;">
<textarea id="note-content" placeholder="内容"></textarea>
<div style="display: flex; gap: 10px; justify-content: flex-end;">
<button onclick="hideEditor()">取消</button>
<button onclick="saveNote()">保存</button>
</div>
</div>
<script>
const { invoke } = window.__TAURI__.tauri;
let currentPath = '/';
let currentNoteId = null;
let notes = [];
// 切换标签页
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.style.display = 'none');
if (tab === 'files') {
document.querySelectorAll('.tab')[0].classList.add('active');
document.getElementById('files-tab').style.display = 'block';
refreshFiles();
} else if (tab === 'notes') {
document.querySelectorAll('.tab')[1].classList.add('active');
document.getElementById('notes-tab').style.display = 'block';
refreshNotes();
} else if (tab === 'system') {
document.querySelectorAll('.tab')[2].classList.add('active');
document.getElementById('system-tab').style.display = 'block';
loadSystemInfo();
}
}
// 文件管理器
async function refreshFiles() {
try {
const files = await invoke('read_directory', { path: currentPath });
renderFileList(files);
document.getElementById('path-input').value = currentPath;
setStatus(`读取 ${files.length} 个项目`);
} catch (error) {
setStatus(`错误: ${error}`, true);
}
}
function renderFileList(files) {
const list = document.getElementById('file-list');
list.innerHTML = '';
files.forEach(file => {
const item = document.createElement('div');
item.className = 'file-item';
item.ondblclick = () => {
if (file.is_dir) {
currentPath = file.path;
refreshFiles();
} else {
openFile(file);
}
};
const icon = document.createElement('span');
icon.className = 'icon';
icon.textContent = file.is_dir ? '📁' : '📄';
const name = document.createElement('span');
name.className = 'name';
name.textContent = file.name;
const size = document.createElement('span');
size.className = 'size';
size.textContent = file.is_dir ? '-' : formatSize(file.size);
const modified = document.createElement('span');
modified.className = 'modified';
modified.textContent = formatDate(file.modified);
item.appendChild(icon);
item.appendChild(name);
item.appendChild(size);
item.appendChild(modified);
list.appendChild(item);
});
}
function navigateTo() {
const path = document.getElementById('path-input').value;
currentPath = path;
refreshFiles();
}
async function deleteSelected() {
if (confirm('确定删除选中的项目?')) {
// 简化版:没有多选,这里需要实现多选逻辑
setStatus('删除功能需要实现多选');
}
}
// 记事本
async function refreshNotes() {
try {
notes = await invoke('get_notes');
renderNotes();
setStatus(`加载 ${notes.length} 条笔记`);
} catch (error) {
setStatus(`错误: ${error}`, true);
}
}
function renderNotes() {
const list = document.getElementById('notes-list');
list.innerHTML = '';
notes.forEach(note => {
const card = document.createElement('div');
card.className = 'note-card';
card.onclick = () => editNote(note.id);
const title = document.createElement('h3');
title.textContent = note.title || '无标题';
const content = document.createElement('p');
content.textContent = note.content.substring(0, 50) + (note.content.length > 50 ? '...' : '');
const date = document.createElement('div');
date.className = 'date';
date.textContent = `修改: ${formatDate(note.modified)}`;
card.appendChild(title);
card.appendChild(content);
card.appendChild(date);
list.appendChild(card);
});
}
function showNoteEditor() {
currentNoteId = null;
document.getElementById('editor-title').textContent = '新建笔记';
document.getElementById('note-title').value = '';
document.getElementById('note-content').value = '';
document.getElementById('editor-overlay').style.display = 'block';
document.getElementById('editor').style.display = 'block';
}
function editNote(id) {
const note = notes.find(n => n.id === id);
if (note) {
currentNoteId = id;
document.getElementById('editor-title').textContent = '编辑笔记';
document.getElementById('note-title').value = note.title;
document.getElementById('note-content').value = note.content;
document.getElementById('editor-overlay').style.display = 'block';
document.getElementById('editor').style.display = 'block';
}
}
function hideEditor() {
document.getElementById('editor-overlay').style.display = 'none';
document.getElementById('editor').style.display = 'none';
}
async function saveNote() {
const title = document.getElementById('note-title').value;
const content = document.getElementById('note-content').value;
try {
if (currentNoteId) {
await invoke('update_note', { id: currentNoteId, title, content });
setStatus('笔记更新成功');
} else {
await invoke('create_note', { title, content });
setStatus('笔记创建成功');
}
hideEditor();
refreshNotes();
} catch (error) {
setStatus(`错误: ${error}`, true);
}
}
// 系统信息
async function loadSystemInfo() {
try {
const info = await invoke('get_system_info');
const div = document.getElementById('system-info');
div.innerHTML = `
<p><strong>操作系统:</strong> ${info.os}</p>
<p><strong>架构:</strong> ${info.arch}</p>
<p><strong>版本:</strong> ${info.version}</p>
<p><strong>CPU核心数:</strong> ${info.cpus}</p>
<p><strong>当前目录:</strong> ${info.current_dir}</p>
`;
setStatus('系统信息加载成功');
} catch (error) {
setStatus(`错误: ${error}`, true);
}
}
// 辅助函数
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
function formatDate(timestamp) {
const date = new Date(timestamp * 1000);
return date.toLocaleString('zh-CN', { hour12: false });
}
function setStatus(message, isError = false) {
const status = document.getElementById('status-bar');
status.textContent = message;
status.style.color = isError ? 'red' : '#666';
}
function openFile(file) {
setStatus(`打开文件: ${file.name}`);
// 可以在这里添加文件预览功能
}
// 初始化
window.addEventListener('DOMContentLoaded', () => {
refreshFiles();
});
</script>
</body>
</html>
7. GUI 开发最佳实践
7.1 架构模式
// MVC 模式示例
mod model {
#[derive(Clone)]
pub struct AppModel {
pub counter: i32,
pub items: Vec<String>,
}
impl AppModel {
pub fn new() -> Self {
Self {
counter: 0,
items: Vec::new(),
}
}
pub fn increment(&mut self) {
self.counter += 1;
}
pub fn add_item(&mut self, item: String) {
self.items.push(item);
}
}
}
mod view {
use super::model::AppModel;
pub trait View {
fn render(&self, model: &AppModel);
fn update(&mut self);
}
pub struct ConsoleView {
last_render: Option<AppModel>,
}
impl ConsoleView {
pub fn new() -> Self {
Self { last_render: None }
}
}
impl View for ConsoleView {
fn render(&self, model: &AppModel) {
println!("=== 应用状态 ===");
println!("计数器: {}", model.counter);
println!("项目列表:");
for (i, item) in model.items.iter().enumerate() {
println!(" {}. {}", i + 1, item);
}
}
fn update(&mut self) {
// 更新视图
}
}
}
mod controller {
use super::{model::AppModel, view::View};
pub struct AppController {
model: AppModel,
view: Box<dyn View>,
}
impl AppController {
pub fn new(model: AppModel, view: Box<dyn View>) -> Self {
Self { model, view }
}
pub fn handle_input(&mut self, input: &str) {
match input {
"inc" => self.model.increment(),
_ => self.model.add_item(input.to_string()),
}
self.view.render(&self.model);
}
}
}
7.2 响应式设计
use egui::{Context, Vec2};
struct ResponsiveApp {
window_size: Vec2,
}
impl ResponsiveApp {
fn new() -> Self {
Self {
window_size: Vec2::new(800.0, 600.0),
}
}
fn ui(&mut self, ctx: &Context) {
self.window_size = ctx.screen_rect().size();
egui::CentralPanel::default().show(ctx, |ui| {
// 根据窗口大小调整布局
if self.window_size.x < 500.0 {
self.render_mobile(ui);
} else if self.window_size.x < 800.0 {
self.render_tablet(ui);
} else {
self.render_desktop(ui);
}
});
}
fn render_mobile(&self, ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
ui.heading("移动版布局");
ui.label("窗口较小,使用垂直布局");
ui.add_space(10.0);
ui.button("菜单");
ui.button("设置");
ui.button("关于");
});
}
fn render_tablet(&self, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.heading("平板版布局");
ui.label("左侧边栏");
ui.button("菜单");
ui.button("设置");
});
ui.vertical(|ui| {
ui.label("主要内容区域");
ui.add(egui::Slider::new(&mut 0.0, 0.0..=100.0));
});
});
}
fn render_desktop(&self, ui: &mut egui::Ui) {
ui.columns(3, |columns| {
columns[0].vertical(|ui| {
ui.heading("面板 1");
ui.label("工具栏");
});
columns[1].vertical(|ui| {
ui.heading("面板 2");
ui.label("内容区域");
});
columns[2].vertical(|ui| {
ui.heading("面板 3");
ui.label("属性面板");
});
});
}
}
7.3 性能优化
use std::collections::HashMap;
use std::time::Instant;
struct OptimizedApp {
cache: HashMap<String, String>,
last_update: Instant,
frame_count: u32,
fps: f32,
}
impl OptimizedApp {
fn new() -> Self {
Self {
cache: HashMap::new(),
last_update: Instant::now(),
frame_count: 0,
fps: 0.0,
}
}
fn update(&mut self, ctx: &egui::Context) {
// FPS 计算
self.frame_count += 1;
let now = Instant::now();
let elapsed = now.duration_since(self.last_update).as_secs_f32();
if elapsed >= 1.0 {
self.fps = self.frame_count as f32 / elapsed;
self.frame_count = 0;
self.last_update = now;
}
// 使用缓存避免重复计算
let expensive_data = self.cache
.entry("data".to_string())
.or_insert_with(|| self.expensive_computation());
egui::CentralPanel::default().show(ctx, |ui| {
ui.label(format!("FPS: {:.1}", self.fps));
ui.label(format!("缓存数据: {}", expensive_data));
// 只在必要时重绘
if ui.input(|i| i.pointer.any_click()) {
ctx.request_repaint();
}
// 限制重绘频率
ctx.request_repaint_after(std::time::Duration::from_millis(16));
});
}
fn expensive_computation(&self) -> String {
// 模拟耗时计算
std::thread::sleep(std::time::Duration::from_millis(100));
"计算结果".to_string()
}
}
8. 总结
本章我们深入学习了 Rust 的 GUI 开发生态:
- egui:即时模式 GUI,适合工具和编辑器
- iced:Elm 架构,适合复杂应用
- slint:声明式 UI,适合嵌入式
- gtk-rs:原生 GTK 应用
- tauri:混合桌面应用
每个框架都有其特点和适用场景:
- 需要快速开发工具?选择 egui
- 需要跨平台复杂应用?选择 iced
- 需要高性能嵌入式?选择 slint
- 开发 Linux 原生应用?选择 gtk-rs
- 需要 Web 技术栈?选择 tauri
GUI 开发不仅仅是选择框架,还需要考虑:
- 架构模式:MVC、MVVM、Elm
- 响应式设计:适配不同屏幕尺寸
- 性能优化:缓存、FPS 控制
- 用户体验:动画、反馈、可访问性
通过本章的学习,你应该能够选择合适的 GUI 框架,并开发出高质量的 Rust GUI 应用!
下一章我们将学习 Web 开发,探索 Rust 在全栈开发中的应用!


2476

被折叠的 条评论
为什么被折叠?



