《精通Rust》里介绍了 GTK+框架的开发,这篇博客记录并扩展一下。rust 可以用于桌面应用开发,我还挺惊讶的,大学的时候也有学习过 VC++,对桌面编程一直都很感兴趣,而且一直有一种妄念,总觉得自己能开发一款很好用的桌面程序,就和总觉得自己能彩票中大奖一样。
环境安装
可能你会需要安装 gtk+3。如果执行 cargo build 的时候提示你找不到 gdk-3.0
,那你就需要手动安装一下:
不过,也不需要提前安装这些依赖。当我们执行 cargo build 编译的时候,结合 rust 的错误提示进行按需安装是比较稳妥的。
功能开发
在《精通Rust》书中的16章节,书中的 Demo 忽略了一个非常重要的细节,就是省略了依赖包的声明,没有依赖包的声明,代码就缺少了灵魂,编译都没有办法通过。
我在考虑要不要使用 GPT 自动生成一下源码,省的麻烦。自动生成代码还是放到最后吧…可以通过这个过程来熟悉一下 rust 的函数。
std::process::exit
这个函数并不陌生,使用一个 code 来退出当前的进程。要想在程序中正常调用这个函数,需要导入如下的头声明:
use std::process;
gtk::Window
代码中使用到的组件都来自于 gtk 包,为了方便起见,可以将 gtk 下的声明全局导入
use gtk::*
std::sync::mpsc::Sender
用来通过 channel 实现异步通讯的能力,代码中用来做数据通讯,有 send
和对应的 try_recv
的两个动作。如果我们不引入这个包,cargo build 还会给我们另一个可选建议 glib::Sender,不过这个函数的解释中提到,两个方式是类似的。
use std::sync::mpsc::Sender
std::sync::mpsc::Receiver
有消息的发送,就应该有消息的接受
use std::sync::mpsc::Receiver
std::sync::mpsc::channel
函数用来创建 Sender、Receive,和上面两个函数是一体的,它创建的是一个异步队列。创建同步队列需要调用 std::sync::mpsc::sync_channel方法。
use std::sync::mpsc::channel
mod
rust 在相同的目录下,不同文件中声明的结构体是无法相互引用的,需要通过 mod 来解决。mod 主要用来解决项目内代码组织的问题,use mod xxx
会尝试去加载当前目录下的 xxx.rs 文件的代码。
在 main.rs 中的 mod hackernews
就是用来加载 hackernews.rs
中声明的导出方法或结构体。如果你将这行代码删除,程序就会找不到文件中声明的 Story 结构体。
use crate::hackernews::Story;
std::sync::Arc
全称是 Atomically Reference Counted,表示线程安全的引用计数器。Arc 表示一个指针,指向堆空间的 T 值。同时,有一个附属的引用计数。
use std::sync::Arc;
reqwest::Client
一个异步的 HTTP 请求客户端,用来发送 HTTP 请求。在说明文档中明确强调:我们不需要使用 Rc 或者 Arc 去包装这个类型,内部已经使用 Arc 包装过了。
use reqwest::Client;
glib::source::timeout_add
函数用于固定间隔执行闭包函数,示例中的作用是固定间隔尝试接受消息。我发现,rust 依赖包的做法特别接近 javascript 。
函数第一个参数的类型是 core::time::Duration,这个时间概念和 Go 语言相近,不过 rust 表示的是秒+毫秒的单位,构造时间的时候可以传递秒和毫秒两个数值。
use glib::source::timeout_add;
std::ops::ControlFlow
代码示例中 ControlFlow::Continue(true)
,目前来看这个 Continue 的含义并不明确,感觉不到它的价值。
use std::ops::ControlFlow
impl App {
pub fn new() -> (App, Receiver<Msg>) {
if gtk::init().is_err() {
println!("Failed to init hews window");
process::exit(1);
}
let (tx, rx) = channel();
let window = gtk::Window::new(gtk::WindowType::Toplevel);
let sw = ScrolledWindow::new(None, None);
let stories = gtk::Box::new(gtk::Orientation::Vertical, 20);
let spinner = gtk::Spinner::new();
let header = Header::new(stories.clone(), tx.clone());
stories.pack_start(&spinner, false, false, 2);
sw.add(&stories);
window.add(&sw);
window.set_default_size(600, 350);
window.set_titlebar(&header.header);
window.connect_delete_event(move |_, _| {
main_quit();
Inhibit(false);
});
}
pub fn launch(&self, rx: Receiver<Msg>) {
self.window.show_all();
let client = Arc::new(reqwest::Client::new());
self.fetch_posts(client.clone());
self.run_event_loop(rx, client);
}
fn fetch_posts(&self, client: Arc<Client>) {
self.spinner.start();
self.tx.send(Msg::Loading).unwrap();
let tx_clone = self.tx.clone();
top_stories(client, 10, &tx_clone);
}
fn run_event_loop(&self, rx: Receiver<Msg>, client: Arc<Client>) {
let container = self.stories.clone();
let spinner = self.spinner.clone();
let header = self.header.clone();
let tx_clone = self.tx.clone();
timeout_add(100, move || {
match rx.try_recv() {
Ok(Msg::NewStory(s)) => App::render_story(s, &container),
Ok(Msg::Loading) => header.disable_refresh(),
Ok(Msg::Loaded) => {
spinner.stop();
header.enable_refresh();
}
Ok(Msg::Refresh) => {
spinner.start();
spinner.show();
(&tx_clone).send(Msg::Loading).unwrap();
top_stories(client.clone(), 10, &tx_clone)
}
Err(_) => {}
}
Continue(true)
});
gtk::main();
}
fn render_story(s: Stroy, stories: >k::Box) {
let title_with_score = format!("{} ({})", s.title, s.score);
let label = gtk::Label::new(&*title_with_score);
let story_url = s.url.unwrap_or("N/A".to_string());
let link_label = gtk::Label::new(&*story_url);
let label_markup = format!("<a href=\"{}\">{}</a>", story_url, story_url);
link_label.set_markup(&label_markup);
stories.pack_start(&label, false, false, 2);
stories.pack_start(&link_label, false, false, 2);
stories.show_all();
}
}