十、菜单和工具栏
本章教你如何创建弹出菜单、菜单栏和工具栏。您从手动创建每一个开始,这样您就了解了小部件是如何构造的。这让你对菜单和工具栏所依赖的所有概念有一个牢固的理解。
在理解了每个小部件之后,会向您介绍Gtk.Builder
,它允许您通过定制的 XML 文件动态创建菜单和工具栏。加载每个用户界面文件,并将每个元素应用于相应的 action 对象,该对象告诉项目如何显示以及如何操作。
在本章中,您将学习以下内容。
-
如何创建弹出菜单、菜单栏和工具栏
-
如何将键盘快捷键应用于菜单项
-
什么是
Gtk.StatusBar
小部件,以及如何使用它向用户提供关于菜单项的更多信息 -
GTK+ 提供了哪些类型的菜单项和工具栏项
-
如何用 UI 文件动态创建菜单和工具栏
-
如何使用
Gtk.IconFactory
创建自定义库存项目
弹出式菜单
本章从学习如何创建弹出式菜单开始。弹出菜单是一个Gtk.Menu
窗口小部件,当鼠标右键悬停在某些窗口小部件上时显示给用户。一些小工具,比如Gtk.Entry
和Gtk.TextView
,已经默认内置了弹出菜单。
如果你想改变一个默认提供弹出菜单的小部件的弹出菜单,你应该在弹出回调函数中编辑提供的Gtk.Menu
小部件。例如,Gtk.Entry
和Gtk.TextView
都有一个 populate-popup 信号,它接收将要显示的Gtk.Menu
。在向用户显示之前,您可以以任何您认为合适的方式编辑该菜单。
创建弹出菜单
对于大多数小部件,您需要创建自己的弹出菜单。在本节中,您将学习如何为一个Gtk.ProgressBar
小部件提供一个弹出菜单。我们要实现的弹出菜单如图 10-1 所示。
图 10-1
包含三个菜单项的简单弹出菜单
三个弹出菜单项使进度条跳动,将其设置为 100%完成,然后清除它。在清单 10-1 中,一个事件框包含了进度条。因为Gtk.ProgressBar
和Gtk.Label
一样,不能自己检测 GDK 事件,我们需要使用事件盒来捕捉button-press-event
信号。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(250, -1)
# Create all of the necessary widgets and initialize the pop-up menu. menu = Gtk.Menu.new()
eventbox = Gtk.EventBox.new()
progress = Gtk.ProgressBar.new() progress.set_text("Nothing Yet Happened")
progress.set_show_text(True) self.create_popup_menu(menu, progress)
progress.set_pulse_step(0.05) eventbox.set_above_child(False)
eventbox.connect("button_press_event", self.button_press_event, menu) eventbox.add(progress)
self.add(eventbox)
eventbox.set_events(Gdk.EventMask.BUTTON_PRESS_MASK)
eventbox.realize()
def create_popup_menu(self, menu, progress):
pulse = Gtk.MenuItem.new_with_label("Pulse Progress")
fill = Gtk.MenuItem.new_with_label("Set as Complete")
clear = Gtk.MenuItem.new_with_label("Clear Progress")
separator = Gtk.SeparatorMenuItem.new()
pulse.connect("activate", self.pulse_activated, progress)
fill.connect("activate", self.fill_activated, progress)
clear.connect("activate", self.clear_activated, progress)
menu.append(pulse)
menu.append(separator)
menu.append(fill)
menu.append(clear)
menu.attach_to_widget(progress, None)
menu.show_all()
def button_press_event(self, eventbox, event, menu):
pass
def pulse_activated(self, item, progress):
pass
def fill_activated(self, item, progress):
pass
def clear_activated(self, item, progress):
pass
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Pop-up Menus")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 10-1Simple Pop-up Menu
在大多数情况下,您希望使用button-press-event
来检测用户希望弹出菜单何时显示。这允许您检查是否单击了鼠标右键。如果点击了鼠标右键,Gdk.EventButton
的按钮成员等于 3。
不过,Gtk.Widget
也提供了popup-menu
信号,当用户按下内置的按键加速器激活弹出菜单时,该信号被激活。大多数用户使用鼠标来激活弹出菜单,所以这在 GTK+ 应用中通常不是一个因素。然而,如果您也想处理这个信号,您应该创建第三个函数来显示由两个回调函数调用的弹出菜单。
用Gtk.Menu.new()
创建新菜单。菜单初始化时没有初始内容,所以下一步是创建菜单项。
在本节中,我们将介绍两种类型的菜单项。第一个是所有其他类型菜单项的基类,Gtk.MenuItem
。为Gtk.MenuItem
提供了三种初始化功能:Gtk.MenuItem.new()
、Gtk.MenuItem.new_with_label()
和Gtk.MenuItem.new_with_mnemonic()
。
pulse = Gtk.MenuItem.new_with_label("Pulse Progress")
大多数情况下,你不需要使用Gtk.MenuItem.new()
,因为一个没有内容的菜单项没有多大用处。如果您使用该函数来初始化菜单项,您必须用代码构造菜单的每个方面,而不是让 GTK+ 来处理细节。
注意
菜单项助记符和键盘快捷键不是一回事。当菜单具有焦点时,当用户按下 Alt 和适当的字母数字键时,助记符激活菜单项。键盘快捷键是一种自定义的组合键,当按下该组合键时会运行回调函数。在下一节中,您将了解菜单的键盘快捷键。
另一种类型的基本菜单项是Gtk.SeparatorMenuItem
,它在其位置放置一个通用分隔符。您可以使用Gtk.SeparatorMenuItem.new()
创建一个新的分隔符菜单项。
在设计菜单结构时,分隔符非常重要,因为它们将菜单项组织成组,这样用户就可以很容易地找到合适的菜单项。例如,在“文件”菜单中,菜单项通常被组织成打开文件、保存文件、打印文件和关闭应用的组。很少会出现许多菜单项之间没有分隔符的情况(例如,最近显示的文件列表可能没有分隔符)。在大多数情况下,您应该将相似的菜单项组合在一起,并在相邻的组之间放置一个分隔符。
在创建菜单项之后,您需要将每个菜单项连接到激活信号,该信号在用户选择该项时发出。或者,您可以使用 activate-item 信号,该信号也会在显示给定菜单项的子菜单时发出。除非菜单项扩展为子菜单,否则这两者之间没有明显的区别。
每个 activate 和 activate-item 回调函数都接收启动动作的Gtk.MenuItem
小部件和您需要传递给该函数的任何数据。在清单 10-2 中,提供了三个菜单项回调函数来脉动进度条,填充到 100%完成,并清除所有进度。
现在您已经创建了所有的菜单项,您需要将它们添加到菜单中。Gtk.Menu
派生自Gtk.MenuShell
,是一个抽象基类,包含并显示子菜单和菜单项。菜单项可以通过menu.append()
添加到菜单外壳中。该函数将每个项目附加到菜单外壳的末尾。
menu.append(pulse)
此外,您可以使用menu.prepend()
或menu.insert()
分别在菜单的开头添加菜单项或将其插入任意位置。menu.insert()
接受的位置从零开始。
在将Gtk.Menu
的所有子控件设置为可见后,您应该调用menu.attach_to_widget()
以便弹出菜单与特定的小部件相关联。该函数接受弹出菜单和它所附加的小部件。
menu.attach_to_widget(progress, None)
menu.attach_to_widget()
的最后一个参数接受一个Gtk.MenuDetachFunc
,它可以在菜单脱离小部件时调用一个特定的函数。
弹出菜单回调方法
在创建了必要的小部件之后,您需要处理button-press-event
信号,如清单 10-2 所示。在本例中,每当鼠标右键单击进度条时,都会显示弹出菜单。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(250, -1)
# Create all of the necessary widgets and initialize the pop-up
menu. menu = Gtk.Menu.new()
eventbox = Gtk.EventBox.new() progress =
Gtk.ProgressBar.new()
progress.set_text("Nothing Yet Happened")
progress.set_show_text(True) self.create_popup_menu(menu, progress)
progress.set_pulse_step(0.05) eventbox.set_above_child(False)
eventbox.connect("button_press_event", self.button_press_event, menu) eventbox.add(progress)
self.add(eventbox)
eventbox.set_events(Gdk.EventMask.BUTTON_PRESS_MASK) eventbox.realize()
def create_popup_menu(self, menu, progress):
pulse = Gtk.MenuItem.new_with_label("Pulse Progress")
fill = Gtk.MenuItem.new_with_label("Set as Complete")
clear = Gtk.MenuItem.new_with_label("Clear Progress")
separator = Gtk.SeparatorMenuItem.new()
pulse.connect("activate", self.pulse_activated, progress)
fill.connect("activate", self.fill_activated, progress)
clear.connect("activate", self.clear_activated, progress)
menu.append(pulse)
menu.append(separator)
menu.append(fill)
menu.append(clear)
menu.attach_to_widget(progress, None)
menu.show_all()
def button_press_event(self, eventbox, event, menu):
if event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: menu.popup(None, None, None, None, event.button, event.time) return True
return False
def pulse_activated(self, item, progress):
progress.pulse()
progress.set_text("Pulse!")
def fill_activated(self, item, progress):
progress.set_fraction(1.0)
progress.set_text("One Hundred Percent")
def clear_activated(self, item, progress):
progress.set_fraction(0.0)
progress.set_text("Reset to Zero")
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Pop-up Menus")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 10-2Callback Functions for the Simple Pop-up Menu
在清单 10-2 中的button-press-event
回调函数中,可以使用menu.popup()
在屏幕上显示菜单。
menu.popup(parent_menu_shell, parent_menu_item, func, func_data, button, event_time)
在清单 10-2 中,除了被点击导致事件的鼠标按钮(事件➤按钮)和事件发生的时间(event.time)之外,所有参数都被设置为None
。如果弹出菜单是由按钮之外的其他东西激活的,您应该为 button 参数提供 0。
注意
如果该操作是由弹出菜单信号调用的,则事件时间将不可用。那样的话,可以用Gtk.get_current_event_time()
。该函数返回当前事件的时间戳,如果没有最近的事件,则返回Gdk.CURRENT_TIME
。
通常,parent_menu_shell
、parent_menu_item
、func
和func_data
被设置为None
,因为它们在菜单是菜单栏结构的一部分时使用。parent_menu_shell
小部件是包含导致弹出初始化的项目的菜单外壳。或者,您可以提供parent_menu_item
,它是导致弹出初始化的菜单项。
Gtk.MenuPositionFunc
是一个决定在屏幕上的什么位置绘制菜单的功能。它接受func_data
作为可选的最后一个参数。这些参数在应用中不经常使用,因此可以安全地设置为None
。在我们的例子中,弹出菜单已经与进度条相关联,所以它被绘制在正确的位置。
键盘快捷键
创建菜单时,最重要的事情之一就是设置键盘快捷键。键盘快捷键是由一个快捷键和一个或多个修饰键组成的组合键,如 Ctrl 或 Shift 。当用户按下组合键时,会发出相应的信号。
清单 10-3 是进度条弹出菜单应用的扩展,它为菜单项添加了键盘快捷键。当用户按 Ctrl+P 时,进度条是脉冲式的,用 Ctrl+F 填充,用 Ctrl+C 清除。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(250, -1)
# Create all of the necessary widgets and initialize the pop-up menu. menu = Gtk.Menu.new()
eventbox = Gtk.EventBox.new() progress = Gtk.ProgressBar.new() progress.set_text("Nothing Yet Happened") progress.set_show_text(True) self.create_popup_menu(menu, progress) progress.set_pulse_step(0.05) eventbox.set_above_child(False)
eventbox.connect("button_press_event", self.button_press_event, menu) eventbox.add(progress)
self.add(eventbox)
eventbox.set_events(Gdk.EventMask.BUTTON_PRESS_MASK)
eventbox.realize()
def create_popup_menu(self, menu, progress):
group = Gtk.AccelGroup.new()
self.add_accel_group(group)
menu.set_accel_group(group)
pulse = Gtk.MenuItem.new_with_label("Pulse Progress")
fill = Gtk.MenuItem.new_with_label("Set as Complete")
clear = Gtk.MenuItem.new_with_label("Clear Progress")
separator = Gtk.SeparatorMenuItem.new()
# Add the necessary keyboard accelerators.
pulse.add_accelerator("activate", group, Gdk.KEY_P, Gdk.ModifierType.CONTROL, Gtk.AccelFlags.VISIBLE)
fill.add_accelerator("activate", group, Gdk.KEY_F, Gdk.ModifierType.CONTROL, Gtk.AccelFlags.VISIBLE)
clear.add_accelerator("activate", group, Gdk.KEY_C, Gdk.ModifierType.CONTROL, Gtk.AccelFlags.VISIBLE)
pulse.connect("activate", self.pulse_activated, progress)
fill.connect("activate", self.fill_activated, progress)
clear.connect("activate", self.clear_activated, progress)
menu.append(pulse)
menu.append(separator)
menu.append(fill)
menu.append(clear)
menu.attach_to_widget(progress, None)
menu.show_all()
def button_press_event(self, eventbox, event, menu):
if event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: menu.popup(None, None, None, None, event.button, event.time) return True
return False
def pulse_activated(self, item, progress):
progress.pulse()
progress.set_text("Pulse!")
def fill_activated(self, item, progress):
progress.set_fraction(1.0)
progress.set_text("One Hundred Percent")
def clear_activated(self, item, progress):
progress.set_fraction(0.0)
progress.set_text("Reset to Zero")
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Pop-up Menus")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 10-3Adding Accelerators to Menu Items
键盘快捷键存储为Gtk.AccelGroup
的一个实例。要在您的应用中实现加速器,您需要用Gtk.AccelGroup.new()
创建一个新的加速器组。该加速器组必须添加到菜单出现的Gtk.Window
中才能生效。它还必须与任何利用其加速器的菜单相关联。在清单 10-3 中,这是在用self.add_accel_group()
和menu.set_accel_group()
创建Gtk.AccelGroup
之后立即执行的。
可以用Gtk.AccelMap
手动创建键盘快捷键,但是在大多数情况下,widget.add_accelerator()
提供了所有必要的功能。这种方法存在的唯一问题是,用户不能在运行时更改用该函数创建的键盘快捷键。
widget.add_accelerator(signal_name, group, accel_key, mods, flags)
要向小部件添加加速器,可以使用widget.add_accelerator()
,当用户按下组合键时,它会在小部件上发出由 signal_name 指定的信号。您需要为该功能指定您的快捷键组,如前所述,它必须与窗口和菜单相关联。
一个加速键和一个或多个修饰键形成完整的组合键。PyGObject API 参考中提供了可用快捷键的列表。所有可用关键字的定义都可以包含在 import GDK 语句中。
修饰符由Gdk.ModifierType
枚举指定。最常用的修饰键是Gdk.ModifierType.SHIFT_MASK
、Gdk.ModifierType.CONTROL_MASK
和Gdk.ModifierType.MOD1_MASK
,分别对应 Shift、Ctrl 和 Alt 键。
小费
当处理键码时,您需要小心,因为在某些情况下,您可能需要为同一个动作提供多个键。例如,如果你想抓住数字 1 键,你需要注意Gdk.KEY_1
和Gdk.KEY_KP_1
——它们对应于键盘顶部的 1 键和数字小键盘上的 1 键。
widget.add_accelerator()
的最后一个参数是加速器标志。由Gtk.AccelFlags
枚举定义了三个标志。如果设置了Gtk.AccelFlags.VISIBLE
,则在标签中可以看到加速器。Gtk.AccelFlags.LOCKED
防止用户修改加速器。Gtk.AccelFlags.MASK
为小工具加速器设置两个标志。
状态栏提示
通常放置在主窗口的底部,Gtk.Statusbar
窗口小部件可以给用户关于应用正在运行的更多信息。状态栏对于菜单也非常有用,因为您可以向用户提供更多关于鼠标光标所悬停的菜单项功能的信息。状态栏截图如图 10-2 所示。
图 10-2
带有状态栏提示的弹出式菜单
状态栏小部件
虽然状态栏一次只能显示一条消息,但是小部件实际上存储了一堆消息。当前显示的消息在栈顶。从堆栈中弹出消息时,会显示上一条消息。如果从顶部弹出一条消息后,堆栈上没有剩余的字符串,则状态栏上不会显示任何消息。
用Gtk.Ststusbar.new()
创建新的状态栏小部件。这将创建一个新的带有空消息堆栈的Gtk.Statusbar
小部件。在能够在新状态栏的堆栈中添加或删除消息之前,必须用statusbar.get_context_id()
检索上下文标识符。
id = statusbar.get_context_id(description)
上下文标识符是与上下文描述字符串相关联的唯一无符号整数。该标识符用于特定类型的所有消息,这允许您对堆栈上的消息进行分类。
例如,如果您的状态栏包含超链接和 IP 地址,您可以从字符串“URL”和“IP”创建两个上下文标识符。当您将消息推入堆栈或从堆栈弹出消息时,您必须指定一个上下文标识符。这允许应用的不同部分在状态栏消息堆栈中推送和弹出消息,而不会相互影响。
小费
对不同类别的消息使用不同的上下文标识符是很重要的。如果应用的一部分试图给用户一个消息,而另一部分试图删除自己的消息,你不希望错误的消息从堆栈中弹出!
在您生成上下文标识符之后,您可以使用statusbar.push()
将一条消息添加到状态栏的栈顶。该函数返回刚刚添加的字符串的唯一消息标识符。这个标识符可以在以后用于从堆栈中移除消息,而不管它在什么位置。
statusbar.push(context_id, message)
有两种方法可以从堆栈中删除消息。如果您想从特定上下文 ID 的堆栈顶部删除一条消息,您可以使用statusbar.pop()
。该函数删除状态栏堆栈中上下文标识符为context_id
的最高消息。
statusbar.pop(context_id)
也可以用statusbar.remove()
从状态栏的消息堆栈中删除特定的消息。为此,您必须提供消息的上下文标识符和想要删除的消息的消息标识符,这是在添加消息时由statusbar.push()
返回的。
statusbar.remove(context_id, message_id)
菜单项信息
状态栏的一个有用的作用是给用户更多关于鼠标光标当前悬停的菜单项的信息。图 10-2 显示了这样一个例子,这是清单 10-4 中的进度条弹出菜单应用的截图。
要实现状态栏提示,你应该将你的每个菜单项连接到Gtk.Widget
的"enter-notify-event"
和"leave-notify-event"
信号。清单 10-4 显示了你已经知道的进度条弹出菜单应用,除了当鼠标光标移动到一个菜单项上时状态栏提示被提供。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
class AppMenuItem(Gtk.MenuItem):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __setattr__(self, name, value):
self.__dict__[name] = value
def __getattr__(self, name):
return self.__dict__[name]
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_border_width(10)
self.set_size_request(250, -1)
# Create all of the necessary widgets and initialize the pop-up menu. menu = Gtk.Menu.new()
eventbox = Gtk.EventBox.new() progress = Gtk.ProgressBar.new()
progress.set_text("Nothing Yet Happened")
progress.set_show_text(True)
statusbar = Gtk.Statusbar.new()
self.create_popup_menu(menu, progress, statusbar)
progress.set_pulse_step(0.05)
eventbox.set_above_child(False)
eventbox.connect("button_press_event", self.button_press_event, menu)
eventbox.add(progress)
vbox = Gtk.Box.new(orientation=Gtk.Orientation.VERTICAL, spacing=0)
vbox.pack_start(eventbox, False, True, 0)
vbox.pack_start(statusbar, False, True, 0)
self.add(vbox)
eventbox.set_events(Gdk.EventMask.BUTTON_PRESS_MASK)
eventbox.realize()
def create_popup_menu(self, menu, progress, statusbar):
pulse = AppMenuItem(label="Pulse Progress")
fill = AppMenuItem(label="Set as Complete")
clear = AppMenuItem(label="Clear Progress")
separator = Gtk.SeparatorMenuItem.new()
pulse.connect("activate", self.pulse_activated, progress)
fill.connect("activate", self.fill_activated, progress)
clear.connect("activate", self.clear_activated, progress)
Connect signals to each menu item for status bar messages. pulse.connect("enter-notify-event", self.statusbar_hint, statusbar) pulse.connect("leave-notify-event", self.statusbar_hint, statusbar) fill.connect("enter-notify-event", self.statusbar_hint, statusbar) fill.connect("leave-notify-event", self.statusbar_hint, statusbar) clear.connect("enter-notify-event", self.statusbar_hint, statusbar) clear.connect("leave-notify-event", self.statusbar_hint, statusbar) pulse.__setattr__("menuhint", "Pulse the progress bar one step.") fill.__setattr__("menuhint", "Set the progress bar to 100%.") clear.__setattr__("menuhint", "Clear the progress bar to 0%.") menu.append(pulse)
menu.append(separator)
menu.append(fill)
menu.append(clear)
menu.attach_to_widget(progress, None) menu.show_all()
def button_press_event(self, eventbox, event, menu):
if event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: menu.popup(None, None, None, None, event.button, event.time) return True
return False
def pulse_activated(self, item, progress):
progress.pulse()
progress.set_text("Pulse!")
def fill_activated(self, item, progress):
progress.set_fraction(1.0)
progress.set_text("One Hundred Percent")
def clear_activated(self, item, progress): progress.set_fraction(0.0) progress.set_text("Reset to Zero")
def statusbar_hint(self, menuitem, event, statusbar): id = statusbar.get_context_id("MenuItemHints")
if event.type == Gdk.EventType.ENTER_NOTIFY:
hint = menuitem.__getattr__("menuhint")
id = statusbar.push(id, hint)
elif event.type == Gdk.EventType.LEAVE_NOTIFY:
statusbar.pop(id)
return False
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Pop-up Menus")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 10-4Displaying More Information About a Menu Item
当实现状态栏提示时,你首先需要弄清楚什么信号是必要的。我们希望能够在鼠标光标移动到菜单项上时向状态栏添加一条消息,并在鼠标光标离开时删除它。从这个描述来看,使用“进入-通知-事件”和“离开-通知-事件”是一个很好的解决方案。
由于 Python 3 的 GTK+ 3 接口没有在 GTK+ 对象上实现get_data()
和set_data()
方法,我们需要子类化Gtk.MenuItem
类来实现相应的 Python 3 属性。这种方法也被用在本书的其他例子中。
使用这两个信号的一个优点是我们只需要一个回调函数,因为每个函数的原型都接收一个Gdk.EventProximity
对象。从这个物体中,我们可以分辨出Gdk.EventType.ENTER_NOTIFY
和Gdk.EventType.LEAVE_NOTIFY
事件。你想从回调函数中返回False
,因为你不想阻止 GTK+ 处理事件;您只想增强它发出时所执行的内容。
在statusbar_hint()
回调方法中,您应该首先检索菜单项消息的上下文标识符。您可以使用任何您想要的字符串,只要您的应用记得使用了什么。清单 10-4 描述了所有添加到状态栏的菜单项消息。如果应用的其他部分使用状态栏,使用不同的上下文标识符将不会影响菜单项提示。
id = statusbar.get_context_id("MenuItemHints")
如果事件类型为Gdk.EventType.ENTER_NOTIFY
,则需要向用户显示消息。在create_popup_menu()
方法中,一个数据参数被添加到每个名为"menuhint"
的菜单项中。这是对菜单项功能的更深入的描述,显示给用户。
hint = menuitem.__getattr__("menuhint")
statusbar.push(id, hint)
然后,使用statusbar.push()
,可以将消息添加到状态栏的"MenuItemHints"
上下文标识符下。该消息被放在堆栈的顶部并显示给用户。您可能想考虑在调用这个方法之后处理所有的 GTK+ 事件,因为用户界面应该立即反映这些变化。
但是,如果事件类型是Gdk.EventType.LEAVE_NOTIFY
,您需要删除使用相同上下文标识符添加的最后一个菜单项消息。最近的消息可以用statusbar.pop()
从堆栈中删除。
菜单项
到目前为止,您已经了解了显示标签和分隔符菜单项的平面菜单。也可以向现有菜单项添加子菜单。GTK+ 还提供了许多其他的Gtk.MenuItem
对象。图 10-3 显示了一个弹出菜单,其中包含一个子菜单以及图像、检查和单选菜单项。
图 10-3
图像、检查和单选菜单项
子菜单
GTK+ 中的子菜单不是由单独类型的菜单项小部件创建的,而是通过调用menuitem.set_submenu()
创建的。该方法调用menu.attach_to_widget()
将子菜单附加到菜单项,并在菜单项旁边放置一个箭头,表示它现在有子菜单。如果菜单项已经有子菜单,它将被给定的Gtk.Menu
小部件替换。
menuitem.set_submenu(submenu)
如果你有一个非常具体的选项列表,会使原本组织有序的菜单结构变得混乱,那么子菜单非常有用。使用子菜单时,您可以使用由Gtk.MenuItem
小部件提供的“activate-item”信号,该信号在菜单项显示其子菜单时发出。
除了Gtk.MenuItem
和菜单项分隔符,还有另外三种类型的菜单项对象:图像、复选、单选菜单项;这些将在本节的剩余部分中介绍。
图像菜单项
警告
从 GTK+ 3.1 开始,Gtk.ImageMenuItem
类就被弃用了。不要在新代码中使用它,注意它可能会在 GTK+ 的新版本中完全消失。
Gtk.ImageMenuItem
与其父类Gtk.MenuItem
非常相似,除了它在菜单项标签的左边显示了一个小图像。为创建新的图像菜单项提供了四个功能。
第一个函数imagemenuitem.new()
创建了一个新的Gtk.ImageMenuItem
对象,它有一个空标签,没有关联的图像。您可以使用图像菜单项的 image 属性来设置菜单项显示的图像。
Gtk.ImageMenuItem.new()
此外,您可以使用Gtk.ImageMenuItem.new_from_stock()
从纸张标识符创建新的图像菜单项。这个函数使用与 stock_id 相关联的标签和图像创建Gtk.ImageMenuItem
。该函数接受股票标识符字符串。
Gtk.ImageMenuItem.new_from_stock(stockid, accel_group)
该函数的第二个参数接受一个加速器组,它被设置为库存项目的默认加速器。如果你想像我们在清单 10-3 中所做的那样为菜单项手动设置键盘快捷键,你可以为这个参数指定None
。
同样,您可以使用Gtk.ImageMenuItem.new_with_label()
创建一个新的Gtk.ImageMenuItem
,最初只有一个标签。稍后,您可以使用 image 属性添加一个图像小部件。GTK+ 还提供了方法imagemenuitem.set_image()
,允许您编辑小部件的图像属性。
Gtk.ImageMenuItem.new_with_label(label)
另外,GTK+ 提供了Gtk.ImageMenuItem.new_with_mnemonic()
,它创建了一个带有助记标签的图像菜单项。与前面的方法一样,您必须在创建菜单项后设置 image 属性。
检查菜单项
Gtk.CheckMenuItem
允许您创建一个菜单项,根据其布尔活动属性是True
还是False
,在标签旁边显示一个复选符号。这将允许用户查看选项是被激活还是被停用。
与Gtk.MenuItem
一样,提供了三个初始化功能。
Gtk.CheckMenuItem.new()
、Gtk.CheckItem.new_with_label()
和Gtk.CheckMenuItem.new_with_mnemonic()
。这些函数分别创建一个没有标签、有初始标签或有助记标签的Gtk.CheckMenuItem
。
Gtk.CheckMenuItem.new()
Gtk.CheckMenuItem.new_with_label(label)
Gtk.CheckMenuItem.new_with_mnemonic(label)
如前所述,check 菜单项的当前状态由小部件的 active 属性保存。GTK+ 提供了checkmenuitem.set_active()
和checkmenuitem.get_active()
两个函数来设置和检索有效值。
与所有的 check button 小部件一样,您可以使用“toggled”信号,当用户切换菜单项的状态时会发出该信号。GTK+ 负责更新 check 按钮的状态,所以这个信号只是允许您更新应用以反映更改后的值。
Gtk.CheckMenuItem
还提供了checkmenuitem.set_inconsistent()
,改变菜单项的不一致属性。当设置为True
时,检查菜单项显示第三种“中间”状态,既不是活动状态也不是非活动状态。这可以向用户显示必须做出尚未设置的选择,或者对选择的不同部分设置和取消设置属性。
单选菜单项
Gtk.RadioMenuItem
是从Gtk.CheckMenuItem
派生出来的一个 widget。通过将 check 菜单项的 draw-as-radio 属性设置为True
,它被呈现为单选按钮而不是复选按钮。单选菜单项的工作方式与普通单选按钮相同。
第一个单选按钮应该使用下列函数之一创建。您可以将单选按钮组设置为None
,因为必需的元素是通过引用第一个元素添加到组中的。这些函数分别创建一个空菜单项、一个带标签的菜单项和一个带助记符的菜单项。
Gtk.RadioMenuItem.new(group)
Gtk.RadioMenuItem.new_with_label(group, text)
Gtk.RadioMenuItem.new_with_mnemonic(group, text)
所有其他单选菜单项都应该使用以下三个函数之一创建,这三个函数将它添加到与 group 关联的单选按钮组中。这些函数分别创建一个空菜单项、一个带标签的菜单项和一个带助记符的菜单项。
Gtk.RadioMenuItem.new_from_widget(group)
Gtk.RadioMenuItem.new_from_widget_with_label(group, text)
Gtk.RadioMenuItem.new_from_widget_with_mnemonic(group, text)
菜单栏
Gtk.MenuBar
是一个将多个弹出菜单组织成水平或垂直行的小部件。每个根元素都是一个Gtk.MenuItem
,它会向下弹出一个子菜单。一个Gtk.MenuBar
的实例通常显示在主应用窗口的顶部,以提供对应用所提供功能的访问。菜单栏示例如图 10-4 所示。
图 10-4
有三个菜单的菜单栏
在清单 10-5 中,创建了一个带有三个菜单的Gtk.MenuBar
小部件:文件、编辑和帮助。每个菜单实际上都是一个带有子菜单的Gtk.MenuItem
。然后,许多菜单项被添加到每个子菜单中。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_size_request(250, -1)
menubar = Gtk.MenuBar.new()
file = Gtk.MenuItem.new_with_label("File")
edit = Gtk.MenuItem.new_with_label("Edit")
help = Gtk.MenuItem.new_with_label("Help")
filemenu = Gtk.Menu.new()
editmenu = Gtk.Menu.new()
helpmenu = Gtk.Menu.new()
file.set_submenu(filemenu)
edit.set_submenu(editmenu)
help.set_submenu(helpmenu)
menubar.append(file)
menubar.append(edit)
menubar.append(help)
# Create the File menu content.
new = Gtk.MenuItem.new_with_label("New")
open = Gtk.MenuItem.new_with_label("Open")
filemenu.append(new)
filemenu.append(open)
# Create the Edit menu content.
cut = Gtk.MenuItem.new_with_label("Cut")
copy = Gtk.MenuItem.new_with_label("Copy")
paste = Gtk.MenuItem.new_with_label("Paste")
editmenu.append(cut)
editmenu.append(copy)
editmenu.append(paste)
# Create the Help menu content.
contents = Gtk.MenuItem.new_with_label("Help")
about = Gtk.MenuItem.new_with_label("About")
helpmenu.append(contents)
helpmenu.append(about)
self.add(menubar)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Menu Bars")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 10-5Creating Groups of Menus
使用Gtk.MenuBar.new()
创建新的Gtk.MenuBar
小部件。这将创建一个空的菜单外壳,您可以在其中添加内容。
创建菜单栏后,可以用menubar.set_pack_direction()
定义菜单栏项目的打包方向。pack_direction
属性的值由Gtk.PackDirection
枚举定义,包括Gtk.PackDirection.LTR
、Gtk.PackDirection.RTL
、Gtk.PackDirection.TTB
或Gtk.PackDirection.BTT
。它们分别从左到右、从右到左、从上到下或从下到上打包菜单项。默认情况下,子部件从左到右打包。
Gtk.MenuBar
还提供了另一个名为child-pack-direction
的属性,该属性设置菜单栏子菜单的菜单项打包的方向。换句话说,它控制子菜单项的打包方式。这个属性的值也由Gtk.PackDirection
枚举定义。
菜单栏中的每个子项实际上都是一个Gtk.MenuItem
小部件。因为Gtk.MenuBar
是从Gtk.MenuShell
派生的,所以您可以使用menuitem.append()
方法向栏中添加一个项目,如下行所示。
menubar.append(file)
您也可以使用file.prepend()
或file.insert()
将项目添加到菜单栏的开头或任意位置。
接下来您需要调用file.set_submenu()
来向每个根菜单项添加一个子菜单。每个子菜单都是一个Gtk.Menu
小部件,创建方式与弹出菜单相同。然后,GTK+ 会在必要时向用户显示子菜单。
file.set_submenu(filemenu)
工具栏
一个Gtk.Toolbar
是一种容器,它在水平或垂直的行中保存了许多小部件。这意味着可以很容易地定制大量的小部件。通常,工具栏包含可以显示图像和文本字符串的工具按钮。然而,工具栏实际上可以容纳任何类型的小部件。图 10-5 中显示了一个包含四个工具按钮和一个分隔符的工具栏。
图 10-5
显示图像和文本的工具栏
在清单 10-6 中,创建了一个简单的工具栏,在水平行中显示五个工具项。每个工具栏项都显示一个图标和一个描述该项目的用途的标签。工具栏还被设置为显示一个箭头,该箭头提供对菜单中不适合的工具栏项目的访问。
在本例中,工具栏为Gtk.Entry
小部件提供剪切、复制、粘贴和全选功能。AppWindow()
方法创建工具栏,将其打包在Gtk.Entry
之上。然后它调用create_toolbar()
,用工具项填充工具栏并连接必要的信号。
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class AppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
toolbar = Gtk.Toolbar.new()
entry = Gtk.Entry.new()
vbox.pack_start(toolbar, True, False, 0)
vbox.pack_start(entry, True, False, 0)
self.create_toolbar(toolbar, entry)
self.add(vbox)
self.set_size_request(310, 75)
def create_toolbar(self, toolbar, entry): icon_theme = Gtk.IconTheme.get_default()
icon = icon_theme.load_icon("edit-cut", -1,
Gtk.IconLookupFlags.FORCE_SIZE)
image = Gtk.Image.new_from_pixbuf(icon)
cut = Gtk.ToolButton.new(image, "Cut")
icon = icon_theme.load_icon("edit-copy", -1,
Gtk.IconLookupFlags.FORCE_SIZE)
image = Gtk.Image.new_from_pixbuf(icon)
copy = Gtk.ToolButton.new(image, "Copy")
icon = icon_theme.load_icon("edit-paste", -1,
Gtk.IconLookupFlags.FORCE_SIZE)
image = Gtk.Image.new_from_pixbuf(icon)
paste = Gtk.ToolButton.new(image, "Paste")
icon = icon_theme.load_icon("edit-select-all", -1, Gtk.IconLookupFlags.FORCE_SIZE)
image = Gtk.Image.new_from_pixbuf(icon)
selectall = Gtk.ToolButton.new(image, "Select All")
separator = Gtk.SeparatorToolItem.new()
toolbar.set_show_arrow(True)
toolbar.set_style(Gtk.ToolbarStyle.BOTH)
toolbar.insert(cut, 0)
toolbar.insert(copy, 1)
toolbar.insert(paste, 2)
toolbar.insert(separator, 3)
toolbar.insert(selectall, 4)
cut.connect("clicked", self.cut_clipboard, entry)
copy.connect("clicked", self.copy_clipboard, entry)
paste.connect("clicked", self.paste_clipboard, entry)
selectall.connect("clicked", self.select_all, entry)
def cut_clipboard(self, button, entry):
entry.cut_clipboard()
def copy_clipboard(self, button, entry):
entry.copy_clipboard()
def paste_clipboard(self, button, entry):
entry.paste_clipboard()
def select_all(self, button, entry):
entry.select_region(0, -1)
class Application(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, application_id="org.example.myapp",
**kwargs)
self.window = None
def do_activate(self):
if not self.window:
self.window = AppWindow(application=self, title="Toolbar")
self.window.show_all()
self.window.present()
if __name__ == "__main__":
app = Application()
app.run(sys.argv)
Listing 10-6Creating a Gtk.Toolbar Widget
用Gtk.Toolbar.new()
创建新的工具栏,它在清单 10-6 中显示的create_toolbar()
函数之前被调用。这将创建一个空的Gtk.Toolbar
小部件,您可以在其中添加工具按钮。
Gtk.Toolbar
提供了许多属性,用于定制它的显示方式和与用户的交互方式,包括方向、按钮样式以及访问不适合工具栏的项目的能力。
如果因为没有足够的空间而无法在工具栏上显示所有的工具栏项目,那么如果您将toolbar.set_show_arrow()
设置为True
,则会出现一个溢出菜单。如果所有项目都可以显示在工具栏上,箭头就会隐藏起来。
toolbar.set_show_arrow(boolean)
另一个Gtk.Toolbar
属性是显示所有菜单项的样式,它是用toolbar.set_style()
设置的。你应该注意到这个属性可能会被主题覆盖,所以你应该通过调用toolbar.unset_style()
来提供使用默认样式的选项。有四种工具栏样式,由Gtk.ToolbarStyle
枚举定义。
-
Gtk.ToolbarStyle.ICONS
:仅显示工具栏中每个工具按钮的图标。 -
Gtk.ToolbarStyle.TEXT
:如何只为工具栏中的每个工具按钮添加标签。 -
Gtk.ToolbarStyle.BOTH
:显示每个工具按钮的图标和标签,图标位于标签上方。 -
Gtk.ToolbarStyle.BOTH_HORIZ
:显示每个工具按钮的图标和标签,图标在标签的左边。仅当工具项目的“is-important"
属性设置为True
时,工具项目的标签文本才会显示。
工具栏的另一个重要属性是可以用toolbar.set_orientation()
设置的方向。由Gtk.Orientation
枚举定义了两个可能的值,Gtk.Orientation.HORIZONTAL
和Gtk.Orientation.VERTICAL
,它们可以使工具栏水平(默认)或垂直。
工具栏项目
清单 10-6 介绍了三种重要的工具项类型:Gtk.ToolItem
、Gtk.ToolButton
和Gtk.SeparatorToolItem
。所有的工具按钮都是从Gtk.ToolItem
类中派生出来的,该类保存了所有工具项使用的基本属性。
如果你使用的是Gtk.ToolbarStyle.BOTH_HORIZ
风格,那么Gtk.ToolItem
中安装的一个基本属性就是"is-important"
设置。如果该属性设置为True
,工具栏项目的标签文本仅针对该样式显示。
与菜单一样,分隔符工具项由Gtk.SeparatorToolItem
提供,并由Gtk.SeparatorToolItem.new()
创建。分隔符工具项有一个 draw 属性,当设置为True
时会绘制一个分隔符。如果您将 draw 设置为False
,它会在没有任何可视分隔符的位置放置填充。
小费
如果您将一个Gtk.SeparatorToolItem
的扩展属性设置为True
并将它的绘制属性设置为False
,您将强制分隔符之后的所有工具项到工具栏的末尾。
大多数工具栏项目都属于Gtk.ToolButton
类型。Gtk.ToolButton
只提供了一个初始化方法Gtk.ToolButton.new()
,因为从 GTK+ 3.1 开始,所有其他的初始化方法都被弃用了。Gtk.ToolButton.new()
可以创建一个带有自定义图标和标签的Gtk.ToolButton
。这些属性中的每一个都可以设置为None
。
Gtk.ToolButton.new(icon, label)
使用toolbutton.set_label()
和toolbutton.set_icon_widget()
可以在初始化后手动设置标签和图标。这些函数提供对工具按钮的标签和图标小部件属性的访问。
此外,您可以定义自己的小部件来代替带有toolbutton.set_label_widget()
的工具按钮的默认Gtk.Label
小部件。这允许您在工具按钮中嵌入任意的小部件,比如一个条目或组合框。如果该属性设置为None
,则使用默认标签。
toolbutton.set_label_widget(label_widget)
创建工具栏项目后,可以用toolbar.insert()
将每个Gtk.ToolItem
插入工具栏。
toolbar.insert(item, pos)
toolbar.insert()
的第二个参数接受将项目插入工具栏的位置。工具按钮位置从零开始索引。负位置会将该项追加到工具栏的末尾。
切换工具按钮
Gtk.ToggleToolButton
衍生自Gtk.ToolButton
,因此只实现初始化和切换能力。切换工具按钮以工具栏项目的形式提供了Gtk.ToggleButton
小部件的功能。它允许用户查看选项是已设置还是未设置。
切换工具按钮是当活动属性设置为True
时保持按下状态的工具按钮。当切换按钮的状态改变时,您可以使用切换信号来接收通知。
只有一种方法可以创建新的Gtk.ToggleToolButton
。这是用Gtk.ToggleToolButton.new()
实现的,它创建了一个空的工具按钮。然后你可以使用Gtk.ToolButton
提供的方法来添加标签和图片。
Gtk.ToggleToolButton.new()
单选工具按钮
Gtk.RadioToolButton
是从Gtk.ToggleToolButton
派生出来的,所以继承了“active”属性和“toggled”信号。因此,小部件只需要为您提供一种方法来创建新的单选工具按钮,并将它们添加到单选按钮组中。
应该用Gtk.RadioToolButton.new()
创建一个单选工具按钮,其中单选组设置为None
。这将为单选工具按钮创建一个默认的初始单选按钮组。
Gtk.RadioToolButton.new(group)
Gtk.RadioToolButton
继承了Gtk.ToolButton
的函数,后者提供了一些函数和属性,可以在必要时设置单选按钮的标签。
所有必需的元素都应该用Gtk.RadioToolButton.from_widget()
创建。将“组”设置为第一个单选按钮会将所有必需的项目添加到同一组中。
Gtk.RadioToolButton.new_from_widget(group)
Gtk.RadioToolButton
提供了一个属性 group,它是属于单选按钮组的另一个单选工具按钮。这允许您将所有单选按钮链接在一起,以便一次只能选择一个。
菜单工具按钮
从Gtk.ToggleToolButton
派生而来的Gtk.MenuToolButton
,允许你在工具按钮上附加一个菜单。小部件在图像和标签旁边放置一个箭头,提供对相关菜单的访问。例如,您可以使用Gtk.MenuToolButton
将最近打开的文件列表添加到工具栏按钮中。图 10-6 是用于此目的的菜单工具按钮的截图。
图 10-6
显示最近打开的文件的菜单工具按钮
清单 10-7 向你展示了如何实现一个菜单工具按钮。实际的工具按钮的创建方式与任何其他Gtk.ToolButton
类似,只是多了一个将菜单附加到Gtk.MenuToolButton
小部件的步骤。
recent = Gtk.Menu.new()
# Add a number of menu items where each corresponds to one recent file. icon_theme = Gtk.IconTheme.get_default()
icon = icon_theme.load_icon("document-open", -1,
Gtk.IconLookupFlags.FORCE_SIZE)
image = Gtk.Image.new_from_pixbuf(icon)
open = Gtk.MenuToolButton.new(image, "Open")
open.set_menu(recent)
Listing 10-7Using Gtk.MenuToolButton
在清单 10-7 中,菜单工具按钮是用一个图像和一个标签Gtk.MenuToolButton.new(image, label)
创建的。如果您想在以后使用Gtk.ToolButton
属性设置它们,您可以将这些参数中的任何一个设置为None
。
Gtk.MenuToolButton.new(image, label)
使Gtk.MenuToolButton
与众不同的是,工具按钮右边的箭头为用户提供了访问菜单的途径。工具按钮的菜单是用menutoolbutton.set_menu()
设置的,或者通过将菜单属性设置为一个Gtk.Menu
小部件来设置。单击箭头时,会向用户显示此菜单。
动态菜单创建
注意
在 GTK+ 3.1 中不推荐使用Gtk.UIManager
,所以 UI 文件的创建和加载不包含在本节中。相反,新的Gtk.Builder
类及其相关的 XML 文件被包含在内。Gtk.Builder
是一个更强大、更灵活的系统,用于管理外部用户界面描述和操作。它还提供了附加功能,并减少了创建和管理用户界面所需的工作量。
虽然可以手动创建每个菜单和工具栏项,但这样做会占用大量空间,并导致您不得不单调地编写不必要的代码。为了自动创建菜单和工具栏,GTK+ 允许您从 XML 文件动态创建菜单。
类可以创建许多用户界面对象,包括菜单、菜单栏、弹出菜单、整个对话框、主窗口等等。本节集中讨论不同类型的菜单,但是你应该记住Gtk.Builder
可以构建许多其他类型的用户界面对象。
创建 XML 文件
用户界面文件以 XML 格式构造。所有的内容都必须包含在和标签之间。您可以创建的一种动态 UI 是带有
10-8.
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="menubar">
<submenu>
<attribute name="label">File</attribute>
</submenu>
<submenu>
<attribute name="label">Edit</attribute>
</submenu>
<submenu>
<attribute name="label">Choices</attribute>
</submenu>
<submenu>
<attribute name="label">Help</attribute>
</submenu>
</menu>
</interface>
Listing 10-8Menu UI File
每个菜单和项目标记都应该有一个与之关联的唯一 ID,这样您就可以从代码中直接访问该项。虽然不是必需的,但您应该始终将 name 属性添加到每个菜单和项目中。name 属性可以访问实际的小部件。
每个
每个
标签有多种用途,但是所有标签共有的一个用途是包含标签属性。该属性提供了在上可见的标签字符串。在这种情况下,与一个Gtk.MenuItem
标签属性相对应的标签指定了出现在菜单项中的字符串。
与每个标签一起出现的另一个标签是 action 属性。该标签指定了点击时要采取的动作。指定的动作与Gtk.Application
和Gtk.ApplicationWindow
类(或它们的子类)紧密相关。每个动作的目标指定哪个类实例——Gtk.ApplicationWindow
或Gtk.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.ActionGroup
和Gtk.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)
正如您所看到的,对齐一个小部件非常简单,并且开销减少了,因为我们没有为每个对齐的小部件调用一个新的类。这种对齐小部件的方法应该足以满足大多数应用的需求。
摘要
本章给出了三个小部件定制示例,这些示例应该为您创建自己的定制小部件提供了足够的信息。提高应用的可用性和质量还有很多可能性。