PyGTK 开发基础知识(四)

原文:Foundations of PyGTK Development

协议:CC BY-NC-SA 4.0

十、菜单和工具栏

本章教你如何创建弹出菜单、菜单栏和工具栏。您从手动创建每一个开始,这样您就了解了小部件是如何构造的。这让你对菜单和工具栏所依赖的所有概念有一个牢固的理解。

在理解了每个小部件之后,会向您介绍Gtk.Builder,它允许您通过定制的 XML 文件动态创建菜单和工具栏。加载每个用户界面文件,并将每个元素应用于相应的 action 对象,该对象告诉项目如何显示以及如何操作。

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

  • 如何创建弹出菜单、菜单栏和工具栏

  • 如何将键盘快捷键应用于菜单项

  • 什么是Gtk.StatusBar小部件,以及如何使用它向用户提供关于菜单项的更多信息

  • GTK+ 提供了哪些类型的菜单项和工具栏项

  • 如何用 UI 文件动态创建菜单和工具栏

  • 如何使用Gtk.IconFactory创建自定义库存项目

弹出式菜单

本章从学习如何创建弹出式菜单开始。弹出菜单是一个Gtk.Menu窗口小部件,当鼠标右键悬停在某些窗口小部件上时显示给用户。一些小工具,比如Gtk.EntryGtk.TextView,已经默认内置了弹出菜单。

如果你想改变一个默认提供弹出菜单的小部件的弹出菜单,你应该在弹出回调函数中编辑提供的Gtk.Menu小部件。例如,Gtk.EntryGtk.TextView都有一个 populate-popup 信号,它接收将要显示的Gtk.Menu。在向用户显示之前,您可以以任何您认为合适的方式编辑该菜单。

创建弹出菜单

对于大多数小部件,您需要创建自己的弹出菜单。在本节中,您将学习如何为一个Gtk.ProgressBar小部件提供一个弹出菜单。我们要实现的弹出菜单如图 10-1 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-1

包含三个菜单项的简单弹出菜单

三个弹出菜单项使进度条跳动,将其设置为 100%完成,然后清除它。在清单 10-1 中,一个事件框包含了进度条。因为Gtk.ProgressBarGtk.Label一样,不能自己检测 GDK 事件,我们需要使用事件盒来捕捉button-press-event信号。

#!/usr/bin/python3

import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk

class AppWindow(Gtk.ApplicationWindow):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_border_width(10)
        self.set_size_request(250, -1)
        # Create all of the necessary widgets and initialize the pop-up menu.  menu = Gtk.Menu.new()
        eventbox = Gtk.EventBox.new()
        progress = Gtk.ProgressBar.new() progress.set_text("Nothing Yet Happened")
        progress.set_show_text(True) self.create_popup_menu(menu, progress)
        progress.set_pulse_step(0.05) eventbox.set_above_child(False)
        eventbox.connect("button_press_event", self.button_press_event, menu) eventbox.add(progress)
        self.add(eventbox)
        eventbox.set_events(Gdk.EventMask.BUTTON_PRESS_MASK)
        eventbox.realize()

    def create_popup_menu(self, menu, progress):
        pulse = Gtk.MenuItem.new_with_label("Pulse Progress")
        fill = Gtk.MenuItem.new_with_label("Set as Complete")
        clear = Gtk.MenuItem.new_with_label("Clear Progress")
        separator = Gtk.SeparatorMenuItem.new()
        pulse.connect("activate", self.pulse_activated, progress)
        fill.connect("activate", self.fill_activated, progress)
        clear.connect("activate", self.clear_activated, progress)
        menu.append(pulse)
        menu.append(separator)
        menu.append(fill)
        menu.append(clear)
        menu.attach_to_widget(progress, None)
        menu.show_all()

    def button_press_event(self, eventbox, event, menu):
        pass

    def pulse_activated(self, item, progress):
        pass

    def fill_activated(self, item, progress):
        pass

    def clear_activated(self, item, progress):
        pass

class Application(Gtk.Application):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, application_id="org.example.myapp",
                         **kwargs)
        self.window = None

    def do_activate(self):
        if not self.window:
            self.window = AppWindow(application=self, title="Pop-up Menus")
        self.window.show_all()
        self.window.present()
    if __name__ == "__main__":
        app = Application()
        app.run(sys.argv)

Listing 10-1Simple Pop-up Menu

在大多数情况下,您希望使用button-press-event来检测用户希望弹出菜单何时显示。这允许您检查是否单击了鼠标右键。如果点击了鼠标右键,Gdk.EventButton的按钮成员等于 3。

不过,Gtk.Widget也提供了popup-menu信号,当用户按下内置的按键加速器激活弹出菜单时,该信号被激活。大多数用户使用鼠标来激活弹出菜单,所以这在 GTK+ 应用中通常不是一个因素。然而,如果您也想处理这个信号,您应该创建第三个函数来显示由两个回调函数调用的弹出菜单。

Gtk.Menu.new()创建新菜单。菜单初始化时没有初始内容,所以下一步是创建菜单项。

在本节中,我们将介绍两种类型的菜单项。第一个是所有其他类型菜单项的基类,Gtk.MenuItem。为Gtk.MenuItem提供了三种初始化功能:Gtk.MenuItem.new()Gtk.MenuItem.new_with_label()Gtk.MenuItem.new_with_mnemonic()

pulse = Gtk.MenuItem.new_with_label("Pulse Progress")

大多数情况下,你不需要使用Gtk.MenuItem.new(),因为一个没有内容的菜单项没有多大用处。如果您使用该函数来初始化菜单项,您必须用代码构造菜单的每个方面,而不是让 GTK+ 来处理细节。

注意

菜单项助记符和键盘快捷键不是一回事。当菜单具有焦点时,当用户按下 Alt 和适当的字母数字键时,助记符激活菜单项。键盘快捷键是一种自定义的组合键,当按下该组合键时会运行回调函数。在下一节中,您将了解菜单的键盘快捷键。

另一种类型的基本菜单项是Gtk.SeparatorMenuItem,它在其位置放置一个通用分隔符。您可以使用Gtk.SeparatorMenuItem.new()创建一个新的分隔符菜单项。

在设计菜单结构时,分隔符非常重要,因为它们将菜单项组织成组,这样用户就可以很容易地找到合适的菜单项。例如,在“文件”菜单中,菜单项通常被组织成打开文件、保存文件、打印文件和关闭应用的组。很少会出现许多菜单项之间没有分隔符的情况(例如,最近显示的文件列表可能没有分隔符)。在大多数情况下,您应该将相似的菜单项组合在一起,并在相邻的组之间放置一个分隔符。

在创建菜单项之后,您需要将每个菜单项连接到激活信号,该信号在用户选择该项时发出。或者,您可以使用 activate-item 信号,该信号也会在显示给定菜单项的子菜单时发出。除非菜单项扩展为子菜单,否则这两者之间没有明显的区别。

每个 activate 和 activate-item 回调函数都接收启动动作的Gtk.MenuItem小部件和您需要传递给该函数的任何数据。在清单 10-2 中,提供了三个菜单项回调函数来脉动进度条,填充到 100%完成,并清除所有进度。

现在您已经创建了所有的菜单项,您需要将它们添加到菜单中。Gtk.Menu派生自Gtk.MenuShell,是一个抽象基类,包含并显示子菜单和菜单项。菜单项可以通过menu.append()添加到菜单外壳中。该函数将每个项目附加到菜单外壳的末尾。

menu.append(pulse)

此外,您可以使用menu.prepend()menu.insert()分别在菜单的开头添加菜单项或将其插入任意位置。menu.insert()接受的位置从零开始。

在将Gtk.Menu的所有子控件设置为可见后,您应该调用menu.attach_to_widget()以便弹出菜单与特定的小部件相关联。该函数接受弹出菜单和它所附加的小部件。

menu.attach_to_widget(progress, None)

menu.attach_to_widget()的最后一个参数接受一个Gtk.MenuDetachFunc,它可以在菜单脱离小部件时调用一个特定的函数。

弹出菜单回调方法

在创建了必要的小部件之后,您需要处理button-press-event信号,如清单 10-2 所示。在本例中,每当鼠标右键单击进度条时,都会显示弹出菜单。

#!/usr/bin/python3

import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk

class AppWindow(Gtk.ApplicationWindow):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_border_width(10)
        self.set_size_request(250, -1)
        # Create all of the necessary widgets and initialize the pop-up
        menu. menu = Gtk.Menu.new()
        eventbox = Gtk.EventBox.new() progress =
        Gtk.ProgressBar.new()
        progress.set_text("Nothing Yet Happened")
        progress.set_show_text(True) self.create_popup_menu(menu, progress)
        progress.set_pulse_step(0.05) eventbox.set_above_child(False)
        eventbox.connect("button_press_event", self.button_press_event, menu) eventbox.add(progress)
        self.add(eventbox)
        eventbox.set_events(Gdk.EventMask.BUTTON_PRESS_MASK) eventbox.realize()

    def create_popup_menu(self, menu, progress):
        pulse = Gtk.MenuItem.new_with_label("Pulse Progress")
        fill = Gtk.MenuItem.new_with_label("Set as Complete")
        clear = Gtk.MenuItem.new_with_label("Clear Progress")
        separator = Gtk.SeparatorMenuItem.new()
        pulse.connect("activate", self.pulse_activated, progress)
        fill.connect("activate", self.fill_activated, progress)
        clear.connect("activate", self.clear_activated, progress)
        menu.append(pulse)
        menu.append(separator)
        menu.append(fill)
        menu.append(clear)
        menu.attach_to_widget(progress, None)
        menu.show_all()

    def button_press_event(self, eventbox, event, menu):
        if event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: menu.popup(None, None, None, None, event.button, event.time) return True

        return False

    def pulse_activated(self, item, progress):
        progress.pulse()
        progress.set_text("Pulse!")

    def fill_activated(self, item, progress):
        progress.set_fraction(1.0)
        progress.set_text("One Hundred Percent")

    def clear_activated(self, item, progress):
        progress.set_fraction(0.0)
        progress.set_text("Reset to Zero")

class Application(Gtk.Application):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, application_id="org.example.myapp",
                         **kwargs)
        self.window = None

    def do_activate(self):
        if not self.window:
            self.window = AppWindow(application=self, title="Pop-up Menus")
        self.window.show_all()
        self.window.present()
    if __name__ == "__main__":
        app = Application()
        app.run(sys.argv)

Listing 10-2Callback Functions for the Simple Pop-up Menu

在清单 10-2 中的button-press-event回调函数中,可以使用menu.popup()在屏幕上显示菜单。

menu.popup(parent_menu_shell, parent_menu_item, func, func_data, button, event_time)

在清单 10-2 中,除了被点击导致事件的鼠标按钮(事件➤按钮)和事件发生的时间(event.time)之外,所有参数都被设置为None。如果弹出菜单是由按钮之外的其他东西激活的,您应该为 button 参数提供 0。

注意

如果该操作是由弹出菜单信号调用的,则事件时间将不可用。那样的话,可以用Gtk.get_current_event_time()。该函数返回当前事件的时间戳,如果没有最近的事件,则返回Gdk.CURRENT_TIME

通常,parent_menu_shellparent_menu_itemfuncfunc_data被设置为None,因为它们在菜单是菜单栏结构的一部分时使用。parent_menu_shell小部件是包含导致弹出初始化的项目的菜单外壳。或者,您可以提供parent_menu_item,它是导致弹出初始化的菜单项。

Gtk.MenuPositionFunc是一个决定在屏幕上的什么位置绘制菜单的功能。它接受func_data作为可选的最后一个参数。这些参数在应用中不经常使用,因此可以安全地设置为None。在我们的例子中,弹出菜单已经与进度条相关联,所以它被绘制在正确的位置。

键盘快捷键

创建菜单时,最重要的事情之一就是设置键盘快捷键。键盘快捷键是由一个快捷键和一个或多个修饰键组成的组合键,如 CtrlShift 。当用户按下组合键时,会发出相应的信号。

清单 10-3 是进度条弹出菜单应用的扩展,它为菜单项添加了键盘快捷键。当用户按 Ctrl+P 时,进度条是脉冲式的,用 Ctrl+F 填充,用 Ctrl+C 清除。

#!/usr/bin/python3

import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk

class AppWindow(Gtk.ApplicationWindow):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_border_width(10)
        self.set_size_request(250, -1)
        # Create all of the necessary widgets and initialize the pop-up menu. menu = Gtk.Menu.new()
        eventbox = Gtk.EventBox.new() progress = Gtk.ProgressBar.new() progress.set_text("Nothing Yet Happened") progress.set_show_text(True) self.create_popup_menu(menu, progress) progress.set_pulse_step(0.05) eventbox.set_above_child(False)
        eventbox.connect("button_press_event", self.button_press_event, menu) eventbox.add(progress)
        self.add(eventbox)
        eventbox.set_events(Gdk.EventMask.BUTTON_PRESS_MASK)
        eventbox.realize()

    def create_popup_menu(self, menu, progress):
        group = Gtk.AccelGroup.new()
        self.add_accel_group(group)
        menu.set_accel_group(group)
        pulse = Gtk.MenuItem.new_with_label("Pulse Progress")
        fill = Gtk.MenuItem.new_with_label("Set as Complete")
        clear = Gtk.MenuItem.new_with_label("Clear Progress")
        separator = Gtk.SeparatorMenuItem.new()
        # Add the necessary keyboard accelerators.
        pulse.add_accelerator("activate", group, Gdk.KEY_P, Gdk.ModifierType.CONTROL, Gtk.AccelFlags.VISIBLE)
        fill.add_accelerator("activate", group, Gdk.KEY_F, Gdk.ModifierType.CONTROL, Gtk.AccelFlags.VISIBLE)
        clear.add_accelerator("activate", group, Gdk.KEY_C, Gdk.ModifierType.CONTROL, Gtk.AccelFlags.VISIBLE)
        pulse.connect("activate", self.pulse_activated, progress)
        fill.connect("activate", self.fill_activated, progress)
        clear.connect("activate", self.clear_activated, progress)
        menu.append(pulse)
        menu.append(separator)
        menu.append(fill)
        menu.append(clear)
        menu.attach_to_widget(progress, None)
        menu.show_all()

    def button_press_event(self, eventbox, event, menu):
        if event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: menu.popup(None, None, None, None, event.button, event.time) return True
        return False

    def pulse_activated(self, item, progress):
        progress.pulse()
        progress.set_text("Pulse!")

    def fill_activated(self, item, progress):
        progress.set_fraction(1.0)
        progress.set_text("One Hundred Percent")

    def clear_activated(self, item, progress):
        progress.set_fraction(0.0)
        progress.set_text("Reset to Zero")

class Application(Gtk.Application):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, application_id="org.example.myapp",
                         **kwargs)
        self.window = None

    def do_activate(self):
        if not self.window:
            self.window = AppWindow(application=self, title="Pop-up Menus")
        self.window.show_all()
        self.window.present()
    if __name__ == "__main__":
        app = Application()
        app.run(sys.argv)

Listing 10-3Adding Accelerators to Menu Items

键盘快捷键存储为Gtk.AccelGroup的一个实例。要在您的应用中实现加速器,您需要用Gtk.AccelGroup.new()创建一个新的加速器组。该加速器组必须添加到菜单出现的Gtk.Window中才能生效。它还必须与任何利用其加速器的菜单相关联。在清单 10-3 中,这是在用self.add_accel_group()menu.set_accel_group()创建Gtk.AccelGroup之后立即执行的。

可以用Gtk.AccelMap手动创建键盘快捷键,但是在大多数情况下,widget.add_accelerator()提供了所有必要的功能。这种方法存在的唯一问题是,用户不能在运行时更改用该函数创建的键盘快捷键。

widget.add_accelerator(signal_name, group, accel_key, mods, flags)

要向小部件添加加速器,可以使用widget.add_accelerator(),当用户按下组合键时,它会在小部件上发出由 signal_name 指定的信号。您需要为该功能指定您的快捷键组,如前所述,它必须与窗口和菜单相关联。

一个加速键和一个或多个修饰键形成完整的组合键。PyGObject API 参考中提供了可用快捷键的列表。所有可用关键字的定义都可以包含在 import GDK 语句中。

修饰符由Gdk.ModifierType枚举指定。最常用的修饰键是Gdk.ModifierType.SHIFT_MASKGdk.ModifierType.CONTROL_MASKGdk.ModifierType.MOD1_MASK,分别对应 Shift、Ctrl 和 Alt 键。

小费

当处理键码时,您需要小心,因为在某些情况下,您可能需要为同一个动作提供多个键。例如,如果你想抓住数字 1 键,你需要注意Gdk.KEY_1Gdk.KEY_KP_1——它们对应于键盘顶部的 1 键和数字小键盘上的 1 键。

widget.add_accelerator()的最后一个参数是加速器标志。由Gtk.AccelFlags枚举定义了三个标志。如果设置了Gtk.AccelFlags.VISIBLE,则在标签中可以看到加速器。Gtk.AccelFlags.LOCKED防止用户修改加速器。Gtk.AccelFlags.MASK为小工具加速器设置两个标志。

状态栏提示

通常放置在主窗口的底部,Gtk.Statusbar窗口小部件可以给用户关于应用正在运行的更多信息。状态栏对于菜单也非常有用,因为您可以向用户提供更多关于鼠标光标所悬停的菜单项功能的信息。状态栏截图如图 10-2 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-2

带有状态栏提示的弹出式菜单

状态栏小部件

虽然状态栏一次只能显示一条消息,但是小部件实际上存储了一堆消息。当前显示的消息在栈顶。从堆栈中弹出消息时,会显示上一条消息。如果从顶部弹出一条消息后,堆栈上没有剩余的字符串,则状态栏上不会显示任何消息。

Gtk.Ststusbar.new()创建新的状态栏小部件。这将创建一个新的带有空消息堆栈的Gtk.Statusbar小部件。在能够在新状态栏的堆栈中添加或删除消息之前,必须用statusbar.get_context_id()检索上下文标识符。

id = statusbar.get_context_id(description)

上下文标识符是与上下文描述字符串相关联的唯一无符号整数。该标识符用于特定类型的所有消息,这允许您对堆栈上的消息进行分类。

例如,如果您的状态栏包含超链接和 IP 地址,您可以从字符串“URL”和“IP”创建两个上下文标识符。当您将消息推入堆栈或从堆栈弹出消息时,您必须指定一个上下文标识符。这允许应用的不同部分在状态栏消息堆栈中推送和弹出消息,而不会相互影响。

小费

对不同类别的消息使用不同的上下文标识符是很重要的。如果应用的一部分试图给用户一个消息,而另一部分试图删除自己的消息,你不希望错误的消息从堆栈中弹出!

在您生成上下文标识符之后,您可以使用statusbar.push()将一条消息添加到状态栏的栈顶。该函数返回刚刚添加的字符串的唯一消息标识符。这个标识符可以在以后用于从堆栈中移除消息,而不管它在什么位置。

statusbar.push(context_id, message)

有两种方法可以从堆栈中删除消息。如果您想从特定上下文 ID 的堆栈顶部删除一条消息,您可以使用statusbar.pop()。该函数删除状态栏堆栈中上下文标识符为context_id的最高消息。

statusbar.pop(context_id)

也可以用statusbar.remove()从状态栏的消息堆栈中删除特定的消息。为此,您必须提供消息的上下文标识符和想要删除的消息的消息标识符,这是在添加消息时由statusbar.push()返回的。

statusbar.remove(context_id, message_id)

菜单项信息

状态栏的一个有用的作用是给用户更多关于鼠标光标当前悬停的菜单项的信息。图 10-2 显示了这样一个例子,这是清单 10-4 中的进度条弹出菜单应用的截图。

要实现状态栏提示,你应该将你的每个菜单项连接到Gtk.Widget"enter-notify-event""leave-notify-event"信号。清单 10-4 显示了你已经知道的进度条弹出菜单应用,除了当鼠标光标移动到一个菜单项上时状态栏提示被提供。

#!/usr/bin/python3

import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk

class AppMenuItem(Gtk.MenuItem):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def __setattr__(self, name, value):
        self.__dict__[name] = value

    def __getattr__(self, name):
        return self.__dict__[name]

class AppWindow(Gtk.ApplicationWindow):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_border_width(10)
        self.set_size_request(250, -1)
        # Create all of the necessary widgets and initialize the pop-up menu. menu = Gtk.Menu.new()
        eventbox = Gtk.EventBox.new() progress = Gtk.ProgressBar.new()
        progress.set_text("Nothing Yet Happened")
        progress.set_show_text(True)
        statusbar = Gtk.Statusbar.new()
        self.create_popup_menu(menu, progress, statusbar)
        progress.set_pulse_step(0.05)
        eventbox.set_above_child(False)
        eventbox.connect("button_press_event", self.button_press_event, menu)
        eventbox.add(progress)
        vbox = Gtk.Box.new(orientation=Gtk.Orientation.VERTICAL, spacing=0)
        vbox.pack_start(eventbox, False, True, 0)
        vbox.pack_start(statusbar, False, True, 0)
        self.add(vbox)
        eventbox.set_events(Gdk.EventMask.BUTTON_PRESS_MASK)
        eventbox.realize()

    def create_popup_menu(self, menu, progress, statusbar):
        pulse = AppMenuItem(label="Pulse Progress")
        fill = AppMenuItem(label="Set as Complete")
        clear = AppMenuItem(label="Clear Progress")
        separator = Gtk.SeparatorMenuItem.new()
        pulse.connect("activate", self.pulse_activated, progress)
        fill.connect("activate", self.fill_activated, progress)
        clear.connect("activate", self.clear_activated, progress)
        Connect signals to each menu item for status bar messages. pulse.connect("enter-notify-event", self.statusbar_hint, statusbar) pulse.connect("leave-notify-event", self.statusbar_hint, statusbar) fill.connect("enter-notify-event", self.statusbar_hint, statusbar) fill.connect("leave-notify-event", self.statusbar_hint, statusbar) clear.connect("enter-notify-event", self.statusbar_hint, statusbar) clear.connect("leave-notify-event", self.statusbar_hint, statusbar) pulse.__setattr__("menuhint", "Pulse the progress bar one step.") fill.__setattr__("menuhint", "Set the progress bar to 100%.") clear.__setattr__("menuhint", "Clear the progress bar to 0%.") menu.append(pulse)
        menu.append(separator)
        menu.append(fill)
        menu.append(clear)
        menu.attach_to_widget(progress, None) menu.show_all()

    def button_press_event(self, eventbox, event, menu):
        if event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: menu.popup(None, None, None, None, event.button, event.time) return True
        return False

    def pulse_activated(self, item, progress):
        progress.pulse()
        progress.set_text("Pulse!")

    def fill_activated(self, item, progress):
        progress.set_fraction(1.0)
        progress.set_text("One Hundred Percent")

    def clear_activated(self, item, progress): progress.set_fraction(0.0) progress.set_text("Reset to Zero")

    def statusbar_hint(self, menuitem, event, statusbar): id = statusbar.get_context_id("MenuItemHints")
        if event.type == Gdk.EventType.ENTER_NOTIFY:
            hint = menuitem.__getattr__("menuhint")
            id = statusbar.push(id, hint)
        elif event.type == Gdk.EventType.LEAVE_NOTIFY:
            statusbar.pop(id)
        return False

class Application(Gtk.Application):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, application_id="org.example.myapp",
                         **kwargs)
        self.window = None

    def do_activate(self):
        if not self.window:
            self.window = AppWindow(application=self, title="Pop-up Menus")
        self.window.show_all()
        self.window.present()
    if __name__ == "__main__":
        app = Application()
        app.run(sys.argv)

Listing 10-4Displaying More Information About a Menu Item

当实现状态栏提示时,你首先需要弄清楚什么信号是必要的。我们希望能够在鼠标光标移动到菜单项上时向状态栏添加一条消息,并在鼠标光标离开时删除它。从这个描述来看,使用“进入-通知-事件”和“离开-通知-事件”是一个很好的解决方案。

由于 Python 3 的 GTK+ 3 接口没有在 GTK+ 对象上实现get_data()set_data()方法,我们需要子类化Gtk.MenuItem类来实现相应的 Python 3 属性。这种方法也被用在本书的其他例子中。

使用这两个信号的一个优点是我们只需要一个回调函数,因为每个函数的原型都接收一个Gdk.EventProximity对象。从这个物体中,我们可以分辨出Gdk.EventType.ENTER_NOTIFYGdk.EventType.LEAVE_NOTIFY事件。你想从回调函数中返回False,因为你不想阻止 GTK+ 处理事件;您只想增强它发出时所执行的内容。

statusbar_hint()回调方法中,您应该首先检索菜单项消息的上下文标识符。您可以使用任何您想要的字符串,只要您的应用记得使用了什么。清单 10-4 描述了所有添加到状态栏的菜单项消息。如果应用的其他部分使用状态栏,使用不同的上下文标识符将不会影响菜单项提示。

id = statusbar.get_context_id("MenuItemHints")

如果事件类型为Gdk.EventType.ENTER_NOTIFY,则需要向用户显示消息。在create_popup_menu()方法中,一个数据参数被添加到每个名为"menuhint"的菜单项中。这是对菜单项功能的更深入的描述,显示给用户。

hint = menuitem.__getattr__("menuhint")
statusbar.push(id, hint)

然后,使用statusbar.push(),可以将消息添加到状态栏的"MenuItemHints"上下文标识符下。该消息被放在堆栈的顶部并显示给用户。您可能想考虑在调用这个方法之后处理所有的 GTK+ 事件,因为用户界面应该立即反映这些变化。

但是,如果事件类型是Gdk.EventType.LEAVE_NOTIFY,您需要删除使用相同上下文标识符添加的最后一个菜单项消息。最近的消息可以用statusbar.pop()从堆栈中删除。

菜单项

到目前为止,您已经了解了显示标签和分隔符菜单项的平面菜单。也可以向现有菜单项添加子菜单。GTK+ 还提供了许多其他的Gtk.MenuItem对象。图 10-3 显示了一个弹出菜单,其中包含一个子菜单以及图像、检查和单选菜单项。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-3

图像、检查和单选菜单项

子菜单

GTK+ 中的子菜单不是由单独类型的菜单项小部件创建的,而是通过调用menuitem.set_submenu()创建的。该方法调用menu.attach_to_widget()将子菜单附加到菜单项,并在菜单项旁边放置一个箭头,表示它现在有子菜单。如果菜单项已经有子菜单,它将被给定的Gtk.Menu小部件替换。

menuitem.set_submenu(submenu)

如果你有一个非常具体的选项列表,会使原本组织有序的菜单结构变得混乱,那么子菜单非常有用。使用子菜单时,您可以使用由Gtk.MenuItem小部件提供的“activate-item”信号,该信号在菜单项显示其子菜单时发出。

除了Gtk.MenuItem和菜单项分隔符,还有另外三种类型的菜单项对象:图像、复选、单选菜单项;这些将在本节的剩余部分中介绍。

图像菜单项

警告

从 GTK+ 3.1 开始,Gtk.ImageMenuItem类就被弃用了。不要在新代码中使用它,注意它可能会在 GTK+ 的新版本中完全消失。

Gtk.ImageMenuItem与其父类Gtk.MenuItem非常相似,除了它在菜单项标签的左边显示了一个小图像。为创建新的图像菜单项提供了四个功能。

第一个函数imagemenuitem.new()创建了一个新的Gtk.ImageMenuItem对象,它有一个空标签,没有关联的图像。您可以使用图像菜单项的 image 属性来设置菜单项显示的图像。

Gtk.ImageMenuItem.new()

此外,您可以使用Gtk.ImageMenuItem.new_from_stock()从纸张标识符创建新的图像菜单项。这个函数使用与 stock_id 相关联的标签和图像创建Gtk.ImageMenuItem。该函数接受股票标识符字符串。

Gtk.ImageMenuItem.new_from_stock(stockid, accel_group)

该函数的第二个参数接受一个加速器组,它被设置为库存项目的默认加速器。如果你想像我们在清单 10-3 中所做的那样为菜单项手动设置键盘快捷键,你可以为这个参数指定None

同样,您可以使用Gtk.ImageMenuItem.new_with_label()创建一个新的Gtk.ImageMenuItem,最初只有一个标签。稍后,您可以使用 image 属性添加一个图像小部件。GTK+ 还提供了方法imagemenuitem.set_image(),允许您编辑小部件的图像属性。

Gtk.ImageMenuItem.new_with_label(label)

另外,GTK+ 提供了Gtk.ImageMenuItem.new_with_mnemonic(),它创建了一个带有助记标签的图像菜单项。与前面的方法一样,您必须在创建菜单项后设置 image 属性。

检查菜单项

Gtk.CheckMenuItem允许您创建一个菜单项,根据其布尔活动属性是True还是False,在标签旁边显示一个复选符号。这将允许用户查看选项是被激活还是被停用。

Gtk.MenuItem一样,提供了三个初始化功能。

Gtk.CheckMenuItem.new()Gtk.CheckItem.new_with_label()Gtk.CheckMenuItem.new_with_mnemonic()。这些函数分别创建一个没有标签、有初始标签或有助记标签的Gtk.CheckMenuItem

Gtk.CheckMenuItem.new()
Gtk.CheckMenuItem.new_with_label(label)
Gtk.CheckMenuItem.new_with_mnemonic(label)

如前所述,check 菜单项的当前状态由小部件的 active 属性保存。GTK+ 提供了checkmenuitem.set_active()checkmenuitem.get_active()两个函数来设置和检索有效值。

与所有的 check button 小部件一样,您可以使用“toggled”信号,当用户切换菜单项的状态时会发出该信号。GTK+ 负责更新 check 按钮的状态,所以这个信号只是允许您更新应用以反映更改后的值。

Gtk.CheckMenuItem还提供了checkmenuitem.set_inconsistent(),改变菜单项的不一致属性。当设置为True时,检查菜单项显示第三种“中间”状态,既不是活动状态也不是非活动状态。这可以向用户显示必须做出尚未设置的选择,或者对选择的不同部分设置和取消设置属性。

单选菜单项

Gtk.RadioMenuItem是从Gtk.CheckMenuItem派生出来的一个 widget。通过将 check 菜单项的 draw-as-radio 属性设置为True,它被呈现为单选按钮而不是复选按钮。单选菜单项的工作方式与普通单选按钮相同。

第一个单选按钮应该使用下列函数之一创建。您可以将单选按钮组设置为None,因为必需的元素是通过引用第一个元素添加到组中的。这些函数分别创建一个空菜单项、一个带标签的菜单项和一个带助记符的菜单项。

Gtk.RadioMenuItem.new(group)
Gtk.RadioMenuItem.new_with_label(group, text)
Gtk.RadioMenuItem.new_with_mnemonic(group, text)

所有其他单选菜单项都应该使用以下三个函数之一创建,这三个函数将它添加到与 group 关联的单选按钮组中。这些函数分别创建一个空菜单项、一个带标签的菜单项和一个带助记符的菜单项。

Gtk.RadioMenuItem.new_from_widget(group)
Gtk.RadioMenuItem.new_from_widget_with_label(group, text)
Gtk.RadioMenuItem.new_from_widget_with_mnemonic(group, text)

菜单栏

Gtk.MenuBar是一个将多个弹出菜单组织成水平或垂直行的小部件。每个根元素都是一个Gtk.MenuItem,它会向下弹出一个子菜单。一个Gtk.MenuBar的实例通常显示在主应用窗口的顶部,以提供对应用所提供功能的访问。菜单栏示例如图 10-4 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-4

有三个菜单的菜单栏

在清单 10-5 中,创建了一个带有三个菜单的Gtk.MenuBar小部件:文件、编辑和帮助。每个菜单实际上都是一个带有子菜单的Gtk.MenuItem。然后,许多菜单项被添加到每个子菜单中。

#!/usr/bin/python3

import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk

class AppWindow(Gtk.ApplicationWindow):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_size_request(250, -1)
        menubar = Gtk.MenuBar.new()
        file = Gtk.MenuItem.new_with_label("File")
        edit = Gtk.MenuItem.new_with_label("Edit")
        help = Gtk.MenuItem.new_with_label("Help")
        filemenu = Gtk.Menu.new()
        editmenu = Gtk.Menu.new()
        helpmenu = Gtk.Menu.new()
        file.set_submenu(filemenu)
        edit.set_submenu(editmenu)
        help.set_submenu(helpmenu)
        menubar.append(file)
        menubar.append(edit)
        menubar.append(help)
        # Create the File menu content.
        new = Gtk.MenuItem.new_with_label("New")
        open = Gtk.MenuItem.new_with_label("Open")
        filemenu.append(new)
        filemenu.append(open)
        # Create the Edit menu content.
        cut = Gtk.MenuItem.new_with_label("Cut")
        copy = Gtk.MenuItem.new_with_label("Copy")
        paste = Gtk.MenuItem.new_with_label("Paste")
        editmenu.append(cut)
        editmenu.append(copy)
        editmenu.append(paste)
        # Create the Help menu content.
        contents = Gtk.MenuItem.new_with_label("Help")
        about = Gtk.MenuItem.new_with_label("About")
        helpmenu.append(contents)
        helpmenu.append(about)
    self.add(menubar)

class Application(Gtk.Application):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, application_id="org.example.myapp",
                         **kwargs)
        self.window = None
    def do_activate(self):
        if not self.window:
            self.window = AppWindow(application=self, title="Menu Bars")
        self.window.show_all()
        self.window.present()
    if __name__ == "__main__":
        app = Application()
        app.run(sys.argv)

Listing 10-5Creating Groups of Menus

使用Gtk.MenuBar.new()创建新的Gtk.MenuBar小部件。这将创建一个空的菜单外壳,您可以在其中添加内容。

创建菜单栏后,可以用menubar.set_pack_direction()定义菜单栏项目的打包方向。pack_direction属性的值由Gtk.PackDirection枚举定义,包括Gtk.PackDirection.LTRGtk.PackDirection.RTLGtk.PackDirection.TTBGtk.PackDirection.BTT。它们分别从左到右、从右到左、从上到下或从下到上打包菜单项。默认情况下,子部件从左到右打包。

Gtk.MenuBar还提供了另一个名为child-pack-direction的属性,该属性设置菜单栏子菜单的菜单项打包的方向。换句话说,它控制子菜单项的打包方式。这个属性的值也由Gtk.PackDirection枚举定义。

菜单栏中的每个子项实际上都是一个Gtk.MenuItem小部件。因为Gtk.MenuBar是从Gtk.MenuShell派生的,所以您可以使用menuitem.append()方法向栏中添加一个项目,如下行所示。

menubar.append(file)

您也可以使用file.prepend()file.insert()将项目添加到菜单栏的开头或任意位置。

接下来您需要调用file.set_submenu()来向每个根菜单项添加一个子菜单。每个子菜单都是一个Gtk.Menu小部件,创建方式与弹出菜单相同。然后,GTK+ 会在必要时向用户显示子菜单。

file.set_submenu(filemenu)

工具栏

一个Gtk.Toolbar是一种容器,它在水平或垂直的行中保存了许多小部件。这意味着可以很容易地定制大量的小部件。通常,工具栏包含可以显示图像和文本字符串的工具按钮。然而,工具栏实际上可以容纳任何类型的小部件。图 10-5 中显示了一个包含四个工具按钮和一个分隔符的工具栏。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-5

显示图像和文本的工具栏

在清单 10-6 中,创建了一个简单的工具栏,在水平行中显示五个工具项。每个工具栏项都显示一个图标和一个描述该项目的用途的标签。工具栏还被设置为显示一个箭头,该箭头提供对菜单中不适合的工具栏项目的访问。

在本例中,工具栏为Gtk.Entry小部件提供剪切、复制、粘贴和全选功能。AppWindow()方法创建工具栏,将其打包在Gtk.Entry之上。然后它调用create_toolbar(),用工具项填充工具栏并连接必要的信号。

#!/usr/bin/python3

import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk

class AppWindow(Gtk.ApplicationWindow):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
        toolbar = Gtk.Toolbar.new()
        entry = Gtk.Entry.new()
        vbox.pack_start(toolbar, True, False, 0)
        vbox.pack_start(entry, True, False, 0)
        self.create_toolbar(toolbar, entry)
        self.add(vbox)
        self.set_size_request(310, 75)

    def create_toolbar(self, toolbar, entry): icon_theme = Gtk.IconTheme.get_default()
        icon = icon_theme.load_icon("edit-cut", -1,
                                    Gtk.IconLookupFlags.FORCE_SIZE)
        image = Gtk.Image.new_from_pixbuf(icon)
        cut = Gtk.ToolButton.new(image, "Cut")
        icon = icon_theme.load_icon("edit-copy", -1,
        Gtk.IconLookupFlags.FORCE_SIZE)
        image = Gtk.Image.new_from_pixbuf(icon)
        copy = Gtk.ToolButton.new(image, "Copy")
        icon = icon_theme.load_icon("edit-paste", -1,
                                    Gtk.IconLookupFlags.FORCE_SIZE)
        image = Gtk.Image.new_from_pixbuf(icon)
        paste = Gtk.ToolButton.new(image, "Paste")
        icon = icon_theme.load_icon("edit-select-all", -1, Gtk.IconLookupFlags.FORCE_SIZE)
        image = Gtk.Image.new_from_pixbuf(icon)
        selectall = Gtk.ToolButton.new(image, "Select All")
        separator = Gtk.SeparatorToolItem.new()
        toolbar.set_show_arrow(True)
        toolbar.set_style(Gtk.ToolbarStyle.BOTH)
        toolbar.insert(cut, 0)
        toolbar.insert(copy, 1)
        toolbar.insert(paste, 2)
        toolbar.insert(separator, 3)
        toolbar.insert(selectall, 4)
        cut.connect("clicked", self.cut_clipboard, entry)
        copy.connect("clicked", self.copy_clipboard, entry)
        paste.connect("clicked", self.paste_clipboard, entry)
        selectall.connect("clicked", self.select_all, entry)

    def cut_clipboard(self, button, entry):
        entry.cut_clipboard()

    def copy_clipboard(self, button, entry):
        entry.copy_clipboard()

    def paste_clipboard(self, button, entry):
        entry.paste_clipboard()

    def select_all(self, button, entry):
        entry.select_region(0, -1)

class Application(Gtk.Application):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, application_id="org.example.myapp",
                         **kwargs)
        self.window = None
    def do_activate(self):
        if not self.window:
            self.window = AppWindow(application=self, title="Toolbar")
        self.window.show_all()
        self.window.present()
    if __name__ == "__main__":
        app = Application()
        app.run(sys.argv)

Listing 10-6Creating a Gtk.Toolbar Widget

Gtk.Toolbar.new()创建新的工具栏,它在清单 10-6 中显示的create_toolbar()函数之前被调用。这将创建一个空的Gtk.Toolbar小部件,您可以在其中添加工具按钮。

Gtk.Toolbar提供了许多属性,用于定制它的显示方式和与用户的交互方式,包括方向、按钮样式以及访问不适合工具栏的项目的能力。

如果因为没有足够的空间而无法在工具栏上显示所有的工具栏项目,那么如果您将toolbar.set_show_arrow()设置为True,则会出现一个溢出菜单。如果所有项目都可以显示在工具栏上,箭头就会隐藏起来。

toolbar.set_show_arrow(boolean)

另一个Gtk.Toolbar属性是显示所有菜单项的样式,它是用toolbar.set_style()设置的。你应该注意到这个属性可能会被主题覆盖,所以你应该通过调用toolbar.unset_style()来提供使用默认样式的选项。有四种工具栏样式,由Gtk.ToolbarStyle枚举定义。

  • Gtk.ToolbarStyle.ICONS:仅显示工具栏中每个工具按钮的图标。

  • Gtk.ToolbarStyle.TEXT:如何只为工具栏中的每个工具按钮添加标签。

  • Gtk.ToolbarStyle.BOTH:显示每个工具按钮的图标和标签,图标位于标签上方。

  • Gtk.ToolbarStyle.BOTH_HORIZ:显示每个工具按钮的图标和标签,图标在标签的左边。仅当工具项目的“is-important"属性设置为True时,工具项目的标签文本才会显示。

工具栏的另一个重要属性是可以用toolbar.set_orientation()设置的方向。由Gtk.Orientation枚举定义了两个可能的值,Gtk.Orientation.HORIZONTALGtk.Orientation.VERTICAL,它们可以使工具栏水平(默认)或垂直。

工具栏项目

清单 10-6 介绍了三种重要的工具项类型:Gtk.ToolItemGtk.ToolButtonGtk.SeparatorToolItem。所有的工具按钮都是从Gtk.ToolItem类中派生出来的,该类保存了所有工具项使用的基本属性。

如果你使用的是Gtk.ToolbarStyle.BOTH_HORIZ风格,那么Gtk.ToolItem中安装的一个基本属性就是"is-important"设置。如果该属性设置为True,工具栏项目的标签文本仅针对该样式显示。

与菜单一样,分隔符工具项由Gtk.SeparatorToolItem提供,并由Gtk.SeparatorToolItem.new()创建。分隔符工具项有一个 draw 属性,当设置为True时会绘制一个分隔符。如果您将 draw 设置为False,它会在没有任何可视分隔符的位置放置填充。

小费

如果您将一个Gtk.SeparatorToolItem的扩展属性设置为True并将它的绘制属性设置为False,您将强制分隔符之后的所有工具项到工具栏的末尾。

大多数工具栏项目都属于Gtk.ToolButton类型。Gtk.ToolButton只提供了一个初始化方法Gtk.ToolButton.new(),因为从 GTK+ 3.1 开始,所有其他的初始化方法都被弃用了。Gtk.ToolButton.new()可以创建一个带有自定义图标和标签的Gtk.ToolButton。这些属性中的每一个都可以设置为None

Gtk.ToolButton.new(icon, label)

使用toolbutton.set_label()toolbutton.set_icon_widget()可以在初始化后手动设置标签和图标。这些函数提供对工具按钮的标签和图标小部件属性的访问。

此外,您可以定义自己的小部件来代替带有toolbutton.set_label_widget()的工具按钮的默认Gtk.Label小部件。这允许您在工具按钮中嵌入任意的小部件,比如一个条目或组合框。如果该属性设置为None,则使用默认标签。

toolbutton.set_label_widget(label_widget)

创建工具栏项目后,可以用toolbar.insert()将每个Gtk.ToolItem插入工具栏。

toolbar.insert(item, pos)

toolbar.insert()的第二个参数接受将项目插入工具栏的位置。工具按钮位置从零开始索引。负位置会将该项追加到工具栏的末尾。

切换工具按钮

Gtk.ToggleToolButton衍生自Gtk.ToolButton,因此只实现初始化和切换能力。切换工具按钮以工具栏项目的形式提供了Gtk.ToggleButton小部件的功能。它允许用户查看选项是已设置还是未设置。

切换工具按钮是当活动属性设置为True时保持按下状态的工具按钮。当切换按钮的状态改变时,您可以使用切换信号来接收通知。

只有一种方法可以创建新的Gtk.ToggleToolButton。这是用Gtk.ToggleToolButton.new()实现的,它创建了一个空的工具按钮。然后你可以使用Gtk.ToolButton提供的方法来添加标签和图片。

Gtk.ToggleToolButton.new()

单选工具按钮

Gtk.RadioToolButton是从Gtk.ToggleToolButton派生出来的,所以继承了“active”属性和“toggled”信号。因此,小部件只需要为您提供一种方法来创建新的单选工具按钮,并将它们添加到单选按钮组中。

应该用Gtk.RadioToolButton.new()创建一个单选工具按钮,其中单选组设置为None。这将为单选工具按钮创建一个默认的初始单选按钮组。

Gtk.RadioToolButton.new(group)

Gtk.RadioToolButton继承了Gtk.ToolButton的函数,后者提供了一些函数和属性,可以在必要时设置单选按钮的标签。

所有必需的元素都应该用Gtk.RadioToolButton.from_widget()创建。将“组”设置为第一个单选按钮会将所有必需的项目添加到同一组中。

Gtk.RadioToolButton.new_from_widget(group)

Gtk.RadioToolButton提供了一个属性 group,它是属于单选按钮组的另一个单选工具按钮。这允许您将所有单选按钮链接在一起,以便一次只能选择一个。

菜单工具按钮

Gtk.ToggleToolButton派生而来的Gtk.MenuToolButton,允许你在工具按钮上附加一个菜单。小部件在图像和标签旁边放置一个箭头,提供对相关菜单的访问。例如,您可以使用Gtk.MenuToolButton将最近打开的文件列表添加到工具栏按钮中。图 10-6 是用于此目的的菜单工具按钮的截图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-6

显示最近打开的文件的菜单工具按钮

清单 10-7 向你展示了如何实现一个菜单工具按钮。实际的工具按钮的创建方式与任何其他Gtk.ToolButton类似,只是多了一个将菜单附加到Gtk.MenuToolButton小部件的步骤。

recent = Gtk.Menu.new()
# Add a number of menu items where each corresponds to one recent file. icon_theme = Gtk.IconTheme.get_default()
icon = icon_theme.load_icon("document-open", -1,
Gtk.IconLookupFlags.FORCE_SIZE)

image = Gtk.Image.new_from_pixbuf(icon)
open = Gtk.MenuToolButton.new(image, "Open")
open.set_menu(recent)

Listing 10-7Using Gtk.MenuToolButton

在清单 10-7 中,菜单工具按钮是用一个图像和一个标签Gtk.MenuToolButton.new(image, label)创建的。如果您想在以后使用Gtk.ToolButton属性设置它们,您可以将这些参数中的任何一个设置为None

Gtk.MenuToolButton.new(image, label)

使Gtk.MenuToolButton与众不同的是,工具按钮右边的箭头为用户提供了访问菜单的途径。工具按钮的菜单是用menutoolbutton.set_menu()设置的,或者通过将菜单属性设置为一个Gtk.Menu小部件来设置。单击箭头时,会向用户显示此菜单。

动态菜单创建

注意

在 GTK+ 3.1 中不推荐使用Gtk.UIManager,所以 UI 文件的创建和加载不包含在本节中。相反,新的Gtk.Builder类及其相关的 XML 文件被包含在内。Gtk.Builder是一个更强大、更灵活的系统,用于管理外部用户界面描述和操作。它还提供了附加功能,并减少了创建和管理用户界面所需的工作量。

虽然可以手动创建每个菜单和工具栏项,但这样做会占用大量空间,并导致您不得不单调地编写不必要的代码。为了自动创建菜单和工具栏,GTK+ 允许您从 XML 文件动态创建菜单。

类可以创建许多用户界面对象,包括菜单、菜单栏、弹出菜单、整个对话框、主窗口等等。本节集中讨论不同类型的菜单,但是你应该记住Gtk.Builder可以构建许多其他类型的用户界面对象。

创建 XML 文件

用户界面文件以 XML 格式构造。所有的内容都必须包含在和标签之间。您可以创建的一种动态 UI 是带有

tag shown in Listing

10-8.

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <menu id="menubar">
    <submenu>
      <attribute name="label">File</attribute>
    </submenu>
    <submenu>
      <attribute name="label">Edit</attribute>
    </submenu>
    <submenu>
      <attribute name="label">Choices</attribute>
    </submenu>
    <submenu>
      <attribute name="label">Help</attribute>
    </submenu>
  </menu>
</interface>

Listing 10-8Menu UI File

每个菜单和项目标记都应该有一个与之关联的唯一 ID,这样您就可以从代码中直接访问该项。虽然不是必需的,但您应该始终将 name 属性添加到每个菜单和项目中。name 属性可以访问实际的小部件。

每个

可以有任意数量的子节点。根据正常的 XML 规则,这两个标签都必须关闭。如果一个标签没有结束标签(例如, ),您必须在标签的末尾加上一个正斜杠字符(`/`),这样解析器就知道标签已经结束了。

每个

和标签也可以有其他子标签,比如
和标签。
标签组织了标签。标签用于描述
和标签(即添加属性)。

标签有多种用途,但是所有标签共有的一个用途是包含标签属性。该属性提供了在上可见的标签字符串。在这种情况下,与一个Gtk.MenuItem标签属性相对应的标签指定了出现在菜单项中的字符串。

与每个标签一起出现的另一个标签是 action 属性。该标签指定了点击时要采取的动作。指定的动作与Gtk.ApplicationGtk.ApplicationWindow类(或它们的子类)紧密相关。每个动作的目标指定哪个类实例——Gtk.ApplicationWindowGtk.Application—创建了Gio.SimpleAction,并将其连接到同一个类实例中的方法来处理动作。您可以将动作标签视为一种信号名称,它是要处理的真实信号的别名。

action 属性适用于除顶级小部件和分隔符之外的所有元素。当加载 UI 文件将一个Gtk.Action对象关联到每个元素时,Gtk.Builder使用动作属性。Gtk.Action保存有关如何绘制该项以及当该项被激活时应该调用什么回调方法(如果有的话)的信息。

分隔符可以放在带有标签的菜单中。您不需要为分隔符提供名称或动作信息,因为添加了一个通用的Gtk.SeparatorMenuItem

除了菜单栏,您还可以在带有标签的 UI 文件中创建工具栏,如清单 10-9 所示。

<?xml version='1.0' encoding='utf-8' ?>
<interface>
<requires lib='gtk+' version='3.4'/>
<object class="GtkToolbar" id="toolbar">
  <property name="visible">True</property>
  <property name="can_focus">False</property>
  <child>
    <object class="GtkToolButton" id="toolbutton_new">
      <property name="visible">True</property> <property name="can_focus">False</property>
      <property name="tooltip_text" translatable="yes">New Standard</property> <property name="action_name">app.newstandard</property>
      <property name="icon_name">document-new</property>
    </object>
    <packing>
    <property name="expand">False</property> <property name="homogeneous">True</property>
    </packing>
  </child>
  <child>
    <object class="GtkToolButton" id="toolbutton_quit"> <property name="visible">True</property> <property name="can_focus">False</property>
      <property name="tooltip_text" translatable="yes">Quit</property> <property name="action_name">app.quit</property>
      <property name="icon_name">application-exit</property> </object>
      <packing>
        <property name="expand">False</property>
        <property name="homogeneous">True</property>
      </packing>
</child>
</object>
</interface>

Listing 10-9Toolbar UI File

每个工具栏可以包含任意数量的元素。工具项目以与菜单项相同的方式指定,带有一个动作("action")和一个 ID。您可以在不同的 UI 文件中使用元素的 ID,但是如果工具栏和菜单栏位于同一个文件中,就不应该使用相同的名称。

但是,您可以并且应该对多个元素使用相同的操作。这导致每个元素以相同的方式绘制,并连接到相同的回调方法。这样做的好处是,您只需要为每个项目类型定义一个Gtk.Action。例如,对于清单 10-8 到 10-10 中的 UI 文件中的 Cut 元素,使用了相同的操作。

小费

虽然工具栏、菜单栏和弹出菜单被分割成单独的 UI 文件,但是您可以在一个文件中包含任意数量的这些小部件。唯一的要求是整个文件内容包含在和标签之间。

除了工具栏和菜单栏,还可以在 UI 文件中定义弹出菜单,如清单 10-10 所示。注意清单 10-8 ,清单 10-9 ,清单 10-10 中有重复的动作。重复动作允许您只定义一个Gtk.Action对象,而不是为动作的每个实例定义单独的对象。

<?xml version='1.0' encoding='utf-8' ?>
<interface>
  <menu id="app-menu">
    <section>
        <item>
            <attribute name="label">About</attribute> <attribute name="action">app.about</attribute>
        </item>
        <item>
            <attribute name="label">Quit</attribute> <attribute name="action">app.quit</attribute>
        </item>
    </section>
  </menu>
</interface>

Listing 10-10Pop-up UI File

UI 文件支持的最后一种顶级小部件是弹出菜单,由标记表示。因为弹出菜单和普通菜单是一回事,所以您仍然可以使用元素作为子元素。

正在加载 XML 文件

创建 UI 文件后,需要将它们加载到应用中,并检索必要的小部件。为此,您需要利用由Gtk.ActionGroupGtk.Builder提供的功能。

Gtk.ActionGroup是一组项目,包括名称、股票标识符、标签、键盘快捷键、工具提示和回调方法。可以将每个动作的名称设置为 UI 文件中的动作参数,以将其与 UI 元素相关联。

是一个允许你动态加载一个或多个用户界面定义的类。它会根据相关的动作组自动创建一个加速器组,并允许您根据 UI 文件中的“ID”参数引用小部件。

在清单 10-11 中,Gtk.UIManager从清单 10-10 中的 UI 文件中加载菜单栏和工具栏。结果应用如图 10-7 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 10-7

动态加载的菜单栏和工具栏

应用中的每个菜单项和工具项都连接到空的回调方法,因为这个示例只是为了向您展示如何从 UI 定义中动态加载菜单和工具栏。在本章末尾的两个练习中,您将使用实际内容实现回调方法。

#!/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)

    def change_label(self):
        pass

    def maximize(self):
        pass

    def about(self):
        pass

    def quit(self):
        self.destroy()

    def newstandard(self):
        pass

class Application(Gtk.Application):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, application_id="org.example.myapp",
                         **kwargs)
        self.window = None

    def do_activate(self):
        if not self.window:
            self.window = AppWindow(application=self, title="Hello World!")
            builder = Gtk.Builder()
            builder.add_from_file("./Menu_XML_File.ui")
            builder.add_from_file("./Toolbar_UI_File.xml")
            builder.connect_signals(self.window)
            self.set_menubar(builder.get_object("menubar"))
            self.window.add(builder.get_object("toolbar"))
        self.window.present()
    if __name__ == "__main__":
        app = Application()
        app.run(sys.argv)

Listing 10-11Loading a Menu with Gtk.Builder

测试你的理解能力

下面两个练习概述了你在本章中学到的菜单和工具栏。

除了完成它们之外,您可能想用默认情况下不支持弹出式菜单的其他小部件来创建弹出式菜单的示例。此外,在完成这两个练习后,您应该通过创建自己的股票图标来扩展它们,这些图标用于替换默认项目。

练习 1:工具栏

在第八章中,您使用Gtk.TextView小部件创建了一个简单的文本编辑器。在本练习中,扩展该应用并提供一个操作工具栏,而不是一个充满Gtk.Button小部件的垂直框。

尽管手动创建工具栏是可能的,但在大多数应用中,您希望利用Gtk.Builder方法来创建工具栏。因此,在本练习中使用该方法。你也应该用Gtk.IconFactory创建自己的。

通常,应用将工具栏作为句柄框的子控件来提供是有利的。为您的文本编辑器执行此操作,将工具栏放置在文本视图上方。此外,设置工具栏,使文本描述符显示在每个工具按钮的下方。

第一个练习教你如何构建自己的工具栏。它还向您展示了如何使用Gtk.HandleBox容器。在下一个练习中,您将使用菜单栏重新实现文本编辑器应用。

练习 2:菜单栏

在本练习中,实现与练习 1 相同的应用,只是这次使用菜单栏。你应该继续使用Gtk.Builder,但是菜单不需要包含在Gtk.HandleBox中。

由于菜单项的工具提示不会自动显示,请使用状态栏来提供每个菜单项的更多信息。菜单栏应该包含两个菜单:文件和编辑。您还应该在文件菜单中提供一个退出菜单项。

摘要

在本章中,您学习了创建菜单、工具栏和菜单栏的两种方法。第一种方法是手动方法,这种方法更加困难,但是可以向您介绍所有必要的小部件。

第一个例子向您展示了如何使用基本菜单项来实现进度条的弹出菜单。这个例子被扩展为使用Gtk.Statusbar小部件向用户提供键盘快捷键和更多信息。您还了解了子菜单以及图像、切换和单选菜单项。

下一节将向您展示如何使用带有子菜单的菜单项来实现带有Gtk.MenuShell的菜单栏。该菜单栏可以水平或垂直、向前或向后显示。

工具栏只是水平或垂直的按钮列表。每个按钮包含一个图标和标签文本。您了解了另外三种工具栏按钮:切换按钮、单选按钮和带有补充菜单的工具按钮。

然后,经过大量艰苦的工作,你学会了如何创建动态可加载菜单。每个菜单或工具栏都保存在一个 UI 定义文件中,该文件由Gtk.Builder类加载。构建器将每个对象与适当的动作相关联,并根据 UI 定义创建小部件。

最后,您学习了如何创建自己的自定义图标。创建自己的图标是必要的,因为动作数组需要一个标识符来给动作添加图标。

在下一章中,我们将暂时放下编码,用 Glade 用户界面构建器来介绍图形用户界面的设计。这个应用创建用户界面 XML 文件,可以在应用启动时动态加载。然后,您将学习如何用Gtk.Builder以编程方式处理这些文件。

十一、动态用户界面

到目前为止,您已经学习了大量关于 GTK+ 及其支持库的知识,并且能够创建相当复杂的应用。然而,手动编写所有代码来创建和配置这些应用的小部件和行为会很快变得乏味。

Glade 用户界面构建器允许您以图形方式设计用户界面,从而消除了您编写所有代码的需要。它支持 GTK+ 小部件库以及 GNOME 库中的各种小部件。用户界面保存为 XML 文件,可以动态构建应用的用户界面。

本章的最后一部分介绍了Gtk.Builder,一个可以动态加载 XML 文件的库。创建所有必要的小部件,并允许您连接 Glade 中定义的任何信号。

注意

本章介绍了撰写本文时的 Glade 用户界面。将来这种情况可能会改变,但任何改变都应该是从本章提供的说明开始的简单过渡。

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

  • 设计图形用户界面(GUI)时应该记住的问题

  • 如何用 Glade 设计定制的图形用户界面

  • 如何用Gtk.Builder动态加载 Glade 用户界面

用户界面设计

在这一章中,你将学习如何使用 Glade 3 和Gtk.Builder来实现动态用户界面。然而,谨慎的做法是首先学习一些在设计图形用户界面时应该记住的概念。这些概念可以帮助你在将来避免让用户困惑和沮丧。

你还必须意识到,虽然你知道如何使用你的应用,因为你设计了它,但你需要尽可能地帮助用户理解它。无论用户是专家还是新手,每个用户都应该能够以最短的学习曲线使用您的应用。也就是说,下面的部分包括许多提示和设计决策,可以帮助您实现这种直观性。它们还提高了应用的可维护性。

了解你的用户

设计用户界面时,最重要的是考虑你的受众。他们是否都有处理手头任务的经验,还是有些人比其他人需要更多的帮助?你能模仿他们已经熟悉的用户界面来设计你的用户界面吗,或者这是一个全新的东西?

最大的可能错误之一是对用户的技能水平做出轻率的概括。您可能认为您布局应用的方式是有意义的,但那是因为您设计了它。你应该把自己放在用户的位置上,理解他们对如何使用你的应用没有预先的了解。

为了避免混淆,请花时间研究类似的应用,注意哪些设计决策似乎是成功的,哪些会导致问题。例如,如果你正在创建一个在 GNOME 桌面环境中使用的应用,你应该检查一下 GNOME 人机界面指南( http://developer.gnome.org ),它可以帮助你设计一个用于其他兼容应用的设计。

设计用户界面时要考虑的另一件事是可访问性。用户可能有视力问题,这可能会阻止他们使用应用。Accessibility Toolkit 为 GTK+ 应用提供了许多工具,使它们与屏幕阅读器兼容。GTK+ 也非常依赖于主题,这就是为什么你应该尽可能避免设置字体,或者为用户提供改变字体的方法。

设计用户界面时,语言是另一个需要考虑的因素。首先,你应该总是使用用户熟悉的行话。例如,您可以在工程应用中自由使用数学术语,但不应该在 web 浏览器中这样做。

许多应用在流行时会被翻译成其他语言,如果您使用在其他文化中可能具有攻击性的词语或图像,这可能会引起问题。

保持设计简单

一旦你了解了你的受众,设计一个有效的用户界面就变得简单多了,但是如果界面太难或者太混乱,你仍然会遇到问题。总是试图将屏幕上的窗口小部件减少到一个合理的数量。

例如,如果您需要向用户提供许多选择,但只能选择一个,您可能会尝试使用许多单选按钮。然而,一个更好的解决方案可能是使用一个Gtk.ComboBox,这可以显著减少所需的小部件数量。

对于分组相似的选项组来说,Gtk.Notebook容器非常有用,否则会使一个巨大的页面变得混乱。在许多应用中,这个小部件将相互关联或依赖的小部件组合到一个首选项对话框中。

菜单布局也是另一个有问题的领域,因为它并不总是以合理的方式完成。如果可能,您应该使用标准菜单,如文件、编辑、查看、帮助、格式和窗口。这些菜单对于有计算经验的用户来说是熟悉的,也是用户所期望的。因此,这些菜单也应该包含标准项目。例如,文件菜单应该包含用于操作文件、打印和退出应用的项目。如果您不确定在哪里放置特定的菜单项,您应该研究一下其他应用是如何布局它们的菜单项的。

重复性的工作,或者那些用户经常执行的工作,应该总是变得快速和简单。有多种方法可以做到这一点。最重要的是为许多操作提供键盘快捷键——在键盘上按 Ctrl+O 比单击文件菜单和打开菜单项要快得多。

注意

只要有可能,你应该总是使用标准的键盘快捷键,比如 Ctrl+X 用于剪切,Ctrl+N 用于创建新的东西。这大大缩短了应用用户的初始学习曲线。事实上,一些键盘快捷键已经内置在许多小部件中,例如 Ctrl+X 用于剪切文本小部件中的选择。

你的用户可能需要一段时间来习惯键盘快捷键,这就是为什么工具栏对于重复选项也非常有用。但是,你需要在工具栏上放置太少和太多的项目之间找到一个平衡点。杂乱的工具栏会让用户感到害怕和困惑,但是项目太少的工具栏是没有用的。如果你的工具栏上有大量用户想要的项目,那么允许用户自己定制工具栏是有意义的。

始终保持一致

在设计图形用户界面时,一致性非常重要,GTK+ 使这变得非常容易。首先,GTK+ 提供了许多库存物品,在可能的情况下,应该总是优先使用国产物品。用户将已经熟悉股票项目的图标,并知道如何使用它们。

警告

如果使用不当,库存物品会非常危险。你不应该使用一个库存项目来完成一个它原本不打算做的动作。例如,你不应该仅仅因为 GTK 股票删除图标看起来像一个“减号”就把它用于减法运算图标由用户的主题定义;他们可能不总是像你想象的那样。

说到主题,你应该尽可能依靠主题提供的设置。这有助于您创建一致的外观——不仅在整个应用中,而且在整个桌面环境中。由于主题应用于整个桌面上的所有应用,因此您的应用与用户运行的大多数其他应用是一致的。

在少数情况下,你确实需要偏离用户主题提供的默认值,你应该总是给用户一个方法来改变设置或者仅仅使用系统默认值。这在处理字体和颜色时尤其重要,因为您的更改可能会使您的应用无法用于某些主题。

一致性的另一个好处是用户可以更快地学会如何使用你的应用。用户只需要学习一个设计,而不是许多。如果你的应用和补充对话框没有使用一致的布局,那么每个新窗口都会给用户带来一次全新的体验。

让用户了解情况

如果应用长时间没有响应,用户会很快失去兴趣。大多数计算机用户都习惯了一两个 bug,但是如果你的应用正在处理信息并且长时间没有响应,用户可能会放弃。

为了避免这种情况,有两种可能的解决方案。首先是让你的应用更有效率。但是,如果您的应用没有问题,或者没有办法让它更有效,您应该使用进度条。进度条告诉用户您的应用仍在工作。只要确保更新你的进度条!如果您不知道这个过程需要多长时间,另一个选择是脉动进度条,并提供消息来更新用户的进程进度。

另外,请记住第章 3 中的以下循环。

while Gtk.events_pending():
    Gtk.main_iteration()

这个循环确保用户界面得到更新,即使处理器正忙于处理另一个任务。如果在 CPU 密集型过程中不更新用户界面,应用可能会在完成之前对用户没有响应!

您还应该在执行操作时向用户提供反馈。如果正在保存文档,您应该将其标记为未修改,或者在状态栏中显示一条消息。如果您在执行某个动作时没有向用户提供反馈,则可以认为该动作没有被执行。

消息对话框是提供反馈的一种非常有用的方式,但是它们应该只在必要的时候使用。如果消息对话框出现得太频繁,用户会感到沮丧,这就是为什么只有严重错误和警告才应该这样报告。

我们都会犯错

不管你是专家还是新手,我们都会犯错。正因为如此,你应该永远原谅你的用户。毕竟,每个人都曾按下过错误的按钮,导致大量工作的丢失。在设计合理的应用中,这种情况永远不会发生。

对于用户不容易撤销的基本操作,您应该提供撤销操作的能力。例如,这些基本操作可能包括从我们的杂货列表应用中删除一个项目,或者在文本视图中移动文本。

对于无法撤消的操作,您应该始终提供一个确认对话框。它应该明确声明这个操作不能撤销,并询问用户是否要继续。例如,您应该始终询问用户,当有未保存更改的文档时,是否应该关闭应用。人们已经使用软件很多年了,并且已经开始期待一个确认对话框来处理不能撤销的操作。

Glade 用户界面生成器

决定 GUI 工具包成败的一个因素是它能否快速部署应用。虽然用户界面对应用的成功极其重要,但它不应该是开发过程中最耗费精力的方面。

Glade 是一个工具,允许您快速有效地设计图形用户界面,以便您可以转移到代码的其他方面。用户界面被保存为 XML 文件,该文件描述了小部件的结构、每个小部件的属性以及与每个小部件相关联的任何信号处理程序。Gtk.Builder然后可以加载用户界面文件,以便在应用加载时动态构建它。这允许您从美学角度改变用户界面,而无需重新编译应用。

注意

Glade 的旧版本允许您生成源代码,而不是将用户界面保存在 XML 文件中。不推荐使用此方法,因为当您想要更改用户界面时很难管理它。因此,您应该遵循本章提供的方法。

你需要从一开始就意识到什么是 Glade,什么不是。Glade 设计应用的用户界面,设置与代码中实现的回调方法相关联的信号,并处理常见的小部件属性。然而,Glade 不是代码编辑器或集成开发环境。它输出的文件必须由您的应用加载,并且您必须在代码中实现所有回调方法。Glade 只是为了简化初始化应用的图形用户界面和连接信号的过程。

小费

本书中使用的 Glade 3.22.1 版本现在允许集成开发环境(如 Anjuta)将其嵌入到用户界面中。这些 ide 为部署 GTK+ 应用提供了一个完整的、自始至终的解决方案。

Glade 的另一个优点是,由于用户界面存储为 XML 文件,它们独立于语言。任何封装了Gtk.Builder提供的功能的语言都可以加载用户界面。这意味着无论您选择哪种编程语言,都可以使用相同的图形用户界面设计器。

在继续本章的其余部分之前,您应该从操作系统的包管理器安装 Glade 和用于Gtk.Builder的开发包。或者,您可以从glade. gnome.org 下载并编译源代码。

此外,在阅读本章的其余部分时,您应该确保遵循并创建这个应用。这为您提供了一个学习 Glade 3 应用的机会,因此您可以在这本书的指导下尽可能多地进行实践。

Glade 界面

当您第一次启动 Glade 时,您会看到一个包含三个窗格的主窗口:主窗口树视图、小部件调色板和小部件属性编辑器。图 11-1 是 Glade 应用主窗口的屏幕截图,其中有一个从FileBrowser.glade打开的项目。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-1

Glade 主窗口

主树视图窗口有助于 Glade 项目管理。主窗口标题栏显示当前打开的项目列表,允许您在它们之间切换。左窗格还包括小部件树视图,它显示了具有焦点的项目的小部件包含。

小部件树视图显示了项目中的父子容器关系。可以有多个顶级小部件。然而,在图 11-1 中窗口是FileBrowser.glade的唯一顶层小部件。

此窗格是您指定项目选项、保存项目和加载现有项目的地方。此窗口中的弹出式菜单还提供了许多其他选项,可以帮助您处理项目,如撤销和重做操作。

注意

如果您决定使用 Glade 2 而不是 Glade 3,请确保经常保存。在 Glade 的旧版本中没有实现撤销和重做支持,如果您因为一次错误的鼠标点击而意外地覆盖了一个小时的工作,那将是非常令人沮丧的!

当您启动 Glade 3 时,显示的中间窗格有用于从小部件面板中选择小部件的按钮,该面板列出了所有可用于设计应用的小部件。图 11-2 中显示了其中一个小工具调色板的屏幕截图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-2

Glade widget 调色盘

默认情况下,可以显示五种类别的小部件:顶级小部件、容器、用于控制的小部件、显示小部件以及复合和贬值小部件。您不应该在新的应用中使用 GTK+ 废弃列表中的任何小部件,因为它们已经过时,可能会在未来的版本中被删除。

除了默认类别的小部件之外,您可能会发现包含其他小部件库的其他类别。这些包括为 GNOME 库或其他自定义小部件库添加的小部件。

通过视图菜单,您可以更改小部件面板的布局。图 11-2 显示了一个设置为显示图标和文本的组件面板。但是,根据您最喜欢的样式,您可以只显示文本或图标。

要向小部件布局窗格添加新的顶级小部件,只需在 Toplevels 部分单击所需小部件的图标。然后会显示一个新的顶级小部件,并将其添加到左窗格的小部件树中。要添加非顶级小部件,您需要首先单击所需小部件的图标,然后在应该放置小部件的位置单击鼠标。您必须单击容器小部件中的空单元格,以便将非顶级小部件插入到用户界面中。

创建窗口

在本章中,你将使用 Glade 和Gtk.Builder创建一个简单的文件浏览器应用。首先,您可以通过单击主 Glade 窗口顶部的 new project 按钮来创建一个新项目,或者使用应用加载时为您创建的空白项目。如果您稍后返回本教程,您可以通过单击主 Glade 窗口顶部的 open 按钮来打开一个现有的项目。

拥有一个空白项目后,您可以通过单击 Toplevels 小部件面板中的窗口图标来创建一个新的顶级Gtk.Window。在新窗口中,您会看到小部件内部的网格图案,如图 11-3 所示。该模式指定了一个区域,可以在该区域将子部件添加到容器中。从小部件面板中选择一个非顶级小部件后,必须单击此区域将小部件添加到容器中。按照此方法添加所有非顶级小部件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-3

默认的 Gtk。窗口小部件

创建顶层窗口后,您会注意到小部件属性窗格的内容发生了变化,如图 11-4 所示。在这个面板中,您可以自定义 Glade 支持的每个小部件的所有属性。

注意

虽然 Glade 允许您编辑许多小部件属性,但有些操作只需在代码中执行。因此,你不应该把 Glade 看作是你在本书中学到的所有东西的替代品。在大多数应用中,您仍然在进行大量的 GTK+ 开发。

图 11-4 中显示的微件属性窗口有各种选项的完整列表。该窗格分为几个部分,这些部分对特定于当前所选小部件类型的基本选项进行了分类。例如,Gtk.Window小部件允许您指定窗口的类型、标题、调整大小的能力、默认大小等等。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-4

小部件属性窗格

ID 字段被滚动到图 11-4 中滚动窗口的边界之外,为小部件提供了一个唯一的名称。Glade 会自动为每个小部件指定一个名称,这个名称对于当前项目来说是唯一的,但是这些都是通用名称。如果您计划从应用中引用一个小部件,您应该给它一个有意义的 ID。当您必须加载三个名为 treeview1、treeview2 和 treeview3 的Gtk.TreeView3小部件时,这很容易让人感到困惑!

“打包”选项卡提供了关于微件如何对其父微件的大小变化做出反应的基本信息,如扩展和填充。通用属性是由Gtk.Widget提供的,并且对所有小部件都可用。例如,您可以在该选项卡中提供尺寸请求。

注意

第一次使用 Glade 时,打包选项有点不直观,因为属性是由子容器而不是父容器设置的。例如,Gtk.Box的子容器的打包选项在子容器本身的打包选项卡中提供,而不是在父容器中。

信号选项卡允许您为通过Gtk.Builder连接的每个微件定义信号。最后,由残障符号指定的“辅助功能”选项卡提供了用于辅助功能支持的选项。

正如您在本书的第一个例子中回忆的那样,空的Gtk.Window小部件除了演示如何创建之外没有任何用处。由于文件浏览器需要将多个小部件打包到这个应用的主窗口中,下一步是添加一个垂直的盒子容器。从面板中选择 Box 小部件,并在窗口的网格图案内单击,将一个Gtk.Box小部件插入到窗口中。然后,您可以使用属性窗格来调整框的方向(垂直或水平)以及Gtk.Box包含的窗格数量。图 11-5 显示了Gtk.Box属性所需的调整。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-5

默认的 Gtk。窗口小部件

默认情况下,创建三个单元格来存放子部件,但是您可以将它更改为大于零的任意数量的项目。默认的三是我们需要多少个子部件。

默认情况下,Gtk.Box的方向是垂直的,但如果需要,您可以将方向改为水平。

注意

如果您不确定容器将容纳多少个小部件,请不要担心。您可以在微件属性窗格的常规选项卡中添加或移除单元格。然后,您可以在打包选项卡下更改小部件在框中的位置。在由Gtk.Builder构建之后,您仍然可以用您的代码编辑用户界面!

添加垂直框后,您会看到三个单独的空容器网格;请注意属性窗格和小部件树视图窗格中的变化。对于这些网格,我们将添加一个工具栏、一个地址栏和一个树形视图。

添加工具栏

旧的 handle box 小部件早就被弃用了,因为它原本要包含的大多数小部件已经得到了增强,可以动态隐藏它们的内容。Gtk.Toolbar是以这种方式增强的小部件之一。这意味着我们可以直接将工具栏添加到之前添加到主窗口的垂直Gtk.Box中。

添加工具栏小部件时,它仅作为一条细条出现在垂直框的顶部窗格中。这是因为它还不包含任何按钮。向工具栏添加按钮的方法并不明显。要在工具栏上添加按钮,右键单击 Gtk。Glade 树视图窗格中的工具栏条目和一个标有“编辑”的弹出菜单…出现,然后显示图 11-6 中的对话框。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-6

工具栏编辑器

工具栏编辑器允许您向工具栏添加任何受支持类型的项目。要添加新项目,您只需点击“添加”按钮。这将在编辑器对话框中显示一个窗格,您可以在其中修改新按钮的属性。这里要小心,因为你的 Glade 版本可能会显示使用股票按钮的选项。股票项目都已被否决,所以你必须创建自己的自定义按钮。

添加新的工具按钮后,下一步是通过从类型组合框中选择一个选项来选择小部件的类型。组合框中包含的工具栏项类型是包含图像和标签的通用工具按钮、切换按钮、单选按钮、菜单工具按钮、工具项和分隔符。当您选择新类型时,对话框会立即发生变化,允许您编辑所选类型的属性。

例如,在图 11-6 中,选择的工具按钮属于Gtk.MenuToolButton类型。每个工具栏项都提供了一个选项,当工具栏是水平还是垂直时,它应该是可见的。这允许您在工具栏为垂直方向时隐藏工具栏项目,而在工具栏为水平方向时向用户显示。

菜单工具按钮还允许您选择要在工具项目中显示的标签和图像。根据您选择的选项,图像可以是库存图像、现有图像文件或自定义图标主题的标识符。

在工具栏编辑器的底部,您会看到一个树形视图,允许您将信号连接到每个工具按钮。Glade 提供了许多命名的回调方法供您选择,这些方法基于信号名和您给工具栏项的名称。您还可以输入自己的自定义回调方法名。可以通过Gtk.Builder指定要传递给每个方法函数的数据,因此通常可以将“用户数据”参数留空。在图 11-6 中,一个名为on_back_clicked()的回调方法被连接到Gtk.MenuToolButton"clicked"信号上。

当您用Gtk.Builder加载用户界面时,您有两种选择来连接 Glade 文件中定义的回调方法和代码中的回调方法。如果您想要手动连接每个回调方法,您可以任意命名信号处理程序,只要该名称是唯一的。然而,Gtk.Builder提供了一个函数,可以自动将所有信号连接到可执行文件或 Python 程序中的适当符号。要使用这个特性,您在 Glade 中定义的回调方法名必须与您代码中的函数名相匹配!

“打包”选项卡包括用于确定小部件周围的填充、打包是从框的开始还是结束,以及确定小部件在容器中的位置的选项。这些属性完全等同于您在使用box.pack_start()和朋友向Gtk.Box添加子部件时使用的设置。

小费

你应该记得在第四章中提供了一个表格,说明了扩展和填充属性对 Gtk 的子部件做了什么。框小部件。Glade 为您提供了一个绝佳的机会来试验打包选项,以便更好地理解它们是如何影响小部件的。因此,请花点时间尝试各种包装方案!

完成工具栏和打包参数设置后,您的应用应该如图 11-7 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-7

运行中的工具栏

图 11-7 所示的工具栏包含两个菜单工具按钮,用于在用户浏览历史中向前和向后移动。还有用于移动到父目录、刷新当前视图、删除文件、移动到主目录以及查看文件信息的工具按钮。每个工具按钮都连接到一个回调方法,您必须在应用的代码中实现该方法。

完成文件浏览器

创建文件浏览器的下一步是创建地址栏,向用户显示当前位置,并允许他们输入新位置。这意味着我们需要一个有三个部件的水平框,如图 11-8 所示。这三个小部件是描述保存在Gtk.Entry小部件中的内容的标签,保存当前位置的Gtk.Entry小部件,以及按下时移动到该位置的按钮。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-8

文件浏览器

为了创建图 11-8 中的按钮,一个带有两个子部件的水平Gtk.Box被添加到按钮中:一个Gtk.Image部件设置为 GTK _ 股票 _ 跳转 _ 股票图像,一个Gtk.Label部件名为 Go。

最后一步是向垂直框中的最后一个单元格添加一个Gtk.ScrolledWindow小部件,向该容器添加一个Gtk.TreeView小部件。完成后的文件浏览器用户界面如图 11-9 所示。然而,我们还没有完成在 Glade 中编辑应用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-9

文件浏览器

做出改变

文件浏览器已经完全设计好了,但现在我决定它应该在窗口底部包含一个Gtk.StatusBar小部件!对用户界面进行更改可能很棘手,因此本节将带您完成一些具有挑战性的操作。

添加状态栏的第一步是扩展主垂直Gtk.Box小部件包含的子小部件的数量。为此,从小部件树视图中选择垂直框。在“属性”窗格中,您可以使用“常规”选项卡中的“项目数”属性来增加子项的数量。这将在垂直框的末尾添加一个新的空白空间,您可以在其中添加状态栏小部件。

如果需要对垂直或水平框的子元素重新排序,首先需要选择要移动的小部件。然后,在“属性”窗格的“打包”选项卡下,您可以通过更改其微调按钮的值来选择新位置。当您更改微调按钮的值时,您可以看到子部件移动到它的新位置。周围子部件的位置会自动调整以反映这些变化。

如果您决定需要将一个容器填充到已经添加了另一个小部件的位置,就会导致另一个有问题的任务。例如,假设您已经决定在文件浏览器应用中放置一个水平窗格来代替滚动窗口。您首先需要从主窗口的小部件树视图中选择小部件,并通过按 Ctrl+X 移除它。之后,会显示一个空框,您可以在其中添加水平窗格。接下来,选择应该放置滚动窗口的窗格,并按 Ctrl+V。

在 Glade 2 中,修改用户界面曾经是一个敏感的话题,因为它不支持撤销和重做操作。过去,由于不小心删除了顶级小部件,很容易犯错误并损失几个小时的工作,因为您不能撤销任何操作。既然 Glade 3 包含了撤销和重做支持,您就不必担心了。

部件信号

这个应用的最后一步是为所有的小部件设置信号。图 11-10 显示了 Go 按钮的小部件属性编辑器的信号选项卡。Gtk.Button小部件连接到被点击的信号,该信号在发出时调用on_button_clicked()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-10

widget 信号编辑器

除了“点击”信号,你还需要连接其他几个信号。除分隔符外,每个工具项都应连接到Gtk.ToolButton的点击信号。此外,您应该连接Gtk.Entry来激活,当条目有焦点时,当用户按 Enter 键时会发出此消息。

注意

这个应用只是一个简单的文件浏览器的设计,旨在向您展示如何使用 Glade 3 设计应用。应用不仅仅是一个设计所需的代码在第十四章中实现。

至于树视图,您应该将其连接到行激活的。当一行被激活时,向用户显示关于该文件的更多信息,或者它导航到所选择的目录。表 11-1 中提供了一个小部件列表以及它们的信号和回调方法,这样你可以很容易地理解这个例子。

表 11-1

部件信号

|

小部件

|

描述

|

信号

|

回调方法

|
| — | — | — | — |
| Gtk.Button | 转到按钮 | “已点击” | on_go_clicked() |
| Gtk.Entry | 位置条目 | “激活” | on_location_activate() |
| Gtk.MenuToolButton回 |   | “已点击” | on_back_clicked() |
| Gtk.MenuToolButton前进 |   | “已点击” | on_forward_clicked() |
| Gtk.ToolButton | 起来 | “已点击” | on_up_clicked() |
| Gtk.ToolButton | 恢复精神 | “已点击” | on_refresh_clicked() |
| Gtk.ToolButton | 主页 | “已点击” | on_home_clicked() |
| Gtk.ToolButton | 删除 | “已点击” | on_delete_clicked() |
| Gtk.ToolButton | 信息 | “已点击” | on_info_clicked() |
| Gtk.TreeView | 文件浏览器 | “行激活” | on_row_activated() |
| Gtk.Window | 主窗口 | “摧毁” | on_window_destroy() |

创建菜单

除了工具栏,在 Glade 3 中还可以创建菜单。图 11-11 所示为菜单栏编辑器,与工具栏编辑器非常相似。它支持普通的菜单项和那些用图像、复选按钮、单选按钮和分隔符呈现的菜单项。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-11

菜单栏编辑器

警告

Glade 3.22.1 编辑器目前仍然使用库存项目作为菜单项。所有的股票项目都被否决,所以你真的应该使用自己的自定义菜单项,只有这个版本的 Glade 不支持自定义菜单项。您可能需要编辑 Glade 生成的 XML 来创建您自己的定制条目。

您现在知道了创建菜单的三种方法;这就产生了一个问题,到底哪一个最好。每种方法都有其优缺点,所以让我们来看看每种方法。

您首先学习了如何手动创建菜单,根据您的需要塑造每个对象。这种方法适用于较小的菜单,因为代码不会占用太多空间,而且实现完全在一个地方。但是,如果您的菜单变大或者包含的内容不仅仅是基本的项目,那么维护代码会变得很繁琐,并且会占用大量空间。

接下来,您学习了如何使用带有 UI 定义的Gtk.Builder来动态创建菜单。这种方法简化了菜单的创建,因为您可以在很小的空间内定义大量的动作。此外,因为菜单是由 UI 定义构造的,所以允许用户编辑菜单非常简单。如果不使用 Glade 设计应用,这显然是创建菜单的首选方法。

Glade 还提供了一个非常吸引人的菜单创建方法,因为在它的初始设计之后,维护是简单的。它也不需要代码来创建菜单,因为Gtk.Builder已经为您构建好了。然而,这种方法的一个问题是,它不像 UI 文件方法那样容易允许用户改变菜单和工具栏的布局。

一种容易采用的方法是将所有的小部件打包到垂直框的末端,或者打包到主窗口的子窗口。然后,当您的应用加载时,您可以简单地用box.pack_start()Gtk.Builder创建的菜单打包到窗口中。然而,如果您不需要允许您的用户定制菜单,那么通过 Glade 创建所有菜单是有意义的。

现在您已经完成了用户界面的创建,您可以将它保存为一个FileBrowser.glade文件,其中 project 可以替换为您选择的名称。可以根据应用的位置或从绝对路径加载该文件。

使用 Gtk。建设者

在 Glade 中设计应用后,下一步是用Gtk.Builder加载用户界面。

这个 GTK+ 类解析 Glade 用户界面,并在运行时创建所有必要的小部件。

提供创建和保存从 XML 文件加载的用户界面所需的方法。它还可以将添加到 Glade 文件中的信号连接到应用中的回调方法。

Gtk.Builder的另一个优点是开销仅在初始化期间增加,与直接从代码创建的接口相比,这是可以忽略的。初始化之后,实际上不会给应用增加任何开销。例如,Gtk.Builder内部连接信号处理程序的方式与您自己的代码相同,因此这不需要额外的处理。

由于Gtk.Builder处理所有的小部件初始化,并且布局已经在 Glade 3 中设计好了,所以代码库的长度可以大大减少。以清单 11-1 为例,如果您必须手工编写所有代码,那么清单会长得多。

#!/usr/bin/python3

import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk

class SignalHandlers():

    def on_back_clicked(self, button ):
        pass

    def on_forward_clicked(self, button ):
        pass

    def on_up_clicked(self, button ):
        pass

    def on_refresh_clicked(self, button ):
        pass

    def on_home_clicked(self, button ):
        pass

    def on_delete_clicked(self, button ):
        pass

    def on_info_clicked(self, button ):
        pass

    def on_go_clicked(self, button ):
        pass

    def on_location_activate(self, button ):
        pass

    def on_row_activated(self, button ):
        pass

    def on_window_destroy(self, button ):
        pass

class Application(Gtk.Application):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, application_id="org.example.myapp",
                         **kwargs)
        self.window = None

    def do_activate(self):
        if not self.window:
            builder = Gtk.Builder()
            builder.add_from_file("./FileBrowser.glade")
            self.window = builder.get_object("main_window")
            self.add_window(self.window)
            builder.connect_signals(SignalHandlers())
            self.add_window(self.window)
        self.window.show_all()

if __name__ == "__main__":
    app = Application()
    app.run(sys.argv)

Listing 11-1Loading the User Interface

加载用户界面

加载 Glade 用户界面是通过builder.add_from_file()完成的。这是您应该调用的第一个Gtk.Builder方法,尽管它应该在获得一个Gtk.Builder的实例后调用。它解析 XML 文件提供的用户界面,创建所有必需的小部件,并提供翻译工具。builder.add_from_file()方法需要的唯一参数是 Glade 项目文件的路径。

builder = Gtk.Builder()
builder.add_from_file("./FileBrowser.glade")

接下来,您需要获取"main_window",连接所有信号,最后将窗口添加到Gtk.Application类实例中。

self.window = builder.get_object("main_window")
builder.connect_signals(SignalHandlers())
self.add_window(self.window)

builder.get_object()需要一个参数,它是您在 Glade 项目中分配给Gtk.Window主窗口的 ID。由此Gtk.Builder可以通过读取 XML 来确定属于主窗口的所有子窗口部件。然后,它可以从 XML 定义中构造窗口。

构建主窗口后,我们需要分配所有的信号处理程序。如果我们提供一个只包含信号处理方法的特殊 Python 类,就可以自动完成这项工作。builder.connect_signals()方法通过将我们的信号处理程序类的实例作为参数提供给它来做到这一点。

最后,我们需要将由Gtk.Builder构造的窗口添加到我们的Gtk.Application中。这个窗口现在由我们的Gtk.Builder实例控制。虽然它不是一个完整的Gtk.ApplicationWindow,但就控制新窗口而言,它非常像一个完整的Gtk.ApplicationWindow。注意,我们使用window.show_all()而不是window.present()方法来显示窗口,因为我们的新窗口没有present()方法。

真的就这么简单。文件浏览器窗口立即出现,您可以关闭并运行。剩下要做的就是填充所有的信号处理程序方法,为Gtk.TreeView小部件创建存储,构建窗口初始化代码,这样就有了一个可以工作的应用。

测试你的理解能力

这两个练习对于你成为一个熟练的 GTK+ 开发者尤为重要。以编程方式设计大型应用的每个方面是不现实的,因为这需要太长时间。

相反,你应该使用 Glade 来设计用户界面,并使用Gtk.Builder来加载设计和连接信号。通过这样做,您能够快速完成应用的图形化方面,并获得使您的应用工作的后端代码。

练习 1: Glade 文本编辑器

本练习实现了 Glade 中“测试您的理解”练习 1 部分的文本编辑器。文本编辑器中的工具栏应该完全在 Glade 中实现。

如果你还有上一章的练习解决方案,这个练习应该不需要额外的编码。你也可以在本书的网站 www.gtkbook.com 找到“测试你的理解”部分的答案。这个练习让您有机会了解 Glade 3,并测试许多小部件属性。

在设计了带有工具栏的应用后,添加菜单栏是一个简单的过渡。在较大的应用中,您应该向用户提供这两个选项。在以下练习中,您将向文本编辑器应用添加一个菜单栏。

练习 2:带菜单的 Glade 文本编辑器

您已经实现了带有菜单栏的文本编辑器。在本练习中,使用 Glade 和 Gtk.Builder 重新设计该练习中的应用。首先,您应该使用 Python 和 GTK+ 实现菜单,这允许您同时使用这两种语言。其次,您应该在 Glade 中再次实现该菜单。

与上一个练习一样,练习 2 的解在 www.gtkbook.com 。使用可下载的解决方案可以让你跳过编写回调函数,因为你已经在前一章中完成了。

摘要

在这一章中,我们暂时停止了编码,研究了设计图形用户界面时需要考虑的问题。简而言之,你必须时刻记住你的用户。你需要知道你的用户期望什么,并在应用的每个方面满足他们的需求。

接下来,您学习了如何使用 Glade 3 设计图形用户界面。当考虑 GUI 工具包时,快速部署应用的图形方面的能力是必要的,GTK+ 有 Glade 来满足这一需求。

Glade 允许您设计用户界面的各个方面,包括小部件属性、布局和信号处理程序。用户界面保存为描述应用结构的可读 XML 文件。

在 Glade 3 中设计了一个应用后,可以用Gtk.Builder动态加载用户界面。这个 GTK+ 类解析 Glade 用户界面,并在运行时创建所有必要的小部件。它还提供了将 Glade 中声明的信号处理程序连接到应用中的回调方法的函数。

在下一章,我们将回到编码,并深入研究GObject系统的复杂性。您将学习如何通过派生新的小部件和类来创建自己的GObject类,以及如何从头开始创建一个小部件。

十二、自定义小部件

到目前为止,您已经了解了很多关于 GTK+ 及其支持库的知识。您已经掌握了足够的知识,可以使用 PyGTK 提供的小部件来创建自己的复杂应用。

然而,你还没有学会的一件事是如何创建你自己的小部件。因此,本章致力于从现有的 GTK+ 类中派生新的类。我们将通过一些例子向您展示使用 PyGTK 实现这一点是多么容易。

在本章中,您将学习如何从 GTK+ 小部件中派生出新的类和小部件。我们提供了几个如何做到这一点的例子,并讨论了在这个过程中可能遇到的一些问题。

图像/标签按钮

从 GTK+ 3.1 开始,所有库存物品都被弃用。虽然我同意这个决定,但我对Gtk.Button没有被扩展到包含一个按钮选项来显示图像和文本感到失望。去掉 use-stock 属性后,Gtk.Button只能显示文本或图像,但不能同时显示两者。

解决这个问题的方法很容易实现,但是非常重复,而且根本不是面向对象的。您可以在“使用按钮”一节中看到一个如何实现该变通方法的示例。你可以很容易地看到,如果你有很多按钮要编码,这个解决方案将是非常重复的,并且你没有很好地利用这个实现的代码重用。

另一个争论点是程序员被迫从字符串中查找他们想要的真实图像。如果新的实现为您做了这些工作,并且您需要向新的小部件提供的只是查找字符串,那会怎么样呢?毕竟,您可能希望使用来自用户默认主题的图像,所以让新的小部件来完成所有工作。

图 12-1 显示了清单 12-1 所示程序创建的图像标签按钮。这个简单的实现展示了如何扩展标准Gtk.Button的功能和风格。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-1

工作中的 ImageLabelButton

清单 12-1 展示了ImageLabelButton的类实现。

#!/usr/bin/python3

import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk

class ImageLabelButton(Gtk.Button):

    def __init__(self, orientation=Gtk.Orientation.HORIZONTAL,
                  image="image-missing", label="Missing", *args,
                  **kwargs):
        super().__init__(*args, **kwargs)
        # now set up more properties
        hbox = Gtk.Box(orientation, spacing=0)
        if not isinstance(image, str):
            raise TypeError("Expected str, got %s instead." % str(image))
        icon_theme = Gtk.IconTheme.get_default()
        icon = icon_theme.load_icon(image, -1,
                                    Gtk.IconLookupFlags.FORCE_SIZE)
        img = Gtk.Image.new_from_pixbuf(icon)
        hbox.pack_start(img, True, True, 0)
        img.set_halign(Gtk.Align.END)
        if not isinstance(label, str):
            raise TypeError("Expected str, got %s instead." % str(label))
        if len(label) > 15:
            raise ValueError("The length of str may not exceed 15 characters.")
        labelwidget = Gtk.Label(label)
        hbox.pack_start(labelwidget, True, True, 0)
        labelwidget.set_halign(Gtk.Align.START)
        self.add(hbox)

class AppWindow(Gtk.ApplicationWindow):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_border_width(25)
        button = ImageLabelButton(image="window-close", label="Close")
        button.connect("clicked", self.on_button_clicked)
        button.set_relief(Gtk.ReliefStyle.NORMAL)
        self.add(button)
        self.set_size_request(170, 50)

    def on_button_clicked(self, button):
        self.destroy()

class Application(Gtk.Application):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, application_id="org.example.myapp",
                         **kwargs)
        self.window = None

    def do_activate(self):
        if not self.window:
            self.window = AppWindow(application=self,
                                    title="ImageLabelButton")
            self.window.show_all()
            self.window.present()

if __name__ == "__main__":
    app = Application()
    app.run(sys.argv)

Listing 12-1ImageLabelButton Class Implementation

要理解的第一点是,当创建一个Gtk.Button时,按钮的样式是在分配 image 或 label 属性时设置的。一旦指定,按钮的样式就不能更改。新款ImageLabelButton也是如此。

为了开始我们的讨论,让我们仔细看看小部件的初始化。我们允许两个新属性并覆盖一个现有属性。属性标签覆盖父属性,但使用方式与标签小部件的文本相同。属性方向和图像是新的。它们分别用于指定标签/图像的方向(水平或垂直)和字符串名称,以查找相应的默认主题图标。

其余的初始化代码很简单。使用默认方向或关键字参数指定的方向创建一个Gtk.Box。接下来,如果指定了 image 关键字,则在默认用户主题中查找名称,获取图标,并将图像添加到Gtk.Box。接下来,如果指定了标签,则创建一个Gtk.Label,并将其添加到Gtk.Box。最后,将框添加到按钮。

我们通过调整图像和标签文本的对齐方式改变了Gtk.ImageLabelButton类,这样无论按钮的大小如何,它们都保持居中。我们使用了set_halign()方法,并关闭了pack_start()方法中使用的填充和扩展属性。

注意,我们没有覆盖底层Gtk.Button的任何其他方法或属性。在这种情况下,没有必要以任何其他方式修改按钮。ImageLabelButton表现得像普通的Gtk.Button一样。因此,我们已经完成了创建一个新的按钮类的任务。

最重要的是,新类中有一些错误检测代码来捕捉无效的数据类型和值。您提供这种参数检查,这一点怎么强调都不为过。缺少适当的错误消息和适当的错误检测会毁掉您为新类所做的所有工作,因为它没有提供足够的调试信息来纠正甚至是很小的错误或问题,这将导致您的类被废弃。

自定义消息对话框

子类化 GTK+ 小部件的另一个原因是通过将更多的行为集成到小部件中来节省工作。例如,在显示对话框之前,标准的 GTK+ 对话框需要大量的初始化工作。通过将标准的外观和感觉集成到所有的消息对话框中,您可以解决重复的工作量。

减少创建对话框所需工作量的方法是创建一个包含您需要的所有功能的设计,使用默认设置或可以激活附加选项/值的参数。在清单 12-2 中,让我们看看一个定制的问题对话框,看看它是如何工作的。

class ooQuestionDialog(Gtk.Dialog):

hbox = None
vbox = None

    def __init__(self, title="Error!", parent=None,
                  flags=Gtk.DialogFlags.MODAL, buttons=("NO",
                  Gtk.ResponseType.NO, "_YES",
                           Gtk.ResponseType.YES)):
        super().__init__(title=title, parent=parent, flags=flags,
                         buttons=buttons)
        self.vbox = self.get_content_area()
        self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)

        icon_theme = Gtk.IconTheme.get_default()
        icon = icon_theme.load_icon("dialog-question", 48,
                                    Gtk.IconLookupFlags.FORCE_SVG)
        image = Gtk.Image.new_from_pixbuf(icon)
        self.hbox.pack_start(image, False, False, 5)
        self.vbox.add(self.hbox)

    def set_message(self, message, add_msg=None):
        self.hbox.pack_start(Gtk.Label(message), False, False, 5)
        if add_msg != None:
           expander = Gtk.Expander.new_with_mnemonic( \ "_Click
                me for more information.")
           expander.add(Gtk.Label(add_msg))
           self.vbox.pack_start(expander, False, False, 10)

    def run(self):
        self.show_all()
        response = super().run()
        self.destroy()
        return response

Listing 12-2A Customized Question Dialog Implementation

这个对话框有一个预定义的设计,对于我们所有的消息对话框都是通用的。它包含以下元素。

  • 每种类型的消息对话框都有单独的类。

  • 对话框总是包含一个图标。显示的图标取决于显示的对话框类型(消息、信息、错误等)。).

  • 对话框总是显示主要消息。

  • 显示的按钮的数量和类型有一个逻辑默认值,用户可以覆盖它。

  • 所有对话框默认为模态。

  • 对话框中还会显示一条附加消息。它包含在一个扩展器中,可以在对话框显示的任何时候使用。

  • 类别提供了两个额外的方法。第一种方法set_message(),设置主要对话消息和可选的附加消息。第二个方法,run(),显示对话框,运行对话框,销毁对话框,并返回response_id。如果您想要显示一个非模态对话框,那么run()方法是可选的。当然,你必须在run()对话框中提供额外的功能来实现。

实例化和运行对话框非常简单。以下代码执行打开对话框所需的所有任务。

dialog = ooQuestionDialog(parent=parentwin)
dialog.set_message("This is a test message.\nAnother line.",
                   add_msg="An extra message line.)
response = dialog.run()

很明显,将自定义设计加载到对话框中既有优点也有缺点。主要的缺点是将设计和功能结合在一起。最大的好处是,如果你想改变设计,只有一个地方可以修改。

从这个例子中,用户可以很容易地为错误、消息、信息和警告对话框创建类似的子类。请记住,一致性是这项任务的关键。

多线程应用

多线程应用是任何高端 GTK+ 应用的核心,它是任何利用数据库、网络通信、客户机-服务器活动、进程间通信和任何其他使用长时间运行事务的进程的应用。所有这些应用都需要多个进程或线程来管理与独立实体之间的通信,以便相互提供和接收信息。

GTK+ 是一个单线程库。从多个线程访问其 API 不是线程安全的。所有 API 调用必须来自应用的主线程。这意味着长时间运行的事务会使用户界面看起来冻结,有时会持续很长时间。

解决这个问题的关键是将所有长时间运行的事务转移到其他线程。但是,这并不容易,因为它涉及到设置线程和为两个或多个线程或进程提供某种类型的线程安全通信。

大多数关于图形用户界面的书籍通常会忽略这个问题,而专注于图形用户界面本身。这对读者来说是一种极大的伤害,因为读者在职业生涯中遇到的几乎所有 GUI 应用都是多线程的,但是读者对这种类型的应用没有经验。

本书提供了一个示例,让您更好地了解多线程应用的样子以及如何组织它的基础知识。该示例不是构建多线程应用的唯一方法,但它确实提供了这种应用的所有基础。对于您的项目,细节和方法可能有所不同,但是您遵循的是我们的示例所提供的相同的基本大纲。

清单 12-3 是多线程应用的例子。这是一个非常简单的程序,它从另一个线程请求信息,主线程正确地等待供应商线程提供数据。我们在清单之后详细描述了这个例子。

#!/usr/bin/python3

import sys, threading, queue, time
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk

def dbsim(q1, q2):
    while True:
        data = q1.get()
        # the request is always the same for our purpose
        items = {'lname':"Bunny", 'fname':"Bugs",
                 'street':"Termite Terrace", 'city':"Hollywood",
                 'state':"California", 'zip':"99999", 'employer':"Warner
                 Bros.", 'position':"Cartoon character", 'credits':"Rabbit
                 Hood, Haredevil Hare, What's Up Doc?"}
        q2.put(items)
        q1.task_done()

class AppWindow(Gtk.ApplicationWindow):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.lname    = None
        self.fname    = None
        self.street   = None
        self.city     = None
        self.state    = None
        self.zip      = None
        self.employer = None
        self.position = None
        self.credits  = None
        self.q1 = queue.Queue()
        self.q2 = queue.Queue()
        self.thrd = threading.Thread(target=dbsim, daemon=True,
                                     args=(self.q1, self.q1, self.q2))
        self.thrd.start()

        # window setup
        self.set_border_width(10)
        grid = Gtk.Grid.new()
        grid.set_column_spacing(5)
        grid.set_row_spacing(5)
        # name
        label = Gtk.Label.new("Last name:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 0, 0, 1, 1)
        self.lname = Gtk.Entry.new()
        grid.attach(self.lname, 1, 0, 1, 1)
        label = Gtk.Label.new("First name:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 2, 0, 1, 1)
        self.fname = Gtk.Entry.new()
        grid.attach(self.fname, 3, 0, 1, 1)
        # address
        label = Gtk.Label.new("Street:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 0, 1, 1, 1)
        self.street = Gtk.Entry.new()
        grid.attach(self.street, 1, 1, 1, 1)
        label = Gtk.Label.new("City:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 2, 1, 1, 1)
        self.city = Gtk.Entry.new()
        grid.attach(self.city, 3, 1, 1, 1)
        label = Gtk.Label.new("State:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 0, 2, 1, 1)
        self.state = Gtk.Entry.new()
        grid.attach(self.state, 1, 2, 1, 1)
        label = Gtk.Label.new("Zip:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 2, 2, 1, 1)
        self.zip = Gtk.Entry.new()
        grid.attach(self.zip, 3, 2, 1, 1)
        # employment status
        label = Gtk.Label.new("Employer:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 0, 3, 1, 1)
        self.employer = Gtk.Entry.new()
        grid.attach(self.employer, 1, 3, 1, 1)
        label = Gtk.Label.new("Position:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 2, 3, 1, 1)
        self.position = Gtk.Entry.new()
        grid.attach(self.position, 3, 3, 1, 1)
        label = Gtk.Label.new("Credits:")
        label.set_halign(Gtk.Align.END)
        grid.attach(label, 0, 4, 1, 1)
        self.credits = Gtk.Entry.new()
        grid.attach(self.credits, 1, 4, 3, 1)
        # buttons
                      bb = Gtk.ButtonBox(Gtk.Orientation.HORIZONTAL)
        load_button = Gtk.Button.new_with_label("Load")
        bb.pack_end(load_button, False, False, 0)
        load_button.connect("clicked", self.on_load_button_clicked)
        save_button = Gtk.Button.new_with_label("Save")
        bb.pack_end(save_button, False, False, 0)
        save_button.connect("clicked", self.on_save_button_clicked)
        cancel_button = Gtk.Button.new_with_label("Cancel")
        bb.pack_end(cancel_button, False, False, 0)
        cancel_button.connect("clicked", self.on_cancel_button_clicked)

        # box setup

        vbox = Gtk.Box.new(orientation=Gtk.Orientation.VERTICAL,
        spacing=5) vbox.add(grid)
        vbox.add(bb)
        self.add(vbox)

    def on_cancel_button_clicked(self, button):
        self.destroy()

    def on_load_button_clicked(self, button):
        self.q1.put('request')
        # wait for the results to be
        queued data = None
        while Gtk.events_pending() or data ==
            None: Gtk.main_iteration()
            try:
                data = self.q2.get(block=False)
            except queue.Empty:
                continue
        self.lname.set_text(data['lname'])
        self.fname.set_text(data['fname'])
        self.street.set_text(data['street'])
        self.city.set_text(data['city'])
        self.state.set_text(data['state'])
        self.zip.set_text(data['zip'])
        self.employer.set_text(data['employer'])
        self.position.set_text(data['position'])
        self.credits.set_text(data['credits'])
        self.q2.task_done()

    def on_save_button_clicked(self, button):
        self.lname.set_text("")
        self.fname.set_text("")
        self.street.set_text("")
        self.city.set_text("")
        self.state.set_text("")
        self.zip.set_text("")
        self.employer.set_text("")
        self.position.set_text("")
        self.credits.set_text("")

class Application(Gtk.Application):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, application_id="org.example.myapp",
                         **kwargs)
        self.window = None

    def do_activate(self):
        if not self.window:
            self.window = AppWindow(application=self, title="Multi-Thread")
        self.window.show_all()
        self.window.present()

if __name__ == "__main__":
    app = Application()
    app.run(sys.argv)

Listing 12-3Multithreaded Application

在我们详细检查清单之前,让我们描述一下应用需求,看看我们是如何满足这些需求的。

我们的应用模拟了一个数据库客户机和一个服务器——都在一个多线程程序中。主窗口向线程服务器请求数据,并等待响应。服务器等待请求,然后将数据返回给客户端。应用的客户端是一个简单的 GTK+ 应用,显示从服务器获取的数据。服务器是在一个线程中运行的单个 Python 函数。它等待一个请求,提供数据,然后等待下一个请求。

所有这些的关键是 GTK+ 客户机不会冻结,不管服务器需要多长时间将数据返回给客户机。这允许应用(和所有其他应用)继续处理桌面事件。

让我们开始检查顶部的清单——dbsim服务器函数,它代表数据库模拟器。我们让这个函数尽可能简单,以揭示基本的功能。该代码是一个无限循环,它等待一个事务出现在队列中。q1.get()尝试从队列中读取事务,并在事务可用时等待返回。dbsim不处理交易数据;相反,它只是构建一个 Python 字典。然后用q2.put(items)将字典放在返回队列中。最后,处理返回到永久循环的顶部,并等待下一个事务。

这里显示的解决方案对于单个客户端工作得很好,但是当多个客户端试图访问服务器时就会崩溃,因为没有办法将客户端请求与返回的数据同步。我们需要增强应用来提供这种级别的同步。

如果您想从服务器尝试更长的事务时间,可以在q1.get()q2.put(items)语句之间插入一个time.sleep()语句。这证明了客户端在长时间运行的事务中不会冻结。

现在让我们看看客户端是如何工作的。客户端是一个标准的 GTK+ 应用,除了on_load_button_clicked()方法。该方法访问数据库模拟器线程,以获取信息来填写主窗口上显示的输入字段。第一个任务是将请求发送到数据库模拟器。它通过将请求放在模拟器读取的队列中来实现这一点。

现在我们到了困难的部分。我们如何在不使主线程休眠的情况下等待返回的信息?我们通过将方法放在一个循环中来处理未决事件,直到从服务器获得信息。让我们来看看这个紧密的循环。

while Gtk.events_pending() or data == None:
    Gtk.main_iteration()
    try:
        data = self.q2.get(block=False)
    except queue.Empty:
        continue

while语句通过检查是否有待处理的 GTK+ 事件以及数据是否已经放入目标变量来开始循环。如果任一条件为True,则进入紧循环。接下来,我们处理一个 GTK+ 事件(如果准备好了的话)。接下来,我们尝试从服务器获取数据。self.q2.get(block=False)是非阻塞请求。如果队列是空的,那么会引发一个异常,然后被忽略,因为我们需要继续循环,直到数据可用。

一旦成功获取数据,on_load_button_clicked()方法继续用提供的信息填充显示的输入字段。

这个谜题还有一个部分。看看创建服务器线程的语句。

self.thrd = threading.Thread(target=dbsim, daemon=True, args=(self.q1, self.q2))

该语句的关键部分是daemon=True参数,它允许线程等待主线程结束,当主线程结束时,它会杀死服务器线程,以便应用优雅地结束。

这个应用示例具有两个线程之间通信的所有基础。我们有两个请求和返回数据的队列。我们有一个线程来执行客户端所需的所有长时间运行的事务。最后,我们有一个在等待服务器信息时不会死机的客户机。这是多线程 GUI 应用的基本架构。

对齐部件的正确方式

在 GTK+ 3.0 之前,对齐小部件的正确方式是通过Gtk.Alignment类。从 GTK+ 3.0 开始,这个类就被弃用了,因此似乎消除了对齐小部件的简单方法。但事实上,Gtk.Widget类中有两个方法可以对齐任何容器中的小部件:halign()valign()方法。

这些方法易于使用,并在 90%的情况下提供程序员希望的对齐类型。清单 12-4 展示了如何使用Gtk.Widget对齐方法产生由halign()valign()方法提供的所有对齐类型。

#!/usr/bin/python3

import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk

class AppWindow(Gtk.ApplicationWindow):

    def __init__(self, *args, **kwargs) :
        super().__init__(*args, **kwargs)
        self.set_border_width(10)
        self.resize(300, 100)
        # create a grid
        grid1 = Gtk.Grid()
        grid1.height = 2
        grid1.width = 2
        grid1.set_column_homogeneous(True)
        grid1.set_row_homogeneous(True)
        self.add(grid1)
        # build the aligned labels
        label1 = Gtk.Label('Top left Aligned')
        label1.can_focus = False
        label1.set_halign(Gtk.Align.START)
        label1.set_valign(Gtk.Align.START)
        grid1.attach(label1, 0, 0, 1, 1)
        label2 = Gtk.Label('Top right Aligned')
        label2.can_focus = False
        label2.set_halign(Gtk.Align.END)
        label2.set_valign(Gtk.Align.START)
        grid1.attach(label2, 1, 0, 1, 1)
        label3 = Gtk.Label('Bottom left Aligned')
        label3.can_focus = False
        label3.set_halign(Gtk.Align.START)
        label3.set_valign(Gtk.Align.END)
        grid1.attach(label3, 0, 1, 1, 1)
        label4 = Gtk.Label('Bottom right Aligned')
        label4.can_focus = False
        label4.set_halign(Gtk.Align.END)
        label4.set_valign(Gtk.Align.END)
        grid1.attach(label4, 1, 1, 1, 1)

class Application(Gtk.Application):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, application_id="org.example.myapp",
                         **kwargs)
        self.window = None
        gtk_version = float(str(Gtk.MAJOR_VERSION)+'.'+str(Gtk.MINOR_VERSION))
        if gtk_version < 3.16:
           print('There is a bug in versions of GTK older that 3.16.')
           print('Your version is not new enough to prevent this bug from')
           print('causing problems in the display of this solution.')
           exit(0)

    def do_activate(self):
        if not self.window:
            self.window = AppWindow(application=self,
                                    title="Alignment")
            self.window.show_all()
            self.window.present()

if __name__ == "__main__":
    app = Application()
    app.run(sys.argv)

Listing 12-4Aligning Widgets

当您运行此示例时,您会看到显示了四种不同的校准,如图 12-2 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-2

对齐示例

下面的代码片段展示了如何将单个标签小部件与Gtk.Grid单元格的左上角对齐。

label1.set_halign(Gtk.Align.START)
label1.set_valign(Gtk.Align.START)

正如您所看到的,对齐一个小部件非常简单,并且开销减少了,因为我们没有为每个对齐的小部件调用一个新的类。这种对齐小部件的方法应该足以满足大多数应用的需求。

摘要

本章给出了三个小部件定制示例,这些示例应该为您创建自己的定制小部件提供了足够的信息。提高应用的可用性和质量还有很多可能性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值