PyGTK-开发基础知识-全-
PyGTK 开发基础知识(全)
一、入门指南
欢迎来到PyGTK 开发的基础。在本书中,您将获得 GIMP Toolkit (GTK+)的全面知识,它允许您创建全面的图形程序。在继续之前,您应该知道这本书是针对 Python 程序员的,所以我们假设您已经很好地理解了 Python 语言,并且您可以直接使用 GTK+。时间不会花在让您熟悉 Python 上。
为了最大限度地利用这本书,你应该按顺序阅读每一章,并研究每一章中的所有例子。在 Linux 上开始使用 GTK+ 非常容易,因为大多数发行版都捆绑了创建和运行 Python/GTK+ 程序所需的一切。我们将在本章后面介绍 Windows 和 macOS 的安装过程。
有几个应该安装的工具可以让你在开始时不会遇到麻烦。首先要安装 Python 3.x。需要运行 GTK+ 3.x Python 程序。其次,应该安装 GTK+ 3.x 运行时库。这些库安装了许多依赖项,包括 GObject、Pango、GLib、GDK、GdkPixbuf 和 ATK。确保安装所有相关的库。
你不需要安装 GNU 编译器集合。在本书提供的例子中,你没有编译任何 C/C++ 程序。您只需要安装 Python 3.x 和 GTK+ 3.x 运行时库来运行示例程序。
GTK+ 2.x 和 3.x 的区别
如果您精通 GTK+ 2.x,您可能会对 3.x 版本中的变化感到惊讶。GTK+ API 和包装这些库的 Python 类有大有小的变化。虽然大多数小部件的基础都没有改变,但是有很多小的“问题”会让你感到悲伤,直到你明白为什么以及哪里发生了变化。
大多数这些变化的原因是由于 GTK+ 理念的变化。GTK+ 2.x 库是围绕所有 GTK+ 程序之间的一致性而设计的,使用 GTK+ 主题作为一致性的基础。随着 GTK+ 库的出现,这种理念完全改变了。虽然主题仍然可用,但现在更容易创建 GTK+ 程序,它们有自己的外观和感觉,独立于当前的 GTK 主题。虽然这给了开发人员更大的控制权,但也需要一些额外的编程步骤来实现外观和感觉。它还删除了一些使小部件易于创建和控制的 API。
下面是 GTK+ 2.x 和 3.x 之间差异的部分列表。其中一些项目有简单的解决方法,但其他项目需要程序员做更多的工作,因为它们的差异足以导致源代码移植问题。
-
许多标准的股票图标已经被删除,主要是按钮和菜单项上使用的图标。如果您需要这些图标,您必须提供自己的图标集。
-
现在,所有 2.x 常量都作为属性分组到 3.x Python 类中。如果您正在移植源代码,这是一个需要解决的主要问题。
-
一些集装箱已经被淘汰。例如,
Gtk.Hbox
和Gtk.Vbox
小部件已经被移除,现在您必须在创建新的Gtk.Box
实例时通过参数指定Gtk.Box
的方向。注意,Gtk.Box
类现在是 GTK+ 3.x 中的一个真实类,而不是一个抽象类。 -
容器的默认包装已被删除;所有包装参数必须提供给 API。
-
一些标准对话框已被删除。您必须创建自己的对话框来替换它们。
-
有两个新的主要类对于大型和小型应用的整体控制非常有用:
Gtk.Application
类和Gtk.ApplicationWindow
类。虽然简单的应用并不一定需要这些类,但是即使对于最简单的应用来说,它们仍然很有用。因此,我们将本书中的所有示例都基于这两个类来包装我们的小部件示例。
使用Gtk.Application
和Gtk.ApplicationWindow
类创建菜单要容易得多。这需要在 GTK+ 2.x 环境中进行复杂的编程,并简化为创建一个 XML 文件来表示您想要在 3.x 环境中创建的菜单。
安装 GTK+ 3.x
在创建程序之前,必须安装 Python、GTK+ 和所有相关的库。本节介绍在 Linux 和其他类似 Unix 的操作系统上安装 GTK+。它不包括如何在 macOS 或 Windows 上安装 GTK+。您需要研究在那些操作系统环境中安装 GTK+ 和 Python 的正确方法。
大多数现代 Linux 发行版都包含 Python 和 GTK+ 作为它们各自仓库的一部分。您只需要从您的 Linux 发行版的包安装程序中选择 Python 3(这有时是默认安装的)和 GTK+ 3.x(使用可用的最新版本,如图 1-1 所示),然后安装这些包以及所有依赖包。
要测试您的安装,请运行以下命令。
图 1-1
GTK+ 3 演示程序
/usr/bin/gtk3-demo
如果程序存在并且小部件文档窗口出现,那么 GTK+ 安装成功。
摘要
本章介绍了 GTK+3 . x 版和 Python 3 以及一些安装先决条件。它展示了一些安装后测试,以确保 GTK+ 成功安装。并且讨论了 GTK+ 2.x 和 3.x 之间的一些差异。
成功安装 GTK+ 3.x 和 Python 3 之后,您的环境应该可以构建您的第一个 Python/GTK+ 程序了。
第二章进一步讨论了Gtk.Application
和Gtk.ApplicationWindow
,所有 Python 3 GTK+ 3.x 程序都应该使用的基类。
二、Application
和ApplicationWindow
类
GTK+ 3.x 中引入了一组新的类:Gtk.Application
和Gtk.ApplicationWindow
。这些类被设计成 GUI 应用的基本实例。它们包装了应用和应用的主窗口行为。它们有许多内置特性,并为应用中的函数提供容器。本章将详细描述Gtk.Application
和Gtk.ApplicationWindow
类,因为它们是本书中所有示例程序的基础。
Gtk。应用类别
Gtk.Application
是 GTK 应用的基类。它的主要目的是将你的程序从 Python __main__
函数中分离出来,这是一个 Python 实现细节。Gtk.Application
的理念是,应用感兴趣的是被告知需要发生什么以及什么时候需要发生来响应用户的动作。Python 启动应用的确切机制并不有趣。
公开一组应用应该响应的信号(或虚拟方法)。
-
startup
:首次启动时设置应用。这个信号的虚拟方法名是do_startup
。 -
shutdown
:执行关机任务。这个信号的虚拟方法名是do_shutdown
。 -
activate
:显示应用默认的第一个窗口(像一个新文档)。这个信号的虚拟方法名是do_activate
。 -
open
:打开文件并在新窗口中显示。这对应于某人试图从文件浏览器或类似的地方使用该应用打开一个(或多个)文档。这个信号的虚拟方法名是do_open
。
当您的应用启动时,会触发startup
信号。这使您有机会执行与显示新窗口没有直接关系的初始化任务。此后,根据应用的启动方式,接下来会调用activate
或open
信号。
信号名称和接收方法名称不应相同。接收方法名应该有一个on_
前缀。例如,一个名为paste
的信号应该有一个类似如下的connect
调用。
action = Gio.SimpleAction.new("paste", None)
action.connect("activate", self.on_paste)
self.add_action(action)
注意,您必须指定新的信号名和相应的方法名。按照 GTK+ 3.x 中的惯例,构建到现有类中的信号在它们对应的方法名前有一个do_
前缀。回调应该有带前缀on_
的方法名。为方法名添加前缀可以防止无意中覆盖不属于信号机制的方法名。
Gtk.Application
默认应用是单实例的。如果用户试图启动单实例应用的第二个实例,那么Gtk.Application
会向第一个实例发出信号,您会收到额外的activate
或open
信号。在这种情况下,第二个实例立即退出,不调用startup
或shutdown
信号。
出于这个原因,你应该从 Python 的__main__
函数中不做任何工作。所有启动初始化都应在Gtk.Application do_startup
中完成。这避免了在程序立即退出的第二种情况下浪费工作。
只要需要,应用就会继续运行。这通常是只要有任何打开的窗口。您还可以通过使用hold
方法来强制应用保持活动状态。
关机时,您会收到一个shutdown
信号,在这里您可以执行任何必要的清理任务(比如将文件保存到磁盘)。
Gtk.Application
不为你实现__main__
;你必须自己做。你的__main__
函数应该尽可能的小,除了创建并运行你的Gtk.Application
之外几乎什么都不做。“真正的工作”应该总是在回应Gtk.Application
发出的信号时进行。
主实例与本地实例
应用的主实例是运行的第一个实例。一个远程实例是一个已经启动但不是主实例的实例。术语本地实例是指当前进程,它可能是也可能不是主实例。
Gtk.Application
仅在主实例中发出信号。对the Gtk.Application
API 的调用可以在主实例或远程实例中进行(并且是从本地实例的角度进行的)。当本地实例是主实例时,对Gtk.Application
的方法调用会导致信号在本地发出。当本地实例是远程实例时,方法调用导致消息被发送到主实例,并且信号在那里发出。
例如,在主实例上调用do_activate
方法会发出activate
信号。在远程实例上调用它会导致一条消息被发送到主实例,并发出activate
信号。
您很少需要知道本地实例是主实例还是远程实例。几乎在所有情况下,您都应该调用您感兴趣的Gtk.Application
方法,并根据需要将其转发或本地处理。
行动
除了默认的activate
和open
动作之外,应用还可以注册一组它支持的动作。动作通过GActionMap
接口添加到应用中,并通过GActionGroup
接口调用或查询。
与activate
和open
信号一样,在主实例上调用activate_action
会激活当前流程中的指定动作。在远程实例上调用activate_action
会向主实例发送一条消息,导致动作在那里被激活。
处理命令行
通常,Gtk.Application
假设在命令行上传递的参数是要打开的文件。如果没有参数被传递,那么它假设一个应用被启动来显示它的主窗口或者一个空文档。给定文件时,你从 open 信号接收这些文件(以GFile
的形式);否则,你会收到一个activate
信号。建议新应用利用命令行参数的这种默认处理方式。
如果您想以更高级的方式处理命令行参数,有几种(互补的)机制可以实现这一点。
首先,发出handle-local-options
信号,信号处理器获得一个包含解析选项的字典。为了利用这一点,您需要用add_main_option_entries
方法注册您的选项。信号处理程序可以返回一个非负值,以此退出代码结束进程,或者返回一个负值,以继续命令行选项的常规处理。这个信号的一个常见用途是实现一个--version
参数,它不需要与远程实例通信。
如果handle-local-options
对于您的需求不够灵活,您可以覆盖local_command_line
虚函数来完全接管本地实例中命令行参数的处理。如果你这样做,你需要负责注册应用和处理一个--help
参数(默认的local_command_line
函数会为你做这件事)。
也可以从handle-local-options
或local_command_line
调用动作来响应命令行参数。例如,邮件客户端可以选择将--compose
命令行参数映射到对其compose
动作的调用。这是通过从local_command_line
实现中调用activate_action
来完成的。如果正在处理的命令行在主实例中,那么本地调用compose
动作。如果是远程实例,动作调用将被转发到主实例。
请特别注意,可以使用动作激活来代替activate
或open
。一个应用可以在没有发出activate
信号的情况下启动,这是完全合理的。activate
只应该是默认的"无选项启动"信号。动作是用来做其他事情的。
一些应用可能希望执行更高级的命令行处理,包括控制远程实例的生命周期及其退出后的退出状态,以及转发命令行参数、环境和转发stdin/stdout/ stderr
的全部内容。这可以通过使用HANDLES_COMMAND_LINE
选项和command-line
信号来实现。
例子
清单 2-1 提供了一个从Gtk.Application
类派生的实例的简单例子。
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, **kwargs)
self.window = None
self.add_main_option("test", ord("t"), GLib.OptionFlags.NONE, GLib.OptionArg.NONE, "Command line test", None)
def do_startup(self):
Gtk.Application.do_startup(self)
action = Gio.SimpleAction.new("quit", None)
action.connect("activate", self.on_quit)
self.add_action(action)
def do_activate(self):
# We only allow a single window and raise any existing ones if not self.window:
# Windows are associated with the application
# when the last one is closed the application shuts down self.window = AppWindow(application=self, title="Main Window")
self.window.present()
def do_command_line(self, command_line):
options = command_line.get_options_dict()
if options.contains("test"):
# This is printed on the main instance
print("Test argument received")
self.activate()
return 0
Listing 2-1An Example of the Gtk.Application Class
这个例子是一个非常简单的Gtk.Application
类的实例。随着你对 GTK+ 3.x 的了解,这个例子将在本书中得到加强。
示例的第 23 行显示了如何创建一个Gtk.ApplicationWindow
类的实例。
下一节概述了Gtk.ApplicationWindow
类。
Gtk。应用窗口类
Gtk.ApplicationWindow
类是应用的主可见窗口。在默认情况下,这是用户唯一可见的主窗口,除非应用已经设置为“多实例”(默认为“单实例”)。
Gtk.ApplicationWindow
是Gtk.Window
的子类,提供额外的功能,以便更好地与Gtk.Application
特性集成。值得注意的是,它可以处理应用菜单和菜单栏(见Gtk.Application.set_app_menu()
和Gtk.Application.set_menubar()
)。
当Gtk.ApplicationWindow
与Gtk.Application
类配合使用时,这两个类之间有密切的关系。这两个类都创建了新的动作(信号),任一类都可以执行这些动作。但是Gtk.ApplicationWindow
类负责窗口中包含的小部件的全部功能。应该注意的是,Gtk.ApplicationWindow
类还为激活相关Gtk.Application
类的do_quit
方法的delete-event
创建了一个连接。
行动
Gtk.ApplicationWindow
类实现了Gio.ActionGroup
和Gio.ActionMap
接口,允许您添加由关联的Gtk.Application
导出的特定于窗口的动作及其应用范围的动作。窗口特定的动作以win
为前缀。前缀和应用范围的操作以app.
前缀为前缀。从Gio.MenuModel
引用动作时,必须使用前缀名称。
注意,如果实现了Gtk.Actionable
接口,放置在Gtk.ApplicationWindow
类中的小部件也可以激活这些动作。
锁
与Gtk.Application
一样,当处理动作从其他进程到达时,GDK 锁被获取,因此在本地激活动作时应该被持有(如果 GDK 线程被启用)。
例子
清单 2-2 是Gtk.Application
类和Gtk.ApplicationWindow
类之间集成的一个非常简单的版本。这个例子成为本书中所有后续例子的基础。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GLib, Gio, Gtk
# This would typically be its own
file MENU_XML="""
<?xml version="1.0" encoding="UTF-8"?> <interface>
<menu id="app-menu">
<section>
<attribute name="label" translatable="yes">Change label</attribute> <item>
<attribute name="action">win.change_label</attribute>
<attribute name="target">String 1</attribute>
<attribute name="label" translatable="yes">String 1</attribute> </item>
<item>
<attribute name="action">win.change_label</attribute>
<attribute name="target">String 2</attribute>
<attribute name="label" translatable="yes">String 2</attribute> </item>
<item>
<attribute name="action">win.change_label</attribute>
<attribute name="target">String 3</attribute>
<attribute name="label" translatable="yes">String 3</attribute> </item>
</section>
<section>
<item>
<attribute name="action">win.maximize</attribute>
<attribute name="label" translatable="yes">Maximize</attribute> </item>
</section>
<section>
<item>
<attribute name="action">app.about</attribute>
<attribute name="label" translatable="yes">_About</attribute>
</item>
<item>
<attribute name="action">app.quit</attribute>
<attribute name="label" translatable="yes">_Quit</attribute> <attribute name="accel"><Primary>q</attribute>
</item>
</section>
</menu>
</interface>
"""
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# This will be in the windows group and have the "win" prefix
max_action = Gio.SimpleAction.new_stateful("maximize", None,
GLib.Variant.new_boolean(False))
max_action.connect("change-state", self.on_maximize_toggle)
self.add_action(max_action)
# Keep it in sync with the actual state
self.connect("notify::is-maximized",
lambda obj, pspec: max_action.set_state(
GLib.Variant.new_boolean(obj.props.is_maximized)))
lbl_variant = GLib.Variant.new_string("String 1")
lbl_action = Gio.SimpleAction.new_stateful("change_label",
lbl_variant.get_type(),
lbl_variant)
lbl_action.connect("change-state", self.on_change_label_state)
self.add_action(lbl_action)
self.label = Gtk.Label(label=lbl_variant.get_string(),
margin=30)
self.add(self.label)
def on_change_label_state(self, action, value):
action.set_state(value)
self.label.set_text(value.get_string())
def on_maximize_toggle(self, action, value):
action.set_state(value)
if value.get_boolean():
self.maximize()
else:
self.unmaximize()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
**kwargs)
self.window = None
self.add_main_option("test", ord("t"),
GLib.OptionFlags.NONE, GLib.OptionArg.NONE, "Command line test", None)
def do_startup(self):
Gtk.Application.do_startup(self)
action = Gio.SimpleAction.new("about", None)
action.connect("activate", self.on_about)
self.add_action(action)
action = Gio.SimpleAction.new("quit", None)
action.connect("activate", self.on_quit)
self.add_action(action)
builder = Gtk.Builder.new_from_string(MENU_XML, -1)
self.set_app_menu(builder.get_object("app-menu"))
def do_activate(self):
# We only allow a single window and raise any existing ones if not self.window:
# Windows are associated with the application
# When the last one is closed the application shuts down self.window = AppWindow(application=self, title="Main Window")
self.window.present()
def do_command_line(self, command_line):
options = command_line.get_options_dict()
if options.contains("test"):
# This is printed on the main instance
print("Test argument received")
self.activate()
return 0
def on_about(self, action, param):
about_dialog = Gtk.AboutDialog(transient_for=self.window, modal=True)
about_dialog.present()
def on_quit(self, action, param):
self.quit()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 2-2An Example of the Gtk.Application and the Gtk.ApplicationWindow Classes
这个例子是一个完整的程序,应该从命令行运行。它修改命令行窗口,并添加一个菜单来控制应用。大多数菜单选项都是非功能性的示例,但是对于解释菜单操作如何操作以及哪个类执行由菜单 XML 文件指定的操作非常有用。
最上面的三行指定了 Python 程序的环境。第 5 行将 Python 环境建立为版本 3.x。这是运行 GTK 3.x 的所有 Python 程序所必需的。接下来的几行将建立 Python 和 GTK 导入。它专门导入 GTK 3.x 导入库。确保使用gi
接口导入模块,这样您就拥有了最新的模块,因为您的系统上可能安装了不止一组模块。
接下来的几行描述了菜单 XML 接口。每个菜单项由三个 XML 属性之一描述。首先是action
属性。它命名一个动作,名字前缀指定哪个类接收动作信号。一个app
前缀意味着Gtk.Application
处理动作信号。一个win
前缀意味着Gtk.ApplicationWindow
处理信号。第二个属性是target
,它指定显示在菜单项中的字符串。第三个属性是label
,它指定是否应该翻译目标属性字符串。
通常,这些 XML 信息存储在自己的文件中,并在运行时读取,但是为了简化示例,我们将它们内联在一起。
接下来的几行描述了Gtk.ApplicationWindow
子类AppWindow
,它封装了主窗口行为和所有主窗口小部件。在本例中,主窗口中没有包含任何小部件。它只拦截来自菜单的动作信号,并对这些信号进行操作。
关于菜单信号方法需要注意的主要事情是,它们与菜单 XML 中指定的名称相同,但是带有一个on_
前缀。接下来的几行将四个窗口操作中的两个变成自动切换。接下来的几行将另外两个信号作为方法调用捕获。
Gtk.Application
子类Application
封装了应用的行为。它提供应用启动和命令行处理,并处理两个菜单 XML 信号。与由Gtk.ApplicationWindow
处理的方法一样,Gtk.Application
方法名有一个on_
前缀。
首先,Gtk.Application
子类的初始化调用超类来初始化它,然后设置一个新的命令行选项。
接下来的几行执行类的激活活动,并创建Gtk.ApplicationWindow
子类。
接下来,在 XML 菜单中定义了两个信号方法,它们被指定给Gtk.Application
子类。
底部是 Python 程序的实际开始。这里应该做的唯一工作是创建Gtk.Application
的类或子类。
摘要
本章介绍了Gtk.Application
和Gtk.ApplicationWindow
类以及这两个类的集成。我们讨论了这些类如何改进您的应用,使其更加面向对象。这些类还可以提高应用的可读性和可维护性。
在随后的章节中,我们将介绍大多数其他 GTK+ 小部件,同时使用本章中介绍的类作为将小部件集成到示例程序中的基础。
三、一些简单的 GTK+ 应用
本章介绍了一些简单的 GTK+ 应用和一些 GTK+ 小部件。我们涵盖了在接下来的章节和示例应用中使用的主题。
本章涵盖了以下概念。
-
所有 GTK+ Python 应用使用的基本函数和方法调用
-
GTK+ widget 系统的面向对象特性
-
信号、回调和事件在应用中扮演的角色
-
如何用 Pango 文本标记语言改变文本样式
-
小部件的一些有用的函数和方法
-
如何制作可点击的标签
-
如何使用小部件方法获取和设置属性
重要的是你要掌握这些概念,这样你就有了一个合适的基础。
你好世界
几乎世界上所有的编程语言书籍都是以 Hello World 示例开始的。虽然这本书没有什么不同,但它使用的例子比大多数其他语言的例子更复杂。这是因为我们的例子基于Gtk.Application
和Gtk.ApplicationWindow
类。这使得示例程序有点长,而且乍一看,对于这样一个简单的 GTK+ 窗口来说有些夸张。但是它也很好地解释了 GTK+ 是如何工作的,以及 Python 绑定是如何将 API 包装成一个非常好的面向对象系统的。
清单 3-1 是本书中最简单的应用之一,但是它提供了解释 GTK+ 应用应该如何组织以及 GTK+ 小部件层次结构如何工作的基础。这是你用 Python 创建的每一个 GTK+ 应用都应该有的基本代码!
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Main Window")
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 3-1HelloWorld.py
Figure 3-1 contains everything you need for a complete GTK+ 3.x Python program.
图 3-1
HelloWorld.py
如果您以前有过 GTK+ 的经验,您可能会注意到缺少一些 GTK+ 2.x 的通用元素。我们在第 1 行显式地将它设为 Python 3 程序。这是必要的,因为 GTK+ 3.x 模块仅在 Python 版本 3.x 中可用。该声明允许第 4–6 行正确地建立 GTK+ 3.x 环境。
第 8–11 行支持可见的 GTK+ 窗口。我们需要支持这个应用的唯一活动是调用super
类来初始化它。但是好像少了一些活动!所有这些缺失的元素要么包含在Gtk.ApplicationWindow
超类中,要么在Gtk.Application
类中得到支持。一个默认的支持动作将delete-event
连接到一个默认的方法来退出应用。
第 13–23 行支持应用逻辑。在我们的子类中定义了Gtk.Application
类的四个默认方法之一。do_activate
方法执行所需的激活活动。
当应用被激活时(启动后)调用do_activate
。在这种情况下,需要两个函数。首先,我们检查这是否是对该方法的初始调用,如果是,我们创建Application
GTK+ window 实例。其次,我们激活并显示(呈现)主应用窗口。
第 25–27 行是启动我们的应用所需的唯一 Python 语句。不需要其他陈述,事实上,也不应该添加任何陈述。所有的应用工作应该发生在Gtk.Application
类或者Gtk.ApplicationWindow
类或者它们的子类中。这可以防止试图启动另一个应用实例的“单实例”应用发生任何不必要的工作。
GTK+ 小部件层次结构
GTK+ 应用编程接口实际上是一个 C 语言 API。然而,它是以这样一种方式组织的,像 Python 这样的面向对象语言可以包装 C API,这样整个 API 集就变成了一组以层次结构组织的类。
从 GTK+ 2.x 到 3.x 的转变有助于其他语言创建更易于维护和实现的面向对象的绑定。例如,虽然 Python 2.x 支持抽象类,但它们隐藏在集合类中,很难在您自己的代码中实现。Python 3.3 提供了collections.abc
模块,这使得你可以很容易地在模块中子类化类来创建你自己的抽象类。此外,GTK+ 3.x API 极大地减少了抽象类的数量。未来很可能全部被淘汰。
GTK+ 3.x 对象层次结构由 PyGObject API 参考( http://lazka.github.io
/ pgi-docs/#Gtk-3.0
)文档记录。这是 Python GTK+ 3.x 参考文档。它涵盖了您需要了解的关于 Python 对象绑定到 GTK+ 的一切,包括对象层次结构、支持的类、接口、函数、方法和属性。虽然该文档基本上是全面的,但它缺少关于一些新类的信息。我们希望这本书能提供这些信息,以及如何使用所有小部件和类的优秀例子。
虽然理解 GTK+ 的层次结构很重要,但是如果只是肤浅地理解,仍然有可能创建良好的 GUI 应用。但是你越了解层次结构,你就能更好地控制你的应用。
扩展 HelloWorld.py
即使清单 3-1 是一个完整的应用,但显然它不是很有用。因此,让我们添加有用的特性和方法调用,为我们的应用提供可视信息和视觉吸引力(参见清单 3-2 )。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
label = Gtk.Label.new("Hello World!")
label.set_selectable(True)
self.add(label)
self.set_size_request(200, 100)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Hello World!")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 3-2HelloWorld with Label
图 3-2 是运行清单 3-2 的结果。请注意,标签已经突出显示。
图 3-2
带标签的 HelloWorld
我们现在有了一个显示数据的应用,因此更加有用。让我们来看看为了达到这个结果我们对程序所做的修改。
第 12–15 行是大部分修改的地方。在第 12 行,我们用文本“Hello World!”创建了Gtk.Label
包含在其中。在第 13 行,我们使文本可选。这允许用户选择文本并将其复制到剪贴板。在第 14 行,我们将标签添加到默认容器中。GTK+ 中的所有主窗口都是从Gtk.Container
派生出来的,所以可以向该容器添加小部件。第 15 行调整大小Gtk.ApplicationWindow
。
第 27 行显示了Gtk.ApplicationWindow
包含的所有小部件。我们需要这个方法调用,因为present
方法不执行那个功能。它只显示主窗口。
这些是对清单 3-1 所做的唯一更改。正如您所看到的,向 Python GTK+ 应用添加新功能并不费力。
GTK。标签小部件
在清单 3-2 中创建了一个GTK.Label
小部件。这是通过以下调用完成的。
label = Gtk.Label.new("Hello World!")
该调用创建一个包含指定文本的新标签。文本可能包含 Python 转义序列(比如"\n"
),GTK+ 用它来格式化屏幕上的文本。
GTK.Label
支持许多有用的方法。以下是一些更有用的列表。
-
set_selectable
:这个方法打开/关闭文本的选择性。默认为关闭。这对于像错误消息这样的事情非常有用,用户可能希望将文本复制到剪贴板。 -
set_text
:该方法用指定的新文本替换当前标签文本。 -
set_text_with_mnemonic
:该方法用指定的新文本替换当前标签文本。新文本中可能包含也可能不包含助记符。如果文本中的字符前面有一个下划线,它们就有下划线,这表明它们代表一个叫做助记符的键盘快捷键。助记键可用于激活另一个小部件,可以自动选择,也可以使用Gtk.Label.set_mnemonic_widget
明确选择。 -
get_text
:该方法获取当前标签文本。
布局容器
Gtk.ApplicationWindow
和Gtk.Window
类都是从Gtk.Container
小部件间接派生的。这意味着Gtk.Container
中的所有方法对于派生的窗口都是可用的。
通过使用add
方法,可以将小部件或其他容器类型添加到主窗口中。这就是如何将GTK.Label
添加到主窗口中。当您将一个小部件添加到一个容器中时,就会形成一种父/子关系;容器成为父容器,标签成为容器的子容器。
小部件之间的父/子关系在 GTK+ 中非常重要,原因有很多。例如,当一个父窗口小部件被销毁时,GTK+ 递归地销毁所有子窗口小部件,不管它们的嵌套有多深。
容器也有默认的大小调整算法。这可能是好消息,也可能是坏消息。在许多情况下,默认的大小正是您想要的;但很多时候,并不是这样。您可以通过调整主窗口的大小来覆盖默认大小。
容器的另一个调整助手是set_border_width
方法。它允许您在文本周围创建一个边框,以便当用户手动缩小窗口时,窗口具有由文本大小和边框宽度决定的最小尺寸。
在第四章中有更多关于容器和布局的信息。
信号和回调
GTK+ 是一个依赖于信号和回调方法的系统。信号是通知你的应用用户已经执行了一些动作。您可以告诉 GTK+ 在发出信号时运行一个方法或函数。这些被称为回调方法 / 函数。
警告
GTK+ 信号与 POSIX 信号不同!GTK+ 中的信号通过来自 X 窗口系统的事件传播。每一个都提供了独立的方法。这两种信号类型不应互换使用。
初始化用户界面后,控制权通过Gtk.Application
类实例交给gtk_main()
函数,它会一直休眠,直到发出信号。此时,控制权被传递给其他方法/函数。
作为程序员,您将信号连接到它们的方法/回调函数。当动作已经发生并且发出信号时,或者当您已经显式发出信号时,回调方法/函数被调用。你也有能力阻止信号的发射。
注意
可以在应用中的任何点连接信号。例如,新信号可以在回调方法/函数中连接。然而,在调用Gtk.Application
实例中的gtk_main()
或present()
方法之前,您应该尝试初始化关键任务回调。
信号有很多种类型,就像函数一样,它们是从父结构继承来的。许多信号是所有微件通用的,如"hide"
和"grab-focus"
,或者是微件专用的,如Gtk.RadioButton
信号"group-changed"
。在任何情况下,从一个类派生的小部件都可以使用它的所有祖先可以使用的所有信号。
连接信号
我们连接到信号的第一个例子是从主窗口截取"destroy"
信号,这样我们可以选择如何处理该信号。我们自己处理这个信号的一个主要原因是在 GTK+ 系统自动破坏窗口之前执行一个动作。
widget.connect("destroy", self.on_window_destroy, extra_param)
当widget.destroy()
在小部件上被调用或者当False
从delete_event()
回调方法/函数返回时,GTK+ 发出"destroy"
信号。如果您参考 API 文档,您会看到销毁信号属于Gtk.Object
类。这意味着 GTK+ 中的每个类都继承了信号。任何 GTK+ 结构/实例的销毁都会通知你。
每个connect()
调用都有两个必需的参数。第一个是您想要跟踪的信号的名称。每个小部件都有许多可能的信号,所有这些都可以在 API 文档中找到。请记住,小部件可以自由使用其祖先的信号,因为每个小部件实际上都是其祖先的实现。您可以使用 API 的“对象层次”部分来引用父类。
widget.connect("signal_name", function_name, extra_param)
键入信号名称时,下划线和破折号字符可以互换。它们被解析为相同的字符,因此您选择哪一个都没有任何区别。我在本书的所有例子中都使用了下划线。
connect()
方法中的第二个参数是发出信号时调用的回调方法/函数。回调方法/函数的格式取决于每个特定信号的函数原型要求。下一节将展示一个回调方法的例子。
connect()
方法中的最后一个参数允许您向回调方法/函数发送额外的参数。与 C 版本的g_signal_connect()
函数不同,Python 版本的connect()
方法调用允许您为回调方法/函数传递任意多的额外参数。这非常有用,因为它防止了人为地创建一个单独的变量容器,该容器包装了许多您希望传递给回调/方法的变量/类。
在这个connect()
实例中,一个标签被传递给回调方法。
widget.connect("destroy", self.on_window_destroy, label)
connect()
的返回值是信号的处理程序标识符。您可以将此与GObject.signal_handler_block()
、GObject.signal_handler_unblock()
和GObject.signal_handler_disconnect()
一起使用。这些函数分别停止调用回调方法/函数,重新启用回调函数,以及从小部件的处理程序列表中删除信号处理程序。更多信息在 API 文档中。
回调方法/函数
在connect()
中指定的回调方法/函数在信号被发送到它所连接的小工具时被调用。对于除事件之外的所有信号,回调方法/函数的形式如下。
# a callback function
def on_window_destroy(widget, extra_arg)
# a callback method
def on_window_destroy(self, widget, extra_arg)
您可以在 API 文档中找到每个信号的回调方法/函数的示例格式,但这是通用格式。widget
参数是执行connect()
调用的对象。
其他可能的必需参数也可能出现在中间,尽管情况并不总是如此。对于这些参数,您需要参考您正在使用的信号的文档。
您的回调方法/函数的最后一个参数对应于connect()
的最后一个参数。请记住,这些可选参数的数量可以根据您的需要而定,但是来自connect()
调用的额外参数的数量和回调方法/函数定义中的额外参数的数量必须相同。
您还应该注意,回调的方法版本的第一个参数是 Python 在方法定义中所需的self
参数;否则,函数和方法定义是相同的。
事件
事件是由 X 窗口系统发出的特殊类型的信号。它们最初由 X 窗口系统发出,然后从窗口管理器发送到您的应用,由 GLib 提供的信号系统进行解释。例如,"destroy"
信号在小部件上发出,但是"delete-event"
事件首先被小部件的底层Gdk.Window
识别,然后作为小部件的信号发出。
您遇到的第一个事件实例是"delete-event"
。当用户试图关闭窗口时,发出"delete-event"
信号。可以通过单击标题栏上的关闭按钮、使用任务栏中的关闭弹出菜单项或通过窗口管理器提供的任何其他方式来退出窗口。
使用connect()
将事件连接到回调函数的方式与其他 GTK + 信号相同。但是,回调函数的设置略有不同。
# an event callback function
def on_window_destroy(widget, event, extra_arg)
# an event callback method
def on_window_destroy(self, widget, event, extra_arg)
回调方法/函数的第一个区别是布尔返回值。如果从事件回调中返回True
, GTK+ 假定事件已经被处理,它不会继续。通过返回False
,您告诉 GTK+ 继续处理事件。False
是函数的默认返回值,所以在大多数情况下不需要使用"delete-event"
信号。这只有在您想要覆盖默认信号处理程序时才有用。
例如,在许多应用中,您可能希望确认程序的退出。通过使用下面的代码,如果用户不想退出,您可以阻止应用退出。
# an event callback method
def on_delete_event(self, widget, event, extra_arg):
answer = # Ask the user if exiting is desired.
if answer:
return False
else:
return True
通过从"delete-event"
回调函数返回False
,在小部件上自动调用widget.destroy()
。这个信号会自动继续动作,所以不需要连接它,除非您想覆盖默认设置。
此外,回调函数包括Gdk.Event
参数。Gdk.Event
是Gdk.EventType
枚举和所有可用事件结构的并集。我们先来看一下Gdk.EventType
枚举。
事件类型
Gdk.EventType
枚举提供了可用事件类型的列表。这些可以用来确定已经发生的事件的类型,因为您可能并不总是知道发生了什么。
例如,如果您将"button-press-event"
信号连接到一个小部件,有三种不同类型的事件可以导致信号的回调函数运行:Gdk.EventType.BUTTON_PRESS
、Gdk.EventType.2BUTTON_PRESS
和Gdk.EventType.3BUTTON_PRESS
。双击和三击也发出Gdk.EventType.BUTTON_PRESS
作为第二个事件,所以能够区分不同类型的事件是必要的。
附录 B 提供了您可以参加的活动的完整列表。它显示了传递给connect()
的信号名称、Gdk.EventType
枚举值和事件描述。
我们来看一下"delete-event"
回调函数。我们已经知道"delete-event"
是Gdk.EventType.DELETE
的类型,但是让我们假设我们不知道。我们可以通过使用下面的条件语句很容易地测试这一点。
def delete_event(self, window, event, data):
if event.type == Gdk.EventType.DELETE:
return False
return True
本例中,如果事件类型为Gdk.EventType.DELETE
,则返回False
,并在小部件上调用widget.destroy()
;否则,返回True
,不采取进一步的行动。
使用特定的事件结构
有时,您可能已经知道发出了哪种类型的事件。在下面的例子中,我们知道总是发出一个"key-press-event"
。
widget.connect("key-press-event", on_key_press)
在这种情况下,可以安全地假设事件的类型总是Gdk.EventType.KEY_PRESS
,回调函数也可以这样声明。
def on_key_press(widget, event):
因为我们知道事件的类型是一个Gdk.EventType.KEY_PRESS
,我们不需要访问Gdk.Event
中的所有结构。我们只使用了Gdk.EventKey
,我们可以在回调方法/函数中使用它来代替Gdk.Event
。由于事件已经被转换为Gdk.EventKey
,我们只能直接访问该结构中的元素。
Gdk.EventKey.type # GDK_KEY_PRESS or GDK_KEY_RELEASE
Gdk.EventKey.window # The window that received the event
Gdk.EventKey.send_event # TRUE if the event used XSendEvent
Gdk.EventKey.time # The length of the event in milliseconds
Gdk.EventKey.state # The state of Control, Shift, and Alt
Gdk.EventKey.keyval # The key that was pressed
Gdk.EventKey.length # The length of string
Gdk.EventKey.string # A string approximating the entered text
Gdk.EventKey.hardware_keycode # Raw code of the key that was pressed or released
Gdk.EventKey.group # The keyboard group
Gdk.EventKey.is_modifier # Whether hardware_keycode was mapped
我们在整本书中使用的Gdk.EventKey
结构中有许多有用的属性。在某些时候,浏览 API 文档中的一些Gdk.Event
结构会很有用。我们涵盖了本书中一些最重要的结构,包括Gdk.EventKey
和Gdk.EventButton
。
所有事件结构中唯一可用的变量是事件类型,它定义了已发生事件的类型。总是检查事件类型以避免以错误的方式处理它是一个好主意。
其他 GTK+ 方法
在继续学习更多的例子之前,我想提醒您注意一些函数,这些函数将在后面的章节中以及在您创建自己的 GTK+ 应用时派上用场。
Gtk。小部件方法
Gtk.Widget
结构包含许多有用的函数,您可以将其用于任何小部件。这一部分概述了您在许多应用中需要的一些。
通过在对象上显式调用widget.destroy()
可以销毁一个小部件。被调用时,widget.destroy()
递归地删除小部件及其所有子部件上的引用计数。然后,小部件及其子部件被销毁,所有内存被释放。
widget.destroy()
通常,这仅在顶级小部件上调用。它通常只用于销毁对话框窗口和实现退出应用的菜单项。在下一个示例中,它用于在单击按钮时退出应用。
您可以使用widget.set_size_request()
来设置小部件的最小尺寸。它会强制小部件比正常情况下更小或更大。但是,它不会调整小部件的大小,使其太小而无法正常工作或在屏幕上自行绘制。
widget.set_size_request(width, height)
通过向任一参数传递-1,您告诉 GTK+ 使用其自然大小,或者如果您没有定义自定义大小,则使用小部件通常被分配的大小。如果您只想指定高度或宽度参数,则使用此选项。它还允许您将小部件重置为其原始大小。
没有办法设置宽度或高度小于 1 个像素的小部件,但是通过向任一参数传递 0,GTK+ 使小部件尽可能小。同样,它不会被调整得太小,以至于不能正常工作或不能自己绘图。
由于国际化,设置任何小部件的大小都有危险。文本在您的计算机上可能看起来很棒,但是在使用应用德语翻译的计算机上,小部件对于文本来说可能太小或太大。主题也带来了小部件大小的问题,因为小部件默认为不同的大小,这取决于主题。所以大多数情况下最好允许 GTK+ 选择小部件和窗口的大小。
您可以使用widget.grab_focus()
来强制小部件获取键盘焦点。这只适用于可以处理键盘交互小部件。使用widget.grab_focus()
的一个例子是当搜索工具栏显示在 Firefox 中时,将光标发送到一个文本条目。它也可以用来给可选择的Gtk.Label
一个焦点。
widget.grab_focus()
通常,您希望将一个小部件设置为非活动的。通过调用widget.set_sensitive()
,指定的小部件及其所有子部件被禁用或启用。通过将小部件设置为不活动,用户被阻止与小部件交互。大多数小部件在设置为非活动状态时也是灰色的。
widget.set_sensitive(boolean)
如果您想要重新启用一个小部件及其子部件,您只需要在同一个小部件上调用这个方法。孩子受父母敏感程度的影响,只是反映了父母的设定,而不是改变他们的属性。
Gtk。窗口方法
现在您已经看到了两个使用Gtk.Window
类的例子。您了解了如何设置窗口标题和添加子窗口小部件。现在,让我们探索更多的功能,让您进一步定制窗口。
默认情况下,所有窗口都设置为可调整大小。这在大多数应用中都是可取的,因为每个用户都有不同的尺寸偏好。但是,如果有特殊的原因,你可以使用window.set_resizable()
来阻止用户调整窗口的大小。
window.set_resizable(boolean)
警告
你应该注意到调整大小的能力是由窗口管理器控制的,所以这个设置并不是在所有情况下都有效!
前面的警告引出了重要的一点。GTK+ 所做的大部分事情都与窗口管理器提供的功能相互作用。因此,并非所有窗口管理器都遵循您的所有窗口设置。这是因为您的设置仅仅是被使用或忽略的提示。您应该记住,在使用 GTK+ 设计应用时,您的请求可能会被接受,也可能不会被接受。
可以用window.set_default_size()
设置Gtk.Window
的默认大小,但是在使用这种方法时有一些事情需要注意。如果窗口的最小尺寸大于您指定的尺寸,GTK+ 会忽略这个方法。如果您之前设置了更大的请求,也会忽略该请求。
window.set_default_size(width, height)
与widget.set_size_request()
不同,window.set_default_size()
只设置窗口的初始大小;这并不妨碍用户将其调整到更大或更小的尺寸。如果将高度或宽度参数设置为 0,则窗口的高度或宽度将设置为可能的最小尺寸。如果将–1 传递给任一参数,窗口将被设置为其自然大小。
您可以用window.move()
请求窗口管理器将窗口移动到指定位置;但是,窗口管理器可以忽略这个请求。这适用于所有需要窗口管理器操作的功能。
window.move(x, y)
默认情况下,窗口在屏幕上的位置是相对于屏幕的左上角计算的,但是您可以使用window.set_gravity()
来改变这个假设。
window.set_gravity(gravity)
这个函数定义了小部件的重心,这是布局计算考虑的点(0, 0)
。Gdk.Gravity
枚举的可能值包括Gdk.Gravity.NORTH_WEST
、Gdk.Gravity.NORTH
、Gdk.Gravity.GRAVITY_NORTH_EAST
、Gdk.Gravity.WEST
、Gdk.Gravity.CENTER
、Gdk.Gravity.EAST
、Gdk.Gravity.SOUTH_WEST
、Gdk.Gravity.SOUTH
、Gdk.Gravity.SOUTH_EAST
和Gdk.Gravity.STATIC
。
北、南、东和西指的是屏幕的上、下、右和左边缘。它们用于构造多种重力类型。Gdk.Gravity.STATIC
指窗口本身的左上角,忽略窗口装饰。
如果您的应用有多个窗口,您可以用window.set_transient_for()
将其中一个设置为父窗口。这允许窗口管理器做一些事情,比如将子窗口放在父窗口的中心,或者确保一个窗口总是在另一个窗口的上面。我们在第六章中讨论对话时会探讨多窗口和瞬时关系的概念。
window.set_transient_for(parent)
你可以通过调用window.set_icon_from_file()
来设置出现在窗口任务栏和标题栏的图标。图标的大小无关紧要,因为当所需的大小已知时,它会调整大小。这允许缩放图标具有最佳质量。
window.set_icon_from_file(filename)
如果图标加载和设置成功,则返回True
。
警告
图标是一个复杂的主题,有许多复杂的行为,包括图标集、缩放和与主题的交互。更多信息参见 GTK+ 文档。
处理待定事件
有时,您可能希望处理应用中的所有未决事件。当您运行一段需要长时间处理的代码时,这非常有用。这会导致您的应用看起来冻结,因为如果 CPU 被另一个进程占用,小部件不会被重绘。例如,在我创建的名为 OpenLDev 的集成开发环境中,我必须在处理构建命令的同时更新用户界面;否则,该窗口将被锁定,并且在构建完成之前不会显示任何构建输出。
下面的循环是这个问题的解决方案。它回答了新 GTK+ 程序员提出的大量问题。
while Gtk.events_pending():
Gtk.main_iteration()
该循环调用Gtk.main_iteration()
,它为您的应用处理第一个未决事件。当Gtk.events_pending()
返回True
时,这个过程继续进行,告诉您是否有事件等待处理。
使用这个循环是解决冻结问题的简单方法,但是更好的方法是使用完全避免这个问题的编码策略。例如,只有当没有更重要的操作需要处理时,才可以使用 GLib 中的空闲函数来调用函数。
小跟班
Gtk.Button
是一种特殊的容器,只能容纳一个孩子。然而,这个子控件本身可以是一个容器,因此允许一个按钮包含多个小部件。Gtk.Button
类是可点击的实体。它可以连接到所属容器或窗口的已定义方法。
Gtk.Button
是一个动作小部件。也就是说,当它被点击时,预期会采取一个动作。程序员通过处理点击按钮时发出的信号来完全控制这个动作。所以让我们看看另一个简单的例子中Gtk.Button
是如何工作的(参见清单 3-3 )。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(25)
button = Gtk.Button.new_with_mnemonic("_Close")
button.connect("clicked", self.on_button_clicked)
button.set_relief(Gtk.ReliefStyle.NORMAL)
self.add(button)
self.set_size_request(200, 100)
def on_button_clicked(self, button):
self.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self,
title="Hello World!")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 3-3HelloWorld with Button
图 3-3 显示了运行清单 3-3 的结果。注意默认情况下按钮是如何居中的。
图 3-3
带按钮的 HelloWorld
那些有经验的 GTK+ 2.x 开发人员可能想知道为什么我们没有使用 stock 按钮。自 GTK+ 3.1 以来,Stock 按钮已被弃用,不应在新代码中使用。这可能会让人大吃一惊,因为在将 2.x 应用升级到 3.x 应用时,这会导致大量工作。但并非一切都像乍看起来那么糟糕。通过转换为非库存按钮,您的应用对于所有支持的平台变得更加可移植。
让我们详细看看按钮代码。有一些有趣的代码。
第 12 行设置稍后创建的按钮周围的边框宽度。第 13–16 行创建按钮,并将其连接到Gtk.Application
实例中的一个方法。第 13 行创建了一个带有助记标签"_Close"
的按钮。下划线表示字母 C 是助记符。当用户按 Alt+C 时,按钮发出"clicked"
信号。
第 14 行将按钮产生的"clicked"
信号连接到Gtk.ApplicationWindow
实例中的on_button_clicked
方法。它通过从kwargs
参数中获取实例来实现这一点。字典名application
在第 28 行被赋予一个值,该值在第 14 行被获取,指向正确的Gtk.Application
实例方法。
您可能想知道为什么我们没有将按钮信号连接到Gtk.ApplicationWindow
类的本地方法。这是因为退出应用的信号属于Gtk.Application
类,而不是Gtk.ApplicationWindow
类。这是很难理解和正确应用的“陷阱”之一。当将信号连接到方法时,您需要仔细考虑,以确保正确的类获得信号。这是一种处理"clicked"
信号的迂回方法。正常的方法是在Gtk.ApplicationWindow
类中创建自己的方法,比如on_button_clicked
,并将信号连接到那个方法。我们展示这个例子只是为了说明您可以向Gtk.ApplicationWindow
实例或Gtk.Application
实例发送信号。
第 14 行设置按钮的浮雕样式。你应该总是使用 Gtk。正常风格,除非你有很好的理由不这样做。
第 16 行将按钮添加到Gtk.ApplicationWindow
容器中。这就像添加一个标签,如清单 3-2 所示。
第 19–20 行处理按钮发出的"clicked"
信号。我们唯一的行动是销毁Gtk.ApplicationWindow
实例。
我们应该注意,当最后一个Gtk.ApplicationWindow
实例被销毁时,Gtk.Application
会导致应用退出。
测试你的理解能力
在本章中,您了解了窗口、按钮和标签小部件。是时候将这些知识付诸实践了。在下面的练习中,您将运用 GTK+ 应用、信号和GObject
属性系统的结构知识。
练习 1:使用事件和属性
本练习通过创建一个具有自我销毁能力的Gtk.ApplicationWindow
类来扩展本章的前两个例子。您应该将您的名字设置为窗口的标题。应该添加一个以您的姓氏作为默认文本字符串的可选择的Gtk.Label
作为窗口的子窗口。
让我们考虑一下这个窗口的其他属性:它不应该是可调整大小的,最小尺寸应该是 300×100 像素。本章讨论了执行这些任务的方法。
接下来,通过查看 API 文档,将key-press-event
信号连接到窗口。在"key-press-event"
回调函数中,切换窗口标题和标签文本。例如,第一次调用回调方法时,窗口标题应该设置为您的姓,标签文本应该设置为您的名。
完成练习 1 后,您可以在附录 d 中找到该解决方案的描述。该解决方案的完整源代码可从 www.gtkbook.com
下载。
一旦你完成了这个练习,你就可以进入下一章了,这一章将讨论容器部件。这些小部件允许你的主窗口包含不止一个小部件,这是本章所有例子的情况。
不过在继续之前,你要了解一下 www.gtkbook.com
,可以补充 PyGTK 开发的基础。这个网站上有很多下载、GTK+ 信息的链接、C 和 Python 复习教程、API 文档等等。你可以在阅读这本书的时候使用它来帮助你学习 GTK+。
摘要
在这一章中,我们介绍了一些简单的 GTK+ 3.x 应用,以及一些简单的小部件,这些小部件还介绍了一些概念,这些概念将在后面的章节中有所帮助。以下是你在本章中学到的一些概念。
-
用一个示例程序介绍了
Gtk.Label
类。 -
用一个示例程序介绍了
Gtk.Button
类。 -
介绍了信号和捕捉信号的方法。这个概念在后面的章节中有更深入的介绍。
-
引入了容器的概念。这一概念在第四章中有更深入的介绍。
在第四章中,我们将介绍Gtk.Container
类和大量可用的集装箱类型。
四、容器
第三章介绍了创建基本 GTK+ 应用所需的基本要素。它还引入了信号、回调方法、Gtk.label
类、Gtk.Button
类和Gtk.Container
类。
在本章中,您将扩展我们对Gtk.Container
类的知识。然后我们展示两种被包含的小部件:布局和装饰容器。此外,我们涵盖了许多派生的小部件,包括盒子、笔记本、手柄盒和扩展器。
最后一个小部件Gtk.EventBox
,允许小部件利用 GDK 事件。
涵盖了以下主题。
-
Gtk.Container
类及其后代的用途 -
如何使用布局容器,包括框、表、网格和窗格
-
何时使用固定集装箱
-
如何使用事件盒向所有小部件提供事件
GTK。容器
在过去的章节中已经简要地介绍了这个类,但是现在我们将更深入地介绍这个类。这是必要的,这样您就有了关于容器的必要基础知识,这样我们就可以在后续章节中讨论所有的派生类。
Gtk.Container
类是一个抽象类。因此,永远不要试图创建该类的实例,而只创建派生类的实例。
容器类的主要用途是允许父部件包含一个或多个子部件。GTK+ 中有两种类型的容器部件,一种用于布置孩子和装饰者,另一种除了定位孩子之外还添加了某种功能。
装饰容器
在第三章中,你被介绍到了Gtk.ApplicationWindow
,一个从Gtk.Window
衍生而来的窗口,它是从Gtk.Bin
衍生而来的——一种容器类,只能容纳一个子部件。从这个类派生的小部件被称为装饰容器,因为它们向子小部件添加了某种类型的功能。
例如,Gtk.Window
为 it child 提供了一些额外的功能,可以放在顶级小部件中。其他示例装饰器包括Gtk.Frame
小部件,它在它的子部件周围绘制一个框架,Gtk.Button
,它使它的子部件成为一个可点击的按钮,以及Gtk.Expander
,它可以向用户隐藏或显示它的子部件。所有这些部件都使用add
方法来添加子部件。
Gtk.Bin
只公开了一个方法get_child
。Gtk.Bin
类的唯一目的是提供一个可实例化的小部件,从中可以派生出所有只需要一个子小部件的子类。它是公共基础的中心类。
binwin = Gtk.Bin()
源自Gtk.Bin
的小部件包括窗口、对齐、框架、按钮、组合框、事件框、扩展器、处理框、滚动窗口和工具项。这些容器中的许多将在本章的后续章节中介绍。
布局容器
GTK+ 提供的另一种容器小部件叫做布局容器。这些小部件用于排列多个小部件。布局容器可以通过它们是直接从Gtk.Container
派生出来的这一事实来识别。
顾名思义,布局容器的目的是根据用户的偏好、您的指令和内置规则正确地排列它们的子容器。用户偏好包括使用主题和字体偏好。这些可以被覆盖,但是在大多数情况下,你应该尊重用户的偏好。还有管理所有容器小部件的调整大小规则,这将在下一节中介绍。
布局容器包括框、固定容器、窗格部件、图标视图、布局、菜单外壳、笔记本、套接字、表格、文本视图、工具栏和树视图。我们将在本章和本书的其余部分介绍大多数布局部件。我们没有涉及到的更多信息可以在 PyGObject API 参考( http://lazka.github.io/pgi-docs/#Gtk-3.0
)文档中找到。
调整子项的大小
除了排列和装饰子部件,容器还负责调整子部件的大小。调整大小分两个阶段执行:大小申请和大小分配。简而言之,这两个步骤协商小部件可用的大小。这是小部件、其祖先和其子部件之间的递归通信过程。
尺寸需求指的是孩子想要的尺寸。这个过程从顶级小部件开始,它会询问其子部件的首选尺寸。孩子们问他们的孩子等等,直到到达最后一个孩子。
此时,最后一个孩子根据需要在屏幕上正确显示的空间和程序员的任何大小请求来决定它想要的大小。例如,Gtk.Label
小部件要求足够的空间来在屏幕上完全显示其文本,或者如果您要求它有更大的尺寸,则要求更多的空间。
然后,子窗口将这个大小传递给它的祖先,直到顶层小部件根据其子窗口的请求获得所需的空间量。
每个小部件都将其大小首选项作为宽度和高度值存储在一个Gtk.Requisition
对象中。请记住,请购单只是一个请求;父小部件不一定要接受它。
当顶级小部件确定了它需要的空间量后,大小分配就开始了。如果您已经将顶层小部件设置为不可调整大小,则该小部件将永远不会调整大小。不会发生进一步的操作,请购单将被忽略;否则,顶级小部件会将自身调整到所需的大小。然后,它将可用空间量传递给其子部件。重复这个过程,直到所有的窗口小部件都调整了自己的大小。
每个小部件的大小分配存储在每个孩子的Gtk.Allocation
结构的一个实例中。这个结构被传递给子部件,以便用size_allocate()
调整大小。程序员也可以显式地调用这个函数,但是在大多数情况下这样做并不是一个好主意。
在大多数情况下,孩子们会得到他们想要的空间,但在某些情况下这是不可能的。例如,当顶级小部件无法调整大小时,申请不被接受。
相反,一旦小部件被其父部件分配了一个尺寸,小部件就别无选择,只能用新的尺寸重画自己。因此,在调用size_allocate()
的地方要小心。在大多数情况下,set_size_request()
最适合用来调整窗口小部件的大小。
集装箱信号
Gtk.Container
类目前提供四种信号。这些是"add"
、"check_resize"
、"remove"
和"set_focus_child"
。
-
"add"
:一个子部件被添加或打包到容器中。即使您没有显式调用add()
,而是使用小部件的内置打包函数,也会发出这个信号。 -
容器正在检查在采取进一步行动之前是否需要为其子容器调整大小。
-
"remove"
:已从容器中移除一个子容器。 -
容器的一个子容器已经从窗口管理器接收到焦点。现在您已经知道了
Gtk.Container
类的用途,我们将继续学习其他类型的容器小部件。你已经了解了窗口,一种Gtk.Bin
小部件,所以我们将从一个叫做Gtk.Box
的布局容器开始这一章。
水平和垂直框
Gtk.Box
是一个容器小部件,允许在一个一维的矩形区域中打包多个孩子。有两种类型的盒子:一种是垂直盒子,它将子元素打包成一列;另一种是水平盒子,它将子元素打包成一行。
注意
在 GTK+ 2.x 中,Gtk.Box
是一个抽象类。两个子类Gtk.HBox
和Gtk.VBox
分别用于创建水平和垂直的盒子。在 GTK+ 3.x 中,这两个类已经被弃用,而Gtk.Box
已经成为一个真正的类,可以用来创建水平和垂直的盒子。
应用的图形输出如清单 4-1 所示。请注意,名称的显示顺序与它们被添加到数组中的顺序相同,即使每个名称都被打包在开始位置。请注意,名称的显示顺序与它们被添加到数组中的顺序相同,即使每个名称都被打包在开始位置。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
names = ["Andrew", "Joe", "Samantha", "Jonathan"]
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
for name in names:
button = Gtk.Button.new_with_label(name)
vbox.pack_start(button, True, True, 0)
button.connect("clicked", self.on_button_clicked)
button.set_relief(Gtk.ReliefStyle.NORMAL)
self.set_border_width(10)
self.set_size_request(200, -1)
self.add(vbox)
self.show_all()
def on_button_clicked(self, widget):
self.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Boxes")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 4-1Vertical Boxes with Default Packing
图 4-1 显示了运行清单 4-1 的结果。
图 4-1
默认包装的垂直框
在分析清单 4-2 ,Gtk.Box
时使用了同样的一套方法。Gtk.Box
使用同一套方法。
与每个小部件一样,您需要在使用对象之前初始化Gtk.Box
。所有传递的参数都是关键字参数。默认方向如果没有关键字"orientation"
被传递,默认是Gtk.Orientation.HORIZONTAL
。其他关键词也可以,比如"spacing"
。如果"homogeneous"
关键字被设置为True
,那么所有的孩子都被给予最小的空间来容纳每个窗口小部件。
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
"spacing"
关键字参数在每个孩子和它的邻居之间放置默认数量的像素间距。如果该框未设置为等间距,则在添加子项时,可以针对单个单元格更改该值。
由于在将标签添加到小部件后,您不需要进一步访问清单 4-2 中的标签,应用不会存储指向每个对象的单独指针。当父类被销毁时,它们都被自动清理。然后使用一种叫做packing.Gtk.Box
小部件的方法将每个按钮添加到框中。
通过使用pack_start()
将小部件添加到框中,子对象有三个自动设置的属性。扩展设置为True
,自动为单元格提供分配给盒子的额外空间。这个空间被平均分配给所有请求它的单元。fill 属性也被设置为True
,这意味着小部件扩展到所有提供的额外空间,而不是用填充填充它。最后,单元格与其相邻单元格之间的填充量被设置为零像素。
vbox.pack_start(button, True, True, 0)
由于函数的命名,打包框可能有点不直观。考虑这个问题的最好方法是从包装的开始考虑。如果在开始位置打包,则会添加子对象,第一个子对象出现在顶部或左侧。如果在结束位置打包,第一个子对象会出现在盒子的底部或右侧。
还应该注意的是,pack_start()
和pack_end()
方法不仅指定打包参数,它们还将小部件添加到指定的小部件实例中。如果调用一个打包方法,就没有必要调用add()
方法来添加小部件。事实上,如果您试图用打包方法和add()
方法添加同一个小部件,这是一个运行时错误。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
names = ["Andrew", "Joe", "Samantha", "Jonathan"]
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
for name in names:
button = Gtk.Button.new_with_label(name)
vbox.pack_end(button, False, False, 5)
button.connect("clicked", self.on_button_clicked)
button.set_relief(Gtk.ReliefStyle.NORMAL)
self.set_border_width(10)
self.set_size_request(200, -1)
self.add(vbox)
self.show_all()
def on_button_clicked(self, widget):
self.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Boxes")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 4-2Vertical_Boxes Specifying Packing Parameters
因为我们从末尾开始打包每个部件,所以它们在图 4-2 中以相反的顺序显示。包装从盒子的末端开始,在前一个孩子之前包装每个孩子。您可以自由地散布对开始和结束打包函数的调用。GTK+ 跟踪两个参考位置。因为我们从末尾开始打包每个小部件,所以它们以相反的顺序显示。包装从盒子的末端开始,在前一个孩子之前包装每个孩子。您可以自由地散布对开始和结束打包函数的调用。GTK+ 跟踪两个参考位置。
图 4-2
指定包装参数的垂直框
通过将 expand 属性设置为True
,单元格将会扩展,从而占用分配给小部件不需要的额外空间。通过将 fill 属性设置为True
,小部件会扩展以填充单元格可用的额外空间。表 4-1 提供了扩展和填充属性所有可能组合的简要描述。
表 4-1
扩展和填充属性
|发展
|
充满
|
结果
|
| --- | --- | --- |
| 真实的 | 真实的 | 单元格会扩展,从而占据分配给框额外空间,子小部件会扩展以填充该空间。 |
| 真实的 | 错误的 | 单元格会扩展,从而占据额外的空间,但小部件不会扩展。相反,多余的空间是空的。 |
| 错误的 | 真实的 | 单元格和小部件都不会扩展来填充额外的空间。这与将两个属性都设置为 False 是一回事。 |
| 错误的 | 错误的 | 单元格和小部件都不会扩展来填充额外的空间。如果调整窗口大小,单元格不会自动调整大小。 |
在前面的pack_end()
调用中,每个单元格被告知在自身和任何相邻单元格之间放置五个像素的间距。此外,根据表 4-1 ,无论是单元格还是它的子窗口小部件都不会扩展来占用盒子提供的额外空间。
vbox.pack_end(button, True, True, 0)
注意
如果你有使用其他图形工具包编程的经验,GTK+ 提供的大小协商系统可能看起来很奇怪。然而,你很快就会了解它的好处。如果您更改用户界面,GTK+ 会自动调整所有内容的大小,而不是要求您以编程方式重新定位所有内容。随着你继续学习 GTK+,你会发现这是一个巨大的好处。
虽然在向用户显示之前,您应该尝试确定Gtk.Box
小部件中元素的顺序,但是可以使用reorder_child()
对盒子中的子小部件进行重新排序。
vbox.reorder_child(child_widget, position)
通过使用这种方法,您可以将一个子部件移动到Gtk.Box
中的一个新位置。第一个小部件在Gtk.Box
容器中的位置从零开始索引。如果您指定的位置值为–1 或大于子项数量的值,则小部件将被放置在框的最后一个位置。
水平和垂直窗格
Gtk.Paned
是一种特殊类型的容器小部件,它恰好包含两个小部件。在它们之间放置了一个调整大小的条,允许用户通过向一个方向或另一个方向拖动该条来调整两个小部件的大小。当用户交互或编程调用移动滚动条时,两个小部件中的一个会收缩,而另一个会扩展。
注意
在 GTK+ 2.x 中,Gtk.Paned
是一个抽象类。两个子类Gtk.HPaned
和Gtk.VPaned
分别用于创建水平和垂直的盒子。在 GTK+ 3.x 中,这两个类已经被弃用,而Gtk.Paned
已经成为一个真正的类,可以用来创建水平和垂直窗格。
有两种类型的面板部件:水平调整大小和垂直调整大小。与框一样,Gtk.Paned
提供了水平和垂直窗格的所有功能。清单 4-3 显示了一个简单的例子,其中两个Gtk.Button
窗口小部件被放置为一个水平窗格的子窗口。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
hpaned = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL)
button1 = Gtk.Button.new_with_label("Resize")
button2 = Gtk.Button.new_with_label("Me!")
button1.connect("clicked", self.on_button_clicked)
button2.connect("clicked", self.on_button_clicked)
hpaned.add1(button1)
hpaned.add2(button2)
self.add(hpaned)
self.set_size_request(225, 150)
self.show_all()
def on_button_clicked(self, button):
self.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Panes")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 4-3Horizontal Paned with Buttons
如图 4-3 所示,Gtk.Paned
小部件在其两个子部件之间放置了一个竖条。通过拖动该栏,一个小部件收缩,而另一个则扩展。事实上,可以移动滚动条,使一个孩子完全隐藏在用户的视野之外。你学习如何用pack1()
和pack2()
方法来防止这种情况。
图 4-3
带按钮的水平窗格
在图 4-3 中,我们用下面的内容创建了一个Gtk.Paned
对象。
hpaned = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL)
如果您想使用垂直窗格小部件,您只需要调用下面的。
vpaned = Gtk.Paned.new(Gtk.Orientation.VERTICAL)
所有的Gtk.Paned
函数都可以与任何一种类型的面板部件一起工作。
由于Gtk.Paned
只能处理两个孩子,GTK+ 提供了打包每个孩子的功能。在下面的例子中,pack1()
和pack2()
方法被用来将两个孩子添加到Gtk.Paned
。这些函数使用Gtk.Paned
小部件的 resize 和 shrink 属性的默认值。
hpaned.add1(button1);
hpaned.add2(button2);
前面的add1()
和add2()
方法调用来自清单 4-3 并且等同于下面的。
hpaned.pack1(label1, False, True);
hpaned.pack2(label2, True, True);
pack1()
和pack2()
中的第二个参数指定当调整窗格大小时,子部件是否应该扩展。如果您将此项设置为False
,无论您将可用区域扩大多少,子部件都不会扩展。
最后一个参数指定是否可以使子元素小于其大小要求。在大多数情况下,您希望将此设置为True
,这样用户可以通过拖动调整大小栏来完全隐藏小部件。如果您想阻止用户这样做,请将第三个参数设置为False
。表 4-2 说明了调整大小和收缩属性是如何相互关联的。
表 4-2
调整大小和收缩属性
|调整大小
|
收缩
|
结果
|
| --- | --- | --- |
| 真实的 | 真实的 | 当调整窗格大小时,小部件会占用所有可用空间,用户可以将它调整到小于其要求的大小。 |
| 真实的 | 错误的 | 调整窗格大小时,小工具会占用所有可用空间,但可用空间必须大于或等于小工具的大小要求。 |
| 错误的 | 真实的 | 小部件不会调整自己的大小来占据窗格中额外的可用空间,但是用户可以使它小于它的尺寸要求。 |
| 错误的 | 错误的 | 微件不会调整自身大小以占用窗格中的额外可用空间,并且可用空间必须大于或等于微件的大小申请。 |
您可以使用set_position()
轻松设置调整大小栏的确切位置。相对于容器的顶部或左侧,以像素为单位计算位置。如果您将条形的位置设置为零,并且小部件允许缩小,则条形会一直移动到顶部或左侧。
paned.set_position(position)
大多数应用希望记住调整大小栏的位置,这样当用户下次加载应用时,它可以恢复到相同的位置。可以用get_position()
检索调整大小条的当前位置。
pos = paned.get_position()
提供了多种信号,但其中最有用的是 move-handle,它告诉你什么时候调整大小条被移动了。如果您想记住调整大小栏的位置,它会告诉您何时需要检索新值。
网格
到目前为止,我介绍的所有布局容器小部件都只允许在一个维度中打包孩子。
然而,Gtk.Grid
小部件允许你在二维空间中打包孩子。
与使用多个Gtk.Box
小部件相比,使用Gtk.Grid
小部件的一个优点是相邻行和列中的子部件会自动相互对齐,这与盒中盒的情况不同。然而,这也是一个缺点,因为你不会总是希望一切都按照这种方式排列。
图 4-4 显示了一个包含三个小部件的简单网格。请注意,单个标签跨越两列。这说明了一个事实,即只要区域是矩形的,网格就允许一个小部件跨越多列和/或多行。
图 4-4
网格显示名称
清单 4-4 将两个Gtk.Label
小部件和一个Gtk.Entry
小部件插入 2 乘 2 区域(您将在第五章中学习如何使用Gtk.Entry
小部件,但这将让您了解接下来会发生什么)。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(150, 100)
grid = Gtk.Grid.new()
label1 = Gtk.Label.new("Enter the following information ...")
label2 = Gtk.Label.new("Name: ")
entry = Gtk.Entry.new()
grid.attach(label1, 0, 0, 2, 1)
grid.attach(label2, 0, 1, 1, 1)
grid.attach(entry, 1, 1, 1, 1)
grid.set_row_spacing(5)
grid.set_column_spacing(5)
self.add(grid)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Tables")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 4-4Grids Displaying Name
栅格间距
如果你想设置网格中每一列的间距,你可以使用set_column_spacing()
。这个函数在set_row_spacing()
中用于在行之间添加填充。这些功能会覆盖网格先前的任何设置。set_row_spacing()
在行间添加衬垫。这些功能会覆盖网格先前的任何设置。
grid.set_column_spacing(5)
grid.attach()
方法需要五个参数,如下所示。
Grid.attach(child_widget, left_pos, top_pos, width, height)
固定集装箱
Gtk.Fixed
小部件是一种布局容器,允许您按像素放置小部件。使用这个小部件时可能会出现许多问题,但是在我们探讨缺点之前,让我们看一个简单的例子。
清单 4-5 显示了包含两个按钮的Gtk.Fixed
小部件,分别位于(0,0)和(20,30)位置,相对于小部件的左上角。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
fixed = Gtk.Fixed.new()
button1 = Gtk.Button.new_with_label("Pixel by pixel ...")
button2 = Gtk.Button.new_with_label("you choose my fate.")
button1.connect("clicked", self.on_button_clicked)
button2.connect("clicked", self.on_button_clicked)
fixed.put(button1, 0, 0)
fixed.put(button2, 22, 35)
self.add(fixed)
self.show_all()
def on_button_clicked(self, widget):
self.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Fixed")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 4-5Specifying Exact Locations
用Gtk.Fixed.new()
初始化的Gtk.Fixed
小部件允许您在特定位置放置特定大小的小部件。使用put()
在指定的水平和垂直位置放置微件。
fixed.put(child, x, y)
固定容器的左上角由位置(0,0)引用。您应该只能指定小部件的真实位置或正空间中的位置。固定容器会自行调整大小,因此每个小部件都是完全可见的。
如果您需要在小部件被放入Gtk.Fixed
容器后移动它,您可以使用move()
。您需要注意不要覆盖已经放置的小部件。在重叠的情况下,Gtk.Fixed
小部件不提供通知。相反,它试图呈现具有不可预测结果的窗口。
fixed.move(child, x, y)
这给我们带来了使用Gtk.Fixed
小部件的固有问题。第一个问题是你的用户可以随意使用任何他们想要的主题。这意味着用户机器上的文本大小可能不同于您机器上的文本大小,除非您明确设置了字体。不同的用户主题中,小部件的大小也各不相同。这可能导致错位和重叠。这在图 4-5 中有所说明,图中显示了两张截图,一张字体较小,一张字体较大。
图 4-5
Gtk 中不同字体大小引起的问题。固定集装箱
您可以显式设置文本的大小和字体以避免重叠,但在大多数情况下不建议这样做。为弱视用户提供了辅助功能选项。如果您更改他们的字体,一些用户可能无法阅读屏幕上的文本。
当您的应用被翻译成其他语言时,使用Gtk.Fixed
会出现另一个问题。用户界面在英语中可能看起来很棒,但在其他语言中显示的字符串可能会导致显示问题,因为宽度不是恒定的。此外,从右向左阅读的语言,如希伯来语和阿拉伯语,不能用Gtk.Fixed
小部件正确地映射。最好使用可变尺寸的容器,例如本例中的Gtk.Box
或Gtk.Grid
。
最后,当使用Gtk.Fixed
容器时,在图形界面中添加和删除小部件可能会很痛苦。更改用户界面需要您重新定位所有的小部件。如果您的应用有很多小部件,这就带来了长期维护的问题。
另一方面,你有网格、盒子和其他各种自动格式化的容器。如果您需要在用户界面中添加或删除小部件,就像添加或删除单元格一样简单。这使得维护更加有效,这是您在大型应用中应该考虑的事情。
因此,除非您知道这些问题不会困扰您的应用,否则您应该使用可变大小的容器来代替Gtk.Fixed
。提供此容器只是为了让您知道,如果出现合适的情况,它是可用的。即使在合适的情况下,灵活的容器几乎总是更好的解决方案,并且是正确的做事方式。
使增大者
容器只能处理一个孩子。通过单击扩展器标签左侧的三角形,可以显示或隐藏子级。在图 4-6 中可以看到这个动作前后的截图。
图 4-6
一辆 Gtk。膨胀容器
清单 4-6 向您介绍了最重要的Gtk.Expander
方法。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(200, 100)
expander = Gtk.Expander.new_with_mnemonic("Click _Me For More!")
label = Gtk.Label.new ("Hide me or show me,\nthat is your choice.")
expander.add(label)
expander.set_expanded(True)
self.add(expander)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Hello World!")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 4-6Gtk.Expander Container
激活一个Gtk.Expander
窗口小部件会使其根据当前状态展开或收缩。
小费
几乎每个显示标签的小部件都有助记符。在可能的情况下,您应该始终使用该功能,因为有些用户更喜欢使用键盘浏览应用。
如果您希望在扩展标签中包含一个下划线字符,您应该在它前面加上第二个下划线。如果你不想利用助记符特性,你可以用标准字符串作为标签,使用Gtk.Expander.new()
来初始化Gtk.Expander
,但是向用户提供助记符作为一个选项总是一个好主意。在普通的扩展标签中,下划线字符不会被解析,而是被视为另一个字符。
Gtk.Expander
小部件本身是从Gtk.Bin
派生出来的,这意味着它只能包含一个子部件。和其他包含一个子组件的容器一样,您需要使用expander.add()
来添加子组件。
通过调用expander.set_expanded()
可以显示或隐藏Gtk.Expander
容器的子部件。expander.set_expanded()
。
expander.set_expanded(boolean)
默认情况下,GTK+ 不会在扩展标签和子部件之间添加任何间距。要添加像素间距,可以使用expander.set_spacing()
添加填充。
expander.set_spacing(spacing)
笔记本
Gtk.Notebook
小部件将子小部件组织成多个页面。用户可以通过单击小部件一边出现的选项卡在这些页面之间切换。
您可以指定选项卡的位置,尽管默认情况下它们显示在顶部。您也可以完全隐藏选项卡。图 4-7 显示了一个带有两个选项卡的Gtk.Notebook
小部件,它是用清单 4-7 中的代码创建的。
图 4-7
具有多页的笔记本容器
创建笔记本容器时,必须为每个选项卡指定一个标签小部件和一个子小部件。标签可以添加到前面或后面,插入,重新排序和删除。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(250, 100)
notebook = Gtk.Notebook.new()
label1 = Gtk.Label.new("Page 1")
label2 = Gtk.Label.new("Page 2")
child1 = Gtk.Label.new("Go to page 2 to find the answer.")
child2 = Gtk.Label.new("Go to page 1 to find the answer.")
notebook.append_page(child1, label1)
notebook.append_page(child2, label2)
notebook.set_tab_pos(Gtk.PositionType.BOTTOM)
self.add(notebook)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Notebook")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 4-7Container with Multiple Pages
在你创建一个Gtk.Notebook
之后,除非你给它添加标签,否则它不是很有用。要将标签添加到标签列表的末尾或开头,可以分别使用notebook.append_page()
或notebook.prepend_page()
。这些方法中的每一个都接受一个子部件,以及一个显示在选项卡中的部件,如下所示。
小费
选项卡标签不一定是Gtk.Label
小部件。例如,您可以使用一个包含标签和关闭按钮的Gtk.Box
小部件。这允许您在标签中嵌入其他有用的小部件,比如按钮和图像。
每个笔记本页面只能显示一个子部件。然而,每个孩子可以是另一个容器,所以每个页面可以显示许多部件。事实上,可以使用Gtk.Notebook
作为另一个Gtk.Notebook
标签的子部件。
警告
将笔记本放在笔记本中是可能的,但是要小心,因为它很容易混淆用户。如果必须这样做,请确保将子笔记本的选项卡放在笔记本的另一侧,而不是父笔记本的选项卡。通过这样做,用户能够找出哪些标签属于哪个笔记本。
如果想在特定位置插入一个 tab,可以使用notebook.insert_page()
。此函数允许您指定选项卡的整数位置。位于插入的选项卡之后的所有选项卡的索引增加 1。
notebook.insert_page (child, tab_label, position)
用于向Gtk.Notebook
添加标签的三个函数都返回所添加标签的整数位置,如果操作失败,则返回–1。
笔记本属性
在清单 4-7 中,为Gtk.Notebook
设置了 tab-position 属性,这是通过以下调用完成的。
notebook.set_tab_pos(position)
通过使用Gtk.PositionType
枚举可以在notebook.set_tab_pos()
中设置标签位置。其中包括Gtk.PositionType.TOP
、Gtk.PositionType.BOTTOM
、Gtk.PositionType.LEFT
、Gtk.PositionType.RIGHT
。
如果你想给用户多种选择,但又想分多个阶段展示,笔记本是很有用的。如果您在每个选项卡中放置几个并使用notebook.set_show_tabs()
隐藏选项卡,您可以让用户在选项之间来回移动。这个概念的一个例子是你在操作系统中看到的许多向导,类似于Gtk.Assistant
小部件提供的功能。
notebook.set_show_tabs(show_tabs)
在某个时候,Gtk.Notebook
用尽了空间来存储标签。为了解决这个问题,您可以使用notebook.set_scrollable()
将笔记本标签设置为可滚动。
notebook.set_scrollable(scrollable)
该属性强制对用户隐藏选项卡。提供了箭头,以便用户能够滚动选项卡列表。这是必要的,因为选项卡只显示在一行或一列中。
如果调整窗口大小,使所有选项卡都无法显示,选项卡将变为可滚动的。如果您将字体大小设置得足够大,以至于无法绘制所有选项卡,也会发生滚动。如果选项卡占用的空间可能会超过分配的空间,那么您应该始终将该属性设置为True
。
选项卡操作
GTK+ 提供了多种功能,允许您与已经存在的选项卡进行交互。在了解这些方法之前,了解这些方法中的大多数会导致发出更改当前页面信号是很有用的。当处于焦点的当前选项卡发生更改时,会发出此信号。
如果你可以添加标签,也必须有一个方法来删除标签。通过使用notebook.remove_page()
,您可以根据索引引用移除标签。如果在将小部件添加到Gtk.Notebook
之前没有增加引用计数,这个函数将释放最后一个引用并销毁子部件。
notebook.remove_page(page_number)
您可以通过调用notebook.reorder_child()
来手动重新排列选项卡。您必须指定要移动的页面的子部件以及它应该移动到的位置。如果指定一个大于制表符数量的数字或负数,制表符将被移动到列表的末尾。
notebook.reorder_child(child, position)
有三种方法可用于更改当前页面。如果知道想要查看的页面的具体索引,可以使用notebook.set_current_page()
移动到该页面。
notebook.set_current_page(page_number)
有时,您可能还希望切换到下一个或上一个标签,这可以通过调用notebook.next_page()
或notebook.prev_page()
来完成。如果调用这两个函数中的任何一个会导致当前选项卡降到零以下或超过当前的选项卡数,则什么也不会发生;该呼叫被忽略。
当决定移动到哪个页面时,了解当前页面和选项卡总数通常是有用的。这些值可以分别用notebook.get_current_page()
获得。
事件框
包括Gtk.Label
在内的各种小部件不会响应 GDK 事件,因为它们没有关联的 GDK 窗口。为了解决这个问题,GTK+ 提供了一个名为Gtk.EventBox
的容器小部件。事件框通过为对象提供一个 GDK 窗口来捕捉子部件的事件。
清单 4-8 通过使用事件盒捕获button-press-event
信号。双击标签时,标签中的文本会根据其当前状态进行更改。当单击发生时,没有任何可见的事情发生,尽管在这种情况下仍然通过使用事件框发出信号(Gtk.Label)
)。双击标签时,标签中的文本会根据其当前状态进行更改。当单击发生时,没有任何可见的事情发生,尽管在这种情况下仍然会发出信号。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(200, 50)
eventbox = Gtk.EventBox.new()
label = Gtk.Label.new("Double-Click Me!")
eventbox.set_above_child(False)
eventbox.connect("button_press_event", self.on_button_pressed, label)
eventbox.add(label)
self.add(eventbox)
eventbox.set_events(Gdk.EventMask.BUTTON_PRESS_MASK)
eventbox.realize()
def on_button_pressed(self, eventbox, event, label):
if event.type == Gdk.EventType._2BUTTON_PRESS:
text = label.get_text()
if text[0] == 'D':
label.set_text("I Was Double-Clicked!")
else:
label.set_text("Double-Click Me Again!")
return False
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Hello World!")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 4-8Adding Events to Gtk.Label
当使用一个事件盒时,你需要决定事件盒的Gdk.Window
应该位于其子窗口的上方还是下方。如果事件框窗口在上方,则事件框内的所有事件都将进入事件框。如果窗口在下面,子部件窗口中的事件首先到达那个部件,然后到达它的父部件。
注意
如果你设置窗口的位置如下,事件会首先到达子部件。但是,这只适用于关联了 GDK 窗口的小部件。如果子窗口是一个Gtk.Label
小部件,它就没有能力自己检测事件。因此,在清单 4-8 中将窗口的位置设置为上方还是下方并不重要。
事件框窗口的位置可以用eventbox.set_above_child()
移动到其子窗口的上方或下方。默认情况下,所有事件框的该属性都设置为False
。这意味着所有事件都由首次发出信号的小部件处理。小部件完成后,事件将被传递给它的父部件。
eventbox.set_above_child(above_child)
接下来,您需要向事件框添加一个事件掩码,以便它知道小部件接收的是什么类型的事件。指定事件屏蔽的Gdk.EventMask
枚举值如表 4-3 所示。如果需要设置多个值,可以将一个由Gdk.EventMask
值组成的按位列表传递给eventbox.set_events()
。
表 4-3
Gdk .事件掩码值
|价值
|
描述
|
| --- | --- |
| Gdk .事件掩码. EXPOSURE_MASK | 当小部件公开时接受事件。 |
| gdk . event mask . pointer _ motion _ mask-事件遮罩 | 接受离开窗口附近时发出的事件。 |
| Gdk。事件掩码。指针 _ 运动 _ 提示 _ 掩码 | 限制 GDK _ 运动 _ 通知事件的数量,这样它们就不会在每次鼠标移动时发出。 |
| gdk . event mask . button _ motion _ mask-事件遮罩 | 当任何按钮被按下时接受指针运动事件。 |
| gdk . event mask . button 1 _ motion _ mask | 按下按钮 1 时接受指针运动事件。 |
| gdk . event mask . button 2 _ motion _ mask | 按下按钮 2 时接受指针运动事件。 |
| gdk . event mask . button 3 _ motion _ mask | 按下按钮 3 时接受指针运动事件。 |
| gdk . event mask . button _ press _ mask-事件遮罩 | 接受鼠标按键事件。 |
| gdk . event mask . button _ release _ mask-事件遮罩 | 接受鼠标按钮释放事件。 |
| gdk . event mask . key _ press _ mask-事件遮罩 | 接受键盘上的按键事件。 |
| gdk . event mask . key _ release _ mask-事件遮罩 | 接受键盘上的按键释放事件。 |
| Gdk。事件掩码。输入 _ 通知 _ 掩码 | 接受进入窗口附近时发出的事件。 |
| Gdk。EventMask.LEAVE_NOTIFY_MASK | 接受离开窗口附近时发出的事件。 |
| Gdk。事件掩码。焦点 _ 变化 _ 掩码 | 接受焦点事件的改变。 |
| Gdk。事件掩码.结构 _ 掩码 | 接受窗口配置发生更改时发出的事件。 |
| Gdk。事件掩码.属性 _ 更改 _ 掩码 | 接受对对象属性的更改。 |
| Gdk。事件掩码。可见性 _ 通知 _ 掩码 | 接受可见性事件的更改。 |
| Gdk。EventMask.PROXIMITY_IN_MASK | 接受当鼠标光标进入小部件附近时发出的事件。 |
| Gdk。EventMask.PROXIMITY_OUT_MASK | 接受当鼠标光标离开小部件附近时发出的事件。 |
| Gdk。事件掩码.子结构 _ 掩码 | 接受更改子窗口配置的事件。 |
| Gdk.EventMask.SCROLL_MASK | 接受所有滚动事件。 |
| Gdk。事件掩码。所有事件掩码 | 接受所有类型的事件。 |
在小部件上调用eventbox.realize()
之前,你必须调用eventbox.set_events()
。如果 GTK+ 已经实现了一个小部件,你必须使用eventbox.add_events()
来添加事件掩码。
在调用eventbox.realize()
之前,您的Gtk.EventBox
还没有关联的Gdk.Window
或任何其他 GDK 小工具资源。通常,实现发生在父实现时,但事件框是个例外。当你在一个 widget 上调用window.show()
时,由 GTK+ 自动实现。当你调用window.show_all()
时,事件框不会被实现,因为它们被设置为不可见。在事件盒上调用eventbox.realize()
是解决这个问题的简单方法。
当您意识到您的事件框时,您需要确保它已经作为一个孩子添加到一个顶级小部件中,否则它将无法工作。这是因为,当您实现一个小部件时,它会自动实现它的祖先。如果它没有祖先,GTK+ 不快乐,实现失败。
事件盒实现后,它有一个关联的Gdk.Window
。Gdk.Window
是一个类,指的是屏幕上绘制小部件的矩形区域。它和Gtk.Window
不是一回事,后者指的是带有标题栏等的顶层窗口。一个Gtk.Window
包含许多Gdk.Window
对象,每个子部件一个。它们用于在屏幕上绘制小部件。
测试你的理解能力
本章向您介绍了 GTK+ 中包含的许多容器小部件。以下两个练习允许您练习您所学到的关于这些新部件的知识。
练习 1:使用多个容器
容器的一个重要特征是每个容器可以容纳其他容器。为了真正理解这一点,在本例中,您使用了大量的容器。主窗口底部显示一个Gtk.Notebook
和两个按钮。
笔记本应该有四页。每个笔记本页面应该包含一个移动到下一页的Gtk.Button
(最后一页的Gtk.Button
应该绕到第一页)。
在窗口底部创建两个按钮。第一个应该移动到Gtk.Notebook
中的前一页,如果必要的话,绕到最后一页。第二个按钮应该关闭窗口,并在单击时退出应用。
练习 1 是一个要实现的简单应用,但是它阐明了几个要点。首先,它展示了Gtk.Box
的用处,以及如何将垂直和水平框一起使用来创建复杂的用户界面。
的确,同样的应用可以用一个Gtk.Grid
作为窗口的直接子窗口来实现,但是用一个水平框来对齐底部的按钮要容易得多。您会注意到按钮被打包在盒子的末尾,这使它们与盒子的右侧对齐,这对于盒子来说更容易实现。
此外,您还看到了容器可以并且应该用来容纳其他容器。例如,在练习 1 中,a Gtk.Window
持有一个垂直的Gtk.Box
,它持有一个水平的Gtk.Box
和一个Gtk.Notebook
。随着应用规模的增长,这种结构会变得更加复杂。
完成练习 1 后,继续练习 2。在下一个问题中,您使用窗格容器而不是垂直框。
练习 2:更多容器
在本练习中,您将扩展练习 1 中编写的代码。不要使用垂直的Gtk.Box
来放置笔记本和水平的按钮框,而是创建一个垂直的Gtk.Paned
小部件。
除了这个改变之外,你应该隐藏Gtk.Notebook
标签,这样用户不按下按钮就不能在页面之间切换。在这种情况下,您无法知道页面何时被更改。因此,Gtk.Notebook
页面中的每个按钮都应该包含在它自己的扩展器中。扩展标签允许您区分笔记本页面。
一旦你完成了练习 2,你就已经练习了Gtk.Box
、Gtk.Paned
、Gtk.Notebook
和Gtk.Expander
——这四个重要的容器贯穿了本书的其余部分。
在继续下一章之前,您可能想测试一下本章中的几个容器,这些容器在练习 1 和 2 中是不需要的。这让你练习使用所有的容器,因为后面的章节不会回顾过去的信息。
摘要
在本章中,你学习了两种类型的容器部件:装饰器和布局容器。涵盖的装饰器类型有扩展器和事件盒。包含的布局容器类型有盒子、窗格、网格、固定容器和笔记本。
事件盒容器将在后面的章节中出现,因为除了Gtk.Label
之外还有其他部件不能处理 GDK 事件。这是在您了解这些小部件时指定的。在后面的章节中你也会看到大多数的容器。
虽然这些容器是 GTK+ 应用开发所必需的,但是在大多数应用中,仅仅在容器中显示Gtk.Label
和Gtk.Button
小部件并不是非常有用(或者有趣)。除了基本的用户交互之外,这种类型的应用几乎不提供任何功能。
因此,在下一章中,你将会学到许多允许你与用户交互的小部件。这些小部件包括按钮、切换、文本输入和微调按钮的类型。
五、基本部件
到目前为止,除了Gtk.Button
之外,您还没有学到任何旨在方便用户交互的小部件。这在本章中有所改变,因为我们将介绍允许用户做出选择、更改设置或输入信息的许多类型的小部件。
这些小部件包括按钮、切换按钮、复选按钮、单选按钮、颜色选择按钮、文件选择器按钮、字体选择按钮、文本输入和数字选择按钮。
在本章中,您将学习
-
如何使用股票项目的可点击按钮。
-
如何使用切换按钮的类型,包括复选按钮和单选按钮。
-
如何使用 entry 小部件进行一行自由格式的文本输入。
-
如何使用微调按钮小部件进行整数或浮点数选择。
-
有哪些专门的按钮。
使用按钮
以前,本节的标题是“使用库存项目”但是 GTK+ 3.x 库存物品已经被弃用,所以我将向您展示如何从标准物品中创建外观相似的库存物品。
图 5-1 显示了如何创建一个相似的股票关闭按钮。
图 5-1
外观相似的库存按钮
使用清单 5-1 中的代码生成相似的股票按钮。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
button = Gtk.Button.new()
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
icon_theme = Gtk.IconTheme.get_default()
icon = icon_theme.load_icon("window-close", -1,
Gtk.IconLookupFlags.FORCE_SIZE)
image = Gtk.Image.new_from_pixbuf(icon)
hbox.add(image)
label = Gtk.Label.new_with_mnemonic("_Close")
hbox.add(label)
hbox.set_homogeneous(True)
button.add(hbox)
button.connect("clicked", self.on_button_clicked)
button.set_relief(Gtk.ReliefStyle.NORMAL)
self.add(button)
self.set_size_request(230, 100)
def on_button_clicked(self, param):
self.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self,
title="Look-alike Stock Item”)
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 5-1Look-alike Stock Button
创建自定义按钮的第一个任务是制作一个标准按钮,然后制作一个水平框。下一个任务是为按钮创建一个图像。下面的语句完成了这项任务。
icon_theme = Gtk.IconTheme.get_default()
icon = icon_theme.load_icon("window-close", -1,
Gtk.IconLookupFlags.FORCE_SIZE)
image = Gtk.Image.new_from_pixbuf(icon)
hbox.add(image)
第一条语句获得默认的 GTK+ 主题。接下来,我们通过名称从主题中加载 PixBuf 图标。
接下来,我们将 PixBuf 图标转换成图像,然后将其添加到水平框中。
现在我们创建一个标签,然后将它添加到水平框中。
label = Gtk.Label.new_with_mnemonic("_Close")
hbox.add(label)
现在我们可以将按钮连接到我们的自定义方法,设置按钮的浮雕样式,然后将按钮添加到Gtk.ApplicationWindow
。
button.connect("clicked", self.on_button_clicked)
button.set_relief(Gtk.ReliefStyle.NORMAL)
self.add(button)
小费
您需要的图标图像可能在默认主题中,也可能不在默认主题中。您可能需要查看其他主题,以找到您可以使用的图像。您可能需要安装一个 GTK+ 主题,以便访问符合您的目的的主题。
切换按钮
Gtk.ToggleButton
小部件是一种类型的Gtk.Button
,它在被点击后保持活动或不活动状态。激活时显示为按下。单击激活的切换按钮会使其返回到正常状态。从Gtk.ToggleButton
衍生出两个 widget:Gtk.CheckButton
和Gtk.RadioButton
。
您可以使用三种功能之一创建一个新的Gtk.ToggleButton
。要创建一个空的切换按钮,使用Gtk.ToggleButton.
new()
。如果希望切换按钮默认包含标签,请使用Gtk.ToggleButton.
new_with_label()
。最后,Gtk.ToggleButton
还支持带Gtk.ToggleButton.
new_with_mnemonic()
的助记标签。
图 5-2 显示了两个Gtk.ToggleButton
窗口小部件,它们是通过调用Gtk.ToggleButton.
new_with_mnemonic()
初始化器用两个助记标签创建的。截图中的小部件是用清单 5-2 中的代码创建的。
图 5-2
两个 Gtk。切换按钮小工具
在清单 5-2 中,当一个切换按钮被激活时,另一个被禁用。使它变得敏感的唯一方法是取消原来的切换按钮。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
vbox = Gtk.Box.new(orientation=Gtk.Orientation.VERTICAL, spacing=0)
toggle1 = Gtk.ToggleButton.new_with_mnemonic("_Deactivate the other one!")
toggle2 = Gtk.ToggleButton.new_with_mnemonic("_No! Deactivate that one!")
toggle1.connect("toggled", self.on_button_toggled, toggle2)
toggle2.connect("toggled", self.on_button_toggled, toggle1)
vbox.pack_start(toggle1, True, True, 1)
vbox.pack_start(toggle2, True, True, 1)
self.add(vbox)
def on_button_toggled(self, toggle, other_toggle):
if (Gtk.ToggleButton.get_active(toggle)):
other_toggle.set_sensitive(False)
else:
other_toggle.set_sensitive(True)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Toggle Buttons")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 5-2Two Gtk.ToggleButton Widgets
由Gtk.ToggleButton
类添加的唯一信号是“toggled"
,它在用户激活或停用按钮时发出。该信号在清单 5-2 中由一个切换按钮触发,以禁用另一个。
在清单 5-2 中,显示了另一条重要信息:多个小部件可以使用同一个回调方法。我们不需要为每个切换按钮创建单独的回调方法,因为每个按钮都需要相同的功能。也可以将一个信号连接到多个回调方法,尽管不建议这样做。相反,您应该只在一个回调方法中实现全部功能。
检查按钮
在大多数情况下,您不会想要使用Gtk.ToggleButton
小部件,因为它看起来和普通的Gtk.Button
完全一样。相反,GTK+ 提供了Gtk.CheckButton
小部件,它在显示文本旁边放置了一个离散的开关。Gtk.CheckButton
是从Gtk.ToggleButton
类派生出来的。该小部件的两个实例如图 5-3 所示。
图 5-3
两个 Gtk。CheckButton 小工具
与切换按钮一样,为Gtk.CheckButton
初始化提供了三个功能。其中包括Gtk.CheckButton.``new()``Gtk.CheckButton.new_with_label()``Gtk.CheckButton.``new_with_mnemonic()
。Gtk.CheckButton
也继承了清单 5-3 中使用的重要的“切换”信号。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
check1 = Gtk.CheckButton.new_with_label("I am the main option.")
check2 = Gtk.CheckButton.new_with_label("I rely on the other guy.")
check2.set_sensitive(False)
check1.connect("toggled", self.on_button_checked, check2)
closebutton = Gtk.Button.new_with_mnemonic("_Close")
closebutton.connect("clicked", self.on_button_close_clicked)
vbox = Gtk.Box.new(orientation=Gtk.Orientation.VERTICAL, spacing=0)
vbox.pack_start(check1, False, True, 0)
vbox.pack_start(check2, False, True, 0)
vbox.pack_start(closebutton, False, True, 0)
self.add(vbox)
def on_button_checked(self, check1, check2):
if check1.get_active():
check2.set_sensitive(True);
else:
check2.set_sensitive(False)
def on_button_close_clicked(self, button):
self.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Check Buttons")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 5-3
Gtk.CheckButtons
除了初始化方法,复选框的所有功能都在Gtk.ToggleButton
类及其祖先中实现。Gtk.CheckButton
仅仅是一个方便的小部件,它提供了与标准Gtk.Button
小部件不同的图形。
单选按钮
从Gtk.ToggleButton
派生的第二种小部件是单选按钮小部件。事实上,Gtk.RadioButton
实际上是来源于Gtk.CheckButton
。单选按钮是通常组合在一起的开关。
在一个组中,当一个单选按钮被选中时,所有其他单选按钮都被取消选中。该组禁止同时选择多个单选按钮。这使您可以向用户提供多个选项,但只能选择一个。
注意
GTK+ 没有提供取消选择单选按钮的方法,所以一个单选按钮是不可取的。用户不能取消选择该选项!如果你只需要一个按钮,你应该使用一个Gtk.CheckButton
或者Gtk.ToggleButton
控件。
单选按钮被绘制为 label 小部件侧面的离散圆形切换按钮,以便与其他类型的切换按钮区分开来。可以使用与Gtk.CheckButton
相同的切换来绘制单选按钮,但是不应该这样做,因为这会使用户困惑和沮丧。垂直框中的一组四个单选按钮如图 5-4 所示。
图 5-4
四 Gtk。单选按钮小部件
要使单选按钮正常工作,它们必须都引用组中的另一个单选按钮。否则,所有按钮都将作为独立的切换按钮。清单 5-4 显示了如何使用多个单选按钮的示例。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
radio1 = Gtk.RadioButton.new_with_label(None, "I want to be clicked!")
radio2 = Gtk.RadioButton.new_with_label_from_widget(radio1,
"Click me instead!)
radio3 = Gtk.RadioButton.new_with_label_from_widget(radio1,
"No! Click me!”)
radio4 = Gtk.RadioButton.new_with_label_from_widget(radio3,
"No! Click me!”)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL,
spacing=0) vbox.pack_start(radio1, False, False, 0)
vbox.pack_start(radio2, False, False, 0)
vbox.pack_start(radio3, False, False, 0)
vbox.pack_start(radio4, False, False, 0)
self.add(vbox)
self.show_all()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Radio Buttons")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 5-4
Gtk.RadioButton
组中的第一个单选按钮可以用以下三个函数中的任何一个来创建。但是,如果您想使用一个Gtk.Label
小部件作为子部件,也可以使用一个助记小部件,这样就可以从键盘上激活切换。
radiobutton = Gtk.RadioButton.new(list)
radiobutton = Gtk.RadioButton.new_with_label(list, "My label")
radiobutton = Gtk.RadioButton.new_with_mnemonic(list, "_My label")
然而,还有第四种方法可以同时创建多个单选按钮和一个列表。通过创建第一个单选按钮而不指定列表,可以做到这一点。后续的单选按钮是引用创建的第一个单选按钮或属于内部组的任何其他单选按钮创建的。
radio1 = Gtk.RadioButton.new_with_label(None, "I want to be clicked!")
radio2 = Gtk.RadioButton.new_with_label_from_widget(radio1, "Click me instead!")
radio3 = Gtk.RadioButton.new_with_label_from_widget(radio1, "No! Click me!")
radio4 = Gtk.RadioButton.new_with_label_from_widget(radio3, "No! Click me instead!
在每个呼叫中为无线电组指定None
。这是因为创建一组单选按钮的最简单方法是将它们与组中的另一个小部件相关联。通过使用这种方法,您可以避免对单链表使用GLib
,因为链表是自动创建和管理的。
将初始化函数引用到一个已经存在的单选按钮会创建这些选项。GTK+ 将新的单选按钮从指定的小部件添加到组中。因此,您只需要引用所需单选按钮组中已经存在的任何小部件。
最后,组中的每个单选按钮必须连接到切换的信号。当一个单选按钮被选中时,正好有两个单选按钮发出切换信号,因为一个被选中,另一个被取消选中。如果不将每个单选按钮都连接到 toggled,您将无法捕捉所有单选按钮信号。
文本条目
Gtk.Entry
小部件是一个单行的、自由格式的文本输入小部件。它是以一种通用的方式实现的,因此它可以被塑造成适合许多类型的解决方案。它可以用于文本输入、密码输入,甚至数字选择。
Gtk.Entry
还实现了Gtk.Editable
接口,该接口提供了大量用于处理文本选择的函数。图 5-5 中显示了一个Gtk.Entry
小部件的例子。该文本条目用于输入密码。
图 5-5
Gtk。密码输入部件
注意
Gtk.Editable
是一种特殊类型的对象,称为接口。接口是由多个小部件实现的一组 API,用于保持一致性。在第十二章中,你将学习如何在你自己的小部件中实现和利用接口。
Gtk.Entry
小部件认为所有文本都是标准字符串。它区分普通文本和密码的唯一方式是显示一种叫做隐形字符的特殊字符,而不是密码内容。清单 5-5 向您展示了如何使用一个Gtk.Entry
小部件来输入密码。如果你想使用一个Gtk.Entry
小部件进行普通的文本输入,你只需要打开可见性。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
import os
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
prompt_str = "What is the password for " + os.getlogin() + "?"
question = Gtk.Label(prompt_str)
label = Gtk.Label("Password:")
passwd = Gtk.Entry()
passwd.set_visibility(False)
passwd.set_invisible_char("*")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
hbox.pack_start(label, False, False, 5)
hbox.pack_start(passwd, False, False, 5)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
vbox.pack_start(question, False, False, 0)
vbox.pack_start(hbox, False, False, 0)
self.add(vbox)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Password")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 5-5
Gtk.Entry
条目属性
Gtk.Entry
小部件是一个高度灵活的小部件,因为它被设计成在最大数量的实例中使用。这可以从类提供的大量属性中看出。本节包括了其中最重要的一些例子。有关属性的完整列表,请参考附录 a。
通常,由于值的字符串限制,您希望限制输入到入口小部件中的自由格式文本的长度。在下面的函数原型中,entry.set_max_length()
将条目的文本限制为最大字符数。当您想要限制用户名、密码或其他长度敏感信息的长度时,这可能很有用。
entry.set_max_length(max_chars)
不可见字符方便了 GTK+ 中的密码输入。不可见字符是替换条目中实际密码内容的字符,可以用entry.
set_invisible_char()
设置。条目的默认字符是星号。
entry.set_invisible_char(single_char)
entry.set_visibility(boolean)
指定不可见字符后,您可以通过使用entry.
set_visibility()
将可见性设置为False
来隐藏所有输入的文本。您仍然能够以编程方式检索条目的实际内容,即使它是隐藏的。
将文本插入 Gtk。入口小部件
在 GTK+ 3.x 中,只有一种方法可以替换Gtk.Entry
小部件中的所有文本。方法entry.
set_text()
用给定的字符串覆盖文本条目的全部内容。但是,只有当您不再关心小部件显示的当前文本时,这才有用。
entry.set_text(text)
用entry.get_text()
可以检索到Gtk.Entry
显示的当前文本。该字符串由小部件内部使用,不得以任何方式释放或修改。也可以使用entry.
insert_text()
将文本插入到Gtk.Entry
小部件中。entry.insert_text()
的参数指定了要插入的文本和要插入文本的字符位置。
微调按钮
Gtk.SpinButton
小部件是一个数字选择小部件,能够处理整数和浮点数。它源于Gtk.Entry
,因此Gtk.SpinButton
继承了它的所有功能和信号。
调整
在介绍Gtk.SpinButton
小部件之前,您必须了解Gtk.Adjustment
类。Gtk.Adjustment
是 GTK+ 中少数几个不被认为是小部件的类之一,因为它直接从Gtk.Object
派生而来。它用于几个小部件,包括旋转按钮、视窗和从Gtk.Range
派生的多个小部件。
使用Gtk.Adjustment.
和new()
创建新的调整。一旦添加到小部件,调整的内存管理由小部件处理,所以您不必担心对象的这一方面。
Gtk.Adjustment.new(initial_value, lower_range, upper_range,
step_increment, page_increment, page_size)
新的调整用六个参数初始化。这些参数的列表如下。
-
initial_value
:调整初始化时存储的值。这对应于Gtk.Adjustment
类的value
属性。 -
lower_range
:调整允许保持的最小值。这对应于Gtk.Adjustment
类的lower
属性。 -
lower_range
:允许调整的最大值。这对应于Gtk.Adjustment
类的upper
属性。 -
step_increment
:使最小变化成为可能的增量。如果您想计算 1 到 10 之间的所有整数,增量将被设置为 1。 -
page_increment
:按下 Page Up 或 Page Down 时的增量。这几乎总是大于 step_increment。 -
page_size
:一页的大小。该值在Gtk.SpinButton
中没有多大用处,所以应该设置为与page_increment
相同的值或 0。
Gtk.Adjustment
类提供了两个有用的信号:changed
和value-changed
。当调整的一个或多个属性被更改时,发出"changed"
信号,不包括值属性。当调整的当前值改变时,发出"value-changed"
信号。
微调按钮示例
微调按钮小部件允许用户通过向上或向下箭头递增或递减来选择整数或浮点数。用户仍然可以用键盘输入一个值,如果超出范围,它将显示为最接近的可接受值。图 5-6 显示了两个显示整数和浮点数的旋转按钮。
图 5-6
微调按钮
微调按钮显示整数或浮点数。实际上,数字存储为double
值。微调按钮用于将数字四舍五入到正确的小数位数。清单 5-6 是一个创建整数和浮点数调节按钮的简单例子。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
integer = Gtk.Adjustment(5.0, 0.0, 10.0, 1.0, 2.0, 2.0)
float_pt = Gtk.Adjustment(5.0, 0.0, 1.0, 0.1, 0.5, 0.5)
spin_int = Gtk.SpinButton()
spin_int.set_adjustment(integer)
spin_int.set_increments(1.0, 0)
spin_int.set_digits(0)
spin_float = Gtk.SpinButton()
spin_float.set_adjustment(float_pt)
spin_float.set_increments(0.1, 0)
spin_float.set_digits(1)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
vbox.pack_start(spin_int, False, False, 5)
vbox.pack_start(spin_float, False, False, 5)
self.add(vbox)
self.set_size_request(180, 100)
self.show_all()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Spin Buttons")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 5-6Integer and Floating-Point Number Selection
在创建微调按钮之前,您应该创建调整。您也可以使用None
调整来初始化微调按钮,但它被设置为不敏感。初始化调整后,您可以使用Gtk.SpinButton.
new()
创建新的微调按钮。初始化函数中的另外两个参数规定了旋钮的爬升率和要显示的小数位数。爬升率是当按下(+)或(-)符号时,该值应该增加或减少的量。
Gtk.SpinButton.new(climb_rate, digits)
或者,您可以使用Gtk.SpinButton.
new_with_range()
创建一个新的微调按钮,它会根据您指定的最小值、最大值和步长值自动创建一个新的调整。初始值默认设置为最小值加上十倍于step_increment
的页面增量。微件的精度自动设置为step_increment
的值。
Gtk.SpinButton.new_with_range (minimum_value, maximum_value, step_increment)
您可以调用spinbutton.
set_digits()
来设置微调按钮的新精度,并调用spinbutton.set_value()
来设置新值。如果该值超出微调按钮的范围,它会自动改变。
spin_button.set_value(value)
水平和垂直刻度
另一种称为 scale 的小部件允许您提供水平或垂直滑块,可以选择整数或浮点数。Gtk.Scale
既是水平缩放小部件,也是垂直缩放小部件。在 GTK+ 2.x 中,Gtk.Scale
是一个抽象类。两个子类Gtk.HScale
和Gtk.VScale
分别用于创建水平和垂直刻度。在 GTK+ 3.x 中,这两个类已经被弃用,而Gtk.Scale
已经成为一个真正的类,可以用来创建水平和垂直的盒子。
Gtk.Scale
widget 的功能和Gtk.SpinButton
没有太大区别。当您想要限制用户输入值时,通常使用它,因为值是通过移动滑块来选择的。图 5-7 显示了两个水平标尺微件的截图。
图 5-7
水平缩放部件
刻度提供了与微调按钮基本相同的功能,只是使用滑块来选择数字。为了展示小部件之间的相似之处,清单 5-7 实现了与清单 5-6 相同的功能:两个滑块允许用户选择一个整数和一个浮点数。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(250, -1)
scale_int = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0.0, 10.0, 1.0)
scale_float = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0.0, 1.0, 0.1)
scale_int.set_digits(0)
scale_float.set_digits(1)
scale_int.set_value_pos(Gtk.PositionType.RIGHT)
scale_float.set_value_pos(Gtk.PositionType.LEFT)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
vbox.pack_start(scale_int, False, False, 5)
vbox.pack_start(scale_float, False, False, 5)
self.add(vbox)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Scales")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 5-7Integer and Floating-Point Number Selection
有多种方法可以创建新的缩放微件。第一个是使用Gtk.Scale.
new()
,它接受一个定义标尺如何工作的Gtk.Adjustment
。
Gtk.Scale.new(adjustment)
或者,您可以使用Gtk.Scale.
new_with_range()
创建刻度。该函数接受刻度的最小值、最大值和步长增量。
Gtk.Scale.new_with_range(minimum, maximum, step)
由于标尺的值总是存储为double
,如果默认值不是您想要的,您需要定义用scale.
set_digits()
显示的小数位数。默认的小数位数是根据为步长增量提供的小数位数计算的。例如,如果您提供的步长增量为 0.01,则默认情况下会显示两位小数。
scale.set_digits (digits)
根据您正在使用的秤部件的类型,您可能希望使用scale.
set_value_pos()
更改值的显示位置。位置由Gtk.PositionType
枚举定义,它们是Gtk.PositionType.LEFT
、Gtk.PositionType.RIGHT
。Gtk.PositionType.TOP
和Gtk.PositionType.BOTTOM
。您也可以使用scale.
set_draw_value()
对用户完全隐藏该值。
scale.set_value_pos(pos)
Gtk.Scale
源自一个名为Gtk.Range
的小工具。这个小部件是一个抽象类型,提供处理调整的能力。你应该使用scale.
和get_value()
来获取当前的刻度值。Gtk.Range
还提供“数值改变”信号,当用户改变刻度位置时发出。
Gtk.Adjustment
小工具也可以与其他小工具共享。一个Gtk.Adjustment
可以与Gtk.SpinButton
和一个Gtk.Scale
小部件共享。有关更多信息,请参见 GTK 文档。
附加按钮
虽然Gtk.Button
小部件允许您创建自己的定制按钮,但是 GTK+ 提供了三个附加的按钮小部件供您使用:颜色选择按钮、文件选择器按钮和字体选择按钮。
涵盖这三个小部件的每一节还涵盖了其他重要的概念,比如Gtk.Color
类、文件过滤器和 Pango 字体。这些概念将在后面的章节中用到,所以现在掌握它们是个好主意。
颜色按钮
Gtk.ColorButton
小部件为您提供了一种简单的方法,允许您的用户选择特定的颜色。这些颜色可以指定为六位十六进制值或 RGB 值。颜色按钮本身在一个矩形块中显示选定的颜色,该矩形块被设置为按钮的子小部件。图 5-8 就是一个例子。
图 5-8
颜色选择对话框
一辆 Gtk。ColorButton 示例
单击颜色按钮会打开一个对话框,允许用户输入颜色值或浏览色轮上的选项。提供色轮是为了使用户不需要知道颜色的数值。清单 5-8 展示了如何在应用中使用Gtk.ColorButton
小部件。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
color = Gdk.RGBA(red=0, green=.33, blue=.66, alpha=1.0)
color = Gdk.RGBA.to_color(color)
button = Gtk.ColorButton.new_with_color(color)
button.set_title("Select a Color!")
label = Gtk.Label("Look at my color!")
label.modify_fg(Gtk.StateType.NORMAL, color)
button.connect("color_set", self.on_color_changed, label)
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
hbox.pack_start(button, False, False, 5)
hbox.pack_start(label, False, False, 5)
self.add(hbox)
def on_color_changed(self, button, label):
color = button.get_color()
label.modify_fg(Gtk.StateType.NORMAL, color)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Color Button")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 5-8Gtk.ColorButton and Gdk.Color
在大多数情况下,你想要创建一个带有初始颜色值的Gtk.ColorButton
,这是通过将一个Gdk.Color
对象指定给button = Gtk.ColorButton.
new_with_color()
来完成的。如果没有提供颜色,默认颜色是禁用 alpha 选项的不透明黑色。
在 Gdk 中存储颜色。颜色
Gdk.Color
是存储颜色的红、绿、蓝值的类。这些值可以使用下面显示的方法来检索或设置。第四个可用值是像素对象。当颜色被分配到颜色映射中时,它会自动存储该颜色的索引,所以您通常不需要更改该值。
创建一个新的Gdk.Color
对象后,如果你已经知道颜色的红色、绿色和蓝色值,你可以用下面的方式指定它们。红色、绿色和蓝色值存储为范围从 0 到 65,535 的无符号整数值,其中 65,535 表示全色强度。例如,下面的颜色指的是白色。
mycolorobj = Gdk.Color.new()
mycolorobj.red = 65535
mycolorobj.green = 65535
mycolorobj.blue = 65535
使用颜色按钮
设置初始颜色后,您可以使用button.
set_title()
选择颜色选择对话框的标题。默认情况下,标题是“选择一种颜色”,所以如果您对这个标题满意,就没有必要设置这个值。
button.get_color()
label.modify_fg(Gtk.StateType.NORMAL, color)
在清单 5-8 中,前景色被设置为正常的窗口小部件状态,这是所有标签的状态,除非它们是可选的。在label.
modify_fg()
中可以使用的Gtk.StateType
枚举有五个选项。您可以通过传递一个None
颜色将小部件的前景色重置为默认值。
文件选择器按钮
Gtk.FileChooserButton
小部件为您提供了一个简单的方法,让您要求用户选择一个文件或文件夹。它实现了 GTK+ 提供的文件选择框架的功能。图 5-9 显示了选择文件夹的文件选择器按钮组和选择文件的按钮组。
图 5-9
文件选择器按钮
当用户单击一个Gtk.FileChooserButton
时,会打开一个Gtk.FileChooserDialog
实例,允许用户浏览并选择一个文件或一个文件夹,这取决于您创建的按钮的类型。
注意
直到第六章的你才学会如何使用Gtk.FileChooserDialog
小部件,但是此时你不需要直接与它交互,因为Gtk.FileChooserButton
处理所有与对话框的交互。
一辆 Gtk。文件选择按钮示例
您可以更改基本设置,例如当前选定的文件、当前文件夹和文件选择窗口的标题。清单 5-9 向您展示了如何使用这两种类型的文件选择器按钮。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from pathlib import Path
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
label = Gtk.Label("")
chooser1 = Gtk.FileChooserButton("Choose a Folder.",
Gtk.FileChooserAction.SELECT_FOLDER)
chooser2 = Gtk.FileChooserButton("Choose a Folder.",
Gtk.FileChooserAction.OPEN)
chooser1.connect("selection_changed",
self.on_folder_changed, chooser2)
chooser2.connect("selection_changed",
self.on_file_changed, label)
chooser1.set_current_folder(str(Path.home()))
chooser2.set_current_folder(str(Path.home()))
filter1 = Gtk.FileFilter()
filter2 = Gtk.FileFilter()
filter1.set_name("Image Files")
filter2.set_name("All Files")
filter1.add_pattern("*.png")
filter1.add_pattern("*.jpg")
filter1.add_pattern("*.gif")
filter2.add_pattern("*")
chooser2.add_filter(filter1)
chooser2.add_filter(filter2)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
vbox.pack_start(chooser1, False, False, 0)
vbox.pack_start(chooser2, False, False, 0)
vbox.pack_start(label, False, False, 0)
self.add(vbox)
self.set_size_request(240, -1)
def on_folder_changed(self,
chooser1, chooser2): folder =
chooser1.get_filename()
chooser2.set_current_folder(folder)
def on_file_changed(self, chooser2, label):
file = chooser2.get_filename()
label.set_text(file)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="File Chooser Button")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 5-9Using the File Chooser Button
文件选择器按钮小部件是用Gtk.FileChooserButton.
new()
创建的。这个小部件有两个用途:选择单个文件或单个文件夹。可以创建四种类型的文件选择器(其余两种在第六章中介绍),但是文件选择器按钮只支持Gtk.FileChooserAction
.OPEN
和Gtk.FileChooserAction.SELECT_FOLDER
。
Gtk.FileChooser
Gtk.FileChooserButton
小部件是由Gtk.FileChooser
类提供的功能的实现。这意味着,尽管按钮不是从Gtk.FileChooser
派生的,但它仍然可以利用Gtk.FileChooser
定义的所有方法。清单 5-9 中相当多的方法利用了Gtk.FileChooser
提供的函数。
在清单 5-9 中,chooser1.
set_current_folder()
用于将每个文件选择器按钮的当前文件夹设置为用户的主目录。当用户最初点击文件选择器按钮时,这个文件夹的内容被显示,除非它通过一些其他方式被改变。如果文件夹被成功修改,这个方法返回True
。
chooser1.set_current_folder(filename)
Path.home()
方法是 Python 提供的一个实用模块,它返回当前用户的主目录。与pathlib
中的大多数特性一样,这种方法是独立于平台的。
这带来了文件选择器界面的一个有用的特性;它可以用来浏览许多类型的文件结构,无论是在 UNIX 还是 Windows 机器上。如果您希望您的应用是为多个操作系统设计的,这一点尤其有用。
由于文件选择器按钮一次只允许选择一个文件,根据文件选择器按钮的类型,您可以使用chooser1.get_filename()
检索当前选择的文件或文件夹。如果没有选择文件,该函数返回None
。
filename = chooser1.get_filename()
至此,您已经有了足够的关于Gtk.FileChooser
类的信息来实现文件选择器按钮。在下一章中,当你学习Gtk.FileChooserDialog
部件时,会更深入地讨论Gtk.FileChooser
。
文件过滤器
Gtk.FileFilter
对象允许你限制文件选择器中显示的文件。例如,在清单 5-9 中,当选择图像文件过滤器时,用户只能查看和选择 PNG、JPG 和 GIF 文件。
文件过滤器是用Gtk.FileFilter.
new()
创建的。因此,您需要使用filefilter.
?? 来设置过滤器类型的显示名称。如果您提供多个过滤器,此名称允许用户在它们之间切换。
filefilter = Gtk.FileFilter.new ();
filefilter.set_name (name)
最后,要完成过滤,您需要添加要显示的文件类型。这样做的标准方式是使用filefilter.
add_pattern()
,如下面的代码片段所示。此功能允许您指定要显示的文件名的格式。通常识别应该显示的文件扩展名可以做到这一点。您可以使用星号字符作为任何类型过滤函数的通配符。
filefilter.add_pattern (pattern)
小费
如清单 5-9 所示,您可能希望提供一个All Files
过滤器来显示目录中的每个文件。为此,您应该创建一个仅将一个模式设置为通配符的过滤器。如果不提供此筛选器,用户将永远无法查看与另一个筛选器提供的模式不匹配的任何文件。
您还可以通过指定多用途互联网邮件扩展(MIME)类型,使用filefilter.
add_mime_type()
指定过滤模式。例如,image/*
显示所有图像 MIME 类型的文件。这个函数的问题是您需要熟悉 MIME 类型。但是,使用 MIME 类型的优点是,您不需要为过滤器指定每个文件扩展名。它允许您归纳到特定 MIME 类别中的所有文件。
filefilter.add_mime_type(mime_type)
创建过滤器后,需要将其添加到文件选择器中,这可以通过filechooser.
add_filter()
来完成。提供过滤器后,默认情况下会在文件选择器中使用第一个指定的过滤器。如果您指定了多个筛选器,用户可以在类型之间切换。
filechooser.add_filter (filter)
字体按钮
Gtk.FontButton
是另一种类型的专用按钮,允许用户选择与当前驻留在用户系统上的字体相对应的字体参数。字体选项是在用户单击按钮时显示的字体选择对话框中选择的。这些选项包括字体名称、样式选项和字体大小。图 5-10 中显示了一个示例Gtk.FontButton
小部件。
图 5-10
字体选择按钮
字体按钮小工具用Gtk.FontButton.
new_with_font()
初始化,允许你指定初始字体。字体以字符串形式提供,格式如下:Family Style Size
。每个参数都是可选的;Gtk.FontButton
的默认字体是 Sans 12,它不提供样式参数。
“系列”是指正式的字体名称,如 Sans、Serif 或 Arial。样式选项因字体而异,但通常包括斜体、粗体和粗斜体。如果选择常规字体样式,则不指定字体样式。大小是文本的磅值,如 12 或 12.5。
一辆 Gtk。FontButton 示例
清单 5-10 创建一个用 Sans Bold 12 字体初始化的Gtk.FontButton
小部件。当按钮中选择的字体改变时,新的字体应用于字体按钮下面的Gtk.Label
小部件。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Pango
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
label = Gtk.Label("Look at the font!")
initial_font = Pango.font_description_from_string("Sans Bold 12")
label.modify_font(initial_font)
button = Gtk.FontButton.new_with_font("Sans Bold 12")
button.set_title("Choose a Font")
button.connect("font_set", self.on_font_changed, label)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
vbox.pack_start(button, False, False, 0)
vbox.pack_start(label, False, False, 0)
self.add(vbox)
def on_font_changed(self, button, label):
font = button.get_font()
desc = Pango.font_description_from_string(font)
buffer = "Font: " + font
label.set_text(buffer)
label.modify_font(desc)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Font Button")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 5-10Using the Font Button
使用字体选择按钮
清单 5-10 中的代码给出了您遇到的Pango.FontDescription
类的第一个样本。Pango.FontDescription
类用于解析字体样式字符串。您可以通过调用如下Pango.
??,从字体字符串(如 Sans Bold 12)创建并使用新的字体描述。
initial_font = Pango.font_description_from_string("Sans Bold 12")
label.modify_font(initial_font)
创建字体描述后,可以调用modify_font()
来设置小部件文本的字体。这个函数编辑由小部件的Gtk.StyleContext
属性存储的字体描述对象。
在清单 5-10 中,当发出“字体设置”信号时,标签的文本被设置为由Gtk.FontButton
存储的字体。使用fontbutton.
get_font_name()
可以检索字体按钮存储的整个字体描述字符串,用于检索标签显示的字体字符串。
fontbutton.get_font_name()
在清单 5-10 中,新的字体样式被应用于Gtk.Label
。但是,如果您将fontbutton.
set_use_font()
和fontbutton.
set_use_size()
设置为True
,字体按钮在呈现其文本时会使用字体系列和字体大小。这允许用户预览字体按钮中的文本。默认情况下,字体按钮是关闭的。
fontbutton.set_use_font(boolean)
fontbutton.set_use_size(boolean)
测试你的理解能力
在这一章中,你学习了一些基本的部件,比如Gtk.Entry
、Gtk.SpinButton
,以及各种类型的开关和按钮。在下面的两个练习中,您将创建两个应用来练习使用这些小部件。
练习 1:重命名文件
在本练习中,使用一个Gtk.FileChooserButton
小部件允许用户选择系统上的一个文件。接下来,使用一个允许用户为文件指定新名称的Gtk.Entry
小部件。(请注意,您可以在 Python 文档中找到本练习所需的文件工具的函数。)
如果文件被成功重命名,您应该禁用Gtk.Entry
小部件和按钮,直到用户选择一个新文件。如果用户没有重命名所选文件的权限,那么Gtk.Entry
小部件和按钮也应该设置为不敏感。完成这个练习后,你可以在附录 d 中找到答案。
这个练习使用了本章中介绍的两个小部件:Gtk.Entry
和Gtk.FileChooserButton
。它还要求您使用 Python 提供的多个实用函数,包括重命名文件和检索现有文件权限信息的函数。
虽然您没有学习任何 Python 文件函数,但是您可能还想尝试一些其他与文件相关的实用函数,例如创建目录、更改文件权限和在整个目录结构中移动的能力。Python 提供了很多功能,值得您在空闲时间研究一下 API 文档。
练习 2:微调按钮和刻度
在本练习中,创建三个小部件:一个微调按钮、一个水平刻度和一个复选按钮。微调按钮和水平刻度应该设置为相同的初始值和界限。如果选择了 check 按钮,两个调整小部件应该同步到相同的值。这意味着当用户更改一个小部件的值时,另一个小部件也会更改为相同的值。
因为这两个小部件都支持整数和浮点数,所以您应该用不同的小数位数来实现这个练习。您还应该练习通过调整和使用方便的初始化器来创建微调按钮和刻度。
摘要
在本章中,您已经了解了以下九个新的小部件,它们为您提供了一种与用户进行交互的有意义的方式。
-
Gtk.ToggleButton
:一种Gtk.Button
小部件,在被点击后保持其活动或不活动状态。当它处于活动状态时,显示为按下。 -
Gtk.CheckButton
:从Gtk.ToggleButton
派生而来,这个小部件被绘制成一个离散的开关,紧挨着显示的文本。这使得它有别于Gtk.Button
。 -
Gtk.RadioButton
:你可以将多个单选按钮部件组合在一起,这样一次只能激活一个开关in the group
。 -
这个小部件允许用户在一行中输入自由格式的文本。它还有助于密码输入。
-
Gtk.SpinButton
:源自Gtk.Entry
,微调按钮允许用户在预定义的范围内选择或输入整数或浮点数。 -
Gtk.Scale
:类似于微调按钮,这个小部件允许用户通过移动垂直或水平滑块来选择整数或浮点数。 -
这种特殊类型的按钮允许用户选择一种特定的颜色和一个可选的 alpha 值。
-
Gtk.FileChooserButton
:这种特殊类型的按钮允许用户选择系统中已经存在的单个文件或文件夹。 -
Gtk.FontButton
:这种特殊类型的按钮允许用户选择字体系列、风格和大小。
在下一章,你将学习如何使用Gtk.Dialog
类创建你自己的自定义对话框,以及 GTK+ 内置的一些对话框。到第六章结束时,你已经很好地掌握了 GTK+ 中最重要的简单部件。从那里,我们继续更复杂的话题。
六、对话框
本章向你介绍一种特殊类型的窗口,叫做对话框。对话框是对顶层窗口的补充。对话框由Gtk.Window
的子类Gtk.Dialog
提供,并扩展了附加功能。这意味着可以在一个或多个对话框中实现整个界面,同时隐藏主窗口。
您可以使用对话框做任何事情,比如显示消息或提示用户选择一个选项。它们的目的是通过提供某种类型的瞬时功能来增强用户体验。
在本章的第一部分,你将学习如何使用Gtk.Dialog
来创建你自己的自定义对话框。下一节介绍 GTK+ 提供的大量内置对话框。最后,您了解了一个名为Gtk.Assistant
的小部件,它允许您创建包含多个页面的对话框;助手旨在帮助用户完成一个多阶段的过程。
在本章中,您将学习以下内容。
-
如何使用
Gtk.Dialog
小部件创建你自己的自定义对话框 -
如何使用
Gtk.MessageDialog
小部件向用户提供一般信息、错误消息和警告 -
如何使用
Gtk.AboutDialog
提供您的申请信息 -
有哪些类型的文件选择器对话框可用
-
使用字体和颜色选择对话框收集信息的方法
-
如何使用
Gtk.Assistant
小部件创建多页面对话框
创建你自己的对话
对话框是一种特殊类型的Gtk.Window
来补充顶层窗口。它可以给用户一个消息,从用户那里检索信息,或者提供一些其他瞬时类型的动作。
对话框小部件被一个不可见的水平分隔符分成两半。顶部是放置对话框用户界面主要部分的地方。下半部分被称为动作区域,它包含一组按钮。单击时,每个按钮都会发出一个唯一的响应标识符,告诉程序员单击了哪个按钮。
在大多数情况下,对话框小部件可以被视为一个窗口,因为它是从Gtk.Window
类派生的。然而,当你有多个窗口时,当对话框是顶层窗口的补充时,应该在对话框和顶层窗口之间建立父子关系。
vbox = mydialog.get_content_area()
Gtk.Dialog
提供对垂直框的访问,该框的底部定义了动作区域。内容区域尚未定义。为了定义它,你从垂直框的开始处开始打包部件。因此你必须总是使用pack_start()
来添加部件到Gtk.Dialog
类中。通过add_button(button_text, response_id)
方法调用,可以很容易地将按钮添加到操作区域。
注意
除了Gtk.Window
提供的其他功能之外,还可以通过创建一个包含所有相同小部件的Gtk.Window
并与set_transient_for()
建立窗口关系来手动实现Gtk.Dialog
的功能。Gtk.Dialog
只是一个提供标准方法的便利小部件。
操作区域和分隔符都打包在对话框的垂直框的末尾。由Gtk.Box
(vbox)提供的保存所有的对话框内容。因为动作区域是在最后打包的,所以您应该使用pack_start()
向Gtk.Dialog
添加小部件,如下所示。
vbox = mydialog.get_ac_area()
vbox.pack_start (child, expand, fill, padding)
通过在框的开始打包部件,操作区域和分隔符总是保持在对话框的底部。
创建消息对话框
Gtk.Dialog
的一个优点是,不管你的对话内容有多复杂,同样的基本概念可以应用于每一个对话。为了说明这一点,我们首先创建一个非常简单的对话框,向用户显示一条消息。图 6-1 是这个对话框的截图。
图 6-1
以编程方式创建的消息对话框
清单 6-1 创建了一个简单的对话框,当按钮发出被点击的信号时通知用户。这个功能是由Gtk.MessageDialog
小部件提供的,这将在本章后面的章节中介绍。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
button = Gtk.Button.new_with_mnemonic("_Click Me")
button.connect("clicked", self.on_button_clicked, self)
self.add(button)
self.set_size_request(150, 50)
def on_button_clicked(self, button, parent):
dialog = Gtk.Dialog(title="Information", parent=parent,
flags=Gtk.DialogFlags.MODAL)
dialog.add_button("Ok", Gtk.ResponseType.OK)
label = Gtk.Label("The button was clicked.")
image = Gtk.Image.new_from_icon_name("dialog-information",
Gtk.IconSize.DIALOG)
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
hbox.pack_start(image, False, False, 0)
hbox.pack_start(label, False, False, 0)
dialog.vbox.pack_start(hbox, False, False, 0)
dialog.show_all()
dialog.run()
dialog.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Dialogs")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 6-1Your First Custom Dialog
创建对话
当单击主窗口中的按钮时,您需要做的第一件事是用Gtk.Dialog.new_with_buttons()
创建Gtk.Dialog
小部件。该函数的前两个参数指定了对话框的标题、指向父窗口的指针和设备标志。
dialog = Gtk.Dialog(title="Information", parent=parent, flags=Gtk.DialogFlags.MODA
该对话框被设置为父窗口的临时窗口,这允许窗口管理器将对话框置于主窗口的中心,并在必要时保持在顶部。这可以通过调用window.set_transient_for()
对任意窗口实现。如果您不希望对话框拥有或识别父窗口,也可以提供None
。
接下来,您可以指定一个或多个对话框标志。该参数的选项由Gtk.DialogFlags
枚举给出。有三个可用值,如下表所示。
-
Gtk.DialogFlags.MODAL
:强制对话框在父窗口顶部保持焦点直到关闭。用户被阻止与父母交互。 -
Gtk.DialogFlags.DESTROY_WITH_PARENT
:当父对话框被销毁时,销毁对话框,但不要强制对话框成为焦点。这将创建一个非模态对话框,除非您调用dialog.run()
。 -
Gtk.DialogFlags.USE_HEADER_BAR
:在标题栏而不是动作区创建一个带有动作的对话框。
在清单 6-1 中,指定Gtk.DialogFlags.MODAL
创建了一个模态对话框。没有必要指定标题或父窗口;这些值可以设置为None
。但是,您应该始终设置标题,以便它可以在窗口管理器中绘制;否则,用户很难选择想要的窗口。
在清单 6-1 中,一个响应为Gtk.ResponseType.OK
的 OK 按钮被添加到对话框中。
在 GTK+ 2.x 中,默认情况下,所有对话框都在对话框的主要内容和操作区域之间放置了一个水平分隔符。该分隔符在 GTK+ 3.x 中已被否决。
创建子部件后,需要将它们添加到对话框中。如前所述,通过调用box.pack_start()
将子部件添加到对话框中。该对话框打包如下。
image = Gtk.Image.new_from_icon_name("dialog-information", Gtk.IconSize.DIALOG)
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
hbox.pack_start(image, False, False, 0)
hbox.pack_start(label, False, False, 0)
dialog.vbox.pack_start(hbox, False, False, 0)
此时,您需要显示对话框及其子部件,因为dialog.run()
只在对话框本身上调用dialog.show()
。为此,调用对话框上的dialog.show_all()
。如果不显示小部件,当调用dialog.run()
时,只有分隔符和操作区域可见。
响应标识符
当一个对话框被完全构建时,显示对话框的一种方法是调用dialog.run()
。这个函数在完成时返回一个名为响应标识符的整数。它还可以防止用户与对话框之外的任何东西进行交互,直到对话框被破坏或操作区域按钮被单击。
dialog.run()
在内部,dialog.run()
为对话框创建一个新的主循环,它阻止你与它的父窗口交互,直到发出一个响应标识符或者用户关闭对话框。不管你设置了什么对话框标志,当你调用这个方法时,对话框总是模态的,因为它调用了dialog.set_modal()
。
如果使用窗口管理器提供的方法手动销毁对话框,则返回Gtk.ResponseType.NONE
;否则,dialog.run()
返回引用被点击按钮的响应标识符。来自Gtk.ResponseType
枚举的可用响应标识符的完整列表如表 6-1 所示。您应该总是使用标识符的可用值,而不是随机整数值,因为它们在 GTK+ 的未来版本中可能会改变。
表 6-1
Gtk。ResponseType 枚举值
|标识符
|
价值
|
描述
|
| --- | --- | --- |
| Gtk.ResponseType.NONE
| –1 | 如果操作小部件没有响应 ID,或者对话框以编程方式隐藏或破坏,则返回。 |
| Gtk.ResponseType.APPLY
| –10 | 由 GTK+ 对话框中的应用按钮返回。 |
| Gtk.ResponseType.HELP
| –11 | 由 GTK+ 对话框中的帮助按钮返回。 |
| Gtk.ResponseType.REJECT
| –2 | 通用响应 ID,不被 GTK+ 对话框使用。 |
| Gtk.ResponseType.ACCEPT
| –3 | 通用响应 ID,不被 GTK+ 对话框使用。 |
| Gtk.ResponseType.DELETE_EVENT
| –4 | 如果对话框被删除,则返回。 |
| Gtk.ResponseType.OK
| –5 | 由 GTK + 对话框中的 OK 按钮返回。 |
| Gtk.ResponseType.CANCEL
| –6 | 由 GTK+ 对话框中的取消按钮返回。 |
| Gtk.ResponseType.CLOSE
| –7 | 由 GTK+ 对话框中的关闭按钮返回。 |
| Gtk.ResponseType.YES
| –8 | 由 GTK + 对话框中的 Yes 按钮返回。 |
| Gtk.ResponseType.No
| –9 | GTK+ 对话框中没有按钮返回。 |
当然,当您创建自己的对话框以及使用接下来几页中介绍的许多内置对话框时,您可以自由选择使用哪个响应标识符。但是,您应该努力抵制将Gtk.ResponseType.CANCEL
标识符应用到 OK 按钮的冲动,或者其他类似的荒谬行为。
注意
您可以自由地创建自己的响应标识符,但是您应该使用正数,因为所有的内置标识符都是负数。这允许您在 GTK+ 的未来版本中添加更多标识符时避免冲突。
对话框返回响应标识符后,需要确保调用dialog.destroy()
,否则会导致内存泄漏。GTK+ 确保对话框的所有子对话框都被销毁,但是你需要记住启动这个过程。
通过调用dialog.destroy()
,父节点的所有子节点都被销毁,其引用计数下降。当对象的引用计数达到零时,该对象被终结,其内存被释放。
Gtk。图像小部件
清单 6-1 引入了另一个名为Gtk.Image
的新部件。图像可以以多种方式加载,但Gtk.Image
的一个优点是,如果加载失败,它会显示指定的图像“图像缺失”。它也是从Gtk.Widget
派生出来的,所以不像其他图像对象,比如Gdk.Pixbuf
,它可以作为容器的子对象添加。
在我们的例子中,new_from_icon_name()
从一个命名的主题项目创建了Gtk.Image
小部件。
image = Gtk.Image.new_from_icon_name("dialog-information", Gtk.IconSize.DIALOG)
加载图像时,您还需要指定图像的大小。GTK+ 自动寻找给定尺寸的股票图标,如果找不到,就把图像调整到那个尺寸。可用的大小参数由Gtk.IconTheme
枚举指定,如下表所示。
-
Gtk.IconSize.INVALID
:未指定尺寸 -
Gtk.IconSize.MENU
16×16 像素 -
Gtk.IconSize.SMALL_TOOLBAR
18×18 像素 -
Gtk.IconSize.LARGE_TOOLBAR
: 24×24 像素 -
Gtk.IconSize.BUTTON
: 24×24 像素 -
Gtk.IconSize.DND
32×32 像素 -
Gtk.IconSize.DIALOG
48×48 像素
正如你所看到的,主题Gtk.Image
对象通常用于较小的图像,比如那些出现在按钮、菜单和对话框中的图像,因为主题图像是以离散的标准尺寸提供的。在清单 6-1 中,图像被设置为Gtk.IconSize.DIALOG
或 48×48 像素。
为Gtk.Image
提供了多个初始化函数,这些函数在 API 文档中有描述,但是new_from_file()
和new_from_pixbuf()
对本书后面的例子尤其重要。
Gtk.Image.new_from_file(filename)
Gtk.Image
自动检测指定给new_from_file()
的文件的图像类型。如果图像无法加载,它会显示一个损坏的图像图标。因此,这个函数永远不会返回一个None
对象。Gtk.Image
还支持图像文件中出现的动画。
调用new_from_pixbuf()
会在之前初始化的Gdk.Pixbuf
中创建一个新的Gtk.Image
小部件。与new_from_file()
不同的是,您可以使用该函数轻松判断图像是否成功加载,因为您首先需要创建一个Gdk.Pixbuf
。
Gdk.Image.new_from_pixbuf(pixbuf)
您需要注意的是,Gtk.Image
创建了它自己对Gdk.Pixbuf
的引用,所以如果它应该被Gtk.Image
销毁,您需要释放对该对象的引用。
非模态消息对话框
通过调用dialog.run()
,你的对话框总是被设置为模态的,这并不总是令人满意的。要创建非模态对话框,您需要连接到Gtk.Dialog
的响应信号。
在清单 6-2 中,图 6-1 中的消息对话框被重新实现为非模态对话框。您应该尝试连续多次单击主窗口中的按钮。这展示了如何不仅可以创建同一个对话框的多个实例,还可以从非模态对话框访问主窗口。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
button = Gtk.Button.new_with_mnemonic("_Click Me")
button.connect("clicked", self.on_button_clicked, self)
self.add(button)
self.set_size_request(150, 50)
self.show_all()
def on_button_clicked(self, button, parent):
dialog = Gtk.Dialog(title="Information", parent=parent)
dialog.add_button("Ok", Gtk.ResponseType.OK)
label = Gtk.Label("The button was clicked.")
image = Gtk.Image.new_from_icon_name("dialog-information",
Gtk.IconSize.DIALOG)
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
hbox.pack_start(image, False, False, 0)
hbox.pack_start(label, False, False, 0)
dialog.vbox.pack_start(hbox, False, False, 0)
dialog.connect("response", self.on_dialog_button_clicked)
dialog.show_all()
def on_dialog_button_clicked(self, dialog, response):
dialog.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Dialogs")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 6-2A Nonmodal Message Dialog
创建一个非模态对话框与前面的例子非常相似,除了你不想调用dialog.run()
。通过调用这个函数,不管对话框标志是什么,都会通过阻塞父窗口的主循环来创建一个模态对话框。
小费
通过设置Gtk.DialogFlags.MODAL
标志,你仍然可以创建一个不使用dialog.run()
的模态对话框。然后,您可以连接到响应信号。这个函数只是提供了一种在一个函数中创建模态对话框和处理响应标识符的便捷方式。
通过连接到Gtk.Dialog
的响应信号,您可以等待一个响应标识符被发出。通过使用此方法,当发出响应标识符时,对话框不会自动取消引用。响应回调方法接收对话框、发出的响应标识符和可选的数据参数。
设计对话框时,你必须做出的最重要的决定之一就是它是模态的还是非模态的。根据经验,如果动作需要在用户继续使用应用之前完成,那么对话框应该是模态的。例如消息对话框、向用户提问的对话框和打开文件的对话框。
如果在对话框打开时,用户没有理由不能继续工作,您应该使用非模态对话框。您还需要记住,可以创建非模态对话框的多个实例,除非您以编程方式阻止这种情况,因此必须只有一个实例的对话框应该创建为模态对话框。
另一个对话框示例
既然您已经从头开始创建了一个简单的消息对话框,那么是时候创建一个更复杂的对话框了。在清单 6-3 中,使用 Python 的实用函数传播了一些关于用户的基本信息。如图 6-2 所示的对话框允许你编辑每条信息。
图 6-2
一个简单的 Gtk。对话框小部件
当然,这些信息在用户的系统中实际上并没有改变;新文本被简单地输出到屏幕上。这个例子说明了这样一个事实,不管对话有多复杂,如何处理响应标识符的基本原则仍然是唯一需要的。
您也可以很容易地将它实现为非模态对话框,尽管这没有多大用处,因为对话框本身就是应用的顶层窗口。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
import os
import getpass
import socket
import pwd
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
button = Gtk.Button.new_with_mnemonic("_Click Me")
button.connect("clicked", self.on_button_clicked, self)
self.add(button)
self.set_size_request(180, 50)
self.show_all()
def on_button_clicked(self, button, parent):
dialog = Gtk.Dialog(title="Edit User Information",
parent=parent, flags=Gtk.DialogFlags.MODAL)
dialog.add_button("Ok", Gtk.ResponseType.OK)
dialog.add_button("Cancel", Gtk.ResponseType.CANCEL)
dialog.set_default_response(Gtk.ResponseType.OK)
lbl1 = Gtk.Label("User Name:")
lbl2 = Gtk.Label("Real Name:")
lbl3 = Gtk.Label("Home Dir:")
lbl4 = Gtk.Label("Host Name:")
user = Gtk.Entry()
real_name = Gtk.Entry()
home = Gtk.Entry()
host = Gtk.Entry()
user.set_text(getpass.getuser())
real_name.set_text(pwd.getpwuid(os.getuid())[4])
home.set_text(os.environ['HOME'])
host.set_text(socket.gethostname())
grid = Gtk.Grid()
grid.attach(lbl1, 0, 0, 1, 1)
grid.attach(lbl2, 0, 1, 1, 1)
grid.attach(lbl3, 0, 2, 1, 1)
grid.attach(lbl4, 0, 3, 1, 1)
grid.attach(user, 1, 0, 1, 1)
grid.attach(real_name, 1, 1, 1, 1)
grid.attach(home, 1, 2, 1, 1)
grid.attach(host, 1, 3, 1, 1)
dialog.vbox.pack_start(grid, False, False, 5)
dialog.show_all()
result = dialog.run()
if result == Gtk.ResponseType.OK:
print("User Name: " + user.get_text())
print("Real Name: " +
real_name.get_text()) print("Home: " +
home.get_text()) print("Host: " +
host.get_text())
dialog.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Simple Dialog")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 6-3Editing Information in a Dialog
处理任何模式对话框的正确方法是使用响应标识符,根据单击的按钮获得正确的响应。由于只需要故意检测一个响应,所以在清单 6-3 中使用了一个条件 if 语句。
然而,让我们假设您需要处理多个响应标识符。在这种情况下,if
语句将是一个更好的解决方案,因为创建它是为了比较单个变量和多个选择,如下面的代码片段所示。
result = dialog.run()
if result == Gtk.ResponseType.OK:
# ... Handle result ...
elif result == Gtk.ResponseType.APPLY:
# ... Handle result ...
else:
# ... Handle default result ...
dialog.destroy()
内置对话框
GTK+ 已经内置了许多类型的对话框。虽然本章没有涵盖所有可用的对话框,但是您可以很好地理解使用任何内置对话框所需的概念。本节包括Gtk.MessageDialog
、GtkAboutDialog
、Gtk.FileChooserDialog
、Gtk.FontChooserDialog
和Gtk.ColorChooserDialog
。
消息对话框
消息对话框提供四种类型的信息性消息之一:一般信息、错误消息、警告和问题。这种类型的对话框决定了要显示的图标、对话框的标题和要添加的按钮。
还提供了一种通用类型,它对消息的内容不做任何假设。在大多数情况下,您不会想要使用它,因为所提供的四种类型可以满足您的大部分需求。
重新创建Gtk.MessageDialog
小部件非常简单。前两个例子实现了一个简单的消息对话框,但是Gtk.MessageDialog
已经提供了这个功能,所以您不需要重新创建这个小部件。使用Gtk.MessageDialog
节省了输入时间,也避免了多次重新创建这个小部件的需要,因为大多数应用都会大量使用Gtk.MessageDialog
。它还为所有 GTK+ 应用的消息对话框提供了统一的外观。
图 6-3 显示了一个Gtk.MessageDialog
的例子(与图 6-1 比较),它给用户一个按钮被点击信号的视觉通知。
图 6-3
在 Gtk.MessageDialog 小部件中
由于消息的内容并不重要,因此其类型被设置为一般消息。可以使用清单 6-4 中所示的代码生成这个消息对话框。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
button = Gtk.Button.new_with_mnemonic("_Click Me")
button.connect("clicked", self.on_button_clicked, self)
self.add(button)
self.set_size_request(150, 50)
def on_button_clicked(self, button, parent):
dialog = Gtk.MessageDialog(type=Gtk.MessageType.INFO, parent=parent,
flags=Gtk.DialogFlags.MODAL,
buttons=("Ok", Gtk.ResponseType.OK),
text="The button was clicked.",
title="Information")
dialog.run()
dialog.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Dialogs")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 6-4Using a Gtk.MessageDialog
单击主窗口中的按钮后,该示例创建一个新的Gtk.MessageDialog
。
如果需要,父窗口可以设置为None
,但大多数情况下,应该建立父子关系。如果没有设置父窗口小部件,消息对话框将不会在父窗口上方居中。
用户应该立即处理消息对话框,因为它们呈现了某种需要用户注意的重要消息或关键问题。由于没有设置父窗口,消息对话框很容易被忽略,这在大多数情况下不是我们想要的。
dialog = Gtk.MessageDialog.(type=Gtk.MessageType.INFO, parent=parent, \
flags=Gtk.DialogFlags.MODAL, \
buttons=("Ok", Gtk.ResponseType.OK), \
text="The button was clicked.", \
title="Information")
您可以指定一个或多个对话框标志。这个参数的选项由前面三个例子中创建定制对话框时使用的Gtk.DialogFlags
枚举给出。
与 GTK+ 2.x 不同,3.x Gtk.MessageDialog
不使用任何位置参数。相反,它专门使用关键字参数。还要注意,Gtk.MessageDialog
没有使用new
方法。这是因为Gtk.MessageDialog
创建了Gtk.MessageDialog
的子类,关键字决定了创建什么样的子类。
另请注意,消息对话框中缺少灯泡图标图像。这是因为 GTK+ 3.x 中的理念变化。如果你的对话框中必须有图标,那么你需要使用Gtk.Dialog
来手工创建你的对话框。
通过使用"buttons"
关键字包含一个逗号分隔的按钮/响应 id 列表,支持多个按钮。
您无法控制提供给Gtk.MessageDialog
的消息的可视格式。如果您想使用 Pango 文本标记语言来格式化消息对话框的文本,您可以在Gtk.MessageDialog
调用中省略“Text”关键字。然后用一串 Pango 标记调用set_markup(str)
方法来设置消息的文本。
可以在消息对话框中添加第二个文本,这使得第一条消息用format_secondary_text()
设置为粗体。提供给这个函数的文本字符串应该类似于 C printf()
所支持的格式。
这个特性非常有用,因为它允许你在主要文本中给出一个快速的摘要,然后在次要文本中详细描述。
关于对话框
Gtk.AboutDialog
小部件为您提供了一种向用户提供应用信息的简单方法。当选择帮助菜单中的项目时,通常会显示此对话框。然而,由于菜单直到第十章才被覆盖,我们的示例对话框被用作顶层窗口。
使用Gtk.AboutDialog
显示各种类型的信息,包括应用的名称、版权、当前版本、许可内容、作者、文档编制者、艺术家和翻译人员。因为应用不会包含所有这些信息,所以每个属性都是可选的。主窗口仅显示基本信息,与图 6-4 中的作者名单一起显示。
图 6-4
一个关于信用对话框和作者信用
通过点击“演职员表”按钮,向用户呈现所提供的任何作者、文档编制者、翻译者和艺术家。许可证按钮弹出一个新的对话框,显示给定的许可证内容。
清单 6-5 是一个简单的例子,展示了如何使用Gtk.AboutDialog
小部件的每个可用属性。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GdkPixbuf
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
button = Gtk.Button.new_with_mnemonic("_Click Me")
button.connect("clicked", self.on_button_clicked, self)
self.add(button)
self.set_size_request(150, 50)
self.show_all()
def on_button_clicked(self, button, parent):
authors = ["Author #1", "Author #2"]
documenters = ["Documenter #1", "Documenter
#2"] dialog = Gtk.AboutDialog(parent=parent)
logo = GdkPixbuf.Pixbuf.new_from_file("./logo.png")
if logo != None:
dialog.set_logo(logo)
else:
print("A GdkPixbuf Error has occurred.")
dialog.set_name("Gtk.AboutDialog")
dialog.set_version("3.0")
dialog.set_copyright("(C) 2007 Andrew Krause")
dialog.set_comments("All about Gtk.AboutDialog")
dialog.set_license("Free to all!")
dialog.set_website("http://book.andrewKrause.net")
dialog.set_website_label("book.andrewkrause.net")
dialog.set_authors(authors)
dialog.set_documenters(documenters)
dialog.set_translator_credits("Translator #1\nTranslator #2")
dialog.connect("response", self.on_dialog_button_clicked)
dialog.run()
def on_dialog_button_clicked(self, dialog, response):
dialog.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="About Dialog")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 6-5Using a Gtk.AboutDialog
在创建自己的Gtk.AboutDialog
实例时,您可以设置许多属性。表 6-2 总结了清单 6-5 中使用的选项。如果未指定许可证,则许可证按钮不可见。如果没有演职员表,则演职员表按钮不可见。
表 6-2
Gtk。关于对话框选项值
|财产
|
描述
|
| --- | --- |
| 程序名称 | 应用的名称。 |
| 版本 | 用户正在运行的应用的当前版本。 |
| 版权 | 不应超过一两行的短版权字符串。 |
| 评论 | 应用的简短描述,不应超过一两行。 |
| 许可证 | 在辅助对话框中显示的许可信息。将此项设置为None
会隐藏许可证按钮。 |
| 网站 | 应用的主页 URL。 |
| 网站 _ 标签 | 代替 URL 显示的标签。 |
| 作者 | 为项目贡献代码的作者的 Python 列表。 |
| 艺术家 | 为项目创作图形的艺术家的 Python 列表。 |
| 资料员 | 为项目编写文档的文档人员的 Python 列表。 |
| 译者 _ 学分 | 一个字符串,它指定当前语言的翻译人员。 |
| 标志;徽标 | 这个Gdk.Pixbuf
对象通常是从文件中加载的,是应用的徽标。 |
与作者、艺术家和文档制作者名单不同,译者名单只有一个字符串。这是因为翻译字符串应该设置为翻译当前使用的语言的人。国际化和gettext
不是本书的主题。更多信息请访问 www.gnu.org/software/gettext
。
皮克斯布夫足球俱乐部
GdkPixbuf
是一个包含存储在内存中的图像信息的类。它允许您通过放置形状或像素来手动构建图像,或者从文件中加载预构建的图像。在大多数情况下,后者是首选,所以这就是本书所涵盖的内容。
由于GdkPixbuf
来源于GObject
,所以支持引用。这意味着通过使用ref()
增加引用计数,可以在一个程序的多个位置使用同一个图像。几乎在所有情况下都会自动取消对GdkPixbuf
对象(pixbufs)的引用。
要从文件中加载 pixbuf,可以使用清单 6-5 中使用的GdkPixbuf.new_from_file()
。该函数加载图像,初始尺寸设置为图像的实际尺寸。
logo = GdkPixbuf.Pixbuf.new_from_file("./logo.png")
加载图像后,您可以使用scale_simple()
调整其大小。该功能接受Gdk.Pixbuf
的新尺寸参数和用于缩放的插值模式。
pixbuf.scale_simple(dest_width, dest_height, interp_type)
以下是四种GdkPixbuf.InterpType
模式。
-
GdkPixbuf.InterpType.NEAREST
:对最近的相邻像素进行采样。这种模式非常快,但它产生的缩放质量最低。它不应该被用来缩小图像的尺寸! -
GdkPixbuf.InterpType.TILES
:此模式将每个像素渲染为一种颜色,并对边缘使用抗锯齿。这类似于使用GdkPixbuf.InterpType.NEAREST
放大图像或使用GdkPixbuf.InterpType.BILINEAR
缩小图像。 -
这种模式是在两个方向上调整图像大小的最佳模式,因为它在速度和图像质量之间取得了平衡。
-
虽然这种方法质量很高,但速度也很慢。它应该只在速度不是问题的时候使用。因此,它不应用于任何用户期望快速显示的应用。在一次函数调用中,
GdkPixbuf.new_from_file_at_size()
方便地在从文件中加载图像后立即调整其大小。
在GdkPixbuf
类中提供了许多其他的特性,但是根据需要,只介绍了其中的一些。关于GdkPixbuf
的更多信息,您应该参考 API 文档。
Gtk.文件选择器对话框
在上一章中,您学习了Gtk.FileChooser
和Gtk.FileChooserButton
小部件。回想一下,Gtk.FileChooser
不是一个小部件,而是一个抽象类。抽象类不同于真实类,因为它们可能不实现它们声明的方法。
GTK+ 提供了以下三个小部件,它们是Gtk.FileChooser
类的子类。
-
Gtk.FileChooserButton
:文件选择器按钮在前一章中已经介绍过了。它允许用户通过点击显示一个Gtk.FileChooser
对话框来选择一个文件或文件夹。 -
Gtk.FileChooserDialog
:这是允许用户选择文件夹的实际部件。它还可以帮助创建文件夹或保存文件。当您使用Gtk.FileChooserDialog
时,您实际上是在使用一个打包到Gtk.Dialog
中的文件选择器小部件。 -
Gtk.FileChooserWidget
:这是允许用户选择文件夹的实际部件。它还可以帮助创建文件夹或保存文件。当您使用Gtk.FileChooserDialog
时,您实际上是在使用一个打包到Gtk.Dialog
中的文件选择器小部件。
你已经学习了Gtk.FileChooserButton
并且使用了文件选择器来打开一个文件和选择一个目录。文件选择器小部件还提供了其他三种功能。在接下来的三个示例中,您将学习如何使用文件选择器对话框来保存文件、创建目录以及选择多个文件。
保存文件
图 6-5 显示了一个正在保存文件的Gtk.FileChooserDialog
小部件。你会注意到它也和下面的两个图相似,因为所有类型的文件选择对话框都有一致的外观,所以它对新用户来说是最小的混淆,对所有人来说是最大的效率。该小部件还使用相同的代码来实现每种对话框类型,以最大限度地减少必要的代码量。
图 6-5
用于保存的文件选择器对话框
文件选择器对话框的使用方式与本章前面两个对话框相同,除了你需要处理由Gtk.Dialog.new()
返回的响应代码。清单 6-6 允许用户选择一个文件名,如果返回正确的响应标识符,则将按钮的文本设置为该文件名。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(200, 100)
button = Gtk.Button.new_with_label("Save as ...")
button.connect("clicked", self.on_button_clicked, self)
self.add(button)
def on_button_clicked(self, button, parentwin):
dialog = Gtk.FileChooserDialog(title="Save file as ...",
parent=parentwin,
action=Gtk.FileChooserAction.SAVE,
buttons=("_Cancel",
Gtk.ResponseType.CANCEL,
"_Save", Gtk.ResponseType.ACCEPT))
response = dialog.run()
if response == Gtk.ResponseType.ACCEPT:
filename = dialog.get_filename()
button.set_label(filename)
dialog.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Save a File")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 6-6Using a Gtk.AboutDialog
无论您选择什么选项,所有文件选择器对话框都是用Gtk.FileChooserDialog()
创建的。与其他对话框一样,首先要设置对话框的标题和父窗口。应该总是设置父窗口,因为文件选择对话框应该是模态的。
dialog = Gtk.FileChooserDialog(title="Save file as ...", \
parent=parentwin, \
action=Gtk.FileChooserAction.SAVE, \
buttons=("_Cancel", Gtk.ResponseType.CANCEL, \
"_Save", Gtk.ResponseType.ACCEPT))
接下来,与文件选择器按钮一样,您必须选择创建的文件选择器的动作。由Gtk.FileChooser
抽象类提供的所有四种动作类型对于Gtk.FileChooserDialog
都是可用的。这些在下面的列表中描述。
-
Gtk.FileChooserAction
.SAVE
:提示用户输入文件名,并浏览整个文件系统寻找位置。返回的文件是所选择的路径,新文件名附加在末尾。Gtk.FileChooser
提供了一些方法,允许您在用户输入一个已经存在的文件名时请求确认。 -
Gtk.FileChooserAction
.OPEN
:文件选择器只允许用户选择一个或多个已经存在于用户系统中的文件。用户能够浏览整个文件系统或选择一个书签位置。 -
Gtk.FileChooserAction.SELECT_FOLDER
:这非常类似于保存动作,因为它允许用户选择一个位置并指定一个新的文件夹名。用户可以输入文件选择器返回时创建的新文件夹名,或者点击创建文件夹按钮,如图 5-6 所示,在当前目录下创建一个新文件夹。 -
Gtk.FileChooserAction
.CREATE_FOLDER
:这非常类似于保存动作,因为它允许用户选择一个位置并指定一个新的文件夹名。用户可以输入文件选择器返回时创建的新文件夹名,或者点击【创建文件夹】按钮,如图 5-6 所示,在当前目录下创建一个新文件夹。
最后,您必须提供按钮的名称/响应 ID 列表,以添加到操作区域。在清单 6-6 中,当点击取消按钮时,发出Gtk.ResponseType.CANCEL
,当点击保存按钮时,发出GTK_RESPONSE_ACCEPT
。
创建文件夹
GTK+ 不仅允许你选择一个文件夹,还允许你创建一个文件夹。使用这种类型的Gtk.FileChooserDialog
小部件可以在图 6-6 中看到,这是清单 6-7 的截图。
图 6-6
用于创建文件夹的文件选择器对话框
清单 6-7 中的对话框在被用户接受时处理创建新文件夹,所以除了销毁对话框之外,你不需要采取任何进一步的行动。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(200, 100)
button = Gtk.Button.new_with_label("Create a Folder ...")
button.connect("clicked", self.on_button_clicked, self)
self.add(button)
def on_button_clicked(self, button, parentwin):
dialog = Gtk.FileChooserDialog(title="Create a Folder ...",
parent=parentwin,
action=Gtk.FileChooserAction.SAVE,
buttons=("_Cancel",
Gtk.ResponseType.CANCEL,
"_Ok", Gtk.ResponseType.OK))
response = dialog.run()
if response == Gtk.ResponseType.OK:
filename = dialog.get_filename()
print("Creating directory: %s\n" % filename)
dialog.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Create Folder")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 6-7Using a Gtk.AboutDialog
对话框的完整文件夹名可以通过使用与前面示例中检索文件名相同的函数get_filename()
来检索。来自os
模块的标准os.mkdir()
方法在所有支持的操作系统上的指定位置创建一个文件夹。
选择多个文件
图 6-7 显示了一个允许用户选择文件的标准文件选择器对话框。使用Gtk.FileChooserAction
.OPEN
类型的Gtk.FileChooserDialog
和Gtk.FileChooserButton
的区别在于对话框可以选择多个文件,而按钮只能选择一个文件。
图 6-7
用于选择多个文件的文件选择器对话框
清单 6-8 向您展示了如何处理多个文件选择。除了选择在 Python 列表中返回这一事实之外,它与单个文件选择非常相似。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(200, 100)
button = Gtk.Button.new_with_label("Open file(s) ...")
button.connect("clicked", self.on_button_clicked, self)
self.add(button)
def on_button_clicked(self, button, parentwin):
dialog = Gtk.FileChooserDialog(title="Open file(s) ...",
parent=parentwin,
action=Gtk.FileChooserAction.OPEN,
buttons=("_Cancel",
Gtk.ResponseType.CANCEL,
"_Open", Gtk.ResponseType.ACCEPT))
dialog.set_select_multiple(True)
response = dialog.run()
if response == Gtk.ResponseType.ACCEPT:
filenames = dialog.get_filenames()
i = 0
while i < len(filenames):
file = filenames[i]
print(file + " was selected.")
i += 1
dialog.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Open Nultiple Files")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 6-8Using A Gtk.FileChooserDialog to Select Multiple Files
函数的作用是:返回所选文件的 Python 列表。
filenames = dialog.get_filenames()
颜色选择对话框
在前一章中,您了解了Gtk.ColorButton
小部件,它允许用户选择一种颜色。单击该按钮后,用户会看到一个对话框。虽然当时没有指定,但是这个对话框是一个Gtk.ColorSelectionDialog
小部件。
与Gtk.FileChooserDialog
类似,颜色选择对话框实际上是一个Gtk.Dialog
容器,其中一个Gtk.ColorSelection
小部件被打包为其子小部件。Gtk.ColorSelection
可轻松独立使用。然而,由于对话框是呈现小部件的自然方式,GTK+ 提供了Gtk.ColorSelectionDialog
。颜色选择对话框如图 6-8 所示。
图 6-8
用于选择多个文件的文件选择器对话框
清单 6-9 包含一个有两个按钮的顶层窗口。当第一个按钮被点击时,一个模态Gtk.ColorSelectionDialog
被创建。另一个按钮创建一个非模态Gtk.ColorSelectionDialog
。每个选项选择全局颜色和不透明度值。
此示例还循环遍历程序参数,设置初始颜色值(如果提供的话)。这允许您在启动应用时传递初始颜色。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
global_color = Gdk.RGBA(red=.50, green=.50, blue=.50,
alpha=1.0).to_color() global_alpha = 65535
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(200, 100)
modal = Gtk.Button.new_with_label("Modal")
nonmodal = Gtk.Button.new_with_label("Non-Modal")
modal.connect("clicked", self.on_run_color_selection_dialog,
self, True)
nonmodal.connect("clicked", self.on_run_color_selection_dialog,
self, False)
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
hbox.pack_start(modal, False, False, 5)
hbox.pack_start(nonmodal, False, False, 5)
self.add(hbox)
def on_dialog_response(self, dialog, result):
if result == Gtk.ResponseType.OK:
colorsel = dialog.get_color_selection()
alpha = colorsel.get_current_alpha()
color = colorsel.get_current_color()
print(color.to_string())
global_color = color
global_alpha = alpha
dialog.destroy()
def on_run_color_selection_dialog(self, button, window, domodal):
if domodal:
title = ("Choose Color -- Modal")
else:
title = ("Choose Color -- Non-Modal")
dialog = Gtk.ColorSelectionDialog(title=title, parent=window,
modal=domodal)
colorsel = dialog.get_color_selection()
colorsel.set_has_opacity_control(True)
colorsel.set_current_color(global_color)
dialog.connect("response", self.on_dialog_response)
dialog.show_all()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self,
title="Color Selection Dialog”)
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 6-9Using a Gtk.ColorSelectionDialog
Gtk.ColorSelectionDialog
类提供的唯一函数是Gtk.ColorSelectionDialog()
。下面的代码可以得到选中的颜色。
alpha = colorsel.get_current_alpha()
color = colorsel.get_current_color()
print(color.to_string())
Gtk.ColorSelectionDialog
提供对其四个子部件的直接访问。首先,colorsel 是一个Gtk.ColorSelection
小部件,它有助于颜色选择。另外三个是确定按钮、取消按钮和帮助按钮。默认情况下,帮助按钮是隐藏的。您可以使用show()
或show_all()
方法将其设置为可见。
和清单 6-2 一样,这个例子连接到响应信号,该信号接收所有的响应标识符,不管对话框是模态的还是非模态的。在Gtk.ColorSelectionDialog
类的不完整性上用“modal”关键字将对话框设置为模态或非模态。
Gtk.ColorSelectionDialog(title=title, parent=window, modal=domodal)
清单 6-9 显示了除了它的 RGB 值之外的第四个颜色属性,它的不透明度(alpha 值)。该值的范围在 0 到 65,535 之间,规定了颜色绘制的透明度,其中 0 表示完全透明,65,535 表示不透明。默认情况下,颜色选择小部件中的不透明度控制是关闭的。您可以调用方法set_has_opacity_control()
来启用该特性。
colorsel.set_has_opacity_control(boolean)
启用“不透明度”时,十六进制颜色值为十六位数字,每个值四位数字:红色、绿色、蓝色和 alpha。您必须使用colorsel.get_current_alpha()
从颜色选择小部件中检索它的值。
字体选择对话框
字体选择对话框是一个允许用户选择字体的对话框,是点击Gtk.FontButton
按钮时显示的对话框。与Gtk.ColorSelectionDialog
一样,通过Gtk.FontSelectionDialog
结构可以直接访问操作区域按钮。字体选择对话框的例子如图 6-9 所示,看起来应该和你在上一章看到的相似。
图 6-9
字体选择对话框
图 6-9 是运行清单 6-10 的结果。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(200, 100)
button = Gtk.Button.new_with_label("Run Font Selection Dialog")
button.connect("clicked", self.on_run_font_selection_dialog)
self.add(button)
def on_run_font_selection_dialog(self, button):
dialog = Gtk.FontSelectionDialog(title="Choose a Font",
buttons=("Apply", Gtk.ResponseType.APPLY),
parent=self)
dialog.set_preview_text("GTK+ 3 Development With Python")
dialog.connect("response", self.on_dialog_response)
dialog.run()
def on_dialog_response(self, dialog, response):
if response == Gtk.ResponseType.OK or response == Gtk.ResponseType.APPLY:
font = dialog.get_font_name()
message = Gtk.MessageDialog(title="Selected Font",
flags=Gtk.DialogFlags.MODAL,
type=Gtk.MessageType.INFO,
text=font,
buttons=("Ok", Gtk.ResponseType.OK),
parent=dialog);
message.run()
message.destroy()
if response == Gtk.ResponseType.OK:
dialog.destroy()
else:
dialog.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self,
title="Font Selection Dialog”)
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 6-10Using a Gtk.FontSelectionDialog
字体选择对话框初始化函数Gtk.FontSelectionDialog()
,返回一个指定标题的新Gtk.FontSelectionDialog
小部件。
对话框本身包含三个按钮:确定、应用和取消。它们分别发出Gtk.ResponseType.OK
、Gtk.ResponseType.APPLY
和Gtk.ResponseType.CANCEL
信号。
不需要创建模态对话框,因为字体选择对话框连接到响应信号。
如果用户单击 OK 按钮,将向用户显示选定的字体,并销毁对话框。通过单击“应用”,选定的字体将呈现给用户,但对话框不会被破坏。这允许您应用新字体,这样用户就可以在不关闭对话框的情况下查看更改。
字体选择小部件包含一个允许用户预览字体的Gtk.Entry
小部件。默认情况下,预览文本设置为“abcdefghijk ABCDEFGHIJK”。这个有些枯燥,所以我决定重置为《用 Python 进行 GTK+ 3 开发》,这本书的书名。
Gtk.FontSelectionDialog()
提供的最后一个方法允许你设置和获取当前的字体字符串。dialog.set_font_name()
和dialog.get_font_name()
使用的字体字符串与我们在上一章中用Pango.FontDescription
解析的格式相同。
多页对话框
随着 GTK+ 2.10 的发布,引入了一个名为Gtk.Assistant
的小部件。Gtk.Assistant
使创建具有多个阶段的对话框变得更加容易,因为您不必以编程方式创建整个对话框。这允许你将原本复杂的对话框分解成指导用户的步骤。这个功能是由各种应用中通常被称为向导的东西来实现的。
图 6-10 显示了一个简单的Gtk.Assistant
小部件的第一页,它是使用清单 6-11 中的代码创建的。这个例子首先向用户提供一般信息。下一页不允许用户继续,直到文本输入到Gtk.Entry
小部件中。第三页不允许用户继续,直到一个Gtk.CheckButton
按钮被激活。第四页不会让你做任何事情,直到进度条被填满,最后一页给出了已经发生的事情的总结。这是每个Gtk.Assistant
widget 应该遵循的一般流程。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
import time
class assistant(Gtk.Assistant):
progress = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_size_request(450, 300)
self.set_title("Gtk.Assistant Example")
self.connect("destroy", Gtk.main_quit, None)
# create page 0
page0_widget = Gtk.Label("This in an example of a Gtk.Assistant. By\n"
+ "clicking the forward button, you can " +
"continue\nto the next section!")
self.append_page(page0_widget)
self.set_page_title(page0_widget, "Introduction")
self.set_page_type(page0_widget, Gtk.AssistantPageType.INTRO)
self.set_page_complete(page0_widget, True)
# create page 1
page1_widget = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
label = Gtk.Label("Your Name: ")
entry = Gtk.Entry()
page1_widget.pack_start(label, False, False, 5)
page1_widget.pack_start(entry, False, False, 5)
self.append_page(page1_widget)
self.set_page_title(page1_widget, "")
self.set_page_type(page1_widget, Gtk.AssistantPageType.CONTENT)
self.set_page_complete(page1_widget, False)
# create page 2
page2_widget = Gtk.CheckButton.new_with_label("Click me to Continue!")
self.append_page(page2_widget)
self.set_page_title(page2_widget, "Click the Check Button")
self.set_page_type(page2_widget, Gtk.AssistantPageType.CONTENT)
self.set_page_complete(page2_widget, False)
# create page 3
page3_widget = Gtk.Alignment.new(0.5, 0.5, 0.0, 0.0)
button = Gtk.Button.new_with_label("Click Me!")
self.progress = Gtk.ProgressBar()
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
hbox.pack_start(self.progress, True, False, 5)
hbox.pack_start(button, False, False, 5)
page3_widget.add(hbox)
self.append_page(page3_widget)
self.set_page_title(page3_widget, "Click the Check Button")
self.set_page_type(page3_widget, Gtk.AssistantPageType.PROGRESS)
self.set_page_complete(page3_widget, False)
# create page 4
page4_widget = Gtk.Label("Text has been entered in the label and the\n" + "combo box is clicked. If you are done, then\n"
+ "it is time to leave!")
self.append_page(page4_widget)
self.set_page_title(page4_widget, "Confirmation")
self.set_page_type(page4_widget, Gtk.AssistantPageType.CONFIRM)
self.set_page_complete(page4_widget, True)
# set up the callbacks
entry.connect("changed",self.entry_changed)
# page2_widget.connect("toggled",self.button_toggle)
button.connect("clicked", self.button_clicked)
self.connect("cancel", self.assistant_canceled)
self.connect("close", self.assistant_close)
def entry_changed(self, entry):
text = entry.get_text()
num = self.get_current_page()
page = self.get_nth_page(num)
self.set_page_complete(page, len(text) > 0)
def button_toggled(self, toggle):
active = toggle.get_active()
self.set_page_complete(toggle, active)
def button_clicked(self, button):
percent = 0.0
button.set_sensitive(False)
page = self.get_nth_page(3)
while (percent <= 100.0):
message = str(percent) + " complete"
print(message)
self.progress.set_fraction(percent / 100.0)
self.progress.set_text(message)
while (Gtk.events_pending()):
Gtk.main_iteration()
time.sleep(1)
percent += 5.0
self.set_page_complete(page, True)
def assistant_canceled(self, response):
self.destroy()
def assistant_close(self, response):
print("You would apply your changes
now!") self.destroy()
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs)
self.set_border_width(25)
button = Gtk.Button.new_with_mnemonic("_Open Assistant")
button.connect("clicked", self.on_start_button_clicked)
button.set_relief(Gtk.ReliefStyle.NORMAL)
self.add(button)
self.set_size_request(200, 100)
def on_start_button_clicked(self, button):
assistant()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp", **kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Gtk.Assistant")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 6-11The Gtk.Assistant Widget
图 6-10
Gtk 的第一页。助手小工具
创建 Gtk。助理页面
一个Gtk.Assistant
小部件是一个有多个页面的对话框,尽管它实际上不是从Gtk.Dialog
派生出来的。通过调用Gtk.Assistant()
,您创建了一个没有初始页面的新的Gtk.Assistant
小部件。
index = assistant.append_page(widget)
助手没有实际的页面小部件,因为每个页面实际上都是添加了assistant.prepend_page()
、assistant.append_page()
或assistant.insert_page()
的子小部件。这些函数都接受子部件(作为页面内容添加)并返回新页面的索引。每个页面都有许多可以设置的属性,每个属性都是可选的。这些选项的列表如下。
-
页面标题:每个页面都应该有一个标题,这样用户就知道它是干什么用的了。你的首页应该是一个介绍性的页面,告诉用户关于助手的信息。最后一页必须是摘要或确认页,以确保用户准备好应用之前的更改。
-
标题图像:在顶部面板中,您可以在标题左侧显示一个可选图像。这通常是应用的徽标或补充助手用途的图像。
-
侧边图片:该可选图片放置在主页面内容旁边的助手左侧。这是为了美观。
-
页面类型:页面类型必须一直设置,否则默认为
Gtk.AssistantPageType.CONTENT
。最后一页必须始终是确认页或摘要页。您还应该使第一页成为介绍性页面,为用户提供有关助手执行什么任务的信息。
设置页面属性后,必须选择页面类型。有五种类型的页面。第一页应该总是Gtk.AssistantPageType.INTRO
。最后一页应该总是Gtk.AssistantPageType.CONFIRM
或Gtk.AssistantPageType.SUMMARY
——如果你的助手没有以这两种类型的页面结束,它就不能正常工作。下表描述了所有可用的页面类型。
-
Gtk.AssistantPageType.CONTENT
:这种类型的页面内容一般,也就是说在助手中几乎每个页面都会用到。它不应该用于助手的最后一页。 -
这种类型的页面为用户提供了介绍性信息。这应该只为助手中的第一页设置。虽然不是必需的,但介绍性页面给用户指明了方向;它们应该在大多数助手中使用。
-
Gtk.AssistantPageType.CONFIRM
:该页面允许用户确认或拒绝一组更改。它通常用于无法撤消的更改,或者如果设置不正确,可能会导致损坏。这应该只为助手的最后一页设置。 -
该页面给出了已经发生的变化的摘要。这应该只为助手的最后一页设置。
-
Gtk.AssistantPageType.PROGRESS
:当一项任务需要很长时间才能完成时,这将阻止助手,直到页面被标记为完成。此页面与普通内容页面的不同之处在于,所有按钮都被禁用,用户无法关闭助手。
警告
如果您没有将最后一个页面类型设置为Gtk.AssistantPageType.CONFIRM
或Gtk.AssistantPageType.SUMMARY
,当计算最后一个按钮状态时,您的应用将中止,并显示 GTK+ 错误。
因为Gtk.Assistant
不是从Gtk.Dialog
派生的,所以你不能在这个小部件上使用dialog.run()
(或者任何其他的Gtk.Dialog
方法)。相反,下面四个信号是为您提供的,用于处理按钮点击信号。
-
"apply"
:当应用按钮或前进按钮点击任何助手页面时,发出该信号。 -
"cancel"
:当取消按钮点击任一助手页面时,发出该信号。 -
"close"
:当点击助手中最后一页的关闭按钮或应用按钮时,发出该信号。 -
在使一个新页面可见之前,这个信号被发出,这样你就可以在它对用户可见之前做任何准备工作。
您可以使用assistant.connect()
或GLib
提供的任何其他信号连接功能连接所有的Gtk.Assistant
信号。不包括"prepare"
,Gtk.Assistant
信号的回调方法接收助手和用户数据参数。prepare 信号的回调方法也接受当前页面的子部件。
默认情况下,每个页面都被设置为不完整。您必须在时机成熟时用assistant.set_page_complete()
手动将每一页设置为完成,否则Gtk.Assistant
将无法进入下一页。
assistant.set_page_complete(page, boolean)
在每一页上,除了其他几个按钮外,还会显示一个“取消”按钮。在第一页以外的页面上,会显示一个总是敏感的后退按钮。这允许您访问先前显示的页面并进行更改。
注意
根据页面索引,用户单击后退按钮时访问的页面并不总是上一页。这是先前显示的页面,根据您如何定义助手的页面流,该页面可能会有所不同。
除了最后一页,每一页都有一个前进按钮,允许用户移动到下一页。在最后一页,显示一个应用按钮,允许用户应用更改。但是,在页面设置为完整之前,助手会将“前进”或“应用”按钮设置为不敏感。这允许您阻止用户继续操作,直到采取某些操作。
在清单 6-11 中,助手的第一页和最后一页被设置为完整的,因为它们仅仅是提供信息的页面。大多数助手都是这样,因为它们应该以介绍页开始,以确认页或摘要页结束。
另外两页是有趣的地方。在第二个页面上,我们希望确保用户在文本输入到Gtk.Entry
小部件之前无法继续操作。看起来我们应该检查一下什么时候插入了文本,然后就完成了。
但是,如果用户删除了所有文本,会发生什么呢?在这种情况下,应该再次禁用前进按钮。要处理这两个动作,可以使用Gtk.Editable
的 changed 信号。这允许您在每次更改时检查条目中文本的当前状态,如清单 6-11 所示。
在第三页上,我们希望仅当 check 按钮处于活动状态时才启用 forward 按钮。为此,我们使用Gtk.ToggleButton
的切换信号来检查检查按钮的当前状态。基于这种状态,前进按钮的灵敏度被设置。
第四个页面有一个类型Gtk.AssistantPageType.PROGRESS
,它禁用所有动作,直到页面被设置为完成。用户被指示点击一个按钮,开始以每秒 10%的速度填充一个Gtk.ProgressBar
小部件。当进度条被填充时,页面被设置为完成。
Gtk.进度条
Gtk.Assistant
示例引入了另一个名为Gtk.ProgressBar
的新部件。进度条是一种简单的方式来显示一个进程已经完成了多少,对于需要很长时间处理的进程非常有用。进度条给用户一个视觉提示,告诉他们正在取得进展,所以他们不会认为程序已经冻结了。
用Gtk.ProgressBar()
创建新的进度条。随着 GTK+ 2.0 的发布,Gtk.ProgressBar
的实现变得简单了许多,所以在使用 API 文档时要小心,因为许多显示的函数和属性被贬低了。下面的两个例子向您展示了如何正确使用Gtk.ProgressBar
小部件。
percent = 0.0
button.set_sensitive(False)
page = self.get_nth_page(3)
while (percent <= 100.0):
message = str(percent) + " complete"
print(message)
self.progress.set_fraction(percent / 100.0)
self.progress.set_text(message)
while (Gtk.events_pending()):
Gtk.main_iteration()
time.sleep(1)
percent += 5.0
您可能还希望显示可以补充进度条的文本。在前面的示例中,progress.set_text()
显示了完成百分比统计信息,该信息叠加在进度条小部件上。
如果你不能检测过程的进展,你可以使用脉冲。在前面的示例中,progress.pulse()
针对每个已处理的未决事件将进度条移动一步。您可以使用progress.set_pulse_step()
设置脉冲步长。
progress.set_pulse_step(0.1)
while (Gtk.events_pending ()):
Gtk.main_iteration()
progress.pulse()
通过将脉冲步长设置为 0.1,进度条会在前十个步长中填满,并在接下来的十个步长中自动清除。只要你继续脉动进度条,这个过程就会继续。
页面转发方法
如果条件正确,有时您可能希望跳到特定的助手页面。例如,让我们假设您的应用正在创建一个新项目。根据所选择的语言,您可以跳到第三页或第四页。在这种情况下,您想要为前进运动定义自己的Gtk.AssistantPageFunc
方法。
您可以使用assistant.set_forward_page_func()
为助手定义新的页面前进功能。默认情况下,GTK+ 直接按顺序逐页递增,一次一页。通过定义一个新的转发函数,可以定义流。
assistant.set_forward_page_func(page_func, data)
例如,assistant_forward()
是一个简单的Gtk.AssistantPageFunc
实现,根据decide_next_page()
返回的条件,从第二页移动到第三页或第四页。
def assistant_forward(self, current_page, data):
next_page = 0;
if current_page == 0:
next_page = 1
elif current_page == 1:
next_page = (decide_next_page() ? 2 : 3)
elif current_page == 2 or current_page == 3:
next_page = 4
else:
next_page = -1
return next_page
注意
通过从向前翻页功能返回–1,用户会看到一个严重错误,助手将不会转到另一页。严重错误消息将告诉用户页面流中断。
在assistant.forward()
方法中,基于虚构函数decide_next_page()
返回的布尔值来改变流。无论哪种情况,最后一页都是第 4 页。如果当前页面不在边界内,则返回–1,因此 GTK+ 会抛出一个异常。
虽然这个Gtk.Assistant
示例非常简单,但是随着页面数量的增加,这个小部件的实现可能会变得非常复杂。这个小部件可以用一个对话框、一个带隐藏标签的Gtk.Notebook
和几个按钮重新创建。(我不得不多次做同样的事情!),但这让过程变得简单了很多。)
测试你的理解能力
在本章的练习中,您将创建自己的自定义对话框。每个对话框都是一种文件选择器对话框的实现。然而,您正在将一个Gtk.FileChooserWidget
嵌入到一个Gtk.Dialog
中,以重新创建内置对话框的功能。
练习 1:实现文件选择器对话框
在本练习中,您将创建一个带有四个按钮的窗口。每个按钮点击时都会打开不同的对话框,实现四个Gtk.FileChooser
动作中的一个。你应该使用添加到Gtk.Dialog
中的Gtk.FileChooserWidget
而不是预建的Gtk.FileChooserDialog
。
-
你的对话框实现了一个
Gtk.FileChooserAction
.SAVE
文件选择对话框。选择的文件名应该打印到屏幕上。 -
你的对话框实现了一个
Gtk.FileChooserAction.CREATE_FOLDER
文件选择器对话框。新的文件夹名称应该打印到屏幕上。您必须使用 Python 函数手动创建新文件夹。 -
你的对话框实现了一个
Gtk.FileChooserAction.OPEN
文件选择器对话框。选择的文件名应该打印到屏幕上。 -
你的对话框实现了一个
Gtk.FileChooserAction
.SELECT_FOLDER
文件选择对话框。选择的文件夹路径应该打印到屏幕上。
您需要将每个对话框设置为合适的大小,以便用户可以看到全部内容。如果你在这个练习中卡住了,你可以在附录 d 中找到解决方法。
摘要
在本章中,你学习了如何创建你自己的自定义对话框。为此,您需要首先初始化对话框。然后,需要将动作区域按钮以及主要内容添加到对话框的垂直位置Gtk.Box
。
对话框可以被创建为模态或非模态的。用dialog.run()
创建的模态对话框阻止用户与父窗口交互,直到它被对话框的主循环破坏。它还将对话框置于其父窗口的中央。非模态对话框允许用户与应用中的任何其他窗口进行交互,并且不会强制将焦点放在对话框上。
在了解了内置对话框之后,您了解了 GTK+ 提供的多种类型的内置对话框。
-
消息对话框 (
Gtk.MessageDialog
):向用户提供一般消息、错误消息、警告或简单的是/否问题。 -
关于对话框 (
Gtk.AboutDialog
):显示应用的信息,包括版本、版权、许可证、作者和其他信息。 -
文件选择对话框 (
Gtk.FileChooserDialog
):允许用户选择一个文件,选择多个文件,保存一个文件,选择一个目录,或者创建一个目录。 -
颜色选择对话框 (
Gtk.ColorSelectionDialog
):允许用户选择颜色以及可选的不透明度值。 -
字体选择对话框 (
Gtk.FontSelectionDialog
):允许用户选择字体及其大小和样式属性。
本章的最后一节向您展示了一个名为Gtk.Assistant
的小部件,它是在 GTK+ 2.10 中引入的。它允许你创建具有多个阶段的对话框。需要注意的是,助手实际上不是一种Gtk.Dialog
小部件,而是直接从Gtk.Window
类派生而来。这意味着你必须通过连接主循环中的信号来处理这些,而不是调用dialog.run()
。
您现在已经对 GTK+ 的许多重要方面有了坚定的理解。第九章解释了名为Gtk.TextView
的多行文本输入部件。其他主题包括剪贴板和Gtk.SourceView
库。
七、Python 和 GTK+
现在,您已经对 GTK+ 和一些简单的小部件有了合理的理解,是时候了解 Python 和 GTK+ 如何协同工作的细节了。我们还涵盖了对您的项目有用的其他 Python 方面,以及一些有用的 PGObject 库。
虽然这本书不是 Python 的全面指南,但我们研究了 GTK+ 使用的几个主题,这些主题通常不会被基本的 Python 编程指南所涵盖。
参数和关键字参数
在整个 GTK+ 库中,使用关键字参数和自变量将类实例属性值从类传递到子类,等等。所以让我们仔细研究一下这个现象。
关于 GTK+ 类属性,需要理解的最重要的一点是,它们在 PyGTK 中是作为 Python 属性实现的。这意味着在访问时,对属性类和方法的引用应该替换为标准的 Python 类和方法。以下示例显示了如何访问名为title
的Gtk.Window
属性。
win = Gtk.Window()
title = win.props.title
也可以使用标准 Python 方法设置属性。
win = Gtk.Window()
win.props.title = "My Main Window"
当然,Gtk.Window
类也提供了get_title()
和set_title()
方法来执行相同的任务,但是快捷的 Python 方法也执行相同的任务。选择使用哪种方法完全取决于你自己。
现在您已经理解了 GTK+ 属性是作为 Python 属性实现的,我们可以继续描述如何使用关键字参数并将其传递给类。让我们继续看看Gtk.Window
以及如何创建该类的实例。Gtk.Window
的类定义如下:
class Gtk.Window(Gtk.Bin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
那么,这些*args
和**kwargs
自变量/参数是什么,它们做什么?PyGTK 使用这种方法将属性名和值传递给类实例。当一个类实例收到这些参数时,它可以选择使用它们,将它们传递给super
类,或者直接丢弃它们。大多数情况下,它使用与类定义的属性相匹配的属性。然后,它定位相应的值,并将其赋给相应的属性名。它使用类似于清单 7-1 中的代码来完成这项任务。
class MyWindow(Gtk.Window):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for arg in argv:
print("Another arg through *argv :",arg) for kw in keywords:
print(kw, ":", keywords[kw])
Listing 7-1Keyword Arguments
示例中没有显示形式参数。参数有一个必需的顺序:形式参数必须首先出现在参数列表中,然后是所有的args
参数,最后是所有的关键字参数。下面的例子展示了如何工作的正式声明。
function(formal_args, args, kwargs)
下面是使用形式参数、变量参数和关键字参数的调用语句。
# function using formal and variable args def function1(title, modal, *args):
# calling function1
function1("My title", False, # variable args follow
"Peaches", "and", "Cream")
function1("My Window", True) # Only formal args, no variable args
function1(True) # Invalid!!! Missing one formal arg
# function using formal and keyword args def function2(title, modal, **kwargs)
# calling function2
function2("My title", True, parent=window, accept_focus=True) function2("My Window", False) # Only formal args, no keyword args
function2(parent=window) # Invalid, no formal args
这些例子还有许多其他的变化,但是如果你遵循这三个简单的规则,你应该没有问题处理所有的变化:
-
形式参数必须首先出现在参数列表中。如果没有形式参数,那么它们可以从参数列表中消失。你需要多少正式论点就有多少。
-
变量参数必须全部出现在参数列表的下一个位置。如果没有可变参数,那么它们可以从参数列表中消失。可以有任意多的变量参数。
-
关键字参数必须全部出现在参数列表的最后。如果没有关键字参数,那么它们可以不在参数列表中。可以有任意多的关键字参数。
PyGTK 很少使用形式参数;它几乎只使用变量和关键字参数。这使得处理所有 GTK+ 类的实例化变得更加容易。请记住,GTK+ 忽略任何关键字参数,这些关键字参数是属性名而不是属性名。当您想要建立和管理自己的属性时,这非常有用。
记录
日志记录跟踪软件运行时发生的事件。软件开发人员将日志调用添加到他们的代码中,以指示某些事件已经发生。事件由描述性消息来描述,该描述性消息可以可选地包含可变数据(即,对于事件的每次发生可能不同的数据)。事件也具有开发者赋予事件的重要性;重要性也可称为级别或严重性。
何时使用日志记录
日志记录为简单的日志记录用法提供了一组方便的函数。这些是debug()
、info()
、warning()
、error()
和critical()
。表 7-1 描述了何时对常见任务使用日志记录,以及对每项任务使用的最佳工具。
表 7-1
日志记录任务
|您要执行的任务
|
任务的最佳工具
|
| --- | --- |
| 命令行脚本或程序的普通用途的显示控制台输出 | print()
|
| 报告程序正常运行期间发生的事件(例如,状态监控或故障调查) | logging.info
()
(或logging.debug()
用于诊断目的的非常详细的输出) |
| 针对特定运行时事件发出警告 | 如果客户端应用对这种情况无能为力,但仍应记录该事件 |
| 报告特定运行时事件的错误 | 引发异常 |
| 报告抑制错误而不引发异常(例如,长时间运行的服务器进程中的错误处理程序) | logging.error()
、logging.exception()
或logging.critical()
,视具体误差和应用领域而定 |
日志记录功能是根据它们所跟踪的事件的级别或严重性来命名的。表 7-2 中描述了标准等级及其适用性(按严重程度递增)。
表 7-2
日志记录级别
|水平
|
用的时候
|
| --- | --- |
| 调试 | 详细信息,通常仅在诊断问题时感兴趣。 |
| 信息 | 确认事情按预期运行。 |
| 警告 | 表明发生了意想不到的事情,或预示着不久的将来会出现一些问题(例如,磁盘空间不足)。该软件仍按预期工作。 |
| 错误 | 由于更严重的问题,软件无法执行功能。 |
| 批评的 | 一个严重错误,表明程序本身可能无法继续运行。 |
默认级别是警告,这意味着只有该级别及以上的事件才会被跟踪,除非日志记录包配置为其他级别。
可以用不同的方式处理被跟踪的事件。处理跟踪事件的最简单方法是将它们打印到控制台。另一种常见的方法是将它们写入磁盘文件。
一些简单的例子
下面是一个非常简单的例子。
import logging
logging.warning('Watch out!') # will print a message to the console logging.info('I told you so') # will not print anything
如果您在脚本中键入这些行并运行它,您会在控制台上看到以下内容。
WARNING:root:Watch out!
因为默认级别是警告,所以不显示信息消息。打印的消息包括记录调用中提供的级别指示和事件描述(即,小心!).现在不要担心“根”的部分;后面会解释。如果需要的话,实际输出的格式可以非常灵活;稍后还会解释格式化选项。
记录到文件
在一个文件中记录日志事件是非常常见的,所以接下来让我们来看看。确保在新启动的 Python 解释器中尝试以下操作;不要只是从前面描述的会话继续。
import logging
logging.basicConfig(filename='example.log',level=logging.DEBUG)
logging.debug('This message should go to the log file')
logging.info('So should this')
logging.warning('And this, too')
现在,如果我们打开文件,看看我们有什么,我们应该会找到日志消息。
DEBUG:root:This message should go to the log file INFO:root:So should this WARNING:root:And this, too
该示例还展示了如何设置日志记录级别,该级别充当跟踪的阈值。在这种情况下,因为我们将阈值设置为 DEBUG,所以所有的消息都被打印出来。
如果您想从命令行选项设置日志记录级别,例如
--log=INFO
并且在一个loglevel
变量中有传递给--log
的参数值,您可以使用
getattr(logging, loglevel.upper())
获取值,通过 level 参数传递给basicConfig()
。
您可能希望对任何用户输入值进行错误检查,如下例所示。
# assuming loglevel is bound to the string value obtained from the
# command line argument. Convert to upper case to allow the user to
# specify --log=DEBUG or --log=debug
numeric_level = getattr(logging, loglevel.upper(), None)
if not isinstance(numeric_level, int):
raise ValueError('Invalid log level: %s' % loglevel)
logging.basicConfig(level=numeric_level, ...)
对basicConfig()
的调用应该在对debug()
、info()
等等的任何调用之前。因为它旨在作为一次性的简单配置工具,只有第一次调用实际上做了什么;随后的呼叫实际上是无效的。
如果多次运行前面的脚本,连续运行的消息将被附加到example.log
文件中。如果您希望每次运行都重新开始,而不是记住以前运行的消息,您可以指定filemode
参数,将示例中的调用改为:
logging.basicConfig(filename='example.log', filemode="w", level=logging.DEBUG)
输出与之前相同,但是不再附加日志文件,因此来自早期运行的消息会丢失。
从多个模块记录日志
如果您的程序由多个模块组成,下面是一个如何组织日志记录的示例。
# myapp.py
# import logging
# import mylib
def main():
logging.basicConfig(filename='myapp.log', level=logging.INFO)
logging.info('Started')
mylib.do_something()
logging.info('Finished')
if __name__ == '__main__':
main()
# mylib.py
import logging
def do_something():
logging.info('Doing something')
如果您运行myapp.py
,您应该在myapp.log
中看到这个:
INFO:root:Started INFO:root:Doing something INFO:root:Finished
这可能是你期望看到的。您可以使用mylib.py
中的模式将其推广到多个模块。注意,对于这个简单的使用模式,除了查看事件描述之外,通过查看日志文件,您不会知道您的消息来自应用的什么地方。如果你想跟踪你的消息的位置,你需要参考超出本教程水平的文档。
记录可变数据
若要记录变量数据,请使用事件描述消息的格式字符串,并将变量数据作为参数追加。例如,
import logging
logging.warning('%s before you %s', 'Look', 'leap!')
显示
WARNING:root:Look before you leap!
如您所见,将变量数据合并到事件描述消息中使用了旧的%
样式的字符串格式。这是为了向后兼容;日志包早于更新的格式选项,比如str.format()
和string.Template
。这些更新的格式选项是受支持的,但是探究它们超出了本书的范围。有关详细信息,请参阅 Python 文档。
更改显示消息的格式
要更改用于显示消息的格式,您需要指定要使用的格式。
import logging
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
logging.debug('This message should appear on the console')
logging.info('So should this')
logging.warning('And this, too')
这应该会打印出如下内容。
2010-12-12 11:41:42,612 is when this event was logged.
日期/时间显示的默认格式是 ISO8601 或[RFC 3339]。如果您需要对日期/时间的格式进行更多的控制,请为basicConfig()
提供一个datefmt
参数,如下所示。
import logging
logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S % logging.warning('is when this event was logged.')
这将显示如下内容。
12/12/2010 11:46:36 AM is when this event was logged.
datefmt
参数的格式与time.strftime()
支持的格式相同。
例外
在 GTK+,application 中运行异常和运行任何标准 Python 程序是一样的。因为 GTK 模块只是一个包装了 GTK+API 的标准 Python 模块,所以库实现将所有 GTK+ 异常都变成了标准 Python 异常。这样做的结果是您不需要担心捕获Glib.Error
错误。没有一个会被 GTK 模块抛出。
这并不意味着您可以忽略标准的 Python 异常。您应该为应用中的任何异常做好准备,就像您为任何 Python 应用所做的那样。让我们回顾一下 Python 异常的一些原则。
引发异常
当应用出错时,异常会自动引发。在我们了解如何处理异常之前,让我们先看看如何手动引发异常,甚至创建自己的异常。
加薪声明
您用raise
语句引发一个异常,该语句接受一个类(它应该是Exception
类的子类)或一个实例的参数。当使用类作为参数时,会自动创建该类的一个实例。下面是一个使用内置Exception
类的例子。
>>> raise Exception
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Exception
> raise Exception('overload') Traceback (most recent call last):
File "<stdin>", line 1, in <module> Exception: overload
第一个例子引发了一个普通的异常,没有关于出错的信息。第二个示例添加了错误消息重载。
许多内置类是可用的。在 Python 库参考的“内置异常”一节中可以找到所有异常类的完整描述。下面列出了所有 Python 3.x 异常的类层次结构。
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
| +-- ModuleNotFoundError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
| +-- RecursionError
+-- SyntaxError
|
+-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- ResourceWarning
自定义异常类
有很多内置的异常,涵盖了很多领域。但是有时候你可能想创建自己的异常类。例如,没有 GTK+ 异常类,所以您可能需要创建自己的异常类。这使您有机会根据异常的类有选择地处理异常。因此,如果您想处理 GTK 运行时错误,您将需要一个单独的异常类。
您可以像创建任何其他类一样创建这样的异常,但是一定要创建Exception
类的子类(直接或间接,这意味着可以创建任何其他内置异常的子类)。下面演示如何编写自定义异常类。
class GtkCustomException)Exception): pass
如果需要的话,可以随意在类中添加自己的方法。
捕捉异常
当然,引发异常只是异常的第一部分。真正有用的部分是在您自己的应用代码中捕捉(或捕获)和处理异常。您可以使用try
和except
语句来实现这一点。让我们看一个简单的例子。
x = input('Enter the first number: ')
y = input('Enter the second number: ')
print(x/y)
直到用户输入零作为第二个数字。
Enter the first number: 10
Enter the second number: 0
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
ZeroDivisionError: division by zero
为了捕捉异常并执行一些错误处理(比如打印一个更友好的错误消息),您可以像这样重写程序:
try:
x = input('Enter the first number: ')
y = input('Enter the second number: ')
print(x/y)
except ZeroDivisionError:
print('The second number can not be zero!')
虽然对于这样一个简单的例子来说,这个解决方案看起来有些夸张,但是当在整个应用中使用数百个 division 语句时,这将是一个更合理的例子。
注意
异常从函数和方法传播到它们被调用的地方,如果它们也没有被捕获,异常将“冒泡”到程序的顶层。这意味着您可以使用try
和except
语句来捕捉您自己和他人的代码(模块、函数、类等)中出现的异常。).
引发和重新引发异常
异常可以在其他异常内部引发,将异常传递给更高级别的代码。为此,必须通过raise
语句不带任何参数地调用后续异常。
下面是这种非常有用的技术的一个例子。如果异常没有被抑制,该示例将ZeroDivisionException
传递给更高级别的代码。
class SuppressedDivision:
suppressed = False
def calc(self, expr):
try:
return eval(expr)
except ZeroDivisionError:
if self.suppressed:
print('Division by zero is illegal!')
else:
raise
正如您所看到的,当计算没有被取消时,ZeroDivisionException
被捕获,但是被传递到更高级别的代码,在那里它将被捕获和处理。
捕捉多个异常
try
和except
块也可以捕捉和处理多个异常。为了了解这是如何工作的,让我们增强前面的例子来捕捉TypeError
异常。
class SuppressedDivision:
suppressed = False
def calc(self, expr):
try:
return eval(expr)
except ZeroDivisionError:
if self.suppressed:
print('Division by zero is illegal!')
else:
raise
except TypeError:
if self.suppressed:
print('One of the operands was not a valid number!')
else:
raise
现在我们开始看到try
和except
代码块以及使用异常的威力。在前面的例子中,我们使用解释器的能力来检查计算的所有变量,而不是我们自己编写基本相同的代码来处理所有变量,以便在我们处理它之前发现计算是否有效。
我们还可以将这两个异常合并到一个代码块中,如下所示。
class SuppressedDivision:
suppressed = False
def calc(self, expr):
try:
return eval(expr)
except ZeroDivisionError, TypeError:
if self.suppressed:
print('One or both operands is illegal!')
else:
raise
我们还可以捕获导致异常的对象。
class SuppressedDivision:
suppressed = False
def calc(self, expr):
try:
return eval(expr)
except (ZeroDivisionError, TypeError_, e:
if self.suppressed:
print('The value "' + str(e) '" is illegal!')
else:
raise
处理异常还有更多的内容,但是这些信息足以激起您的兴趣。
有关异常的更完整信息,请参考 Python 资源。
八、文本视图小部件
本章教你如何使用Gtk.TextView
小工具。文本视图小部件类似于Gtk.Entry
小部件,只是它能够保存跨越多行的文本。滚动窗口允许文档存在于屏幕边界之外。
在学习Gtk.TextView
之前,本章首先介绍几个新的小部件。前两个小部件是滚动窗口和视口。滚动窗口由两个滚动子部件的滚动条组成。一些小工具已经支持滚动,包括Gtk.Layout
、Gtk.TreeView
和Gtk.TextView
。对于您想要滚动的所有其他小部件,您需要首先将它们添加到一个Gtk.Viewport
小部件中,这将为其子小部件提供滚动能力。
在本章中,您将学习以下内容:
-
如何使用滚动窗口和视窗
-
如何使用
Gtk.TextView
小部件和应用文本缓冲区 -
文本迭代器和文本标记在处理缓冲区时执行的功能
-
将样式应用于整个或部分文档的方法
-
如何在剪贴板上剪切、复制和粘贴
-
如何在文本视图中插入图像和子部件
滚动窗口
在了解Gtk.TextView
小部件之前,您需要了解两个名为Gtk.ScrolledWindow
和Gtk.Viewport
的容器小部件。滚动窗口使用两个滚动条来允许小部件占据比屏幕上可见的空间更多的空间。这个小部件允许Gtk.TextView
小部件包含超出窗口边界的文档。
滚动窗口中的两个滚动条都有关联的Gtk.Adjustment
对象。这些调整跟踪滚动条的当前位置和范围;但是,在大多数情况下,您不需要直接访问调整。
滚动条的Gtk.Adjustment
保存了关于滚动范围、步数和当前位置的信息。value 变量是滚动条在边界之间的当前位置。此变量必须始终介于下限值和上限值之间,这是校正的界限。page_size
是屏幕上一次可以看到的区域,取决于小工具的大小。step_increment
和page_increment
变量用于按下箭头或向下翻页键时的步进。
图 8-1 是用清单 8-1 中的代码创建的窗口截图。两个滚动条都被启用,因为包含按钮的表格比可见区域大。
图 8-1
同步滚动窗口和视窗
清单 8-1 显示了如何使用滚动窗口和视窗。滚动条移动时,视口也会滚动,因为调整是同步的。尝试调整窗口的大小,看看滚动条在变得比子窗口小部件更大和更小时会有什么反应。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
grid1 = Gtk.Grid.new()
grid2 = Gtk.Grid.new()
grid1.set_column_homogeneous = True
grid2.set_column_homogeneous = True
grid1.set_row_homogeneous = True
grid2.set_row_homogeneous = True
grid1.set_column_spacing = 5
grid2.set_column_spacing = 5
grid1.set_row_spacing = 5
grid2.set_row_spacing = 5
i = 0
while i < 10:
j = 0
while j < 10:
button = Gtk.Button.new_with_label("Close")
button.set_relief(Gtk.ReliefStyle.NONE)
button.connect("clicked", self.on_button_clicked)
grid1.attach(button, i, j, 1, 1)
button = Gtk.Button.new_with_label("Close")
button.set_relief(Gtk.ReliefStyle.NONE)
button.connect("clicked", self.on_button_clicked)
grid2.attach(button, i, j, 1, 1)
j += 1
i += 1
swin = Gtk.ScrolledWindow.new(None, None)
horizontal = swin.get_hadjustment()
vertical = swin.get_vadjustment()
viewport = Gtk.Viewport.new(horizontal, vertical)
swin.set_border_width(5)
swin.set_propagate_natural_width(True)
swin.set_propagate_natural_height(True)
viewport.set_border_width(5)
swin.set_policy (Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
swin.add_with_viewport(grid1)
viewport.add(grid2)
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 5)
vbox.set_homogeneous = True
vbox.pack_start(viewport, True, True, 5)
vbox.pack_start(swin, True, True, 5)
self.add (vbox)
self.show_all()
def on_button_clicked(self, button):
self.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self,
title="Scrolled Windows & Viewports")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 8-1Using Scrolled Windows
新滚动的窗口用Gtk.ScrolledWindow.new()
创建。在清单 8-1 中,每个参数都被设置为None
,这使得滚动窗口为您创建两个默认调整。在大多数情况下,您希望使用默认调整,但是也可以为滚动条指定您自己的水平和垂直调整。
当用Gtk.Viewport.new()
创建新视区时,使用本例中的调整。视口调整用滚动窗口中的调整初始化,这确保了两个容器同时滚动。
当你设置一个可滚动的窗口时,你需要做的第一个决定是滚动条何时可见。在这个例子中,Gtk.PolicyType.AUTOMATIC
被用于两个滚动条,所以只有在需要的时候才显示。Gtk.PolicyType.ALWAYS
是两个滚动条的默认策略。下面是由Gtk.PolicyType
提供的三个枚举值。
-
滚动条总是可见的。如果不能滚动,它将显示为禁用或灰色。
-
Gtk.PolicyType.AUTOMATIC
:滚动条只有在可以滚动时才可见。如果不需要,滚动条会暂时消失。 -
Gtk.PolicyType.NEVER
:滚动条从不显示。
另一个属性是滚动条的位置,虽然没有被很多应用使用。在大多数应用中,您希望滚动条出现在小部件的底部和右侧,这是默认功能。
但是,如果你想改变这一点,你可以调用set_placement()
。这个函数接收一个Gtk.CornerType
值,它定义了内容相对于滚动条的位置。例如,默认值是Gtk.CornerType.TOP_LEFT
,因为内容通常出现在滚动条的左上方。
swin.set_placement(window_placement)
可用的Gtk.CornerType
值包括Gtk.CornerType.TOP_LEFT
、Gtk.CornerType.BOTTOM_LEFT
、Gtk.CornerType.TOP_RIGHT
和Gtk.CornerType.BOTTOM_RIGHT
,它们定义了内容相对于滚动条的位置。
警告
应该使用set_placement()
的场合非常少见!在几乎所有可能的情况下,你都不应该使用这个函数,因为它会让用户感到困惑。除非您有充分的理由更改位置,否则请使用默认值。
可以通过调用set_shadow_type()
来设置小部件相对于子小部件的阴影类型。
swin.set_shadow_type(type)
在第四章中,你学习了如何使用Gtk.ShadowType
枚举和句柄框来设置放置在子部件周围的边框类型。与之前相同的值设置了滚动窗口的阴影类型。
在你设置了一个滚动窗口之后,你应该添加一个子部件来使用它。有两种可能的方法可以做到这一点,方法的选择基于子部件的类型。如果您使用的是Gtk.TextView
、Gtk.TreeView
、Gtk.IconView
、Gtk.Viewport
或Gtk.Layout
小部件,您应该使用默认的add()
方法,因为这五个小部件都包含本地滚动支持。
所有其他 GTK+ 小部件都没有本地滚动支持。对于那些小部件,应该使用add_with_viewport()
。这个函数通过首先将它打包到一个名为Gtk.Viewport
的容器小部件中,为子控件提供滚动支持。这个小部件为缺少自身支持的子小部件实现了滚动功能。然后视口被自动添加到滚动窗口中。
警告
千万不要将Gtk.TextView
、Gtk.TreeView
、Gtk.IconView
、Gtk.Viewport
或Gtk.Layout
小部件打包到带有add_with_viewport()
的滚动窗口中,因为小部件上的滚动可能无法正确执行!
可以手动将一个小部件添加到一个新的Gtk.Viewport
,然后用add()
将该视窗添加到一个滚动窗口,但是便利功能允许您完全忽略该视窗。
滚动窗口只是一个带有滚动条的容器。容器和滚动条本身都不执行任何操作。滚动是由子部件处理的,这就是为什么子部件必须已经有本地滚动支持才能正确使用Gtk.ScrolledWindow
部件。
当您添加一个支持滚动的子部件时,会调用一个函数来为每个轴添加调整。除非子部件支持滚动,否则什么也不做,这就是为什么大多数部件都需要一个视口。当用户单击并拖动滚动条时,调整中的值会发生变化,这会导致发出值已更改的信号。此操作还会导致子小部件相应地呈现自身。
因为Gtk.Viewport
小部件没有自己的滚动条,它完全依靠调整来定义它在屏幕上的当前位置。滚动条在Gtk.ScrolledWindow
小部件中用作调整当前调整值的简单机制。
文本视图
Gtk.TextView
小部件显示文档的多行文本。它提供了多种方式来定制整个文档或文档的单个部分。甚至可以将GdkPixbuf
对象和子部件插入到文档中。Gtk.TextView
是到目前为止您遇到的第一个合理涉及的小部件,所以本章的其余部分将致力于小部件的许多方面。这是一个非常通用的小部件,您需要在许多 GTK+ 应用中使用。
本章的前几个例子可能会让你认为Gtk.TextView
只能显示简单的文档,但事实并非如此。它还可以显示各种应用使用的多种类型的富文本、文字处理和交互式文档。您将在接下来的章节中学习如何做到这一点。
图 8-2 向您介绍了一个简单的文本视图窗口,允许您输入文本并进行一些基本的布局设计。但是它也没有太多的功能,而且缺少许多文字处理器的功能。
图 8-2
Gtk.TextView widget
使用 GTK+ 的每一种文本和文档编辑应用中都使用文本视图。如果您曾经使用过 AbiWord、gedit 或其他大多数为 GNOME 创建的文本编辑器,那么您一定使用过Gtk.TextView
小部件。它还用于即时消息窗口中的 Gaim 应用。(实际上,本书中的所有例子都是在 OpenLDev 应用中创建的,该应用使用Gtk.TextView
进行源代码编辑!)
文本缓冲区
每个文本视图显示一个名为Gtk.TextBuffer
的类的内容。文本缓冲区存储文本视图中内容的当前状态。它们保存文本、图像、子部件、文本标签以及呈现文档所需的所有其他信息。
单个文本缓冲区能够由多个文本视图显示,但是每个文本视图只有一个关联的缓冲区。大多数程序员没有利用这个特性,但是当您在后面的章节中学习如何将子部件嵌入到文本缓冲区时,这个特性就变得很重要了。
与 GTK+ 中的所有文本小部件一样,文本被存储为 UTF-8 字符串。UTF-8 是一种字符编码类型,每个字符使用 1 到 4 个字节。为了区分字符占用的字节数,“0”总是在 1 字节字符之前,“110”在 2 字节字符之前,“1110”在 3 字节序列之前,依此类推。跨越多个字节的 UTF-8 字符在其余字节的两个最高有效位中具有“10”。
通过这样做,仍然支持基本的 128 个 ASCII 字符,因为在初始“0”之后的单字节字符中还有另外 7 位可用。UTF-8 还为许多其他语言的字符提供支持。此方法还可避免在较大的字节序列中出现较小的字节序列。
在处理文本缓冲区时,需要知道两个术语:偏移量和索引。单词“偏移”指的是一个字符。UTF-8 字符可能跨越缓冲区中的一个或多个字节,因此Gtk.TextBuffer
中的字符偏移量可能不是一个字节长。
警告
单词“索引”指的是一个单独的字节。在后面的示例中,当单步执行文本缓冲区时需要小心,因为不能引用两个字符偏移量之间的索引。
清单 8-2 展示了一个你可以创建的最简单的文本视图例子。创建了一个新的Gtk.TextView
小部件。检索其缓冲区,并将文本插入缓冲区。然后,滚动窗口包含文本视图。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(250, 150)
textview = Gtk.TextView.new()
buffer = textview.get_buffer()
text = "Your 1st GtkTextView widget!"
buffer.set_text(text, len(text))
scrolled_win = Gtk.ScrolledWindow.new (None, None)
scrolled_win.add(textview)
self.add(scrolled_win)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Text Views")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 8-2A Simple Gtk.TextView Example
大多数新的Gtk.TextView
小部件都是用Gtk.TextView.new()
创建的。通过使用这个函数,可以为您创建一个空的缓冲区。该默认缓冲区稍后可以用set_buffer()
替换或用get_buffer()
检索。
如果您想将初始缓冲区设置为您已经创建的缓冲区,您可以使用Gtk.TextView.new_with_buffer()
创建文本视图。在大多数情况下,简单地使用默认的文本缓冲区更容易。
一旦访问了一个Gtk.TextBuffer
对象,有许多方法可以添加内容,但是最简单的方法是调用set_text()
。这个函数接收一个文本缓冲区、一个设置为缓冲区新文本的 UTF-8 文本字符串以及文本的长度。
set_text(text, length)
如果文本字符串以 NULL 结尾,则可以使用–1 作为字符串的长度。如果在指定长度的文本前发现空字符,此函数将自动失败。
缓冲区的当前内容被新的文本字符串完全替换。在“文本迭代器和标记”一节中,向您介绍了一些函数,这些函数允许您在不覆盖当前内容的情况下将文本插入到缓冲区中,这些函数更适合于插入大量文本。
回想一下上一节,有五个小部件具有本地滚动能力,包括Gtk.TextView
小部件。因为文本视图已经有了管理调整的工具,container.add()
应该总是将它们添加到滚动窗口中。
文本视图属性
Gtk.TextView
是一个非常通用的小工具。因此,为小部件提供了许多属性。在本节中,您将了解这些小部件的许多属性。
让文本视图小部件非常有用的一个特性是,您可以将更改应用到整个小部件或仅应用到小部件的单个部分。文本标签改变一段文本的属性。仅自定义文档的一部分将在本章的后面部分介绍。
清单 8-3 展示了许多可以定制Gtk.TextBuffer
内容的属性。您应该注意到,这些属性中的许多可以在文档的单个部分中用文本标签覆盖。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Pango
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(260, 150)
font = Pango.font_description_from_string("Monospace Bold 10")
textview = Gtk.TextView.new()
textview.modify_font(font)
textview.set_wrap_mode(Gtk.WrapMode.WORD)
textview.set_justification(Gtk.Justification.RIGHT)
textview.set_editable(True)
textview.set_cursor_visible(True)
textview.set_pixels_above_lines(5)
textview.set_pixels_below_lines(5)
textview.set_pixels_inside_wrap(5)
textview.set_left_margin(10)
textview.set_right_margin(10)
buffer = textview.get_buffer()
text = "This is some text!\nChange me!\nPlease!"
buffer.set_text(text, len(text))
scrolled_win = Gtk.ScrolledWindow.new(None, None)
scrolled_win.set_policy(Gtk.PolicyType.AUTOMATIC,
Gtk.PolicyType.ALWAYS)
scrolled_win.add(textview)
self.add(scrolled_win)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self,
title="Text Views Properties")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 8-3Using Gtk.TextView Properties
解释Gtk.TextView
的每个属性做什么的最好方式是给你看一个结果的截图,可以在图 8-3 中看到。你应该在你自己的机器上编译这个应用,并试着改变清单 8-3 中使用的值,感受一下它们的作用。
图 8-3
Gtk。具有非默认属性的 TextView
可以改变文本视图内容的单个部分的字体和颜色,但是如清单 8-3 所示,仍然可以使用以前章节中的函数来改变整个小部件的内容。这在编辑具有一致样式的文档(如文本文件)时非常有用。
在处理多行显示文本的小部件时,您需要决定文本是否换行以及如何换行。在清单 8-3 中,使用set_wrap_mode()
将包装模式设置为Gtk.WrapMode.WORD
。此设置使文本换行,但不会将一个单词拆分为两行。在Gtk.WrapMode
枚举中有四种类型的回绕模式。
-
Gtk.WrapMode.NONE
:不发生缠绕。如果滚动窗口包含视图,则滚动条会扩展;否则,文本视图会在屏幕上展开。如果滚动窗口不包含Gtk.TextView
小部件,它会水平扩展小部件。 -
Gtk.WrapMode.CHAR
:换行到字符,即使换行点出现在单词中间。对于文本编辑器来说,这通常不是一个好的选择,因为它将单词分成两行。 -
用尽可能多的单词填满一行,但不要换行。相反,把整个单词放到下一行。
-
Gtk.WrapMode.WORD_CHAR
:换行方式与 GTK_WRAP_WORD 相同,但如果整个单词占据文本视图的一个以上可视宽度,则按字符换行。
有时,您可能希望阻止用户编辑文档。使用set_editable()
可以更改整个文本视图的可编辑属性。值得注意的是,使用文本标签,您可以为文档的某些部分覆盖set_editable()
,因此它并不总是万能的解决方案。
与set_sensitive()
形成对比,它完全阻止用户与小部件交互。如果文本视图被设置为不可编辑,用户仍然能够对文本执行不需要编辑文本缓冲区的操作,例如选择文本。将文本视图设置为不敏感会阻止用户执行任何这些操作。
当您禁用文档内的编辑时,使用set_cursor_visible()
阻止光标可见也很有用。默认情况下,这两个属性都被设置为True
,因此需要对它们进行更改以保持同步。
默认情况下,行与行之间没有额外的间距,但是清单 8-3 向您展示了如何在一行之上、一行之下以及换行之间添加间距。这些函数增加了行与行之间的额外空间,所以你可以假设行与行之间已经有足够的空间了。在大多数情况下,您不应该使用此功能,因为间距对用户来说可能不正确。
对齐是文本视图的另一个重要属性,尤其是在处理富文本文档时。有四个默认对齐值:Gtk.Justification.LEFT
、Gtk.Justification.RIGHT
、Gtk.Justification.CENTER
和Gtk.Justification.FILL
。
可以使用set_justification()
为整个文本视图设置对齐,但是可以使用文本标签覆盖文本的特定部分。在大多数情况下,您希望使用默认的Gtk.Justification.LEFT
对齐,除非用户希望更改它。默认情况下,文本在视图的左侧对齐。
textview.set_justification(justification)
清单 8-3 最后设置的属性是左边距和右边距。默认情况下,左侧或右侧都不会添加额外的空白空间,但是您可以使用set_left_margin()
向左侧或使用set_right_margin()
向右侧添加一定数量的像素。
Pango Tab 阵列
添加到文本视图中的制表符被设置为默认宽度,但有时您想要更改该宽度。例如,在源代码编辑器中,一个用户可能希望缩进两个空格,而另一个用户可能希望缩进五个空格。GTK+ 提供了Pango.TabArray
对象,它定义了一个新的标签尺寸。
当更改默认制表符大小时,首先根据当前字体计算制表符所占的水平像素数。下面的make_tab_array()
函数可以计算一个新的标签尺寸。该函数首先用所需数量的空格创建一个字符串。该字符串然后被转换成一个Pango.Layout
对象,该对象检索显示字符串的像素宽度。最后,Pango.Layout
被翻译成Pango.TabArray
,它可以应用于文本视图。
def make_tab_array(fontdesc, tab_size, textview):
if tab_size < 100:
return
tab_string = ' ' * tab_size
layout = Gtk.Widget.create_pango_layout(textview, tab_string)
layout.set_font_description(fontdesc)
(width, height) = layout.get_pixel_size()
tab_array = Pango.TabArray.new(1, True)
tab_array.set_tab(0, Pango.TabAlign.LEFT, width)
textview.set_tabs(tab_array)
Pango.Layout
对象代表一整段文本。通常,Pango 在内部使用它来在小部件中布局文本。但是,本示例可以使用它来计算制表符串的宽度。
我们首先从Gtk.TextView
创建一个新的Pango.Layout
对象,并用Gtk.Widget.create_pango_layout()
创建标签字符串。这使用文本视图的默认字体描述。如果整个文档都应用了相同的字体,这是没有问题的。Pango.Layout
描述如何渲染一段文字。
layout = Gtk.Widget.create_pango_layout(textview, tab_string)
如果文档中的字体不同,或者字体尚未应用于文本视图,则需要指定用于计算的字体。您可以使用set_font_description()
设置Pango.Layout
的字体。这使用了一个Pango.FontDescription
对象来描述布局的字体。
layout.set_font_description(fd)
一旦你正确地配置了你的Pango.Layout
,字符串的宽度可以用get_pixel_size()
来检索。这是字符串在缓冲区中占用的计算空间,当用户在小部件中按下选项卡键时,应该添加这个空间。
(width, height) = layout.get_pixel_size()
现在您已经获得了选项卡的宽度,您需要用Pango.TabArray.new()
创建一个新的Pango.TabArray
。该函数接收应该添加到数组中的元素数量,以及每个元素的大小是否以像素为单位的通知。
tab_array = Pango.TabArray.new(1, True)
您应该始终创建只包含一个元素的 tab 数组,因为此时只支持一种 tab 类型。如果第二个参数没有指定True
,则制表符被存储为 Pango 单位;1 个像素等于 1,024 个 Pango 单位。
在应用 tab 数组之前,您需要添加宽度。这是用set_tab()
完成的。整数“0”指的是Pango.TabArray
中的第一个元素,唯一应该存在的元素。Pango.TabAlign.LEFT
必须始终为第二个参数指定,因为它是当前唯一支持的值。最后一个参数是选项卡的宽度,以像素为单位。
tab_array.set_tab(0, Pango.TabAlign.LEFT, width)
当您从函数接收到 tab 数组时,您需要用set_tab()
将它应用到整个文本视图。这可以确保文本视图中的所有选项卡都设置为相同的宽度。但是,与所有其他文本视图属性一样,该值可以被文本的单个段落或部分覆盖。
textview.set_tabs(tab_array)
文本迭代器和标记
当操作Gtk.TextBuffer
中的文本时,有两个对象可以跟踪缓冲区中的位置:Gtk.TextIter
和Gtk.TextMark
。GTK + 提供了在这两种类型的对象之间进行转换的函数。
文本迭代器表示缓冲区中两个字符之间的位置。在缓冲区内操作文本时会用到它们。文本迭代器带来的问题是,当编辑文本缓冲区时,它们会自动失效。即使插入了相同的文本,然后又从缓冲区中删除,文本迭代器也会失效,因为迭代器应该在堆栈中分配并立即使用。
为了跟踪文本缓冲区中的位置变化,提供了Gtk.TextMark
对象。操纵缓冲区时,文本标记保持不变,并根据缓冲区的操纵方式移动它们的位置。您可以使用get_iter_at_mark()
检索指向文本标记的迭代器,这使得标记非常适合跟踪文档中的位置。
get_iter_at_mark(iter, mark)
文本标记就像是文本中不可见的光标,根据文本的编辑方式改变位置。如果在标记前添加文本,它将向右移动,以便保持在相同的文本位置。
默认情况下,文本标记的重力设置为向右。这意味着它会随着文本的添加而向右移动。让我们假设标记周围的文本被删除。标记移动到被删除文本两侧的两段文本之间的位置。然后,如果在文本标记处插入文本,由于其右侧的重力设置,它将保持在插入文本的右侧。这类似于光标,因为当插入文本时,光标保持在插入文本的右侧。
小费
默认情况下,文本中的文本标记是不可见的。但是,您可以通过调用set_visible()
将Gtk.TextMark
设置为可见,这将放置一个竖线来指示它所在的位置。
可以通过两种方式访问文本标记。您可以在特定的Gtk.TextIter
位置检索文本标记。还可以用字符串作为名称来设置文本标记,这使得标记易于跟踪。
GTK+ 总是为每个Gtk.TextBuffer
: insert
和selection_bound
提供两个默认文本标记。插入文本标记指的是光标在缓冲区中的当前位置。selection_bound
文本标记是指如果有选中文本,则选中文本的边界。如果没有选择文本,这两个标记指向相同的位置。
操作缓冲区时,"insert"
和"selection_bound"
文本标记非常有用。可以操纵它们来自动选择或取消选择缓冲区中的文本,并帮助您确定文本在缓冲区中的逻辑插入位置。
编辑文本缓冲区
GTK+ 提供了大量检索文本迭代器和操作文本缓冲区的函数。在这一节中,您将看到清单 8-4 中使用的一些最重要的方法,然后还会向您介绍更多方法。图 8-4 显示了一个用Gtk.TextBuffer
插入和检索文本的应用。
图 8-4
使用 Gtk 的应用。TextView widget
清单 8-4 是一个执行两个功能的简单例子。当点击图 8-4 所示的插入文本按钮时,在当前光标位置插入Gtk.Entry
小工具中显示的字符串。当点击“获取文本”按钮时,任何选定的文本都将通过print()
输出。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(-1, -1)
textview = Gtk.TextView.new()
entry = Gtk.Entry.new()
insert_button = Gtk.Button.new_with_label("Insert Text")
retrieve = Gtk.Button.new_with_label("Get Text")
insert_button.connect("clicked", self.on_insert_text, (entry, textview))
retrieve.connect("clicked", self.on_retrieve_text, (entry, textview))
scrolled_win = Gtk.ScrolledWindow.new(None, None)
scrolled_win.add(textview)
hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 5)
hbox.pack_start(entry, True, True, 0)
hbox.pack_start(insert_button, True, True, 0)
hbox.pack_start(retrieve, True, True, 0)
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 5)
vbox.pack_start(scrolled_win, True, True, 0)
vbox.pack_start(hbox, True, True, 0)
self.add(vbox)
self.show_all()
def on_insert_text(self, button, w):
buffer = w[1].get_buffer()
text = w[0].get_text()
mark = buffer.get_insert()
iter = buffer.get_iter_at_mark(mark)
buffer.insert(iter, text, len(text))
def on_retrieve_text(self, button, w):
buffer = w[1].get_buffer()
(start, end) = buffer.get_selection_bounds()
text = buffer.get_text(start, end, False)
print(text)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Text Iterators")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 8-4Using Text Iterators
迭代器的一个重要特性是同一个迭代器可以重复使用,因为每次编辑文本缓冲区时迭代器都会失效。这样,您可以继续重用同一个Gtk.TextIter
对象,而不是创建大量的变量。
检索文本迭代器和标记
如前所述,有相当多的函数可用于检索文本迭代器和文本标记,其中许多函数在本章中都会用到。
清单 8-4 从用buffer.get_insert()
检索插入标记开始。也可以使用buffer.get_selection_bound()
来检索“selection_bound”文本标记。
mark = buffer.get_insert()
iter = buffer.get_iter_at_mark(mark)
一旦你获得了一个标记,你可以用textbuffer.get_iter_at_mark()
把它翻译成一个文本迭代器,这样它就可以操作缓冲区。
清单 8-4 给出的另一个检索文本迭代器的函数是buffer.get_selection_bounds()
,它返回位于 insert 和 selection_bound 标记处的迭代器。您可以将一个或两个文本迭代器参数设置为None
,这可以防止值返回,尽管如果您只需要其中一个,那么使用特定标记的函数会更有意义。
当检索缓冲区的内容时,您需要为文本片段指定一个开始和结束迭代器。如果想得到文档的全部内容,需要指向文档开头和结尾的迭代器,可以用buffer.get_bounds()
检索。
buffer.get_bounds(start, end)
也可以用buffer.get_start_iter()
或buffer.get_end_iter()
独立地检索文本缓冲区的开始或结束迭代器。
可以用buffer.get_text()
检索缓冲区内的文本。它返回起始迭代器和结束迭代器之间的所有文本。如果最后一个参数设置为True
,那么也返回不可见文本。
buffer.get_text(start, end, boolean)
警告
你应该只使用buffer.get_text()
来获取一个缓冲区的全部内容。它会忽略文本缓冲区中嵌入的任何图像或小部件对象,因此字符索引可能不会对应到正确的位置。对于检索文本缓冲区的单个部分,使用buffer.get_slice()
代替。
回想一下,偏移量指的是缓冲区中单个字符的数量。这些字符可以是一个或多个字节长。buffer.get_iter_at_offset()
函数允许您在从缓冲区开始的特定偏移位置检索迭代器。
buffer.get_iter_at_offset(iter, character_offset)
GTK+ 还提供了buffer.get_iter_at_line_index()
,它选择一个单独的字节在指定行上的位置。使用这个函数时应该非常小心,因为索引必须始终指向 UTF-8 字符的开头。请记住,UTF-8 中的字符可能不仅仅是一个字节!
您可以用buffer.get_iter_at_line()
检索指定行上的第一个迭代器,而不是选择字符偏移量。
buffer.get_iter_at_line(iter, character_offset)
如果您想从特定行的第一个字符的偏移量处检索迭代器,buffer.get_iter_at_line_offset()
就可以做到这一点。
更改文本缓冲区内容
您已经学习了如何重置整个文本缓冲区的内容,但是只编辑文档的一部分也很有用。为此提供了许多功能。清单 8-4 向您展示了如何将文本插入到缓冲区中。
如果需要在缓冲区的任意位置插入文本,应该使用buffer.insert()
。为此,您需要一个指向插入点的Gtk.TextIter
、要插入缓冲区的文本字符串(必须是 UTF-8)和文本长度。
buffer.get_insert()
当调用这个函数时,文本缓冲区发出 insert-text 信号,文本迭代器失效。但是,文本迭代器会被重新初始化到插入文本的末尾。
一个名为insert_at_cursor()
的方便方法可以在光标当前位置调用buffer.insert()
。这可以很容易地通过使用插入文本标记来实现,但是它可以帮助您避免重复调用。
buffer.insert_at_cursor(text, length)
可以用gtk_text_buffer_delete()
删除两个文本迭代器之间的文本。指定迭代器的顺序无关紧要,因为方法会自动将它们按正确的顺序放置。
buffer.delete(start, end)
这个函数发出"delete-range"
信号,两个迭代器都失效了。然而,开始和结束迭代器都被重新初始化到被删除文本的开始位置。
剪切、复制和粘贴文本
图 8-5 显示了一个带有输入字段和按钮的文本视图,可以通过文本视图对象访问剪贴板功能。
图 8-5
Gtk.文本查看剪贴板按钮
三个剪贴板选项是剪切、复制和粘贴,这是几乎所有文本编辑器的标准选项。它们内置于每个Gtk.TextView
小工具中。但是,有时您希望实现这些功能的自己版本,以包含在应用菜单或工具栏中。
清单 8-5 给出了每种方法的例子。当点击三个Gtk.Button
部件中的一个时,一些动作被初始化。尝试使用按钮和右键菜单来显示两者使用相同的Gtk.Clipboard
对象。这些函数也可以使用内置的键盘快捷键调用,分别是 Ctrl+C ,Ctrl+X,Ctrl+V
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
textview = Gtk.TextView.new()
cut = Gtk.Button.new_with_label("Cut")
copy = Gtk.Button.new_with_label("Copy")
paste = Gtk.Button.new_with_label("Paste")
cut.connect("clicked", self.on_cut_clicked, textview)
copy.connect("clicked", self.on_copy_clicked, textview)
paste.connect("clicked", self.on_paste_clicked, textview)
scrolled_win = Gtk.ScrolledWindow.new(None, None)
scrolled_win.set_size_request(300, 200)
scrolled_win.add(textview)
hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 5)
hbox.pack_start(cut, True, True, 0)
hbox.pack_start(copy, True, True, 0)
hbox.pack_start(paste, True, True, 0)
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 5)
vbox.pack_start(scrolled_win, True, True, 0)
vbox.pack_start(hbox, True, True, 0)
self.add(vbox)
def on_cut_clicked(self, button, textview):
clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False))
buffer = textview.get_buffer()
buffer.cut_clipboard(clipboard, True)
def on_copy_clicked(self, button, textview):
clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False))
buffer = textview.get_buffer()
buffer.copy_clipboard(clipboard)
def on_paste_clicked(self, button, textview):
clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False))
buffer = textview.get_buffer()
buffer.paste_clipboard (clipboard, None, True)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Cut, Copy & Paste")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 8-5Using Text Iterators
Gtk.Clipboard
是一个中心类,数据可以很容易地在应用之间传输。要检索已经创建的剪贴板,您应该使用clipboard.get()
。GTK+ 3.x 只提供了一个默认的剪贴板。GTK+ 2.x 提供了命名的剪贴板,但是不再支持该功能。
注意
虽然可以创建自己的Gtk.Clipboard
对象,但是在执行基本任务时,应该使用默认的剪贴板。您可以通过执行方法Gdk.Atom.intern("CLIPBOARD", False)
到Gtk.Clipboard.get()
来检索它。
可以直接与您已经创建的Gtk.Clipboard
对象交互,从其中添加和删除数据。然而,当执行简单的任务时,包括为Gtk.TextView
小部件复制和检索文本字符串,使用Gtk.TextBuffer
的内置方法更有意义。
Gtk.TextBuffer
的三个剪贴板动作中最简单的一个是复制文本,可以通过以下方式完成:
buffer.copy_clipboard(clipboard)
第二个剪贴板函数,buffer.cut_clipboard(clipboard, True)
将选择复制到剪贴板并从缓冲区中移除。如果任何选定的文本没有设置可编辑标志,它将被设置为此函数的第三个参数。该函数不仅复制文本,还复制嵌入的对象,如图像和文本标签。
buffer.cut_clipboard(clipboard, True)
最后一个剪贴板功能,buffer.paste_clipboard()
首先检索剪贴板的内容。接下来,该函数做两件事情中的一件。如果已经指定了接受一个Gtk.TextIter
的第二个参数,那么内容将被插入到该迭代器的位置。如果您为第三个参数指定None
,内容将插入光标处。
buffer.paste_clipboard (clipboard, None, True)
如果任何要粘贴的内容没有设置可编辑标志,则它会自动设置为default_editable
。大多数情况下,您希望将此参数设置为True
,因为它允许编辑粘贴的内容。您还应该注意,粘贴操作是异步的。
搜索文本缓冲区
在大多数使用Gtk.TextView
小部件的应用中,您需要在一个或多个实例中搜索文本缓冲区。GTK+ 提供了两个在缓冲区中查找文本的函数:forward_search()
和backward_search()
。
以下示例向您展示了如何使用这些函数中的第一个函数在Gtk.TextBuffer
中搜索文本字符串;示例截图如图 8-6 所示。当用户单击“查找”按钮时,该示例开始。
图 8-6
搜索文本缓冲区的应用
清单 8-6 中的应用在文本缓冲区中搜索指定字符串的所有实例。向用户显示一个对话框,显示该字符串在文档中被找到的次数。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
textview = Gtk.TextView.new()
entry = Gtk.Entry.new()
entry.set_text("Search for ...")
find = Gtk.Button.new_with_label("Find")
find.connect("clicked", self.on_find_clicked, (textview, entry))
scrolled_win = Gtk.ScrolledWindow.new (None, None)
scrolled_win.set_size_request(250, 200)
scrolled_win.add(textview)
hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 5)
hbox.pack_start(entry, True, True, 0)
hbox.pack_start(find, True, True, 0)
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 5)
vbox.pack_start(scrolled_win, True, True, 0)
vbox.pack_start(hbox, True, True, 0)
self.add(vbox)
def on_find_clicked(self, button, w):
find = w[1].get_text()
find_len = len(find)
buffer = w[0].get_buffer()
start = buffer.get_start_iter()
end_itr = buffer.get_end_iter()
i = 0
while True:
end = start.copy()
end.forward_chars(find_len)
text = buffer.get_text(start, end, False)
if text == find:
i += 1
start.forward_chars(find_len)
else:
start.forward_char()
if end.compare(end_itr) == 0:
break
output = "The string '"+find+"' was found " + str(i) + " times!"
dialog = Gtk.MessageDialog(parent=self,
flags=Gtk.DialogFlags.MODAL,
message_type=Gtk.MessageType.INFO,
text=output, title="Information",
buttons=("OK", Gtk.ResponseType.OK))
dialog.run()
dialog.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self,
title="Searching Buffers")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 8-6Using The Gtk.TextIter Find Function
搜索函数需要做的第一件事是用buffer.get_start_iter()
和buffer.get_end_iter()
检索文档的搜索下限和上限。在后面的代码中,我们使用最终上限进行测试。
end = start.copy()
end.forward_chars(find_len)
搜索循环通过设置 end Gtk.TextIter
开始,然后增加搜索字符串的长度。这将创建一个与搜索字符串长度相等的缓冲区。
text = buffer.get_text(start, end, False)
buffer.get_text()
检索两个Gtk.TextIter
之间的文本。第三个参数是一个布尔值,指定是只检索文本还是在文本中包含其他标记。
if text == find:
i += 1
start.forward_chars(find_len)
else:
start.forward_char()
if end.compare(end_itr) == 0:
break
接下来,我们测试搜索字符串是否与缓冲区中的字符串匹配。如果找到一个匹配,那么我们增加我们的匹配计数器,并且将开始位置Gtk.TextIter
移过我们在缓冲区中找到的字符串。如果没有找到匹配,那么开始Gtk.TextIter
增加一个字符。最后,我们测试搜索上限Gtk.TextIter
是否等于缓冲区的结尾,如果两者相等,我们就跳出无限循环。
在我们跳出循环之后,我们向用户报告搜索结果。
滚动文本缓冲区
GTK+ 不会自动滚动到您选择的搜索匹配项。为此,您需要首先调用buffer.create_mark()
在找到文本的位置创建一个临时的Gtk.TextMark
。
buffer.create_mark(name, location, left_gravity)
buffer.create_mark()
的第二个参数允许你指定一个文本字符串作为标记的名称。该名称可以在没有实际标记对象的情况下引用标记。在指定文本迭代器的位置创建标记。如果设置为True
,最后一个参数创建一个左重力标记。
然后,使用view.scroll_mark_onscreen()
滚动缓冲区,使标记出现在屏幕上。完成标记后,您可以使用buffer.delete_mark()
将其从缓冲器中移除。
textview.scroll_mark_onscreen(mark)
view.scroll_mark_onscreen()
的问题是,它只滚动最小距离来显示屏幕上的标记。例如,您可能希望标记在缓冲区内居中。要指定标记在可见缓冲区中出现的位置的对准参数,调用textview.scroll_to_mark()
。
textview.scroll_to_mark(mark, margin, use_align, xalign, yalign)
首先放置一个页边空白,这样可以减少可滚动区域。边距必须指定为浮点数,以该系数缩小区域。在大多数情况下,您希望使用 0.0 作为边距,这样区域就不会缩小。
如果您为use_align
参数指定False
,该功能将滚动最小距离以在屏幕上显示标记;否则,该函数使用两个对齐参数作为指导,允许您在可见区域内指定标记的水平和垂直对齐。
0.0 的对齐指的是可视区域的左侧或顶部,1.0 指的是右侧或底部,0.5 指的是中心。该功能尽可能滚动,但可能无法将标记滚动到指定位置。例如,如果缓冲区大于一个字符高,就不可能将缓冲区中的最后一行滚动到顶部。
还有另一个函数textview.scroll_to_iter()
,其行为方式与textview.scroll_to_mark()
相同。唯一的区别是它接收一个Gtk.TextIter
而不是一个Gtk.TextMark
来表示位置,尽管在大多数情况下,你应该使用文本标记。
文本标签
提供了许多功能来改变一个Gtk.TextBuffer
中所有文本的属性,这在前面的章节中已经介绍过了。但是,如前所述,也可以用Gtk.TextTag
对象只改变单个文本部分的显示属性。
文本标签允许你创建文本风格在文本的不同部分有所不同的文档,这通常被称为富文本编辑。使用多种文本样式的Gtk.TextView
的截图如图 8-7 所示。
图 8-7
文本缓冲区中的格式化文本
文本标签实际上是一个非常简单的概念。在清单 8-7 中,创建了一个应用,允许用户应用多种样式或从选择中移除所有标签。阅读完本节的其余部分后,您可能想通过修改清单 8-7 来尝试其他文本属性,以包含不同的样式选项。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Pango
text_to_scales = [("Quarter Sized", 0.25),
("Double Extra Small", 0.5787037037037), ("Extra Small", 0.6444444444444), ("Small", 0.8333333333333), ("Medium", 1.0), ("Large", 1.2), ("Extra Large", 1.4399999999999), ("Double Extra Large", 1.728), ("Double Sized", 2.0)]
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(500, -1)
textview = Gtk.TextView.new()
buffer = textview.get_buffer()
buffer.create_tag("bold", weight=Pango.Weight.BOLD)
buffer.create_tag("italic", style=Pango.Style.ITALIC)
buffer.create_tag("strike", strikethrough=True)
buffer.create_tag("underline", underline=Pango.Underline.SINGLE)
bold = Gtk.Button.new_with_label("Bold")
italic = Gtk.Button.new_with_label("Italic")
strike = Gtk.Button.new_with_label("Strike")
underline = Gtk.Button.new_with_label("Underline")
clear = Gtk.Button.new_with_label("Clear")
scale_button = Gtk.ComboBoxText.new()
i = 0
while i < len(text_to_scales):
(name, scale) = text_to_scales[i]
scale_button.append_text(name)
buffer.create_tag(tag_name=name, scale=scale)
i += 1
bold.__setattr__("tag", "bold")
italic.__setattr__("tag", "italic")
strike.__setattr__("tag", "strike")
underline.__setattr__("tag", "underline")
bold.connect("clicked", self.on_format, textview)
italic.connect("clicked", self.on_format, textview)
strike.connect("clicked", self.on_format, textview)
underline.connect("clicked", self.on_format, textview)
clear.connect("clicked", self.on_clear_clicked, textview)
scale_button.connect("changed", self.on_scale_changed, textview)
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 5)
vbox.pack_start(bold, False, False, 0)
vbox.pack_start(italic, False, False, 0)
vbox.pack_start(strike, False, False, 0)
vbox.pack_start(underline, False, False, 0)
vbox.pack_start(scale_button, False, False, 0)
vbox.pack_start(clear, False, False, 0)
scrolled_win = Gtk.ScrolledWindow.new(None, None)
scrolled_win.add(textview)
scrolled_win.set_policy(Gtk.PolicyType.AUTOMATIC,
Gtk.PolicyType.ALWAYS)
hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 5)
hbox.pack_start(scrolled_win, True, True, 0)
hbox.pack_start(vbox, False, True, 0)
self.add(hbox)
def on_format(self, button, textview):
tagname = button.tag
buffer = textview.get_buffer()
(start, end) = buffer.get_selection_bounds()
buffer.apply_tag_by_name(tagname, start, end)
def on_scale_changed(self, button, textview):
if button.get_active() == -1:
return
text = button.get_active_text()
button.__setattr__("tag", text)
self.on_format(button, textview)
button.set_active(-1)
def on_clear_clicked(self, button, textview):
buffer = textview.get_buffer()
(start, end) = buffer.get_selection_bounds()
buffer.remove_all_tags(start, end)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Text Tags")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 8-7Using Text Iterators
当您创建一个文本标签时,您通常必须将它添加到一个Gtk.TextBuffer
的标签表中,这个对象包含了文本缓冲区中所有可用的标签。您可以用Gtk.TextTag.new()
创建一个新的Gtk.TextTag
对象,然后将它添加到标记表中。然而,你可以用buffer.create_tag()
一步完成这一切。
buffer.create_tag(tag_name, property_name=value)
第一个参数指定要添加到表Gtk.TextTag
中的标签的名称。该名称可以引用一个标签,对于该标签,您不再拥有Gtk.TextTag
对象。接下来的参数是一组关键字/值列表的Gtk.TextTag
样式属性和它们的值。
例如,如果您想创建一个文本标签,将背景色和前景色分别设置为黑色和白色,您可以使用下面的方法。这个函数返回创建的文本标记,尽管它已经被添加到文本缓冲区的标记表中。
buffer.create_tag("colors", background="#000000", foreground="#FFFFFF")
GTK+ 中有大量的样式属性可用。
一旦您创建了文本标签并将其添加到Gtk.TextBuffer
的标签表中,您就可以将其应用到文本范围。在清单 8-7 中,当点击一个按钮时,标签被应用到选中的文本。如果没有选定的文本,光标位置将设置为该样式。在该位置键入的所有文本也将应用标签。
标签一般应用于带buffer.apply_tag_by_name()
的文本。标签应用于起始迭代器和结束迭代器之间的文本。如果您仍然可以访问Gtk.TextTag
对象,您还可以应用一个带有buffer.apply_tag()
的标签。
buffer.apply_tag_by_name(tag_name, start, end)
虽然没有在清单 8-7 中使用,但是可以用buffer.remove_tag_by_name()
从文本区域中移除标签。这个函数删除两个迭代器之间标签的所有实例,如果它们存在的话。
buffer.remove_tag_by_name(tag_name, start, end)
注意
这些函数仅从特定范围的文本中移除标签。如果标签被添加到比指定范围更大的文本范围中,则标签将从较小的范围中删除,并在所选内容的两侧创建新的边界。您可以用清单 8-7 中的应用对此进行测试。
如果你有访问Gtk.TextTag
对象的权限,你可以用buffer.remove_tag()
移除标签。
也可以用buffer.remove_all_tags()
删除范围内的每个标签。
插入图像
在某些应用中,您可能希望将图像插入文本缓冲区。这很容易用Gdk.Pixbuf
对象来完成。在图 8-8 中,两幅图像作为Gdk.Pixbuf
对象被插入到文本缓冲区中。
图 8-8
文本缓冲区中的格式化文本
将 pixbuf 添加到Gtk.TextBuffer
分三步进行。首先,您必须创建 pixbuf 对象,并在它被插入的地方检索Gtk.TextIter
。然后,您可以使用buffer.insert_pixbuf()
将其添加到缓冲区。清单 8-8 展示了从一个文件创建一个Gdk.Pixbuf
对象并将其添加到一个文本缓冲区的过程。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(200, 150)
textview = Gtk.TextView.new()
buffer = textview.get_buffer()
text = " Undo\n Redo"
buffer.set_text(text, len(text))
icon_theme = Gtk.IconTheme.get_default()
undo = icon_theme.load_icon("edit-undo", -1,
Gtk.IconLookupFlags.FORCE_SIZE)
line = buffer.get_iter_at_line (0)
buffer.insert_pixbuf(line, undo)
redo = icon_theme.load_icon("edit-redo", -1,
Gtk.IconLookupFlags.FORCE_SIZE)
line = buffer.get_iter_at_line (1)
buffer.insert_pixbuf(line, redo)
scrolled_win = Gtk.ScrolledWindow.new(None, None)
scrolled_win.add(textview)
self.add (scrolled_win)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Pixbufs")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 8-8Inserting Images into Text Buffers
使用buffer.insert_pixbuf()
将Gdk.Pixbuf
对象插入文本缓冲区。在指定位置插入Gdk.Pixbuf
对象,它可以是缓冲区中任何有效的文本迭代器。
buffer.insert_pixbuf(iter, pixbuf)
不同的函数对 Pixbufs 进行不同的处理。例如,buffer.get_slice()
将0xFFFC
字符放在 pixbuf 所在的位置。然而,0xFFFC
字符可以作为一个实际的字符出现在缓冲区中,所以这不是一个 pixbuf 位置的可靠指示器。
另一个例子是buffer.get_text()
,它完全忽略了非文本元素,所以无法使用这个函数检查文本中的 pixbufs。
因此,如果在一个Gtk.TextBuffer
中使用 pixbufs,最好用buffer.get_slice()
从缓冲区中检索文本。然后你可以使用iter.get_pixbuf()
来检查0xFFFC
字符是否代表一个Gdk.Pixbuf
对象;如果在那个位置没有找到 pixbuf,它返回None
。
iter.get_pixbuf()
插入子部件
将小部件插入文本缓冲区比 pixbufs 稍微复杂一些,因为您必须通知文本缓冲区和文本视图来嵌入小部件。首先创建一个Gtk.TextChildAnchor
对象,它标记小部件在Gtk.TextBuffer
中的位置。然后,您将小部件添加到Gtk.TextView
小部件中。
图 8-9 显示了一个包含子Gtk.Button
小部件的Gtk.TextView
小部件。清单 8-9 创建了这个窗口。按下按钮后,调用self.destroy
,终止应用。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(25)
self.set_border_width(10)
self.set_size_request(250, 100)
textview = Gtk.TextView.new()
buffer = textview.get_buffer()
text = "\n Click to exit!"
buffer.set_text(text, len(text))
iter = buffer.get_iter_at_offset(8)
anchor = buffer.create_child_anchor(iter)
button = Gtk.Button.new_with_label("the button")
button.connect("clicked", self.on_button_clicked)
button.set_relief(Gtk.ReliefStyle.NORMAL)
textview.add_child_at_anchor(button, anchor)
scrolled_win = Gtk.ScrolledWindow.new(None, None)
scrolled_win.add(textview)
scrolled_win.set_policy(Gtk.PolicyType.AUTOMATIC,
Gtk.PolicyType.ALWAYS)
self.add(scrolled_win)
def on_button_clicked(self, button):
self.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Child Widgets")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 8-9Inserting Child Widgets into a Text Buffer
图 8-9
插入文本缓冲区的子部件
当创建一个Gtk.TextChildAnchor
时,需要对其进行初始化,并将其插入到一个Gtk.TextBuffer
中。你可以通过呼叫buffer.create_child_anchor()
来做到这一点。
buffer.create_child_anchor(iter)
在指定文本迭代器的位置创建一个子锚点。这个子锚只是一个标记,告诉 GTK+ 可以将一个子部件添加到文本缓冲区中的那个位置。
接下来,您需要使用textview.add_child_at_anchor()
向锚点添加一个子部件。与Gdk.Pixbuf
对象一样,子部件以0xFFFC
角色出现。这意味着,如果您看到这个字符,您需要检查它是一个子部件还是一个 pixbuf,因为否则它们是无法区分的。
textview.add_child_at_anchor(child, anchor)
要检查一个子部件是否在一个0xFFFC
字符的位置,你应该调用iter.get_child_anchor()
,如果一个子锚点不在那个位置,它将返回None
。
iter.get_child_anchor()
然后,您可以使用anchor.get_widgets()
检索在锚点添加的小部件列表。需要注意的是,在一个锚点上只能添加一个子部件,所以返回的列表通常只包含一个元素。
anchor.get_widgets()
例外情况是,当您对多个文本视图使用同一个缓冲区时。在这种情况下,可以将多个小部件添加到文本视图中的同一个锚点,只要没有文本视图包含一个以上的小部件。这是因为子部件被附加到由文本视图而不是文本缓冲区处理的锚点。
德国技术合作公司。source view-来源检视
Gtk.SourceView
是一个小部件,它实际上不是 GTK+ 库的一部分。它是一个扩展Gtk.TextView
小部件的外部库。如果你曾经使用过 gedit,你就体验过Gtk.SourceView
小部件。
Gtk.SourceView
小部件添加到文本视图中的特性有很多。下面是一些最著名的例子:
-
加行号
-
许多编程和脚本语言的语法突出显示
-
对包含语法突出显示的文档的打印支持
-
自动缩进
-
括号匹配
-
撤消/重做支持
-
用于在源代码中表示位置的源标记
-
突出显示当前行
图中显示了使用Gtk.SourceView
小部件的 gedit 的屏幕截图。它打开了行编号、语法突出显示、括号匹配和行突出显示。
图 8-10
插入文本缓冲区的子部件
Gtk.SourceView
库有完整独立的 API 文档,可以在 http://gtksourceview.sourceforge.net
查看。
测试你的理解能力
以下练习指导您创建一个具有基本功能的文本编辑应用。它让你练习与一个Gtk.TextView
小部件交互。
练习 1:文本编辑器
使用Gtk.TextView
小部件创建一个简单的文本编辑器。您应该能够执行多种文本编辑功能,包括创建新文档、打开文件、保存文件、搜索文档、剪切文本、复制文本和粘贴文本。
创建新文档时,您应该确保用户确实想要继续,因为所有的更改都会丢失。当保存按钮被按下时,它应该总是询问保存文件的位置。完成这个练习后,附录 d 中显示了一个解决方案。
暗示
这是一个比本书之前创建的任何 GTK+ 应用都要大得多的应用,所以在开始编写代码之前,您可能需要花几分钟时间在纸上规划您的解决方案。然后,一次实现一个功能,确保它在继续下一个功能之前有效。我们也将在后面的章节中展开这个练习,所以请将您的解决方案放在手边!
这是您在本书中使用的文本编辑器应用的第一个实例。在本书的最后几章中,您将学习一些新元素,帮助您创建一个功能全面的文本编辑器。
该应用在第十章中展开,您可以在其中添加一个菜单和一个工具栏。在第十三章中,你添加了打印支持和记忆过去打开的文件和搜索的能力。
本练习的解决方案在附录 d 中。文本编辑器解决方案的大部分功能已经由本章中的其他示例实现。因此,大多数解决方案对您来说应该很熟悉。这是一个最低限度的解决方案,我鼓励你扩展练习的基本要求,进行更多的练习。
摘要
在这一章中,你学习了所有关于Gtk.TextView
的知识,它允许你显示多行文本。文本视图通常包含在一种叫做Gtk.ScrolledWindow
的特殊类型的Gtk.Bin
容器中,该容器为子部件提供滚动条来实现滚动功能。
一个Gtk.TextBuffer
处理视图中的文本。文本缓冲区允许您使用文本标签来更改整个文本或部分文本的许多不同属性。它们还提供剪切、复制和粘贴功能。
您可以通过使用Gtk.TextIter
对象在整个文本缓冲区中移动,但是一旦文本缓冲区被改变,文本迭代器就变得无效。文本迭代器可以在整个文档中向前或向后搜索。要在缓冲区的更改上保留一个位置,您需要使用文本标记。文本视图不仅能够显示文本,还能够显示图像和子部件。子部件被添加在整个文本缓冲区的锚点上。
本章的最后一节简要介绍了Gtk.SourceView
小部件,它扩展了Gtk.TextView
小部件的功能。当您需要语法突出显示和行号等功能时,可以使用它。
在第九章中,你会看到两个新的部件:组合框和树形视图。组合框允许您从下拉列表中选择一个选项。树状视图允许您从滚动窗口通常包含的列表中选择一个或多个选项。Gtk.TreeView
是本书中最难的部件,所以请慢慢阅读下一章。
九、树形视图小部件
本章向您展示如何将Gtk.ScrolledWindow
小部件与另一个强大的小部件Gtk.TreeView
结合使用。树视图小部件可用于显示跨越一列或多列的列表或树中的数据。例如,一个Gtk.TreeView
可以用来实现一个文件浏览器或者显示构建一个集成开发环境的输出。
Gtk.TreeView
是一个复杂的小部件,因为它提供了多种多样的功能,所以请务必仔细阅读本章的每一节。然而,一旦你学会了这个强大的部件,你就能够在许多应用中应用它。
本章向您介绍了Gtk.TreeView
提供的大量特性。本章提供的信息使您能够构建树形视图小部件来满足您的需求。具体来说,在本章中,您将学习以下内容。
-
用什么对象来创建一个
Gtk.TreeView
以及它的模型-视图-控制器设计如何使它独一无二 -
如何用
Gtk.TreeView
小部件创建列表和树形结构 -
何时使用
Gtk.TreePath
、Gtk.TreeIter
或Gtk.TreeRowReference
来引用Gtk.TreeView
中的行 -
如何处理双击、单行选择和多行选择
-
如何创建可编辑的树视图单元格或使用单元格渲染器函数自定义单个单元格
-
可以嵌入到单元格中的小部件,包括切换按钮、像素缓冲器、微调按钮、组合框、进度条和键盘快捷键字符串
树视图的一部分
Gtk.TreeView
小部件用于显示组织成列表或树的数据。视图中显示的数据被组织成列和行。用户能够使用鼠标或键盘在树形视图中选择一行或多行。使用Gtk.TreeView
的 Nautilus 应用的截图如图 9-1 所示。
图 9-1
使用 Gtk。TreeView 部件
Gtk.TreeView
是一个很难使用的小部件,更难理解,所以这一整章都致力于使用它。但是,一旦您理解了小部件的工作原理,您就能够将它应用到各种各样的应用中,因为几乎可以定制小部件显示给用户的方式的每个方面。
Gtk.TreeView
的独特之处在于它遵循了通常被称为模型-视图-控制器(MVC)设计的设计概念。MVC 是一种信息和呈现方式完全相互独立的设计方法,类似于Gtk.TextView
和Gtk.TextBuffer
的关系。
Gtk 树模型
数据本身存储在实现Gtk.TreeModel
接口的类中。GTK+ 提供了四种内置的树模型类,但本章只介绍了Gtk.ListStore
和Gtk.TreeStore
。
Gtk.TreeModel
接口提供了一套标准的方法来检索关于存储数据的一般信息。例如,它允许您获得树中的行数和某一行的子行数。Gtk.TreeModel
还为您提供了一种检索存储在商店特定行中的数据的方法。
注意
模型、渲染器和列被称为对象,而不是小部件,即使它们是 GTK+ 库的一部分。这是一个重要的区别——因为它们不是从Gtk.Widget
派生的,它们没有 GTK+ 小部件可用的相同的功能、属性和信号。
Gtk.ListStore
允许你创建一个多列的元素列表。每一行都是根节点的子节点,因此只显示一层行。基本上,Gtk.ListStore
是一个没有层次的树形结构。之所以提供它,是因为存在更快的算法来与没有任何子项的模型进行交互。
Gtk.TreeStore
提供与Gtk.ListStore
相同的功能,只是数据可以组织成多层树。GTK+ 也提供了一种创建您自己的定制模型类型的方法,但是这两种可用的类型在大多数情况下应该是合适的。
虽然Gtk.ListStore
和Gtk.TreeStore
应该适合大多数应用,但有时您可能需要实现自己的商店对象。例如,如果它需要保存大量的行,您应该创建一个更高效的新模型。在第十二章中,你将学习如何创建从GObject
派生的新类,这可以作为你开始派生一个实现Gtk.TreeModel
接口的新类的指南。
创建树模型后,视图用于显示数据。通过分离树视图及其模型,您能够在多个视图中显示同一组数据。这些视图可以是彼此的精确副本,或者数据可以以不同的方式显示。对模型进行修改时,所有视图都会同时更新。
小费
虽然在多个树视图中显示同一组数据可能不会立即带来好处,但是可以考虑使用文件浏览器。如果您需要在多个文件浏览器中显示同一组文件,对每个视图使用相同的模型将节省内存,并使您的程序运行得更快。当您想要为文件浏览器提供多个显示选项时,这也很有用。在显示模式之间切换时,您不需要改变数据本身。
模型由包含相同数据类型的列和保存每组数据的行组成。每个模型列可以保存一种类型的数据。不应将树模型列与树视图列相混淆,树视图列由单个标题组成,但可以用来自多个模型列的数据来呈现。例如,树列可以显示一个文本字符串,该字符串具有由用户不可见的模型列定义的前景色。图 9-2 说明了模型列和树列的区别。
图 9-2
模型和树列之间的关系
模型中的每一行都包含对应于每个模型列的一段数据。在图 9-2 中,每一行包含一个文本字符串和一个Gdk.Color
值。这两个值用于在树列中用相应的颜色显示文本。在本章的后面,你将学习如何用代码实现这一点。现在,您应该简单地理解这两种类型的列之间的区别以及它们之间的关系。
新的列表和树存储是用许多列创建的,每一列都由现有的GObject.TYPE
定义。通常,您只需要使用那些已经在GLib
中实现的。例如,如果你想显示文本,你可以使用GObject.TYPE_STRING
、GObject.TYPE_BOOLEAN
和一些数字类型,如GObject.TYPE_INT
。
小费
因为可以用GObject.TYPE_POINTER
存储任意数据类型,所以可以使用一个或多个树模型列来简单地存储关于每一行的信息。当有大量行时,您只需要小心,因为内存使用量会迅速增加。您还必须自己负责释放指针。
Gtk。TreeViewColumn 和 Gtk。单元格渲染器
如前所述,树形视图显示一个或多个Gtk.TreeViewColumn
对象。树列由组织成一列的标题和数据单元格组成。每个树视图列还包含一个或多个可见的数据列。例如,在文件浏览器中,树视图列可能包含一列图像和一列文件名。
Gtk.TreeViewColumn
小部件的头部包含一个标题,描述下面的单元格中保存了什么数据。如果使列可排序,则当单击其中一个列标题时,将对行进行排序。
树状视图列实际上不会向屏幕呈现任何内容。这是通过从Gtk.CellRenderer
派生的对象来完成的。单元渲染器被打包到树视图列中,类似于将小部件添加到水平框中。每个树视图列可以包含一个或多个单元渲染器,用于渲染数据。例如,在文件浏览器中,图像列将使用
Gtk.CellRendererPixbuf
和带Gtk.CellRendererText
的文件名。这方面的一个例子如图 9-1 所示。
每个单元格呈现器负责呈现一列单元格,树视图中的每一行都有一个单元格。它从第一行开始,呈现其单元格,然后向下进行到下一行,直到呈现整列或部分列。
在 GTK+ 3 中g_object_set()
功能不再可用。因此,您必须向渲染器添加属性。列属性对应树模型列,并与单元渲染器属性相关联,如图 9-3 所示。这些属性在呈现时应用于每个单元格。
图 9-3
应用单元格渲染器属性
在图 9-3 中,有两个树模型列,类型分别为GObject.TYPE_STRING
和Gdk.RGBA
。这些应用于Gtk.CellRendererText
的文本和前景属性,并用于相应地渲染树视图列。
更改单元渲染器属性的另一种方法是定义单元数据函数。在呈现树视图之前,将为树视图中的每一行调用此函数。这允许您自定义每个单元格的呈现方式,而不需要将数据存储在树模型中。例如,单元格数据函数可用于定义要显示的浮点数的小数位数。单元数据函数在本章的“单元数据方法”一节中有详细介绍。
本章还介绍了用于显示文本(字符串、数字和布尔值)、切换按钮、微调按钮、进度条、像素缓冲区、组合框和键盘快捷键的单元格渲染器。此外,您可以创建自定义的单元格渲染器类型,但这通常是不需要的,因为 GTK+ 现在提供了如此广泛的类型。
这一节已经教了你使用Gtk.TreeView
小部件需要什么对象,它们做什么,以及它们如何相互关联。现在您已经对Gtk.TreeView
小部件有了基本的了解,下一节有一个简单的Gtk.ListStore
树模型的例子。
使用 Gtk。列表存储
回想一下上一节,Gtk.TreeModel
只是一个由数据存储实现的接口,比如Gtk.ListStore
。Gtk.ListStore
用于创建行之间没有层次关系的数据列表。
在本节中,实现了一个简单的杂货列表应用,它包含三列,所有列都使用Gtk.CellRendererText
。图 9-4 是这个应用的截图。第一列是显示True
或False
的布尔值,用于定义是否应该购买该产品。
小费
您通常不希望将布尔值显示为文本,因为如果有许多布尔列,用户将无法管理。相反,您希望使用切换按钮。您将在后面的章节中学习如何使用Gtk.CellRendererToggle
来完成这项工作。布尔值通常也用作列属性来定义单元格渲染器属性。
图 9-4
使用 Gtk 的树视图小部件。列表存储树模型
清单 9-1 创建了一个Gtk.ListStore
对象,它显示了一份食品清单。除了显示产品,列表商店还显示是否购买产品以及购买多少。
在本章的其余部分,这个杂货列表应用被用于许多示例。因此,一些函数的内容如果出现在前面的例子中,以后可能会被排除。此外,为了让事情有条理,在每个示例中,setup_tree_view()
用于设置列和呈现器。每个例子的完整代码清单可以在 www.gtkbook.com
下载。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject
BUY_IT = 0
QUANTITY = 1
PRODUCT = 2
GroceryItem = (( True, 1, "Paper Towels" ),
( True, 2, "Bread" ),
( False, 1, "Butter" ),
( True, 1, "Milk" ),
( False, 3, "Chips" ),
( True, 4, "Soda" ))
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(250, 175)
treeview = Gtk.TreeView.new()
self.setup_tree_view(treeview)
store = Gtk.ListStore.new((GObject.TYPE_BOOLEAN,
GObject.TYPE_INT,
GObject.TYPE_STRING))
for row in GroceryItem:
iter = store.append(None)
store.set(iter, BUY_IT, row[BUY_IT], QUANTITY,
row[QUANTITY], PRODUCT, row[PRODUCT])
treeview.set_model(store)
scrolled_win = Gtk.ScrolledWindow.new(None, None)
scrolled_win.set_policy(Gtk.PolicyType.AUTOMATIC,
Gtk.PolicyType.AUTOMATIC)
scrolled_win.add(treeview)
self.add(scrolled_win)
def setup_tree_view(self, treeview):
renderer = Gtk.CellRendererText.new()
column = Gtk.TreeViewColumn("Buy", renderer, text=BUY_IT)
treeview.append_column(column)
renderer = Gtk.CellRendererText.new()
column = Gtk.TreeViewColumn("Count", renderer, text=QUANTITY)
treeview.append_column(column)
renderer = Gtk.CellRendererText.new()
column = Gtk.TreeViewColumn("Product", renderer, text=PRODUCT)
treeview.append_column(column)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Grocery List")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 9-1Using a Gtk.FontSelectionDialog
创建树视图
创建Gtk.TreeView
小部件是这个过程中最简单的部分。你只需要调用Gtk.TreeView.new()
。用treeview.set_model(store)
初始化后,树形模型可以很容易地应用到Gtk.TreeView
上。
在 GTK+ 3 出现之前,有一些函数可以隐藏/取消隐藏Gtk.TreeViewColumn
的列标题。这些函数在 GTK+ 3 中已经被否决了,现在所有的列标题都是可见的。
对于某些树视图,标题提供了比列标题更多的功能。在可排序的树模型中,单击列标题可以根据相应列中保存的数据启动所有行的排序。如果适用,它还会直观地指示列的排序顺序。如果用户需要标题来对树视图行进行排序,则不应隐藏标题。
作为一名 GTK+ 开发人员,您应该非常小心地改变视觉属性。用户可以选择适合他们需求的主题,并且您可以通过改变小部件的显示方式来使您的应用不可用。
渲染器和列
在创建了Gtk.TreeView
之后,您需要向视图中添加一个或多个列,这样它才能发挥作用。每个Gtk.TreeViewColumn
由一个标题和至少一个单元格渲染器组成,标题显示其内容的简短描述。树状视图列实际上不呈现任何内容。树视图列包含一个或多个用于在屏幕上绘制数据的单元渲染器。
所有的单元格渲染器都是从Gtk.CellRenderer
类派生的,在本章中被称为对象,因为Gtk.CellRenderer
是直接从GObject
派生的,而不是从Gtk.Widget
派生的。每个单元格渲染器都包含许多属性,这些属性决定了数据在单元格中的绘制方式。
Gtk.CellRenderer
类为所有衍生渲染器提供了公共属性,包括背景颜色、大小参数、对齐、可见性、敏感度和填充。在附录 a 中可以找到Gtk.CellRenderer
属性的完整列表。它还提供了编辑取消和编辑开始信号,允许您在自定义单元渲染器中实现编辑。
在清单 9-1 中,向您介绍了Gtk.CellRendererText
,它能够将字符串、数字和布尔值呈现为文本。文本单元格渲染器用Gtk.CellRendererText.new()
初始化。
Gtk.CellRendererText
提供了许多附加属性,这些属性决定了每个单元格是如何呈现的。您应该始终设置 text 属性,它是显示在单元格中的字符串。其余的属性类似于文本标签使用的属性。
包含了大量的属性,这些属性决定了每一行是如何呈现的。在下面的例子中使用了renderer.foreground-rgba()
将渲染器中每段文本的前景色设置为橙色。一些属性也有相应的 set 属性,如果您想使用这个值,必须将它设置为True
。例如,您应该将前景设置为True
以使更改生效。
renderer.props.foreground-rgba = Gdk.RGBA(red=1.0, green=0.65, blue=0.0,
alpha=1.0)
创建单元格渲染器后,需要将其添加到Gtk.TreeViewColumn
中。如果您只想让列显示一个单元格渲染器,可以使用Gtk.TreeViewColumn()
创建树视图列。在下面的代码中,创建了一个标题为“Buy”的树视图列和一个具有一个属性的渲染器。当Gtk.ListStore
被填充时,该属性被称为BUY_IT
(值为 0)。
column = Gtk.TreeViewColumn("Buy", renderer, text=BUY_IT)
前面的函数接受在列标题、单元格渲染器和属性列表中显示的字符串。每个属性都包含一个字符串,该字符串引用渲染器属性和树视图列号。需要认识的重要一点是,提供给Gtk.TreeViewColumn()
的列号指的是树模型列,它可能与树视图使用的树模型列或单元渲染器的数量不同。
在 Python 3 中,Gtk.TreeViewColumn()
很难实现。这不仅是一种方便的方法,也是创建Gtk.TreeViewColumn()
的首选方法。下面的代码片段是在 Python 3 中创建一个Gtk.TreeViewColumn()
并分配至少一个属性的正确方法。
renderer = Gtk.CellRendererText.new()
column = Gtk.TreeViewColumn("Buy", renderer, text=BUY_IT)
treeview.append_column(column)
如果要向树视图列添加多个渲染器,需要打包每个渲染器并单独设置其属性。例如,在文件管理器中,您可能希望在同一列中包含文本和图像渲染器。但是,如果每一列只需要一个单元格渲染器,那么使用Gtk.TreeViewColumn()
是最简单的。
注意
如果您希望某个属性(比如前景色)在列中的每一行都设置为相同的值,那么您应该使用renderer.foreground-rgba()
将该属性直接应用到单元格渲染器。但是,如果该属性因行而异,则应该将其作为给定渲染器的列的属性添加。
在您完成了树视图列的设置后,需要用treeview.append_column(column)
将其添加到树视图中。也可以用treeview.insert_column(column)
将列添加到树形视图的任意位置,或者用treeview.remove_column(column)
将列从视图中删除。
创建 Gtk。列表存储
现在已经用所需的单元渲染器设置了树视图列,所以是时候创建渲染器和树视图之间的树模型了。对于清单 9-1 中的例子,我们使用了Gtk.ListStore
,这样项目就会显示为一个元素列表。
使用Gtk.ListStore.new()
创建新的列表存储。该函数接受列数和每列保存的数据类型。在清单 9-1 中,列表存储有三列,分别存储布尔、整数和字符串数据类型。
Gtk.ListStore.new((GObject.TYPE_BOOLEAN, GObject.TYPE_INT,
GObject.TYPE_STRING))
在 Python 3 中,列类型参数形成一个元组。它不仅告诉方法列的类型,还告诉方法列的数量。
在创建列表存储之后,您需要添加带有store.append(None)
的行,这样它才有用。这个方法向列表存储追加一个新行,迭代器被设置为指向新行。在这一章的后面你会学到更多关于树迭代器的知识。现在,知道它指向新的树视图行就足够了。
iter = store.append(None)
store.set(iter, BUY_IT, row[BUY_IT], QUANTITY, row[QUANTITY],
PRODUCT, row[PRODUCT])
接下来,我们需要设置哪一列和哪些值将被加载数据。这是通过store.set()
方法完成的。使用此方法可以设置一行或多行。前面的示例从左到右在该行的每一列中存储值,但是该列可以按任何顺序列出,因为我们还指定了加载值的列号。
注意
Gtk.CellRendererText
自动将布尔值和数字转换成可在屏幕上显示的文本字符串。因此,应用于文本属性列的数据类型不必是文本本身,而只需与在初始化Gtk.ListStore
期间定义的列表存储列类型一致。
向列表存储中添加行还有多个其他函数,包括store.prepend()
和store.insert()
。可用函数的完整列表可以在Gtk.ListStore
API 文档中找到。
除了添加行,还可以用store.remove()
删除行。该函数删除Gtk.TreeIter
引用的行。删除行后,迭代器指向列表存储中的下一行,函数返回True
。如果刚刚删除了最后一行,迭代器就无效了,函数返回False
。
store.remove(iter)
此外,还提供了store.clear()
,它可用于从列表存储中删除所有行。留给您的是一个不包含任何数据的Gtk.ListStore
。
创建列表存储后,需要调用treeview.set_model()
将其添加到树视图中。通过调用此方法,树模型的引用计数增加 1。
使用 Gtk。TreeStore
还有一种称为Gtk.TreeStore
的内置树模型,它将行组织成多级树结构。也可以用一个Gtk.TreeStore
树模型实现一个列表,但是不推荐这样做,因为当对象假设行可能有一个或多个子对象时,会增加一些开销。
图 9-5 显示了一个示例树存储,它包含两个根元素,每个元素都有自己的子元素。通过单击带有子项的行左侧的扩展器,可以显示或隐藏其子项。这类似于Gtk.Expander
小部件提供的功能。
图 9-5
使用 Gtk 的树视图小部件。TreeStore 树模型
用Gtk.TreeStore
而不是Gtk.ListStore
实现的Gtk.TreeView
之间的唯一区别在于商店的创建。对于这两个模型,添加列和渲染器的方式是相同的,因为列是视图的一部分,而不是模型的一部分。执行清单 9-2 将产生如图 9-5 所示的对话框。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject
BUY_IT = 0
QUANTITY = 1
PRODUCT = 2
PRODUCT_CATEGORY = 0
PRODUCT_CHILD = 1
GroceryItem = (( PRODUCT_CATEGORY, True, 0, "Cleaning Supplies"),
( PRODUCT_CHILD, True, 1, "Paper Towels" ),
( PRODUCT_CHILD, True, 3, "Toilet Paper" ),
( PRODUCT_CATEGORY, True, 0, "Food"),
( PRODUCT_CHILD, True, 2, "Bread" ),
( PRODUCT_CHILD, False, 1, "Butter" ),
( PRODUCT_CHILD, True, 1, "Milk" ),
( PRODUCT_CHILD, False, 3, "Chips" ),
( PRODUCT_CHILD, True, 4, "Soda" ))
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(275, 270)
treeview = Gtk.TreeView.new()
self.setup_tree_view(treeview)
store = Gtk.TreeStore.new((GObject.TYPE_BOOLEAN,
GObject.TYPE_INT,
GObject.TYPE_STRING))
iter = None
i = 0
for row in GroceryItem:
(ptype, buy, quant, prod) = row
if ptype == PRODUCT_CATEGORY:
j = i + 1
(ptype1, buy1, quant1, prod1) = GroceryItem[j]
while j < len(GroceryItem) and ptype1 == PRODUCT_CHILD:
if buy1:
quant += quant1
j += 1;
if j < len(GroceryItem):
(ptype1, buy1, quant1, prod1) = GroceryItem[j] iter = store.append(None)
store.set(iter, BUY_IT, buy, QUANTITY, quant, PRODUCT, prod)
else:
child = store.append(iter)
store.set(child, BUY_IT, buy, QUANTITY, quant, PRODUCT, prod)
i += 1
treeview.set_model(store)
treeview.expand_all()
scrolled_win = Gtk.ScrolledWindow.new(None, None)
scrolled_win.set_policy(Gtk.PolicyType.AUTOMATIC,
Gtk.PolicyType.AUTOMATIC)
scrolled_win.add(treeview)
self.add(scrolled_win)
def setup_tree_view(self, treeview):
renderer = Gtk.CellRendererText.new()
column = Gtk.TreeViewColumn("Buy", renderer, text=BUY_IT)
treeview.append_column(column)
renderer = Gtk.CellRendererText.new()
column = Gtk.TreeViewColumn("Count", renderer, text=QUANTITY)
treeview.append_column(column)
renderer = Gtk.CellRendererText.new()
column = Gtk.TreeViewColumn("Product", renderer, text=PRODUCT)
treeview.append_column(column)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Grocery List")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 9-2Creating a Gtk.TreeStore
树存储用Gtk.TreeStore.new()
初始化,它接受与Gtk.ListStore.new()
相同的参数。列类型参数形成一个元组。它不仅告诉方法列的类型,还告诉方法列的数量。
向树存储添加行与向列表存储添加行略有不同。使用store.append()
向树存储添加行,它接受一个迭代器或None
。迭代器应该指向新行的父行。该方法返回一个迭代器,当函数返回时,该迭代器指向插入的行,第二个。
iter = store.append(None)
在前面对store.append()
的调用中,通过将None
作为父迭代器传递,一个根元素被追加到列表中。该方法返回的iter
树迭代器被设置为新行的位置。
在随后对store.append()
的第二次调用中,该行被添加为 iter 的子行。接下来,当方法返回时,子树迭代器被设置为树存储中新行的当前位置。
child = store.append(iter)
与列表存储一样,有许多方法可用于向树存储添加行。这些包括store.insert()
、store.prepend()
和store.insert_before()
等等。要获得完整的方法列表,您应该参考Gtk.TreeStore
API 文档。
向树存储中添加一行后,它只是一个没有数据的空行。要向行中添加数据,调用store.set()
。该功能的工作方式与store.set()
相同。它接受树存储、指向行位置的树迭代器和列数据对列表。这些列号对应于设置单元渲染器属性时使用的列号。
store.set(child, BUY_IT, buy, QUANTITY, quant, PRODUCT, prod)
除了向树存储中添加行之外,还可以用store.remove()
删除它们。该函数删除由Gtk.TreeIter
引用的行。删除行后,iter
指向树存储中的下一行,函数返回True
。如果您删除的行是树存储中的最后一行,迭代器将失效,函数将返回False
。
store.remove(iter)
此外,还提供了store.clear()
,它可用于从树存储中移除所有行。留给您的是一个不包含任何数据的Gtk.TreeStore
。
在清单 9-2 中,treeview.expand_all()
被调用来展开所有的行。这是一个递归函数,可以扩展每一个可能的行,尽管它只影响具有父子行关系的树模型。此外,您可以使用treeview.collapse_all()
折叠所有的行。默认情况下,所有行都是折叠的。
引用行
有三个对象可用于引用树模型中的特定行;各有各的独特优势。他们是Gtk.TreePath
、Gtk.TreeIter
和Gtk.TreeRowReference
。在下面几节中,您将了解每个对象的工作原理以及如何在您自己的程序中使用它们。
树状路径
例如,如果您看到的是字符串 3:7:5,那么您将从第四个根元素开始(回想一下,索引从零开始,因此元素 3 实际上是该级别中的第四个元素)。接下来,您将继续处理该根元素的第八个子元素。这一排是那个孩子的第六个孩子。
为了形象地说明这一点,图 9-6 显示了在图 9-5 中创建的树形视图,其中标注了树路径。每个根元素仅被称为一个元素,即 0 和 1。第一个根元素有两个子元素,称为 0:0 和 0:1。
图 9-6
使用 Gtk 的树视图的树路径。TreeStore
提供了两个函数,允许您在路径和它的等价字符串之间来回转换:treepath.to_string()
和Gtk.TreePath.new_from_string()
。除非您试图保存树视图的状态,否则通常不必直接处理字符串路径,但是使用它有助于理解树路径的工作方式。
清单 9-3 给出了一个使用树路径的简短例子。它首先创建一个指向面包产品行的新路径。接下来,treepath.up()
在路径中向上移动一级。当您将路径转换回字符串时,您会看到结果输出为 1,指向食物行。
treepath = Gtk.TreePath.new_from_string("1:0")
treepath.up(path)
str = treepath.to_string(path)
print(str)
Listing 9-3Converting Between Paths and Strings
小费
如果您需要获得一个树迭代器,并且只有可用的路径字符串,您可以将字符串转换成一个Gtk.TreePath
然后转换成一个Gtk.TreeIter
。然而,更好的解决方案是用treemodel.get_iter_from_string()
跳过中间步骤,将树路径字符串直接转换成树迭代器。
除了treepath.up()
之外,还有其他的功能可以让你浏览一个树形模型。您可以使用treepath.down()
移动到子行,使用treepath.next()
或treepath.prev()
移动到同一级别的下一行或上一行。当您移动到前一行或父行时,如果不成功,将返回False
。
有时,您可能需要一个整数列表而不是字符串形式的树路径。treepath.get_indices()
函数返回组成路径字符串的整数。
treepath.get_indices(path)
当在树模型中添加或删除一行时,树路径可能会出现问题。该路径可能会指向树中的另一行,或者更糟,指向一个不再存在的行!例如,如果一个树路径指向树的最后一个元素,而您删除了该行,那么它现在指向树的界限之外。要解决这个问题,您可以将树路径转换为树行引用。
树行引用
对象用于观察树模型的变化。在内部,它们连接到“行插入”、“行删除”和“行重新排序”信号,根据变化更新存储的路径。
从现有的Gtk.TreeModel
和Gtk.TreePath
中用Gtk.TreeRowReference.new()
创建新的树行引用。复制到行引用中的树路径会随着模型中发生的变化而更新。
treerowref.new(model, path)
当您需要检索路径时,您可以使用treerowref.get_path()
,如果该行不再存在于模型中,它将返回None
。树行引用能够基于树模型中的更改更新树路径,但是如果您从与树路径的行相同的级别中移除所有元素,则树路径将不再有指向的行。
您应该知道,当在树模型中添加、删除或排序行时,树行引用确实会增加一点处理开销,因为引用必须处理这些操作发出的所有信号。对于大多数应用来说,这种开销无关紧要,因为没有足够的行让用户注意到。但是,如果您的应用包含大量的行,您应该明智地使用树行引用。
树迭代器
GTK+ 提供了Gtk.TreeIter
对象,可以用来引用Gtk.TreeModel
中的特定行。这些迭代器由模型内部使用,这意味着您永远不应该直接改变树迭代器的内容。
您已经看到了Gtk.TreeIter
的多个实例,从中可以看出树迭代器的使用方式与Gtk.TreeIter
类似。树迭代器用于树模型的操作。然而,树路径用于以提供人类可读界面的方式指向树模型中的行。树行引用可用于确保树路径在树模型的整个变化中调整它们指向的位置。
GTK+ 提供了许多内置方法来对树迭代器执行操作。通常,迭代器用于向模型添加行,设置行的内容,以及检索模型的内容。在图 9-1 和图 9-2 中,使用树迭代器向Gtk.ListStore
和Gtk.TreeStore
模型添加行,然后设置每行的初始内容。
Gtk.TreeModel
提供了许多iter_*()
方法,可以用来移动迭代器和检索关于它们的信息。例如,要移动到下一个迭代器位置,您可以使用treemodel.iter_next()
,如果操作成功,它将返回True
。可用函数的完整列表可以在Gtk.TreeModel
API 文档中找到。
使用treemodel.get_path()
和treemodel.get_iter()
很容易在树迭代器和树路径之间转换。树路径或迭代器必须有效,这些函数才能正常工作。清单 9-4 给出了一个如何在Gtk.TreeIter
和Gtk.TreePath
之间转换的简短示例。
path = treemodel.get_path(model, iter)
iter = treemodel.get_iter(model, path)
Listing 9-4Converting Between Paths and Iterators
清单 9-4 ,treemodel.get_path()
中的第一个方法将一个有效的树迭代器转换成一个树路径。该路径随后被发送到treemodel.get_iter()
,后者将其转换回迭代器。注意,第二个方法接受两个参数。
Gtk.TreeIter
提出的一个问题是,不保证迭代器在一个模型被编辑后存在。这并不是在所有情况下都成立,你可以使用treemodel.get_flags()
来检查Gtk.TreeModelFlags.ITERS_PERSIST
标志,默认情况下Gtk.ListStore
和Gtk.TreeStore
的标志是打开的。如果设置了这个标志,只要行存在,树迭代器总是有效的。
treemodel.get_flags()
即使迭代器被设置为持久化,存储树迭代器对象也不是一个好主意,因为它们是由树模型内部使用的。相反,您应该使用树行引用来随时跟踪行,因为树模型改变时引用不会失效。
添加行和处理选择
到目前为止,给出的两个例子都是在启动过程中定义树模型的。内容在最初设置后不会改变。在这个部分中,食品杂货列表应用被扩展为允许用户添加和删除产品。在介绍这个例子之前,您将学习如何处理单选和多选。
单项选择
每个树形视图的选择信息由一个Gtk.TreeSelection
对象保存。可以用treeview.get_selection()
检索这个对象。每个Gtk.TreeView
都会自动为您创建一个Gtk.TreeSelection
对象,因此您无需创建自己的树选择。
警告
Gtk.TreeSelection
提供一个信号“changed”,当选择改变时发出。使用这种信号时要小心,因为它并不总是可靠的。当用户选择一个已经选定的行而没有发生任何更改时,可以发出该消息。因此,最好使用Gtk.TreeView
提供的信号进行选择处理,这在附录 b 中。
树状视图支持多种类型的选择。您可以使用treeselection.set_mode()
改变选择类型。选择类型由Gtk.SelectionMode
枚举定义,它包括以下值。
-
Gtk.SelectionMode.NONE
:禁止用户选择任何行。 -
Gtk.SelectionMode.SINGLE
:用户最多可以选择一行,也有可能不选择任何一行。默认情况下,树选择用Gtk.SelectionMode.SINGLE
初始化。 -
Gtk.SelectionMode.BROWSE
:用户可以选择一行。在极少数情况下,可能没有选定的行。该选项实际上禁止用户取消选择某一行,除非将选择移动到另一行。 -
Gtk.SelectionMode.MULTIPLE
:用户可以选择任意行数。用户能够使用 Ctrl 和 Shift 键来选择附加元素或元素范围。
如果您将选择类型定义为Gtk.SelectionMode.SINGLE
或Gtk.SelectionMode.BROWSE
,您可以确保只选择一行。对于只有一个选择的树视图,您可以使用treeselection.get_selected()
来检索选中的行。
treeselection.get_selected(model, iter)
treeselection.get_selected()
方法可用于检索与Gtk.TreeSelection
对象相关联的树模型和指向所选行的树迭代器。如果模型和迭代器设置成功,则返回True
。该功能在选择模式为Gtk.SelectionMode.MULTIPLE
时不起作用!
如果没有选择行,树迭代器被设置为None
,函数返回False
。因此,treeselection.get_selected()
也可以作为一个测试来检查是否有一个选中的行。
多重选择
如果您的树选择允许选择多行(Gtk.SelectionMode.MULTIPLE
),那么您有两个选项来处理选择,为每一行调用一个函数或以 Python 列表的形式检索所有选择的行。您的第一个选择是用treeselection.selected_foreach()
为每个选中的行调用一个函数。
treeselection.selected_foreach(selected, foreach_func, None)
这个函数允许您为每个选中的行调用selected_foreach_func()
,传递一个可选的数据参数。在前面的示例中,None
被传递给了函数。该函数必须是 Python 函数或方法,如清单 9-5 所示。清单 9-5 中的函数检索产品字符串并将其打印到屏幕上。
foreach_func(model, path, iter, data)
(text,) = model.get(iter, PRODUCT)
print ("Selected Product: %s" % text)
Listing 9-5Selected for-each Function
注意
您不应该在foreach_func
实现中修改树模型或选择!如果这样做,GTK+ 会给用户带来严重的错误,因为可能会导致无效的树路径和迭代器。
还要注意方法model.get()
总是返回一个元组,即使你只要求一个单独的列。
使用树选择foreach_func
函数的一个问题是,您不能从函数内部操纵选择。为了解决这个问题,更好的解决方案是使用treeselection.get_selected_rows()
,它返回一个包含Gtk.TreePath
对象的 Python 列表,每个对象指向一个选定的行。
treeselection.get_selected_rows(model)
然后,您可以对列表中的每一行执行一些操作。然而,你需要小心。如果您需要编辑列表中的树模型,您需要首先将所有的树路径转换为树行引用,以便它们在整个操作期间继续有效。
如果您想手动遍历所有的行,您也可以使用treeselection.count_selected_rows()
,它返回当前选择的行数。
添加新行
现在您已经了解了选择,是时候添加向列表中添加新产品的功能了。
与之前的杂货列表应用相比,本例中唯一的区别如图 9-7 所示,图中显示了在树形视图的底部添加了添加和删除按钮。此外,选择模式已更改为允许用户一次选择多行。
图 9-7
编辑杂货清单中的商品
清单 9-6 是用户点击添加按钮时运行的回调函数的实现。它向用户呈现一个Gtk.Dialog
,要求用户选择一个类别,输入产品名称和要购买的产品数量,并选择是否购买该产品。
如果所有字段都有效,该行将被添加到所选类别下。此外,如果用户指定应该购买该产品,则该数量将被添加到该类别的总数量中。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject
BUY_IT = 0
QUANTITY = 1
PRODUCT = 2
PRODUCT_CATEGORY = 0
PRODUCT_CHILD = 1
GroceryItem = (( PRODUCT_CATEGORY, True, 0, "Cleaning Supplies"), ( PRODUCT_CHILD, True, 1, "Paper Towels" ),
( PRODUCT_CHILD, True, 3, "Toilet Paper" ), ( PRODUCT_CATEGORY, True, 0, "Food"), ( PRODUCT_CHILD, True, 2, "Bread" ),
( PRODUCT_CHILD, False, 1, "Butter" ),
( PRODUCT_CHILD, True, 1, "Milk" ),
( PRODUCT_CHILD, False, 3, "Chips" ),
( PRODUCT_CHILD, True, 4, "Soda" ))
class AddDialog(Gtk.Dialog):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
parent = kwargs['parent']
# set up buttons
self.add_button("Add", Gtk.ResponseType.OK)
self.add_button("Cancel", Gtk.ResponseType.CANCEL)
# set up dialog widgets
combobox = Gtk.ComboBoxText.new()
entry = Gtk.Entry.new()
spin = Gtk.SpinButton.new_with_range(0, 100, 1)
check = Gtk.CheckButton.new_with_mnemonic("_Buy the Product")
spin.set_digits(0)
# Add all of the categories to the combo box. for row in GroceryItem:
(ptype, buy, quant, prod) = row
if ptype == PRODUCT_CATEGORY:
combobox.append_text(prod)
# create a grid
grid = Gtk.Grid.new()
grid.set_row_spacing (5)
grid.set_column_spacing(5)
# fill out the grid
grid.attach(Gtk.Label.new("Category:"), 0, 0, 1, 1)
grid.attach(Gtk.Label.new("Product:"), 0, 1, 1, 1)
grid.attach(Gtk.Label.new("Quantity:"), 0, 2, 1, 1)
grid.attach(combobox, 1, 0, 1, 1)
grid.attach(entry, 1, 1, 1, 1)
grid.attach(spin, 1, 2, 1, 1)
grid.attach(check, 1, 3, 1, 1)
self.get_content_area().pack_start(grid, True, True, 5) self.show_all()
# run the dialog and check the results
if self.run() != Gtk.ResponseType.OK:
self.destroy()
return
quantity = spin.get_value()
product = entry.get_text()
category = combobox.get_active_text()
buy = check.get_active()
if product == "" or category == None:
print("All of the fields were not correctly filled out!")
return
model = parent.get_treeview().get_model();
iter = model.get_iter_from_string("0")
# Retrieve an iterator pointing to the selected category. while iter:
(name,) = model.get(iter, PRODUCT)
if name == None or name.lower() == category.lower():
break
iter = model.iter_next(iter)
#
#
# Convert the category iterator to a path so that it # will not become invalid and add the new product as a child of the category.
path = model.get_path(iter)
child = model.append(iter)
model.set(child, BUY_IT, buy, QUANTITY, quantity, PRODUCT, product)
# Add the quantity to the running total if it is to be purchased. if buy:
iter = model.get_iter(path)
(i,) = model.get(iter, QUANTITY) i += quantity
model.set(iter, QUANTITY, i)
self.destroy()
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(275, 270)
self.treeview = Gtk.TreeView.new()
self.setup_tree_view(self.treeview)
store = Gtk.TreeStore.new((GObject.TYPE_BOOLEAN,
GObject.TYPE_INT,
GObject.TYPE_STRING))
iter = None
i = 0
for row in GroceryItem:
(ptype, buy, quant, prod) = row
if ptype == PRODUCT_CATEGORY:
j = i + 1
(ptype1, buy1, quant1, prod1) = GroceryItem[j]
while j < len(GroceryItem) and ptype1 == PRODUCT_CHILD:
if buy1:
quant += quant1
j += 1;
if j < len(GroceryItem):
(ptype1, buy1, quant1, prod1) = GroceryItem[j] iter = store.append(None)
store.set(iter, BUY_IT, buy, QUANTITY, quant, PRODUCT, prod)
else:
child = store.append(iter)
store.set(child, BUY_IT, buy, QUANTITY, quant, PRODUCT, prod)
i += 1
self.treeview.set_model(store)
self.treeview.expand_all()
scrolled_win = Gtk.ScrolledWindow.new(None, None)
scrolled_win.set_policy(Gtk.PolicyType.AUTOMATIC,
Gtk.PolicyType.AUTOMATIC)
scrolled_win.add(self.treeview)
button_add = Gtk.Button.new_with_label("Add")
button_add.connect("clicked", self.on_add_button_clicked, self)
button_remove = Gtk.Button.new_with_label("Remove")
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
hbox.pack_end(button_remove, False, True, 5)
hbox.pack_end(button_add, False, True, 5)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
vbox.pack_end(hbox, False, True, 5)
vbox.pack_end(scrolled_win, True, True, 5)
self.add(vbox)
def setup_tree_view(self, treeview):
renderer = Gtk.CellRendererText.new()
column = Gtk.TreeViewColumn("Buy", renderer, text=BUY_IT)
self.treeview.append_column(column)
renderer = Gtk.CellRendererText.new()
column = Gtk.TreeViewColumn("Count", renderer, text=QUANTITY)
treeview.append_column(column)
renderer = Gtk.CellRendererText.new()
column = Gtk.TreeViewColumn("Product", renderer, text=PRODUCT)
treeview.append_column(column)
def on_add_button_clicked(self, button, parent):
dialog = AddDialog(title="Add a Product", parent=parent,
flags=Gtk.DialogFlags.MODAL)
def get_treeview(self):
return self.treeview
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Grocery List")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 9-6Adding a New Product
检索行数据
检索存储在树模型行中的值与添加行非常相似。在清单中,9-6 model.get_iter_from_string()
首先用于检索指向树视图中第一行的树迭代器。这对应于第一类。
接下来,model.iter_next()
用于遍历所有根级别的行。对于每个根级别的行,运行以下代码。首先,用model.get()
检索产品名称。这个函数的工作方式类似于treestore.set()
,它接受一个Gtk.TreeModel
,一个指向一行的迭代器,以及一个或多个列号的列表。即使您提供单个列作为参数,此方法也总是返回一个元组。
(name,) = model.get(iter, PRODUCT)
if name.lower() == category.lower():
break
然后将当前产品与选择的类别名称进行比较。如果两个字符串匹配,则循环退出,因为找到了正确的类别。iter
变量现在指向所选择的类别。
添加新行
向树模型添加新行的方式与启动时最初添加的方式相同。在下面的代码中,指向所选类别的Gtk.TreeIter
首先被转换成一个树路径,因为当树存储被更改时,它就失效了。请注意,它不必转换为树行引用,因为它的位置可能不会改变。
path = model.get_path(iter)
child = model.append(iter)
model.set(child, BUY_IT, buy, QUANTITY, quantity, PRODUCT, product)
接下来,一个新行被附加上treestore.append()
,其中iter
是父行。使用用户在对话框中输入的数据,用treestore.set()
填充该行。
组合框
清单 9-6 引入了一个名为Gtk.ComboBox
的新部件。
Gtk.ComboBox
是一个允许用户从下拉列表中选择选项的小工具。
组合框以正常状态显示选定的选项。组合框有两种不同的使用方式,这取决于您使用什么方法来实例化小部件,要么使用自定义的Gtk.TreeModel
要么使用只有一列字符串的默认模型。
在清单 9-6 中,用Gtk.ComboBoxText.new()
创建了一个新的Gtk.ComboBox
,它创建了一个专门的组合框,只包含一列字符串。这只是一个方便的方法,因为组合框的下拉列表是用一个Gtk.TreeModel
在内部处理的。这使您可以通过以下方法轻松地附加和预置选项以及插入新选项。
combobox.append_text(text)
combobox.prepend_text(text)
combobox.insert_text(position, text)
第一个函数combobox.get_active_text()
返回一个引用当前行索引的整数,如果没有选择,则返回-1。这可以转换成一个字符串,然后转换成一个Gtk.TreePath
。另外,combobox.get_active_iter()
检索指向所选行的迭代器,如果设置了迭代器,则返回True
。
删除多行
下一步是添加从列表中删除产品的功能。由于我们添加了选择多行的功能,代码也必须能够删除多行。
清单 9-7 实现了两种方法。第一个方法remove_row()
是为每一个选中的行调用的,如果它不是一个类别,则删除该行。如果被删除的行将被购买,其数量将从类别的运行总数中删除。第二个函数remove_products()
是单击 Remove 按钮时运行的方法。
def remove_row(self, ref, model):
# Convert the tree row reference to a path and retrieve the iterator. path = ref.get_path()
iter = model.get_iter(path)
# Only remove the row if it is not a root row.
parent = model.iter_parent(iter)
if parent:
(buy, quantity) = model.get(iter, BUY_IT, QUANTITY)
(pnum,) = model.get(parent, QUANTITY)
if (buy):
pnum -= quantity
model.set(parent, QUANTITY, pnum)
iter = model.get_iter(path)
model.remove(iter)
def remove_products(self, button, treeview):
selection = treeview.get_selection()
model = treeview.get_model()
rows = selection.get_selected_rows(model)
# Create tree row references to all of the selected rows. references = []
for data in rows:
ref = Gtk.TreeRowReference.new(model, data)
references.append(ref)
for ref in references:
self.remove_row(ref, model)
Listing 9-7Removing One or More Products
当按下 Remove 按钮时,调用remove_products()
方法。这个函数首先调用selection.get_selected_rows()
来检索指向所选行的树路径的 Python 列表。因为应用正在改变行,所以路径列表被转换为行引用列表。这确保了所有的树路径保持有效。
将路径转换为树行引用后,通过 Python for
语句迭代列表,并为每个条目调用remove_row()
方法。在remove_row()
中,一个新的函数用于检查该行是否是一个类别。
如果选择的行是一个类别,我们知道它是一个根元素,没有父元素。因此,下面的model.iter_parent()
调用执行两个任务。首先,如果没有设置父迭代器,这个方法返回False
,类别行不被删除。如果该行有一个父行,这意味着它是一个产品,父迭代器将被设置并在函数中使用。
parent = model.iter_parent(iter)
其次,该函数检索关于所选产品及其父类别的信息。如果产品被设置为购买,其数量将从按类别显示的产品总数中减去。因为更改这些数据会使迭代器失效,所以路径被转换成迭代器,并且行被从树模型中删除。
处理双击
双击由Gtk.TreeView
的行激活信号处理。当用户双击一行时,当用户在不可编辑的行上按空格键、Shift+ 空格键、Return 或 Enter 时,或者当您调用treeview.row_activated()
时,都会发出信号。
def row_activated(self, treeview, path, column, data):
model = treeview.get_model()
if model.get_iter(path))
# Handle the selection ...
Listing 9-8Editing a Clicked Row
在清单 9-8 中,当用户激活树视图中的一行时,回调方法row_activated()
被调用。使用treemodel.get_iter()
从树路径对象中检索激活的行。从那里,您可以自由地使用您到目前为止所学的任何函数/方法来检索或更改行的内容。
可编辑文本呈现器
允许用户编辑树形视图的内容将非常有用。这可以通过显示一个包含Gtk.Entry
的对话框来实现,用户可以在其中编辑单元格的内容。然而,GTK+ 提供了一种更简单的方法来编辑文本组件,它通过使用Gtk.CellRendererText
的编辑信号集成到树单元中。
当用户单击选定行中标记为可编辑的单元格时,一个Gtk.Entry
将被放置在包含该单元格当前内容的单元格中。正在编辑的单元格示例如图 9-8 所示。
图 9-8
可编辑的单元格
在用户按下 Enter 键或者将焦点从文本条目上移开之后,编辑过的小部件就会发出。您需要连接到该信号,并在信号发出后应用更改。清单 9-9 向您展示了如何创建Gtk.ListStore
杂货清单应用,其中产品列是可编辑的。
def set_up_treeview(self, treeview):
renderer = Gtk.CellRenderer.Text.new()
column = Gtk.TreeViewColumn.new_with_attributes("Buy", renderer, "text", BUY_IT)
treeview.append_column(column)
renderer = Gtk.CellRendererText.new()
column = Gtk.TreeViewColumn.new_with_attributes("Count", renderer, "text", QUANTITY)
treeview.append_column(column)
# Set up the third column in the tree view to be editable. renderer = Gtk.CellRendererText.new() renderer.set_property("editable", True) renderer.connect("edited", self.cell_edited, treeview)
column = Gtk.TreeViewColumn.new_with_attributes("Product", renderer, "text", PRODUCT)
treeview.append_column(column)
def cell_edited(self, renderer, path, new_text, treeview):Tree View Widget
if len(new_text) > 0:
model = treeview.get_model()
iter = model.get_iter_from_string(path)
if iter:
model.set(iter, PRODUCT, new_text)
Listing 9-9Editing a Cell’s Text
创建可编辑的Gtk.CellRendererText
单元格是一个非常简单的过程。您需要做的第一件事是将文本呈现器的 editable 和 editable-set 属性设置为True
。
renderer.set_property("editable", True)
请记住,设置 editable 属性会将其应用于渲染器绘制的整列数据。如果要逐行指定单元格是否可编辑,应该将其作为列的属性添加。
接下来你需要做的是将单元格渲染器连接到由Gtk.CellRendererText
提供的编辑过的信号。这个信号的回调函数接收单元格渲染器、一个指向被编辑行的Gtk.TreePath
字符串和用户输入的新文本。当用户在编辑单元格时按下 Enter 键或将焦点从单元格的Gtk.Entry
移开时,会发出该信号。
编辑过的信号是必需的,因为更改不会自动应用到单元格。这允许您过滤掉无效的条目。例如,在清单 9-9 中,当新字符串为空时,不会应用新文本。
iter = model.get_iter_from_string(path)
if iter:
model.set(iter, PRODUCT, new_text)
一旦您准备好应用文本,您就可以用model.get_iter_from_string()
将Gtk.TreePath
字符串直接转换成Gtk.TreeIter
。如果迭代器设置成功,这个函数返回True
,这意味着路径字符串指向一个有效的行。
警告
您总是希望检查路径是否有效,即使它是由 GTK+ 提供的,因为自回调函数初始化以来,该行有可能已经被移除或移动。
在检索到Gtk.TreeIter
之后,您可以使用model.set()
将新的文本字符串应用到列中。在清单 9-9 中,new_text
被应用于Gtk.ListStore
的产品列。
单元格数据方法
如果需要在每个单元格呈现到屏幕上之前对其进行进一步定制,可以使用单元格数据方法。它们允许你修改每个细胞的属性。例如,您可以根据单元格的内容设置前景色,或者限制显示的浮点数的小数位数。它还可以用来设置在运行时计算的属性。
图 9-9 创建了一个颜色列表,显示了一个应用,它使用单元格数据函数根据Gtk.CellRendererText
的文本属性设置每个单元格的背景颜色。
图 9-9
清单 9-10 的截图
警告
如果您的树模型中有大量的行,请确保不要使用单元数据函数。单元格数据函数会在呈现列之前处理列中的每个单元格,因此它们会显著降低包含许多行的树模型的速度。
在清单 9-10 中,单元格数据函数用于将背景颜色设置为单元格存储的颜色字符串的值。每个单元格的前景色也被设置为白色,尽管这也可以用model.set()
应用于整个渲染器。这个应用显示了 256 种网页安全色的列表。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GObject
clr = ( "00", "33", "66", "99", "CC", "FF" )
COLOR = 0
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(250, 175)
treeview = Gtk.TreeView.new()
self.setup_tree_view(treeview)
store = Gtk.ListStore.new((GObject.TYPE_STRING,
GObject.TYPE_STRING, GObject.TYPE_STRING))
for var1 in clr:
for var2 in clr:
for var3 in clr:
color = "#" + var1 + var2 + var3
iter = store.append()
store.set(iter, (COLOR,), (color,))
treeview.set_model(store)
scrolled_win = Gtk.ScrolledWindow.new(None, None)
scrolled_win.set_policy(Gtk.PolicyType.AUTOMATIC,
Gtk.PolicyType.AUTOMATIC)
scrolled_win.add(treeview)
self.add(scrolled_win)
def setup_tree_view(self, treeview):
renderer = Gtk.CellRendererText.new()
column = Gtk.TreeViewColumn.new()
column.pack_start(renderer, True)
column.add_attribute(renderer, "text", COLOR)
column.set_title("Standard Colors")
treeview.append_column(column)
column.set_cell_data_func(renderer, self.cell_data_func, None)
def cell_data_func(self, column, renderer, model, iter, data):
# Get the color string stored by the column and make it the
# foreground color
(text,) = model.get(iter, COLOR)
renderer.props.foreground_rgba = Gdk.RGBA(red=1.0, green=1.0,
blue=1.0, alpha=1.0)
red = int(text[1:3], 16) / 255
green = int(text[3:5], 16) / 255 blue = int(text[5:7], 16) / 255
renderer.props.background_rgba = Gdk.RGBA(red=red, green=green,
blue=blue, alpha=1.0)
renderer.props.text = text
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp", **kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Color List")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 9-10Using Cell Data Functions
另一个有用的单元格数据函数的例子是当您使用浮点数时,您需要控制显示的小数位数。事实上,当您在本章的“微调按钮渲染器”一节中学习微调按钮单元格渲染器时,会用到该示例。
一旦设置了单元格数据函数,就需要通过调用column.set_cell_data_func()
将其连接到特定的列。此函数的最后两个参数允许您提供传递给单元格数据函数的数据,以及一个被调用来销毁数据的附加函数。如果不需要,您可以将这两个参数都设置为None
。
column.set_cell_data_func(renderer, self.cell_data_func, None)
如果您已经添加了一个单元格数据函数到您现在想要删除的列,您应该调用设置为None
的column.set_cell_data_func()
函数参数。
如前所述,只有在明确需要微调数据呈现时,才应该使用单元格数据函数。在大多数情况下,您希望使用附加的列属性或column.property_set()
来改变属性,这取决于设置的范围。根据经验,单元格数据函数应该仅用于应用不能用列属性处理的设置,或者不能为每个单元格设置。
单元渲染器
到目前为止,您只学习了一种类型的单元格渲染器,Gtk.CellRendererText
。这个渲染器允许您将字符串、数字和布尔值显示为文本。您可以使用单元格渲染器属性和单元格数据函数自定义文本的显示方式,并允许用户对其进行编辑。
GTK+ 提供了大量的单元格渲染器,可以显示除文本之外的其他类型的小部件。这些是切换按钮、图像、旋转按钮、组合框、进度条和加速器,它们都在本章中介绍。
切换按钮渲染器
用Gtk.CellRendererText
将布尔值显示为“真”或“假”有点俗气,而且每行都要占用大量宝贵的空间,尤其是当有很多可见的布尔列时。您可能会想,如果可以显示布尔值的复选按钮而不是文本字符串,那该多好。事实证明你可以——在一种叫做Gtk.CellRendererToggle
的单元格渲染器的帮助下。
默认情况下,切换按钮单元格渲染器被绘制为复选按钮,如图 9-10 所示。您还可以将切换按钮呈现器设置为单选按钮,但是您需要自己管理单选按钮的功能。
图 9-10
切换按钮渲染器
与可编辑文本呈现器一样,您必须手动应用用户执行的更改;否则,按钮不会在屏幕上直观地切换。因此,Gtk.CellRendererToggle
提供了切换信号,当用户按下检查按钮时会发出该信号。清单 9-11 展示了杂货清单应用的切换回调函数。在这个版本的应用中,BUY_IT 列用Gtk.CellRendererToggle
呈现。
def buy_it_toggled(renderer, path, treeview):
model = treeview.get_model()
iter = model.get_iter_from_string(path)
if iter:
(value,) = model.get(iter, BUY_IT)
model.set_row(iter, (!value, None))
Listing 9-11Using Cell Data Functions
使用Gtk.CellRendererToggle.new()
创建切换单元渲染器。创建切换单元格渲染器后,您希望将其可激活属性设置为True
,以便可以切换它;否则,用户将无法切换按钮(如果您只想显示设置,但不允许对其进行编辑,这可能很有用)。column.property_set()
可以用来将这个设置应用到每一个单元格。
接下来,active 属性应该作为列属性而不是文本添加,这是由Gtk.CellRendererText
使用的。该属性被设置为True
或False
,这取决于切换按钮的期望状态。
然后,您应该将Gtk.CellRendererToggle
单元格渲染器连接到切换信号的回调函数。清单 9-11 给出了切换信号的回调函数示例。这个回调函数接收单元格渲染器和一个指向包含切换按钮的行的Gtk.TreePath
字符串。
在回调函数中,您需要手动切换切换按钮显示的当前值,如下面两行代码所示。触发信号的发射只告诉你用户想要按钮被触发;它不会为您执行操作。
(value,) = model.get(iter, BUY_IT)
model.set_row(iter, (!value, None))
要切换值,您可以使用model.get()
来检索单元格存储的当前值。由于单元格存储的是布尔值,您可以将新值设置为与model.set_row()
中的当前值相反。
如前所述,Gtk.CellRendererToggle
还允许您将切换呈现为单选按钮。这可以通过用renderer.set_radio()
改变 radio 属性来初始设置为渲染器。
renderer.set_radio(radio)
你需要意识到将 radio 设置为True
唯一改变的是切换按钮的渲染!您必须通过切换回调函数手动实现单选按钮的功能。这包括激活新的切换按钮和去激活先前选择的切换按钮。
pixbuf 渲染器
以GdkPixbuf
对象的形式添加图像作为Gtk.TreeView
中的一列是Gtk.CellRendererPixbuf
提供的一个非常有用的特性。pixbuf 渲染器的一个例子如图 9-11 所示,其中每个项目的左边都有一个小图标。
图 9-11
pixbuf 渲染器
在前面的章节中,你已经学习了几乎所有将GdkPixbuf
图片添加到树视图的必要知识,但是清单 9-12 给出了一个简单的例子来指导你。在大多数情况下,不需要为 pixbufs 创建单独的列标题,因此清单 9-12 向您展示了如何在一列中包含多个渲染器。Pixbuf 单元渲染器在树视图实现类型中非常有用,例如文件系统浏览器。
def set_up_treeview(self, treeview):
column = Gtk.TreeViewColumn.new()
column.set_resizable(True)
column.set_title("Some Items")
renderer = Gtk.CellRendererPixbuf.new()
# it is important to pack the renderer BEFORE adding attributes!! column.pack_start(renderer, False) column.add_attribute(renderer, "pixbuf", ICON)
renderer = Gtk.CellRendererText.new()
# it is important to pack the renderer BEFORE adding attributes!! column.pack_start(renderer, True) column.add_attribute(renderer, "text", ICON_NAME) treeview.append_column(column)
Listing 9-12GdkPixbuf Cell Renderers
用Gtk.CellRendererPixbuf.new()
创建新的Gtk.CellRendererPixbuf
对象。然后,您希望将渲染器添加到列中。由于我们的列中有多个渲染器Gtk.CellRendererPixbuf.new()
,您可以使用column.pack_start()
将渲染器添加到列中。在添加属性之前,将渲染器打包到列中非常重要。如果不这样做,渲染器将失效,您将收到运行时警告,并且列中不会显示任何数据。
接下来,您需要为Gtk.CellRendererPixbuf
的列添加属性。在清单 9-12 中,使用了 pixbuf 属性,这样我们就可以从文件中加载一个自定义图标。然而,pixbufs 并不是Gtk.CellRendererPixbuf
支持的唯一图像类型。
如果您正在使用Gtk.TreeStore
,当行被展开和收缩时,显示不同的 pixbuf 是有用的。为此,可以为 pixbuf-expander-open 和 pixbuf-expander-closed 指定两个GdkPixbuf
对象。例如,您可能希望在行展开时显示一个打开的文件夹,在行收缩时显示一个关闭的文件夹。
当您创建树模型时,您需要使用一个名为GdkPixbuf.Pixbuf
的新类型,它在每个模型列中存储GdkPixbuf
对象。每当您向树模型列添加一个GdkPixbuf
时,它的引用计数就会增加 1。
微调按钮渲染器
在第五章中,你学习了如何使用Gtk.SpinButton
小部件。虽然Gtk.CellRendererText
可以显示数字,但更好的选择是使用Gtk.CellRendererSpin
。当要编辑内容时,不显示Gtk.Entry
,而是使用Gtk.SpinButton
。图 9-12 显示了一个用Gtk.CellRendererSpin
渲染的正在编辑的单元格的例子。
图 9-12
微调按钮渲染器
您会注意到图 9-12 中第一列的浮点数显示了多个小数位。您可以设置微调按钮中显示的小数位数,但不能设置显示的文本。要减少或消除小数位数,您应该使用单元格数据函数。清单 9-13 中显示了一个隐藏小数位的单元格数据函数的示例。
def cell_edited(self, renderer, path, new_text, treeview):
# Retrieve the current value stored by the spin button renderer's adjustme adjustment = renderer.get_property("adjustment")
value = "%.0f" % adjustment.get_value() model = treeview.get_model()
iter = model.get_iter_from_string(path) if iter:
model.set(iter, QUANTITY, value)
Listing 9-13Cell Data Function for Floating-Point Numbers
回想一下,如果您想使用Gtk.CellRendererText
或另一个派生的渲染器指定一列中浮点数显示的小数位数,您需要使用单元格数据函数。在清单 9-13 中,显示了一个样本单元格数据函数,它读入当前浮点数并强制渲染器不显示小数位。这是必要的,因为Gtk.CellRendererSpin
将数字存储为浮点数。
Gtk.CellRendererSpin
兼容整数和浮点数,因为它的参数存储在Gtk.Adjustment
中。清单 9-13 是杂货清单应用的一个实现,其中数量列用Gtk.CellRendererSpin
呈现。
def setup_tree_view(self, renderer, column, adj):
adj = Gtk.Adjustment.new(0.0, 0.0, 100.0, 1.0, 2.0, 2.0)
renderer = Gtk.CellRendererSpin(editable=True, adjustment=adj, digits=0)
column = Gtk.TreeViewColumn("Count", renderer, text=QUANTITY)
treeview.append_column(column)
renderer.connect("edited", self.cell_edited, treeview)
# Add a cell renderer for the PRODUCT column
Listing 9-14Spin Button Cell Renderers
用Gtk.CellRendererSpin()
创建新的Gtk.CellRendererSpin
对象。创建渲染器时,应设置对象的 editable、adjustment 和 digits 属性,如下所示。
Gtk.CellRendererSpin(editable=True, adjustment=adj, digits=0)
Gtk.CellRendererSpin
提供三种属性:调整、爬升率、位数。它们存储在一个Gtk.Adjustment
中,分别定义了微调按钮的属性、按下箭头按钮时的加速度以及微调按钮中显示的小数位数。默认情况下,爬升率和显示的小数位数都设置为零。
设置单元格渲染器后,您应该将编辑后的信号连接到单元格渲染器,该渲染器用于将用户选择的新值应用到单元格。通常不需要过滤该值,因为调整已经限制了单元格允许的值。回调函数在用户按下回车键或从正在编辑的单元格的旋转按钮上移开焦点后运行。
在清单 9-14 中的cell_edited()
回调方法中,您需要首先检索微调按钮渲染器的调整,因为它存储了将要显示的新值。然后,可以将这个新值应用于给定的单元格。
注意
虽然编辑过的Gtk.CellRendererText
信号仍接收到new_text
参数,但不应使用。参数不存储数值调节钮值的文本版本。此外,model.set()
中使用的替换当前值的值必须以浮点数的形式提供,因此不管其内容如何,字符串都是不可接受的。
您可以使用renderer.get_property("adjustment")
检索调整值,将其应用到适当的列。如果 QUANTITY 列用于显示浮点数(GObject.TYPE_FLOAT
),则可以使用当前状态的返回类型。我们选择将浮点值转换为字符串值。
当创建树模型时,列的类型必须是GObject.TYPE_FLOAT
,即使您想要存储一个整数。您应该使用单元格数据函数来限制每个单元格显示的小数位数。
组合框渲染器
Gtk.CellRendererCombo
为您刚刚了解的小部件Gtk.ComboBox
提供单元格渲染器。组合框单元格渲染器很有用,因为它们允许您向用户呈现多个预定义的选项。Gtk.CellRendererCombo
以类似于Gtk.CellRendererText
的方式呈现文本,但是在编辑时不显示Gtk.Entry
小部件,而是向用户呈现一个Gtk.ComboBox
小部件。正在编辑的Gtk.CellRendererCombo
单元格示例如图 9-13 所示。
图 9-13
组合框单元格渲染器
要使用Gtk.CellRendererCombo
,您需要为列中的每个单元格创建一个Gtk.TreeModel
。在清单 9-15 中,清单 9-1 中的杂货清单应用的数量列用Gtk.CellRendererCombo
呈现。
def setup_tree_view(self, treeview):
# Create a GtkListStore that will be used for the combo box
renderer. model = Gtk.ListStore.new((GObject.TYPE_STRING,
GObject.TYPE_STRING))
iter = model.append()
model.set(iter, 0, "None")
iter = model.append()
model.set(iter, 0, "One")
iter = model.append()
model.set(iter, 0, "Half a Dozen")
iter = model.append()
model.set(iter, 0, "Dozen")
iter = model.append()
model.set(iter, 0, "Two Dozen")
# Create the GtkCellRendererCombo and add the tree model. Then, add the
# renderer to a new column and add the column to the GtkTreeView.
renderer = Gtk.CellRendererCombo(text_column=0, editable=True,
has_entry=True, model=model)
column = Gtk.TreeViewColumn("Count", renderer, text=QUANTITY)
treeview.append_column(column)
renderer.connect("edited", self.cell_edited, treeview)
renderer = Gtk.CellRendererText.new()
column = Gtk.TreeViewColumn("Product", renderer, text=PRODUCT)
treeview.append_column(column)
def cell_edited(self, renderer, path, new_text, treeview):
# Make sure the text is not empty. If not, apply it to the tree view
cell. if new_text != "":
model = treeview.get_model()
iter = model.get_iter_from_string(path)
if iter:
model.set(iter, QUANTITY, new_text)
Listing 9-15Combo Box Cell Renderers
用Gtk.CellRendererCombo()
创建新的组合框单元渲染器。Gtk.CellRendererCombo
除了从Gtk.CellRendererText
继承的属性之外,还有三个属性:"has_entry"
、"model"
和"text_column"
。
renderer = Gtk.CellRendererCombo(text_column=0, editable=True,
has_entry=True, model=model)
您需要设置的第一个属性是"text_column"
,它指的是单元格渲染器中显示的组合框树模型中的列。这必须是Gtk.CellRendererText
支持的类型,如GObject.TYPE_STRING
、GObject.TYPE_INT
或GObject.TYPE_BOOLEAN
。model 属性是一个用作组合框内容的Gtk.TreeModel
。您还必须将 editable 属性设置为True
,以便可以编辑单元格内容。
最后,有一个名为Gtk.ComboBoxEntry
的小部件,它像普通的组合框一样为用户提供选择,但它也使用一个Gtk.Entry
小部件来允许用户输入自定义字符串,而不是选择现有选项。要允许组合框单元格渲染器的这一功能,必须将 has-entry 属性设置为True
。这在默认情况下是打开的,这意味着您必须关闭它,以将选择限制在那些出现在Gtk.CellRendererCombo
的树模型中的选项。
与从Gtk.CellRendererText
派生的其他单元渲染器一样,您希望使用文本字段作为列属性,并在创建树视图的模型时设置其初始文本。然后,您可以使用编辑过的信号将文本应用到树模型。在清单 9-15 中,只有当“new_text”字符串不为空时才会应用更改,因为用户也可以自由输入自由格式的文本。
进度条呈现器
另一种单元格渲染器是Gtk.CellRendererProgress
,它实现了Gtk.ProgressBar
小部件。虽然进度条支持脉冲,Gtk.CellRendererProgress
只允许你设置进度条的当前值。图 9-14 显示了一个Gtk.TreeView
小部件,它在第二列有一个进度条单元格渲染器,显示文本反馈。
图 9-14
进度条单元渲染器
进度条单元格渲染器是另一个在程序中实现的简单功能。您可以使用Gtk.CellRendererProgress()
创建新的Gtk.CellRendererProgress
对象。Gtk.CellRendererProgress
提供了两个属性:"text"
和"value"
。进度条状态由"value"
属性定义,该属性是一个 0 到 100 之间的整数。值 0 表示一个空的进度条,100 表示一个完整的进度条。由于它被存储为一个整数,对应于进度条值的树模型列应该具有类型GObject.TYPE_INT
。
Gtk.CellRendererProgress
提供的第二个属性是文本。这个属性是一个绘制在进度条顶部的字符串。在某些情况下,可以忽略该属性,但是向用户提供有关进程进度的更多信息通常是个好主意。可能的进度条字符串的例子有“67%完成”、“80 个文件中的 3 个已处理”、“正在安装 foo”。。.",等等。
在某些情况下,Gtk.CellRendererProgress
是一个有用的单元格渲染器,但是在部署时应该小心。您应该避免在一行中使用多个进度条,因为这样做可能会让用户感到困惑,并且会占用大量的水平空间。此外,包含许多行的树视图显得杂乱无章。在许多情况下,用户最好使用文本单元格渲染器,而不是进度条单元格渲染器。
但是,在某些情况下,Gtk.CellRendererProgress
是一个很好的选择。例如,如果您的应用必须同时管理多个下载,进度条单元格渲染器是一种简单的方法,可以为每个下载的进度提供一致的反馈。
键盘快捷键渲染器
GTK+ 2.10 引入了一种叫做Gtk.CellRendererAccel
的新型单元格渲染器,它显示键盘快捷键的文本表示。图 9-15 显示了一个加速器单元渲染器的例子。
图 9-15
加速器单元渲染器
清单 9-16 创建了一个动作列表以及它们的键盘快捷键。这种类型的树视图可用于允许用户编辑应用的加速器。加速器显示为文本,因为渲染器是从Gtk.CellRendererText
派生的。
要编辑加速器,用户需要单击一次单元格。然后单元格显示一个字符串,要求输入一个密钥。新的按键代码将与任何掩码键(如 Ctrl 和 Shift)一起添加到单元格中。基本上,按下的第一个键盘快捷键由单元格显示。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GObject
ACTION = 0
MASK = 1
VALUE = 2
list = [( "Cut", Gdk.ModifierType.CONTROL_MASK, Gdk.KEY_X ), ( "Copy", Gdk.ModifierType.CONTROL_MASK, Gdk.KEY_C ), ( "Paste", Gdk.ModifierType.CONTROL_MASK, Gdk.KEY_V ), ( "New", Gdk.ModifierType.CONTROL_MASK, Gdk.KEY_N ), ( "Open", Gdk.ModifierType.CONTROL_MASK, Gdk.KEY_O ), ( "Print", Gdk.ModifierType.CONTROL_MASK, Gdk.KEY_P )]
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_size_request(250, 250)
treeview = Gtk.TreeView.new()
self.setup_tree_view(treeview)
store = Gtk.ListStore(GObject.TYPE_STRING,
GObject.TYPE_INT, GObject.TYPE_UINT)
for row in list:
(action, mask, value) = row
iter = store.append(None)
store.set(iter, ACTION, action, MASK, mask, VALUE, value)
treeview.set_model(store)
scrolled_win = Gtk.ScrolledWindow.new(None, None)
scrolled_win.set_policy(Gtk.PolicyType.AUTOMATIC,
Gtk.PolicyType.AUTOMATIC)
scrolled_win.add(treeview)
self.add(scrolled_win)
def setup_tree_view(self, treeview):
renderer = Gtk.CellRendererAccel()
column = Gtk.TreeViewColumn("Action", renderer, text=ACTION)
treeview.append_column(column)
renderer = Gtk.CellRendererAccel(accel_mode=Gtk.CellRendererAccelMode.GTK, editable=True)
column = Gtk.TreeViewColumn("Key", renderer, accel_mods=MASK, accel_key=VALUE)
treeview.append_column(column)
renderer.connect("accel_edited", self.accel_edited, treeview)
def accel_edited(self, renderer, path, accel_key, mask, hardware_keycode, treeview):
model = treeview.get_model()
iter = model.get_iter_from_string(path)
if iter:
model.set(iter, MASK, mask, VALUE, accel_key)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Accelerator Keys")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 9-16Combo Box Cell Renderers
您可以使用Gtk.CellRendererAccel()
创建新的Gtk.CellRendererAccel
对象。Gtk.CellRendererAccel
提供了以下四个可以通过renderer.get()
访问的属性。
-
Gdk.ModifierType.SHIFT_MASK
:Shift 键。 -
Gdk.ModifierType.CONTROL_MASK
:Ctrl 键。 -
Gdk.ModifierType.MOD_MASK
、Gdk.ModifierType.MOD2_MASK
、Gdk.ModifierType.MOD3_MASK
、Gdk.ModifierType.MOD4_MASK
、Gdk.ModifierType.MOD5_MASK
:第一个修饰符通常代表 Alt 键,但是这些是基于你的 X 服务器对这些键的映射来解释的。它们也可以对应于 Meta、Super 或 Hyper 键。 -
在 2.10 中引入,这允许你显式地声明超级修饰符。此修饰符可能不是在所有系统上都可用!
-
在 2.10 中引入,这允许你显式地声明超级修饰符。此修饰符可能不是在所有系统上都可用!
-
Gdk.ModifierType.META_MODIFIER
:在 2.10 中引入,这允许你显式地声明元修饰符。此修饰符可能不是在所有系统上都可用!
在大多数情况下,您希望使用Gtk.CellRendererAccel
将修改器遮罩(acel-mods)和快捷键键值(accel-key)设置为树视图列的两个属性。在这种情况下,修改器遮罩的类型为GObject.TYPE_INT
,加速器键值为GObject.TYPE_UINT
。因此,在设置修饰符掩码列的内容时,您需要确保将Gdk.ModifierType
的值转换为 int。
store = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT, GObject.TYPE_UINT)
Gtk.CellRendererAccel
提供两种信号。第一个是accel-cleared
,允许您在用户删除当前值时重置加速器。在大多数情况下,您不需要这样做,除非您有一个默认值,您希望加速器恢复到这个值。
更重要的是,accel-edited
允许您应用用户对键盘快捷键所做的更改,只要您将 editable 属性设置为True
。回调函数接收一个指向相关行的路径字符串,以及快捷键代码、掩码和硬件键码。在回调函数中,您可以使用store.set()
应用更改,就像您对任何其他可编辑类型的单元格所做的那样。
测试你的理解能力
在练习 1 中,您有机会练习使用Gtk.TreeView
小部件以及多种类型的单元渲染器。这对您来说是一个非常重要的尝试,因为您需要在许多应用中使用Gtk.TreeView
小部件。和往常一样,当你完成后,你可以在附录 d 中找到一个可能的解决方案。
练习 1:文件浏览器
到目前为止,您可能已经受够了杂货清单应用,所以让我们尝试一些不同的东西。在本练习中,使用Gtk.TreeView
小部件创建一个文件浏览器。您应该将Gtk.ListStore
用于文件浏览器,并允许用户浏览文件系统。
文件浏览器应该显示图像来区分目录和文件。图片可在 www.gtkbook.com
的可下载源代码中找到。您还可以使用 Python 目录工具函数来检索目录内容。双击一个目录应该会把你移动到那个位置。
摘要
在本章中,您学习了如何使用Gtk.TreeView
小部件。这个小部件允许你分别用Gtk.ListStore
和Gtk.TreeStore
显示数据的列表和树形结构。您还了解了树视图、树模型、列和单元渲染器之间的关系,以及如何使用每个对象。
接下来,您了解了可用于引用树视图中行的对象类型。这些包括树迭代器、路径和行引用。这些对象中的每一个都有自己的优点和缺点。树迭代器可以直接用于模型,但是当树模型改变时,它们就失效了。树路径很容易理解,因为它们有相关的人类可读的字符串,但是如果树模型被改变,它们可能不指向同一行。最后,树行引用是有用的,因为只要行存在,它们就保持有效,即使模型发生了变化。
接下来,您学习了如何处理一行或多行的选择。对于多行选择,您可以使用一个for-each
函数,或者您可以获得所选行的 Python 列表。处理选择时一个有用的信号是Gtk.TreeView
的行激活信号,它允许你处理双击。
之后,您学习了如何使用Gtk.CellRendererText
的已编辑信号创建可编辑单元格,该信号显示一个Gtk.Entry
以允许用户编辑单元格中的内容。单元格数据函数也可以连接到列。这些单元格数据函数允许您在将每个单元格呈现到屏幕之前对其进行定制。
最后,您了解了许多单元格渲染器,这些渲染器允许您显示切换按钮、像素缓冲区、微调按钮、组合框、进度条和键盘快捷键字符串。还向您介绍了Gtk.ComboBox
小部件。
恭喜你!您现在已经熟悉了 GTK+ 提供的最难也是最通用的小部件之一。在下一章中,您将学习如何创建菜单、工具栏和弹出菜单。您还将学习如何使用用户界面(UI)文件自动创建菜单。
十、菜单和工具栏
本章教你如何创建弹出菜单、菜单栏和工具栏。您从手动创建每一个开始,这样您就了解了小部件是如何构造的。这让你对菜单和工具栏所依赖的所有概念有一个牢固的理解。
在理解了每个小部件之后,会向您介绍Gtk.Builder
,它允许您通过定制的 XML 文件动态创建菜单和工具栏。加载每个用户界面文件,并将每个元素应用于相应的 action 对象,该对象告诉项目如何显示以及如何操作。
在本章中,您将学习以下内容。
-
如何创建弹出菜单、菜单栏和工具栏
-
如何将键盘快捷键应用于菜单项
-
什么是
Gtk.StatusBar
小部件,以及如何使用它向用户提供关于菜单项的更多信息 -
GTK+ 提供了哪些类型的菜单项和工具栏项
-
如何用 UI 文件动态创建菜单和工具栏
-
如何使用
Gtk.IconFactory
创建自定义库存项目
弹出式菜单
本章从学习如何创建弹出式菜单开始。弹出菜单是一个Gtk.Menu
窗口小部件,当鼠标右键悬停在某些窗口小部件上时显示给用户。一些小工具,比如Gtk.Entry
和Gtk.TextView
,已经默认内置了弹出菜单。
如果你想改变一个默认提供弹出菜单的小部件的弹出菜单,你应该在弹出回调函数中编辑提供的Gtk.Menu
小部件。例如,Gtk.Entry
和Gtk.TextView
都有一个 populate-popup 信号,它接收将要显示的Gtk.Menu
。在向用户显示之前,您可以以任何您认为合适的方式编辑该菜单。
创建弹出菜单
对于大多数小部件,您需要创建自己的弹出菜单。在本节中,您将学习如何为一个Gtk.ProgressBar
小部件提供一个弹出菜单。我们要实现的弹出菜单如图 10-1 所示。
图 10-1
包含三个菜单项的简单弹出菜单
三个弹出菜单项使进度条跳动,将其设置为 100%完成,然后清除它。在清单 10-1 中,一个事件框包含了进度条。因为Gtk.ProgressBar
和Gtk.Label
一样,不能自己检测 GDK 事件,我们需要使用事件盒来捕捉button-press-event
信号。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(250, -1)
# Create all of the necessary widgets and initialize the pop-up menu. menu = Gtk.Menu.new()
eventbox = Gtk.EventBox.new()
progress = Gtk.ProgressBar.new() progress.set_text("Nothing Yet Happened")
progress.set_show_text(True) self.create_popup_menu(menu, progress)
progress.set_pulse_step(0.05) eventbox.set_above_child(False)
eventbox.connect("button_press_event", self.button_press_event, menu) eventbox.add(progress)
self.add(eventbox)
eventbox.set_events(Gdk.EventMask.BUTTON_PRESS_MASK)
eventbox.realize()
def create_popup_menu(self, menu, progress):
pulse = Gtk.MenuItem.new_with_label("Pulse Progress")
fill = Gtk.MenuItem.new_with_label("Set as Complete")
clear = Gtk.MenuItem.new_with_label("Clear Progress")
separator = Gtk.SeparatorMenuItem.new()
pulse.connect("activate", self.pulse_activated, progress)
fill.connect("activate", self.fill_activated, progress)
clear.connect("activate", self.clear_activated, progress)
menu.append(pulse)
menu.append(separator)
menu.append(fill)
menu.append(clear)
menu.attach_to_widget(progress, None)
menu.show_all()
def button_press_event(self, eventbox, event, menu):
pass
def pulse_activated(self, item, progress):
pass
def fill_activated(self, item, progress):
pass
def clear_activated(self, item, progress):
pass
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Pop-up Menus")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 10-1Simple Pop-up Menu
在大多数情况下,您希望使用button-press-event
来检测用户希望弹出菜单何时显示。这允许您检查是否单击了鼠标右键。如果点击了鼠标右键,Gdk.EventButton
的按钮成员等于 3。
不过,Gtk.Widget
也提供了popup-menu
信号,当用户按下内置的按键加速器激活弹出菜单时,该信号被激活。大多数用户使用鼠标来激活弹出菜单,所以这在 GTK+ 应用中通常不是一个因素。然而,如果您也想处理这个信号,您应该创建第三个函数来显示由两个回调函数调用的弹出菜单。
用Gtk.Menu.new()
创建新菜单。菜单初始化时没有初始内容,所以下一步是创建菜单项。
在本节中,我们将介绍两种类型的菜单项。第一个是所有其他类型菜单项的基类,Gtk.MenuItem
。为Gtk.MenuItem
提供了三种初始化功能:Gtk.MenuItem.new()
、Gtk.MenuItem.new_with_label()
和Gtk.MenuItem.new_with_mnemonic()
。
pulse = Gtk.MenuItem.new_with_label("Pulse Progress")
大多数情况下,你不需要使用Gtk.MenuItem.new()
,因为一个没有内容的菜单项没有多大用处。如果您使用该函数来初始化菜单项,您必须用代码构造菜单的每个方面,而不是让 GTK+ 来处理细节。
注意
菜单项助记符和键盘快捷键不是一回事。当菜单具有焦点时,当用户按下 Alt 和适当的字母数字键时,助记符激活菜单项。键盘快捷键是一种自定义的组合键,当按下该组合键时会运行回调函数。在下一节中,您将了解菜单的键盘快捷键。
另一种类型的基本菜单项是Gtk.SeparatorMenuItem
,它在其位置放置一个通用分隔符。您可以使用Gtk.SeparatorMenuItem.new()
创建一个新的分隔符菜单项。
在设计菜单结构时,分隔符非常重要,因为它们将菜单项组织成组,这样用户就可以很容易地找到合适的菜单项。例如,在“文件”菜单中,菜单项通常被组织成打开文件、保存文件、打印文件和关闭应用的组。很少会出现许多菜单项之间没有分隔符的情况(例如,最近显示的文件列表可能没有分隔符)。在大多数情况下,您应该将相似的菜单项组合在一起,并在相邻的组之间放置一个分隔符。
在创建菜单项之后,您需要将每个菜单项连接到激活信号,该信号在用户选择该项时发出。或者,您可以使用 activate-item 信号,该信号也会在显示给定菜单项的子菜单时发出。除非菜单项扩展为子菜单,否则这两者之间没有明显的区别。
每个 activate 和 activate-item 回调函数都接收启动动作的Gtk.MenuItem
小部件和您需要传递给该函数的任何数据。在清单 10-2 中,提供了三个菜单项回调函数来脉动进度条,填充到 100%完成,并清除所有进度。
现在您已经创建了所有的菜单项,您需要将它们添加到菜单中。Gtk.Menu
派生自Gtk.MenuShell
,是一个抽象基类,包含并显示子菜单和菜单项。菜单项可以通过menu.append()
添加到菜单外壳中。该函数将每个项目附加到菜单外壳的末尾。
menu.append(pulse)
此外,您可以使用menu.prepend()
或menu.insert()
分别在菜单的开头添加菜单项或将其插入任意位置。menu.insert()
接受的位置从零开始。
在将Gtk.Menu
的所有子控件设置为可见后,您应该调用menu.attach_to_widget()
以便弹出菜单与特定的小部件相关联。该函数接受弹出菜单和它所附加的小部件。
menu.attach_to_widget(progress, None)
menu.attach_to_widget()
的最后一个参数接受一个Gtk.MenuDetachFunc
,它可以在菜单脱离小部件时调用一个特定的函数。
弹出菜单回调方法
在创建了必要的小部件之后,您需要处理button-press-event
信号,如清单 10-2 所示。在本例中,每当鼠标右键单击进度条时,都会显示弹出菜单。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(250, -1)
# Create all of the necessary widgets and initialize the pop-up
menu. menu = Gtk.Menu.new()
eventbox = Gtk.EventBox.new() progress =
Gtk.ProgressBar.new()
progress.set_text("Nothing Yet Happened")
progress.set_show_text(True) self.create_popup_menu(menu, progress)
progress.set_pulse_step(0.05) eventbox.set_above_child(False)
eventbox.connect("button_press_event", self.button_press_event, menu) eventbox.add(progress)
self.add(eventbox)
eventbox.set_events(Gdk.EventMask.BUTTON_PRESS_MASK) eventbox.realize()
def create_popup_menu(self, menu, progress):
pulse = Gtk.MenuItem.new_with_label("Pulse Progress")
fill = Gtk.MenuItem.new_with_label("Set as Complete")
clear = Gtk.MenuItem.new_with_label("Clear Progress")
separator = Gtk.SeparatorMenuItem.new()
pulse.connect("activate", self.pulse_activated, progress)
fill.connect("activate", self.fill_activated, progress)
clear.connect("activate", self.clear_activated, progress)
menu.append(pulse)
menu.append(separator)
menu.append(fill)
menu.append(clear)
menu.attach_to_widget(progress, None)
menu.show_all()
def button_press_event(self, eventbox, event, menu):
if event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: menu.popup(None, None, None, None, event.button, event.time) return True
return False
def pulse_activated(self, item, progress):
progress.pulse()
progress.set_text("Pulse!")
def fill_activated(self, item, progress):
progress.set_fraction(1.0)
progress.set_text("One Hundred Percent")
def clear_activated(self, item, progress):
progress.set_fraction(0.0)
progress.set_text("Reset to Zero")
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Pop-up Menus")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 10-2Callback Functions for the Simple Pop-up Menu
在清单 10-2 中的button-press-event
回调函数中,可以使用menu.popup()
在屏幕上显示菜单。
menu.popup(parent_menu_shell, parent_menu_item, func, func_data, button, event_time)
在清单 10-2 中,除了被点击导致事件的鼠标按钮(事件➤按钮)和事件发生的时间(event.time)之外,所有参数都被设置为None
。如果弹出菜单是由按钮之外的其他东西激活的,您应该为 button 参数提供 0。
注意
如果该操作是由弹出菜单信号调用的,则事件时间将不可用。那样的话,可以用Gtk.get_current_event_time()
。该函数返回当前事件的时间戳,如果没有最近的事件,则返回Gdk.CURRENT_TIME
。
通常,parent_menu_shell
、parent_menu_item
、func
和func_data
被设置为None
,因为它们在菜单是菜单栏结构的一部分时使用。parent_menu_shell
小部件是包含导致弹出初始化的项目的菜单外壳。或者,您可以提供parent_menu_item
,它是导致弹出初始化的菜单项。
Gtk.MenuPositionFunc
是一个决定在屏幕上的什么位置绘制菜单的功能。它接受func_data
作为可选的最后一个参数。这些参数在应用中不经常使用,因此可以安全地设置为None
。在我们的例子中,弹出菜单已经与进度条相关联,所以它被绘制在正确的位置。
键盘快捷键
创建菜单时,最重要的事情之一就是设置键盘快捷键。键盘快捷键是由一个快捷键和一个或多个修饰键组成的组合键,如 Ctrl 或 Shift 。当用户按下组合键时,会发出相应的信号。
清单 10-3 是进度条弹出菜单应用的扩展,它为菜单项添加了键盘快捷键。当用户按 Ctrl+P 时,进度条是脉冲式的,用 Ctrl+F 填充,用 Ctrl+C 清除。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(250, -1)
# Create all of the necessary widgets and initialize the pop-up menu. menu = Gtk.Menu.new()
eventbox = Gtk.EventBox.new() progress = Gtk.ProgressBar.new() progress.set_text("Nothing Yet Happened") progress.set_show_text(True) self.create_popup_menu(menu, progress) progress.set_pulse_step(0.05) eventbox.set_above_child(False)
eventbox.connect("button_press_event", self.button_press_event, menu) eventbox.add(progress)
self.add(eventbox)
eventbox.set_events(Gdk.EventMask.BUTTON_PRESS_MASK)
eventbox.realize()
def create_popup_menu(self, menu, progress):
group = Gtk.AccelGroup.new()
self.add_accel_group(group)
menu.set_accel_group(group)
pulse = Gtk.MenuItem.new_with_label("Pulse Progress")
fill = Gtk.MenuItem.new_with_label("Set as Complete")
clear = Gtk.MenuItem.new_with_label("Clear Progress")
separator = Gtk.SeparatorMenuItem.new()
# Add the necessary keyboard accelerators.
pulse.add_accelerator("activate", group, Gdk.KEY_P, Gdk.ModifierType.CONTROL, Gtk.AccelFlags.VISIBLE)
fill.add_accelerator("activate", group, Gdk.KEY_F, Gdk.ModifierType.CONTROL, Gtk.AccelFlags.VISIBLE)
clear.add_accelerator("activate", group, Gdk.KEY_C, Gdk.ModifierType.CONTROL, Gtk.AccelFlags.VISIBLE)
pulse.connect("activate", self.pulse_activated, progress)
fill.connect("activate", self.fill_activated, progress)
clear.connect("activate", self.clear_activated, progress)
menu.append(pulse)
menu.append(separator)
menu.append(fill)
menu.append(clear)
menu.attach_to_widget(progress, None)
menu.show_all()
def button_press_event(self, eventbox, event, menu):
if event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: menu.popup(None, None, None, None, event.button, event.time) return True
return False
def pulse_activated(self, item, progress):
progress.pulse()
progress.set_text("Pulse!")
def fill_activated(self, item, progress):
progress.set_fraction(1.0)
progress.set_text("One Hundred Percent")
def clear_activated(self, item, progress):
progress.set_fraction(0.0)
progress.set_text("Reset to Zero")
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Pop-up Menus")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 10-3Adding Accelerators to Menu Items
键盘快捷键存储为Gtk.AccelGroup
的一个实例。要在您的应用中实现加速器,您需要用Gtk.AccelGroup.new()
创建一个新的加速器组。该加速器组必须添加到菜单出现的Gtk.Window
中才能生效。它还必须与任何利用其加速器的菜单相关联。在清单 10-3 中,这是在用self.add_accel_group()
和menu.set_accel_group()
创建Gtk.AccelGroup
之后立即执行的。
可以用Gtk.AccelMap
手动创建键盘快捷键,但是在大多数情况下,widget.add_accelerator()
提供了所有必要的功能。这种方法存在的唯一问题是,用户不能在运行时更改用该函数创建的键盘快捷键。
widget.add_accelerator(signal_name, group, accel_key, mods, flags)
要向小部件添加加速器,可以使用widget.add_accelerator()
,当用户按下组合键时,它会在小部件上发出由 signal_name 指定的信号。您需要为该功能指定您的快捷键组,如前所述,它必须与窗口和菜单相关联。
一个加速键和一个或多个修饰键形成完整的组合键。PyGObject API 参考中提供了可用快捷键的列表。所有可用关键字的定义都可以包含在 import GDK 语句中。
修饰符由Gdk.ModifierType
枚举指定。最常用的修饰键是Gdk.ModifierType.SHIFT_MASK
、Gdk.ModifierType.CONTROL_MASK
和Gdk.ModifierType.MOD1_MASK
,分别对应 Shift、Ctrl 和 Alt 键。
小费
当处理键码时,您需要小心,因为在某些情况下,您可能需要为同一个动作提供多个键。例如,如果你想抓住数字 1 键,你需要注意Gdk.KEY_1
和Gdk.KEY_KP_1
——它们对应于键盘顶部的 1 键和数字小键盘上的 1 键。
widget.add_accelerator()
的最后一个参数是加速器标志。由Gtk.AccelFlags
枚举定义了三个标志。如果设置了Gtk.AccelFlags.VISIBLE
,则在标签中可以看到加速器。Gtk.AccelFlags.LOCKED
防止用户修改加速器。Gtk.AccelFlags.MASK
为小工具加速器设置两个标志。
状态栏提示
通常放置在主窗口的底部,Gtk.Statusbar
窗口小部件可以给用户关于应用正在运行的更多信息。状态栏对于菜单也非常有用,因为您可以向用户提供更多关于鼠标光标所悬停的菜单项功能的信息。状态栏截图如图 10-2 所示。
图 10-2
带有状态栏提示的弹出式菜单
状态栏小部件
虽然状态栏一次只能显示一条消息,但是小部件实际上存储了一堆消息。当前显示的消息在栈顶。从堆栈中弹出消息时,会显示上一条消息。如果从顶部弹出一条消息后,堆栈上没有剩余的字符串,则状态栏上不会显示任何消息。
用Gtk.Ststusbar.new()
创建新的状态栏小部件。这将创建一个新的带有空消息堆栈的Gtk.Statusbar
小部件。在能够在新状态栏的堆栈中添加或删除消息之前,必须用statusbar.get_context_id()
检索上下文标识符。
id = statusbar.get_context_id(description)
上下文标识符是与上下文描述字符串相关联的唯一无符号整数。该标识符用于特定类型的所有消息,这允许您对堆栈上的消息进行分类。
例如,如果您的状态栏包含超链接和 IP 地址,您可以从字符串“URL”和“IP”创建两个上下文标识符。当您将消息推入堆栈或从堆栈弹出消息时,您必须指定一个上下文标识符。这允许应用的不同部分在状态栏消息堆栈中推送和弹出消息,而不会相互影响。
小费
对不同类别的消息使用不同的上下文标识符是很重要的。如果应用的一部分试图给用户一个消息,而另一部分试图删除自己的消息,你不希望错误的消息从堆栈中弹出!
在您生成上下文标识符之后,您可以使用statusbar.push()
将一条消息添加到状态栏的栈顶。该函数返回刚刚添加的字符串的唯一消息标识符。这个标识符可以在以后用于从堆栈中移除消息,而不管它在什么位置。
statusbar.push(context_id, message)
有两种方法可以从堆栈中删除消息。如果您想从特定上下文 ID 的堆栈顶部删除一条消息,您可以使用statusbar.pop()
。该函数删除状态栏堆栈中上下文标识符为context_id
的最高消息。
statusbar.pop(context_id)
也可以用statusbar.remove()
从状态栏的消息堆栈中删除特定的消息。为此,您必须提供消息的上下文标识符和想要删除的消息的消息标识符,这是在添加消息时由statusbar.push()
返回的。
statusbar.remove(context_id, message_id)
菜单项信息
状态栏的一个有用的作用是给用户更多关于鼠标光标当前悬停的菜单项的信息。图 10-2 显示了这样一个例子,这是清单 10-4 中的进度条弹出菜单应用的截图。
要实现状态栏提示,你应该将你的每个菜单项连接到Gtk.Widget
的"enter-notify-event"
和"leave-notify-event"
信号。清单 10-4 显示了你已经知道的进度条弹出菜单应用,除了当鼠标光标移动到一个菜单项上时状态栏提示被提供。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
class AppMenuItem(Gtk.MenuItem):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __setattr__(self, name, value):
self.__dict__[name] = value
def __getattr__(self, name):
return self.__dict__[name]
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(250, -1)
# Create all of the necessary widgets and initialize the pop-up menu. menu = Gtk.Menu.new()
eventbox = Gtk.EventBox.new() progress = Gtk.ProgressBar.new()
progress.set_text("Nothing Yet Happened")
progress.set_show_text(True)
statusbar = Gtk.Statusbar.new()
self.create_popup_menu(menu, progress, statusbar)
progress.set_pulse_step(0.05)
eventbox.set_above_child(False)
eventbox.connect("button_press_event", self.button_press_event, menu)
eventbox.add(progress)
vbox = Gtk.Box.new(orientation=Gtk.Orientation.VERTICAL, spacing=0)
vbox.pack_start(eventbox, False, True, 0)
vbox.pack_start(statusbar, False, True, 0)
self.add(vbox)
eventbox.set_events(Gdk.EventMask.BUTTON_PRESS_MASK)
eventbox.realize()
def create_popup_menu(self, menu, progress, statusbar):
pulse = AppMenuItem(label="Pulse Progress")
fill = AppMenuItem(label="Set as Complete")
clear = AppMenuItem(label="Clear Progress")
separator = Gtk.SeparatorMenuItem.new()
pulse.connect("activate", self.pulse_activated, progress)
fill.connect("activate", self.fill_activated, progress)
clear.connect("activate", self.clear_activated, progress)
Connect signals to each menu item for status bar messages. pulse.connect("enter-notify-event", self.statusbar_hint, statusbar) pulse.connect("leave-notify-event", self.statusbar_hint, statusbar) fill.connect("enter-notify-event", self.statusbar_hint, statusbar) fill.connect("leave-notify-event", self.statusbar_hint, statusbar) clear.connect("enter-notify-event", self.statusbar_hint, statusbar) clear.connect("leave-notify-event", self.statusbar_hint, statusbar) pulse.__setattr__("menuhint", "Pulse the progress bar one step.") fill.__setattr__("menuhint", "Set the progress bar to 100%.") clear.__setattr__("menuhint", "Clear the progress bar to 0%.") menu.append(pulse)
menu.append(separator)
menu.append(fill)
menu.append(clear)
menu.attach_to_widget(progress, None) menu.show_all()
def button_press_event(self, eventbox, event, menu):
if event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: menu.popup(None, None, None, None, event.button, event.time) return True
return False
def pulse_activated(self, item, progress):
progress.pulse()
progress.set_text("Pulse!")
def fill_activated(self, item, progress):
progress.set_fraction(1.0)
progress.set_text("One Hundred Percent")
def clear_activated(self, item, progress): progress.set_fraction(0.0) progress.set_text("Reset to Zero")
def statusbar_hint(self, menuitem, event, statusbar): id = statusbar.get_context_id("MenuItemHints")
if event.type == Gdk.EventType.ENTER_NOTIFY:
hint = menuitem.__getattr__("menuhint")
id = statusbar.push(id, hint)
elif event.type == Gdk.EventType.LEAVE_NOTIFY:
statusbar.pop(id)
return False
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Pop-up Menus")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 10-4Displaying More Information About a Menu Item
当实现状态栏提示时,你首先需要弄清楚什么信号是必要的。我们希望能够在鼠标光标移动到菜单项上时向状态栏添加一条消息,并在鼠标光标离开时删除它。从这个描述来看,使用“进入-通知-事件”和“离开-通知-事件”是一个很好的解决方案。
由于 Python 3 的 GTK+ 3 接口没有在 GTK+ 对象上实现get_data()
和set_data()
方法,我们需要子类化Gtk.MenuItem
类来实现相应的 Python 3 属性。这种方法也被用在本书的其他例子中。
使用这两个信号的一个优点是我们只需要一个回调函数,因为每个函数的原型都接收一个Gdk.EventProximity
对象。从这个物体中,我们可以分辨出Gdk.EventType.ENTER_NOTIFY
和Gdk.EventType.LEAVE_NOTIFY
事件。你想从回调函数中返回False
,因为你不想阻止 GTK+ 处理事件;您只想增强它发出时所执行的内容。
在statusbar_hint()
回调方法中,您应该首先检索菜单项消息的上下文标识符。您可以使用任何您想要的字符串,只要您的应用记得使用了什么。清单 10-4 描述了所有添加到状态栏的菜单项消息。如果应用的其他部分使用状态栏,使用不同的上下文标识符将不会影响菜单项提示。
id = statusbar.get_context_id("MenuItemHints")
如果事件类型为Gdk.EventType.ENTER_NOTIFY
,则需要向用户显示消息。在create_popup_menu()
方法中,一个数据参数被添加到每个名为"menuhint"
的菜单项中。这是对菜单项功能的更深入的描述,显示给用户。
hint = menuitem.__getattr__("menuhint")
statusbar.push(id, hint)
然后,使用statusbar.push()
,可以将消息添加到状态栏的"MenuItemHints"
上下文标识符下。该消息被放在堆栈的顶部并显示给用户。您可能想考虑在调用这个方法之后处理所有的 GTK+ 事件,因为用户界面应该立即反映这些变化。
但是,如果事件类型是Gdk.EventType.LEAVE_NOTIFY
,您需要删除使用相同上下文标识符添加的最后一个菜单项消息。最近的消息可以用statusbar.pop()
从堆栈中删除。
菜单项
到目前为止,您已经了解了显示标签和分隔符菜单项的平面菜单。也可以向现有菜单项添加子菜单。GTK+ 还提供了许多其他的Gtk.MenuItem
对象。图 10-3 显示了一个弹出菜单,其中包含一个子菜单以及图像、检查和单选菜单项。
图 10-3
图像、检查和单选菜单项
子菜单
GTK+ 中的子菜单不是由单独类型的菜单项小部件创建的,而是通过调用menuitem.set_submenu()
创建的。该方法调用menu.attach_to_widget()
将子菜单附加到菜单项,并在菜单项旁边放置一个箭头,表示它现在有子菜单。如果菜单项已经有子菜单,它将被给定的Gtk.Menu
小部件替换。
menuitem.set_submenu(submenu)
如果你有一个非常具体的选项列表,会使原本组织有序的菜单结构变得混乱,那么子菜单非常有用。使用子菜单时,您可以使用由Gtk.MenuItem
小部件提供的“activate-item”信号,该信号在菜单项显示其子菜单时发出。
除了Gtk.MenuItem
和菜单项分隔符,还有另外三种类型的菜单项对象:图像、复选、单选菜单项;这些将在本节的剩余部分中介绍。
图像菜单项
警告
从 GTK+ 3.1 开始,Gtk.ImageMenuItem
类就被弃用了。不要在新代码中使用它,注意它可能会在 GTK+ 的新版本中完全消失。
Gtk.ImageMenuItem
与其父类Gtk.MenuItem
非常相似,除了它在菜单项标签的左边显示了一个小图像。为创建新的图像菜单项提供了四个功能。
第一个函数imagemenuitem.new()
创建了一个新的Gtk.ImageMenuItem
对象,它有一个空标签,没有关联的图像。您可以使用图像菜单项的 image 属性来设置菜单项显示的图像。
Gtk.ImageMenuItem.new()
此外,您可以使用Gtk.ImageMenuItem.new_from_stock()
从纸张标识符创建新的图像菜单项。这个函数使用与 stock_id 相关联的标签和图像创建Gtk.ImageMenuItem
。该函数接受股票标识符字符串。
Gtk.ImageMenuItem.new_from_stock(stockid, accel_group)
该函数的第二个参数接受一个加速器组,它被设置为库存项目的默认加速器。如果你想像我们在清单 10-3 中所做的那样为菜单项手动设置键盘快捷键,你可以为这个参数指定None
。
同样,您可以使用Gtk.ImageMenuItem.new_with_label()
创建一个新的Gtk.ImageMenuItem
,最初只有一个标签。稍后,您可以使用 image 属性添加一个图像小部件。GTK+ 还提供了方法imagemenuitem.set_image()
,允许您编辑小部件的图像属性。
Gtk.ImageMenuItem.new_with_label(label)
另外,GTK+ 提供了Gtk.ImageMenuItem.new_with_mnemonic()
,它创建了一个带有助记标签的图像菜单项。与前面的方法一样,您必须在创建菜单项后设置 image 属性。
检查菜单项
Gtk.CheckMenuItem
允许您创建一个菜单项,根据其布尔活动属性是True
还是False
,在标签旁边显示一个复选符号。这将允许用户查看选项是被激活还是被停用。
与Gtk.MenuItem
一样,提供了三个初始化功能。
Gtk.CheckMenuItem.new()
、Gtk.CheckItem.new_with_label()
和Gtk.CheckMenuItem.new_with_mnemonic()
。这些函数分别创建一个没有标签、有初始标签或有助记标签的Gtk.CheckMenuItem
。
Gtk.CheckMenuItem.new()
Gtk.CheckMenuItem.new_with_label(label)
Gtk.CheckMenuItem.new_with_mnemonic(label)
如前所述,check 菜单项的当前状态由小部件的 active 属性保存。GTK+ 提供了checkmenuitem.set_active()
和checkmenuitem.get_active()
两个函数来设置和检索有效值。
与所有的 check button 小部件一样,您可以使用“toggled”信号,当用户切换菜单项的状态时会发出该信号。GTK+ 负责更新 check 按钮的状态,所以这个信号只是允许您更新应用以反映更改后的值。
Gtk.CheckMenuItem
还提供了checkmenuitem.set_inconsistent()
,改变菜单项的不一致属性。当设置为True
时,检查菜单项显示第三种“中间”状态,既不是活动状态也不是非活动状态。这可以向用户显示必须做出尚未设置的选择,或者对选择的不同部分设置和取消设置属性。
单选菜单项
Gtk.RadioMenuItem
是从Gtk.CheckMenuItem
派生出来的一个 widget。通过将 check 菜单项的 draw-as-radio 属性设置为True
,它被呈现为单选按钮而不是复选按钮。单选菜单项的工作方式与普通单选按钮相同。
第一个单选按钮应该使用下列函数之一创建。您可以将单选按钮组设置为None
,因为必需的元素是通过引用第一个元素添加到组中的。这些函数分别创建一个空菜单项、一个带标签的菜单项和一个带助记符的菜单项。
Gtk.RadioMenuItem.new(group)
Gtk.RadioMenuItem.new_with_label(group, text)
Gtk.RadioMenuItem.new_with_mnemonic(group, text)
所有其他单选菜单项都应该使用以下三个函数之一创建,这三个函数将它添加到与 group 关联的单选按钮组中。这些函数分别创建一个空菜单项、一个带标签的菜单项和一个带助记符的菜单项。
Gtk.RadioMenuItem.new_from_widget(group)
Gtk.RadioMenuItem.new_from_widget_with_label(group, text)
Gtk.RadioMenuItem.new_from_widget_with_mnemonic(group, text)
菜单栏
Gtk.MenuBar
是一个将多个弹出菜单组织成水平或垂直行的小部件。每个根元素都是一个Gtk.MenuItem
,它会向下弹出一个子菜单。一个Gtk.MenuBar
的实例通常显示在主应用窗口的顶部,以提供对应用所提供功能的访问。菜单栏示例如图 10-4 所示。
图 10-4
有三个菜单的菜单栏
在清单 10-5 中,创建了一个带有三个菜单的Gtk.MenuBar
小部件:文件、编辑和帮助。每个菜单实际上都是一个带有子菜单的Gtk.MenuItem
。然后,许多菜单项被添加到每个子菜单中。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_size_request(250, -1)
menubar = Gtk.MenuBar.new()
file = Gtk.MenuItem.new_with_label("File")
edit = Gtk.MenuItem.new_with_label("Edit")
help = Gtk.MenuItem.new_with_label("Help")
filemenu = Gtk.Menu.new()
editmenu = Gtk.Menu.new()
helpmenu = Gtk.Menu.new()
file.set_submenu(filemenu)
edit.set_submenu(editmenu)
help.set_submenu(helpmenu)
menubar.append(file)
menubar.append(edit)
menubar.append(help)
# Create the File menu content.
new = Gtk.MenuItem.new_with_label("New")
open = Gtk.MenuItem.new_with_label("Open")
filemenu.append(new)
filemenu.append(open)
# Create the Edit menu content.
cut = Gtk.MenuItem.new_with_label("Cut")
copy = Gtk.MenuItem.new_with_label("Copy")
paste = Gtk.MenuItem.new_with_label("Paste")
editmenu.append(cut)
editmenu.append(copy)
editmenu.append(paste)
# Create the Help menu content.
contents = Gtk.MenuItem.new_with_label("Help")
about = Gtk.MenuItem.new_with_label("About")
helpmenu.append(contents)
helpmenu.append(about)
self.add(menubar)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Menu Bars")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 10-5Creating Groups of Menus
使用Gtk.MenuBar.new()
创建新的Gtk.MenuBar
小部件。这将创建一个空的菜单外壳,您可以在其中添加内容。
创建菜单栏后,可以用menubar.set_pack_direction()
定义菜单栏项目的打包方向。pack_direction
属性的值由Gtk.PackDirection
枚举定义,包括Gtk.PackDirection.LTR
、Gtk.PackDirection.RTL
、Gtk.PackDirection.TTB
或Gtk.PackDirection.BTT
。它们分别从左到右、从右到左、从上到下或从下到上打包菜单项。默认情况下,子部件从左到右打包。
Gtk.MenuBar
还提供了另一个名为child-pack-direction
的属性,该属性设置菜单栏子菜单的菜单项打包的方向。换句话说,它控制子菜单项的打包方式。这个属性的值也由Gtk.PackDirection
枚举定义。
菜单栏中的每个子项实际上都是一个Gtk.MenuItem
小部件。因为Gtk.MenuBar
是从Gtk.MenuShell
派生的,所以您可以使用menuitem.append()
方法向栏中添加一个项目,如下行所示。
menubar.append(file)
您也可以使用file.prepend()
或file.insert()
将项目添加到菜单栏的开头或任意位置。
接下来您需要调用file.set_submenu()
来向每个根菜单项添加一个子菜单。每个子菜单都是一个Gtk.Menu
小部件,创建方式与弹出菜单相同。然后,GTK+ 会在必要时向用户显示子菜单。
file.set_submenu(filemenu)
工具栏
一个Gtk.Toolbar
是一种容器,它在水平或垂直的行中保存了许多小部件。这意味着可以很容易地定制大量的小部件。通常,工具栏包含可以显示图像和文本字符串的工具按钮。然而,工具栏实际上可以容纳任何类型的小部件。图 10-5 中显示了一个包含四个工具按钮和一个分隔符的工具栏。
图 10-5
显示图像和文本的工具栏
在清单 10-6 中,创建了一个简单的工具栏,在水平行中显示五个工具项。每个工具栏项都显示一个图标和一个描述该项目的用途的标签。工具栏还被设置为显示一个箭头,该箭头提供对菜单中不适合的工具栏项目的访问。
在本例中,工具栏为Gtk.Entry
小部件提供剪切、复制、粘贴和全选功能。AppWindow()
方法创建工具栏,将其打包在Gtk.Entry
之上。然后它调用create_toolbar()
,用工具项填充工具栏并连接必要的信号。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
toolbar = Gtk.Toolbar.new()
entry = Gtk.Entry.new()
vbox.pack_start(toolbar, True, False, 0)
vbox.pack_start(entry, True, False, 0)
self.create_toolbar(toolbar, entry)
self.add(vbox)
self.set_size_request(310, 75)
def create_toolbar(self, toolbar, entry): icon_theme = Gtk.IconTheme.get_default()
icon = icon_theme.load_icon("edit-cut", -1,
Gtk.IconLookupFlags.FORCE_SIZE)
image = Gtk.Image.new_from_pixbuf(icon)
cut = Gtk.ToolButton.new(image, "Cut")
icon = icon_theme.load_icon("edit-copy", -1,
Gtk.IconLookupFlags.FORCE_SIZE)
image = Gtk.Image.new_from_pixbuf(icon)
copy = Gtk.ToolButton.new(image, "Copy")
icon = icon_theme.load_icon("edit-paste", -1,
Gtk.IconLookupFlags.FORCE_SIZE)
image = Gtk.Image.new_from_pixbuf(icon)
paste = Gtk.ToolButton.new(image, "Paste")
icon = icon_theme.load_icon("edit-select-all", -1, Gtk.IconLookupFlags.FORCE_SIZE)
image = Gtk.Image.new_from_pixbuf(icon)
selectall = Gtk.ToolButton.new(image, "Select All")
separator = Gtk.SeparatorToolItem.new()
toolbar.set_show_arrow(True)
toolbar.set_style(Gtk.ToolbarStyle.BOTH)
toolbar.insert(cut, 0)
toolbar.insert(copy, 1)
toolbar.insert(paste, 2)
toolbar.insert(separator, 3)
toolbar.insert(selectall, 4)
cut.connect("clicked", self.cut_clipboard, entry)
copy.connect("clicked", self.copy_clipboard, entry)
paste.connect("clicked", self.paste_clipboard, entry)
selectall.connect("clicked", self.select_all, entry)
def cut_clipboard(self, button, entry):
entry.cut_clipboard()
def copy_clipboard(self, button, entry):
entry.copy_clipboard()
def paste_clipboard(self, button, entry):
entry.paste_clipboard()
def select_all(self, button, entry):
entry.select_region(0, -1)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Toolbar")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 10-6Creating a Gtk.Toolbar Widget
用Gtk.Toolbar.new()
创建新的工具栏,它在清单 10-6 中显示的create_toolbar()
函数之前被调用。这将创建一个空的Gtk.Toolbar
小部件,您可以在其中添加工具按钮。
Gtk.Toolbar
提供了许多属性,用于定制它的显示方式和与用户的交互方式,包括方向、按钮样式以及访问不适合工具栏的项目的能力。
如果因为没有足够的空间而无法在工具栏上显示所有的工具栏项目,那么如果您将toolbar.set_show_arrow()
设置为True
,则会出现一个溢出菜单。如果所有项目都可以显示在工具栏上,箭头就会隐藏起来。
toolbar.set_show_arrow(boolean)
另一个Gtk.Toolbar
属性是显示所有菜单项的样式,它是用toolbar.set_style()
设置的。你应该注意到这个属性可能会被主题覆盖,所以你应该通过调用toolbar.unset_style()
来提供使用默认样式的选项。有四种工具栏样式,由Gtk.ToolbarStyle
枚举定义。
-
Gtk.ToolbarStyle.ICONS
:仅显示工具栏中每个工具按钮的图标。 -
Gtk.ToolbarStyle.TEXT
:如何只为工具栏中的每个工具按钮添加标签。 -
Gtk.ToolbarStyle.BOTH
:显示每个工具按钮的图标和标签,图标位于标签上方。 -
Gtk.ToolbarStyle.BOTH_HORIZ
:显示每个工具按钮的图标和标签,图标在标签的左边。仅当工具项目的“is-important"
属性设置为True
时,工具项目的标签文本才会显示。
工具栏的另一个重要属性是可以用toolbar.set_orientation()
设置的方向。由Gtk.Orientation
枚举定义了两个可能的值,Gtk.Orientation.HORIZONTAL
和Gtk.Orientation.VERTICAL
,它们可以使工具栏水平(默认)或垂直。
工具栏项目
清单 10-6 介绍了三种重要的工具项类型:Gtk.ToolItem
、Gtk.ToolButton
和Gtk.SeparatorToolItem
。所有的工具按钮都是从Gtk.ToolItem
类中派生出来的,该类保存了所有工具项使用的基本属性。
如果你使用的是Gtk.ToolbarStyle.BOTH_HORIZ
风格,那么Gtk.ToolItem
中安装的一个基本属性就是"is-important"
设置。如果该属性设置为True
,工具栏项目的标签文本仅针对该样式显示。
与菜单一样,分隔符工具项由Gtk.SeparatorToolItem
提供,并由Gtk.SeparatorToolItem.new()
创建。分隔符工具项有一个 draw 属性,当设置为True
时会绘制一个分隔符。如果您将 draw 设置为False
,它会在没有任何可视分隔符的位置放置填充。
小费
如果您将一个Gtk.SeparatorToolItem
的扩展属性设置为True
并将它的绘制属性设置为False
,您将强制分隔符之后的所有工具项到工具栏的末尾。
大多数工具栏项目都属于Gtk.ToolButton
类型。Gtk.ToolButton
只提供了一个初始化方法Gtk.ToolButton.new()
,因为从 GTK+ 3.1 开始,所有其他的初始化方法都被弃用了。Gtk.ToolButton.new()
可以创建一个带有自定义图标和标签的Gtk.ToolButton
。这些属性中的每一个都可以设置为None
。
Gtk.ToolButton.new(icon, label)
使用toolbutton.set_label()
和toolbutton.set_icon_widget()
可以在初始化后手动设置标签和图标。这些函数提供对工具按钮的标签和图标小部件属性的访问。
此外,您可以定义自己的小部件来代替带有toolbutton.set_label_widget()
的工具按钮的默认Gtk.Label
小部件。这允许您在工具按钮中嵌入任意的小部件,比如一个条目或组合框。如果该属性设置为None
,则使用默认标签。
toolbutton.set_label_widget(label_widget)
创建工具栏项目后,可以用toolbar.insert()
将每个Gtk.ToolItem
插入工具栏。
toolbar.insert(item, pos)
toolbar.insert()
的第二个参数接受将项目插入工具栏的位置。工具按钮位置从零开始索引。负位置会将该项追加到工具栏的末尾。
切换工具按钮
Gtk.ToggleToolButton
衍生自Gtk.ToolButton
,因此只实现初始化和切换能力。切换工具按钮以工具栏项目的形式提供了Gtk.ToggleButton
小部件的功能。它允许用户查看选项是已设置还是未设置。
切换工具按钮是当活动属性设置为True
时保持按下状态的工具按钮。当切换按钮的状态改变时,您可以使用切换信号来接收通知。
只有一种方法可以创建新的Gtk.ToggleToolButton
。这是用Gtk.ToggleToolButton.new()
实现的,它创建了一个空的工具按钮。然后你可以使用Gtk.ToolButton
提供的方法来添加标签和图片。
Gtk.ToggleToolButton.new()
单选工具按钮
Gtk.RadioToolButton
是从Gtk.ToggleToolButton
派生出来的,所以继承了“active”属性和“toggled”信号。因此,小部件只需要为您提供一种方法来创建新的单选工具按钮,并将它们添加到单选按钮组中。
应该用Gtk.RadioToolButton.new()
创建一个单选工具按钮,其中单选组设置为None
。这将为单选工具按钮创建一个默认的初始单选按钮组。
Gtk.RadioToolButton.new(group)
Gtk.RadioToolButton
继承了Gtk.ToolButton
的函数,后者提供了一些函数和属性,可以在必要时设置单选按钮的标签。
所有必需的元素都应该用Gtk.RadioToolButton.from_widget()
创建。将“组”设置为第一个单选按钮会将所有必需的项目添加到同一组中。
Gtk.RadioToolButton.new_from_widget(group)
Gtk.RadioToolButton
提供了一个属性 group,它是属于单选按钮组的另一个单选工具按钮。这允许您将所有单选按钮链接在一起,以便一次只能选择一个。
菜单工具按钮
从Gtk.ToggleToolButton
派生而来的Gtk.MenuToolButton
,允许你在工具按钮上附加一个菜单。小部件在图像和标签旁边放置一个箭头,提供对相关菜单的访问。例如,您可以使用Gtk.MenuToolButton
将最近打开的文件列表添加到工具栏按钮中。图 10-6 是用于此目的的菜单工具按钮的截图。
图 10-6
显示最近打开的文件的菜单工具按钮
清单 10-7 向你展示了如何实现一个菜单工具按钮。实际的工具按钮的创建方式与任何其他Gtk.ToolButton
类似,只是多了一个将菜单附加到Gtk.MenuToolButton
小部件的步骤。
recent = Gtk.Menu.new()
# Add a number of menu items where each corresponds to one recent file. icon_theme = Gtk.IconTheme.get_default()
icon = icon_theme.load_icon("document-open", -1,
Gtk.IconLookupFlags.FORCE_SIZE)
image = Gtk.Image.new_from_pixbuf(icon)
open = Gtk.MenuToolButton.new(image, "Open")
open.set_menu(recent)
Listing 10-7Using Gtk.MenuToolButton
在清单 10-7 中,菜单工具按钮是用一个图像和一个标签Gtk.MenuToolButton.new(image, label)
创建的。如果您想在以后使用Gtk.ToolButton
属性设置它们,您可以将这些参数中的任何一个设置为None
。
Gtk.MenuToolButton.new(image, label)
使Gtk.MenuToolButton
与众不同的是,工具按钮右边的箭头为用户提供了访问菜单的途径。工具按钮的菜单是用menutoolbutton.set_menu()
设置的,或者通过将菜单属性设置为一个Gtk.Menu
小部件来设置。单击箭头时,会向用户显示此菜单。
动态菜单创建
注意
在 GTK+ 3.1 中不推荐使用Gtk.UIManager
,所以 UI 文件的创建和加载不包含在本节中。相反,新的Gtk.Builder
类及其相关的 XML 文件被包含在内。Gtk.Builder
是一个更强大、更灵活的系统,用于管理外部用户界面描述和操作。它还提供了附加功能,并减少了创建和管理用户界面所需的工作量。
虽然可以手动创建每个菜单和工具栏项,但这样做会占用大量空间,并导致您不得不单调地编写不必要的代码。为了自动创建菜单和工具栏,GTK+ 允许您从 XML 文件动态创建菜单。
类可以创建许多用户界面对象,包括菜单、菜单栏、弹出菜单、整个对话框、主窗口等等。本节集中讨论不同类型的菜单,但是你应该记住Gtk.Builder
可以构建许多其他类型的用户界面对象。
创建 XML 文件
用户界面文件以 XML 格式构造。所有的内容都必须包含在
10-8.
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="menubar">
<submenu>
<attribute name="label">File</attribute>
</submenu>
<submenu>
<attribute name="label">Edit</attribute>
</submenu>
<submenu>
<attribute name="label">Choices</attribute>
</submenu>
<submenu>
<attribute name="label">Help</attribute>
</submenu>
</menu>
</interface>
Listing 10-8Menu UI File
每个菜单和项目标记都应该有一个与之关联的唯一 ID,这样您就可以从代码中直接访问该项。虽然不是必需的,但您应该始终将 name 属性添加到每个菜单和项目中。name 属性可以访问实际的小部件。
每个
十一、动态用户界面
到目前为止,您已经学习了大量关于 GTK+ 及其支持库的知识,并且能够创建相当复杂的应用。然而,手动编写所有代码来创建和配置这些应用的小部件和行为会很快变得乏味。
Glade 用户界面构建器允许您以图形方式设计用户界面,从而消除了您编写所有代码的需要。它支持 GTK+ 小部件库以及 GNOME 库中的各种小部件。用户界面保存为 XML 文件,可以动态构建应用的用户界面。
本章的最后一部分介绍了Gtk.Builder
,一个可以动态加载 XML 文件的库。创建所有必要的小部件,并允许您连接 Glade 中定义的任何信号。
注意
本章介绍了撰写本文时的 Glade 用户界面。将来这种情况可能会改变,但任何改变都应该是从本章提供的说明开始的简单过渡。
在本章中,您将学习以下内容。
-
设计图形用户界面(GUI)时应该记住的问题
-
如何用 Glade 设计定制的图形用户界面
-
如何用
Gtk.Builder
动态加载 Glade 用户界面
用户界面设计
在这一章中,你将学习如何使用 Glade 3 和Gtk.Builder
来实现动态用户界面。然而,谨慎的做法是首先学习一些在设计图形用户界面时应该记住的概念。这些概念可以帮助你在将来避免让用户困惑和沮丧。
你还必须意识到,虽然你知道如何使用你的应用,因为你设计了它,但你需要尽可能地帮助用户理解它。无论用户是专家还是新手,每个用户都应该能够以最短的学习曲线使用您的应用。也就是说,下面的部分包括许多提示和设计决策,可以帮助您实现这种直观性。它们还提高了应用的可维护性。
了解你的用户
设计用户界面时,最重要的是考虑你的受众。他们是否都有处理手头任务的经验,还是有些人比其他人需要更多的帮助?你能模仿他们已经熟悉的用户界面来设计你的用户界面吗,或者这是一个全新的东西?
最大的可能错误之一是对用户的技能水平做出轻率的概括。您可能认为您布局应用的方式是有意义的,但那是因为您设计了它。你应该把自己放在用户的位置上,理解他们对如何使用你的应用没有预先的了解。
为了避免混淆,请花时间研究类似的应用,注意哪些设计决策似乎是成功的,哪些会导致问题。例如,如果你正在创建一个在 GNOME 桌面环境中使用的应用,你应该检查一下 GNOME 人机界面指南(
http://developer.gnome.org
)
,它可以帮助你设计一个用于其他兼容应用的设计。
设计用户界面时要考虑的另一件事是可访问性。用户可能有视力问题,这可能会阻止他们使用应用。Accessibility Toolkit 为 GTK+ 应用提供了许多工具,使它们与屏幕阅读器兼容。GTK+ 也非常依赖于主题,这就是为什么你应该尽可能避免设置字体,或者为用户提供改变字体的方法。
设计用户界面时,语言是另一个需要考虑的因素。首先,你应该总是使用用户熟悉的行话。例如,您可以在工程应用中自由使用数学术语,但不应该在 web 浏览器中这样做。
许多应用在流行时会被翻译成其他语言,如果您使用在其他文化中可能具有攻击性的词语或图像,这可能会引起问题。
保持设计简单
一旦你了解了你的受众,设计一个有效的用户界面就变得简单多了,但是如果界面太难或者太混乱,你仍然会遇到问题。总是试图将屏幕上的窗口小部件减少到一个合理的数量。
例如,如果您需要向用户提供许多选择,但只能选择一个,您可能会尝试使用许多单选按钮。然而,一个更好的解决方案可能是使用一个Gtk.ComboBox
,这可以显著减少所需的小部件数量。
对于分组相似的选项组来说,Gtk.Notebook
容器非常有用,否则会使一个巨大的页面变得混乱。在许多应用中,这个小部件将相互关联或依赖的小部件组合到一个首选项对话框中。
菜单布局也是另一个有问题的领域,因为它并不总是以合理的方式完成。如果可能,您应该使用标准菜单,如文件、编辑、查看、帮助、格式和窗口。这些菜单对于有计算经验的用户来说是熟悉的,也是用户所期望的。因此,这些菜单也应该包含标准项目。例如,文件菜单应该包含用于操作文件、打印和退出应用的项目。如果您不确定在哪里放置特定的菜单项,您应该研究一下其他应用是如何布局它们的菜单项的。
重复性的工作,或者那些用户经常执行的工作,应该总是变得快速和简单。有多种方法可以做到这一点。最重要的是为许多操作提供键盘快捷键——在键盘上按 Ctrl+O 比单击文件菜单和打开菜单项要快得多。
注意
只要有可能,你应该总是使用标准的键盘快捷键,比如 Ctrl+X 用于剪切,Ctrl+N 用于创建新的东西。这大大缩短了应用用户的初始学习曲线。事实上,一些键盘快捷键已经内置在许多小部件中,例如 Ctrl+X 用于剪切文本小部件中的选择。
你的用户可能需要一段时间来习惯键盘快捷键,这就是为什么工具栏对于重复选项也非常有用。但是,你需要在工具栏上放置太少和太多的项目之间找到一个平衡点。杂乱的工具栏会让用户感到害怕和困惑,但是项目太少的工具栏是没有用的。如果你的工具栏上有大量用户想要的项目,那么允许用户自己定制工具栏是有意义的。
始终保持一致
在设计图形用户界面时,一致性非常重要,GTK+ 使这变得非常容易。首先,GTK+ 提供了许多库存物品,在可能的情况下,应该总是优先使用国产物品。用户将已经熟悉股票项目的图标,并知道如何使用它们。
警告
如果使用不当,库存物品会非常危险。你不应该使用一个库存项目来完成一个它原本不打算做的动作。例如,你不应该仅仅因为 GTK 股票删除图标看起来像一个“减号”就把它用于减法运算图标由用户的主题定义;他们可能不总是像你想象的那样。
说到主题,你应该尽可能依靠主题提供的设置。这有助于您创建一致的外观——不仅在整个应用中,而且在整个桌面环境中。由于主题应用于整个桌面上的所有应用,因此您的应用与用户运行的大多数其他应用是一致的。
在少数情况下,你确实需要偏离用户主题提供的默认值,你应该总是给用户一个方法来改变设置或者仅仅使用系统默认值。这在处理字体和颜色时尤其重要,因为您的更改可能会使您的应用无法用于某些主题。
一致性的另一个好处是用户可以更快地学会如何使用你的应用。用户只需要学习一个设计,而不是许多。如果你的应用和补充对话框没有使用一致的布局,那么每个新窗口都会给用户带来一次全新的体验。
让用户了解情况
如果应用长时间没有响应,用户会很快失去兴趣。大多数计算机用户都习惯了一两个 bug,但是如果你的应用正在处理信息并且长时间没有响应,用户可能会放弃。
为了避免这种情况,有两种可能的解决方案。首先是让你的应用更有效率。但是,如果您的应用没有问题,或者没有办法让它更有效,您应该使用进度条。进度条告诉用户您的应用仍在工作。只要确保更新你的进度条!如果您不知道这个过程需要多长时间,另一个选择是脉动进度条,并提供消息来更新用户的进程进度。
另外,请记住第章 3 中的以下循环。
while Gtk.events_pending():
Gtk.main_iteration()
这个循环确保用户界面得到更新,即使处理器正忙于处理另一个任务。如果在 CPU 密集型过程中不更新用户界面,应用可能会在完成之前对用户没有响应!
您还应该在执行操作时向用户提供反馈。如果正在保存文档,您应该将其标记为未修改,或者在状态栏中显示一条消息。如果您在执行某个动作时没有向用户提供反馈,则可以认为该动作没有被执行。
消息对话框是提供反馈的一种非常有用的方式,但是它们应该只在必要的时候使用。如果消息对话框出现得太频繁,用户会感到沮丧,这就是为什么只有严重错误和警告才应该这样报告。
我们都会犯错
不管你是专家还是新手,我们都会犯错。正因为如此,你应该永远原谅你的用户。毕竟,每个人都曾按下过错误的按钮,导致大量工作的丢失。在设计合理的应用中,这种情况永远不会发生。
对于用户不容易撤销的基本操作,您应该提供撤销操作的能力。例如,这些基本操作可能包括从我们的杂货列表应用中删除一个项目,或者在文本视图中移动文本。
对于无法撤消的操作,您应该始终提供一个确认对话框。它应该明确声明这个操作不能撤销,并询问用户是否要继续。例如,您应该始终询问用户,当有未保存更改的文档时,是否应该关闭应用。人们已经使用软件很多年了,并且已经开始期待一个确认对话框来处理不能撤销的操作。
Glade 用户界面生成器
决定 GUI 工具包成败的一个因素是它能否快速部署应用。虽然用户界面对应用的成功极其重要,但它不应该是开发过程中最耗费精力的方面。
Glade 是一个工具,允许您快速有效地设计图形用户界面,以便您可以转移到代码的其他方面。用户界面被保存为 XML 文件,该文件描述了小部件的结构、每个小部件的属性以及与每个小部件相关联的任何信号处理程序。Gtk.Builder
然后可以加载用户界面文件,以便在应用加载时动态构建它。这允许您从美学角度改变用户界面,而无需重新编译应用。
注意
Glade 的旧版本允许您生成源代码,而不是将用户界面保存在 XML 文件中。不推荐使用此方法,因为当您想要更改用户界面时很难管理它。因此,您应该遵循本章提供的方法。
你需要从一开始就意识到什么是 Glade,什么不是。Glade 设计应用的用户界面,设置与代码中实现的回调方法相关联的信号,并处理常见的小部件属性。然而,Glade 不是代码编辑器或集成开发环境。它输出的文件必须由您的应用加载,并且您必须在代码中实现所有回调方法。Glade 只是为了简化初始化应用的图形用户界面和连接信号的过程。
小费
本书中使用的 Glade 3.22.1 版本现在允许集成开发环境(如 Anjuta)将其嵌入到用户界面中。这些 ide 为部署 GTK+ 应用提供了一个完整的、自始至终的解决方案。
Glade 的另一个优点是,由于用户界面存储为 XML 文件,它们独立于语言。任何封装了Gtk.Builder
提供的功能的语言都可以加载用户界面。这意味着无论您选择哪种编程语言,都可以使用相同的图形用户界面设计器。
在继续本章的其余部分之前,您应该从操作系统的包管理器安装 Glade 和用于Gtk.Builder
的开发包。或者,您可以从glade.
gnome.org
下载并编译源代码。
此外,在阅读本章的其余部分时,您应该确保遵循并创建这个应用。这为您提供了一个学习 Glade 3 应用的机会,因此您可以在这本书的指导下尽可能多地进行实践。
Glade 界面
当您第一次启动 Glade 时,您会看到一个包含三个窗格的主窗口:主窗口树视图、小部件调色板和小部件属性编辑器。图 11-1 是 Glade 应用主窗口的屏幕截图,其中有一个从FileBrowser.glade
打开的项目。
图 11-1
Glade 主窗口
主树视图窗口有助于 Glade 项目管理。主窗口标题栏显示当前打开的项目列表,允许您在它们之间切换。左窗格还包括小部件树视图,它显示了具有焦点的项目的小部件包含。
小部件树视图显示了项目中的父子容器关系。可以有多个顶级小部件。然而,在图 11-1 中窗口是FileBrowser.glade
的唯一顶层小部件。
此窗格是您指定项目选项、保存项目和加载现有项目的地方。此窗口中的弹出式菜单还提供了许多其他选项,可以帮助您处理项目,如撤销和重做操作。
注意
如果您决定使用 Glade 2 而不是 Glade 3,请确保经常保存。在 Glade 的旧版本中没有实现撤销和重做支持,如果您因为一次错误的鼠标点击而意外地覆盖了一个小时的工作,那将是非常令人沮丧的!
当您启动 Glade 3 时,显示的中间窗格有用于从小部件面板中选择小部件的按钮,该面板列出了所有可用于设计应用的小部件。图 11-2 中显示了其中一个小工具调色板的屏幕截图。
图 11-2
Glade widget 调色盘
默认情况下,可以显示五种类别的小部件:顶级小部件、容器、用于控制的小部件、显示小部件以及复合和贬值小部件。您不应该在新的应用中使用 GTK+ 废弃列表中的任何小部件,因为它们已经过时,可能会在未来的版本中被删除。
除了默认类别的小部件之外,您可能会发现包含其他小部件库的其他类别。这些包括为 GNOME 库或其他自定义小部件库添加的小部件。
通过视图菜单,您可以更改小部件面板的布局。图 11-2 显示了一个设置为显示图标和文本的组件面板。但是,根据您最喜欢的样式,您可以只显示文本或图标。
要向小部件布局窗格添加新的顶级小部件,只需在 Toplevels 部分单击所需小部件的图标。然后会显示一个新的顶级小部件,并将其添加到左窗格的小部件树中。要添加非顶级小部件,您需要首先单击所需小部件的图标,然后在应该放置小部件的位置单击鼠标。您必须单击容器小部件中的空单元格,以便将非顶级小部件插入到用户界面中。
创建窗口
在本章中,你将使用 Glade 和Gtk.Builder
创建一个简单的文件浏览器应用。首先,您可以通过单击主 Glade 窗口顶部的 new project 按钮来创建一个新项目,或者使用应用加载时为您创建的空白项目。如果您稍后返回本教程,您可以通过单击主 Glade 窗口顶部的 open 按钮来打开一个现有的项目。
拥有一个空白项目后,您可以通过单击 Toplevels 小部件面板中的窗口图标来创建一个新的顶级Gtk.Window
。在新窗口中,您会看到小部件内部的网格图案,如图 11-3 所示。该模式指定了一个区域,可以在该区域将子部件添加到容器中。从小部件面板中选择一个非顶级小部件后,必须单击此区域将小部件添加到容器中。按照此方法添加所有非顶级小部件。
图 11-3
默认的 Gtk。窗口小部件
创建顶层窗口后,您会注意到小部件属性窗格的内容发生了变化,如图 11-4 所示。在这个面板中,您可以自定义 Glade 支持的每个小部件的所有属性。
注意
虽然 Glade 允许您编辑许多小部件属性,但有些操作只需在代码中执行。因此,你不应该把 Glade 看作是你在本书中学到的所有东西的替代品。在大多数应用中,您仍然在进行大量的 GTK+ 开发。
图 11-4 中显示的微件属性窗口有各种选项的完整列表。该窗格分为几个部分,这些部分对特定于当前所选小部件类型的基本选项进行了分类。例如,Gtk.Window
小部件允许您指定窗口的类型、标题、调整大小的能力、默认大小等等。
图 11-4
小部件属性窗格
ID 字段被滚动到图 11-4 中滚动窗口的边界之外,为小部件提供了一个唯一的名称。Glade 会自动为每个小部件指定一个名称,这个名称对于当前项目来说是唯一的,但是这些都是通用名称。如果您计划从应用中引用一个小部件,您应该给它一个有意义的 ID。当您必须加载三个名为 treeview1、treeview2 和 treeview3 的Gtk.TreeView3
小部件时,这很容易让人感到困惑!
“打包”选项卡提供了关于微件如何对其父微件的大小变化做出反应的基本信息,如扩展和填充。通用属性是由Gtk.Widget
提供的,并且对所有小部件都可用。例如,您可以在该选项卡中提供尺寸请求。
注意
第一次使用 Glade 时,打包选项有点不直观,因为属性是由子容器而不是父容器设置的。例如,Gtk.Box
的子容器的打包选项在子容器本身的打包选项卡中提供,而不是在父容器中。
信号选项卡允许您为通过Gtk.Builder
连接的每个微件定义信号。最后,由残障符号指定的“辅助功能”选项卡提供了用于辅助功能支持的选项。
正如您在本书的第一个例子中回忆的那样,空的Gtk.Window
小部件除了演示如何创建之外没有任何用处。由于文件浏览器需要将多个小部件打包到这个应用的主窗口中,下一步是添加一个垂直的盒子容器。从面板中选择 Box 小部件,并在窗口的网格图案内单击,将一个Gtk.Box
小部件插入到窗口中。然后,您可以使用属性窗格来调整框的方向(垂直或水平)以及Gtk.Box
包含的窗格数量。图 11-5 显示了Gtk.Box
属性所需的调整。
图 11-5
默认的 Gtk。窗口小部件
默认情况下,创建三个单元格来存放子部件,但是您可以将它更改为大于零的任意数量的项目。默认的三是我们需要多少个子部件。
默认情况下,Gtk.Box
的方向是垂直的,但如果需要,您可以将方向改为水平。
注意
如果您不确定容器将容纳多少个小部件,请不要担心。您可以在微件属性窗格的常规选项卡中添加或移除单元格。然后,您可以在打包选项卡下更改小部件在框中的位置。在由Gtk.Builder
构建之后,您仍然可以用您的代码编辑用户界面!
添加垂直框后,您会看到三个单独的空容器网格;请注意属性窗格和小部件树视图窗格中的变化。对于这些网格,我们将添加一个工具栏、一个地址栏和一个树形视图。
添加工具栏
旧的 handle box 小部件早就被弃用了,因为它原本要包含的大多数小部件已经得到了增强,可以动态隐藏它们的内容。Gtk.Toolbar
是以这种方式增强的小部件之一。这意味着我们可以直接将工具栏添加到之前添加到主窗口的垂直Gtk.Box
中。
添加工具栏小部件时,它仅作为一条细条出现在垂直框的顶部窗格中。这是因为它还不包含任何按钮。向工具栏添加按钮的方法并不明显。要在工具栏上添加按钮,右键单击 Gtk。Glade 树视图窗格中的工具栏条目和一个标有“编辑”的弹出菜单...出现,然后显示图 11-6 中的对话框。
图 11-6
工具栏编辑器
工具栏编辑器允许您向工具栏添加任何受支持类型的项目。要添加新项目,您只需点击“添加”按钮。这将在编辑器对话框中显示一个窗格,您可以在其中修改新按钮的属性。这里要小心,因为你的 Glade 版本可能会显示使用股票按钮的选项。股票项目都已被否决,所以你必须创建自己的自定义按钮。
添加新的工具按钮后,下一步是通过从类型组合框中选择一个选项来选择小部件的类型。组合框中包含的工具栏项类型是包含图像和标签的通用工具按钮、切换按钮、单选按钮、菜单工具按钮、工具项和分隔符。当您选择新类型时,对话框会立即发生变化,允许您编辑所选类型的属性。
例如,在图 11-6 中,选择的工具按钮属于Gtk.MenuToolButton
类型。每个工具栏项都提供了一个选项,当工具栏是水平还是垂直时,它应该是可见的。这允许您在工具栏为垂直方向时隐藏工具栏项目,而在工具栏为水平方向时向用户显示。
菜单工具按钮还允许您选择要在工具项目中显示的标签和图像。根据您选择的选项,图像可以是库存图像、现有图像文件或自定义图标主题的标识符。
在工具栏编辑器的底部,您会看到一个树形视图,允许您将信号连接到每个工具按钮。Glade 提供了许多命名的回调方法供您选择,这些方法基于信号名和您给工具栏项的名称。您还可以输入自己的自定义回调方法名。可以通过Gtk.Builder
指定要传递给每个方法函数的数据,因此通常可以将“用户数据”参数留空。在图 11-6 中,一个名为on_back_clicked()
的回调方法被连接到Gtk.MenuToolButton
的"clicked"
信号上。
当您用Gtk.Builder
加载用户界面时,您有两种选择来连接 Glade 文件中定义的回调方法和代码中的回调方法。如果您想要手动连接每个回调方法,您可以任意命名信号处理程序,只要该名称是唯一的。然而,Gtk.Builder
提供了一个函数,可以自动将所有信号连接到可执行文件或 Python 程序中的适当符号。要使用这个特性,您在 Glade 中定义的回调方法名必须与您代码中的函数名相匹配!
“打包”选项卡包括用于确定小部件周围的填充、打包是从框的开始还是结束,以及确定小部件在容器中的位置的选项。这些属性完全等同于您在使用box.pack_start()
和朋友向Gtk.Box
添加子部件时使用的设置。
小费
你应该记得在第四章中提供了一个表格,说明了扩展和填充属性对 Gtk 的子部件做了什么。框小部件。Glade 为您提供了一个绝佳的机会来试验打包选项,以便更好地理解它们是如何影响小部件的。因此,请花点时间尝试各种包装方案!
完成工具栏和打包参数设置后,您的应用应该如图 11-7 所示。
图 11-7
运行中的工具栏
图 11-7 所示的工具栏包含两个菜单工具按钮,用于在用户浏览历史中向前和向后移动。还有用于移动到父目录、刷新当前视图、删除文件、移动到主目录以及查看文件信息的工具按钮。每个工具按钮都连接到一个回调方法,您必须在应用的代码中实现该方法。
完成文件浏览器
创建文件浏览器的下一步是创建地址栏,向用户显示当前位置,并允许他们输入新位置。这意味着我们需要一个有三个部件的水平框,如图 11-8 所示。这三个小部件是描述保存在Gtk.Entry
小部件中的内容的标签,保存当前位置的Gtk.Entry
小部件,以及按下时移动到该位置的按钮。
图 11-8
文件浏览器
为了创建图 11-8 中的按钮,一个带有两个子部件的水平Gtk.Box
被添加到按钮中:一个Gtk.Image
部件设置为 GTK _ 股票 _ 跳转 _ 股票图像,一个Gtk.Label
部件名为 Go。
最后一步是向垂直框中的最后一个单元格添加一个Gtk.ScrolledWindow
小部件,向该容器添加一个Gtk.TreeView
小部件。完成后的文件浏览器用户界面如图 11-9 所示。然而,我们还没有完成在 Glade 中编辑应用。
图 11-9
文件浏览器
做出改变
文件浏览器已经完全设计好了,但现在我决定它应该在窗口底部包含一个Gtk.StatusBar
小部件!对用户界面进行更改可能很棘手,因此本节将带您完成一些具有挑战性的操作。
添加状态栏的第一步是扩展主垂直Gtk.Box
小部件包含的子小部件的数量。为此,从小部件树视图中选择垂直框。在“属性”窗格中,您可以使用“常规”选项卡中的“项目数”属性来增加子项的数量。这将在垂直框的末尾添加一个新的空白空间,您可以在其中添加状态栏小部件。
如果需要对垂直或水平框的子元素重新排序,首先需要选择要移动的小部件。然后,在“属性”窗格的“打包”选项卡下,您可以通过更改其微调按钮的值来选择新位置。当您更改微调按钮的值时,您可以看到子部件移动到它的新位置。周围子部件的位置会自动调整以反映这些变化。
如果您决定需要将一个容器填充到已经添加了另一个小部件的位置,就会导致另一个有问题的任务。例如,假设您已经决定在文件浏览器应用中放置一个水平窗格来代替滚动窗口。您首先需要从主窗口的小部件树视图中选择小部件,并通过按 Ctrl+X 移除它。之后,会显示一个空框,您可以在其中添加水平窗格。接下来,选择应该放置滚动窗口的窗格,并按 Ctrl+V。
在 Glade 2 中,修改用户界面曾经是一个敏感的话题,因为它不支持撤销和重做操作。过去,由于不小心删除了顶级小部件,很容易犯错误并损失几个小时的工作,因为您不能撤销任何操作。既然 Glade 3 包含了撤销和重做支持,您就不必担心了。
部件信号
这个应用的最后一步是为所有的小部件设置信号。图 11-10 显示了 Go 按钮的小部件属性编辑器的信号选项卡。Gtk.Button
小部件连接到被点击的信号,该信号在发出时调用on_button_clicked()
。
图 11-10
widget 信号编辑器
除了“点击”信号,你还需要连接其他几个信号。除分隔符外,每个工具项都应连接到Gtk.ToolButton
的点击信号。此外,您应该连接Gtk.Entry
来激活,当条目有焦点时,当用户按 Enter 键时会发出此消息。
注意
这个应用只是一个简单的文件浏览器的设计,旨在向您展示如何使用 Glade 3 设计应用。应用不仅仅是一个设计所需的代码在第十四章中实现。
至于树视图,您应该将其连接到行激活的。当一行被激活时,向用户显示关于该文件的更多信息,或者它导航到所选择的目录。表 11-1 中提供了一个小部件列表以及它们的信号和回调方法,这样你可以很容易地理解这个例子。
表 11-1
部件信号
|小部件
|
描述
|
信号
|
回调方法
|
| --- | --- | --- | --- |
| Gtk.Button
| 转到按钮 | "已点击" | on_go_clicked()
|
| Gtk.Entry
| 位置条目 | "激活" | on_location_activate()
|
| Gtk.MenuToolButton
回 | | "已点击" | on_back_clicked()
|
| Gtk.MenuToolButton
前进 | | "已点击" | on_forward_clicked()
|
| Gtk.ToolButton
| 起来 | "已点击" | on_up_clicked()
|
| Gtk.ToolButton
| 恢复精神 | "已点击" | on_refresh_clicked()
|
| Gtk.ToolButton
| 主页 | "已点击" | on_home_clicked()
|
| Gtk.ToolButton
| 删除 | "已点击" | on_delete_clicked()
|
| Gtk.ToolButton
| 信息 | "已点击" | on_info_clicked()
|
| Gtk.TreeView
| 文件浏览器 | "行激活" | on_row_activated()
|
| Gtk.Window
| 主窗口 | “摧毁” | on_window_destroy()
|
创建菜单
除了工具栏,在 Glade 3 中还可以创建菜单。图 11-11 所示为菜单栏编辑器,与工具栏编辑器非常相似。它支持普通的菜单项和那些用图像、复选按钮、单选按钮和分隔符呈现的菜单项。
图 11-11
菜单栏编辑器
警告
Glade 3.22.1 编辑器目前仍然使用库存项目作为菜单项。所有的股票项目都被否决,所以你真的应该使用自己的自定义菜单项,只有这个版本的 Glade 不支持自定义菜单项。您可能需要编辑 Glade 生成的 XML 来创建您自己的定制条目。
您现在知道了创建菜单的三种方法;这就产生了一个问题,到底哪一个最好。每种方法都有其优缺点,所以让我们来看看每种方法。
您首先学习了如何手动创建菜单,根据您的需要塑造每个对象。这种方法适用于较小的菜单,因为代码不会占用太多空间,而且实现完全在一个地方。但是,如果您的菜单变大或者包含的内容不仅仅是基本的项目,那么维护代码会变得很繁琐,并且会占用大量空间。
接下来,您学习了如何使用带有 UI 定义的Gtk.Builder
来动态创建菜单。这种方法简化了菜单的创建,因为您可以在很小的空间内定义大量的动作。此外,因为菜单是由 UI 定义构造的,所以允许用户编辑菜单非常简单。如果不使用 Glade 设计应用,这显然是创建菜单的首选方法。
Glade 还提供了一个非常吸引人的菜单创建方法,因为在它的初始设计之后,维护是简单的。它也不需要代码来创建菜单,因为Gtk.Builder
已经为您构建好了。然而,这种方法的一个问题是,它不像 UI 文件方法那样容易允许用户改变菜单和工具栏的布局。
一种容易采用的方法是将所有的小部件打包到垂直框的末端,或者打包到主窗口的子窗口。然后,当您的应用加载时,您可以简单地用box.pack_start()
将Gtk.Builder
创建的菜单打包到窗口中。然而,如果您不需要允许您的用户定制菜单,那么通过 Glade 创建所有菜单是有意义的。
现在您已经完成了用户界面的创建,您可以将它保存为一个FileBrowser.glade
文件,其中 project 可以替换为您选择的名称。可以根据应用的位置或从绝对路径加载该文件。
使用 Gtk。建设者
在 Glade 中设计应用后,下一步是用Gtk.Builder
加载用户界面。
这个 GTK+ 类解析 Glade 用户界面,并在运行时创建所有必要的小部件。
提供创建和保存从 XML 文件加载的用户界面所需的方法。它还可以将添加到 Glade 文件中的信号连接到应用中的回调方法。
Gtk.Builder
的另一个优点是开销仅在初始化期间增加,与直接从代码创建的接口相比,这是可以忽略的。初始化之后,实际上不会给应用增加任何开销。例如,Gtk.Builder
内部连接信号处理程序的方式与您自己的代码相同,因此这不需要额外的处理。
由于Gtk.Builder
处理所有的小部件初始化,并且布局已经在 Glade 3 中设计好了,所以代码库的长度可以大大减少。以清单 11-1 为例,如果您必须手工编写所有代码,那么清单会长得多。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class SignalHandlers():
def on_back_clicked(self, button ):
pass
def on_forward_clicked(self, button ):
pass
def on_up_clicked(self, button ):
pass
def on_refresh_clicked(self, button ):
pass
def on_home_clicked(self, button ):
pass
def on_delete_clicked(self, button ):
pass
def on_info_clicked(self, button ):
pass
def on_go_clicked(self, button ):
pass
def on_location_activate(self, button ):
pass
def on_row_activated(self, button ):
pass
def on_window_destroy(self, button ):
pass
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
builder = Gtk.Builder()
builder.add_from_file("./FileBrowser.glade")
self.window = builder.get_object("main_window")
self.add_window(self.window)
builder.connect_signals(SignalHandlers())
self.add_window(self.window)
self.window.show_all()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 11-1Loading the User Interface
加载用户界面
加载 Glade 用户界面是通过builder.add_from_file()
完成的。这是您应该调用的第一个Gtk.Builder
方法,尽管它应该在获得一个Gtk.Builder
的实例后调用。它解析 XML 文件提供的用户界面,创建所有必需的小部件,并提供翻译工具。builder.add_from_file()
方法需要的唯一参数是 Glade 项目文件的路径。
builder = Gtk.Builder()
builder.add_from_file("./FileBrowser.glade")
接下来,您需要获取"main_window"
,连接所有信号,最后将窗口添加到Gtk.Application
类实例中。
self.window = builder.get_object("main_window")
builder.connect_signals(SignalHandlers())
self.add_window(self.window)
builder.get_object()
需要一个参数,它是您在 Glade 项目中分配给Gtk.Window
主窗口的 ID。由此Gtk.Builder
可以通过读取 XML 来确定属于主窗口的所有子窗口部件。然后,它可以从 XML 定义中构造窗口。
构建主窗口后,我们需要分配所有的信号处理程序。如果我们提供一个只包含信号处理方法的特殊 Python 类,就可以自动完成这项工作。builder.connect_signals()
方法通过将我们的信号处理程序类的实例作为参数提供给它来做到这一点。
最后,我们需要将由Gtk.Builder
构造的窗口添加到我们的Gtk.Application
中。这个窗口现在由我们的Gtk.Builder
实例控制。虽然它不是一个完整的Gtk.ApplicationWindow
,但就控制新窗口而言,它非常像一个完整的Gtk.ApplicationWindow
。注意,我们使用window.show_all()
而不是window.present()
方法来显示窗口,因为我们的新窗口没有present()
方法。
真的就这么简单。文件浏览器窗口立即出现,您可以关闭并运行。剩下要做的就是填充所有的信号处理程序方法,为Gtk.TreeView
小部件创建存储,构建窗口初始化代码,这样就有了一个可以工作的应用。
测试你的理解能力
这两个练习对于你成为一个熟练的 GTK+ 开发者尤为重要。以编程方式设计大型应用的每个方面是不现实的,因为这需要太长时间。
相反,你应该使用 Glade 来设计用户界面,并使用Gtk.Builder
来加载设计和连接信号。通过这样做,您能够快速完成应用的图形化方面,并获得使您的应用工作的后端代码。
练习 1: Glade 文本编辑器
本练习实现了 Glade 中“测试您的理解”练习 1 部分的文本编辑器。文本编辑器中的工具栏应该完全在 Glade 中实现。
如果你还有上一章的练习解决方案,这个练习应该不需要额外的编码。你也可以在本书的网站 www.gtkbook.com
找到“测试你的理解”部分的答案。这个练习让您有机会了解 Glade 3,并测试许多小部件属性。
在设计了带有工具栏的应用后,添加菜单栏是一个简单的过渡。在较大的应用中,您应该向用户提供这两个选项。在以下练习中,您将向文本编辑器应用添加一个菜单栏。
练习 2:带菜单的 Glade 文本编辑器
您已经实现了带有菜单栏的文本编辑器。在本练习中,使用 Glade 和 Gtk.Builder 重新设计该练习中的应用。首先,您应该使用 Python 和 GTK+ 实现菜单,这允许您同时使用这两种语言。其次,您应该在 Glade 中再次实现该菜单。
与上一个练习一样,练习 2 的解在 www.gtkbook.com
。使用可下载的解决方案可以让你跳过编写回调函数,因为你已经在前一章中完成了。
摘要
在这一章中,我们暂时停止了编码,研究了设计图形用户界面时需要考虑的问题。简而言之,你必须时刻记住你的用户。你需要知道你的用户期望什么,并在应用的每个方面满足他们的需求。
接下来,您学习了如何使用 Glade 3 设计图形用户界面。当考虑 GUI 工具包时,快速部署应用的图形方面的能力是必要的,GTK+ 有 Glade 来满足这一需求。
Glade 允许您设计用户界面的各个方面,包括小部件属性、布局和信号处理程序。用户界面保存为描述应用结构的可读 XML 文件。
在 Glade 3 中设计了一个应用后,可以用Gtk.Builder
动态加载用户界面。这个 GTK+ 类解析 Glade 用户界面,并在运行时创建所有必要的小部件。它还提供了将 Glade 中声明的信号处理程序连接到应用中的回调方法的函数。
在下一章,我们将回到编码,并深入研究GObject
系统的复杂性。您将学习如何通过派生新的小部件和类来创建自己的GObject
类,以及如何从头开始创建一个小部件。
十二、自定义小部件
到目前为止,您已经了解了很多关于 GTK+ 及其支持库的知识。您已经掌握了足够的知识,可以使用 PyGTK 提供的小部件来创建自己的复杂应用。
然而,你还没有学会的一件事是如何创建你自己的小部件。因此,本章致力于从现有的 GTK+ 类中派生新的类。我们将通过一些例子向您展示使用 PyGTK 实现这一点是多么容易。
在本章中,您将学习如何从 GTK+ 小部件中派生出新的类和小部件。我们提供了几个如何做到这一点的例子,并讨论了在这个过程中可能遇到的一些问题。
图像/标签按钮
从 GTK+ 3.1 开始,所有库存物品都被弃用。虽然我同意这个决定,但我对Gtk.Button
没有被扩展到包含一个按钮选项来显示图像和文本感到失望。去掉 use-stock 属性后,Gtk.Button
只能显示文本或图像,但不能同时显示两者。
解决这个问题的方法很容易实现,但是非常重复,而且根本不是面向对象的。您可以在“使用按钮”一节中看到一个如何实现该变通方法的示例。你可以很容易地看到,如果你有很多按钮要编码,这个解决方案将是非常重复的,并且你没有很好地利用这个实现的代码重用。
另一个争论点是程序员被迫从字符串中查找他们想要的真实图像。如果新的实现为您做了这些工作,并且您需要向新的小部件提供的只是查找字符串,那会怎么样呢?毕竟,您可能希望使用来自用户默认主题的图像,所以让新的小部件来完成所有工作。
图 12-1 显示了清单 12-1 所示程序创建的图像标签按钮。这个简单的实现展示了如何扩展标准Gtk.Button
的功能和风格。
图 12-1
工作中的 ImageLabelButton
清单 12-1 展示了ImageLabelButton
的类实现。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class ImageLabelButton(Gtk.Button):
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL,
image="image-missing", label="Missing", *args,
**kwargs):
super().__init__(*args, **kwargs)
# now set up more properties
hbox = Gtk.Box(orientation, spacing=0)
if not isinstance(image, str):
raise TypeError("Expected str, got %s instead." % str(image))
icon_theme = Gtk.IconTheme.get_default()
icon = icon_theme.load_icon(image, -1,
Gtk.IconLookupFlags.FORCE_SIZE)
img = Gtk.Image.new_from_pixbuf(icon)
hbox.pack_start(img, True, True, 0)
img.set_halign(Gtk.Align.END)
if not isinstance(label, str):
raise TypeError("Expected str, got %s instead." % str(label))
if len(label) > 15:
raise ValueError("The length of str may not exceed 15 characters.")
labelwidget = Gtk.Label(label)
hbox.pack_start(labelwidget, True, True, 0)
labelwidget.set_halign(Gtk.Align.START)
self.add(hbox)
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(25)
button = ImageLabelButton(image="window-close", label="Close")
button.connect("clicked", self.on_button_clicked)
button.set_relief(Gtk.ReliefStyle.NORMAL)
self.add(button)
self.set_size_request(170, 50)
def on_button_clicked(self, button):
self.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self,
title="ImageLabelButton")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 12-1ImageLabelButton Class Implementation
要理解的第一点是,当创建一个Gtk.Button
时,按钮的样式是在分配 image 或 label 属性时设置的。一旦指定,按钮的样式就不能更改。新款ImageLabelButton
也是如此。
为了开始我们的讨论,让我们仔细看看小部件的初始化。我们允许两个新属性并覆盖一个现有属性。属性标签覆盖父属性,但使用方式与标签小部件的文本相同。属性方向和图像是新的。它们分别用于指定标签/图像的方向(水平或垂直)和字符串名称,以查找相应的默认主题图标。
其余的初始化代码很简单。使用默认方向或关键字参数指定的方向创建一个Gtk.Box
。接下来,如果指定了 image 关键字,则在默认用户主题中查找名称,获取图标,并将图像添加到Gtk.Box
。接下来,如果指定了标签,则创建一个Gtk.Label
,并将其添加到Gtk.Box
。最后,将框添加到按钮。
我们通过调整图像和标签文本的对齐方式改变了Gtk.ImageLabelButton
类,这样无论按钮的大小如何,它们都保持居中。我们使用了set_halign()
方法,并关闭了pack_start()
方法中使用的填充和扩展属性。
注意,我们没有覆盖底层Gtk.Button
的任何其他方法或属性。在这种情况下,没有必要以任何其他方式修改按钮。ImageLabelButton
表现得像普通的Gtk.Button
一样。因此,我们已经完成了创建一个新的按钮类的任务。
最重要的是,新类中有一些错误检测代码来捕捉无效的数据类型和值。您提供这种参数检查,这一点怎么强调都不为过。缺少适当的错误消息和适当的错误检测会毁掉您为新类所做的所有工作,因为它没有提供足够的调试信息来纠正甚至是很小的错误或问题,这将导致您的类被废弃。
自定义消息对话框
子类化 GTK+ 小部件的另一个原因是通过将更多的行为集成到小部件中来节省工作。例如,在显示对话框之前,标准的 GTK+ 对话框需要大量的初始化工作。通过将标准的外观和感觉集成到所有的消息对话框中,您可以解决重复的工作量。
减少创建对话框所需工作量的方法是创建一个包含您需要的所有功能的设计,使用默认设置或可以激活附加选项/值的参数。在清单 12-2 中,让我们看看一个定制的问题对话框,看看它是如何工作的。
class ooQuestionDialog(Gtk.Dialog):
hbox = None
vbox = None
def __init__(self, title="Error!", parent=None,
flags=Gtk.DialogFlags.MODAL, buttons=("NO",
Gtk.ResponseType.NO, "_YES",
Gtk.ResponseType.YES)):
super().__init__(title=title, parent=parent, flags=flags,
buttons=buttons)
self.vbox = self.get_content_area()
self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
icon_theme = Gtk.IconTheme.get_default()
icon = icon_theme.load_icon("dialog-question", 48,
Gtk.IconLookupFlags.FORCE_SVG)
image = Gtk.Image.new_from_pixbuf(icon)
self.hbox.pack_start(image, False, False, 5)
self.vbox.add(self.hbox)
def set_message(self, message, add_msg=None):
self.hbox.pack_start(Gtk.Label(message), False, False, 5)
if add_msg != None:
expander = Gtk.Expander.new_with_mnemonic( \ "_Click
me for more information.")
expander.add(Gtk.Label(add_msg))
self.vbox.pack_start(expander, False, False, 10)
def run(self):
self.show_all()
response = super().run()
self.destroy()
return response
Listing 12-2A Customized Question Dialog Implementation
这个对话框有一个预定义的设计,对于我们所有的消息对话框都是通用的。它包含以下元素。
-
每种类型的消息对话框都有单独的类。
-
对话框总是包含一个图标。显示的图标取决于显示的对话框类型(消息、信息、错误等)。).
-
对话框总是显示主要消息。
-
显示的按钮的数量和类型有一个逻辑默认值,用户可以覆盖它。
-
所有对话框默认为模态。
-
对话框中还会显示一条附加消息。它包含在一个扩展器中,可以在对话框显示的任何时候使用。
-
类别提供了两个额外的方法。第一种方法
set_message()
,设置主要对话消息和可选的附加消息。第二个方法,run()
,显示对话框,运行对话框,销毁对话框,并返回response_id
。如果您想要显示一个非模态对话框,那么run()
方法是可选的。当然,你必须在run()
对话框中提供额外的功能来实现。
实例化和运行对话框非常简单。以下代码执行打开对话框所需的所有任务。
dialog = ooQuestionDialog(parent=parentwin)
dialog.set_message("This is a test message.\nAnother line.",
add_msg="An extra message line.”)
response = dialog.run()
很明显,将自定义设计加载到对话框中既有优点也有缺点。主要的缺点是将设计和功能结合在一起。最大的好处是,如果你想改变设计,只有一个地方可以修改。
从这个例子中,用户可以很容易地为错误、消息、信息和警告对话框创建类似的子类。请记住,一致性是这项任务的关键。
多线程应用
多线程应用是任何高端 GTK+ 应用的核心,它是任何利用数据库、网络通信、客户机-服务器活动、进程间通信和任何其他使用长时间运行事务的进程的应用。所有这些应用都需要多个进程或线程来管理与独立实体之间的通信,以便相互提供和接收信息。
GTK+ 是一个单线程库。从多个线程访问其 API 不是线程安全的。所有 API 调用必须来自应用的主线程。这意味着长时间运行的事务会使用户界面看起来冻结,有时会持续很长时间。
解决这个问题的关键是将所有长时间运行的事务转移到其他线程。但是,这并不容易,因为它涉及到设置线程和为两个或多个线程或进程提供某种类型的线程安全通信。
大多数关于图形用户界面的书籍通常会忽略这个问题,而专注于图形用户界面本身。这对读者来说是一种极大的伤害,因为读者在职业生涯中遇到的几乎所有 GUI 应用都是多线程的,但是读者对这种类型的应用没有经验。
本书提供了一个示例,让您更好地了解多线程应用的样子以及如何组织它的基础知识。该示例不是构建多线程应用的唯一方法,但它确实提供了这种应用的所有基础。对于您的项目,细节和方法可能有所不同,但是您遵循的是我们的示例所提供的相同的基本大纲。
清单 12-3 是多线程应用的例子。这是一个非常简单的程序,它从另一个线程请求信息,主线程正确地等待供应商线程提供数据。我们在清单之后详细描述了这个例子。
#!/usr/bin/python3
import sys, threading, queue, time
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
def dbsim(q1, q2):
while True:
data = q1.get()
# the request is always the same for our purpose
items = {'lname':"Bunny", 'fname':"Bugs",
'street':"Termite Terrace", 'city':"Hollywood",
'state':"California", 'zip':"99999", 'employer':"Warner
Bros.", 'position':"Cartoon character", 'credits':"Rabbit
Hood, Haredevil Hare, What's Up Doc?"}
q2.put(items)
q1.task_done()
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.lname = None
self.fname = None
self.street = None
self.city = None
self.state = None
self.zip = None
self.employer = None
self.position = None
self.credits = None
self.q1 = queue.Queue()
self.q2 = queue.Queue()
self.thrd = threading.Thread(target=dbsim, daemon=True,
args=(self.q1, self.q1, self.q2))
self.thrd.start()
# window setup
self.set_border_width(10)
grid = Gtk.Grid.new()
grid.set_column_spacing(5)
grid.set_row_spacing(5)
# name
label = Gtk.Label.new("Last name:")
label.set_halign(Gtk.Align.END)
grid.attach(label, 0, 0, 1, 1)
self.lname = Gtk.Entry.new()
grid.attach(self.lname, 1, 0, 1, 1)
label = Gtk.Label.new("First name:")
label.set_halign(Gtk.Align.END)
grid.attach(label, 2, 0, 1, 1)
self.fname = Gtk.Entry.new()
grid.attach(self.fname, 3, 0, 1, 1)
# address
label = Gtk.Label.new("Street:")
label.set_halign(Gtk.Align.END)
grid.attach(label, 0, 1, 1, 1)
self.street = Gtk.Entry.new()
grid.attach(self.street, 1, 1, 1, 1)
label = Gtk.Label.new("City:")
label.set_halign(Gtk.Align.END)
grid.attach(label, 2, 1, 1, 1)
self.city = Gtk.Entry.new()
grid.attach(self.city, 3, 1, 1, 1)
label = Gtk.Label.new("State:")
label.set_halign(Gtk.Align.END)
grid.attach(label, 0, 2, 1, 1)
self.state = Gtk.Entry.new()
grid.attach(self.state, 1, 2, 1, 1)
label = Gtk.Label.new("Zip:")
label.set_halign(Gtk.Align.END)
grid.attach(label, 2, 2, 1, 1)
self.zip = Gtk.Entry.new()
grid.attach(self.zip, 3, 2, 1, 1)
# employment status
label = Gtk.Label.new("Employer:")
label.set_halign(Gtk.Align.END)
grid.attach(label, 0, 3, 1, 1)
self.employer = Gtk.Entry.new()
grid.attach(self.employer, 1, 3, 1, 1)
label = Gtk.Label.new("Position:")
label.set_halign(Gtk.Align.END)
grid.attach(label, 2, 3, 1, 1)
self.position = Gtk.Entry.new()
grid.attach(self.position, 3, 3, 1, 1)
label = Gtk.Label.new("Credits:")
label.set_halign(Gtk.Align.END)
grid.attach(label, 0, 4, 1, 1)
self.credits = Gtk.Entry.new()
grid.attach(self.credits, 1, 4, 3, 1)
# buttons
bb = Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL)
load_button = Gtk.Button.new_with_label("Load")
bb.pack_end(load_button, False, False, 0)
load_button.connect("clicked", self.on_load_button_clicked)
save_button = Gtk.Button.new_with_label("Save")
bb.pack_end(save_button, False, False, 0)
save_button.connect("clicked", self.on_save_button_clicked)
cancel_button = Gtk.Button.new_with_label("Cancel")
bb.pack_end(cancel_button, False, False, 0)
cancel_button.connect("clicked", self.on_cancel_button_clicked)
# box setup
vbox = Gtk.Box.new(orientation=Gtk.Orientation.VERTICAL,
spacing=5) vbox.add(grid)
vbox.add(bb)
self.add(vbox)
def on_cancel_button_clicked(self, button):
self.destroy()
def on_load_button_clicked(self, button):
self.q1.put('request')
# wait for the results to be
queued data = None
while Gtk.events_pending() or data ==
None: Gtk.main_iteration()
try:
data = self.q2.get(block=False)
except queue.Empty:
continue
self.lname.set_text(data['lname'])
self.fname.set_text(data['fname'])
self.street.set_text(data['street'])
self.city.set_text(data['city'])
self.state.set_text(data['state'])
self.zip.set_text(data['zip'])
self.employer.set_text(data['employer'])
self.position.set_text(data['position'])
self.credits.set_text(data['credits'])
self.q2.task_done()
def on_save_button_clicked(self, button):
self.lname.set_text("")
self.fname.set_text("")
self.street.set_text("")
self.city.set_text("")
self.state.set_text("")
self.zip.set_text("")
self.employer.set_text("")
self.position.set_text("")
self.credits.set_text("")
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Multi-Thread")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 12-3Multithreaded Application
在我们详细检查清单之前,让我们描述一下应用需求,看看我们是如何满足这些需求的。
我们的应用模拟了一个数据库客户机和一个服务器——都在一个多线程程序中。主窗口向线程服务器请求数据,并等待响应。服务器等待请求,然后将数据返回给客户端。应用的客户端是一个简单的 GTK+ 应用,显示从服务器获取的数据。服务器是在一个线程中运行的单个 Python 函数。它等待一个请求,提供数据,然后等待下一个请求。
所有这些的关键是 GTK+ 客户机不会冻结,不管服务器需要多长时间将数据返回给客户机。这允许应用(和所有其他应用)继续处理桌面事件。
让我们开始检查顶部的清单——dbsim
服务器函数,它代表数据库模拟器。我们让这个函数尽可能简单,以揭示基本的功能。该代码是一个无限循环,它等待一个事务出现在队列中。q1.get()
尝试从队列中读取事务,并在事务可用时等待返回。dbsim
不处理交易数据;相反,它只是构建一个 Python 字典。然后用q2.put(items)
将字典放在返回队列中。最后,处理返回到永久循环的顶部,并等待下一个事务。
这里显示的解决方案对于单个客户端工作得很好,但是当多个客户端试图访问服务器时就会崩溃,因为没有办法将客户端请求与返回的数据同步。我们需要增强应用来提供这种级别的同步。
如果您想从服务器尝试更长的事务时间,可以在q1.get()
和q2.put(items)
语句之间插入一个time.sleep()
语句。这证明了客户端在长时间运行的事务中不会冻结。
现在让我们看看客户端是如何工作的。客户端是一个标准的 GTK+ 应用,除了on_load_button_clicked()
方法。该方法访问数据库模拟器线程,以获取信息来填写主窗口上显示的输入字段。第一个任务是将请求发送到数据库模拟器。它通过将请求放在模拟器读取的队列中来实现这一点。
现在我们到了困难的部分。我们如何在不使主线程休眠的情况下等待返回的信息?我们通过将方法放在一个循环中来处理未决事件,直到从服务器获得信息。让我们来看看这个紧密的循环。
while Gtk.events_pending() or data == None:
Gtk.main_iteration()
try:
data = self.q2.get(block=False)
except queue.Empty:
continue
while
语句通过检查是否有待处理的 GTK+ 事件以及数据是否已经放入目标变量来开始循环。如果任一条件为True
,则进入紧循环。接下来,我们处理一个 GTK+ 事件(如果准备好了的话)。接下来,我们尝试从服务器获取数据。self.q2.get(block=False)
是非阻塞请求。如果队列是空的,那么会引发一个异常,然后被忽略,因为我们需要继续循环,直到数据可用。
一旦成功获取数据,on_load_button_clicked()
方法继续用提供的信息填充显示的输入字段。
这个谜题还有一个部分。看看创建服务器线程的语句。
self.thrd = threading.Thread(target=dbsim, daemon=True, args=(self.q1, self.q2))
该语句的关键部分是daemon=True
参数,它允许线程等待主线程结束,当主线程结束时,它会杀死服务器线程,以便应用优雅地结束。
这个应用示例具有两个线程之间通信的所有基础。我们有两个请求和返回数据的队列。我们有一个线程来执行客户端所需的所有长时间运行的事务。最后,我们有一个在等待服务器信息时不会死机的客户机。这是多线程 GUI 应用的基本架构。
对齐部件的正确方式
在 GTK+ 3.0 之前,对齐小部件的正确方式是通过Gtk.Alignment
类。从 GTK+ 3.0 开始,这个类就被弃用了,因此似乎消除了对齐小部件的简单方法。但事实上,Gtk.Widget
类中有两个方法可以对齐任何容器中的小部件:halign()
和valign()
方法。
这些方法易于使用,并在 90%的情况下提供程序员希望的对齐类型。清单 12-4 展示了如何使用Gtk.Widget
对齐方法产生由halign()
和valign()
方法提供的所有对齐类型。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs) :
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.resize(300, 100)
# create a grid
grid1 = Gtk.Grid()
grid1.height = 2
grid1.width = 2
grid1.set_column_homogeneous(True)
grid1.set_row_homogeneous(True)
self.add(grid1)
# build the aligned labels
label1 = Gtk.Label('Top left Aligned')
label1.can_focus = False
label1.set_halign(Gtk.Align.START)
label1.set_valign(Gtk.Align.START)
grid1.attach(label1, 0, 0, 1, 1)
label2 = Gtk.Label('Top right Aligned')
label2.can_focus = False
label2.set_halign(Gtk.Align.END)
label2.set_valign(Gtk.Align.START)
grid1.attach(label2, 1, 0, 1, 1)
label3 = Gtk.Label('Bottom left Aligned')
label3.can_focus = False
label3.set_halign(Gtk.Align.START)
label3.set_valign(Gtk.Align.END)
grid1.attach(label3, 0, 1, 1, 1)
label4 = Gtk.Label('Bottom right Aligned')
label4.can_focus = False
label4.set_halign(Gtk.Align.END)
label4.set_valign(Gtk.Align.END)
grid1.attach(label4, 1, 1, 1, 1)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
gtk_version = float(str(Gtk.MAJOR_VERSION)+'.'+str(Gtk.MINOR_VERSION))
if gtk_version < 3.16:
print('There is a bug in versions of GTK older that 3.16.')
print('Your version is not new enough to prevent this bug from')
print('causing problems in the display of this solution.')
exit(0)
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self,
title="Alignment")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 12-4Aligning Widgets
当您运行此示例时,您会看到显示了四种不同的校准,如图 12-2 所示。
图 12-2
对齐示例
下面的代码片段展示了如何将单个标签小部件与Gtk.Grid
单元格的左上角对齐。
label1.set_halign(Gtk.Align.START)
label1.set_valign(Gtk.Align.START)
正如您所看到的,对齐一个小部件非常简单,并且开销减少了,因为我们没有为每个对齐的小部件调用一个新的类。这种对齐小部件的方法应该足以满足大多数应用的需求。
摘要
本章给出了三个小部件定制示例,这些示例应该为您创建自己的定制小部件提供了足够的信息。提高应用的可用性和质量还有很多可能性。
十三、更多 GTK 部件
到目前为止,你已经学会了这本书要教你的几乎所有东西。然而,有许多小部件不太适合前面的章节。因此,本章涵盖了这些小部件。
前两个小部件用于绘图,命名为Gtk.DrawingArea
和Gtk.Layout
。这两个窗口小部件非常相似,除了Gtk.Layout
窗口小部件允许你在其中嵌入任意的窗口小部件,还可以使用函数来绘图。
此外,您还将了解支持自动完成和日历的Gtk.Entry
小部件。最后,向您介绍了 GTK+ 2.10 中添加的小部件,包括状态图标、打印支持和最近文件管理器。
在本章中,您将学习以下内容。
-
如何使用绘图小工具
Gtk.DrawingArea
和Gtk.Layout
-
如何使用
Gtk.Calendar
小部件来跟踪一年中几个月的信息 -
如何使用 GTK+ 2.10 中引入的提供最近文件跟踪、打印支持和状态图标的小部件
-
如何通过应用
Gtk.EntryCompletion
对象在Gtk.Entry
小部件中实现自动完成
绘制小部件
Gtk.DrawingArea
只提供一个方法Gtk.DrawingArea.new()
,它不接受任何参数,返回一个新的绘图区小工具。
Gtk.DrawingArea.new()
要开始使用小部件,您只需要使用父小部件Gdk.Window
提供的在区域上绘图。记住一个Gdk.Window
对象也是一个Gdk.Drawable
对象。
Gtk.DrawingArea
的一个优势是它来源于Gtk.Widget
,这意味着它可以连接到 GDK 事件。您希望将绘图区域连接到许多事件。您首先要连接到 realize,这样您就可以处理小部件实例化时需要执行的任何任务,比如创建 GDK 资源。当您必须处理小部件大小的变化时,"configure-event"
信号会通知您。另外,"expose-event"
允许您在暴露之前隐藏的部分时重新绘制小部件。"expose-event"
信号尤其重要,因为如果你想让绘图区域的内容在"expose-event"
回调中保持不变,你必须重新绘制它的内容。最后,您可以连接到按钮和鼠标点击事件,以便用户可以与小部件交互。
注意
要接收某些类型的事件,您需要将它们添加到受widget.add_events()
支持的小部件事件列表中。此外,为了接收用户的键盘输入,您需要设置widget.set_can_focus(True)
标志,因为只有聚焦的小部件可以检测按键。
绘图区域示例
清单 13-1 使用Gtk.DrawingArea
小部件实现了一个简单的绘图程序。自从引入 GTK+ 3 以来,Cairo 绘图库已经取代了 GTK+ 早期版本中使用的旧绘图原语。这个库与旧的图元不同,它使用矢量图形来绘制形状,而不是使用手绘技术。矢量图形很有趣,因为它们在调整大小或变换时不会失去清晰度。
图 13-1 是这个应用的截图。
图 13-1
带有用鼠标绘制的文本的绘图区域小工具
虽然这是一个非常简单的程序,但是它展示了如何与Gtk.DrawingArea
小部件交互。
#!/usr/bin/python3
import sys
import cairo
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
SIZE = 30
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_size_request(450, 550)
drawingarea = Gtk.DrawingArea()
self.add(drawingarea)
drawingarea.connect('draw', self.draw)
def triangle(self, ctx):
ctx.move_to(SIZE, 0)
ctx.rel_line_to(SIZE, 2 * SIZE)
ctx.rel_line_to(-2 * SIZE, 0)
ctx.close_path()
def square(self, ctx):
ctx.move_to(0, 0)
ctx.rel_line_to(2 * SIZE, 0)
ctx.rel_line_to(0, 2 * SIZE)
ctx.rel_line_to(-2 * SIZE, 0)
ctx.close_path()
def bowtie(self, ctx):
ctx.move_to(0, 0)
ctx.rel_line_to(2 * SIZE, 2 * SIZE)
ctx.rel_line_to(-2 * SIZE, 0)
ctx.rel_line_to(2 * SIZE, -2 * SIZE)
ctx.close_path()
def inf(self, ctx):
ctx.move_to(0, SIZE)
ctx.rel_curve_to(0, SIZE, SIZE, SIZE, 2 * SIZE, 0)
ctx.rel_curve_to(SIZE, -SIZE, 2 * SIZE, -SIZE, 2 * SIZE, 0)
ctx.rel_curve_to(0, SIZE, -SIZE, SIZE, -2 * SIZE, 0)
ctx.rel_curve_to(-SIZE, -SIZE, -2 * SIZE, -SIZE, -2 * SIZE, 0)
ctx.close_path()
def draw_shapes(self, ctx, x, y, fill):
ctx.save()
ctx.new_path()
ctx.translate(x + SIZE, y + SIZE)
self.bowtie(ctx)
if fill:
ctx.fill()
else:
ctx.stroke()
ctx.new_path()
ctx.translate(3 * SIZE, 0)
self.square(ctx)
if fill:
ctx.fill()
else:
ctx.stroke()
ctx.new_path()
ctx.translate(3 * SIZE, 0)
self.triangle(ctx)
if fill:
ctx.fill()
else:
ctx.stroke()
ctx.new_path()
ctx.translate(3 * SIZE, 0)
self.inf(ctx)
if fill:
ctx.fill()
else:
ctx.stroke()
ctx.restore()
def fill_shapes(self, ctx, x, y):
self.draw_shapes(ctx, x, y, True)
def stroke_shapes(self, ctx, x, y):
self.draw_shapes(ctx, x, y, False)
def draw(self, da, ctx):
ctx.set_source_rgb(0, 0, 0)
ctx.set_line_width(SIZE / 4)
ctx.set_tolerance(0.1)
ctx.set_line_join(cairo.LINE_JOIN_ROUND)
ctx.set_dash([SIZE / 4.0, SIZE / 4.0], 0)
self.stroke_shapes(ctx, 0, 0)
ctx.set_dash([], 0)
self.stroke_shapes(ctx, 0, 3 * SIZE)
ctx.set_line_join(cairo.LINE_JOIN_BEVEL)
self.stroke_shapes(ctx, 0, 6 * SIZE)
ctx.set_line_join(cairo.LINE_JOIN_MITER)
self.stroke_shapes(ctx, 0, 9 * SIZE)
self.fill_shapes(ctx, 0, 12 * SIZE)
ctx.set_line_join(cairo.LINE_JOIN_BEVEL)
self.fill_shapes(ctx, 0, 15 * SIZE)
ctx.set_source_rgb(1, 0, 0)
self.stroke_shapes(ctx, 0, 15 * SIZE)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Drawing Areas")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 13-1The Drawing Area Widget
理解如何使用 Cairo 的最好方法是想象你是一个艺术家,正在用画笔在画布上画出一个形状。
首先,你可以选择画笔的一些特征。你可以选择画笔的粗细和你想要的颜色。您也可以选择画笔笔尖的形状。你可以选择圆形或方形。
一旦你选择了画笔,你就可以开始绘画了。在描述你想要呈现的形象时,你必须非常精确。
首先,决定你想把画笔放在画布上的什么地方。您可以通过提供 x 和 y 坐标来实现这一点。接下来,定义您希望画笔描边看起来是什么样子——弧线、直线等等。最后,再次通过提供 x 和 y 坐标来定义您希望笔画结束的点。三角形和正方形非常好做!
通过使用 Cairo 的 Python 接口,使用上述主题的变体和一些附加内容生成更复杂的图形,如填充(着色)、变换(放大、移动)等。
几乎所有的工作都围绕着使用cairo.Context
(或 Cairo C API 中的cairo_t
)。这是您向其发送绘图命令的对象。有几个选项可以用不同的方式初始化这个对象。
非常重要的一点是,要知道你描述你的图形的坐标和你显示你的图形的坐标是不同的。做演示时,你事先在透明醋酸纤维上画画,然后在高射投影仪上显示出来。Cairo 称之为用户空间坐标的透明醋酸盐和设备空间坐标的投影图像。
当初始化 Cairo 上下文对象时,我们告诉它应该如何显示我们的描述。为此,我们提供了一个转换矩阵。修改变换矩阵会导致一些非常有趣的结果。
Cairo 最强大的特性之一是它可以输出许多不同格式的图形(它可以使用多个后端)。为了打印,我们可以让 Cairo 将我们的图形翻译成 postscript 发送给打印机。对于屏幕显示,Cairo 可以将我们的图形转换成 gtk 可以理解的硬件加速渲染!它有许多更重要和有用的目标后端。在初始化cairo.Context
时,我们设置它的目标后端,提供一些细节(比如颜色深度和大小),如下例所示。
布局小部件
除了Gtk.DrawingArea
,GTK+ 还提供了另一个画图小部件,名为Gtk.Layout
。这个小部件实际上是一个容器,与Gtk.DrawingArea
的不同之处在于,它不仅支持绘图原语,还支持子小部件。此外,Gtk.Layout
本身提供了滚动支持,所以在添加到滚动窗口时不需要视口。
注意
布局需要注意的一个重要区别是你应该绘制到Gtk.Layout
的 bin_window 成员,而不是Gtk.Widget
的窗口。例如,您需要绘制到父二进制窗口,而不是布局窗口。你可以通过调用layout.get_bin_window()
方法获得二进制窗口。这允许子部件被正确地嵌入到部件中。
新的Gtk.Layout
小部件是用Gtk.Layout.new()
创建的,它接受水平和垂直调整。如果您将None
传递给两个函数参数,就会为您创建调整。由于Gtk.Layout
有原生的滚动支持,当你需要使用滚动窗口时,它比Gtk.DrawingArea
有用得多。
然而,Gtk.Layout
确实增加了一些开销,因为它也能够包含小部件。正因为如此,如果你只需要在小部件的Gdk.Window
上画画的话,Gtk.DrawingArea
是更好的选择。
使用layout.put()
将子部件添加到一个Gtk.Layout
容器中,它将子部件放置在容器的左上角。由于Gtk.Layout
是直接从Gtk.Container
派生出来的,所以它能够支持多个子节点。
layout.put(child_widget, x, y)
稍后可以调用layout.move()
将子小部件重新定位到Gtk.Layout
容器中的另一个位置。
警告
因为您将子部件放置在特定的水平和垂直位置,Gtk.Layout
呈现出与Gtk.Fixed
相同的问题。使用布局小部件时,您需要小心这些!你可以在第四章的“固定容器”一节中阅读更多关于Gtk.Fixed
小部件问题的内容。
最后,如果您想要强制布局为特定的大小,您可以向layout.set_size()
发送新的宽度和高度参数。您应该使用此方法而不是layout.set_size_request()
,因为它也调整调整参数。
layout.set_size(width, height)
此外,与大小请求不同,布局大小函数需要无符号数字。这意味着您必须为布局小部件指定一个绝对大小。这个大小应该是布局的总大小,包括窗口小部件在屏幕上不可见的部分,因为它们超出了滚动区域的边界!Gtk.Layout
小工具的大小默认为 100×100 像素。
日历
GTK+ 提供了Gtk.Calendar
小部件,这是一个显示日历中一个月的小部件。它允许用户使用滚动箭头在月份和年份之间移动,如图 13-2 所示。您还可以显示所选年份的日名称和周编号的三个字母缩写。
图 13-2
Gtk。日历小部件
使用Gtk.Calendar.new()
创建新的Gtk.Calendar
小部件。默认情况下,选择当前日期。因此,计算机存储的当前月份和年份也会显示出来。您可以使用calendar.get_date()
检索所选日期,或者使用calendar.select_day()
选择新的日期。要取消选择当前选定的日期,您应该使用日期值为零的calendar.select_day()
。
为了定制Gtk.Calendar
小部件的显示方式以及它与用户的交互方式,您应该使用calendar.set_display_options()
来设置一个由Gtk.CalendarDisplayOptions
值组成的按位列表。以下是此枚举的非预定值。
-
Gtk.CalendarDisplayOptions.SHOW_HEADING
:如果设置,显示月份和年份的名称。 -
Gtk.CalendarDisplayOptions.SHOW_DAY_NAMES
:如果设置,则在相应的日期栏上方显示每一天的三个字母的缩写。它们呈现在标题和主日历内容之间。 -
Gtk.CalendarDisplayOptions.SHOW_DETAILS
:提供详细信息时,仅显示 a。参见calendar.set_detail_func()
。 -
Gtk.CalendarDisplayOptions.NO_MONTH_CHANGE
:阻止用户更改日历的当前月份。如果没有设置这个标志,您将看到允许您转到下个月或上个月的箭头。默认情况下,箭头处于启用状态。 -
Gtk.CalendarDisplayOptions.SHOW_WEEK_NUMBERS
:沿日历左侧显示当年的周数。默认情况下,周数是隐藏的。
除了选择一天之外,您还可以用calendar.mark_day()
一次标记任意多天。如果成功标记了日期,该函数将返回True
。
calendar.mark_day(day)
有两种信号可用于检测用户何时选择一天。当用户用鼠标或键盘选择新的一天时,发出第一个信号"day-selected"
。当用户双击选择某一天时,会发出"day-selected-double-click"
信号。这意味着在大多数情况下,您不应该需要带有Gtk.Calendar
小部件的"button-press-event"
信号。
打印支持
GTK+ 2.10 引入了许多新的小部件和对象,为这个库增加了打印支持。虽然这个 API 中有许多对象,但在大多数情况下,您只需要直接与Gtk.PrintOperation
交互,这是一个可以跨多个平台使用的高级打印 API。它充当处理大多数打印操作的前端接口。
在本节中,我们实现了一个应用,它打印用户在Gtk.FileChooserButton
小部件中选择的文本文件的内容。图 13-3 是 Linux 系统上默认打印对话框的截图。用户使用Gtk.FileChooserButton
小部件从磁盘中选择一个文件,然后单击主窗口中的打印按钮打开该对话框。
图 13-3
打印对话框
清单 13-2 从为应用定义必要的数据结构和设置用户界面开始。PrintData
类保存关于当前打印作业的信息,这有助于呈现最终产品。Widgets
是一个简单的结构,在回调方法中提供对多个小部件和打印作业信息的访问。
#!/usr/bin/python3
import sys
import math
from os.path import expanduser
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('PangoCairo', '1.0')
from gi.repository import Gtk, cairo, Pango, PangoCairo
class Widgets:
def __init__(self):
self.window = None
self.chooser = None
self.data = None
self.settings = Gtk.PrintSettings.new()
class PrintData:
def __init__(self):
self.filename = None
self.fontsize = None
self.lines_per_page = None
self.lines = None
self.total_lines = None
self.total_pages = None
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.HEADER_HEIGHT = 20.0
self.HEADER_GAP = 8.5
w = Widgets()
w.window = self
self.set_border_width(10)
w.chooser = Gtk.FileChooserButton.new ("Select a File",
Gtk.FileChooserAction.OPEN)
w.chooser.set_current_folder(expanduser("~"))
print = Gtk.Button.new_with_label("Print")
print.connect("clicked", self.print_file, w)
hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 5)
hbox.pack_start(w.chooser, False, False, 0)
hbox.pack_start(print, False, False, 0)
self.add(hbox)
def print_file(self, button, w):
filename = w.chooser.get_filename()
if filename == None:
return
operation = Gtk.PrintOperation.new()
if w.settings != None:
operation.set_print_settings(w.settings)
w.data = PrintData()
w.data.filename = filename
w.data.font_size = 10.0
operation.connect("begin_print", self.begin_print, w)
operation.connect("draw_page", self.draw_page, w)
operation.connect("end_print", self.end_print, w)
res = operation.run(Gtk.PrintOperationAction.PRINT_DIALOG,
w.window)
if res == Gtk.PrintOperationResult.APPLY:
if w.settings != None:
w.settings = None
settings = operation.get_print_settings()
elif res == Gtk.PrintOperationResult.ERROR:
dialog = Gtk.MessageDialog.new(w.window,
Gtk.DialogFlags.DESTROY_WITH_PARENT,
Gtk.MessageType.ERROR,
Gtk.ButtonsType.S_CLOSE,
"Print operation error.")
dialog.run()
dialog.destroy()
def begin_print(self, operation, context, w):
w.data.lines = []
f = open(w.data.filename)
for line in f:
w.data.lines.append(line)
f.close()
w.data.total_lines = len(w.data.lines)
height = context.get_height() - self.HEADER_HEIGHT –
self.HEADER_GAP w.data.lines_per_page = math.floor(height /
(w.data.font_size + 3)) w.data.total_pages =
(w.data.total_lines - 1) / w.data.lines_per_page+1
operation.set_n_pages(w.data.total_pages)
def draw_page(self, operation, context, page_nr, w):
cr = context.get_cairo_context()
width = context.get_width()
layout = context.create_pango_layout()
desc = Pango.font_description_from_string("Monospace")
desc.set_size(w.data.font_size * Pango.SCALE)
layout.set_font_description(desc)
layout.set_text(w.data.filename, -1)
layout.set_width(-1)
layout.set_alignment(Pango.Alignment.LEFT)
(width, height) = layout.get_size()
text_height = height / Pango.SCALE
cr.move_to(0, (self.HEADER_HEIGHT - text_height) / 2)
PangoCairo.show_layout(cr, layout)
page_str = "%d of %d" % (page_nr + 1, w.data.total_pages)
layout.set_text(page_str, -1)
(width, height) = layout.get_size()
layout.set_alignment(Pango.Alignment.RIGHT)
cr.move_to(width - (width / Pango.SCALE),
(self.HEADER_HEIGHT - text_height) / 2)
PangoCairo.show_layout(cr, layout)
cr.move_to(0, self.HEADER_HEIGHT + self.HEADER_GAP)
line = page_nr * w.data.lines_per_page
i = 0
while i < w.data.lines_per_page and line <
w.data.total_lines:
layout.set_text(w.data.lines[line], -1)
PangoCairo.show_layout(cr, layout)
cr.rel_move_to(0, w.data.font_size + 3)
line += 1
i += 1
def end_print(self, operation, context, w):
w.data.lines = None
w.data = None
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self,
title="Calendar")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 13-2GTK+ Printing Example
在清单 13-2 中的AppWindow
类的顶部定义了两个值,称为HEADER_HEIGHT
和HEADER_GAP
。HEADER_HEIGHT
是可用于呈现标题文本的空间量。这将显示信息,如文件名和页码。HEADER_GAP
是在页眉和实际页面内容之间放置填充。
PrintData
类存储关于当前打印作业的信息。这包括文件在磁盘上的位置、字体大小、单页上可以呈现的行数、文件内容、总行数和总页数。
打印操作
下一步是实现点击打印按钮时运行的print_file
回调方法。该方法在清单 13-2 中实现。它负责创建PrintData
,连接所有必要的信号,并创建打印操作。
打印的第一步是创建一个新的打印操作,这是通过调用Gtk.PrintOperation.new()
来完成的。Gtk.PrintOperation
的独特之处在于它使用平台的本地打印对话框(如果有的话)。在像 UNIX 这样不提供这种对话框的平台上,使用Gtk.PrintUnixDialog
或 GNOME 对话框。
注意
对于大多数应用,您应该尽可能使用Gtk.PrintOperation
方法,而不是直接与打印对象交互。Gtk.PrintOperation
是作为独立于平台的打印解决方案创建的,如果没有大量的代码,就无法轻松地重新实施。
下一步是调用operation.set_print_settings()
将打印设置应用到操作中。在这个应用中,Gtk.PrintSettings
对象作为属性存储在Widgets
类实例中。如果打印操作成功,您应该存储当前的打印设置,以便这些相同的设置可以应用于将来的打印作业。
然后,通过分配一个新实例来设置PrintData
类。文件名被设置为Gtk.FileChooserButton
中当前选中的文件,该文件已经被确认存在。打印字体大小也设置为 10.0 磅。在文本编辑应用中,你通常会从Gtk.TextView
的当前字体中检索这个字体。在更复杂的打印应用中,整个文档中的字体大小可能会有所不同,但这只是一个简单的示例,只是为了帮助您入门。
接下来,我们连接三个Gtk.PrintOperation
信号,这将在本节稍后详细讨论。简而言之,begin_print
是在页面渲染之前调用的,可以用来设置页数,做必要的准备。打印作业中的每一页都要调用draw_page
信号,这样才能呈现出来。最后,在打印操作完成后调用end_print
信号,而不管它是成功还是失败。这个回调方法在打印作业之后进行清理。在整个印刷操作中可以使用许多其他信号。完整列表见附录 b。
一旦设置好打印操作,下一步就是通过调用operation.run()
开始打印。此方法用于定义打印操作执行的任务。
operation.run(action, parent)
下面列表中显示的Gtk.PrintOperationAction
枚举定义了打印操作执行的打印任务。要打印文档,您应该使用Gtk.PrintOperationAction.PRINT_DIALOG
。
-
Gtk.PrintOperationAction.ERROR
:打印操作中出现某种错误。 -
Gtk.PrintOperationAction.PREVIEW
:预览以当前设置执行的打印作业。它使用与打印操作相同的回调函数进行渲染,因此启动并运行它应该不需要太多工作。 -
Gtk.PrintOperationAction.PRINT
:使用当前打印设置开始打印,不显示打印对话框。只有当您百分之百确定用户同意此操作时,您才应该这样做。例如,您应该已经向用户显示了一个确认对话框。 -
Gtk.PrintOperationAction.EXPORTPRINT
:将打印作业导出到文件。要使用此设置,您必须在运行操作之前设置 export-filename 属性。
operation.run()
的最后两个参数允许你定义一个父窗口用于打印对话框,使用 None 忽略这个参数。该函数直到所有页面都被渲染并发送到打印机后才返回。
当函数返回控制时,它返回一个Gtk.PrintOperationResult
枚举值。这些值指示您下一步应该执行什么任务,以及打印操作是成功还是失败。下面的列表中显示了四个枚举值。
-
Gtk.PrintOperationResult.ERROR
:打印操作中出现某种错误。 -
Gtk.PrintOperationResult.APPLY
:打印设置被更改。因此,应该立即存储它们,以免丢失更改。 -
Gtk.PrintOperationResult.CANCEL
:用户取消了打印操作,您不应保存对打印设置的更改。 -
Gtk.PrintOperationResult.PROGRESS
:打印操作尚未完成。只有在异步运行任务时,才能获得该值。
可以异步运行打印操作,这意味着operation.run()
可能会在页面呈现之前返回。这是用operation.set_allow_async()
设定的。你要注意,不是所有的平台都允许这个操作,所以你要做好这个不行的准备!
如果异步运行打印操作,可以使用 done 信号在打印完成时检索通知。此时,您得到了打印操作结果,您需要相应地处理它。
处理完打印操作结果后,如果设置了错误并且存在错误,还应该处理结果错误。
在附录 e 中可以找到Gtk.PrintError
域名下可能出现的错误的完整列表。
Gtk.PrintOperation
提供的一个独特功能是能够在打印操作运行时显示进度对话框。这个默认是关闭的,但是可以用operation.set_show_progress()
打开。如果您允许用户同时运行多个打印操作,这尤其有用。
operation.set_show_progress(boolean)
有时可能需要取消当前的打印作业,这可以通过调用operation.cancel()
来完成。这个函数通常在begin_print
、paginate
或draw_page
回调方法中使用。它还允许您提供一个取消按钮,以便用户可以在活动的打印操作中间停止。
operation_cancel()
也可以为打印作业指定一个唯一的名称,以便在外部打印监控应用中识别它。打印作业以operation.set_job_name()
命名。如果未设置,GTK+ 会自动为打印作业指定一个名称,并相应地对连续的打印作业进行编号。
如果您正在异步运行打印作业,您可能想要检索打印作业的当前状态。通过调用operation.get_status()
,返回一个Gtk.PrintStatus
枚举值,它给出了关于打印作业状态的更多信息。以下是可能的打印作业状态值列表。
-
Gtk.PrintStatus.INITIAL
:打印操作尚未开始。当打印对话框仍然可见时,将返回此状态,因为这是默认的初始值。 -
Gtk.PrintStatus.PREPARING
:打印操作被分割成页,发出开始打印信号。 -
页面正在被渲染。这在发出绘图页信号时设置。此时没有数据发送到打印机。
-
Gtk.PrintStatus.SENDING_DATA
:关于打印作业的数据正在发送到打印机。 -
Gtk.PrintStatus.PENDING
:所有的数据都已发送到打印机,但作业尚未处理。打印机有可能停止工作。 -
打印过程中出现了一个问题。例如,打印机可能缺纸,或者可能卡纸。
-
Gtk.PrintStatus.PRINTING
:打印机当前正在处理打印作业。 -
Gtk.PrintStatus.FINISHED
:打印作业已成功完成。 -
Gtk.PrintStatus.FINISHED_ABORTED
:打印作业被中止。除非您再次运行该作业,否则不会采取进一步的操作。
由operation.get_status()
返回的值可以在应用中使用,因为它是一个数值。然而,GTK+ 还提供了用operation.get_status_string()
检索字符串的能力,这是一个人类可读的打印作业状态描述。它用于调试输出或向用户显示有关打印作业的更多信息。例如,它可以显示在状态栏或消息对话框中。
开始打印操作
既然打印操作已经设置好了,是时候实现必要的信号回调方法了。当用户开始打印时,会发出“开始打印”信号,这意味着从用户的角度来看,所有设置都已完成。
在清单 13-2 中,begin_print
回调方法首先检索文件的内容,并将其分成若干行。然后计算总行数,可以检索页数。
要计算打印操作所需的页数,您需要计算每页可以呈现多少行。用context.get_height()
检索每个页面的总高度,存储在一个Gtk.PrintContext
对象中。Gtk.PrintContext
存储关于如何绘制页面的信息。例如,它存储页面设置、宽度和高度尺寸以及两个方向上每英寸的点数。我们将在本章后面的draw_page
回调方法中进行更详细的讨论。
一旦获得了可用于呈现文本的页面总高度,下一步就是将该高度除以文本的字体大小,再加上每行之间要添加的 3 个像素的间距。floor()
函数向下舍入每页的行数,这样就不会在整个页面的底部出现剪裁。
一旦你知道了每页的行数,你就可以计算出页数。然后,您必须在这个回调方法结束时将这个值发送给operation.set_n_pages()
。使用页数是为了让 GTK+ 知道调用draw_page
回调方法的次数。该值必须设置为正值,以便在更改默认值–1 之前不会开始渲染。
呈现页面
下一步是实现draw_page
回调方法,需要呈现的每个页面都会调用一次。这个回调方法需要引入一个名为 Cairo 的库。它是一个矢量图形库,除了其他功能之外,还可以呈现打印操作。
清单 13-2 从用context.get_cairo_context()
检索当前Gtk.PrintContext
的 Cairo 绘图上下文开始。返回的上下文对象呈现打印内容,然后将其应用于PangoLayout
。
在这个回调方法的开始,我们还需要从Gtk.PrintContext
中检索另外两个值。第一个是context.get_width()
,返回文档的宽度。请注意,我们不需要检索页面的高度,因为我们已经计算了适合每页的行数。如果文本比页面宽,则会被裁剪。您必须修改这个示例,以避免剪切文档。
警告
由Gtk.PrintContext
返回的宽度以像素为单位。您需要小心,因为不同的函数可能会使用不同的比例,如 Pango 单位或点!
下一步是用context.create_pango_layout()
创建一个PangoLayout
,用于打印上下文。您应该以这种方式为打印操作创建 Pango 布局,因为打印上下文已经应用了正确的字体度量。
该函数执行的下一个操作是将文件名添加到页面的左上角。首先,layout.set_text()
将布局存储的当前文本设置为文件名。布局的宽度设置为–1,这样文件名就不会在正斜杠字符处换行。文本也与布局左侧的layout.set_alignment()
对齐。
现在文本已经添加到布局中,cr.move_to()
将 Cairo 上下文中的当前点移动到页面的左侧和页眉的中心。请注意,PangoLayout
的高度必须先降低一个Pango.SCALE
的系数!
cairo.move_to(x, y)
接下来,我们调用cr.show_layout()
在 Cairo 上下文上绘制PangoLayout
。布局的左上角呈现在 Cairo 上下文中的当前点。这就是为什么首先需要用cr.move_to()
移动到想要的位置。
cairo.show_layout(layout)
在呈现文件名之后,相同的方法将页数添加到每个页面的右上角。你应该再次注意到由PangoLayout
返回的宽度必须被Pango.SCALE
缩小,这样它就可以和其他的 Cairo 值使用相同的单位。
下一步是呈现当前页面的所有行。我们首先移动到页面的左边,标题下面的单元。然后,用cr.show_layout()
将每一行递增地呈现给 Cairo 上下文。值得注意的一点是,循环中的光标位置是用cr.rel_move_to()
移动的。
cairo.rel_move_to(dx, dy)
这个函数相对于前一个位置移动当前位置。因此,在呈现一行之后,当前位置向下移动一行,这等于文本的字体大小,因为字体是等宽的。
小费
通过相对于前一个位置移动光标,很容易在每行文本和相邻文本之间添加任意数量的间距,只要在计算begin_print
回调方法中的页数时已经考虑了这个额外的高度。
当使用 GTK+ 开发时,您可以使用整个 Cairo 库。更多的基础知识将在本章的“开罗绘画背景”一节中介绍;但是,如果您在自己的应用中实现打印,您应该花时间从 Cairo API 文档中了解更多关于这个库的信息。
完成打印操作
在所有页面都被渲染后,发出"end-print"
信号。清单 13-2 显示了用于信号的end_print
回调方法。它重置PrintData
实例的修改属性。
开罗绘画背景
Cairo 是一个图形渲染库,在整个 GTK+ 库中使用。在这本书的上下文中,Cairo 在打印操作期间呈现页面。本节将向您介绍 Pycairo 库以及一些与它们相关的类和绘图方法。
GTK+ 中打印操作的页面被呈现为 Cairo 上下文对象。此对象允许您呈现文本、绘制各种形状和线条,以及用颜色填充裁剪区域。让我们来看看 Cairo 提供的几个用于操作 Cairo 绘图上下文的方法。
绘制路径
Cairo 上下文中的形状是用路径呈现的。用cairo.new_path()
创建一个新路径。然后,您可以使用cairo.copy_path()
检索新路径的副本,并向该路径添加新的线条和形状。
cairo.copy_path()
为绘制路径提供了许多功能,如表 13-1 所列。关于每个函数的更多信息可以在 Cairo API 文档中找到。
表 13-1
Cairo 路径绘制方法
|方法
|
描述
|
| --- | --- |
| cairo.arc()
| 在当前路径中绘制弧线。您必须提供弧的半径、其中心的水平和垂直位置,以及以弧度表示的曲线的开始和结束角度。 |
| cairo.curve_to()
| 在当前路径中创建一条贝塞尔曲线。您必须提供曲线的终点位置和计算曲线的两个控制点。 |
| cairo.line_to()
| 从当前位置到指定点画一条线。如果初始点不存在,则简单地移动当前位置。 |
| cairo.move_to()
| 移动到上下文中的新位置,这将创建一个新子路径。 |
| cairo.rectangle()
| 在当前路径中绘制一个矩形。您必须提供矩形左上角的坐标、宽度和高度。 |
| cairo.rel_curve_to()
| 该功能与cairo.curve_to()
相同,只是它是相对于当前位置绘制的。 |
| cairo.rel_line_to()
| 该功能与cairo.line_to()
相同,只是它是相对于当前位置绘制的。 |
| cairo.rel_move_to()
| 该功能与cairo.move_to()
相同,只是它是相对于当前位置绘制的。 |
当您完成一个子路径时,您可以用cairo.path_close()
关闭它。这会封闭当前路径,以便在必要时可以用颜色填充。
渲染选项
在源上进行绘图操作所使用的当前颜色是cairo.set_source_rgb()
。在设置新颜色之前,将一直使用该颜色。除了选择颜色,您还可以使用cairo.set_source_rgba()
,它接受第五个 alpha 参数。每个颜色参数都是一个介于 0.0 和 1.0 之间的浮点数。
移动到特定点并设置源颜色后,可以用cairo.fill()
填充当前路径,它只接受上下文。或者,你可以用cairo.fill_extents()
填充一个矩形区域。此函数计算角点为(x1,y1)和(x2,y2)的区域,填充这些点之间的所有区域,这些区域也包含在当前路径中。
cairo.fill_extents(x1, y1, x2, y2)
绘制操作(如曲线)会导致边缘变得参差不齐。为了解决这个问题,Cairo 用cairo.set_antialias()
为图形提供了抗锯齿功能。
cairo.set_antialias(antialias)
抗锯齿设置由cairo.Antialias
枚举提供。下面是此枚举提供的值列表。
-
cairo.Antialias.DEFAULT
:使用默认的抗锯齿算法。 -
cairo.Antialias.NONE
:不发生抗锯齿;取而代之的是使用 alpha 蒙版。 -
cairo.Antialias.GRAY
:仅使用单一颜色进行抗锯齿。这种颜色不一定是灰色,而是根据前景色和背景色选择的。 -
cairo.Antialias.SUBPIXEL
:使用 LCD 屏幕提供的子像素阴影。
这只是对开罗绘画背景的一个简短介绍。关于 Cairo 的更多信息,您可以参考位于 www.cairographics.org
的其 API 文档。
最近的文件
在 GTK+ 2.10 中,引入了一个新的 API,允许您跨应用跟踪最近打开的文件。在本节中,我们将在简单的文本编辑应用中实现这一功能。这个带有最近文件选择器的应用如图 13-4 所示。稍后,在本章的练习中,您将为您的文本编辑器添加最近文件支持。
图 13-4
最近文件选择器对话框
清单 13-3 中的代码设置了文本编辑应用。两个按钮允许您使用Gtk.FileChooserDialog
打开现有文件并保存您的更改。
然后,有一个Gtk.MenuToolButton
提供了两个功能。点击该按钮时,会显示一个Gtk.RecentChooserDialog
,允许您从列表中选择一个最近的文件。Gtk.MenuToolButton
小部件中的菜单属于Gtk.RecentChooserMenu
类型,它显示了最近的十个文件。
#!/usr/bin/python3
import sys
import urllib
from urllib.request import pathname2url
import os
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Pango
class Widgets():
def __init__(self):
self.window = None
self.textview = None
self.recent = None
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
w = Widgets()
w.window = self
self.set_border_width(5)
self.set_size_request(600, 400)
w.textview = Gtk.TextView.new()
fd = Pango.font_description_from_string("Monospace 10")
self.modify_font(fd)
swin = Gtk.ScrolledWindow.new(None, None)
openbutton = Gtk.Button.new_with_label("open")
save = Gtk.Button.new_with_label("Save")
icon_theme = Gtk.IconTheme.get_default()
icon = icon_theme.load_icon("document-open", -1,
Gtk.IconLookupFlags.FORCE_SIZE)
image = Gtk.Image.new_from_pixbuf(icon)
w.recent = Gtk.MenuToolButton.new(image, "Recent Files")
manager = Gtk.RecentManager.get_default()
menu = Gtk.RecentChooserMenu.new_for_manager(manager)
w.recent.set_menu(menu)
menu.set_show_not_found(False)
menu.set_local_only(True)
menu.set_limit(10)
menu.set_sort_type(Gtk.RecentSortType.MRU)
menu.connect("selection-done", self.menu_activated, w)
openbutton.connect("clicked", self.open_file, w)
save.connect("clicked", self.save_file, w)
w.recent.connect("clicked", self.open_recent_file, w)
hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 5)
hbox.pack_start(openbutton, False, False, 0)
hbox.pack_start(save, False, False, 0)
hbox.pack_start(w.recent, False, False, 0)
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 5)
swin.add(w.textview)
vbox.pack_start(hbox, False, False, 0)
vbox.pack_start(swin, True, True, 0)
w.window.add(vbox)
def save_file(self, save, w):
filename = w.window.get_title()
buffer = w.textview.get_buffer()
(start, end) = buffer.get_bounds()
content = buffer.get_text(start, end, False)
f = open(filename, 'w')
f.write(content)
f.close()
def menu_activated(self, menu, w):
filename = menu.get_current_uri()
if filename != None:
fn = os.path.basename(filename)
f = open(fn, 'r')
contents = f.read()
f.close()
w.window.set_title(fn)
buffer = w.textview.get_buffer()
buffer.set_text(content, -1)
else:
print("The file '%s' could not be read!" % filename)
def open_file(self, openbutton, w):
dialog = Gtk.FileChooserDialog(title="Open File", parent=w.window,
action=Gtk.FileChooserAction.OPEN,
buttons=("Cancel", Gtk.ResponseType.CANCEL,"Open", Gtk.ResponseType.OK))
if dialog.run() == Gtk.ResponseType.OK:
filename = dialog.get_filename()
content = ""
f = open(filename, 'r')
content = f.read()
f.close()
if len(content) > 0:
# Create a new recently used
resource. data = Gtk.RecentData()
data.display_name = None
data.description = None
data.mime_type = "text/plain"
data.app_name =
os.path.basename(__file__)
data.app_exec = " " + data.app_name +
"%u" #data.groups = ["testapp", None]
data.is_private = False
url = pathname2url(filename)
# Add the recently used resource to the default
recent manager. manager =
Gtk.RecentManager.get_default()
result = manager.add_full(url, data)
# Load the file and set the filename as the title of
the window. w.window.set_title(filename)
buffer =
w.textview.get_buffer()
buffer.set_text(content,-1)
dialog.destroy()
def open_recent_file(self, recent, w):
manager = Gtk.RecentManager.get_default()
dialog = Gtk.RecentChooserDialog(title="Open Recent File",
parent=w.window,
recent_manager=manager,
buttons=("Cancel",
Gtk.ResponseType.CANCEL,
"Open",
Gtk.ResponseType.OK))
# Add a filter that will display all of the files in
the dialog. filter = Gtk.RecentFilter.new()
filter.set_name("All Files")
filter.add_pattern("*") dialog.add_filter(filter)
# Add another filter that will only display plain
text files. filter = Gtk.RecentFilter.new()
filter.set_name("Plain Text")
filter.add_mime_type("text/plain")
dialog.add_filter(filter)
dialog.set_show_not_found(False)
dialog.set_local_only(True)
dialog.set_limit(10)
dialog.set_sort_type(Gtk.RecentSortType.MRU)
if dialog.run() == Gtk.ResponseType.OK:
filename = dialog.get_current_uri()
if filename != None:
# Remove the "file://" prefix from the beginning of the
# URI if it exists.
content = ""
fn = os.path.basename(filename)
f = open(fn, 'r')
contents = f.read()
f.close()
if len(content) > 0:
w.window.set_title(fn)
buffer = w.textview.get_buffer()
buffer.set_text(content, -1)
else:
print("The file '%s' could not be read!" % filename)
dialog.destroy()
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Recent Files")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 13-3Remembering Recently Opened Files
名为Gtk.RecentManager
的中央类处理最近的文件信息。从头开始创建自己的文件是可能的,但是如果您想在应用之间共享最近的文件,您可以使用Gtk.RecentManager.get_default()
检索默认文件。这允许你与应用共享最近的文件,比如 gedit,GNOME 最近的文档菜单,以及其他利用Gtk.RecentManager
API 的应用。
接下来,我们从默认的Gtk.RecentManager
创建一个新的Gtk.RecentChooserMenu
小部件。该菜单显示最近的文件,并对用Gtk.RecentChooserMenu.new_for_manager()
创建的菜单项进行编号。默认情况下,文件没有编号,但是可以通过将“显示编号”设置为True
或调用menu.set_show_numbers()
来更改该属性。
Gtk.RecentChooserMenu
实现了Gtk.RecentChooser
接口,它提供了与小部件交互所需的功能。在清单 13-3 中,许多Gtk.RecentChooser
属性定制了菜单。这些也适用于实现Gtk.RecentChooser
接口的另外两个小部件:Gtk.RecentChooserDialog
和Gtk.RecentChooserWidget
。
列表中最近的文件在添加后可能已经被删除。在这种情况下,您可能不想在列表中显示它们。您可以使用rchooser.set_show_not_found()
隐藏不再存在的最近文件。此属性仅适用于位于本地计算机上的文件。
小费
实际上,您可能希望向用户显示找不到的文件。如果用户选择了一个不存在的文件,在通知用户问题后,您可以轻松地将其从列表中删除。
默认情况下,只显示本地文件,这意味着它们有一个file://
统一资源标识符(URI)前缀。URI 是指基于前缀的事物,如文件位置或互联网地址。仅使用前缀file://
可以保证它们位于本地机器上。您可以将该属性设置为False
,以显示位于远程位置的最新文件。您应该注意,如果远程文件不再存在,它们不会被过滤掉!
如果列表中包含大量最近的文件,您可能不想在菜单中列出所有这些文件。有一百个项目的菜单是相当大的!因此,您可以使用recentchooser.set_limit()
设置菜单中显示的最近项目的最大数量。
recentchooser.set_limit(limit)
当您设置元素数量限制时,显示哪些文件取决于您用recentchooser.set_sort_type()
定义的排序类型。默认情况下,这被设置为Gtk.RecentSortType.NONE
。以下是Gtk.RecentSortType
枚举中的可用值。
-
Gtk.RecentSortType.NONE
:最近的文件列表根本不排序,按照它们出现的顺序返回。当您限制显示的元素数量时,不应该使用这种方法,因为您无法预测将显示哪些文件! -
Gtk.RecentSortType.MRU
:将最近添加的文件排在列表的第一位。这很可能是您想要使用的排序方法,因为它将最新的文件放在列表的开头。 -
Gtk.RecentSortType.LRU
:将最近最少添加的文件排在列表的第一位。 -
Gtk.RecentSortType.CUSTOM
:使用自定义排序功能对最近的文件进行排序。要使用这个,你需要recentmanager.set_sort_func()
来定义排序方法。
此示例的最后一部分以指定的名称保存文件。当在这个文本编辑器中打开一个文件时,窗口标题被设置为文件名。该文件名用于保存文件。因此,要小心,因为这个简单的文本编辑器不能用来创建新文件!
最近选择菜单
您已经了解了Gtk.RecentChooserMenu
小部件。清单 13-3 实现了与其连接的"selection-done"
回调方法。该函数检索选定的 URI 并打开文件(如果存在)。
您可以使用recentchooser.get_current_uri()
检索当前选择的最近文件,因为只能选择一个项目。因为我们将菜单限制为只显示本地文件,所以我们需要从 URI 中删除前缀file://
。如果您允许显示远程文件,您可能需要从 URI 中删除不同的前缀,例如http://
。您可以使用 Python 方法os.path.basename()
删除 URI 前缀。
os.path.basename(filename)
os.path.basename(filename)
前缀删除后,我们尝试打开文件。如果文件被成功打开,则窗口标题被设置为文件名,并且文件被打开;否则,将向用户显示文件无法打开的警告。
添加最近的文件
当按下打开按钮时,我们希望允许用户从Gtk.FileChooserDialog
中选择要打开的文件。如果文件被打开,它将被添加到默认的Gtk.RecentManager
。
如果文件成功打开,recentmanager.add_full()
会将其作为新的最近项目添加到默认的Gtk.RecentManager
中。要使用这种方法,您需要两件物品。首先,您需要 URI,它是通过将文件名附加到file://
来表示它是一个本地文件而创建的。这个文件名可以用从url
导入的pathname2url()
构建。
pathname2url(filepath)
其次,你需要一个Gtk.RecentData
类的实例。这个类的内容是一组属性,描述将文件信息存储到Gtk.RecentManager
所需的数据。display_name
显示一个简称而不是文件名,description
是文件的简短描述。这两个值都可以安全地设置为None
。
然后,您必须指定文件的 MIME 类型、应用的名称以及用于打开文件的命令行。您的应用的名称可以通过调用 Python 库方法os.path.basename(__file__)
来检索。有很多方法可以获得程序名,但是你也可以安全地将其设置为None
。
接下来,groups 是指定资源属于哪个组的字符串列表。您可以使用它来过滤掉不属于特定组的文件。
最后一个成员is_private
指定该资源是否可用于未注册它的应用。通过将此设置为True
,您可以阻止其他使用Gtk.RecentManager
API 的应用显示这个最近的文件。
一旦构建了Gtk.RecentData
实例,它就可以和最近的文件 URI 一起作为新资源添加到recentmanager.add_full()
中。您还可以使用recentmanager.add_item()
添加一个新的最近项目,这会为您创建一个Gtk.RecentData
对象。
要删除最近的项目,请调用recentmanager.remove_item()
。如果具有指定 URI 的文件被成功删除,该函数返回True
。如果不是,则在Gtk.RecentManagerError
域下设置一个错误。您也可以使用recentmanager.purge_items()
从列表中删除所有最近的项目。
recentmanagerremove_item(uri)
警告
您应该避免清除默认Gtk.RecentManager
中的所有项目!这将删除每个应用注册的最近项目,这可能是用户不希望看到的,因为您的应用不应该改变来自其他应用的最近资源。
最近选择器对话框
GTK+ 还提供了一个名为Gtk.RecentChooserDialog
的小部件,可以在一个方便的对话框中显示最近的文件。这个小部件实现了Gtk.RecentChooser
接口,所以它在功能上非常类似于Gtk.RecentChooserMenu
。在清单 13-3 中,open_recent_file
展示了如何允许用户用这个小部件打开一个最近的文件。
新的Gtk.RecentChooserDialog
小部件的创建方式类似于与Gtk.RecentChooserDialog()
的对话。这个函数接受一个对话框标题、一个父窗口、一个要显示的Gtk.RecentManager
小部件以及成对的按钮和响应标识符。
清单 13-3 介绍了最近的文件过滤器。用Gtk.RecentFilter.new()
创建新的Gtk.RecentFilter
对象。过滤器仅显示遵循已安装模式的最新文件。
filter.set_name("All Files")
filter.add_pattern("*")
dialog.add_filter(filter)
下一步是设置过滤器的名称。该名称显示在组合框中,用户可以在其中选择要使用的筛选器。有许多方法可以创建过滤器,包括使用filter.
add_pattern()
,它可以找到具有匹配模式的过滤器。星号字符可以用作通配符。还有用于匹配 MIME 类型、图像文件类型、应用名称、组名称和天数的函数。接下来,使用recentchooser.add_filter()
将Gtk.RecentFilter
添加到最近选择器中。
使用Gtk.RecentChooserDialog
小部件,可以使用recentchooser.set_select_multiple()
选择多个文件。如果用户可以选择多个文件,你要使用recentchooser.get_uris()
来检索所有选中的文件。
recentchooser.get_uris(length)
该函数还返回字符串列表中元素的数量。
自动完成
您在第五章中学习了Gtk.Entry
小部件,但是 GTK+ 也提供了Gtk.EntryCompletion
对象。Gtk.EntryCompletion
来源于GObject
,在Gtk.Entry
中为用户提供自动补全。图 13-5 显示了为用户提供多项选择的示例Gtk.Entry
。请注意,用户也可以选择忽略选项并输入任意字符串。
图 13-5
Gtk。自动完成
清单 13-4 实现了一个Gtk.Entry
小部件,要求您输入 GTK+ 小部件的名称。在Gtk.EntryCompletion
小部件中,与输入文本具有相同前缀的所有字符串都显示为选项。这个例子显示了自动完成并运行是多么容易。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
widgets = ["GtkDialog", "GtkWindow", "GtkContainer",
"GtkWidget"] self.set_border_width(10)
label = Gtk.Label.new("Enter a widget in the following GtkEntry:")
entry = Gtk.Entry.new()
# Create a GtkListStore that will hold autocompletion
possibilities. types = (GObject.TYPE_STRING,)
store = Gtk.ListStore.new(types) for widget in widgets:
iter = store.append() store.set(iter, 0, widget)
completion = Gtk.EntryCompletion.new()
entry.set_completion(completion)
completion.set_model(store)
completion.set_text_column(0)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
vbox.pack_start(label, False, False, 0)
vbox.pack_start(entry, False, False, 0)
self.add(vbox)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Automatic Completion")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 13-4Automatic Completion
要实现一个Gtk.EntryCompletion
,您需要首先创建一个新的Gtk.ListStore
来显示选择。本例中的模型只有一个文本列,但是只要其中一列的类型是GObject.TYPE_STRING
,就可以提供一个更复杂的Gtk.ListStore
。
用Gtk.EntryCompletion.new()
创建新的Gtk.EntryCompletion
对象。然后,您可以使用entry.set_completion()
将它应用到现有的Gtk.Entry
小部件。默认情况下,GTK+ 负责显示匹配和应用选择。
接下来,completion.set_model()
将树模型应用于Gtk.EntryCompletion
对象。如果已经有一个模型应用于该对象,它将被替换。您还必须使用completion.set_text_column()
来指定哪一列包含该字符串,因为模型不一定只有一列。如果不设置文本列,自动完成将不起作用,因为默认情况下文本列设置为–1。
可以显示所有与completion.set_inline_completion()
匹配的前缀。您应该注意到内联完成是区分大小写的,但是自动完成不是!如果你正在使用这个,你可能想要设置completion.set_popup_single_match()
,当只有一个匹配时,它阻止弹出菜单被显示。
您可以使用completion.set_popup_set_width()
强制弹出菜单与Gtk.Entry
小部件的宽度相同。这对应的是Gtk.EntryCompletion
的popupset_width
属性。
如果有很多匹配,您可能想用completion.set_minimum_key_length()
设置最小匹配长度。当列表中有如此大量的元素,以至于要花很长时间才能在屏幕上呈现列表时,这很有用。
测试你的理解能力
在本章的练习中,您将完成文本编辑应用,这是前几章中多次练习的重点。它要求您将自动完成、打印和最近文件功能集成到您的应用中。
练习 1:创建全文编辑器
在本练习中,您将完成在前几章中创建的文本编辑器。您向应用添加了三个新特性。
首先,添加自动补全功能,实现该功能是为了记住搜索工具栏中过去的搜索。应用必须记住过去的搜索,只搜索应用运行时的当前实例。接下来,添加打印支持,包括打印和打印预览功能。打印支持可以用高级的Gtk.PrintOperation
类轻松实现。最后,指示文本编辑器记住使用Gtk.RecentManager
类加载的最后五个文件。
这样你就不必重写应用的前几个方面,你应该使用第十一章练习的解决方案,或者从本书的官方网站下载该解决方案。
摘要
在这一章中,您学习了许多不太适合前几章的小部件。下面的列表总结了这些小部件和对象。
-
Gtk.DrawingArea
:一个空的小部件,允许你在它的Gdk.Window
对象上绘图,这个对象也是一个Gdk.Drawable
对象。 -
Gtk.Layout
:这个小部件类似于Gtk.DrawingArea
,除了它允许你在它的界面中嵌入小部件。它引入了额外的开销,所以如果您只想要绘图功能,就不应该|使用这个小部件。 -
Gtk.Calendar
:显示所选年份的单月。这个小部件允许用户选择一个日期,并且您可以以编程方式标记多个日期。 -
Gtk.PrintOperation
:独立于平台的高级打印 API。为实现打印支持提供了许多其他对象,但是大多数操作应该用Gtk.PrintOperation
类来处理,这样它就可以跨多个平台工作。 -
一个管理最近文件列表的简单类。这些列表可以跨应用共享。菜单和对话框小部件用于显示最近的文件。
-
Gtk.EntryCompletion
:为Gtk.Entry
widgets 提供自动完成支持。这些选项由一个填充了可能匹配的Gtk.ListStore
对象组成。
你现在已经学会了本书打算介绍的所有主题。在下一章中,您将看到五个完整的应用,它们利用了过去 12 章中讨论的主题。
十四、整合一切
到目前为止,您已经深入了解了使用 GTK+ 和相关技术可以做的一切。在本章中,我们将通过构建几个应用来运用这些知识。
本章介绍了五个完整的应用:在第十一章中设计的文件浏览器、一个计算器、一个 ping 工具、一个 hangman 游戏和一个日历。然而,本章不包含示例的源代码。本章中每个应用的代码可以从 www.gtkbook.com
下载。
我将通过提供其他学习资源的链接来结束这本书的最后一章,这样你就可以继续扩展你的 GTK+ 知识。
文件浏览器
在第十一章中,你在 Glade 中实现了一个文件浏览器应用的用户界面。用户界面动态加载,所有信号自动连接Gtk.Builder
。
在第十一章的结尾,你被告知回调方法将在这一章中实现,我们现在就要实现。图 14-1 显示了首次启动时的文件浏览器应用。它正在显示根文件夹。
图 14-1
使用 Gtk 的文件浏览器。TreeView
在这个应用中,文件浏览功能是特别重要的。它们与第九章的“练习 1:文件浏览器”部分非常相似。在那个练习中,您使用一个可以浏览用户文件系统的Gtk.TreeView
小部件创建了一个简单的应用。文件浏览器的当前位置存储在一个链表中,从这个链表中可以构建完整的路径。列表中的每个节点都是路径的一部分,目录分隔符放在每个字符串之间以构建完整的路径。还提供了一个Gtk.Entry
小部件,允许用户用键盘编辑路径。
可以使用几种不同的方法在文件系统中导航。可以在地址栏中输入位置,尽管必须在激活Gtk.Entry
小部件时验证位置的有效性。除了这种方法之外,用户还可以使用后退、前进、向上或主工具栏按钮来分别浏览浏览历史、移动到父目录或转到主目录。最后,Gtk.TreeView
的行激活信号允许用户移动到选定的目录或查看有关选定文件的信息。
窗口底部放置了一个Gtk.StatusBar
小工具。它跟踪当前目录中的项目总数以及这些项目的总大小。这个例子的源代码,以及本章中的其他四个应用,可以从 www.gtkbook.com
下载。
计算器
计算器是一个简单的应用,在大多数 GUI 编程书籍中都有实现。这个例子旨在向您展示实现一个计算器是多么容易。图 14-2 是应用的截图。
图 14-2
一个简单的计算器应用
这个计算器应用是用 Glade 设计的,所以用户界面完全不用代码就可以完成。因为本例中的大多数小部件都是Gtk.Button
小部件,所以只需要 clicked 和 destroy 信号。
该计算器允许用户输入带可选小数点的数字,执行四种基本运算(加、减、乘、除),求数字的反,以及计算平方根和指数。为了减少所需回调方法的数量,所有的数字和小数位都被连接到一个名为num_clicked()
的回调方法,四个基本操作和幂操作也相互连接。这允许您利用这些操作组需要大量相似代码才能工作的事实。
当单击一个数字或小数点按钮时,该字符将被追加到当前值的末尾,尽管数字的长度被限制为十位数。单击操作按钮时,执行操作,并存储新值。它还设置了一个名为clear_flag
的标志,告诉计算器当用户按下一个数字或小数位时,应该开始一个新的数字。
Ping 工具
在这个程序中,您将学习如何使用 GLib 库中的通道通过管道与应用进行通信。ping 工具显示在图 14-3 中;它允许用户 ping 一个地址特定的次数或不断,直到应用停止。
图 14-3
ping 工具应用
在这个应用中,GLib spawn_async_with_pipes()
函数用于派生带有指定地址的 ping 应用实例。这个函数接收到的 shell 命令用shell_parse_argv()
函数进行了解析,所以它的格式是正确的。Ping 按钮被禁用,这将阻止用户运行子进程的多个实例。
在生成子进程之后,输出管道用于创建一个新的通道对象,该对象监视管道读取数据。当数据准备好被读取时,它被解析,以便每个 ping 的统计数据可以显示在一个Gtk.TreeView
小部件中。这将持续指定的次数,或者直到用户停止子对象。
当子进程正在运行时,会启用一个停止按钮,这允许用户在子进程完成之前终止它。这个函数简单地调用下面的os.killpg()
函数的实例,强制子进程关闭。
当进程被杀死时,管道被破坏,这导致通道在 watch 函数中关闭。这确保了我们能够为下一个子流程重用同一个通道对象。
日历
本章的最后一个应用创建了一个为用户组织事件的日历。它使用Gtk.Calendar
小部件允许用户浏览日期。Gtk.TreeView
显示给定日期的事件。图 14-4 显示了该日历应用。
图 14-4
具有两个事件的日历应用
创建日历应用的大部分代码应该看起来非常熟悉,因为它使用了前面章节中介绍的功能。除了熟悉的功能之外,应用还使用 XML-SAX 提供的 XML 解析器来打开日历文件,这些文件存储为 XML 文件。清单 14-1 显示了包含一个事件的示例日历文件。
<calendar>
<event>
<name>Release of the Book</name>
<location>Everywhere</location>
<day>16</day>
<month>3</month>
<year>2007</year>
<start>All Day</start>
<end></end>
</event>
</calendar>
Listing 14-1Calendar File
通过单击“新建”工具栏按钮创建一个新日历,该按钮要求输入日历文件名和位置。每次添加或删除事件时都会保存日历,因此不提供保存按钮。您也可以通过按下打开工具栏按钮来打开现有日历。
标记解析器函数
为了打开日历,这个应用使用 XML-SAX 的解析器来检索文件的内容。这个解析器非常容易使用,并且支持基本的 XML 文件。使用解析器需要做的第一件事是定义一个新的xmlparser
对象。这个对象有很多属性,包括四个用户自定义函数,需要自己编码;我一次覆盖一个。这些功能中的任何一个都可以设置为None
。
第一个方法是StartElement()
,为每个打开的标签调用,比如
StartElement(name, attributes)
下一个方法EndElement()
是为每个结束标记调用的,比如和。对于没有结束标签的标签也调用它,比如
EndElement(name)
对于在StartElement()
和EndElement()
调用之间发现的数据,调用CharacterData()
方法。它接受两个标记之间的文本以及文本的长度。在日历应用中调用这个函数来读取事件的内容。
CharacterData(data)
注意
不仅包含字符串的标签会调用CharacterData()
方法,调用其他标签的标签也会调用该方法;因此,这个函数可能有一个填充空格和换行符的文本参数!
解析 XML 文件
XML 文本的解析是用一个xmlparser
对象完成的。您可以用xml.sax.parse(filename, contenthandler)
创建一个新的解析器:
xml.sax.parse(filename, contenthandler))
该函数创建并返回一个新的xmlparser
对象。
XML-SAX 还可以为您处理 XML 名称空间。有关更多信息,请参见文档。
更多资源
恭喜你!您现在已经读完了这本书,并且您已经了解了足够的知识来开发和管理复杂的 GTK+ 应用。然而,你可能想知道你该何去何从。当您开始自己开发应用时,有许多库和资源将变得不可或缺。
第一个资源是该书的网站( www.gtkbook.com
)。这个站点包含了 GTK + 开发者在线资源的链接,以及本书不适合的主题的教程。您可以将它作为寻找 GTK+ 应用开发帮助的起点。
另一个很好的资源是 GTK+ 网站( www.gtk.org
)。这个站点包括关于 GTK+ 的邮件列表、下载和错误跟踪的信息。您也可以在该网站上找到最新的文档。
GNOME 开发者的网站( http://developer.gnome.org
)也是了解更多信息的理想场所。除了 GTK+ 和它的支持库之外,还有许多其他的库用来为 GNOME 开发应用,您会不断地遇到这些库。下面的列表简要总结了其中的一些库。
-
PyGObject API Reference(
http://lazka.github.io/pgi-docs
)是一个一站式网站,提供与 Python、GNOME、GTK+、ATK、GDK 和许多其他库相关的所有内容。 -
py cairo API Reference(
http://pycairo.readthedocs.io/en/latest/reference/index.html
)中有针对 Cairo 的所有 Python APIs 的文档。 -
Python 网站(
www.python.org
)提供了 Python 2.x 和 3.x 所有版本的文档,其中包括参考资料、教程、操作指南、常见问题解答、PyPi 和有关 Python 软件基础的信息。
摘要
您已经熟悉了 GTK+ 的大部分内容及其支持库。这些知识可以用来为许多平台上的应用实现图形用户界面。
这本书旨在让您对 GTK+ 有一个透彻的了解,我希望在您开发应用时,它将继续成为一个有价值的资源。附录是 API 文档中未完全记录的主题不可或缺的参考资料;即使当你成为专家时,它们也可以被使用。最后一个附录提供了练习解决方案的简短描述和如何完成它们的提示。
既然你已经掌握了这些知识,实践和经验将帮助你成为一名优秀的图形应用开发人员。你已经拥有了继续独立生活所需的一切。我希望你读这本书的乐趣和我写这本书的乐趣一样多!