使用 Glade 创建用户界面
Glade 是 Gtk+ 工具包的 GUI 构建器。Glade 使交互式创建用户界面变得容易,并且它可以为这些界面生成源代码,以及用户界面回调的存根。
libglade 库允许程序轻松实例化在 Glade 项目文件中定义的 widget 层次结构。它包含一种将项目文件中命名的回调绑定到程序提供的回调例程的方法。
James Henstridge 维护着 libglade 和 gnome-python 包,gnome-python 包是 Gtk+ 工具包、GNOME 用户界面库和 libglade 本身的 Python 绑定。使用 libglade 绑定构建基于 Python 的 GUI 应用程序可以显著节省开发和维护成本。
本文中的所有代码示例均使用在 Mandrake Linux 7.2 上运行的 Glade 0.5.11、gnome-python 1.0.53 和 Python 2.1b1 开发。
启动后,Glade 会显示三个顶级窗口(见图 1)。应用程序主窗口显示当前 Glade 项目文件的内容,以项目文件中定义的顶级窗口和对话框列表的形式呈现。调色板窗口显示 Glade 支持的 Gtk+ 和 GNOME widget。当选择 widget 进行编辑时,属性窗口会显示该 widget 属性的当前值。
调色板窗口将 Glade 支持的 widget 分为三组。“GTK+ Basic” widget 是最常用的 Gtk+ widget。“GTK+ Additional” 是不太常用的 widget,例如标尺和日历。“Gnome” widget 取自 GNOME UI 库。
属性窗口在一个四页的笔记本中显示 widget 属性。“Widget”页面显示 widget 的名称以及特定于 widget 类别的任何属性。当 widget 放置在基于约束的容器(例如 GtkTable 或 GtkVBox)中时,“Place”页面显示控制 widget 在其容器中位置的属性;否则,“Place”页面为空。“Basic”页面显示所有类型的 widget 都具有的基本属性,例如宽度和高度。最后,“Signals”页面允许您浏览所选 widget 可以发出的 Gtk+ 信号集,并允许您将信号处理函数绑定到这些信号。
在 Glade 中布局 widget 层次结构的过程类似于 Visual Basic 等环境。每个层次结构的根都是一个顶级窗口或对话框。widget 可以放置在这些顶级容器中,方法是首先在 Glade 调色板窗口中选择要创建的 widget 类型,然后单击容器内的任何交叉阴影区域。
Glade 属性窗口的“Signals”页面允许您向应用程序添加特定于应用程序的行为。页面的顶部列出了为当前 widget 定义的信号处理程序。页面底部的控件允许您选择 widget 发出的信号之一,并为其创建一个新的处理程序。
要定义新的信号处理程序,请单击“Signal”输入字段右侧的省略号按钮。将出现“选择信号”对话框,列出此 widget 可以发出的所有信号。信号按定义它们的 Gtk+ widget 类分组(见图 2)。
选择信号后,单击“选择信号”对话框中的“确定”。所选信号的名称将出现在“Signals”页面的“Signal”输入字段中。Glade 还会自动填充“Handler”字段,使用“on_<widget>_<signal>”的命名约定。如果 Glade 的命名约定不适合您的需求,您可以手动更改名称。
“Signals”页面的底部部分提供了额外的输入字段,您可以在其中提供特定于应用程序的数据,指定接收信号的对象等等。我总是将这些字段留空,因为在使用 gnome-python 时不需要它们。
Glade 将有关项目的信息保存在 XML 格式的项目文件中,该文件具有 .glade 的文件名扩展名。Glade 对 XML 的使用使得构建在项目文件上运行的单独的附加工具变得容易,例如用于新编程语言的代码生成器。
首次保存新项目时,Glade 会向您显示“项目选项”对话框。“项目选项”对话框中的大多数设置仅在您使用 Glade 为项目生成源代码时才重要。但是,即使您只是将 Glade 用作布局工具,某些设置(例如项目目录)也很重要。
默认情况下,Glade 假设您要将新项目保存在您的登录目录下,在名为 Projects/project1 的子目录中。这可能不是您想要的。我通常将项目保存在启动 Glade 的目录中。
幸运的是,重置项目目录很容易。只需单击“项目目录”输入字段右侧的“浏览...”按钮,就会出现一个名为“选择项目目录”的对话框。默认情况下,此对话框选择 Glade 的当前工作目录,因此您只需单击其“确定”按钮。
当您这样做时,“项目选项”对话框中的“项目目录”字段将更改为当前工作目录,“项目名称”字段将变为空白。输入新的项目名称,“程序名称”和“项目文件”字段将相应更新(见图 3)。当您单击“确定”时,您的项目将保存到指定的项目文件中。
创建 Glade 项目文件后,您可以使用 gnome-python 的 libglade 模块来创建项目文件中描述的可视化层次结构,并获得对层次结构中 widget 的编程访问权限
import libglade loader = libglade.GladeXML ("helloworld.glade", "window1")
libglade 库定义了一个类 GladeXML,它完成了大部分工作。要加载 widget 层次结构,请实例化 GladeXML 并将 Glade 项目文件的名称以及要从文件中加载的最顶层 widget 的名称传递给它。
请注意,您可以提供层次结构中任何 widget 的名称,即使它深深地隐藏在顶级窗口中。这使得可以将复杂的可视化层次结构(例如,基于复杂笔记本界面的页面)跨多个 Glade 项目文件进行分区。这也使得处理具有动态视觉内容的项目变得容易,仅加载在任何给定时间合适的组件。
加载 widget 层次结构后,GladeXML 允许您通过 get_widget 方法按名称查找特定 widget。get_widget 返回您请求的 widget,如果找不到该 widget,则返回“None”
window1 = loader.get_widget("window1") if window1: window1.set_title("Hello, World!")
GladeXML 最强大的功能之一是它可以将 Python 可调用对象(方法、函数等)绑定到 Glade 项目文件中命名的信号处理程序。signal_autoconnect 方法使这成为可能。
signal_autoconnect 接受一个参数:一个将信号处理程序名称映射到 Python 可调用对象的字典。对于您在 Glade 项目文件中定义的每个信号处理程序,signal_autoconnect 都会在提供的字典中查找相应的 Python 可调用对象。如果找到匹配的条目,它将绑定到该信号。换句话说,您的 Python 可调用对象将被安装为信号处理程序
def button1_click_handler(*args): print "Don't push that button!" signal_handlers = { # Exit the main event loop when the user closes the main window. 'on_window1_delete_event': gtk.mainquit, # Call button1_click_handler when the user clicks button1. 'on_button1_clicked': button1_click_handler } loader.signal_autoconnect(signal_handlers)
libglade 本身就大大减少了 gnome-python 应用程序所需的手动编码。可以使用 Glade 布局 widget 层次结构,并且只需两三行代码即可加载它们,而不是使用直接 pygtk 调用创建它们所需的数百行代码。更重要的是,可以通过组装 Python 可调用对象字典并将其传递给 GladeXML.signal_autoconnect 来添加行为,而不是重复调用 widget 连接方法。
libglade 节省了很多精力,但它可以做得更多。例如,大型 Python 应用程序通常结构为一个小型主程序和安装在 Python 路径某处的关联 Python 包集合。如果应用程序的 Glade 项目文件可以与其 Python 包一起存储,并在运行时通过相对路径名“导入”,则可以降低维护成本。
如果可以直接将 widget 作为某种 UI 层次结构对象的实例变量访问,而无需通过 GladeXML.get_widget 定位,那也将很有帮助。
最后,应该可以自动构建对象命名空间中可调用对象的字典,并将该字典传递给 signal_autoconnect。这将允许客户端将信号处理程序定义为对象方法,并避免显式注册处理程序。
以下部分描述了一个名为 GladeBase 的模块,该模块提供了这些功能。GladeBase 还重塑了 libglade 的服务,以适应 MVC(模型-视图-控制器)设计模式(请参阅 ftp://ftp.linuxjournal.com/pub/lj/listings/issue87/ 中的列表 1)。GladeBase 有两个主要导出项:类 UI 和类 Controller。
GladeBase.UI 对应于 MVC 设计模式的视图组件。它负责从 Glade 项目文件创建 widget 层次结构,并在关联控制器的指导下更新应用程序的可视化内容。GladeBase.UI 派生自 libglade 的 GladeXML 类,因此它继承了前面讨论的所有方法。
GladeBase.UI 构造函数接受三个参数:Glade 项目文件的文件名(从中加载 widget 层次结构)、作为层次结构根的 widget 名称以及可选的关键字参数 gladeDir,它是用于查找 Glade 项目文件的目录的相对路径名。
gladeDir 关键字参数默认为当前工作目录。它与 filename 参数连接以形成 Glade 项目文件的相对路径名。
使用 gladeDir 和 filename 参数而不是用单个相对路径名指定 Glade 项目文件的位置可能看起来很奇怪。但是,对于任何将其 Glade 项目文件存储在单个子包中的应用程序,这种分离可以降低维护成本。
这样的应用程序可以定义 GladeBase.UI 的子类,该子类为 gladeDir 提供硬编码值
import GladeBase class UIBase(GladeBase.UI): def __init__(self, filename, rootname): GladeBase.UI.__init__(self, filename, rootname, gladeDir="MyApp/GladeFiles") class MainWinUI(UIBase): def __init__(self): UIBase.__init__(self, "main_win.glade", "window1")
然后,应用程序可以从此子类派生其所有 UI 类。通过这种方式,应用程序可以在一个位置指定包含其所有 Glade 项目文件的目录的相对路径名。
一个辅助模块 PathFinder.py 使 GladeBase.UI 能够在 Python 路径中搜索文件。PathFinder.find 函数将路径名作为其唯一参数。如果路径名是绝对路径名,则直接返回,不做进一步处理。如果是相对路径名,则 find 函数将其与每个 Python 路径条目依次连接以创建候选路径名。如果候选路径名存在,则返回该路径名。如果没有候选路径名匹配,则 find 引发 PathFinder.Error 异常(请参阅列表 2)。
GladeBase.UI.__getattr__ 方法使客户端可以访问 GladeBase.UI 层次结构中的 widget,就像它们是实例的属性一样。__getattr__ 方法假定调用者提供的属性名称是 widget 的名称,并使用 GladeXML.get_widget 查找 widget。找到 widget 后,它将作为新的实例变量缓存,以加快未来的访问速度。如果找不到请求的 widget,__getattr__ 会引发 AttributeError。
如果 widget 层次结构包含多个同名的 widget,则无法确定 GladeBase.UI 将返回哪个 widget。当您使用 GladeBase.UI 时,最好以与命名 Python 实例属性相同的方式命名 widget:每个名称对于对象应该是唯一的,并且应该是有效的 Python 标识符。
特定于应用程序的 UI 类通常使用执行复杂用户界面更新的方法扩展 GladeBase.UI。
GladeBase.Controller 对应于 MVC 的控制器组件。控制器通过将用户输入事件转换为应用程序数据模型状态的更改来响应用户输入事件。同样,它通过将数据模型中的更改转换为 UI 更新来响应数据模型中的更改。
GladeBase.Controller 不会帮助您响应应用程序数据模型中的更改,但它会自动将信号处理程序方法连接到 Glade 项目文件中定义的信号处理程序。
GladeBase.Controller 构造函数接受一个参数:GladeBase.UI 的实例,即要控制的 UI。在初始化期间,新的 GladeBase.Controller 实例遍历其类层次结构,构建实例命名空间中所有可调用对象的字典(遍历从实例字典开始,以防任何可调用对象被定义为实例属性)。然后,GladeBase.Controller 将此字典传递给提供的 GladeBase.UI 实例的 signal_autoconnect 方法。
特定于应用程序的控制器类通过简单地定义信号处理程序方法来扩展 GladeBase.Controller
class Controller(GladeBase.Controller): def __init__(self, ui): ... GladeBase.Controller.__init__(self, ui) def on_window1_delete_event(self, *args): gtk.mainquit() def on_button1_clicked(self, *args): print "Button 1 clicked."
GladeBase 自动化了 Gtk+ widget 层次结构到 Python 对象层次结构的转换,并自动连接基于 Python 的信号处理程序,但它仍然需要您识别和实现 Glade 项目文件中定义的所有信号处理程序。对于纯 Gtk+ 项目,这不是问题,因为唯一的信号处理程序是您显式定义的那些。
但是,当您使用 Glade 构建 GNOME 应用程序时,许多信号处理程序是自动定义的。例如,一个新的 Gnome 应用程序窗口是使用标准菜单栏创建的,其菜单项都具有预定义的 activate 信号处理程序。手动浏览基于 GNOME 的项目,手动定位预定义的信号处理程序并将它们添加到您的应用程序控制器中可能会很乏味。
如前所述,Glade 项目文件以 XML 格式存储(到目前为止,还没有描述项目文件结构的 DTD,但通过检查很容易理解)。Python 2.0 包含一个 XML 库,它位于 James Clark 的 Expat 库之上。因此,构建一个 Python 应用程序来浏览 Glade 项目文件,识别给定 widget 层次结构中声明的所有信号处理程序,并为该层次结构生成存根控制器模块是相当容易的。
GladeProjectSignals.py(请参阅 ftp://ftp.linuxjournal.com/pub/lj/listings/issue87/ 中的列表 3)从 Glade 项目文件中提取信号处理程序信息。该模块有两个主要抽象。WidgetTreeSignals 类遍历表示 widget 层次结构的 XML DOM(文档对象模型)树,并记录它找到的所有信号处理程序声明。GladeProjectSignals 类加载 Glade 项目文件,并构建 WidgetTreeSignal 实例的字典,每个实例对应于项目文件中定义的顶级 widget。
WidgetTreeSignals 的构造函数接受一个 DOM 节点作为参数。它假定此节点描述了一个 widget,并期望它包含一个定义 widget 名称的名称节点。记录 widget 的名称后,WidgetTreeSignals 会遍历 DOM 树。它检查每个访问的节点,以查看它是否是信号节点。如果是,WidgetTreeSignals 会记录节点处理程序子节点的值,该值应该是信号处理程序的名称。否则,WidgetTreeSignals 假定该节点包含子节点并继续遍历这些子节点。
GladeProjectSignals 相对简单。它使用 Python 的 xml.dom.minidom 包将 Glade 项目文件加载为 DOM 树。然后,它在树中搜索顶级 widget 节点(Glade 设计文件包含其他顶级节点,例如 GTK-Interface 和 project 节点)。对于找到的每个 widget 节点,GladeProjectSignals 创建一个新的 WidgetTreeSignals 实例,该实例反过来列出由该 widget 及其后代定义的信号处理程序。每个 WidgetTreeSignal 实例都存储在字典 self.widgets 中,该字典以顶级 widget 名称为键。
ControllerGenerator.py(请参阅 ftp://ftp.linuxjournal.com/pub/lj/listings/issue87/ 中的列表 4),当使用 Glade 项目文件名和在该文件中定义的顶级 widget 名称调用时,会为该 widget 及其子项打印一个存根控制器。
模块的大部分工作由 ControllerGenerator 类完成。此类定义了一个 generate 方法,该方法接受 Glade 项目文件名和顶级 widget 名称作为参数。generate 方法使用 GladeProjectSignals 的实例来查找命名 widget 的处理程序。然后,它为这些处理程序创建一个存根列表。使用模板字符串和 Python 的字符串格式化运算符,generate 生成一个包含存根控制器模块主体的字符串,并将其返回给其调用者。
Glade、libglade 和 gnome-python 可以大大减少在 Python 中构建 Gtk+ 和 GNOME 应用程序的工作量。本文中介绍的工具通过自动化 Glade widget 层次结构到 Python 对象层次结构的转换、自动连接控制器中定义的信号处理程序以及生成存根控制器,进一步降低了维护成本。
