读项目NeteaseCloudMusicGtk4

netease-cloud-music-gtk4 是基于 GTK4 + Libadwaita 构造的网易云音乐播放器,专为 Linux 系统打造,已在 openSUSE Tumbleweed + GNOME 环境下测试。

如何将项目运行起来

我们要读一个程序的代码,不管它怎么样,先跑起来再说。这个程序使用的是 GTK4 + Libadwaita,是 Linux 桌面常用的 GUI 库。这个程序没有在 Window 上构建的说明,并且可能是只能在 Linux 上正常编译,因此我将在 Ubuntu 上尝试运行这个程序。

运行一个使用 GTK 的项目我尝试了两种方法:

  • 使用 IDE(GNOME Builder)
  • 使用文本编辑器(VS Code)

On GNOME Builder

通过 flatpak 安装 GNOME Builder.

flatpak install flathub org.gnome.Builder

启动后选择克隆仓库(Clone Repository)即可下载并打开项目。

这个项目中已经有了 flatpak 的描述文件,GNOME Builder 能够通过com.gitee.gmg137.NeteaseCloudMusicGtk4.json 文件自动安装需要的依赖。

要启用调试信息需要在 meson.build 中指定 buildtype=debug,这样 GNOME Builder 才会在编译的时候加上调试信息,打的断点才会有效。

GNOME Builder 用起来不是很习惯,很多使用 GTK 的人可能是使用 Vim 或者 VSCode.

On VSCode

尽管 VSCode 也有 flatpak 的插件,但是我尝试了没有成功。那不使用 GNOME Builder,也可以不使用 flatpak。不使用 flatpak 需要自己安装依赖:

sudo apt install -y libssl-dev libgtk-4-dev libdbus-glib-1-dev libadwaita-1-dev libgstreamer-plugins-bad1.0-dev gettext

这个项目使用 Rust 语言开发,对于 rustc 我推荐使用 Rust 官网提供的 rustup 安装。项目使用了 meson 作为构建工具,通过 pip 安装最新的 meson 和 ninja。

下载源码,编译,安装。

git clone https://github.com/gmg137/netease-cloud-music-gtk.git
cd netease-cloud-music-gtk

meson setup build
cd build
ninja install

现在通过 VSCode 打开 netease-cloud-music-gtk 就可以通过 VSCode 的 Rust 插件来运行和调试代码了。

为什么需要安装?因为 GTK 程序使用的资源和 UI 文件必须被编译被安装才能被程序访问到,换言之,修改 ui 文件后就要重新安装。

也可以不安装,但是需要修改现在的加载资源的方式,参考GUI development with Rust and GTK 4

程序的初始化

在 VSCode 中配置好后,就可以点击 main 函数上的 run 或者 debug 来启动程序了。一定要通过 meson 安装资源才能在不改动原来的代码的情况下正常启动程序。

整个程序都被封装在 NeteaseCloudMusicGtk4Application 内,main() 函数只做了各种初始化。程序激活后程序才会创建应用的主窗口,所有的其他的页面和组件都在主窗口内去创建和处理。整个应用程序的 activate() 只将窗口创建出来,然后设置一个循环等待消息。

application.rs

fn activate(&self) {
	let obj = self.obj();
	let app = obj
		.downcast_ref::<super::NeteaseCloudMusicGtk4Application>()
		.unwrap();

	if let Some(weak_window) = self.window.get() {
		weak_window.upgrade().unwrap().present();
		return;
	}

	let window = app.create_window();
	let _ = self.window.set(window.downgrade());

	// Setup action channel
	let receiver = self.receiver.borrow_mut().take().unwrap();
	MAINCONTEXT.spawn_local_with_priority(
		Priority::HIGH,
		clone!(@strong app => async move {
			while let Ok(action) = receiver.recv().await {
				app.process_action(action);
			}
		}),
	);

	// Ask the window manager/compositor to present the window
	window.present();
}

在 Rust and GTK4 那本书中介绍了异步函数之间的通信使用的是 async_channel。要在普通的代码中使用异步函数就要有一个 runtime 来 spawn 异步块,MAINCONTEXT 也是 Glib 提供的用来 spawn 异步块的。

整个程序在创建窗口后就一直等待其他的 Action 的发起,这里的 Action 不是 GTK 库的 Action, 相当于传递消息。process_action() 就是收到传递来的消息,然后做对应的事情。

如何响应用户的操作

GNOME 有一个调试用的工具,提供 Ctrl+Shift+I 可以调出一个类似于 Chrome 里面的开发者工具的 Inspector。这样可以找到对应的组件的名字和id,顺着id搜索代码就能找到注册的回调函数。顺着名字就能找到对应的 ui 文件查看组件的结构。

以下一曲按钮(next_button)为例,因为下一曲按钮放置在一个名为 player_controls 的组件里面,在 palyer_controls.ui 中我们找到下面的代码:

<object class="GtkButton" id="next_button">
	<property name="halign">fill</property>
	<property name="valign">center</property>
	<property name="icon-name">media-skip-forward-symbolic</property>
	<signal name="clicked" handler="next_button_clicked_cb" swapped="true" />
	<style>
		<class name="circular" />
		<class name="flat" />
	</style>
</object>

这个按钮声明了一个信号处理函数,根据这个 handler 的名字就可以在Rust代码中找到回调函数的定义。

#[template_callback]
fn next_button_clicked_cb(&self) {
	let sender = self.sender.get().unwrap().clone();
	if let Ok(mut playlist) = self.playlist.lock() {
		if let Some(song_info) = playlist.next_song() {
			let song_info = song_info.to_owned();
			sender
				.send_blocking(Action::Play(song_info.to_owned()))
				.unwrap();
			sender
				.send_blocking(Action::UpdateLyrics(song_info))
				.unwrap();
			sender
				.send_blocking(
					Action::UpdatePlayListStatus(playlist.get_position()))
				.unwrap();
			return;
		}
	}
	sender
		.send_blocking(Action::AddToast(gettext("No more songs!")))
		.unwrap();
}

尽管这看起来有一点复杂,但是其实就是拿到下一首歌曲的信息然后发送3个消息:

  1. 播放歌曲
  2. 更新歌词
  3. 更新播放列表的状态

所有发送的消息最终被 process_action() 接收,process_action 函数内部相当于一个巨大的 switch-case.

按照这样的思路,去调试代码就能找到自己感兴趣的部分了。

总结

读一个项目大概就是这样,运行起来,可以调试。

此前我并没有写过什么 GUI 程序,即便是写过的通常也就是只有一个页面,所以对图形界面程序的页面路由和复杂的状态管理没有什么经验。这个程序当然也没有做的特别复杂。

GTK 这个图形库的学习资料比较少,也有类似 Qt 中的属性、信号的概念,只不过这些都是原生的 C 语言做的,因为 Rust 不支持继承,尽管实际上做同样的事情,但是代码的组织方式却不一样。

从项目的代码来看,能看到作者从 GNOME Builder 生成的 Hello World 模板逐步迭代开发的痕迹,代码当中有多处是代码重复的(很明显的拷贝粘贴),有时候作者采用的方法和 Rust GTK4 书上的例子的做法不一样,当然没有孰优孰劣之分,只是喜好不同。尽管代码在实现上还有一些缺点,但是作为一个可以正常使用且使用 GTK 开发的应用已经很棒了。

posted @ 2024-04-09 15:32  wngtk  阅读(17)  评论(0编辑  收藏  举报