本系列教程對應的代碼已開源在 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狀態更新