PyGTK-开发基础知识-三-

41 阅读24分钟

PyGTK 开发基础知识(三)

原文:Foundations of PyGTK Development

协议:CC BY-NC-SA 4.0

八、文本视图小部件

本章教你如何使用Gtk.TextView小工具。文本视图小部件类似于Gtk.Entry小部件,只是它能够保存跨越多行的文本。滚动窗口允许文档存在于屏幕边界之外。

在学习Gtk.TextView之前,本章首先介绍几个新的小部件。前两个小部件是滚动窗口和视口。滚动窗口由两个滚动子部件的滚动条组成。一些小工具已经支持滚动,包括Gtk.LayoutGtk.TreeViewGtk.TextView。对于您想要滚动的所有其他小部件,您需要首先将它们添加到一个Gtk.Viewport小部件中,这将为其子小部件提供滚动能力。

在本章中,您将学习以下内容:

  • 如何使用滚动窗口和视窗

  • 如何使用Gtk.TextView小部件和应用文本缓冲区

  • 文本迭代器和文本标记在处理缓冲区时执行的功能

  • 将样式应用于整个或部分文档的方法

  • 如何在剪贴板上剪切、复制和粘贴

  • 如何在文本视图中插入图像和子部件

滚动窗口

在了解Gtk.TextView小部件之前,您需要了解两个名为Gtk.ScrolledWindowGtk.Viewport的容器小部件。滚动窗口使用两个滚动条来允许小部件占据比屏幕上可见的空间更多的空间。这个小部件允许Gtk.TextView小部件包含超出窗口边界的文档。

滚动窗口中的两个滚动条都有关联的Gtk.Adjustment对象。这些调整跟踪滚动条的当前位置和范围;但是,在大多数情况下,您不需要直接访问调整。

滚动条的Gtk.Adjustment保存了关于滚动范围、步数和当前位置的信息。value 变量是滚动条在边界之间的当前位置。此变量必须始终介于下限值和上限值之间,这是校正的界限。page_size是屏幕上一次可以看到的区域,取决于小工具的大小。step_incrementpage_increment变量用于按下箭头或向下翻页键时的步进。

图 8-1 是用清单 8-1 中的代码创建的窗口截图。两个滚动条都被启用,因为包含按钮的表格比可见区域大。

img/142357_2_En_8_Fig1_HTML.jpg

图 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_LEFTGtk.CornerType.BOTTOM_LEFTGtk.CornerType.TOP_RIGHTGtk.CornerType.BOTTOM_RIGHT,它们定义了内容相对于滚动条的位置。

警告

应该使用set_placement()的场合非常少见!在几乎所有可能的情况下,你都不应该使用这个函数,因为它会让用户感到困惑。除非您有充分的理由更改位置,否则请使用默认值。

可以通过调用set_shadow_type()来设置小部件相对于子小部件的阴影类型。

swin.set_shadow_type(type)

在第四章中,你学习了如何使用Gtk.ShadowType枚举和句柄框来设置放置在子部件周围的边框类型。与之前相同的值设置了滚动窗口的阴影类型。

在你设置了一个滚动窗口之后,你应该添加一个子部件来使用它。有两种可能的方法可以做到这一点,方法的选择基于子部件的类型。如果您使用的是Gtk.TextViewGtk.TreeViewGtk.IconViewGtk.ViewportGtk.Layout小部件,您应该使用默认的add()方法,因为这五个小部件都包含本地滚动支持。

所有其他 GTK+ 小部件都没有本地滚动支持。对于那些小部件,应该使用add_with_viewport()。这个函数通过首先将它打包到一个名为Gtk.Viewport的容器小部件中,为子控件提供滚动支持。这个小部件为缺少自身支持的子小部件实现了滚动功能。然后视口被自动添加到滚动窗口中。

警告

千万不要将Gtk.TextViewGtk.TreeViewGtk.IconViewGtk.ViewportGtk.Layout小部件打包到带有add_with_viewport()的滚动窗口中,因为小部件上的滚动可能无法正确执行!

可以手动将一个小部件添加到一个新的Gtk.Viewport,然后用add()将该视窗添加到一个滚动窗口,但是便利功能允许您完全忽略该视窗。

滚动窗口只是一个带有滚动条的容器。容器和滚动条本身都不执行任何操作。滚动是由子部件处理的,这就是为什么子部件必须已经有本地滚动支持才能正确使用Gtk.ScrolledWindow部件。

当您添加一个支持滚动的子部件时,会调用一个函数来为每个轴添加调整。除非子部件支持滚动,否则什么也不做,这就是为什么大多数部件都需要一个视口。当用户单击并拖动滚动条时,调整中的值会发生变化,这会导致发出值已更改的信号。此操作还会导致子小部件相应地呈现自身。

因为Gtk.Viewport小部件没有自己的滚动条,它完全依靠调整来定义它在屏幕上的当前位置。滚动条在Gtk.ScrolledWindow小部件中用作调整当前调整值的简单机制。

文本视图

Gtk.TextView小部件显示文档的多行文本。它提供了多种方式来定制整个文档或文档的单个部分。甚至可以将GdkPixbuf对象和子部件插入到文档中。Gtk.TextView是到目前为止您遇到的第一个合理涉及的小部件,所以本章的其余部分将致力于小部件的许多方面。这是一个非常通用的小部件,您需要在许多 GTK+ 应用中使用。

本章的前几个例子可能会让你认为Gtk.TextView只能显示简单的文档,但事实并非如此。它还可以显示各种应用使用的多种类型的富文本、文字处理和交互式文档。您将在接下来的章节中学习如何做到这一点。

图 8-2 向您介绍了一个简单的文本视图窗口,允许您输入文本并进行一些基本的布局设计。但是它也没有太多的功能,而且缺少许多文字处理器的功能。

img/142357_2_En_8_Fig2_HTML.jpg

图 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 中使用的值,感受一下它们的作用。

img/142357_2_En_8_Fig3_HTML.jpg

图 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.LEFTGtk.Justification.RIGHTGtk.Justification.CENTERGtk.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.TextIterGtk.TextMark。GTK + 提供了在这两种类型的对象之间进行转换的函数。

文本迭代器表示缓冲区中两个字符之间的位置。在缓冲区内操作文本时会用到它们。文本迭代器带来的问题是,当编辑文本缓冲区时,它们会自动失效。即使插入了相同的文本,然后又从缓冲区中删除,文本迭代器也会失效,因为迭代器应该在堆栈中分配并立即使用。

为了跟踪文本缓冲区中的位置变化,提供了Gtk.TextMark对象。操纵缓冲区时,文本标记保持不变,并根据缓冲区的操纵方式移动它们的位置。您可以使用get_iter_at_mark()检索指向文本标记的迭代器,这使得标记非常适合跟踪文档中的位置。

get_iter_at_mark(iter, mark)

文本标记就像是文本中不可见的光标,根据文本的编辑方式改变位置。如果在标记前添加文本,它将向右移动,以便保持在相同的文本位置。

默认情况下,文本标记的重力设置为向右。这意味着它会随着文本的添加而向右移动。让我们假设标记周围的文本被删除。标记移动到被删除文本两侧的两段文本之间的位置。然后,如果在文本标记处插入文本,由于其右侧的重力设置,它将保持在插入文本的右侧。这类似于光标,因为当插入文本时,光标保持在插入文本的右侧。

小费

默认情况下,文本中的文本标记是不可见的。但是,您可以通过调用set_visible()Gtk.TextMark设置为可见,这将放置一个竖线来指示它所在的位置。

可以通过两种方式访问文本标记。您可以在特定的Gtk.TextIter位置检索文本标记。还可以用字符串作为名称来设置文本标记,这使得标记易于跟踪。

GTK+ 总是为每个Gtk.TextBuffer : insertselection_bound提供两个默认文本标记。插入文本标记指的是光标在缓冲区中的当前位置。selection_bound文本标记是指如果有选中文本,则选中文本的边界。如果没有选择文本,这两个标记指向相同的位置。

操作缓冲区时,"insert""selection_bound"文本标记非常有用。可以操纵它们来自动选择或取消选择缓冲区中的文本,并帮助您确定文本在缓冲区中的逻辑插入位置。

编辑文本缓冲区

GTK+ 提供了大量检索文本迭代器和操作文本缓冲区的函数。在这一节中,您将看到清单 8-4 中使用的一些最重要的方法,然后还会向您介绍更多方法。图 8-4 显示了一个用Gtk.TextBuffer插入和检索文本的应用。

img/142357_2_En_8_Fig4_HTML.jpg

图 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 显示了一个带有输入字段和按钮的文本视图,可以通过文本视图对象访问剪贴板功能。

img/142357_2_En_8_Fig5_HTML.jpg

图 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 所示。当用户单击“查找”按钮时,该示例开始。

img/142357_2_En_8_Fig6_HTML.jpg

图 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 所示。

img/142357_2_En_8_Fig7_HTML.jpg

图 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对象被插入到文本缓冲区中。

img/142357_2_En_8_Fig8_HTML.jpg

图 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

img/142357_2_En_8_Fig9_HTML.jpg

图 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 的屏幕截图。它打开了行编号、语法突出显示、括号匹配和行突出显示。

img/142357_2_En_8_Fig10_HTML.jpg

图 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.TreePathGtk.TreeIterGtk.TreeRowReference来引用Gtk.TreeView中的行

  • 如何处理双击、单行选择和多行选择

  • 如何创建可编辑的树视图单元格或使用单元格渲染器函数自定义单个单元格

  • 可以嵌入到单元格中的小部件,包括切换按钮、像素缓冲器、微调按钮、组合框、进度条和键盘快捷键字符串

树视图的一部分

Gtk.TreeView小部件用于显示组织成列表或树的数据。视图中显示的数据被组织成列和行。用户能够使用鼠标或键盘在树形视图中选择一行或多行。使用Gtk.TreeView的 Nautilus 应用的截图如图 9-1 所示。

img/142357_2_En_9_Fig1_HTML.jpg

图 9-1

使用 Gtk。TreeView 部件

Gtk.TreeView是一个很难使用的小部件,更难理解,所以这一整章都致力于使用它。但是,一旦您理解了小部件的工作原理,您就能够将它应用到各种各样的应用中,因为几乎可以定制小部件显示给用户的方式的每个方面。

Gtk.TreeView的独特之处在于它遵循了通常被称为模型-视图-控制器(MVC)设计的设计概念。MVC 是一种信息和呈现方式完全相互独立的设计方法,类似于Gtk.TextViewGtk.TextBuffer的关系。

Gtk 树模型

数据本身存储在实现Gtk.TreeModel接口的类中。GTK+ 提供了四种内置的树模型类,但本章只介绍了Gtk.ListStoreGtk.TreeStore

Gtk.TreeModel接口提供了一套标准的方法来检索关于存储数据的一般信息。例如,它允许您获得树中的行数和某一行的子行数。Gtk.TreeModel还为您提供了一种检索存储在商店特定行中的数据的方法。

注意

模型、渲染器和列被称为对象,而不是小部件,即使它们是 GTK+ 库的一部分。这是一个重要的区别——因为它们不是从Gtk.Widget派生的,它们没有 GTK+ 小部件可用的相同的功能、属性和信号。

Gtk.ListStore允许你创建一个多列的元素列表。每一行都是根节点的子节点,因此只显示一层行。基本上,Gtk.ListStore是一个没有层次的树形结构。之所以提供它,是因为存在更快的算法来与没有任何子项的模型进行交互。

Gtk.TreeStore提供与Gtk.ListStore相同的功能,只是数据可以组织成多层树。GTK+ 也提供了一种创建您自己的定制模型类型的方法,但是这两种可用的类型在大多数情况下应该是合适的。

虽然Gtk.ListStoreGtk.TreeStore应该适合大多数应用,但有时您可能需要实现自己的商店对象。例如,如果它需要保存大量的行,您应该创建一个更高效的新模型。在第十二章中,你将学习如何创建从GObject派生的新类,这可以作为你开始派生一个实现Gtk.TreeModel接口的新类的指南。

创建树模型后,视图用于显示数据。通过分离树视图及其模型,您能够在多个视图中显示同一组数据。这些视图可以是彼此的精确副本,或者数据可以以不同的方式显示。对模型进行修改时,所有视图都会同时更新。

小费

虽然在多个树视图中显示同一组数据可能不会立即带来好处,但是可以考虑使用文件浏览器。如果您需要在多个文件浏览器中显示同一组文件,对每个视图使用相同的模型将节省内存,并使您的程序运行得更快。当您想要为文件浏览器提供多个显示选项时,这也很有用。在显示模式之间切换时,您不需要改变数据本身。

模型由包含相同数据类型的列和保存每组数据的行组成。每个模型列可以保存一种类型的数据。不应将树模型列与树视图列相混淆,树视图列由单个标题组成,但可以用来自多个模型列的数据来呈现。例如,树列可以显示一个文本字符串,该字符串具有由用户不可见的模型列定义的前景色。图 9-2 说明了模型列和树列的区别。

img/142357_2_En_9_Fig2_HTML.jpg

图 9-2

模型和树列之间的关系

模型中的每一行都包含对应于每个模型列的一段数据。在图 9-2 中,每一行包含一个文本字符串和一个Gdk.Color值。这两个值用于在树列中用相应的颜色显示文本。在本章的后面,你将学习如何用代码实现这一点。现在,您应该简单地理解这两种类型的列之间的区别以及它们之间的关系。

新的列表和树存储是用许多列创建的,每一列都由现有的GObject.TYPE定义。通常,您只需要使用那些已经在GLib中实现的。例如,如果你想显示文本,你可以使用GObject.TYPE_STRINGGObject.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 所示。这些属性在呈现时应用于每个单元格。

img/142357_2_En_9_Fig3_HTML.jpg

图 9-3

应用单元格渲染器属性

在图 9-3 中,有两个树模型列,类型分别为GObject.TYPE_STRINGGdk.RGBA。这些应用于Gtk.CellRendererText的文本和前景属性,并用于相应地渲染树视图列。

更改单元渲染器属性的另一种方法是定义单元数据函数。在呈现树视图之前,将为树视图中的每一行调用此函数。这允许您自定义每个单元格的呈现方式,而不需要将数据存储在树模型中。例如,单元格数据函数可用于定义要显示的浮点数的小数位数。单元数据函数在本章的“单元数据方法”一节中有详细介绍。

本章还介绍了用于显示文本(字符串、数字和布尔值)、切换按钮、微调按钮、进度条、像素缓冲区、组合框和键盘快捷键的单元格渲染器。此外,您可以创建自定义的单元格渲染器类型,但这通常是不需要的,因为 GTK+ 现在提供了如此广泛的类型。

这一节已经教了你使用Gtk.TreeView小部件需要什么对象,它们做什么,以及它们如何相互关联。现在您已经对Gtk.TreeView小部件有了基本的了解,下一节有一个简单的Gtk.ListStore树模型的例子。

使用 Gtk。列表存储

回想一下上一节,Gtk.TreeModel只是一个由数据存储实现的接口,比如Gtk.ListStoreGtk.ListStore用于创建行之间没有层次关系的数据列表。

在本节中,实现了一个简单的杂货列表应用,它包含三列,所有列都使用Gtk.CellRendererText。图 9-4 是这个应用的截图。第一列是显示TrueFalse的布尔值,用于定义是否应该购买该产品。

小费

您通常不希望将布尔值显示为文本,因为如果有许多布尔列,用户将无法管理。相反,您希望使用切换按钮。您将在后面的章节中学习如何使用Gtk.CellRendererToggle来完成这项工作。布尔值通常也用作列属性来定义单元格渲染器属性。

img/142357_2_En_9_Fig4_HTML.jpg

图 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小部件提供的功能。

img/142357_2_En_9_Fig5_HTML.jpg

图 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.TreePathGtk.TreeIterGtk.TreeRowReference。在下面几节中,您将了解每个对象的工作原理以及如何在您自己的程序中使用它们。

树状路径

例如,如果您看到的是字符串 3:7:5,那么您将从第四个根元素开始(回想一下,索引从零开始,因此元素 3 实际上是该级别中的第四个元素)。接下来,您将继续处理该根元素的第八个子元素。这一排是那个孩子的第六个孩子。

为了形象地说明这一点,图 9-6 显示了在图 9-5 中创建的树形视图,其中标注了树路径。每个根元素仅被称为一个元素,即 0 和 1。第一个根元素有两个子元素,称为 0:0 和 0:1。

img/142357_2_En_9_Fig6_HTML.jpg

图 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.TreeModelGtk.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.ListStoreGtk.TreeStore模型添加行,然后设置每行的初始内容。

Gtk.TreeModel提供了许多iter_*()方法,可以用来移动迭代器和检索关于它们的信息。例如,要移动到下一个迭代器位置,您可以使用treemodel.iter_next(),如果操作成功,它将返回True。可用函数的完整列表可以在Gtk.TreeModel API 文档中找到。

使用treemodel.get_path()treemodel.get_iter()很容易在树迭代器和树路径之间转换。树路径或迭代器必须有效,这些函数才能正常工作。清单 9-4 给出了一个如何在Gtk.TreeIterGtk.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.ListStoreGtk.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.SINGLEGtk.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 所示,图中显示了在树形视图的底部添加了添加和删除按钮。此外,选择模式已更改为允许用户一次选择多行。

img/142357_2_En_9_Fig7_HTML.jpg

图 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 所示。

img/142357_2_En_9_Fig8_HTML.jpg

图 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的文本属性设置每个单元格的背景颜色。

img/142357_2_En_9_Fig9_HTML.jpg

图 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)

如果您已经添加了一个单元格数据函数到您现在想要删除的列,您应该调用设置为Nonecolumn.set_cell_data_func()函数参数。

如前所述,只有在明确需要微调数据呈现时,才应该使用单元格数据函数。在大多数情况下,您希望使用附加的列属性或column.property_set()来改变属性,这取决于设置的范围。根据经验,单元格数据函数应该仅用于应用不能用列属性处理的设置,或者不能为每个单元格设置。

单元渲染器

到目前为止,您只学习了一种类型的单元格渲染器,Gtk.CellRendererText。这个渲染器允许您将字符串、数字和布尔值显示为文本。您可以使用单元格渲染器属性和单元格数据函数自定义文本的显示方式,并允许用户对其进行编辑。

GTK+ 提供了大量的单元格渲染器,可以显示除文本之外的其他类型的小部件。这些是切换按钮、图像、旋转按钮、组合框、进度条和加速器,它们都在本章中介绍。

切换按钮渲染器

Gtk.CellRendererText将布尔值显示为“真”或“假”有点俗气,而且每行都要占用大量宝贵的空间,尤其是当有很多可见的布尔列时。您可能会想,如果可以显示布尔值的复选按钮而不是文本字符串,那该多好。事实证明你可以——在一种叫做Gtk.CellRendererToggle的单元格渲染器的帮助下。

默认情况下,切换按钮单元格渲染器被绘制为复选按钮,如图 9-10 所示。您还可以将切换按钮呈现器设置为单选按钮,但是您需要自己管理单选按钮的功能。

img/142357_2_En_9_Fig10_HTML.jpg

图 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使用的。该属性被设置为TrueFalse,这取决于切换按钮的期望状态。

然后,您应该将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 所示,其中每个项目的左边都有一个小图标。

img/142357_2_En_9_Fig11_HTML.jpg

图 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渲染的正在编辑的单元格的例子。

img/142357_2_En_9_Fig12_HTML.jpg

图 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 所示。

img/142357_2_En_9_Fig13_HTML.jpg

图 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_STRINGGObject.TYPE_INTGObject.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小部件,它在第二列有一个进度条单元格渲染器,显示文本反馈。

img/142357_2_En_9_Fig14_HTML.jpg

图 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 显示了一个加速器单元渲染器的例子。

img/142357_2_En_9_Fig15_HTML.jpg

图 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_MASKGdk.ModifierType.MOD2_MASKGdk.ModifierType.MOD3_MASKGdk.ModifierType.MOD4_MASKGdk.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.ListStoreGtk.TreeStore显示数据的列表和树形结构。您还了解了树视图、树模型、列和单元渲染器之间的关系,以及如何使用每个对象。

接下来,您了解了可用于引用树视图中行的对象类型。这些包括树迭代器、路径和行引用。这些对象中的每一个都有自己的优点和缺点。树迭代器可以直接用于模型,但是当树模型改变时,它们就失效了。树路径很容易理解,因为它们有相关的人类可读的字符串,但是如果树模型被改变,它们可能不指向同一行。最后,树行引用是有用的,因为只要行存在,它们就保持有效,即使模型发生了变化。

接下来,您学习了如何处理一行或多行的选择。对于多行选择,您可以使用一个for-each函数,或者您可以获得所选行的 Python 列表。处理选择时一个有用的信号是Gtk.TreeView的行激活信号,它允许你处理双击。

之后,您学习了如何使用Gtk.CellRendererText的已编辑信号创建可编辑单元格,该信号显示一个Gtk.Entry以允许用户编辑单元格中的内容。单元格数据函数也可以连接到列。这些单元格数据函数允许您在将每个单元格呈现到屏幕之前对其进行定制。

最后,您了解了许多单元格渲染器,这些渲染器允许您显示切换按钮、像素缓冲区、微调按钮、组合框、进度条和键盘快捷键字符串。还向您介绍了Gtk.ComboBox小部件。

恭喜你!您现在已经熟悉了 GTK+ 提供的最难也是最通用的小部件之一。在下一章中,您将学习如何创建菜单、工具栏和弹出菜单。您还将学习如何使用用户界面(UI)文件自动创建菜单。