博客 / 詳情

返回

【Rust GUI開發入門】編寫一個本地音樂播放器(3. UI與後台線程通信)

本系列教程對應的代碼已開源在 Github zeedle

UI線程 $\xrightarrow{消息}$ 後台線程

使用枚舉定義消息類型

enum PlayerCommand {
    Play(SongInfo, TriggerSource), // 從頭播放某個音頻文件
    Pause,                         // 暫停/繼續播放
    ChangeProgress(f32),           // 拖拽進度條
    PlayNext,                      // 播放下一首
    PlayPrev,                      // 播放上一首
    SwitchMode(PlayMode),          // 切換播放模式
    RefreshSongList(PathBuf),      // 刷新歌曲列表
    SortSongList(SortKey, bool),   // 排序歌曲列表
    SetLang(String),               // 設置語言
}

通過管道發送數據

從UI主線程通過管道(channel)向後台線程發送信息,例如:

// UI 主線程
...
let (tx, rx) = std::sync::channel::<PlayerCommand>();
std::thread::spawn(move || {
    // 後台線程
    while let Ok(cmd) = rx.recv() {
        match cmd {
            PlayerCommand::Pause => {...},
            PlayerCommand::PlayNext => {...},
            PlayerCommand::PlayNext => {...},
            ....
        }
    }
});

...
tx.send(PlayerCommand::Pause);
...

後台線程 $\xrightarrow{消息}$ UI線程

全局狀態

Slint UI支持在.slint文件中聲明全局變量,然後在Rust代碼中訪問/修改該變量的值,這樣即可完成UI狀態的更新:

// ui state
export global UIState {
    // 當前播放進度 (秒)
    in-out property <float> progress;
    // 總時長 (秒)
    in-out property <float> duration;
    // 當前播放進度文本
    in-out property <string> progress_info_str;
    // 播放/暫停狀態
    in-out property <bool> paused;
    // 是否正在拖動進度條
    in-out property <bool> dragging;
    // 歌曲列表
    in-out property <[SongInfo]> song_list;
    // 當前播放歌曲的信息
    in-out property <SongInfo> current_song;
    // 播放模式
    in-out property <PlayMode> play_mode;
    // 是否已被用户觸發播放
    in-out property <bool> user_listening;
    // 當前播放歌曲的歌詞
    in-out property <[LyricItem]> lyrics;
    // 當前歌詞視窗的滾動條位置(一般為負數)
    in property <length> lyric_viewport_y;
    // 當前一行歌詞的高度
    in-out property <length> lyric_line_height: 40px;
    // 歌曲文件夾配置
    in-out property <string> song_dir;
    // 關於信息
    in property <string> about_info;
    // 專輯封面圖像
    in property <image> album_image;
    // 播放歷史
    in property <[SongInfo]> play_history;
    // 播放歷史索引
    in property <int> history_index: 0;
    // 歌曲排序方式
    in-out property <SortKey> sort_key;
    in-out property <SortKey> last_sort_key;
    // 升序/降序
    in-out property <bool> sort_ascending: true;
    // 當前語言
    in-out property <string> lang;
    // 主題顏色
    in-out property <bool> light_ui;
}

從後台線程添加任務到UI主線程

與上文中使用管道發送指令不同,Slint UI提供了一種從後台線程發送指令到UI主線程的簡便方式,即通過slint::invoke_from_event_loop(task_closure),將task添加到UI主線程的下一輪事件循環中執行,更新UI狀態,例如:

// 後台線程
let ui_weak = ui.as_weak();
while let Ok(cmd) = rx.recv() {
    match cmd {
        PlayerCommand::Pause => {
            ...
            slint::invoke_from_event_loop(move || {
                // 此閉包雖然在後台線程中定義,但是執行是發生在UI主線程裏面的
                // 只有在UI主線程中才能獲得UI窗口對象的強引用,否則會失敗
                if let Some(ui) = ui_weak.upgrade(){
                    let ui_state = ui.global::<UIState>();
                    ui_state.set_paused(true);
                }
            })
            ...
        },
    }
}

總結

在Slint UI中,一種典型的範式是:

  • UI線程通過管道向後台線程發送數據
  • .slint中定義的全局變量(export global {...})可以在Rust代碼中直接使用
  • 後台線程通過slint::invoke_from_event_loop(task_closure)向UI主線程的事件循環中添加任務,典型做法是此任務修改上述全局變量(即UIState),來完成UI狀態更新
user avatar ftkj_2018 頭像 apollo008star 頭像 u_16213656 頭像
3 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.