rust精通教程 (3)

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

Rust 精通教程 3:GUI 开发

GUI(图形用户界面)开发是许多应用程序的重要组成部分。Rust 生态系统提供了多种 GUI 框架,从轻量级的原生绑定到功能丰富的跨平台框架。今天我们将深入探索 Rust 的 GUI 开发生态。

1. GUI 框架概览

Rust 的主要 GUI 框架:

框架特点适用场景
egui即时模式、WebAssembly 支持、简单易用工具、编辑器、Web 应用
iced类 Elm 架构、跨平台、响应式跨平台桌面应用
slint声明式 UI、高性能、商业支持嵌入式、桌面应用
gtk-rsGTK4/3 绑定、成熟稳定Linux 原生应用
druid数据驱动、跨平台数据密集型应用
tauriWeb 技术栈、小巧高效混合桌面应用

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(&gtk::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 在全栈开发中的应用!

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王小玗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值