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+ 中最重要的简单部件。从那里,我们继续更复杂的话题。