Ximba Radio:开发用于 XM 卫星广播的 GTK+/Glade GUI
当美国电视逐渐沉沦到虚构现实的深渊时,您可能会发现自己像我一样,回归到电子娱乐的根源——广播。卫星广播是这种媒介的最新形式,它提供了范围广泛的电台,几乎可以在您开车去的任何地方访问。
因为我花费更多的时间在显示器前而不是在方向盘后,我很幸运地找到了一个基于 PC 的卫星广播解决方案。XMPCR 是一款 USB 连接的接收器,用于 XM 卫星广播系统,主要面向 Microsoft Windows 系统销售。该设备在 Linux 下由 OpenXM 项目支持,这是一组 Perl 脚本,充当控制设备的网络守护进程。不幸的是,该守护进程唯一的用户界面是一个有限的基于文本的工具。
Ximba Radio 诞生于 OpenXM 的图形化前端。该应用程序提供了一个极简主义的主窗口,其中包含当前频道信息,可以展开以显示频道列表和用户收藏。顶部的按钮栏提供了对广播和电台导航的轻松管理,以及对配置选项的快速访问。菜单栏为用户提供了传统桌面应用程序的舒适感。
频道列表以多种格式显示。主频道列表窗口显示所有频道,而特定类别的选项卡显示相关频道。单独的选项卡提供对用户可选择的艺术家和频道收藏以及当前会话历史的访问。可以使用“首选项”对话框隐藏类别选项卡,该对话框还允许用户设置性能设置和收藏夹通知。
在 Ximba Radio 的后端是 OpenXM 守护进程。这个 Perl 脚本和相关的 Perl 模块驱动与 USB 端口的连接。守护进程从配置文件读取,或者可以接受命令行参数。与守护进程的通信发生在可配置的 TCP 端口上,并有一个可接受的客户端列表。该守护进程也可以在 Windows 系统上运行。
我对这个应用程序的目标是匹配 Windows 版本的功能,提供一个极简主义的界面,并且易于配置。此外,实现需要在不到一人周(40 小时)的时间内完成。
另一个目标是尽可能保持用户界面代码独立于应用程序代码。应用程序代码应该能够与任何合适的用户界面一起使用,因此一个好的设计可以在顶部放置一个 curses 或 Web 界面,而无需额外的工作。这个目标符合 GNOME 开发指南以及 Glade 的未来计划。
为了实现这些目标,我计划使用单个头文件和单个 C 模块。为了加速实现并解决一些耗时的问题,我选择使用全局变量。请记住,这是一个简单桌面应用程序的原型,而不是企业级 24/7 容错巨兽。如果管理得当,全局变量的使用可以在未来的更新中删除。
有了目标,我开始使用 Glade 来雕琢用户界面;我将在下一节介绍具体的代码细节。我将 Glade 配置为生成 C 代码,并在“选项”对话框中对所有其他内容采用了默认设置。这里最重要的是保存用户界面定义(interface.c)和控件回调(callbacks.c)的源文件和头文件,以及生成 main.c 文件的选项。
Glade 为构建应用程序生成完整的构建环境。这包括源目录(src)和一个用于图像文件(pixmaps)的目录。GtkImage 控件中使用的图像文件需要为 .xpm 格式。其他生成的文件包括 autoconf 和 automake 模板以及用于国际化字符串的 pot 文件。
国际化是可选的,并通过 GNU gettext 支持处理。例如,interface.c 文件具有启用 gettext 的字符串。即使启用了此选项,您也不必为任何语言创建 pot 文件;Glade 只是使用您提供的任何文本字符串作为默认语言。
我发现使用 Glade 原型化 Ximba Radio 只需要手动编辑两个生成的文件,main.c 和 callbacks.c。前者只需要对与应用程序的配置处理相关的初始化选项进行少量添加。callbacks.c 文件主要被修改为将调用传递给我的 C 模块 utils.c。
当我在 Glade 中编辑我的项目并重新生成 C 代码时,callbacks.c 文件会附加新的函数。幸运的是,Glade 在知道回调已经存在时做得很好,并且没有破坏我的任何更改。不幸的是,它有时会重新添加一个现有的函数。有时需要手动删除这些额外的函数。当使用 libGlade 时,它直接处理 Glade XML 文件而不是使用生成的 C 代码,这个问题就不存在了。对 libGlade 的讨论超出了本文的范围。
Ximba Radio 需要两个主要窗口,主窗口和首选项对话框,以及一些辅助弹出窗口。主窗口的按钮栏是使用 Glade 的工具栏控件创建的,按钮是手动添加到工具栏控件中的。GTK+ 按钮可以有文本或图像。Glade 允许选择应用程序图像、股票按钮或股票图标。股票按钮使用与股票图标相同的图标,但工具提示不可用。因此,我建议使用股票图标并留空股票按钮字段。
工具栏中的每个按钮都有一个附加到点击信号的单个回调函数。回调函数可以有任何名称,如果需要,可以将控件本身的名称作为参数传递。对于附加到点击信号的回调,后者是不必要的。在附加到实现信号的回调中(我稍后会讨论),控件名称会传递给函数。
我在主窗口中添加了三个 GtkImage 控件。第一个是状态图标,位于主机名字段的右侧。我将其设置为“删除”图标——Glade 提供了许多股票图标——以显示未连接到守护进程。为了显示已连接状态,我使用了“应用”图标。为了在运行时更改图标,我在实现回调中保存了这个 GtkImage 的控件 ID。在正常使用期间,这个图标也可以更改为“关闭”股票图标,以便显示已连接但已静音的状态。我将在下一节检查处理这些更改的代码。
收藏夹按钮是加号。Glade 和 GTK+ 将这些称为“添加”图标。这些按钮有一个附加到其点击信号的单个回调。回调将当前艺术家或频道添加到相应的收藏列表。
笔记本控件提供了对完整频道列表的访问,以及类别、收藏夹和会话特定的列表。所有这些都通过 CList 控件提供。即使 GTK+ 更倾向于新代码使用更新和更复杂的树和列表控件,Glade 也完全支持此控件。我稍后将更详细地讨论这个有争议的决定。
Glade 为回调生成空函数,通常称为存根函数。存根函数使在原型开发中遵循一个简单的过程成为可能:设计 UI,生成代码,编写回调,测试和重复。除了菜单退出功能之外,我将大部分回调编码留到 UI 完成之后。稍后,我回去填写回调。这种方法使我能够在深入了解布局实际将做什么之前,先尝试应用程序的布局。同样,这是将用户界面代码与应用程序代码分离的整个目标的一部分。通过将这两个部分分开,我允许将来对 UI 进行更改,而不会对核心代码产生严重影响。回调是 UI 和应用程序代码之间的粘合剂,因为它们将 UI 事件映射到执行某些操作的代码。
回调具有不同的接口。按钮点击信号需要回调,这些回调将按钮控件 ID 和用户数据作为输入参数。CList 回调的 select-row 信号(在单击行时发送)接收五个参数。让 Glade 生成它们可以快速学习这些不同的接口。事实上,由于回调的 API 文档不完善——至少文档不容易找到——让 Glade 创建这些是学习回调语法的最佳方式。
填充回调代码可以直接在 callbacks.c 中完成,但是当我移动到 libGlade 时,这个 C 模块将在未来被删除。相反,我通常将参数直接传递给 utils.c 中类似的函数,该函数执行实际的工作。尽管有这个一般规则,但有一段重要的代码被放在了 callbacks.c 中:将控件 ID 分配给全局变量。清单 1 显示了如何在回调中使用全局变量来保存首选项对话框的 ID。
清单 1. 首选项对话框在第一次请求时创建,控件 ID 保存在全局变量中。
void XRPreferences (GtkButton *button, gpointer user_data) { /* If it hasn't been opened before, create * the dialog. */ if ( XR_Preferences_Window == NULL ) { XR_Preferences_Window = create_preferences(); gtk_widget_realize(XR_Preferences_Window); } ... }
跟踪单个控件对于多种原因变得必要。首先,一些图标根据程序的不同状态动态变化。其次,许多窗口仅临时显示,创建和销毁它们是多余的。更简单的方法是创建它们一次,然后根据需要简单地隐藏和显示它们。最后,Glade 生成的 CList 需要在运行时更新。保存控件 ID 的变量的作用域仅在 interface.c 文件中,这意味着这个 Glade 生成的文件之外的函数无法轻松地更改这些控件。
为了解决这个问题,我为我需要在运行时访问的每个控件设置了一个实现信号。Glade 允许您指定要在 interface.c 中定义的变量的名称。与实现信号关联的回调将该变量值作为对象参数传递。在回调中,该值保存在 xr.h 中定义的全局变量中,xr.h 是我为这个项目创建的单个头文件。所有全局变量都使用 #ifdefs 作用域,#defines 在 C 模块的顶部指定,如清单 2 和 3 的两个代码片段所示。
清单 2. 全局变量和函数在 xr.h 中声明。
code: #ifdef XR_CB_C GtkWidget *XR_Msg_Window = NULL; GtkWidget *XR_Msg = NULL; void XRUMsg(); void XRUInit(); #else extern GtkWidget *XR_Msg_Window; extern GtkWidget *XR_Msg; extern void XRUMsg(); extern void XRUInit(); #endif
清单 3. #defines 使变量和函数可以正确地被 C 模块访问。
code: #define XR_UTIL_C #include <stdio.h> #include <stdlib.h> #include "xr.h" ...
这种方法的一个问题是定义控件 ID 在何时变得可用。实现信号的回调仅在控件实际变得可见之前立即调用。有时您需要在发生这种情况之前访问该控件 ID。幸运的是,这很容易解决。控件在 signal handlers 设置之前在 interface.c 中创建。信号处理程序是一个将回调与某些事件关联的函数。
因此,到配置实现信号时,本地命名的变量都具有有效值。因此,可以为一个控件设置多个回调,所有这些回调都设置为该控件的实现信号,这会保存其他控件的控件 ID。例如,Ximba Radio 的主窗口控件为其设置了实现回调,这些回调保存了频道列表窗口中所有预定义的 CList 控件的控件 ID;有四个这样的控件。这是必需的,因为最初,即使在主窗口可见之后,这些 CList 也不可见,我需要立即开始更新列表。如果我不使用主窗口来保存 CList 的控件 ID,我将无法开始使用频道信息更新它们,直到这些列表至少显示一次。
对控件的动态更改也需要保存控件 ID。Ximba Radio 的状态图标就是一个例子。要更改状态图标,我只需要使用 GTK+ 股票图标并保存 Glade 生成的 GtkImage 控件的控件 ID。当用户更改程序状态时——无论是断开与守护进程的连接还是启用或禁用静音——状态图标都可以通过单个 GTK+ 函数调用轻松更改,如清单 4 所示。GTK+ 股票图标的完整列表在在线 GTK+ 文档中列出。
清单 4. 此函数使用 GTK+ 所谓的“应用”图标将状态图标更改为连接状态。
code: gtk_image_set_from_stock(GTK_IMAGE(XR_Status_Image), GTK_STOCK_APPLY, GTK_ICON_SIZE_BUTTON);
使用全局变量不是经验丰富的开发人员可能对我的方法提出的唯一问题。另一个是我选择使用已弃用的 GTK+ 控件:CList,也称为列式列表控件。弃用意味着该控件虽然仍然是当前发行版的一部分,但不再被积极开发,并且将来可能会从 GTK+ 发行版中删除。
当前 CList 控件的替代品是树和列表控件。Ximba Radio 需要频繁地动态添加和删除列表条目。CList 和树和列表控件都对这个要求没有帮助。但是,由于 CList 专门为处理列表而不是可扩展树而设计,我发现它是两者中复杂度较低的选择。按照定义,原型需要使用标准或典型的界面快速启动并运行应用程序。Glade 中的 CList 支持使这成为可能,而无需学习树和列表控件的复杂性。权衡将在稍后当我必须处理 CList 的最终处置时到来。
Ximba Radio 大量使用列表。所有频道、类别和收藏夹信息都保存在列式列表中。虽然完整的频道列表和收藏夹是永远不会完全消失的静态列表,但类别列表是动态的。用户可以启用或禁用它们从显示中显示,从而更容易根据自己的偏好找到电台。为了管理类别列表的动态性质,我使用了 GLib 的双向链表 GList。任何复杂度的应用程序都无法避免使用链表,而 GList 非常易于使用。存在单向链表选项 GSList,但 GList 中的双向链接几乎不需要任何成本,并且保留在列表中双向移动的选项值得 GList 可能增加的任何额外重量。
清单 5. 这两个函数将首选项数据写入文件,遍历 GList 以获取类别信息。
code: void XRUSavePrefs() { ... /* Write the preferences to it. */ fprintf(fd, "hostname:%s\n", prefs.hostname); if ( prefs.daemondir ) fprintf(fd, "daemondir:%s\n",prefs.daemondir); else fprintf(fd, "daemondir:\n"); fprintf(fd, "favorites:%d\n", (int)prefs.enable_favorites); fprintf(fd, "channels:%d\n", (int)prefs.channel_windows); fprintf(fd, "performance:%d\n", prefs.performance); /* Run the list of categories and save them * and their states */ g_list_foreach(prefs.categories, SavePrefsCategory, fd); ... } static void SavePrefsCategory( CatEntryT *catentry, FILE *fd ) { fprintf(fd, "category:%s:%d\n", catentry->name, catentry->state); }
如果在使用 pixmaps 时测试您的应用程序,则需要避免的另一个陷阱。Ximba Radio 在多个地方使用了徽标 pixmap。如果您从默认的 src 目录运行应用程序,则找不到 pixmap,除非您首先执行完整的构建
./configure --prefix=<install directory> make make install
此过程将 pixmaps 复制到安装目录前缀下的 pixmap 目录。例如,如果 <安装目录> 设置为 /usr/local/ximbaradio,则 pixmaps 安装在 /usr/local/ximbaradio/share/ximbaradio/pixmaps 中。安装后,编译后的程序可以正确找到 pixmaps。如果 pixmaps 已更新,则需要重新运行 make install 步骤。通过修改 support.c 文件,可以将徽标切换到编译后的徽标——即,使它们成为二进制文件的一部分,以便重定位问题无关紧要。我为以前的应用程序做过这件事,但该技术超出了本文的范围。
我设法在总共约 30 小时的工作时间内完成了整个应用程序的编写。其中大部分时间花在了核心代码上。UI 代码可能在总共十小时的工作时间内完成。该应用程序在所有主要功能上都与 Windows UI 匹配,并且首选项易于管理。UI 代码独立于核心代码,尽管仍然存在对 callbacks.c 文件的一些依赖性。
Ximba Radio 的开发仍在继续。计划包括添加音频控制选项和基于 GStreamer 的反射器。反射器、Ximba Radio 和 OpenXM 将结合起来,允许远程访问基于 PC 的 XM 卫星广播服务。
Glade 3 正在开发中,预计代码生成功能将被删除。由于这个即将发布的版本已经开发了很长时间,并且其发布似乎还遥遥无期,因此在不久的将来,使用生成的代码仍然是 Glade 构建的原型的可行选择。也就是说,使用 GTK+ 和 Glade 进行原型开发仍然快速且轻松。
Michael J. Hammel 是一位软件工程师和作家,与妻子 Brinda 和女儿 Ryann 一起住在德克萨斯州休斯顿。Michael 是一位狂热的跑步者和网球运动员,他爱他的狗 Reba 和 Bailey 就像爱他的电脑一样,他把空闲时间花在护理老化的膝盖、清理撕破的沙发垫上,并问自己为什么没有空闲时间。他的网站是 graphics-muse.com,可以通过 mjhammel@graphics-muse.org 与他联系。